local Event = require 'utils.event'
local Task = require 'utils.task'
local Token = require 'utils.token'
local Global = require 'utils.global'
local math = require 'utils.math'
local table = require 'utils.table'
local Color = require 'resources.color_presets'

local random = math.random
local set_timeout_in_ticks = Task.set_timeout_in_ticks
local ceil = math.ceil
local draw_arc = rendering.draw_arc
local fast_remove = table.fast_remove
local round = math.round

local tau = 2 * math.pi
local start_angle = -tau / 4
local update_rate = 4 -- ticks between updates
local time_to_live = update_rate + 1
local pole_respawn_time = 60 * 60

local no_coin_entity = {}

Global.register(
    {no_coin_entity = no_coin_entity},
    function(tbl)
        no_coin_entity = tbl.no_coin_entity
    end
)

local entity_drop_amount = {
    --[[['small-biter'] = {low = -62, high = 1},
    ['small-spitter'] = {low = -62, high = 1},
    ['medium-biter'] = {low = -14, high = 1},
    ['medium-spitter'] = {low = -14, high = 1},
    ['big-biter'] = {low = -2, high = 1},
    ['big-spitter'] = {low = -2, high = 1},
    ['behemoth-biter'] = {low = 1, high = 1},
    ['behemoth-spitter'] = {low = 1, high = 1}, ]]
    ['biter-spawner'] = {low = 8, high = 24},
    ['spitter-spawner'] = {low = 8, high = 24},
    ['small-worm-turret'] = {low = 3, high = 10},
    ['medium-worm-turret'] = {low = 8, high = 24},
    ['big-worm-turret'] = {low = 15, high = 30},
    ['behemoth-worm-turret'] = {low = 25, high = 45}
}

local spill_items =
    Token.register(
    function(data)
        local stack = {name = 'coin', count = data.count}
        data.surface.spill_item_stack(data.position, stack, true)
    end
)

local entity_spawn_map = {
    ['medium-biter'] = {name = 'small-worm-turret', count = 1, chance = 0.2},
    ['big-biter'] = {name = 'medium-worm-turret', count = 1, chance = 0.2},
    ['behemoth-biter'] = {name = 'big-worm-turret', count = 1, chance = 0.2},
    ['medium-spitter'] = {name = 'small-worm-turret', count = 1, chance = 0.2},
    ['big-spitter'] = {name = 'medium-worm-turret', count = 1, chance = 0.2},
    ['behemoth-spitter'] = {name = 'big-worm-turret', count = 1, chance = 0.2},
    ['biter-spawner'] = {type = 'biter', count = 5, chance = 1},
    ['spitter-spawner'] = {type = 'spitter', count = 5, chance = 1},
    ['behemoth-worm-turret'] = {
        type = 'compound',
        spawns = {
            {name = 'behemoth-spitter', count = 2},
            {name = 'behemoth-biter', count = 2}
        },
        chance = 1
    },
    ['stone-furnace'] = {type = 'cause', count = 2, chance = 1},
    ['steel-furnace'] = {type = 'cause', count = 2, chance = 1},
    ['electric-furnace'] = {type = 'cause', count = 4, chance = 1},
    ['assembling-machine-1'] = {type = 'cause', count = 4, chance = 1},
    ['assembling-machine-2'] = {type = 'cause', count = 4, chance = 1},
    ['assembling-machine-3'] = {type = 'cause', count = 4, chance = 1},
    ['chemical-plant'] = {type = 'cause', count = 4, chance = 1},
    ['centrifuge'] = {type = 'cause', count = 6, chance = 1},
    ['pumpjack'] = {type = 'cause', count = 6, chance = 1},
    ['storage-tank'] = {type = 'cause', count = 4, chance = 1},
    ['oil-refinery'] = {type = 'cause', count = 8, chance = 1},
    ['offshore-pump'] = {type = 'cause', count = 2, chance = 1},
    ['boiler'] = {type = 'cause', count = 2, chance = 1},
    ['heat-exchanger'] = {type = 'cause', count = 4, chance = 1},
    ['steam-engine'] = {type = 'cause', count = 6, chance = 1},
    ['steam-turbine'] = {type = 'cause', count = 10, chance = 1},
    ['nuclear-reactor'] = {type = 'cause', count = 20, chance = 1},
    ['rocket-silo'] = {type = 'cause', count = 40, chance = 1},
    ['train-stop'] = {type = 'cause', count = 2, chance = 1},
    ['burner-mining-drill'] = {type = 'cause', count = 2, chance = 1},
    ['electric-mining-drill'] = {type = 'cause', count = 4, chance = 1},
    ['lab'] = {type = 'cause', count = 6, chance = 1},
    ['solar-panel'] = {type = 'cause', count = 4, chance = 1},
    ['accumulator'] = {type = 'cause', count = 2, chance = 1},
    ['beacon'] = {type = 'cause', count = 6, chance = 1},
    ['radar'] = {type = 'cause', count = 4, chance = 1}
}

