local Public = require 'modules.wave_defense.core'
local Event = require 'utils.event'
local BiterHealthBooster = require 'modules.biter_health_booster_v2'
local Difficulty = require 'modules.difficulty_vote_by_amount'
local Alert = require 'utils.alert'
local Server = require 'utils.server'
local Collapse = require 'modules.collapse'
Collapse.read_tables_only = true

local random = math.random
local floor = math.floor
local sqrt = math.sqrt
local round = math.round
local raise = Event.raise
local check_if_near_target

local function valid(userdata)
    if not (userdata and userdata.valid) then
        return false
    end
    return true
end

local function normalize_spawn_position()
    local collapse_spawn_position = Collapse.get_position()
    local inverted = Public.get('inverted')
    if inverted then
        local new_pos = { x = 0, y = collapse_spawn_position.y + 40 }
        Public.set_spawn_position(new_pos)
        return new_pos
    else
        local new_pos = { x = 0, y = collapse_spawn_position.y }
        Public.set_spawn_position(new_pos)
        return new_pos
    end
end

local function find_initial_spot(surface, position)
    local pos = surface.find_non_colliding_position('boiler', position, 128, 1)
    if not pos then
        pos = surface.find_non_colliding_position('boiler', position, 148, 1)
    end
    if not pos then
        pos = surface.find_non_colliding_position('boiler', position, 164, 1)
    end
    if not pos then
        pos = surface.find_non_colliding_position('boiler', position, 200, 1)
    end
    if not pos then
        pos = position
    end

    if not pos then
        pos = position
    end

    Public.set('spot', pos)
    return pos
end

local function is_closer(pos1, pos2, pos)
    return ((pos1.x - pos.x) ^ 2 + (pos1.y - pos.y) ^ 2) < ((pos2.x - pos.x) ^ 2 + (pos2.y - pos.y) ^ 2)
end

local function shuffle_distance(tbl, position)
    local size = #tbl
    for i = size, 1, -1 do
        local rand = random(size)
        if is_closer(tbl[i].position, tbl[rand].position, position) and i > rand then
            tbl[i], tbl[rand] = tbl[rand], tbl[i]
        end
    end
    return tbl
end

local function is_position_near(pos_to_check, check_against)
    local function inside(pos)
        return pos.x >= pos_to_check.x and pos.y >= pos_to_check.y and pos.x <= pos_to_check.x and pos.y <= pos_to_check.y
    end

    if inside(check_against) then
        return true
    end

    return false
end

local function remove_trees(entity)
    if not valid(entity) then
        return
    end
    local surface = entity.surface
    local radius = 10
    local pos = entity.position
    local area = { { pos.x - radius, pos.y - radius }, { pos.x + radius, pos.y + radius } }

    -- Find all trees within the bounding rectangle
    local trees = surface.find_entities_filtered { area = area, type = 'tree' }
    if #trees > 0 then
        for _, tree in pairs(trees) do
            if tree and tree.valid then
                local dx = tree.position.x - pos.x
                local dy = tree.position.y - pos.y
                local distance_squared = dx * dx + dy * dy

                if distance_squared <= radius * radius then
                    tree.destroy()
                end
            end
        end
    end
end

local function remove_rocks(entity)
    if not valid(entity) then
        return
    end
    local surface = entity.surface
    local radius = 10
    local pos = entity.position
    local area = { { pos.x - radius, pos.y - radius }, { pos.x + radius, pos.y + radius } }
    local rocks = surface.find_entities_filtered { area = area, type = 'simple-entity' }
    if #rocks > 0 then
        for _, rock in pairs(rocks) do
            if rock and rock.valid then
                local dx = rock.position.x - pos.x
                local dy = rock.position.y - pos.y
                local distance_squared = dx * dx + dy * dy

                if distance_squared <= radius * radius then
                    rock.destroy()
                end
            end
        end
    end
end

local function fill_tiles(entity, size)
    if not valid(entity) then
        return
    end
    local surface = entity.surface
    local radius = size or 10
    local pos = entity.position
    local t =
    {
        'water',
        'water-green',
        'water-mud',
        'water-shallow',
        'deepwater',
        'deepwater-green'
    }
    local area = { { pos.x - radius, pos.y - radius }, { pos.x + radius, pos.y + radius } }
    local tiles = surface.find_tiles_filtered { area = area, name = t }
    if #tiles > 0 then
        for _, tile in pairs(tiles) do
            local dx = tile.position.x - pos.x
            local dy = tile.position.y - pos.y
            local distance_squared = dx * dx + dy * dy

            if distance_squared <= radius * radius then
                surface.set_tiles({ { name = 'sand-1', position = tile.position } }, true)
            end
        end
    end
    Public.debug_print('fill_tiles - filled tiles cause we found non-placable tiles.')
end

local function get_spawn_pos(spawner)
    local surface_index = Public.get('surface_index')
    local surface = game.surfaces[surface_index]
    if not surface then
        return Public.debug_print('get_spawn_pos - surface was not valid?')
    end

    local c = 0

    ::retry::

    local initial_position = Public.get('spawn_position')
    local target = Public.get('target')

    local inverted = Public.get('inverted')
    if not spawner then
        if inverted then
            if initial_position.y - target.position.y < -10 then
                initial_position = { x = initial_position.x, y = initial_position.y + 50 }
            end
            if initial_position.y - target.position.y > 10 then
                initial_position = { x = initial_position.x, y = initial_position.y - 10 }
            end
        end
    end

    local located_position
    if not spawner then
        located_position = find_initial_spot(surface, initial_position)
    else
        located_position = initial_position
    end
    local valid_position = surface.find_non_colliding_position('stone-furnace', located_position, 32, 1)
    local debug = Public.get('debug')
    if debug then
        if valid_position then
            local x = valid_position.x
            local y = valid_position.y
            game.print('[gps=' .. x .. ',' .. y .. ',' .. surface.name .. ']')
        end
    end

    if not valid_position then
        local remove_entities = Public.get('remove_entities')
        if remove_entities then
            c = c + 1
            valid_position = Public.get('spawn_position')
            Public.debug_print(serpent.block('valid_position - x:' .. valid_position.x .. ' y:' .. valid_position.y))
            remove_trees({ surface = surface, position = valid_position, valid = true })
            remove_rocks({ surface = surface, position = valid_position, valid = true })
            fill_tiles({ surface = surface, position = valid_position, valid = true })
            Public.set('spot', 'nil')
            if c == 5 then
                return Public.debug_print('get_spawn_pos - we could not find a spawning pos?')
            end
            goto retry
        else
            return Public.debug_print('get_spawn_pos - we could not find a spawning pos?')
        end
    end

    Public.debug_print(serpent.block('valid_position - x:' .. valid_position.x .. ' y:' .. valid_position.y))

    return valid_position
