local ShieldProjector = {}

ShieldProjector.scale_up = 4/3

ShieldProjector.name_entity = "shield-projector"
ShieldProjector.name_floor_prefix = "shield-projector-shield-floor-"
ShieldProjector.name_wall_prefix = "shield-projector-shield-wall-"
ShieldProjector.name_barrier = "shield-projector-barrier"
ShieldProjector.directions = {"north", "south", "east", "west", "northeast", "northwest", "southeast", "southwest"}

ShieldProjector.cardinal_distance = 12--9
ShieldProjector.diagonal_distance = 7.75--6
ShieldProjector.cardinal_circle_origin = -5---4
ShieldProjector.diagonal_circle_origin = -4---3
ShieldProjector.barrier_from_circle = 18--13.5

ShieldProjector.tick_interval = 60

ShieldProjector.threshold_charging = 0.1 -- below this no charging animation is shown
ShieldProjector.threshold_project = 0.9 -- above this the shield is projected if not alreay active
ShieldProjector.threshold_collapse = 0.1 -- below this an active shield will collapse.

ShieldProjector.energy_per_hit_point = 40000
ShieldProjector.free_regen = 1
ShieldProjector.regen_lag = 10
ShieldProjector.max_health_for_instant_regen = 500

ShieldProjector.event_filters = {{filter="name", name=ShieldProjector.name_entity}}

function ShieldProjector.rebuild_cache()
  storage.shield_projector_buffer_capacity = prototypes.entity[ShieldProjector.name_entity].electric_energy_source_prototype.buffer_capacity
  storage.shield_projector_barrier_max_health = prototypes.entity[ShieldProjector.name_barrier].get_max_health()
end

function ShieldProjector.register_dolly_moved_event()
  if remote.interfaces["PickerDollies"] and remote.interfaces["PickerDollies"]["dolly_moved_entity_id"] then
    script.on_event(remote.call("PickerDollies", "dolly_moved_entity_id"), ShieldProjector.on_dolly_moved)
  end
end

---@param event ConfigurationChangedData
function ShieldProjector.on_configuration_changed(event)
  Migrate.do_migrations(event)

  ShieldProjector.rebuild_cache()
end
script.on_configuration_changed(ShieldProjector.on_configuration_changed)

script.on_init(function()
  ShieldProjector.rebuild_cache()
  ShieldProjector.register_dolly_moved_event()
end)
script.on_load(ShieldProjector.register_dolly_moved_event)

function ShieldProjector.get_sub_entity_names()
  local names = {ShieldProjector.name_barrier}
  for _, direction in pairs(ShieldProjector.directions) do
    table.insert(names, ShieldProjector.name_floor_prefix .. direction)
    table.insert(names, ShieldProjector.name_wall_prefix .. direction)
  end
  return names
end

---Handles creation of a shield projector.
---@param event EventData.on_built_entity|EventData.on_robot_built_entity|EventData.script_raised_built|EventData.script_raised_revive|EventData.on_space_platform_built_entity
function ShieldProjector.on_entity_created(event)
  local entity = event.entity

  -- Double check entity name, sim helper ignores event filters
  if entity.name ~= ShieldProjector.name_entity then
    return
  end

  storage.shield_projectors = storage.shield_projectors or {}
  for _, shield_projector in pairs(storage.shield_projectors) do
    if shield_projector.entity == entity then
      return -- already is set up
    end
  end

  local shield_projector = {
    unit_number = entity.unit_number,
    entity = entity,
    surface = entity.surface,
    force = entity.force,
    barriers = {},
    floor_effect = nil,
    wall_effect = nil,
    is_projecting = false
  }
  storage.shield_projectors[shield_projector.unit_number] = shield_projector

  local max_energy = storage.shield_projector_buffer_capacity
  entity.energy = math.max(entity.energy, max_energy * ShieldProjector.threshold_charging) -- start at the point where charging is shown, good to show activity
  entity.rotatable = false
  entity.operable = false
end
script.on_event(defines.events.on_built_entity, ShieldProjector.on_entity_created, ShieldProjector.event_filters)
script.on_event(defines.events.on_robot_built_entity, ShieldProjector.on_entity_created, ShieldProjector.event_filters)
script.on_event(defines.events.script_raised_built, ShieldProjector.on_entity_created, ShieldProjector.event_filters)
script.on_event(defines.events.script_raised_revive, ShieldProjector.on_entity_created, ShieldProjector.event_filters)
script.on_event(defines.events.on_space_platform_built_entity, ShieldProjector.on_entity_created, ShieldProjector.event_filters)

---Handles removal of a shield projector.
---@param event EventData.on_entity_died|EventData.on_robot_mined_entity|EventData.on_player_mined_entity|EventData.script_raised_destroy|EventData.on_space_platform_mined_entity
function ShieldProjector.on_removed_entity(event)
  local unit_number = event.entity.unit_number

  if storage.shield_projectors and storage.shield_projectors[unit_number] then
    ShieldProjector.remove(storage.shield_projectors[unit_number])
  end