local unit_levels = {
    biter = {'small-biter', 'medium-biter', 'big-biter', 'behemoth-biter'},
    spitter = {
        'small-spitter',
        'medium-spitter',
        'big-spitter',
        'behemoth-spitter'
    }
}

local worms = {
    ['small-worm-turret'] = true,
    ['medium-worm-turret'] = true,
    ['big-worm-turret'] = true,
    ['behemoth-worm-turret'] = true
}

local allowed_cause_source = {
    ['small-biter'] = true,
    ['medium-biter'] = true,
    ['big-biter'] = true,
    ['behemoth-biter'] = true,
    ['small-spitter'] = true,
    ['medium-spitter'] = true,
    ['big-spitter'] = true,
    ['behemoth-spitter'] = true
}

local turret_evolution_factor = {
    ['gun-turret'] = 0.001,
    ['laser-turret'] = 0.002,
    ['flamethrower-turret'] = 0.0015,
    ['artillery-turret'] = 0.004
}

local spawn_worm =
    Token.register(
    function(data)
        local surface = data.surface
        local name = data.name
        local position = data.position

        local p = surface.find_non_colliding_position(name, position, 8, 1)

        if p then
            local entity = surface.create_entity({name = data.name, position = data.position})
            no_coin_entity[entity.unit_number] = true
        end
    end
)

local function get_level()
    local ef = game.forces.enemy.evolution_factor
    if ef == 0 then
        return 1
    else
        return ceil(ef * 4)
    end
end

local spawn_units =
    Token.register(
    function(data)
        local surface = data.surface
        local name = data.name
        local position = data.position
        for _ = 1, data.count do
            local p = surface.find_non_colliding_position(name, position, 8, 1)
            if p then
                surface.create_entity {name = name, position = p}
            end
        end
    end
)

local spawn_player =
    Token.register(
    function(player)
        if player and player.valid then
            player.ticks_to_respawn = 3600
        end
    end
)

local function has_valid_turret(turrets)
    for i = #turrets, 1, -1 do
        local turret = turrets[i]
        if turret.valid then
            return true
        else
            fast_remove(turrets, i)
        end
    end

    return false
end

local pole_callback
pole_callback =
    Token.register(
    function(data)
        if not has_valid_turret(data.turrets) then
            return
        end

        local tick = data.tick
        local now = game.tick

        if now >= tick then
            data.surface.create_entity(
                {
                    name = data.name,
                    force = 'enemy',
                    position = data.position
                }
            )
            return
        end

        local fraction = ((now - tick) / pole_respawn_time) + 1

        draw_arc(
            {
                color = {1 - fraction, fraction, 0},
                max_radius = 0.5,
                min_radius = 0.4,
                start_angle = start_angle,
                angle = fraction * tau,
                target = data.position,
                surface = data.surface,
                time_to_live = time_to_live
            }
        )

        set_timeout_in_ticks(update_rate, pole_callback, data)
    end
)

local filter = {area = nil, name = 'laser-turret', force = 'enemy'}

