local Event = require 'utils.event'
local CommonFunctions = require 'utils.common'
local Token = require 'utils.token'
local Task = require 'utils.task'
local Global = require 'utils.global'

local this = {
    timers = {}
}

Global.register(
    this,
    function(tbl)
        this = tbl
    end
)

local Public = {}
local remove = table.remove

Public.command = {
    --[[
      @param args nil
   --]]
    noop = 0,
    --[[
      @param args nil
   --]]
    seek_and_destroy_player = 1,
    --[[
      @param args = {
          agents, // All movable agents
          positions, // Table of positions to attack
      }
   --]]
    attack_objects = 2
}

local function _get_direction(src, dest)
    local src_x = CommonFunctions.get_axis(src, 'x')
    local src_y = CommonFunctions.get_axis(src, 'y')
    local dest_x = CommonFunctions.get_axis(dest, 'x')
    local dest_y = CommonFunctions.get_axis(dest, 'y')

    local step = {
        x = nil,
        y = nil
    }

    local precision = CommonFunctions.rand_range(1, 10)
    if dest_x - precision > src_x then
        step.x = 1
    elseif dest_x < src_x - precision then
        step.x = -1
    else
        step.x = 0
    end

    if dest_y - precision > src_y then
        step.y = 1
    elseif dest_y < src_y - precision then
        step.y = -1
    else
        step.y = 0
    end

    return CommonFunctions.direction_lookup[step.x][step.y]
end

local function _move_to(ent, trgt, min_distance)
    local state = {
        walking = false
    }

    local distance = CommonFunctions.get_distance(trgt.position, ent.position)
    if min_distance < distance then
        local dir = _get_direction(ent.position, trgt.position)
        if dir then
            state = {
                walking = true,
                direction = dir
            }
        end
    end

    ent.walking_state = state
    return state.walking
end

local function refill_ammo(ent)
    if not ent or not ent.valid then
        return
    end
    local weapon = ent.get_inventory(defines.inventory.character_guns)[ent.selected_gun_index]
    if weapon and weapon.valid_for_read then
        local selected_ammo = ent.get_inventory(defines.inventory.character_ammo)[ent.selected_gun_index]
        if selected_ammo then
            if not selected_ammo.valid_for_read then
                if weapon.name == 'shotgun' then
                    ent.insert({name = 'shotgun-shell', count = 5})
                end
                if weapon.name == 'pistol' then
                    ent.insert({name = 'firearm-magazine', count = 5})
                end
            end
        end
    end
end

local function _shoot_at(ent, trgt)
    ent.shooting_state = {
        state = defines.shooting.shooting_enemies,
        position = trgt.position
    }
end

local function _shoot_stop(ent)
    ent.shooting_state = {
        state = defines.shooting.not_shooting,
        position = {0, 0}
    }
end

local function set_noise_hostile_hook(ent)
    if not ent or not ent.valid then
        return
    end
    local weapon = ent.get_inventory(defines.inventory.character_guns)[ent.selected_gun_index]
    if weapon and weapon.valid_for_read then
        return
    end

    if CommonFunctions.rand_range(1, 5) == 1 then
        ent.insert({name = 'shotgun', count = 1})
        ent.insert({name = 'shotgun-shell', count = 5})
    end
end

local function _do_job_seek_and_destroy_player(data)
    local active_surface = data.active_surface
    local surf = game.get_surface(active_surface)
    if not surf or not surf.valid then
        return
    end
    local force = data.force
    local players = game.connected_players

    if type(surf) == 'number' then
        surf = game.surfaces[surf]
        if not surf or not surf.valid then
            this.timers[game.tick] = nil
            return
        end
    end

    for _, player in pairs(players) do
        if player and player.valid and player.character then
            local position = data.position or player.character.position
            local search_info = {
                name = 'character',
                position = position,
                radius = 30,
                force = force or 'enemy'
            }

            local ents = surf.find_entities_filtered(search_info)
            if ents and #ents > 0 then
                for _, e in pairs(ents) do
                    if e and e.valid then
                        if e.player == nil then
                            set_noise_hostile_hook(e)
                            refill_ammo(e)
                            if not _move_to(e, player.character, CommonFunctions.rand_range(5, 10)) then
                                _shoot_at(e, player.character)
                            else
                                _shoot_stop(e)
                            end
                        end
                    else
                        if data.repeat_function then
                            this.timers[data.new] = nil
                        end
                    end
                end
            else
                if data.repeat_function then
                    this.timers[data.new] = nil
                end
            end
        end
    end
