-- Biters, Spawners and Worms gain additional health / resistance -- mewmew
-- modified by Gerkiz
-- Use this.biter_health_boost or this.biter_health_boost_forces to modify their health.
-- 1 = vanilla health, 2 = 200% vanilla health
-- do not use values below 1

local Event = require 'utils.event'
local LootDrop = require 'modules.mobs_drop_loot'
local WD = require 'modules.wave_defense.table'
local Global = require 'utils.global'
local Task = require 'utils.task'
local Token = require 'utils.token'
local Server = require 'utils.server'

local floor = math.floor
local insert = table.insert
local random = math.random
local sqrt = math.sqrt
local round = math.round
local Public = {}

Public.events =
{
    custom_on_entity_died = Event.generate_event_name('custom_on_entity_died')
}

local this =
{
    enabled = true,
    biter_health_boost = 1,
    biter_health_boost_forced = false,
    biter_health_boost_forces = {},
    biter_health_boost_units = {},
    biter_health_boost_count = 0,
    make_normal_unit_mini_bosses = false,
    active_surface = 'nauvis',
    active_surfaces = {},
    acid_lines_delay = {},
    acid_nova = false,
    boss_spawns_projectiles = false,
    enable_boss_loot = false,
    randomize_stun_and_slowdown_sticker = false
}