local function do_pole(entity)
    if entity.type ~= 'electric-pole' then
        return
    end

    local supply_area_distance = entity.prototype.supply_area_distance
    if not supply_area_distance then
        return
    end

    local surface = entity.surface
    local position = entity.position
    local x, y = position.x, position.y
    local d = supply_area_distance / 2
    filter.area = {{x - d, y - d}, {x + d, y + d}}

    local turrets = surface.find_entities_filtered(filter)

    if #turrets == 0 then
        return
    end

    set_timeout_in_ticks(
        update_rate,
        pole_callback,
        {
            name = entity.name,
            position = position,
            surface = surface,
            tick = game.tick + pole_respawn_time,
            turrets = turrets
        }
    )
end

local function do_evolution(entity, entity_name, entity_force)
    local factor = turret_evolution_factor[entity_name]
    if factor then
        local old = entity_force.evolution_factor
        local extra = (1 - old) * factor
        local new = old + extra
        if new < 1 then
            entity_force.evolution_factor = new
            local position = entity.position
            entity.surface.create_entity{name="flying-text", position = {position.x - 1, position.y}, text = "+" .. round(extra*100,2) .. "% evo", color = Color.plum}
        end
    end
end

local bot_spawn_whitelist = {
    ['gun-turret'] = true,
    ['laser-turret'] = true,
    ['flamethrower-turret'] = true,
    ['artillery-turret'] = true
}

local bot_cause_whitelist = {
    ['character'] = true,
    ['artillery-turret'] = true,
    ['artillery-wagon'] = true,
    ['spidertron'] = true
}

-- https://en.wikipedia.org/wiki/Euclidean_distance#Two_dimensions
-- math.sqrt is computationally expensive so we compare to the distance squared instead
local function euclidean_distance_squared(p, q)
    local distance = (p.x-q.x)^2 + (p.y-q.y)^2
    return distance
end

local destroyer_callback
destroyer_callback =
    Token.register(
    function(data)
        local entity = data.entity
        local destroyer = data.destroyer
        if not destroyer or not destroyer.valid or not entity or not entity.valid then
            return
        end
        local distance = 10
        if euclidean_distance_squared(destroyer.position, entity.position) < distance^2 then
            entity.surface.create_entity{name = "laser", position=destroyer.position, target=entity, speed=1}
        end
        set_timeout_in_ticks(30, destroyer_callback, data)
    end
)

local artillery_cooldown_callback
artillery_cooldown_callback =
    Token.register(
    function(entity)
        if not entity.valid then
            return
        end

        entity.minable = true
    end
)

local function do_bot_spawn(entity_name, entity, event)
    -- Return if the entity killed is not on the white list
    if not bot_spawn_whitelist[entity_name] then
            return
    end

    -- Return if the evolution is too low
    local entity_force = entity.force
    local ef = entity_force.evolution_factor
    if ef <= 0.2 then
        return
    end

    local cause = event.cause
    local create_entity = entity.surface.create_entity

    local spawn_entity = {
        position = entity.position,
        target = cause,
        force = entity_force
    }

    -- Cbeck if there is a cause for the entity's death
    -- If there is no cause then the player probably picked up an artillery turret before the projectile hit the entity.
    -- This causes no bots to spawn because there is no cause. Punish the player for the behaviour by sending some bots to spawn instead of their location
    if not cause then
        return
    end

    -- Now we have checked for no cause, check for if the cause was on the cause whitelist (players, artillery, spidertrons)
    if not bot_cause_whitelist[cause.name] then
        return
    end

    local repeat_cycle = 1 -- The number of times a squad of robots are spawned default must be 1
    if ef > .95 then
        repeat_cycle = 2
    end

    if cause.name ~= 'character' then
        if (entity_name == 'artillery-turret') then
            repeat_cycle = 8
        else
            repeat_cycle = 4
        end
        for i = 1, repeat_cycle do
            if (cause.name == 'artillery-turret') then
                cause.minable = false
                set_timeout_in_ticks(300, artillery_cooldown_callback, cause)
                spawn_entity.name = 'defender'
                create_entity(spawn_entity)
                create_entity(spawn_entity)

                spawn_entity.name = 'destroyer'
                create_entity(spawn_entity)
                create_entity(spawn_entity)

            elseif (cause.name == 'artillery-wagon') then
                cause.minable = false
                set_timeout_in_ticks(300, artillery_cooldown_callback, cause)
                spawn_entity.name = 'defender'
                create_entity(spawn_entity)
                create_entity(spawn_entity)

                spawn_entity.name = 'destroyer'
                local destroyer = create_entity(spawn_entity)
                set_timeout_in_ticks(random(30,60), destroyer_callback, {destroyer = destroyer, entity = cause})
                destroyer = create_entity(spawn_entity)
                set_timeout_in_ticks(random(30,60), destroyer_callback, {destroyer = destroyer, entity = cause})
            else
                spawn_entity.name = 'defender'
                create_entity(spawn_entity)
                create_entity(spawn_entity)

                spawn_entity.name = 'destroyer'
                create_entity(spawn_entity)
                create_entity(spawn_entity)
            end
        end
    elseif entity_name == 'gun-turret' then
        for i = 1, repeat_cycle do
            spawn_entity.name = 'defender'
            create_entity(spawn_entity)
            create_entity(spawn_entity)

            spawn_entity.name = 'destroyer'
            create_entity(spawn_entity)
        end
    elseif entity_name == 'laser-turret' then
        for i = 1, repeat_cycle do
            spawn_entity.name = 'defender'
            create_entity(spawn_entity)

            spawn_entity.name = 'destroyer'
            create_entity(spawn_entity)
            create_entity(spawn_entity)
        end
    else
        for i = 1, repeat_cycle do
            spawn_entity.name = 'distractor-capsule'
            spawn_entity.speed = 0
            create_entity(spawn_entity)
        end
    end