end

local function _do_job_attack_objects(data)
    local active_surface = data.active_surface
    local surf = game.get_surface(active_surface)
    if not surf or not surf.valid then
        return
    end
    local force = data.force
    local args = data.args
    local position = data.position

    if type(surf) == 'number' then
        surf = game.surfaces[surf]
        if not surf or not surf.valid then
            if data.repeat_function then
                this.timers[data.new] = nil
            end
            return
        end
    end

    local search_info = {
        name = 'character',
        position = position,
        radius = 30,
        force = force or 'enemy'
    }

    local ents = surf.find_entities_filtered(search_info)
    if ents and #ents > 0 then
        local target, closest, agent, query
        for i = #ents, 1, -1 do
            agent = ents[i]
            if not agent.valid then
                remove(ents, i)
                goto continue
            end

            if game.tick % i ~= 0 then
                goto continue
            end

            query = {
                position = agent.position,
                radius = 15,
                type = {
                    'projectile',
                    'beam'
                },
                force = {
                    'enemy',
                    'player',
                    'neutral'
                },
                invert = true
            }
            closest = surf.find_entities_filtered(query)
            if #closest ~= 0 then
                target = CommonFunctions.get_closest_neighbour(agent.position, closest)
            else
                if not args or not args.objects then
                    goto continue
                end

                if #args.objects == 0 then
                    _shoot_stop(agent)
                    goto continue
                end

                target = CommonFunctions.get_closest_neighbour(agent.position, args.objects)
            end

            if target == nil or not target.valid then
                goto continue
            end

            if not _move_to(agent, target, CommonFunctions.rand_range(5, 15)) then
                _shoot_at(agent, target)
            else
                _shoot_stop(agent)
            end

            ::continue::
        end
    else
        if data.repeat_function then
            this.timers[data.new] = nil
        end
    end
end

local do_job_token

do_job_token =
    Token.register(
    function(data)
        local surf = data.surface
        local command = data.command

        if type(surf) == 'number' then
            surf = game.surfaces[surf]
            if not surf or not surf.valid then
                this.timers[game.tick] = nil
                return
            end
        end

        if command == Public.command.seek_and_destroy_player then
            _do_job_seek_and_destroy_player(data)
        elseif command == Public.command.attack_objects then
            _do_job_attack_objects(data)
        end
    end
)

function Public.add_job_to_task(data)
    if not type(data) == 'table' then
        return
    end

    if not data.tick then
        return
    end

    if not this.timers[game.tick + data.tick] then
        this.timers[game.tick + data.tick] = data
    end
end

--[[
do_job - Perform non-stateful operation on all chosen force "character" entities.
@param surf - LuaSurface, on which everything is happening.
@param command - Command to perform on all non-player controllable characters.
--]]
Public.do_job = function(surf, command, args, force)
    if args == nil then
        args = {}
    end

    if not surf and not surf.valid then
        return
    end

    if command == Public.command.seek_and_destroy_player then
        local data = {
            active_surface = surf.index,
            force = force
        }
        _do_job_seek_and_destroy_player(data)
    elseif command == Public.command.attack_objects then
        local data = {
            active_surface = surf.index,
            args = args
        }
        _do_job_attack_objects(data)
    end
end

Event.add(
    defines.events.on_tick,
    function()
        local tick = game.tick

        if not this.timers[tick] then
            return
        end
        local data = this.timers[tick]
        if not data then
            return
        end

        if data.repeat_function then
            this.timers[tick + data.tick] = data
            this.timers[tick + data.tick].previous = tick
            data.new = tick + data.tick
        end

        Task.set_timeout_in_ticks(data.tick, do_job_token, data)

        this.timers[tick] = nil
    end
)

return Public
