--[[
  Refactored version from Diggy/diggy_cave_collapse.lua
  Provides the ability to collapse caves when digging.
]]
local Event = require 'utils.event'
local Template = require 'map_gen.maps.april_fools.scenario.template'
local ScoreTracker = require 'utils.score_tracker'
local Task = require 'utils.task'
local Token = require 'utils.token'
local Global = require 'utils.global'
local CreateParticles = require 'features.create_particles'
local Popup = require 'features.gui.popup'
local table = require 'utils.table'
local random = math.random
local floor = math.floor
local pairs = pairs
local pcall = pcall
local template_insert = Template.insert
local is_diggy_rock = Template.is_diggy_rock
local collapse_rocks = Template.diggy_rocks
local collapse_rocks_size = #collapse_rocks
local raise_event = script.raise_event
local set_timeout = Task.set_timeout
local set_timeout_in_ticks = Task.set_timeout_in_ticks
local ceiling_crumble = CreateParticles.ceiling_crumble
local clear_table = table.clear_table
local cave_collapses_name = 'cave-collapses'

-- this
local CaveCollapse = {}

local config = {
  enabled = true,
  -- shows players an initial warning about mining stones
  onboarding = false,
  -- adds per tile what the current stress is
  enable_stress_grid = false,
  -- shows the mask on spawn
  enable_mask_debug = false,
  --the size of the mask used
  mask_size = 16, --9
  --how much the mask will effect tiles in the different rings of the mask
  mask_relative_ring_weights = {2, 3, 4},
  -- delay in seconds before the cave collapses
  collapse_delay = 2.5,
  -- the threshold that will be applied to all neighbors on a collapse via a mask
  collapse_threshold_total_strength = 40, --16
  support_beam_entities = {
    ['tunnel'] = 40,
    ['dungeon-support'] = 40,
    ['market'] = 9,
    ['nuclear-reactor'] = 4,
    ['stone-wall'] = 3,

    ['out-of-map'] = 1,
    ['stone-path'] = 0.03,
    ['concrete'] = 0.04,
    ['hazard-concrete'] = 0.04,
    ['refined-concrete'] = 0.06,
    ['refined-hazard-concrete'] = 0.06

    -- rocks added below
  },
  cracking_sounds = {
    {'diggy.cracking_sound_1'},
    {'diggy.cracking_sound_2'}
  }
}

for _, rock_name in pairs(collapse_rocks) do
  if rock_name:find('big', 1, true) ~= nil then
    config.support_beam_entities[rock_name] = 2
  elseif rock_name:find('huge', 1, true) ~= nil then
    config.support_beam_entities[rock_name] = 2.5
  end
end

local n = 9
local radius = 0
local radius_sq = 0
local center_radius_sq = 0
local disc_radius_sq = 0

local center_weight
local disc_weight
local ring_weight

local disc_blur_sum = 0

local center_value = 0
local disc_value = 0
local ring_value = 0

local stress_map_add
local mask_disc_blur
local mask_init
local stress_map_check_stress_in_threshold
local support_beam_entities

local stress_threshold_causing_collapse = 3.57
local near_stress_threshold_causing_collapse = 3.3 -- just above the threshold of a normal 4 pillar grid

local show_deconstruction_alert_message = {}
local stress_map_storage = {}
stress_map_storage[3] = {
  surface_index = 3,
  [1] = { index = 1 },
  [2] = { index = 2 },
  [3] = { index = 3 },
  [4] = { index = 4 },
}
local new_tile_map = {}
local collapse_positions_storage = {}


Global.register({
  new_tile_map = new_tile_map,
  stress_map_storage = stress_map_storage,
  deconstruction_alert_message_shown = show_deconstruction_alert_message,
  collapse_positions_storage = collapse_positions_storage,
}, function(tbl)
  new_tile_map = tbl.new_tile_map
  stress_map_storage = tbl.stress_map_storage
  show_deconstruction_alert_message = tbl.deconstruction_alert_message_shown
  collapse_positions_storage = tbl.collapse_positions_storage
end)