end

local function do_coin_drop(entity_name, entity, cause)
    local position = entity.position
    local bounds = entity_drop_amount[entity_name]
    if not bounds then
        return
    end

    local unit_number = entity.unit_number
    if no_coin_entity[unit_number] then
        no_coin_entity[unit_number] = nil
        return
    end

    local count = random(bounds.low, bounds.high)
    if count <= 0 then
        return
    end

    if cause and cause.name == 'character' then
        local player = cause.player
        if player and player.valid then
            local coins = {name = "coin", count = count}
            if player.can_insert(coins) then
                player.insert(coins)
                entity.surface.create_entity{name="flying-text", position = {position.x - 1, position.y}, text = "+" .. count .. " [img=item.coin]", color = {1, 0.8, 0, 0.5}, render_player_index = player.index}
                return
            end
        end
    end

    -- spill them on the floor
    set_timeout_in_ticks(
        1,
        spill_items,
        {
            count = count,
            surface = entity.surface,
            position = position
        }
    )
end

local function do_spawn_entity(entity_name, entity, event)
    local spawn = entity_spawn_map[entity_name]
    if not spawn then
        return
    end

    local chance = spawn.chance
    if chance ~= 1 and random() > chance then
        return
    end

    local name = spawn.name
    if name == nil then
        local type = spawn.type
        if type == 'cause' then
            local cause = event.cause
            if not cause then
                return
            end
            name = cause.name
            if not allowed_cause_source[cause.name] then
                return
            end
        elseif type == 'compound' then
            local spawns = spawn.spawns
            spawn = spawns[random(#spawns)]
            name = spawn.name
        else
            name = unit_levels[type][get_level()]
        end
    end

    local position = entity.position
    if worms[name] then
        set_timeout_in_ticks(
            5,
            spawn_worm,
            {
                surface = entity.surface,
                name = name,
                position = position
            }
        )
    else
        set_timeout_in_ticks(
            5,
            spawn_units,
            {
                surface = entity.surface,
                name = name,
                position = position,
                count = spawn.count
            }
        )
    end
end

Event.add(
    defines.events.on_entity_died,
    function(event)
        local entity = event.entity
        if not entity or not entity.valid then
            return
        end

        local entity_force = entity.force
        local entity_name = entity.name
        local cause = event.cause

        if entity_force.name == 'enemy' then
            do_pole(entity)
            do_evolution(entity, entity_name, entity_force)
            do_coin_drop(entity_name, entity, cause)
            do_bot_spawn(entity_name, entity, event)
        end

        do_spawn_entity(entity_name, entity, event)
    end
)

Event.add(
    defines.events.on_player_died,
    function(event)
        local player = game.get_player(event.player_index)
        set_timeout_in_ticks(1, spawn_player, player)
    end
)