end

local function is_unit_valid(biter, max_biter_age, tick)
    if not biter.entity then
        Public.debug_print('is_unit_valid - unit destroyed - does no longer exist')
        return false
    end
    if not biter.entity.valid then
        Public.debug_print('is_unit_valid - unit destroyed - invalid')
        return false
    end
    if biter.spawn_tick + max_biter_age < tick then
        Public.debug_print('is_unit_valid - unit destroyed - timed out')
        return false
    end
    return true
end

local function refresh_active_unit_threat()
    local threat_values = Public.get('threat_values')
    local active_biter_threat = Public.get('active_biter_threat')
    local generated_units = Public.get('generated_units')
    Public.debug_print('refresh_active_unit_threat - current value ' .. active_biter_threat)
    local biter_threat = 0
    for k, biter in pairs(generated_units.active_biters) do
        if valid(biter.entity) then
            biter_threat = biter_threat + threat_values[biter.entity.name]
        else
            generated_units.active_biters[k] = nil
        end
    end
    local biter_health_boost = BiterHealthBooster.get('biter_health_boost')
    Public.set('active_biter_threat', round(biter_threat * biter_health_boost, 2))
    Public.debug_print('refresh_active_unit_threat - new value ' .. active_biter_threat)
    if generated_units.unit_group_pos.index > 500 then
        generated_units.unit_group_pos.positions = {}
        generated_units.unit_group_pos.index = 0
    end
end

local function time_out_biters()
    local generated_units = Public.get('generated_units')
    local active_biter_count = Public.get('active_biter_count')
    local max_active_biters = Public.get('max_active_biters')
    local active_biter_threat = Public.get('active_biter_threat')
    local valid_enemy_forces = Public.get('valid_enemy_forces')
    local threat_values = Public.get('threat_values')

    if active_biter_count < 0 then
        Public.set('active_biter_count', 0)
        Public.debug_print('Resetting active_biter_count')
    elseif active_biter_count > max_active_biters then
        Public.set('active_biter_count', max_active_biters)
        Public.debug_print('Resetting active_biter_count')
    end

    local biter_health_boost = BiterHealthBooster.get('biter_health_boost')
    local max_biter_age = Public.get('max_biter_age')
    local tick = game.tick

    for k, biter in pairs(generated_units.active_biters) do
        if not is_unit_valid(biter, max_biter_age, tick) then
            Public.set('active_biter_count', active_biter_count - 1)
            local entity = biter.entity
            if entity and entity.valid then
                Public.set('active_biter_threat', active_biter_threat - round(threat_values[entity.name] * biter_health_boost, 2))
                if valid_enemy_forces[entity.force.name] then
                    entity.destroy()
                end
            end
            Public.debug_print('time_out_biters: ' .. k .. ' got deleted.')
            generated_units.active_biters[k] = nil
        end
    end
end