local radius = 6
local targets = {}
local acid_splashes =
{
    ['big-biter'] = 'acid-stream-worm-medium',
    ['behemoth-biter'] = 'acid-stream-worm-big'
}
local acid_lines =
{
    ['small-spitter'] = 'acid-stream-spitter-small',
    ['medium-spitter'] = 'acid-stream-spitter-medium',
    ['big-spitter'] = 'acid-stream-spitter-big',
    ['behemoth-spitter'] = 'acid-stream-spitter-big'
}
for x = radius * -1, radius, 1 do
    for y = radius * -1, radius, 1 do
        if sqrt(x ^ 2 + y ^ 2) <= radius then
            targets[#targets + 1] = { x = x, y = y }
        end
    end
end

local projectiles =
{
    'slowdown-capsule',
    'defender-capsule',
    'destroyer-capsule',
    'laser',
    'distractor-capsule',
    'rocket',
    'explosive-rocket',
    'grenade',
    'rocket',
    'grenade'
}

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

--[[ local filters = {
    { filter = 'name', name = 'unit' },
    { filter = 'name', name = 'turret' },
    { filter = 'name', name = 'ammo-turret' },
    { filter = 'name', name = 'electric-turret' },
    { filter = 'name', name = 'unit-spawner' }
} ]]

local entity_types =
{
    ['unit'] = true,
    ['turret'] = true,
    ['ammo-turret'] = true,
    ['electric-turret'] = true,
    ['unit-spawner'] = true
}

local remove_unit_token =
    Token.register(
        function (data)
            local unit_number = data.unit_number
            if not unit_number then
                return
            end

            this.biter_health_boost_units[unit_number] = nil
        end
    )

local function loaded_biters(event)
    local cause = event.cause
    local entity = event.entity
    if not entity or not entity.valid then
        return
    end

    local position = false
    if cause then
        if cause.valid then
            position = cause.position
        end
    end
    if not position then
        position = { entity.position.x + (-20 + random(0, 40)), entity.position.y + (-20 + random(0, 40)) }
    end

    entity.surface.create_entity(
        {
            name = projectiles[random(1, 10)],
            position = entity.position,
            force = 'enemy',
            source = entity.position,
            target = position,
            max_range = 16,
            speed = 0.01
        }
    )
end

local function acid_nova(event)
    for _ = 1, random(20, 40) do
        local i = random(1, #targets)
        event.entity.surface.create_entity(
            {
                name = acid_splashes[event.entity.name],
                position = event.entity.position,
                force = event.entity.force.name,
                source = event.entity.position,
                target = { x = event.entity.position.x + targets[i].x, y = event.entity.position.y + targets[i].y },
                max_range = radius,
                speed = 0.001
            }
        )
    end
end

local function create_entity_radius(surface, name, source, target)
    local distance = sqrt((source.x - target.x) ^ 2 + (source.y - target.y) ^ 2)
    local modifier = { (target.x - source.x) / distance, (target.y - source.y) / distance }

    local position = { source.x, source.y }

    for _ = 1, distance * 1.5, 1 do
        if random(1, 2) ~= 1 then
            surface.create_entity(
                {
                    name = name,
                    position = source,
                    force = 'enemy',
                    source = source,
                    target = position,
                    max_range = 25,
                    speed = 1
                }
            )
        end
        position = { position[1] + modifier[1], position[2] + modifier[2] }
    end
end

local function clean_table()
    local units_to_delete = {}

    --Mark all health boost entries for deletion
    for key, _ in pairs(this.biter_health_boost_units) do
        units_to_delete[key] = true
    end

    --Remove valid health boost entries from deletion
    local validTypes = {}
    for k, v in pairs(entity_types) do
        if v then
            insert(validTypes, k)
        end
    end

    for name, enabled in pairs(this.active_surfaces) do
        local surface = game.surfaces[name]
        if surface and surface.valid and enabled then
            for _, unit in pairs(surface.find_entities_filtered({ type = validTypes })) do
                units_to_delete[unit.unit_number] = nil
            end
        end
    end

    local surface = game.surfaces[this.active_surface]
    if not (surface and surface.valid) then
        return
    end

    for _, unit in pairs(surface.find_entities_filtered({ type = validTypes })) do
        units_to_delete[unit.unit_number] = nil
    end

    --Remove abandoned health boost entries
    for key, _ in pairs(units_to_delete) do
        this.biter_health_boost_units[key] = nil
    end
end

local function check_clear_table()
    this.biter_health_boost_count = this.biter_health_boost_count + 1
    if this.biter_health_boost_count >= 500 then
        clean_table()
        this.biter_health_boost_count = 0
    end
end

local function create_boss_healthbar(entity, size)
    return rendering.draw_sprite(
        {
            sprite = 'virtual-signal/signal-white',
            tint = { 0, 200, 0 },
            x_scale = size * 15,
            y_scale = size,
            render_layer = 'light-effect',
            target =
            {
                entity = entity,
                offset = { 0, -2.25 },
            },
            surface = entity.surface
        }
    )
end

local function set_boss_healthbar(health, max_health, healthbar_id)
    local m = health / max_health
    if m < 0 then
        return
    end
    local x_scale = healthbar_id.y_scale * 15
    healthbar_id.x_scale = x_scale * m
    healthbar_id.color = { floor(255 - 255 * m), floor(200 * m), 0 }
end

local function extra_projectiles(cause, target)
    if not cause or not cause.valid then
        return
    end
    local biter_health_boost_units = this.biter_health_boost_units
    local cause_unit_number = cause.unit_number
    local cause_health_pool = biter_health_boost_units[cause_unit_number]
    if cause_health_pool and cause_health_pool[3] and cause_health_pool[3].healthbar_id and entity_types[cause.type] then
        if this.acid_nova then
            if acid_lines[cause.name] then
                if not this.acid_lines_delay[cause_unit_number] then
                    this.acid_lines_delay[cause_unit_number] = 0
                end
                if this.acid_lines_delay[cause_unit_number] < game.tick then
                    create_entity_radius(cause.surface, acid_lines[cause.name], cause.position, target.position)
                    this.acid_lines_delay[cause_unit_number] = game.tick + 180
                end
            end
        end
    end
end

---@param event EventData.on_entity_damaged
local function on_entity_damaged(event)
    if not this.enabled then
        return
    end
    local biter = event.entity
    if not (biter and biter.valid) then
        return
    end
    local surface = biter.surface
    local cause = event.cause

    local biter_health_boost_units = this.biter_health_boost_units

    local unit_number = biter.unit_number

    local damage = event.final_damage_amount

    --Create new health pool
    local health_pool = biter_health_boost_units[unit_number]
    extra_projectiles(cause, biter)

    if not entity_types[biter.type] then
        return
    end

    if this.randomize_stun_and_slowdown_sticker then
        local damage_type = event.damage_type
        if damage_type and damage_type.name == 'electric' then
            local stickers = biter.stickers
            if stickers and #stickers > 0 then
                for i = 1, #stickers, 1 do
                    if random(1, 4) == 1 then -- there's a % that biters can recover from stun and get slowed instead.
                        if stickers[i].sticked_to == biter then
                            if stickers[i].name == 'stun-sticker' then
                                stickers[i].destroy()
                                if random(1, 2) == 1 then
                                    local slow = surface.create_entity { name = 'slowdown-sticker', position = biter.position, target = biter }
                                    slow.time_to_live = 200
                                end
                                break
                            elseif stickers[i].name == 'slowdown-sticker' then
                                stickers[i].destroy()
                            end
                        end
                    end
                end
            end
        end
    end

    if not health_pool and this.make_normal_unit_mini_bosses then
        if this.biter_health_boost_forces[biter.force.index] then
            Public.add_unit(biter, this.biter_health_boost_forces[biter.force.index])
        else
            Public.add_unit(biter, this.biter_health_boost)
        end
        health_pool = this.biter_health_boost_units[unit_number]
    end

    if not health_pool then
        return
    end

    --Process boss unit health bars
    local boss = health_pool[3]
    if boss and boss.healthbar_id then
        set_boss_healthbar(health_pool[1], boss.max_health, boss.healthbar_id)
    end

    --Reduce health pool
    health_pool[1] = round(health_pool[1] - damage)

    --Set entity health relative to health pool
    local max_health = health_pool[3].max_health
    local m = health_pool[1] / max_health
    local final_health = round(biter.max_health * m)
    biter.health = final_health

    --Proceed to kill entity if health is 0
    if biter.health > 0 and health_pool[1] > 0 then
        return
    end

    if cause then
        if cause.valid then
            Event.raise(Public.events.custom_on_entity_died, event)
            biter.die(cause.force, cause)
            return
        end
    end

    Event.raise(Public.events.custom_on_entity_died, event)
    biter.die(biter.force)
end

local function on_entity_died(event)
    if not this.check_on_entity_died then
        return
    end

    if not this.enabled then
        return
    end

    local biter = event.entity
    if not (biter and biter.valid) then
        return
    end
    if not entity_types[biter.type] then
        return
    end

    local biter_health_boost_units = this.biter_health_boost_units

    local unit_number = biter.unit_number
    local health_pool = biter_health_boost_units[unit_number]
    local wave_count = WD.get_wave()

    if health_pool then
        Task.set_timeout_in_ticks(30, remove_unit_token, { unit_number = unit_number })
        if health_pool[3] and health_pool[3].healthbar_id then
            if this.enable_boss_loot then
                if random(1, 128) == 1 then
                    LootDrop.drop_loot(biter, wave_count)
                end
            end
            if this.boss_spawns_projectiles then
                if random(1, 32) == 1 then
                    loaded_biters(event)
                end
            end
            if this.acid_nova then
                if acid_splashes[biter.name] then
                    acid_nova(event)
                end
                if this.acid_lines_delay[biter.unit_number] then
                    this.acid_lines_delay[biter.unit_number] = nil
                end
            end
        end
    end
end

--- Use this function to retrieve a key from the global table.
---@param key string
function Public.get(key)
    if key then
        return this[key]
    else
        return this
    end
end

--- Using this function can set a new value to an exist key or create a new key with value
---@param key string
---@param value any
function Public.set(key, value)
    if key and (value or value == false) then
        this[key] = value
        return this[key]
    elseif key then
        return this[key]
    else
        return this
    end
end

--- Use this function to reset the global table to it's init values.
function Public.reset_table()
    this.biter_health_boost = 1
    this.biter_health_boost_forces = false
    this.biter_health_boost_forces = {}
    this.biter_health_boost_units = {}
    this.biter_health_boost_count = 0
    this.make_normal_unit_mini_bosses = false
    this.active_surface = 'nauvis'
    this.active_surfaces = {}
    this.check_on_entity_died = false
    this.acid_lines_delay = {}
    this.acid_nova = false
    this.boss_spawns_projectiles = false
    this.enable_boss_loot = false
end

--- Use this function to add a new unit that has extra health
---@param unit userdata
---@param health_multiplier number
function Public.add_unit(unit, health_multiplier)
    if not health_multiplier then
        health_multiplier = this.biter_health_boost
    end
    local health = floor(unit.max_health * health_multiplier)
    local xp_modifier = round(1 / health_multiplier, 5)
    this.biter_health_boost_units[unit.unit_number] =
    {
        health,
        xp_modifier,
        { max_health = health }
    }

    check_clear_table()
end

--- Use this function to add a new boss unit (with healthbar)
---@param unit userdata
---@param health_multiplier number
---@param health_bar_size number|nil
function Public.add_boss_unit(unit, health_multiplier, health_bar_size)
    if not health_multiplier then
        health_multiplier = this.biter_health_boost
    end
    if not health_bar_size then
        health_bar_size = 0.5
    end
    local xp_modifier = round(1 / health_multiplier, 5)
    local health = floor(unit.max_health * health_multiplier)
    this.biter_health_boost_units[unit.unit_number] =
    {
        health,
        xp_modifier,
        { max_health = health, healthbar_id = create_boss_healthbar(unit, health_bar_size) }
    }

    check_clear_table()
end

--- This sets the active surface that we check and have the script active.
--- This deletes the list of surfaces if we use multiple, so use it only before setting more of them.
---@param str string
function Public.set_active_surface(str)
    if str and type(str) == 'string' then
        Server.output_script_data('Biter Health Booster active surface is now: ' .. tostring(str))
        this.active_surfaces = {}
        this.active_surface = str or 'nauvis'
    end
    return this.active_surface
end

--- This sets if this surface is active, when we using multiple surfaces. The default active surface does not need to be added again
---@param name string
---@param value boolean
function Public.set_surface_activity(name, value)
    if name and type(name) == 'string' and type(value) == 'boolean' then
        Server.output_script_data('Biter Health Booster active surfaces is now: ' .. tostring(value))
        this.active_surfaces[name] = value
    end
    return this.active_surfaces
end

--- Enables that biter bosses (units with health bars) spawns acid on death.
---@param boolean boolean
function Public.acid_nova(boolean)
    Server.output_script_data('Biter Health Booster acid nova is now: ' .. tostring(boolean))
    this.acid_nova = boolean or false
    return this.acid_nova
end

--- Enables that we clear units from the global table when a unit dies.
---@param boolean boolean
function Public.check_on_entity_died(boolean)
    Server.output_script_data('Biter Health Booster check on entity died is now: ' .. tostring(boolean))
    this.check_on_entity_died = boolean or false
    return this.check_on_entity_died
end

--- Enables that biter bosses (units with health bars) spawns projectiles on death.
---@param boolean boolean
function Public.boss_spawns_projectiles(boolean)
    Server.output_script_data('Biter Health Booster boss spawns projectiles is now: ' .. tostring(boolean))
    this.boss_spawns_projectiles = boolean or false

    return this.boss_spawns_projectiles
end

--- Enables that biter bosses (units with health bars) drops loot.
---@param boolean boolean
function Public.enable_boss_loot(boolean)
    Server.output_script_data('Biter Health Booster enable boss loot is now: ' .. tostring(boolean))
    this.enable_boss_loot = boolean or false

    return this.enable_boss_loot
end

--- Forces a value of biter_health_boost
---@param boolean boolean
function Public.enable_biter_health_boost_forced(boolean)
    Server.output_script_data('Biter Health Booster biter health boost forced is now: ' .. tostring(boolean))
    this.biter_health_boost_forced = boolean or false

    return this.biter_health_boost_forced
end

--- Enables that normal units have boosted health.
---@param boolean boolean
function Public.enable_make_normal_unit_mini_bosses(boolean)
    Server.output_script_data('Biter Health Booster make normal unit mini bosses is now: ' .. tostring(boolean))
    this.make_normal_unit_mini_bosses = boolean or false

    return this.make_normal_unit_mini_bosses
end

--- Enables that enemies can recover from stun randomly.
---@param boolean boolean
function Public.enable_randomize_stun_and_slowdown_sticker(boolean)
    Server.output_script_data('Biter Health Booster randomize stun and slowdown sticker is now: ' .. tostring(boolean))
    this.randomize_stun_and_slowdown_sticker = boolean or false

    return this.randomize_stun_and_slowdown_sticker
end

--- Enables or disables the module.
---@param boolean boolean
function Public.set_module_state(boolean)
    Server.output_script_data('Biter Health Booster is now: ' .. tostring(boolean))
    this.enabled = boolean or false
    return this.enabled
end

Event.on_init(
    function ()
        Public.reset_table()
    end
)

Event.add(defines.events.on_entity_damaged, on_entity_damaged)
Event.on_nth_tick(7200, check_clear_table)
Event.add(defines.events.on_entity_died, on_entity_died)

return Public