local defaultValue = 0
local collapse_alert = { type = 'item', name = 'stone' }

CaveCollapse.events = {
  --[[
    When stress at certain position is above the collapse threshold
      - position LuaPosition
      - surface LuaSurface
      - player_index Number (index of player that caused the collapse)
    ]]
  on_collapse_triggered = Event.generate_event_name('on_collapse_triggered'),
  --[[
    After a collapse
      - position LuaPosition
      - surface LuaSurface
      - player_index Number (index of player that caused the collapse)
    ]]
  on_collapse = Event.generate_event_name('on_collapse'),
}

local function create_collapse_template(positions, surface)
  local entities = {}
  local entity_count = 0
  local find_entities_filtered = surface.find_entities_filtered

  for _, position in pairs(positions) do
    local x = position.x
    local y = position.y
    local do_insert = true

    for _, entity in pairs(find_entities_filtered({ area = { position, { x + 1, y + 1 } } })) do
      pcall(function()
        local strength = support_beam_entities[entity.name]
        if strength then
          do_insert = false
        else
          if entity.name ~= 'tank' then
            entity.die()
          else
            entity.health = entity.health - 100
            if entity.health == 0 then
              entity.die()
            end
          end
        end
      end)
    end

    if do_insert then
      entity_count = entity_count + 1
      entities[entity_count] = { position = position, name = collapse_rocks[random(collapse_rocks_size)] }
    end
  end

  return entities
end

local function create_collapse_alert(surface, position)
  local target = surface.create_entity({ position = position, name = 'rock-big' })
  for _, player in pairs(game.connected_players) do
    player.add_custom_alert(target, collapse_alert, { 'diggy.cave_collapse' }, true)
  end
  target.destroy()
end

local function collapse(args)
  local position = args.position
  local surface = args.surface
  local positions = {}
  local count = 0
  local strength = config.collapse_threshold_total_strength
  mask_disc_blur(position.x, position.y, strength, function(x, y, value)
    stress_map_check_stress_in_threshold(surface, x, y, value, function(_, c_x, c_y)
      count = count + 1
      positions[count] = { x = c_x, y = c_y }
    end)
  end)

  if #positions == 0 then
    return
  end

  create_collapse_alert(surface, position)

  template_insert(surface, {}, create_collapse_template(positions, surface))

  raise_event(CaveCollapse.events.on_collapse, args)
  ScoreTracker.change_for_global(cave_collapses_name, 1)
end

local on_collapse_timeout_finished = Token.register(collapse)
local on_near_threshold = Token.register(function(params)
  ceiling_crumble(params.surface, params.position)
end)