end
script.on_event(defines.events.on_entity_died, ShieldProjector.on_removed_entity, ShieldProjector.event_filters)
script.on_event(defines.events.on_robot_mined_entity, ShieldProjector.on_removed_entity, ShieldProjector.event_filters)
script.on_event(defines.events.on_player_mined_entity, ShieldProjector.on_removed_entity, ShieldProjector.event_filters)
script.on_event(defines.events.script_raised_destroy, ShieldProjector.on_removed_entity, ShieldProjector.event_filters)
script.on_event(defines.events.on_space_platform_mined_entity, ShieldProjector.on_removed_entity, ShieldProjector.event_filters)

function ShieldProjector.remove(shield_projector)
  ShieldProjector.remove_projection(shield_projector)
  storage.shield_projectors[shield_projector.unit_number] = nil
end

function ShieldProjector.remove_projection(shield_projector)
  if shield_projector.floor_effect and shield_projector.floor_effect.valid then shield_projector.floor_effect.destroy() end
  shield_projector.floor_effect = nil
  if shield_projector.wall_effect and shield_projector.wall_effect.valid then shield_projector.wall_effect.destroy() end
  shield_projector.wall_effect = nil

  for _, barrier in pairs(shield_projector.barriers) do
    if barrier.valid then barrier.destroy() end
  end
  shield_projector.barriers = {}
end

function ShieldProjector.update(shield_projector)
  if not shield_projector.entity.valid then
    return ShieldProjector.remove(shield_projector)
  end
  local energy = shield_projector.entity.energy
  local energy_p = energy / storage.shield_projector_buffer_capacity

  if shield_projector.projection_disabled then return end

  if shield_projector.is_projecting then
    shield_projector.every_tick = false
    for _, barrier in pairs(shield_projector.barriers) do
      if barrier.valid then
        ShieldProjector.repair_barrier(shield_projector, barrier, true)
      else
        shield_projector.entity.energy = 0
        energy = 0
        energy_p = 0
        break
      end
    end

    if energy_p <= ShieldProjector.threshold_collapse then
      ShieldProjector.remove_projection(shield_projector)
      shield_projector.is_projecting = false
      shield_projector.entity.energy = 0
    end
  else -- not projecting
    shield_projector.every_tick = false
    if energy_p > ShieldProjector.threshold_project then
      shield_projector.is_projecting = true
      local orientation = shield_projector.entity.orientation
      local orientation_based_direction = Util.orientation_to_direction(orientation)
      local direction = shield_projector.entity.direction
      
      local vector = Util.direction_to_vector(orientation_based_direction)
      local cardinal = (orientation * 4) % 1 == 0
      local position = shield_projector.entity.position

      -- graphics
      local graphic_position = table.deepcopy(position)
      local graphic_offset = cardinal and ShieldProjector.cardinal_distance or ShieldProjector.diagonal_distance
      graphic_position.x = graphic_position.x + vector.x * graphic_offset
      graphic_position.y = graphic_position.y + vector.y * graphic_offset

      shield_projector.floor_effect = shield_projector.entity.surface.create_entity{name = ShieldProjector.name_floor_prefix..Util.direction_to_string(orientation_based_direction),
        position = graphic_position, direction = orientation_based_direction, force = shield_projector.entity.force}
      shield_projector.floor_effect.destructible = false

      shield_projector.wall_effect = shield_projector.entity.surface.create_entity{name = ShieldProjector.name_wall_prefix..Util.direction_to_string(orientation_based_direction),
          position = graphic_position, direction = orientation_based_direction, force = shield_projector.entity.force}
      shield_projector.wall_effect.destructible = false

      -- barriers
      local circle_center = table.deepcopy(position)
      local circle_offset = cardinal and ShieldProjector.cardinal_circle_origin or ShieldProjector.diagonal_circle_origin
      circle_center.x = circle_center.x + vector.x * circle_offset
      circle_center.y = circle_center.y + vector.y * circle_offset
      for _, orientation_offset in pairs{-1/24, 0, 1/24} do
        local barrier_orientation = orientation + orientation_offset
        local b_position = table.deepcopy(circle_center)
        b_position.x = b_position.x + math.sin(math.pi * 2 * barrier_orientation) * ShieldProjector.barrier_from_circle
        b_position.y = b_position.y + -math.cos(math.pi * 2 * barrier_orientation) * ShieldProjector.barrier_from_circle
        local barrier = shield_projector.entity.surface.create_entity{name = ShieldProjector.name_barrier, position = b_position, direction = orientation_based_direction, force = shield_projector.entity.force}
        barrier.orientation = barrier_orientation
        barrier.minable = false
        barrier.operable = false
        table.insert(shield_projector.barriers, barrier)
        storage.shield_projector_barriers = storage.shield_projector_barriers or {}
        storage.shield_projector_barriers[barrier.unit_number] = shield_projector.unit_number
      end
    end
  end