local function get_random_close_spawner()
    local generated_units = Public.get('generated_units')
    local target = Public.get('target')
    local get_random_close_spawner_attempts = Public.get('get_random_close_spawner_attempts')
    local center = target.position
    local spawner
    local retries = 0
    for _ = 1, get_random_close_spawner_attempts, 1 do
        ::retry::
        if #generated_units.nests < 1 then
            return false
        end
        local k = random(1, #generated_units.nests)
        local spawner_2 = generated_units.nests[k]
        if not spawner_2 or not spawner_2.valid then
            generated_units.nests[k] = nil
            retries = retries + 1
            if retries == 5 then
                break
            end
            goto retry
        end
        if not spawner or (center.x - spawner_2.position.x) ^ 2 + (center.y - spawner_2.position.y) ^ 2 < (center.x - spawner.position.x) ^ 2 + (center.y - spawner.position.y) ^ 2 then
            spawner = spawner_2
            if spawner and spawner.position then
                Public.debug_print('get_random_close_spawner - Found at x' .. spawner.position.x .. ' y' .. spawner.position.y)
            end
        end
    end
    return spawner
end

local function get_random_character()
    local characters = {}
    local surface_index = Public.get('surface_index')
    local p = game.connected_players
    for _, player in pairs(p) do
        if player.character then
            if player.character.valid then
                if player.character.surface.index == surface_index then
                    characters[#characters + 1] = player.character
                end
            end
        end
    end
    if not characters[1] then
        return
    end
    return characters[random(1, #characters)]
end

local function set_main_target()
    local target = Public.get('target')
    if target then
        if target.valid then
            return
        end
    end

    local unit_groups_size = Public.get('unit_groups_size')
    if unit_groups_size < 0 then
        unit_groups_size = 0
    end
    Public.set('unit_groups_size', unit_groups_size)

    local sec_target = Public.get_side_target()
    if not sec_target then
        sec_target = get_random_character()
    end
    if not sec_target then
        return
    end

    Public.set('target', sec_target)
    raise(Public.events.on_target_aquired, { target = target })
    Public.debug_print('set_main_target -- New main target ' .. sec_target.name .. ' at position x' .. sec_target.position.x .. ' y' .. sec_target.position.y .. ' selected.')
end

local function set_group_spawn_position(surface)
    local spawner = get_random_close_spawner()
    if not spawner then
        return
    end
    local position = surface.find_non_colliding_position('behemoth-biter', spawner.position, 128, 1)
    if not position then
        return
    end
    Public.set('spawn_position', { x = position.x, y = position.y })
    local spawn_position = get_spawn_pos(true)
    if spawn_position then
        Public.debug_print('set_group_spawn_position -- Changed position to x' .. spawn_position.x .. ' y' .. spawn_position.y .. '.')
    end
end

local function set_enemy_evolution()
    local wave_number = Public.get('wave_number')
    local generated_units = Public.get('generated_units')
    local threat = Public.get('threat')
    local evolution_factor = wave_number * 0.0012
    local enemy = game.forces.enemy

    local biter_health_boost = 1

    if evolution_factor > 1 then
        evolution_factor = 1
    end

    if not next(generated_units.active_biters) then
        Public.set('active_biter_count', 0)
    end

    if threat > 50000 then
        biter_health_boost = round(biter_health_boost + (threat - 50000) * 0.000033, 3)
    end

    BiterHealthBooster.set('biter_health_boost', biter_health_boost)

    local surface_index = Public.get('surface_index')

    if enemy.get_evolution_factor(surface_index) == 1 and evolution_factor == 1 then
        return
    end

    enemy.set_evolution_factor(evolution_factor, surface_index)

    raise(Public.events.on_evolution_factor_changed, { evolution_factor = evolution_factor })
end

local function can_units_spawn()
    local threat = Public.get('threat')

    if threat <= 0 then
        Public.debug_print('can_units_spawn - threat too low')
        time_out_biters()
        return false
    end

    local active_biter_count = Public.get('active_biter_count')
    local max_active_biters = Public.get('max_active_biters')
    if active_biter_count >= max_active_biters then
        Public.debug_print('can_units_spawn - active biter count too high')
        time_out_biters()
        return false
    end

    local active_biter_threat = Public.get('active_biter_threat')
    if active_biter_threat >= threat then
        Public.debug_print('can_units_spawn - active biter threat too high (' .. active_biter_threat .. ')')
        time_out_biters()
        return false
    end
    return true
end

local function get_active_unit_groups_count()
    local generated_units = Public.get('generated_units')
    local count = 0

    for k, g in pairs(generated_units.unit_groups) do
        if g.valid then
            if #g.members > 0 then
                count = count + 1
            else
                g.destroy()
                generated_units.unit_groups[k] = nil
                local unit_groups_size = Public.get('unit_groups_size')
                Public.set('unit_groups_size', unit_groups_size - 1)
            end
        else
            generated_units.unit_groups[k] = nil
            generated_units.unit_group_pos.positions[k] = nil
            local unit_groups_size = Public.get('unit_groups_size')
            Public.set('unit_groups_size', unit_groups_size - 1)
            if generated_units.unit_group_last_command[k] then
                generated_units.unit_group_last_command[k] = nil
            end
        end
    end
    Public.debug_print('Active unit group count: ' .. count)
    return count
end

local function spawn_biter(surface, position, fs, is_boss_biter, unit_settings, only_spitters)
    if not (fs and fs.bypass) then
        if not is_boss_biter then
            if not can_units_spawn() then
                return false
            end
        end
    end

    local final_battle = Public.get('final_battle')

    local boosted_health = BiterHealthBooster.get('biter_health_boost')
    local threat_values = Public.get('threat_values')

    local name

    if (fs and fs.random_bosses) then
        name = Public.wave_defense_roll_boss_name()
        if not name then
            if only_spitters or random(1, 100) > 73 then
                name = Public.wave_defense_roll_spitter_name()
            else
                name = Public.wave_defense_roll_biter_name()
            end
        end
    else
        if only_spitters or random(1, 100) > 73 then
            name = Public.wave_defense_roll_spitter_name()
        else
            name = Public.wave_defense_roll_biter_name()
        end
    end

    local old_position = position

    local enable_random_spawn_positions = Public.get('enable_random_spawn_positions')

    if enable_random_spawn_positions then
        if random(1, 3) == 1 then
            position = { x = (-1 * (position.x + random(1, 10))), y = (position.y + random(1, 10)) }
        else
            position = { x = (position.x + random(1, 10)), y = (position.y + random(1, 10)) }
        end
    end

    position = surface.find_non_colliding_position('steel-chest', position, 3, 1)
    if not position then
        position = old_position
    end

    local force = 'enemy'
    local es_settings = Public.get_es('settings')

    if es_settings.enabled then
        force = 'aggressors'
    end

    local e = { name = name, position = position, force = force }

    if not surface.can_place_entity(e) then
        return false
    end

    local biter = surface.create_entity(e)
    if not biter or not biter.valid then
        return false
    end

    biter.ai_settings.allow_destroy_when_commands_fail = true
    biter.ai_settings.allow_try_return_to_spawner = false
    biter.ai_settings.do_separation = true

    local increase_health_per_wave = Public.get('increase_health_per_wave')
    local boost_units_when_wave_is_above = Public.get('boost_units_when_wave_is_above')
    local boost_bosses_when_wave_is_above = Public.get('boost_bosses_when_wave_is_above')
    local wave_number = Public.get('wave_number')

    if (increase_health_per_wave and (wave_number >= boost_units_when_wave_is_above)) and not is_boss_biter then
        local modified_unit_health = Public.get('modified_unit_health')
        local final_health = round(modified_unit_health.current_value * unit_settings.scale_units_by_health[biter.name], 3)
        if final_health < 1 then
            final_health = 1
        end
        Public.debug_print_health('final_health - unit: ' .. biter.name .. ' with h-m: ' .. final_health)
        BiterHealthBooster.add_unit(biter, final_health)
    end

    if is_boss_biter then
        if (wave_number >= boost_bosses_when_wave_is_above) then
            local increase_boss_health_per_wave = Public.get('increase_boss_health_per_wave')
            if final_battle then
                local modified_boss_unit_health = Public.get('modified_boss_unit_health')
                local add_value = modified_boss_unit_health.current_value * 0.5
                BiterHealthBooster.add_boss_unit(biter, modified_boss_unit_health.current_value + add_value, 0.55)
            else
                if increase_boss_health_per_wave then
                    local modified_boss_unit_health = Public.get('modified_boss_unit_health')
                    BiterHealthBooster.add_boss_unit(biter, modified_boss_unit_health.current_value, 0.55)
                else
                    local sum = boosted_health * 5
                    BiterHealthBooster.add_boss_unit(biter, sum, 0.55)
                end
            end
        else
            local sum = boosted_health * 5
            BiterHealthBooster.add_boss_unit(biter, sum, 0.55)
        end
    end

    local generated_units = Public.get('generated_units')

    if is_boss_biter then
        if not generated_units.boss_units then
            generated_units.boss_units = {}
        end

        generated_units.boss_units[#generated_units.boss_units + 1] = biter
    else
        generated_units.active_biters[biter.unit_number] = { entity = biter, spawn_tick = game.tick }
    end
    local active_biter_count = Public.get('active_biter_count')
    Public.set('active_biter_count', active_biter_count + 1)
    local active_biter_threat = Public.get('active_biter_threat')
    Public.set('active_biter_threat', active_biter_threat + round(threat_values[name] * boosted_health, 2))
    return biter
end

local function spawn_worm(surface, position, is_boss_worm)
    local boosted_health = BiterHealthBooster.get('biter_health_boost')

    local name = Public.wave_defense_roll_worm_name()

    local old_position = position

    local enable_random_spawn_positions = Public.get('enable_random_spawn_positions')

    if enable_random_spawn_positions then
        if random(1, 3) == 1 then
            position = { x = (-1 * (position.x + random(1, 10))), y = (position.y + random(1, 10)) }
        else
            position = { x = (position.x + random(1, 10)), y = (position.y + random(1, 10)) }
        end
    end

    position = surface.find_non_colliding_position('steel-chest', position, 3, 1)
    if not position then
        position = old_position
    end

    local force = 'enemy'
    local es_settings = Public.get_es('settings')

    if es_settings.enabled then
        force = 'aggressors'
    end

    local worm = surface.create_entity({ name = name, position = position, force = force })
    local increase_health_per_wave = Public.get('increase_health_per_wave')
    local boost_units_when_wave_is_above = Public.get('boost_units_when_wave_is_above')
    local boost_bosses_when_wave_is_above = Public.get('boost_bosses_when_wave_is_above')
    local wave_number = Public.get('wave_number')

    if (increase_health_per_wave and (wave_number >= boost_units_when_wave_is_above)) and not is_boss_worm then
        local modified_unit_health = Public.get('modified_unit_health')
        local unit_settings = Public.get('unit_settings')
        local final_health = round(modified_unit_health.current_value * unit_settings.scale_worms_by_health[worm.name], 3)
        if final_health < 1 then
            final_health = 1
        end
        Public.debug_print_health('final_health - unit: ' .. worm.name .. ' with h-m: ' .. final_health)
        BiterHealthBooster.add_unit(worm, final_health)
    end

    if is_boss_worm then
        if (wave_number >= boost_bosses_when_wave_is_above) then
            local increase_boss_health_per_wave = Public.get('increase_boss_health_per_wave')
            if increase_boss_health_per_wave then
                local modified_boss_unit_health = Public.get('modified_boss_unit_health')
                BiterHealthBooster.add_boss_unit(worm, modified_boss_unit_health.current_value, 0.55)
            else
                local sum = boosted_health * 5
                BiterHealthBooster.add_boss_unit(worm, sum, 0.55)
            end
        else
            local sum = boosted_health * 5
            BiterHealthBooster.add_boss_unit(worm, sum, 0.55)
        end
    end

    return worm
end

local function increase_biter_damage(force)
    local increase_damage_per_wave = Public.get('increase_damage_per_wave')
    if not increase_damage_per_wave then
        return
    end

    local e = force or game.forces.enemy
    local new = Difficulty.get('value') * 0.04
    local melee = new
    local bio = new - 0.02
    local e_old_melee = e.get_ammo_damage_modifier('melee')
    local e_old_biological = e.get_ammo_damage_modifier('biological')
    local e_old_speed_melee = e.get_gun_speed_modifier('melee')
    local e_old_speed_biological = e.get_gun_speed_modifier('biological')

    Public.debug_print('Melee damage: ' .. melee + e_old_melee)
    Public.debug_print('Biological damage: ' .. bio + e_old_biological)

    Public.debug_print('Melee speed: ' .. melee + e_old_speed_melee)
    Public.debug_print('Biological speed: ' .. bio + e_old_speed_biological)

    e.set_ammo_damage_modifier('melee', melee + e_old_melee)
    e.set_ammo_damage_modifier('biological', bio + e_old_biological)

    e.set_gun_speed_modifier('melee', melee + e_old_speed_melee)
    e.set_gun_speed_modifier('biological', bio + e_old_speed_biological)
end

local function increase_biters_health()
    local increase_health_per_wave = Public.get('increase_health_per_wave')
    if not increase_health_per_wave then
        return
    end

    -- this sets normal units health
    local modified_unit_health = Public.get('modified_unit_health')
    if modified_unit_health.current_value > modified_unit_health.limit_value then
        modified_unit_health.current_value = modified_unit_health.limit_value
    end
    Public.debug_print_health('modified_unit_health.current_value: ' .. modified_unit_health.current_value)
    Public.set('modified_unit_health').current_value = modified_unit_health.current_value + modified_unit_health.health_increase_per_boss_wave

    -- this sets boss units health
    local modified_boss_unit_health = Public.get('modified_boss_unit_health')
    if modified_boss_unit_health.current_value > modified_boss_unit_health.limit_value then
        modified_boss_unit_health.current_value = modified_boss_unit_health.limit_value
    end
    Public.debug_print_health('modified_boss_unit_health.current_value: ' .. modified_boss_unit_health.current_value)
    Public.set('modified_boss_unit_health').current_value = modified_boss_unit_health.current_value + modified_boss_unit_health.health_increase_per_boss_wave
end

local function increase_unit_group_size()
    local increase_average_unit_group_size = Public.get('increase_average_unit_group_size')
    if not increase_average_unit_group_size then
        return
    end

    local boost_spawner_sizes_wave_is_above = Public.get('boost_spawner_sizes_wave_is_above')
    local wave_number = Public.get('wave_number')

    if (wave_number >= boost_spawner_sizes_wave_is_above) then
        local average_unit_group_size = Public.get('average_unit_group_size')
        local new_average_unit_group_size = average_unit_group_size + 1

        if new_average_unit_group_size > 128 then
            new_average_unit_group_size = 128
        end

        Public.set('average_unit_group_size', new_average_unit_group_size)
        Public.debug_print_health('average_unit_group_size - ' .. new_average_unit_group_size)
    end
end

local function set_multi_command()
    local surface_index = Public.get('surface_index')
    local surface = game.get_surface(surface_index)
    if not surface or not surface.valid then
        return
    end

    local target = Public.get('target')
    if not valid(target) then
        Event.raise(Public.events.on_primary_target_missing)
        return
    end

    surface.set_multi_command(
        {
            command =
            {
                type = defines.command.attack,
                target = target,
                distraction = defines.distraction.none
            },
            unit_count = 256,
            force = 'aggressors',
            unit_search_distance = 1024
        }
    )
end

local function increase_max_active_unit_groups()
    local _increase_max_active_unit_groups = Public.get('increase_max_active_unit_groups')
    if not _increase_max_active_unit_groups then
        return
    end

    local boost_spawner_sizes_wave_is_above = Public.get('boost_spawner_sizes_wave_is_above')
    local wave_number = Public.get('wave_number')

    if (wave_number >= boost_spawner_sizes_wave_is_above) then
        local max_active_unit_groups = Public.get('max_active_unit_groups')
        local new_max_active_unit_groups = max_active_unit_groups + 1

        if new_max_active_unit_groups > 64 then
            new_max_active_unit_groups = 64
        end

        Public.set('max_active_unit_groups', new_max_active_unit_groups)
        Public.debug_print_health('max_active_unit_groups - ' .. new_max_active_unit_groups)
    end
end

local function set_next_wave()
    local wave_number = Public.get('wave_number')
    Public.set('wave_number', wave_number + 1)
    wave_number = Public.get('wave_number')

    local event_data = {}

    local threat_gain_multiplier = Public.get('threat_gain_multiplier')
    local threat_gain = wave_number * threat_gain_multiplier

    if wave_number > 1000 then
        threat_gain = threat_gain * (wave_number * 0.001)
    end
    if wave_number % 50 == 0 then
        increase_unit_group_size()
    end
    if wave_number % 200 == 0 then
        increase_max_active_unit_groups()
    end

    event_data.wave_number = wave_number

    if wave_number % 50 == 0 then
        increase_biter_damage()
        increase_biters_health()
        Public.set('boss_wave', true)
        event_data.boss_wave = true
        Public.set('boss_wave_warning', true)
        local alert_boss_wave = Public.get('alert_boss_wave')
        local spawn_position = get_spawn_pos()
        if alert_boss_wave then
            local msg = 'Boss Wave: ' .. wave_number
            local pos =
            {
                position = spawn_position
            }
            Alert.alert_all_players_location(pos, msg, { r = 0.8, g = 0.1, b = 0.1 })
        end
        threat_gain = threat_gain * 2
    else
        local boss_wave_warning = Public.get('boss_wave_warning')
        if boss_wave_warning then
            Public.set('boss_wave_warning', false)
        end
    end

    local log_wave_to_discord = Public.get('log_wave_to_discord')

    if wave_number % 100 == 0 and log_wave_to_discord then
        Server.to_discord_embed('Current wave: ' .. wave_number)
    end

    local threat = Public.get('threat')
    Public.set('threat', threat + floor(threat_gain))

    local wave_enforced = Public.get('wave_enforced')
    local next_wave = Public.get('next_wave')
    local wave_interval = Public.get('wave_interval')
    event_data.next_wave = next_wave
    event_data.wave_interval = wave_interval
    event_data.threat_gain = threat_gain
    if not wave_enforced then
        Public.set('last_wave', next_wave)
        Public.set('next_wave', game.tick + wave_interval)
    end

    raise(Public.events.on_wave_created, event_data)
end

local function reform_group(group)
    local unit_group_command_step_length = Public.get('unit_group_command_step_length')
    local group_position = { x = group.position.x, y = group.position.y }
    local step_length = unit_group_command_step_length
    local generated_units = Public.get('generated_units')
    local position = group.surface.find_non_colliding_position('biter-spawner', group_position, step_length, 4)
    if position then
        local new_group = group.surface.create_unit_group { position = position, force = group.force }
        for _, biter in pairs(group.members) do
            new_group.add_member(biter)
        end
        Public.debug_print('Creating new unit group, because old one was stuck.')
        generated_units.unit_groups[new_group.unique_id] = new_group
        local unit_groups_size = Public.get('unit_groups_size')
        Public.set('unit_groups_size', unit_groups_size + 1)

        return new_group
    else
        Public.debug_print('Destroying stuck group.')
        if generated_units.unit_groups[group.unique_id] then
            if generated_units.unit_group_last_command[group.unique_id] then
                generated_units.unit_group_last_command[group.unique_id] = nil
            end
            local positions = generated_units.unit_group_pos.positions
            if positions[group.unique_id] then
                positions[group.unique_id] = nil
            end
            table.remove(generated_units.unit_groups, group.unique_id)
            local unit_groups_size = Public.get('unit_groups_size')
            Public.set('unit_groups_size', unit_groups_size - 1)
        end
        group.destroy()
    end
    return nil
end

local function get_side_targets(group)
    if not group then
        return
    end
    local unit_group_command_step_length = Public.get('unit_group_command_step_length')
    local search_side_targets = Public.get('search_side_targets')

    local commands = {}
    local group_position = { x = group.position.x, y = group.position.y }
    local step_length = unit_group_command_step_length

    local side_target = Public.get_side_target()
    if not side_target then
        return
    end
    local target_position = side_target.position
    local distance_to_target = floor(sqrt((target_position.x - group_position.x) ^ 2 + (target_position.y - group_position.y) ^ 2))
    local steps = floor(distance_to_target / step_length) + 1

    for _ = 1, steps, 1 do
        local old_position = group_position
        local obstacles =
            group.surface.find_entities_filtered
            {
                position = old_position,
                radius = step_length * 2,
                type = search_side_targets,
                limit = 100
            }
        if obstacles then
            for v = 1, #obstacles, 1 do
                if obstacles[v].valid then
                    commands[#commands + 1] =
                    {
                        type = defines.command.attack,
                        destination = obstacles[v].position,
                        distraction = defines.distraction.by_anything
                    }
                end
            end
        end

        commands[#commands + 1] =
        {
            type = defines.command.attack,
            target = side_target,
            distraction = defines.distraction.by_anything
        }
    end

    return commands
end

local function get_main_command(group)
    local unit_group_command_step_length = Public.get('unit_group_command_step_length')
    local commands = {}
    local group_position = { x = group.position.x, y = group.position.y }
    local step_length = unit_group_command_step_length

    local target = Public.get('target')
    if not valid(target) then
        Event.raise(Public.events.on_primary_target_missing)
        return
    end

    Public.debug_print('get_main_command - starting')

    local target_position = target.position
    local distance_to_target = floor(sqrt((target_position.x - group_position.x) ^ 2 + (target_position.y - group_position.y) ^ 2))
    local steps = floor(distance_to_target / step_length) + 1
    local vector =
    {
        round((target_position.x - group_position.x) / steps, 3),
        round((target_position.y - group_position.y) / steps, 3)
    }

    Public.debug_print('get_commmands - to main target x' .. target_position.x .. ' y' .. target_position.y)
    Public.debug_print('get_commmands - distance_to_target:' .. distance_to_target .. ' steps:' .. steps)
    Public.debug_print('get_commmands - vector ' .. vector[1] .. '_' .. vector[2])

    if Public.get('enable_side_target') then
        for _ = 1, steps, 1 do
            local old_position = group_position
            group_position.x = group_position.x + vector[1]
            group_position.y = group_position.y + vector[2]
            local obstacles =
                group.surface.find_entities_filtered
                {
                    position = old_position,
                    radius = step_length / 2,
                    type = { 'simple-entity', 'tree' },
                    limit = 50
                }
            if obstacles then
                shuffle_distance(obstacles, old_position)
                for ii = 1, #obstacles, 1 do
                    if obstacles[ii].valid then
                        commands[#commands + 1] =
                        {
                            type = defines.command.attack,
                            target = obstacles[ii],
                            distraction = defines.distraction.by_anything
                        }
                    end
                end
            end
            local position = group.surface.find_non_colliding_position('behemoth-biter', group_position, step_length, 1)
            if position then
                commands[#commands + 1] =
                {
                    type = defines.command.attack_area,
                    destination = { x = position.x, y = position.y },
                    radius = 16,
                    distraction = defines.distraction.by_anything
                }
            end
        end
    end

    commands[#commands + 1] =
    {
        type = defines.command.attack_area,
        destination = { x = target_position.x, y = target_position.y },
        radius = 8,
        distraction = defines.distraction.by_anything
    }

    commands[#commands + 1] =
    {
        type = defines.command.attack,
        target = target,
        distraction = defines.distraction.by_anything
    }

    return commands
end

local function command_to_main_target(group, bypass)
    if not valid(group) then
        return
    end
    local generated_units = Public.get('generated_units')
    local unit_group_command_delay = Public.get('unit_group_command_delay')
    if not bypass then
        if not generated_units.unit_group_last_command[group.unique_id] then
            generated_units.unit_group_last_command[group.unique_id] = game.tick - (unit_group_command_delay + 1)
        end

        if generated_units.unit_group_last_command[group.unique_id] then
            if generated_units.unit_group_last_command[group.unique_id] + unit_group_command_delay > game.tick then
                return
            end
        end
    end

    local fill_tiles_so_biter_can_path = Public.get('fill_tiles_so_biter_can_path')
    if fill_tiles_so_biter_can_path then
        fill_tiles(group, 10)
    end

    local tile = group.surface.get_tile(group.position)
    if tile.valid and tile.collides_with('player') then
        group = reform_group(group)
    end
    if not valid(group) then
        return
    end

    local commands = get_main_command(group)

    Public.debug_print('get_main_command - got commands')

    local surface_index = Public.get('surface_index')

    if group.surface.index ~= surface_index then
        return
    end

    group.set_command(
        {
            type = defines.command.compound,
            structure_type = defines.compound_command.return_last,
            commands = commands
        }
    )
    Public.debug_print('get_main_command - sent commands')
    if valid(group) then
        generated_units.unit_group_last_command[group.unique_id] = game.tick
    end
end

local function command_to_side_target(group)
    local generated_units = Public.get('generated_units')
    local unit_group_command_delay = Public.get('unit_group_command_delay')
    if not generated_units.unit_group_last_command[group.unique_id] then
        generated_units.unit_group_last_command[group.unique_id] = game.tick - (unit_group_command_delay + 1)
    end

    if generated_units.unit_group_last_command[group.unique_id] then
        if generated_units.unit_group_last_command[group.unique_id] + unit_group_command_delay > game.tick then
            return
        end
    end

    local tile = group.surface.get_tile(group.position)
    if tile.valid and tile.collides_with('player') then
        group = reform_group(group)
    end

    local commands = get_side_targets(group)
    if not commands then
        return
    end

    group.set_command(
        {
            type = defines.command.compound,
            structure_type = defines.compound_command.return_last,
            commands = commands
        }
    )

    generated_units.unit_group_last_command[group.unique_id] = game.tick
end

local function give_side_commands_to_group()
    local enable_side_target = Public.get('enable_side_target')
    if not enable_side_target then
        return
    end

    local target = Public.get('target')
    if not valid(target) then
        Event.raise(Public.events.on_primary_target_missing)
        return
    end

    local generated_units = Public.get('generated_units')
    for _, group in pairs(generated_units.unit_groups) do
        if type(group) ~= 'number' then
            if group.valid then
                command_to_side_target(group)
            else
                get_active_unit_groups_count()
            end
        end
    end
end

local function give_main_command_to_group()
    local target = Public.get('target')
    if not valid(target) then
        Event.raise(Public.events.on_primary_target_missing)
        return
    end

    -- This is called even if the target is valid
    Event.raise(Public.events.on_primary_target_missing)

    local generated_units = Public.get('generated_units')
    for _, group in pairs(generated_units.unit_groups) do
        if type(group) ~= 'number' then
            if group.valid then
                if group.surface.index == target.surface.index then
                    command_to_main_target(group)
                end
            else
                get_active_unit_groups_count()
            end
        end
    end
end

local function spawn_unit_group(fs, only_bosses)
    if fs then
        Public.debug_print('spawn_unit_group - forcing new biters')
    else
        if not can_units_spawn() then
            Public.debug_print('spawn_unit_group - Cant spawn units?')
            return
        end
    end
    local target = Public.get('target')
    if not valid(target) then
        Public.debug_print('spawn_unit_group - Target was not valid?')
        Event.raise(Public.events.on_primary_target_missing)
        return
    end

    local max_active_unit_groups = Public.get('max_active_unit_groups')
    if fs then
        Public.debug_print('spawn_unit_group - forcing new biters')
    else
        if get_active_unit_groups_count() >= max_active_unit_groups then
            Public.debug_print('spawn_unit_group - unit_groups at max')
            return
        end
    end
    local surface_index = Public.get('surface_index')
    local remove_entities = Public.get('remove_entities')

    local surface = game.surfaces[surface_index]
    set_group_spawn_position(surface)

    local spawn_position = get_spawn_pos()
    if not spawn_position then
        return
    end

    if check_if_near_target(spawn_position) then
        spawn_position = normalize_spawn_position()
        Public.debug_print('spawn_unit_group - cannot spawn unit group near target')
        if not spawn_position then
            Public.debug_print('spawn_unit_group - spawn_position was invalid')
            return
        end
    end

    local radius = 10
    local area =
    {
        left_top = { spawn_position.x - radius, spawn_position.y - radius },
        right_bottom = { spawn_position.x + radius, spawn_position.y + radius }
    }
    for _, v in pairs(surface.find_entities_filtered { area = area, name = 'land-mine' }) do
        if v and v.valid then
            Public.debug_print('spawn_unit_group - found land-mines')
            v.die()
        end
    end

    if remove_entities and not (fs and fs.bypass) then
        remove_trees({ surface = surface, position = spawn_position, valid = true })
        remove_rocks({ surface = surface, position = spawn_position, valid = true })
        fill_tiles({ surface = surface, position = spawn_position, valid = true })
    end

    local wave_number = Public.get('wave_number')
    Public.wave_defense_set_unit_raffle(wave_number)

    Public.debug_print('Spawning unit group at x' .. spawn_position.x .. ' y' .. spawn_position.y)

    local event_data = {}

    local es_settings = Public.get_es('settings')

    local force = 'enemy'
    if es_settings.enabled then
        force = 'aggressors'
    end

    local generated_units = Public.get('generated_units')

    local unit_group = surface.create_unit_group({ position = spawn_position, force = force }) --[[@as LuaCommandable]]

    event_data.unit_group = unit_group

    generated_units.unit_group_pos.index = generated_units.unit_group_pos.index + 1
    generated_units.unit_group_pos.positions[unit_group.unique_id] = { position = unit_group.position, index = 0 }
    local average_unit_group_size = Public.get('average_unit_group_size')
    local unit_settings = Public.get('unit_settings')
    event_data.unit_settings = unit_settings

    local group_size = floor(average_unit_group_size * Public.group_size_modifier_raffle[random(1, Public.group_size_modifier_raffle_size)])

    event_data.group_size = group_size
    event_data.boss_wave = false

    if not fs or not fs.random_bosses then
        local boss_wave = Public.get('boss_wave')
        if not boss_wave and not only_bosses then
            for _ = 1, group_size, 1 do
                local biter = spawn_biter(surface, spawn_position, fs, false, unit_settings)
                if not biter then
                    Public.debug_print('spawn_unit_group - No biters were found?')
                    break
                end
                unit_group.add_member(biter)

                raise(Public.events.on_entity_created, { entity = biter, boss_unit = false })
                -- command_to_side_target(unit_group)
            end
        end

        if boss_wave or only_bosses then
            event_data.boss_wave = true
            local count = random(1, floor(wave_number * 0.01) + 2)
            if count > 16 then
                count = 16
            end
            if count <= 4 then
                count = 4
            end
            event_data.spawn_count = count
            local only_spitters = random(1, 2) == 1
            if only_spitters then
                es_settings.only_spitters = true
            end
            if es_settings.wave_number ~= wave_number then
                es_settings.only_spitters = nil
            end
            es_settings.wave_number = wave_number
            for _ = 1, count, 1 do
                local biter = spawn_biter(surface, spawn_position, fs, true, unit_settings, es_settings.only_spitters)
                if not biter then
                    Public.debug_print('spawn_unit_group - No biter was found?')
                    break
                end
                unit_group.add_member(biter)
                raise(Public.events.on_entity_created, { entity = biter, boss_unit = true })
            end
            Public.set('boss_wave', false)
        end
    else
        if not es_settings.generated_units then
            es_settings.generated_units = 0
        end
        if es_settings.generated_units > 100 then
            return
        end

        local count = fs.scale or 1
        event_data.spawn_count = count
        for _ = 1, count, 1 do
            local biter = spawn_biter(surface, spawn_position, fs, true, unit_settings)
            if not biter then
                Public.debug_print('spawn_unit_group - No biter was found?')
                break
            end
            unit_group.add_member(biter)
            raise(Public.events.on_entity_created, { entity = biter, boss_unit = true })
        end
    end

    generated_units.unit_groups[unit_group.unique_id] = unit_group
    local unit_groups_size = Public.get('unit_groups_size')
    Public.set('unit_groups_size', unit_groups_size + 1)
    if random(1, 2) == 1 then
        Public.set('random_group', unit_group)
    end
    Public.set('spot', 'nil')
    raise(Public.events.on_unit_group_created, event_data)
    return true
end

local function spawn_unit_group_simple(fs)
    local target = Public.get('target')
    if not valid(target) then
        Public.debug_print('spawn_unit_group_simple - Target was not valid?')
        Event.raise(Public.events.on_primary_target_missing)
        return
    end

    local surface_index = Public.get('surface_index')

    local surface = game.surfaces[surface_index]
    local spawn_position = Public.get('spawn_position')
    if not spawn_position then
        return
    end

    local wave_number = Public.get('wave_number')
    Public.wave_defense_set_unit_raffle(wave_number)


    local es_settings = Public.get_es('settings')

    local force = 'enemy'
    if es_settings.enabled then
        force = 'aggressors'
    end

    local generated_units = Public.get('generated_units')

    local unit_group = surface.create_unit_group({ position = spawn_position, force = force })


    generated_units.unit_group_pos.index = generated_units.unit_group_pos.index + 1
    generated_units.unit_group_pos.positions[unit_group.unique_id] = { position = unit_group.position, index = 0 }
    local unit_settings = Public.get('unit_settings')

    if not es_settings.generated_units then
        es_settings.generated_units = 0
    end
    if es_settings.generated_units > 100 then
        return
    end

    local count = fs.scale or 1
    local s = 0
    for i = 1, count, 1 do
        local is_boss = i % 4 == 0
        local biter = spawn_biter(surface, spawn_position, fs, is_boss, unit_settings)
        if biter then
            s = s + 1
            unit_group.add_member(biter)
            raise(Public.events.on_entity_created, { entity = biter, boss_unit = is_boss })
        end
    end

    if s == 0 then
        Public.debug_print('spawn_unit_group - No biter was spawned?')
        return
    end

    generated_units.unit_groups[unit_group.unique_id] = unit_group
    local unit_groups_size = Public.get('unit_groups_size')
    Public.set('unit_groups_size', unit_groups_size + 1)
    if random(1, 2) == 1 then
        Public.set('random_group', unit_group)
    end
    return true
end

local function check_group_positions()
    local resolve_pathing = Public.get('resolve_pathing')
    if not resolve_pathing then
        return
    end

    local generated_units = Public.get('generated_units')
    local target = Public.get('target')
    if not valid(target) then
        Event.raise(Public.events.on_primary_target_missing)
        return
    end

    for _, group in pairs(generated_units.unit_groups) do
        if group.valid then
            local ugp = generated_units.unit_group_pos.positions
            if group.state == defines.group_state.finished then
                return command_to_main_target(group, true)
            end
            if ugp[group.unique_id] then
                local success = is_position_near(group.position, ugp[group.unique_id].position)
                if success then
                    ugp[group.unique_id].index = ugp[group.unique_id].index + 1
                    if ugp[group.unique_id].index >= 2 then
                        command_to_main_target(group, true)
                        fill_tiles(group, 30)
                        remove_rocks(group)
                        remove_trees(group)
                        if valid(group) and ugp[group.unique_id].index >= 4 then
                            generated_units.unit_group_pos.positions[group.unique_id] = nil
                            reform_group(group)
                        end
                    end
                end
            end
        end
    end
end

local function log_threat()
    local enable_threat_log = Public.get('enable_threat_log')
    if not enable_threat_log then
        return
    end

    local threat_log_index = Public.get('threat_log_index')
    Public.set('threat_log_index', threat_log_index + 1)
    local threat_log = Public.get('threat_log')
    local threat = Public.get('threat')
    threat_log_index = Public.get('threat_log_index')
    threat_log[threat_log_index] = threat
    if threat_log_index > 900 then
        threat_log[threat_log_index - 901] = nil
    end
end

if is_loaded_bool('maps.mountain_fortress_v3.table') then
    local Core = require 'maps.mountain_fortress_v3.core'

    check_if_near_target = function (position)
        local entity =
        {
            valid = true,
            position = position
        }
        local disable_spawn_near_target = Public.get('disable_spawn_near_target')

        if Core.is_around_train(entity) and disable_spawn_near_target then
            Public.debug_print('check_if_near_target  - cannot spawn inside locomotive aura')
            return true
        end

        return false
    end
else
    check_if_near_target = function ()
        return false
    end
end

local tick_tasks =
{
    [30] = set_main_target,
    [60] = set_enemy_evolution,
    [90] = check_group_positions,
    [120] = give_main_command_to_group,
    [150] = log_threat,
    [180] = Public.build_worm,
    [210] = Public.build_nest,
    [1600] = set_multi_command,
}

local tick_tasks_t2 =
{
    [1200] = give_side_commands_to_group,
    [3600] = time_out_biters,
    [7200] = refresh_active_unit_threat
}

Public.spawn_unit_group = spawn_unit_group

Event.on_nth_tick(30,
    function ()
        local tick = game.tick

        local t = tick % 300
        local t2 = tick % 18000

        if tick_tasks[t] then
            tick_tasks[t]()
        end

        if tick_tasks_t2[t2] then
            tick_tasks_t2[t2]()
        end

        local game_lost = Public.get('game_lost')
        if game_lost then
            return
        end
        local final_battle = Public.get('final_battle')
        if final_battle then
            return
        end

        local paused = Public.get('paused')
        if paused then
            local players = game.connected_players
            for _, player in pairs(players) do
                Public.update_gui(player)
            end
            return
        end

        local enable_grace_time = Public.get('enable_grace_time')
        if enable_grace_time and (not enable_grace_time.enabled) then
            if not enable_grace_time.set then
                Public.set('next_wave', game.tick + 100)
                enable_grace_time.set = true
            end
        end

        local next_wave = Public.get('next_wave')
        if tick > next_wave then
            set_next_wave()
        end



        local players = game.connected_players
        for _, player in pairs(players) do
            Public.update_gui(player)
        end
    end
)

Event.add(
    Public.events.on_biters_evolved,
    function (event)
        if not event then
            event = { force = game.forces.enemy }
        end

        increase_biter_damage(event.force)

        if event.health_increase then
            increase_biters_health()
        end
    end
)

Event.add(Public.events.on_spawn_unit_group, spawn_unit_group)
Event.add(Public.events.on_spawn_unit_group_simple, spawn_unit_group_simple)

Event.on_nth_tick(
    100,
    function ()
        local final_battle = Public.get('final_battle')
        if final_battle then
            return
        end


        local tick_to_spawn_unit_groups = Public.get('tick_to_spawn_unit_groups')
        local tick = game.tick
        local will_not_spawn = tick % tick_to_spawn_unit_groups ~= 0
        if will_not_spawn then
            return
        end

        local game_lost = Public.get('game_lost')
        if game_lost then
            return
        end

        local paused = Public.get('paused')
        if paused then
            return
        end

        spawn_unit_group()
    end
)

Public.set_next_wave = set_next_wave
Public.normalize_spawn_position = normalize_spawn_position
Public.check_if_near_target = check_if_near_target
Public.spawn_worm = spawn_worm
Public.set_main_target = set_main_target

return Public