local function spawn_collapse_text(surface, position)
  local color = { r = 1, g = random(1, 100) * 0.01, b = 0 }

  surface.create_entity{
    name = 'tutorial-flying-text',
    color = color,
    text = config.cracking_sounds[random(#config.cracking_sounds)],
    position = position
  }
end

local function on_collapse_triggered(event)
  local surface = event.surface
  local position = event.position
  local x = position.x
  local y = position.y

  local x_t = new_tile_map[x]
  if x_t and x_t[y] then
    template_insert(surface, {}, { { position = position, name = collapse_rocks[math.random(collapse_rocks_size)] } })
    return
  end
  spawn_collapse_text(surface, position)
  set_timeout(config.collapse_delay, on_collapse_timeout_finished, event)
end

local function on_built_tile(surface, new_tile, tiles)
  local new_tile_strength = support_beam_entities[new_tile.name]

  for _, tile in pairs(tiles) do
    if new_tile_strength then
      stress_map_add(surface, tile.position, -1 * new_tile_strength, true)
    end

    local old_tile_strength = support_beam_entities[tile.old_tile.name]
    if (old_tile_strength) then
      stress_map_add(surface, tile.position, old_tile_strength, true)
    end
  end
end

-- It is impossible to track which player marked the tile for deconstruction
local function on_robot_mined_tile(event)
  local surface
  for _, tile in pairs(event.tiles) do
    local strength = support_beam_entities[tile.old_tile.name]
    if strength then
      surface = surface or event.robot.surface
      stress_map_add(surface, tile.position, strength, true)
    end
  end
end

local function on_player_mined_tile(event)
  local surface = game.surfaces[event.surface_index]
  for _, tile in pairs(event.tiles) do
    local strength = support_beam_entities[tile.old_tile.name]

    if strength then
      stress_map_add(surface, tile.position, strength, true, event.player_index)
    end
  end
end

local function on_mined_entity(event)
  local entity = event.entity
  local name = entity.name
  local strength = support_beam_entities[name]
  if strength then
    local player_index
    if not is_diggy_rock(name) then
      player_index = event.player_index
    end
    stress_map_add(entity.surface, entity.position, strength, false, player_index)
  end
end

local function on_entity_died(event)
  local entity = event.entity
  local name = entity.name
  local strength = support_beam_entities[name]
  if strength then
    local player_index
    if not is_diggy_rock(name) then
      local cause = event.cause
      player_index = cause and cause.type == 'character' and cause.player and cause.player.index or nil
    end
    stress_map_add(entity.surface, entity.position, strength, false, player_index)
  end
end

local function script_raised_destroy(event)
  local cause = event.cause
  if cause and not (cause == 'room_clearing' or cause == 'die_faster' or cause == 'alien_emerges') then
    return
  end
  local entity = event.entity
  local name = entity.name
  local strength = support_beam_entities[name]
  if strength then
    stress_map_add(entity.surface, entity.position, strength, false)
  end
end

local function on_built_entity(event)
  local entity = event.created_entity
  local strength = support_beam_entities[entity.name]

  if strength then
    stress_map_add(entity.surface, entity.position, -1 * strength)
  end
end

local function on_placed_entity(event)
  local strength = support_beam_entities[event.entity.name]

  if strength then
    stress_map_add(event.entity.surface, event.entity.position, -1 * strength)
  end
end

local on_new_tile_timeout_finished = Token.register(function(args)
  local x_t = new_tile_map[args.x]
  if x_t then
    x_t[args.y] = nil -- reset new tile status. This tile can cause a chain collapse now
  end
end)

local function on_void_removed(event)
  local strength = support_beam_entities['out-of-map']

  local position = event.position
  if strength then
    stress_map_add(event.surface, position, strength)
  end

  local x = position.x
  local y = position.y

  -- To avoid room collapse:
  local x_t = new_tile_map[x]
  if x_t then
    x_t[y] = true
  else
    x_t = { [y] = true }
    new_tile_map[x] = x_t
  end
  set_timeout(3, on_new_tile_timeout_finished, { x = x, y = y })
end

local function on_surface_created(event)
  local index = event.surface_index
  local surface = game.surfaces[index]
  if not (surface and surface.valid and surface.name == 'mines') then
    return
  end

  if stress_map_storage[index] then
    clear_table(stress_map_storage[index])
  else
    stress_map_storage[index] = {}
  end

  local map = stress_map_storage[index]

  map['surface_index'] = index
  map[1] = { index = 1 }
  map[2] = { index = 2 }
  map[3] = { index = 3 }
  map[4] = { index = 4 }
end

-- ============================================================================

--
-- STRESS MAP
--
--[[--
    Adds a fraction to a given location on the stress_map. Returns the new
    fraction value of that position.

    @param stress_map Table of {x,y}
    @param position Table with x and y
    @param number fraction

    @return number sum of old fraction + new fraction
]]
---Adds a fraction to a given location on the stress_map. Returns the new fraction value of that position.
---@param stress_map table
---@param x number
---@param y number
---@param fraction number
---@param player_index number
---@param surface LuaSurface
local function add_fraction(stress_map, x, y, fraction, player_index, surface)
  if not (surface and surface.valid and surface.name == 'mines') then
    return
  end

  x = 2 * floor(x * 0.5)
  y = 2 * floor(y * 0.5)

  local x_t = stress_map[x]
  if not x_t then
    x_t = {}
    stress_map[x] = x_t
  end

  local value = x_t[y]
  if not value then
    value = defaultValue
  end

  value = value + fraction

  x_t[y] = value

  if fraction > 0 then
    if value > stress_threshold_causing_collapse then
      raise_event(CaveCollapse.events.on_collapse_triggered, { surface = surface, position = { x = x, y = y }, player_index = player_index })
    elseif value > near_stress_threshold_causing_collapse then
      set_timeout_in_ticks(2, on_near_threshold, { surface = surface, position = { x = x, y = y } })
    end
  end
  return value
end



---Checks whether a tile's pressure is within a given threshold and calls the handler if not.
---@param surface LuaSurface
---@param x number
---@param y number
---@param threshold number
---@param callback function
stress_map_check_stress_in_threshold = function(surface, x, y, threshold, callback)
  if not (surface and surface.valid and surface.name == 'mines') then
    return
  end

  local stress_map = stress_map_storage[surface.index]
  local value = add_fraction(stress_map, x, y, 0, nil, surface)

  if (value >= stress_threshold_causing_collapse - threshold) then
    callback(surface, x, y)
  end
end

stress_map_add = function(surface, position, factor, no_blur, player_index)
  if not (surface and surface.valid and surface.name == 'mines') then
    return
  end

  local x_start = floor(position.x)
  local y_start = floor(position.y)

  local stress_map = stress_map_storage[surface.index]
  if not stress_map then
    game.print("error - no_stress_map")
    return
  end

  if no_blur then
    add_fraction(stress_map, x_start, y_start, factor, player_index, surface)
    return
  end

  for x = -radius, radius do
    for y = -radius, radius do
      local value = 0
      local distance_sq = x * x + y * y
      if distance_sq <= center_radius_sq then
        value = center_value
      elseif distance_sq <= disc_radius_sq then
        value = disc_value
      elseif distance_sq <= radius_sq then
        value = ring_value
      end
      if value > 0.001 or value < -0.001 then
        add_fraction(stress_map, x + x_start, y + y_start, value * factor, player_index, surface)
      end
    end
  end
end

CaveCollapse.stress_map_add = stress_map_add

--
-- MASK
--

mask_init = function(config) -- luacheck: ignore 431 (intentional upvalue shadow)
  n = config.mask_size
  local ring_weights = config.mask_relative_ring_weights

  ring_weight = ring_weights[1]
  disc_weight = ring_weights[2]
  center_weight = ring_weights[3]

  radius = floor(n * 0.5)

  radius_sq = (radius + 0.2) * (radius + 0.2)
  center_radius_sq = radius_sq / 9
  disc_radius_sq = radius_sq * 4 / 9

  for x = -radius, radius do
    for y = -radius, radius do
      local distance_sq = x * x + y * y
      if distance_sq <= center_radius_sq then
        disc_blur_sum = disc_blur_sum + center_weight
      elseif distance_sq <= disc_radius_sq then
        disc_blur_sum = disc_blur_sum + disc_weight
      elseif distance_sq <= radius_sq then
        disc_blur_sum = disc_blur_sum + ring_weight
      end
    end
  end
  center_value = center_weight / disc_blur_sum
  disc_value = disc_weight / disc_blur_sum
  ring_value = ring_weight / disc_blur_sum
end

--[[--
    Applies a blur
    Applies the disc in 3 discs: center, (middle) disc and (outer) ring.
    The relative weights for tiles in a disc are:
    center: 3/3
    disc: 2/3
    ring: 1/3
    The sum of all values is 1

    @param x_start number center point
    @param y_start number center point
    @param factor the factor to multiply the cell value with (value = cell_value * factor)
    @param callback function to execute on each tile within the mask callback(x, y, value)
]]
mask_disc_blur = function(x_start, y_start, factor, callback)
  x_start = floor(x_start)
  y_start = floor(y_start)
  for x = -radius, radius do
    for y = -radius, radius do
      local value = 0
      local distance_sq = x * x + y * y
      if distance_sq <= center_radius_sq then
        value = center_value
      elseif distance_sq <= disc_radius_sq then
        value = disc_value
      elseif distance_sq <= radius_sq then
        value = ring_value
      end
      if value > 0.001 or value < -0.001 then
        callback(x_start + x, y_start + y, value * factor)
      end
    end
  end
end

-- ============================================================================

--[[
  Registers all event handlers
  @param global_config Table {@see Diggy.Config}.
]]
mask_init(config)

ScoreTracker.register(cave_collapses_name, { 'diggy.score_cave_collapses' }, '[img=entity.small-remnants]')

local global_to_show = global.config.score.global_to_show
global_to_show[#global_to_show + 1] = cave_collapses_name

support_beam_entities = config.support_beam_entities
support_beam_entities['stone-brick'] = support_beam_entities['stone-path']
support_beam_entities['hazard-concrete-left'] = support_beam_entities['hazard-concrete']
support_beam_entities['hazard-concrete-right'] = support_beam_entities['hazard-concrete']
support_beam_entities['refined-hazard-concrete-left'] = support_beam_entities['refined-hazard-concrete']
support_beam_entities['refined-hazard-concrete-right'] = support_beam_entities['refined-hazard-concrete']

-- General events
Event.add(CaveCollapse.events.on_collapse_triggered, on_collapse_triggered)
Event.add(Template.events.on_void_removed, on_void_removed)
-- Not raised because 'mines' is generated on_init
Event.add(defines.events.on_surface_created, on_surface_created)

if config.onboarding then
  Event.add(defines.events.on_player_created, function(event)
    show_deconstruction_alert_message[event.player_index] = true
  end)

  Event.add(defines.events.on_pre_player_mined_item, function(event)
    local player_index = event.player_index
    if not show_deconstruction_alert_message[player_index] then
      return
    end

    if (nil ~= support_beam_entities[event.entity.name]) then
      Popup.player(game.get_player(player_index), { 'diggy.cave_collapse_warning' })
      show_deconstruction_alert_message[player_index] = nil
    end
  end)
end

-- ADD
-- -- tile
Event.add(defines.events.on_robot_built_tile, function(event)
  on_built_tile(event.robot.surface, event.item, event.tiles)
end)
Event.add(defines.events.on_player_built_tile, function(event)
  on_built_tile(game.surfaces[event.surface_index], event.tile, event.tiles)
end)
-- -- entity
Event.add(Template.events.on_placed_entity, on_placed_entity)
Event.add(defines.events.on_robot_built_entity, on_built_entity)
Event.add(defines.events.on_built_entity, on_built_entity)

-- REMOVE
-- -- tile
Event.add(defines.events.on_robot_mined_tile, on_robot_mined_tile)
Event.add(defines.events.on_player_mined_tile, on_player_mined_tile)
-- -- entity
Event.add(defines.events.on_entity_died, on_entity_died)
Event.add(defines.events.script_raised_destroy, script_raised_destroy)
Event.add(defines.events.on_player_mined_entity, on_mined_entity)
Event.add(defines.events.on_marked_for_deconstruction, function(event)
  local entity = event.entity
  local name = entity.name
  if is_diggy_rock(name) then
    return
  end

  if name == 'deconstructible-tile-proxy' or nil ~= support_beam_entities[name] then
    entity.cancel_deconstruction(game.get_player(event.player_index).force)
  end
end)