end

function ShieldProjector.on_tick(event)
  if storage.shield_projectors then
    if event.tick % 600 == 143 then
      -- reset the barrier index periodically so we get rid of destroyed barriers
      storage.shield_projector_barriers = nil
    end
    for _, shield_projector in pairs(storage.shield_projectors) do
      if shield_projector.every_tick or (shield_projector.unit_number + event.tick) % ShieldProjector.tick_interval == 0 then
        ShieldProjector.update(shield_projector)
      end
    end
  else
    storage.shield_projectors = {} -- init
    for _, surface in pairs(game.surfaces) do
      for _, entity in pairs(surface.find_entities_filtered{name=ShieldProjector.get_sub_entity_names()}) do
        entity.destroy()
      end
      ShieldProjector.find_on_surface(surface)
    end
  end
end
script.on_event(defines.events.on_tick, ShieldProjector.on_tick)

function ShieldProjector.repair_barrier(shield_projector, barrier, free_regen)
  local health = barrier.health
  local max_health = storage.shield_projector_barrier_max_health
  if health < max_health then
    local damage = max_health - health
    local damage_percent = damage / max_health
    local energy_required = damage * ShieldProjector.energy_per_hit_point
    local energy = shield_projector.entity.energy
    local max_energy = storage.shield_projector_buffer_capacity

    local energy_available = energy - max_energy * ShieldProjector.threshold_collapse
    local energy_for_repair = math.min(
      energy_available/3,-- /3 minimum so energy is shared over segments
      energy_required * damage_percent * damage_percent / ShieldProjector.regen_lag
    )
    health = health + energy_for_repair / ShieldProjector.energy_per_hit_point + (free_regen and ShieldProjector.free_regen or 0)
    barrier.health = health
    shield_projector.entity.energy = energy - energy_for_repair
    if health < max_health * 0.01 then
      barrier.destroy()
    elseif health < max_health then
      shield_projector.every_tick = true
    end
  end
end

---Handles damage to the shield projector.
---@param event on_entity_damaged
function ShieldProjector.on_entity_damaged(event)
  if not (event.entity and event.entity.valid) then return end
  local unit_number = event.entity.unit_number

  -- Ensure barrier is part of barrier index
  if not (storage.shield_projector_barriers and storage.shield_projector_barriers[unit_number]) then
    ShieldProjector.rebuild_barrier_index()
  end

  -- Get the parent shield projector
  local shield_projector = storage.shield_projectors
    and storage.shield_projectors[storage.shield_projector_barriers[unit_number]]

  if shield_projector then
    shield_projector.every_tick = true
    if event.entity.health < ShieldProjector.max_health_for_instant_regen then
      ShieldProjector.repair_barrier(shield_projector, event.entity)
    end
  end
end
script.on_event(defines.events.on_entity_damaged, ShieldProjector.on_entity_damaged,
  {{filter="name", name=ShieldProjector.name_barrier}})

function ShieldProjector.rebuild_barrier_index()
  storage.shield_projector_barriers = {}
  for _, shield_projector in pairs(storage.shield_projectors) do
    for _, barrier in pairs(shield_projector.barriers) do
      if barrier and barrier.valid then
        storage.shield_projector_barriers[barrier.unit_number] = shield_projector.unit_number
      end
    end
  end
end

--- Ensures that shield projectors cloned to a surface are created properly
---@param surface any
---@param area any
function ShieldProjector.find_on_surface(surface, area)
  local entities = surface.find_entities_filtered{
    name = ShieldProjector.name_entity,
    area = area
  }
  for _, entity in pairs(entities) do
    ShieldProjector.on_entity_created({entity = entity})
  end
end

function ShieldProjector.on_mode_toggle(event)
  local player = game.players[event.player_index]
  local entity = player.selected
  if entity and entity.valid and entity.name == ShieldProjector.name_entity and entity.force == player.force then
    -- allow R to "cycle" the shield and allow bots to place something under the shield.
    local shield_projector = storage.shield_projectors[entity.unit_number]
    if shield_projector then
      ShieldProjector.remove_projection(shield_projector)
      shield_projector.is_projecting = false
      if shield_projector.projection_disabled then
        shield_projector.projection_disabled = nil
      else
        shield_projector.projection_disabled = true
      end
    end
  end
end
script.on_event('shield-projector-mode-toggle', ShieldProjector.on_mode_toggle)

-- When moved by Picker Dollies
function ShieldProjector.on_dolly_moved(event)
  if event.moved_entity and event.moved_entity.unit_number and storage.shield_projectors[event.moved_entity.unit_number] then
    local shield_projector = storage.shield_projectors[event.moved_entity.unit_number]
    ShieldProjector.remove_projection(shield_projector)
    shield_projector.is_projecting = false
  end
end

return ShieldProjector
