--[[-- Control Module - Protection
    - Controls protected entities
    @control Protection
    @alias Protection
]]

local Global = require 'utils.global' --- @dep utils.global
local Event = require 'utils.event' --- @dep utils.event
local config = require 'config.protection' --- @dep config.protection
local EntityProtection = {
    protected_entity_names = table.deep_copy(config.always_protected_names),
    protected_entity_types = table.deep_copy(config.always_protected_types),
    events = {
        --- When a player mines a protected entity
        -- @event on_player_mined_protected
        -- @tparam number player_index the player index of the player who got mined the entity
        -- @tparam LuaEntity entity the entity which was mined
        on_player_mined_protected = script.generate_event_name(),
        --- When a player repeatedly mines protected entities
        -- @event on_repeat_violation
        -- @tparam number player_index the player index of the player who got mined the entities
        -- @tparam LuaEntity entity the last entity which was mined
        on_repeat_violation = script.generate_event_name(),
    }
}

-- Convert config tables into lookup tables
for _, config_key in ipairs{'always_protected_names', 'always_protected_types', 'always_trigger_repeat_names', 'always_trigger_repeat_types'} do
    local tbl = config[config_key]
    for key, value in ipairs(tbl) do
        tbl[key] = nil
        tbl[value] = true
    end
end

-- Require roles if a permission is assigned in the config
local Roles
if config.ignore_permission then
    Roles = require 'expcore.roles' --- @dep expcore.roles
end

----- Global Variables -----
--- Variables stored in the global table

local protected_entities = {} -- All entities which are protected
local protected_areas = {} -- All areas which are protected
local repeats = {} -- Stores repeat removals by players

Global.register({
    protected_entities = protected_entities,
    protected_areas = protected_areas,
    repeats = repeats
}, function(tbl)
    protected_entities = tbl.protected_entities
    protected_areas = tbl.protected_areas
    repeats = tbl.repeats
end)

----- Local Functions -----
--- Functions used internally to search and add to the protected array

--- Get the key used in protected_entities
local function get_entity_key(entity)
    return string.format('%i,%i', math.floor(entity.position.x), math.floor(entity.position.y))
end

--- Get the key used in protected_areas
local function get_area_key(area)
    return string.format('%i,%i', math.floor(area.left_top.x), math.floor(area.left_top.y))
end

--- Check if an entity is always protected
local function check_always_protected(entity)
    return config.always_protected_names[entity.name] or config.always_protected_types[entity.type] or false
end

--- Check if an entity always triggers repeat protection
local function check_always_trigger_repeat(entity)
    return config.always_trigger_repeat_names[entity.name] or config.always_trigger_repeat_types[entity.type] or false
end

----- Public Functions -----
--- Functions used to add and remove protected entities

--- Add an entity to the protected list
function EntityProtection.add_entity(entity)
    local entities = protected_entities[entity.surface.index]
    if not entities then
        entities = {}
        protected_entities[entity.surface.index] = entities
    end
    entities[get_entity_key(entity)] = entity
end

--- Remove an entity from the protected list
function EntityProtection.remove_entity(entity)
    local entities = protected_entities[entity.surface.index]
    if not entities then return end
    entities[get_entity_key(entity)] = nil
end

--- Get all protected entities on a surface
function EntityProtection.get_entities(surface)
    return protected_entities[surface.index] or {}
end

--- Check if an entity is protected
function EntityProtection.is_entity_protected(entity)
    if check_always_protected(entity) then return true end
    local entities = protected_entities[entity.surface.index]
    if not entities then return false end
    return entities[get_entity_key(entity)] == entity
end

--- Add an area to the protected list
function EntityProtection.add_area(surface, area)
    local areas = protected_areas[surface.index]
    if not areas then
        areas = {}
        protected_areas[surface.index] = areas
    end
    areas[get_area_key(area)] = area
end

--- Remove an area from the protected list
function EntityProtection.remove_area(surface, area)
    local areas = protected_areas[surface.index]
    if not areas then return end
    areas[get_area_key(area)] = nil
end

--- Get all protected areas on a surface
function EntityProtection.get_areas(surface)
    return protected_areas[surface.index] or {}
end

--- Check if an entity is protected
function EntityProtection.is_position_protected(surface, position)
    local areas = protected_areas[surface.index]
    if not areas then return false end
    for _, area in pairs(areas) do
        if area.left_top.x <= position.x and area.left_top.y <= position.y
        and area.right_bottom.x >= position.x and area.right_bottom.y >= position.y
        then
            return true
        end
    end
    return false
end

----- Events -----
--- All events registered by this module

--- Raise events for protected entities
Event.add(defines.events.on_pre_player_mined_item, function(event)
    local entity = event.entity
    local player = game.get_player(event.player_index)
    -- Check if the player should be ignored
    if config.ignore_admins and player.admin then return end
    if entity.last_user == nil or entity.last_user.index == player.index then return end
    if config.ignore_permission and Roles.player_allowed(player, config.ignore_permission) then return end

    -- Check if the entity is protected
    if EntityProtection.is_entity_protected(entity)
    or EntityProtection.is_position_protected(entity.surface, entity.position)
    then
        -- Update repeats
        local player_repeats = repeats[player.name]
        if not player_repeats then
            player_repeats = { last = game.tick, count = 0 }
            repeats[player.name] = player_repeats
        end
        player_repeats.last = game.tick
        player_repeats.count = player_repeats.count + 1
        -- Send events
        event.name = EntityProtection.events.on_player_mined_protected
        script.raise_event(EntityProtection.events.on_player_mined_protected, event)
        if check_always_trigger_repeat(entity) or player_repeats.count >= config.repeat_count then
            player_repeats.count = 0 -- Reset to avoid spamming of events
            event.name = EntityProtection.events.on_repeat_violation
            script.raise_event(EntityProtection.events.on_repeat_violation, event)
        end
    end
end)

--- Remove old repeats
Event.on_nth_tick(config.refresh_rate, function()
    local old = game.tick - config.repeat_lifetime
    for player_name, player_repeats in pairs(repeats) do
        if player_repeats.last <= old then
            repeats[player_name] = nil
        end
    end
end)

--- When an entity is removed remove it from the protection list
local function event_remove_entity(event)
    EntityProtection.remove_entity(event.entity)
end

Event.add(defines.events.on_pre_player_mined_item, event_remove_entity)
Event.add(defines.events.on_robot_pre_mined, event_remove_entity)
Event.add(defines.events.on_entity_died, event_remove_entity)
Event.add(defines.events.script_raised_destroy, event_remove_entity)

return EntityProtection