require 'modules.rocks_broken_paint_tiles'

local Event = require 'utils.event'
local Public = require 'maps.mountain_fortress_v3.table'
local Server = require 'utils.server'
local RPG = require 'modules.rpg.main'
local Collapse = require 'modules.collapse'
local Alert = require 'utils.alert'
local Task = require 'utils.task_token'
local Score = require 'utils.gui.score'
local Discord = require 'utils.discord'
local Core = require 'utils.core'
local Diff = require 'modules.difficulty_vote_by_amount'
local format_number = require 'util'.format_number
local RPG_Progression = require 'utils.datastore.rpg_data'
local WD = require 'modules.wave_defense.table'
local StatData = require 'utils.datastore.statistics'
StatData.add_normalize('coins', 'Coins collected'):set_tooltip('The amount of coins the player has collected through mining/killed enemies.')


local random = math.random
local floor = math.floor
local abs = math.abs
local round = math.round

-- Use these settings for live
local send_ping_to_channel = Discord.channel_names.mtn_channel
-- Use these settings for testing
-- bot-lounge
-- local send_ping_to_channel = Discord.channel_names.bot_quarters
-- dev
-- local send_ping_to_channel = Discord.channel_names.bot_quarters
-- local role_to_mention = Discord.role_mentions.test_role

local chests =
{
    'wooden-chest',
    'iron-chest',
    'steel-chest',
    'crash-site-chest-1',
    'crash-site-chest-2',
    'crash-site-spaceship-wreck-big-1',
    'crash-site-spaceship-wreck-big-2',
    'crash-site-spaceship-wreck-medium-1',
    'crash-site-spaceship-wreck-medium-2',
    'crash-site-spaceship-wreck-medium-3'
}

local valid_forces =
{
    [2] = true,
    [4] = true,
    [5] = true
}

local size_chests = #chests

local treasure_chest_messages =
{
    ({ 'entity.treasure_1' }),
    ({ 'entity.treasure_2' }),
    ({ 'entity.treasure_3' })
}

local rare_treasure_chest_messages =
{
    ({ 'entity.treasure_rare_1' }),
    ({ 'entity.treasure_rare_2' }),
    ({ 'entity.treasure_rare_3' })
}

local disabled_threats =
{
    ['entity-ghost'] = true,
    ['raw-fish'] = true
}

local defeated_messages =
{
    ({ 'entity.defeated_1' }),
    ({ 'entity.defeated_2' }),
    ({ 'entity.defeated_3' }),
    ({ 'entity.defeated_4' })
}

local protect_types =
{
    ['cargo-wagon'] = true,
    ['artillery-wagon'] = true,
    ['fluid-wagon'] = true,
    ['locomotive'] = true,
    ['reactor'] = true,
    ['spider-vehicle'] = true,
    ['car'] = true
}

local change_force_for_drills_token =
    Task.register(
        function (event)
            local entity = event.entity
            if not entity or not entity.valid then return end
            entity.force = 'bonus_drill'
        end
    )

local function get_random_weighted(player, weighted_table, item_index, weight_index)
    local total_weight = 0
    item_index = item_index or 1
    weight_index = weight_index or 2
    local debug_vars = Public.get('debug_vars')

    for _, w in pairs(weighted_table) do
        total_weight = total_weight + w[weight_index]
    end

    local index = random() * total_weight
    local weight_sum = 0
    for _, w in pairs(weighted_table) do
        weight_sum = weight_sum + w[weight_index]
        if weight_sum >= index then
            if debug_vars and debug_vars.enabled and debug_vars.vars then
                local vars = debug_vars.vars.mining_chance
                vars[player.name] = vars[player.name] or {}
                vars[player.name][w[3]] = vars[player.name][w[3]] or 0
                vars[player.name][w[3]] = vars[player.name][w[3]] + 1
            end
            return w[item_index]
        end
    end
end

local function on_entity_removed(data)
    local entity = data.entity
    local upgrades = Public.get('upgrades')

    local built =
    {
        ['land-mine'] = upgrades.landmine.built,
        ['flamethrower-turret'] = upgrades.flame_turret.built
    }

    local validator =
    {
        ['land-mine'] = 'landmine',
        ['flamethrower-turret'] = 'flame_turret'
    }

    local name = validator[entity.name]

    if built[entity.name] and entity.force.index == 1 then
        upgrades[name].built = upgrades[name].built - 1
        if upgrades[name].built <= 0 then
            upgrades[name].built = 0
        end
    end
end

local function check_health()
    local locomotive_health = Public.get('locomotive_health')
    local locomotive_max_health = Public.get('locomotive_max_health')
    local carriages = Public.get('carriages')
    if locomotive_health <= 0 then
        Public.set('locomotive_health', 0)
    end

    local m = locomotive_health / locomotive_max_health
    if carriages then
        for i = 1, #carriages do
            local entity = carriages[i]
            if not (entity and entity.valid) then
                return
            end
            if entity.type == 'locomotive' then
                entity.health = entity.max_health * m
            else
                entity.health = entity.max_health * m
            end
        end
    end
end

local function check_health_final_damage(final_damage_amount)
    local carriages = Public.get('carriages')
    if carriages then
        for i = 1, #carriages do
            local entity = carriages[i]
            if not (entity and entity.valid) then
                return
            end
            entity.health = entity.health + final_damage_amount
        end
    end
end

local function set_train_final_health(final_damage_amount, repair)
    if final_damage_amount == 0 then
        return
    end

    local locomotive = Public.get('locomotive')
    if not (locomotive and locomotive.valid) then
        return
    end

    local locomotive_health = Public.get('locomotive_health')
    local locomotive_max_health = Public.get('locomotive_max_health')

    if not repair then
        local poison_deployed = Public.get('poison_deployed')
        local robotics_deployed = Public.get('robotics_deployed')

        local lower_high = locomotive_max_health * 0.4
        local higher_high = locomotive_max_health * 0.5
        local lower_low = locomotive_max_health * 0.2
        local higher_low = locomotive_max_health * 0.3

        if locomotive_health >= lower_high and locomotive_health <= higher_high then
            if not poison_deployed then
                local carriages = Public.get('carriages')

                if WD.get('wave_number') <= 800 then
                    if carriages then
                        for i = 1, #carriages do
                            local entity = carriages[i]
                            Public.enable_poison_defense(entity.position)
                        end
                    end
                end

                local p =
                {
                    position = locomotive.position
                }
                local msg = ({ 'entity.train_taking_damage' })
                Alert.alert_all_players_location(p, msg)
                Public.set().poison_deployed = true
            end
        elseif locomotive_health >= lower_low and locomotive_health <= higher_low then
            if not robotics_deployed then
                local carriages = Public.get('carriages')

                if carriages then
                    for i = 1, #carriages do
                        local entity = carriages[i]
                        Public.enable_robotic_defense(entity.position)
                    end
                end
                local p =
                {
                    position = locomotive.position
                }
                local msg = ({ 'entity.train_taking_damage' })
                Alert.alert_all_players_location(p, msg)
                Public.set().robotics_deployed = true
            end
        elseif locomotive_health >= locomotive_max_health then
            Public.set().poison_deployed = false
        end
    end

    if locomotive_health <= 0 then
        Public.set('locomotive_position', locomotive.position)
        Public.game_is_over()
    end

    if locomotive_health <= 0 then
        check_health_final_damage(final_damage_amount)
        return
    end

    Public.set('locomotive_health', floor(locomotive_health - final_damage_amount))
    if locomotive_health > locomotive_max_health then
        Public.set('locomotive_health', locomotive_max_health)
    end
    locomotive_health = Public.get('locomotive_health')

    check_health()

    local health_text = Public.get('health_text')

    if health_text and health_text.valid then
        health_text.text = 'HP: ' .. round(locomotive_health) .. ' / ' .. round(locomotive_max_health)
    end
end

local function is_protected(e)
    local starting_planet = Public.get_planet()
    if string.sub(e.surface.name, 0, #starting_planet) ~= starting_planet then
        return true
    end

    if protect_types[e.type] then
        return true
    end

    return false
end

local function protect_entities(data)
    local cause = data.cause
    local entity = data.entity
    local force = data.force
    local dmg = data.final_damage_amount

    if not dmg then
        return
    end



    local check_heavy_damage = Public.get('check_heavy_damage')

    if check_heavy_damage then
        if (entity.type == 'simple-entity' or entity.type == 'simple-entity-with-owner') and dmg >= 500 then
            entity.health = entity.health + dmg
        end
    end

    if entity.force.index ~= 1 then
        return
    end

    if entity.position.x > 1000 then
        entity.health = entity.health + dmg
        return
    end

    local carriages_numbers = Public.get('carriages_numbers')
    if is_protected(entity) then
        if (cause and cause.valid) then
            if Public.valid_enemy_forces[cause.force.name] then
                if carriages_numbers and carriages_numbers[entity.unit_number] then
                    set_train_final_health(dmg, false)
                    return
                else
                    entity.health = entity.health - dmg
                    return
                end
            end
        elseif not (cause and cause.valid) then
            if force and Public.valid_enemy_forces[force.name] then
                if carriages_numbers and carriages_numbers[entity.unit_number] then
                    set_train_final_health(dmg, false)
                    return
                else
                    entity.health = entity.health - dmg
                    return
                end
            end
        end

        entity.health = entity.health + dmg
    end
end

local function hidden_treasure(player, entity)
    local rpg = RPG.get_value_from_player(player.index)
    if not rpg then
        return
    end

    local magic = rpg.magicka
    local magic_requirement = Public.get('magic_requirement')

    if magic >= magic_requirement then
        local msg = rare_treasure_chest_messages[random(1, #rare_treasure_chest_messages)]
        Alert.alert_player(player, 5, msg)
        Public.add_loot_rare(entity.surface, entity.position, 'wooden-chest', magic)
        return
    end
    local msg = treasure_chest_messages[random(1, #treasure_chest_messages)]
    Alert.alert_player(player, 5, msg, nil, nil, 0.3)
    Public.add_loot(entity.surface, entity.position, chests[random(1, size_chests)])
end

local function biters_chew_rocks_faster(data)
    local cause = data.cause
    local entity = data.entity
    local final_damage_amount = data.final_damage_amount

    if entity.force.index ~= 3 then
        return
    end --Neutral Force
    if not cause then
        return
    end
    if not cause.valid then
        return
    end
    if not valid_forces[cause.force.index] then
        return
    end --Enemy Force

    entity.health = entity.health - final_damage_amount * 7
end

local projectiles = { 'grenade', 'explosive-rocket', 'grenade', 'explosive-rocket', 'explosive-cannon-projectile' }

local function angry_tree(entity, cause, player)
    if entity.type ~= 'tree' then
        return
    end

    if random(1, 6) == 1 then
        Public.buried_biter(entity.surface, entity.position)
    end
    if random(1, 8) == 1 then
        Public.buried_worm(entity.surface, entity.position)
    end
    if random(1, 32) ~= 1 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
    if player then
        local forest_zone = RPG.get_value_from_player(player.index, 'forest_zone')
        if forest_zone and random(1, 32) == 1 then
            local cbl = Public.refill_turret_callback
            local data = { callback_data = Public.piercing_rounds_magazine_ammo }
            local e =
                entity.surface.create_entity(
                    {
                        name = 'gun-turret',
                        position = entity.position,
                        force = 'enemy'
                    }
                )
            if e.can_insert(Public.piercing_rounds_magazine_ammo) then
                e.insert(Public.piercing_rounds_magazine_ammo)
            end
            local callback = Task.get(cbl)
            callback(e, data)
            return
        end
    end

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

local function give_coin(player)
    local coin_amount = Public.get('coin_amount')
    local coin_override = Public.get('coin_override')
    local forest_zone = RPG.get_value_from_player(player.index, 'forest_zone')

    if forest_zone then
        if random(1, 3) ~= 1 then
            return
        end
    end

    if coin_amount >= 1 then
        if coin_override then
            player.insert({ name = 'coin', count = coin_override })
            StatData.get_data(player):increase('coins', coin_override)
        else
            ---@diagnostic disable-next-line: param-type-mismatch
            player.insert({ name = 'coin', count = random(1, coin_amount) })

            StatData.get_data(player):increase('coins', coin_amount)
        end
    end
end

local immunity_spawner =
    Task.register(
        function (data)
            local entity = data.entity
            if not entity or not entity.valid then
                return
            end
            entity.destructible = true
        end
    )

local unstuck_player_token =
    Task.register(
        function (data)
            local index = data.index
            if not index then
                return
            end
            local player = game.get_player(index)
            if not player or not player.valid then
                return
            end

            local surface = player.physical_surface
            local position = surface.find_non_colliding_position('character', player.physical_position, 32, 1)
            if not position then
                return
            end
            player.teleport(position, surface)
        end
    )

local mining_events =
{
    {
        function ()
        end,
        300000,
        'Nothing #1'
    },
    {
        function ()
        end,
        16384,
        'Nothing #2'
    },
    {
        function ()
        end,
        4096,
        'Nothing #3'
    },
    {
        function (entity)
            if Public.is_around_train(entity) then
                entity.destroy()
                return
            end

            Public.buried_biter(entity.surface, entity.position)
            entity.destroy()
        end,
        4096,
        'Angry Biter #1'
    },
    {
        function (entity)
            if Public.is_around_train(entity) then
                entity.destroy()
                return
            end

            Public.buried_biter(entity.surface, entity.position)
            entity.destroy()
        end,
        512,
        'Angry Biter #2'
    },
    {
        function (entity)
            if Public.is_around_train(entity) then
                entity.destroy()
                return
            end

            Public.buried_worm(entity.surface, entity.position)
            entity.destroy()
        end,
        2048,
        'Angry Worm #1'
    },
    {
        function (entity)
            if Public.is_around_train(entity) then
                entity.destroy()
                return
            end

            Public.tick_tack_trap(entity)
            entity.destroy()
        end,
        2048,
        'Dangerous Trap #1'
    },
    {
        function (entity, index)
            if Public.is_around_train(entity) then
                entity.destroy()
                return
            end

            local player = game.get_player(index)

            if entity.type == 'tree' then
                angry_tree(entity, player.character, player)
                entity.destroy()
            end
        end,
        1024,
        'Angry Tree #1'
    },
    {
        function (entity, index)
            local player = game.get_player(index)
            hidden_treasure(player, entity)
            Task.set_timeout_in_ticks(5, unstuck_player_token, { index = index })
        end,
        1024,
        'Treasure Tier #1'
    },
    {
        function (entity, index)
            local player = game.get_player(index)
            hidden_treasure(player, entity)
            Task.set_timeout_in_ticks(5, unstuck_player_token, { index = index })
        end,
        512,
        'Treasure Tier #2'
    },
    {
        function (entity, index)
            local player = game.get_player(index)
            hidden_treasure(player, entity)
            Task.set_timeout_in_ticks(5, unstuck_player_token, { index = index })
        end,
        256,
        'Treasure Tier #3'
    },
    {
        function (entity, index)
            local player = game.get_player(index)
            hidden_treasure(player, entity)
            Task.set_timeout_in_ticks(5, unstuck_player_token, { index = index })
        end,
        128,
        'Treasure Tier #4'
    },
    {
        function (entity, index)
            local player = game.get_player(index)
            hidden_treasure(player, entity)
            Task.set_timeout_in_ticks(5, unstuck_player_token, { index = index })
        end,
        64,
        'Treasure Tier #5'
    },
    {
        function (entity, index)
            local player = game.get_player(index)
            hidden_treasure(player, entity)
            Task.set_timeout_in_ticks(5, unstuck_player_token, { index = index })
        end,
        32,
        'Treasure Tier #6'
    },
    {
        function (entity, index)
            local player = game.get_player(index)
            hidden_treasure(player, entity)
            Task.set_timeout_in_ticks(5, unstuck_player_token, { index = index })
        end,
        16,
        'Treasure Tier #7'
    },
    {
        function (entity, index)
            if Public.is_around_train(entity) then
                entity.destroy()
                return
            end

            local ent_to_create = { 'biter-spawner', 'spitter-spawner' }

            local position = entity.position
            local surface = entity.surface
            local spawner_name = ent_to_create[random(1, #ent_to_create)]
            if
                surface.count_entities_filtered
                {
                    area = { { position.x - 40, position.y - 40 }, { position.x + 40, position.y + 40 } },
                    name = spawner_name,
                } <= 2
            then
                local e = surface.create_entity({ name = spawner_name, position = position, force = 'enemy' })

                e.destructible = false
                Task.set_timeout_in_ticks(300, immunity_spawner, { entity = e })
                Task.set_timeout_in_ticks(5, unstuck_player_token, { index = index })
            end
        end,
        512,
        'Nest #1'
    },
    {
        function (entity, index)
            if Public.is_around_train(entity) then
                entity.destroy()
                return
            end

            local ent_to_create = { 'biter-spawner', 'spitter-spawner' }

            local position = entity.position
            local surface = entity.surface
            local spawner_name = ent_to_create[random(1, #ent_to_create)]
            if
                surface.count_entities_filtered
                {
                    area = { { position.x - 40, position.y - 40 }, { position.x + 40, position.y + 40 } },
                    name = spawner_name,
                } <= 2
            then
                local e = surface.create_entity({ name = spawner_name, position = position, force = 'enemy' })

                e.destructible = false
                Task.set_timeout_in_ticks(300, immunity_spawner, { entity = e })
                Task.set_timeout_in_ticks(5, unstuck_player_token, { index = index })
            end
        end,
        512,
        'Nest #2'
    },
    {
        function (entity)
            local chest = 'crash-site-chest-' .. random(1, 2)
            local container = entity.surface.create_entity({ name = chest, position = entity.position, force = 'neutral' })
            if container and container.health then
                container.insert({ name = 'vehicle-machine-gun', count = 1 })
                container.health = random(1, container.health)
            end
        end,
        64,
        'VSMG #1'
    },
    {
        function (entity, index)
            local position = entity.position
            local surface = entity.surface

            local quality = 'normal'
            if random(1, 256) == 1 then
                quality = 'uncommon'
            elseif random(1, 512) == 1 then
                quality = 'rare'
            elseif random(1, 1024) == 1 then
                quality = 'epic'
            elseif random(1, 2048) == 1 then
                quality = 'legendary'
            end

            surface.create_entity({ name = 'car', position = position, force = 'player', quality = quality })
            Task.set_timeout_in_ticks(5, unstuck_player_token, { index = index })
            local player = game.players[index]
            local msg = ({ 'entity.found_car', player.name })
            Alert.alert_player(player, 15, msg)
        end,
        32,
        'Car #1'
    }
}

local function on_player_mined_entity(event)
    local entity = event.entity
    local player = game.get_player(event.player_index)
    if not player.valid then
        return
    end
    if not entity.valid then
        return
    end
    local current_task = Public.get('current_task')
    if not current_task.done then
        return
    end

    local rpg_char = RPG.get_value_from_player(player.index)
    if not rpg_char then return end

    local starting_planet = Public.get_planet()

    if string.sub(entity.surface.name, 0, #starting_planet) ~= starting_planet then
        return
    end

    local d =
    {
        entity = entity
    }

    on_entity_removed(d)

    if disabled_threats[entity.name] then
        return
    end

    local mined_scrap = Public.get('mined_scrap')

    if entity.type == 'simple-entity' or entity.type == 'simple-entity-with-owner' or entity.type == 'tree' then
        Public.set().mined_scrap = mined_scrap + 1
        Public.on_player_mined_entity(event)
        if entity.type == 'tree' then
            if random(1, 3) == 1 then
                give_coin(player)
            end
        elseif entity.type == 'simple-entity-with-owner' then
            if random(1, 6) == 1 then
                give_coin(player)
            end
        else
            give_coin(player)
        end
        if rpg_char.stone_path then
            local upgrades = Public.get('upgrades')
            if not upgrades.has_upgraded_tile_when_mining then
                entity.surface.set_tiles({ { name = 'stone-path', position = entity.position } }, true)
            else
                entity.surface.set_tiles({ { name = 'black-refined-concrete', position = entity.position } }, true)
            end
        end

        local func = get_random_weighted(player, mining_events)
        func(entity, player.index)
    end
end

local function on_robot_mined_entity(event)
    local entity = event.entity

    if not entity.valid then
        return
    end

    local current_task = Public.get('current_task')
    if not current_task.done then
        return
    end

    local starting_planet = Public.get_planet()
    if string.sub(entity.surface.name, 0, #starting_planet) ~= starting_planet then
        return
    end

    local d =
    {
        entity = entity
    }

    on_entity_removed(d)
end

local function get_damage(data)
    local entity = data.entity
    local original_damage_amount = data.original_damage_amount

    local damage = original_damage_amount + original_damage_amount * random(1, 100)
    if entity.prototype.resistances then
        if entity.prototype.resistances.physical then
            damage = damage - entity.prototype.resistances.physical.decrease
            damage = damage - damage * entity.prototype.resistances.physical.percent
        end
    end
    damage = round(damage, 3)
    if damage < 1 then
        damage = 1
    end
    return damage
end

local function kaboom(entity, target, damage)
    local base_vector = { target.position.x - entity.position.x, target.position.y - entity.position.y }

    local vector = { base_vector[1], base_vector[2] }
    vector[1] = vector[1] * 512
    vector[2] = vector[2] * 256

    local msg = { 'TASTY', 'MUNCH', 'SNACK_TIME', 'OVER 9000!' }

    entity.surface.create_entity(
        {
            name = 'compi-speech-bubble',
            position = { entity.position.x + base_vector[1] * 0.5, entity.position.y + base_vector[2] * 0.5 },
            text = msg[random(1, #msg)],
            source = entity,
            lifetime = 30
        }
    )

    if abs(vector[1]) > abs(vector[2]) then
        local d = abs(vector[1])
        if abs(vector[1]) > 0 then
            vector[1] = vector[1] / d
        end
        if abs(vector[2]) > 0 then
            vector[2] = vector[2] / d
        end
    else
        local d = abs(vector[2])
        if abs(vector[2]) > 0 then
            vector[2] = vector[2] / d
        end
        if abs(vector[1]) > 0 and d > 0 then
            vector[1] = vector[1] / d
        end
    end

    vector[1] = vector[1] * 1.6
    vector[2] = vector[2] * 1.6

    local a = 0.30

    for i = 1, 8, 1 do
        for x = i * -1 * a, i * a, 1 do
            for y = i * -1 * a, i * a, 1 do
                local p = { entity.position.x + x + vector[1] * i, entity.position.y + y + vector[2] * i }
                entity.surface.create_trivial_smoke({ name = 'fire-smoke', position = p })
                for _, e in pairs(entity.surface.find_entities({ { p[1] - a, p[2] - a }, { p[1] + a, p[2] + a } })) do
                    if e.valid then
                        if e.health then
                            if e.destructible and e.minable then
                                if e.force.index ~= entity.force.index then
                                    e.health = e.health - damage * 0.05
                                    if e.health <= 0 then
                                        e.die(e.force.name, entity)
                                    end
                                end
                            end
                        end
                    end
                end
            end
        end
    end
end

local function boss_puncher(data)
    local cause = data.cause
    local entity = data.entity

    if not cause then
        return
    end
    if not cause.valid then
        return
    end

    if cause.force.index ~= 2 then
        return
    end

    if entity.force.index ~= 1 then
        return
    end
    if not entity then
        return
    end
    if not entity.valid then
        return
    end

    if random(1, 10) == 1 then
        kaboom(cause, entity, get_damage(data))
    end
end

local function on_entity_damaged(event)
    local entity = event.entity

    if not (entity and entity.valid) then
        return
    end

    local cause = event.cause
    local force = event.force
    local final_damage_amount = event.final_damage_amount
    local original_damage_amount = event.original_damage_amount

    local wave_number = WD.get_wave()
    local boss_wave_warning = WD.get_alert_boss_wave()
    local munch_time = Public.get('munch_time')
    local final_battle = Public.get('final_battle')

    local data =
    {
        cause = cause,
        entity = entity,
        final_damage_amount = final_damage_amount,
        original_damage_amount = original_damage_amount,
        force = force,
        final_battle = final_battle
    }

    protect_entities(data)
    biters_chew_rocks_faster(data)

    if munch_time then
        if boss_wave_warning or wave_number >= 1000 then
            if random(0, 512) == 1 then
                boss_puncher(data)
            end
        end
    end
end

local function on_player_repaired_entity(event)
    if not event.entity then
        return
    end
    if not event.entity.valid then
        return
    end
    if not event.entity.health then
        return
    end
    local entity = event.entity
    local carriages_numbers = Public.get('carriages_numbers')
    local tick = game.tick

    if carriages_numbers[entity.unit_number] then
        local player = game.get_player(event.player_index)
        local rpg_t = RPG.get_value_from_player(player.index)
        local repair_speed

        if not rpg_t.mtn_repair then
            rpg_t.mtn_repair = 0
        end

        if rpg_t.mtn_repair > tick then
            repair_speed = 1
        else
            repair_speed = RPG.get_magicka(player)
        end

        if repair_speed <= 1 then
            if rpg_t.mtn_repair < tick then
                rpg_t.mtn_repair = tick + 10
            end
            set_train_final_health(-1, true)
            return
        else
            if rpg_t.mtn_repair < tick then
                rpg_t.mtn_repair = tick + 10
            end
            set_train_final_health(-repair_speed, true)
            return
        end
    end
end

local function on_entity_died(event)
    local entity = event.entity
    if not entity.valid then
        return
    end

    local unit_number = entity.unit_number

    local cause = event.cause
    local starting_planet = Public.get_planet()

    if string.sub(entity.surface.name, 0, #starting_planet) ~= starting_planet then
        return
    end

    local d =
    {
        entity = entity
    }

    local enemy_spawners = Public.get('enemy_spawners')
    if enemy_spawners.spawners[unit_number] then
        enemy_spawners.spawners[unit_number] = nil
    end

    on_entity_removed(d)

    local player
    local valid_enemy_forces = Public.valid_enemy_forces

    if cause then
        if cause.valid then
            if (cause and cause.name == 'character' and cause.player) then
                player = cause.player
            end
            if valid_enemy_forces[cause.force.name] or cause.force.index == 3 then
                entity.destroy()
                return
            end
        end
    end

    if disabled_threats[entity.name] then
        return
    end

    local biters_killed = Public.get('biters_killed')
    local biters = Public.get('biters')

    if entity.type == 'unit' or entity.type == 'unit-spawner' then
        Public.set().biters_killed = biters_killed + 1
        biters.amount = biters.amount - 1
        if biters.amount <= 0 then
            biters.amount = 0
        end
        if Public.is_around_train(entity) then
            return
        end
        if random(1, 512) == 1 then
            Public.tick_tack_trap(entity)
            return
        end
    end

    if entity.type == 'tree' then
        for _, e in pairs(
            entity.surface.find_entities_filtered(
                {
                    area =
                    {
                        { entity.position.x - 4, entity.position.y - 4 },
                        { entity.position.x + 4, entity.position.y + 4 }
                    },
                    name = 'fire-flame-on-tree'
                }
            )
        ) do
            if e.valid then
                e.destroy()
                return
            end
        end
        if Public.is_around_train(entity) then
            return
        end
        angry_tree(entity, cause, player)
        return
    end

    if entity.type == 'simple-entity' then
        if Public.is_around_train(entity) then
            entity.destroy()
            return
        end
        if random(1, 32) == 1 then
            Public.buried_biter(entity.surface, entity.position, 6)
            entity.destroy()
            return
        end
        if random(1, 64) == 1 then
            Public.buried_worm(entity.surface, entity.position)
            entity.destroy()
            return
        end
        if random(1, 512) == 1 then
            Public.tick_tack_trap(entity)
            return
        end
        entity.destroy()
        return
    end
end

local function get_sorted_list(column_name, score_list)
    for _ = 1, #score_list, 1 do
        for y = 1, #score_list, 1 do
            if not score_list[y + 1] then
                break
            end
            if score_list[y][column_name] < score_list[y + 1][column_name] then
                local key = score_list[y]
                score_list[y] = score_list[y + 1]
                score_list[y + 1] = key
            end
        end
    end
    return score_list
end

local function get_mvps(force)
    local get_score = Score.get_table().score_table
    if not get_score[force] then
        return false
    end
    local score = get_score[force]
    local score_list = {}
    for _, p in pairs(game.players) do
        if score.players[p.name] then
            local killscore = 0
            if score.players[p.name].killscore then
                killscore = score.players[p.name].killscore
            end
            local built_entities = 0
            if score.players[p.name].built_entities then
                built_entities = score.players[p.name].built_entities
            end
            local mined_entities = 0
            if score.players[p.name].mined_entities then
                mined_entities = score.players[p.name].mined_entities
            end
            table.insert(score_list, { name = p.name, killscore = killscore, built_entities = built_entities, mined_entities = mined_entities })
        end
    end
    local mvp = {}
    if not score_list[1] or not score_list[1].name then return mvp end
    score_list = get_sorted_list('killscore', score_list)
    mvp.killscore = { name = score_list[1].name, score = score_list[1].killscore }
    score_list = get_sorted_list('mined_entities', score_list)
    mvp.mined_entities = { name = score_list[1].name, score = score_list[1].mined_entities }
    score_list = get_sorted_list('built_entities', score_list)
    mvp.built_entities = { name = score_list[1].name, score = score_list[1].built_entities }
    return mvp
end

local function show_mvps_ingame(player, mvp)
    local get_score = Score.get_table().score_table
    local wave_defense_table = WD.get_table()
    if not get_score then
        return
    end
    if player.gui.left['mvps'] then
        return
    end
    local frame = player.gui.left.add({ type = 'frame', name = 'mvps', direction = 'vertical' })
    local l = frame.add({ type = 'label', caption = 'MVPs:' })
    l.style.font = 'default-listbox'
    l.style.font_color = { r = 0.55, g = 0.55, b = 0.99 }

    local t = frame.add({ type = 'table', column_count = 2 })
    if mvp then
        local wave_defense = t.add({ type = 'label', caption = 'Highest Wave >> ' })
        wave_defense.style.font = 'default-listbox'
        wave_defense.style.font_color = { r = 0.22, g = 0.77, b = 0.44 }
        local wave_defense_text = t.add({ type = 'label', caption = 'This rounds highest wave was: ' .. wave_defense_table.wave_number })
        wave_defense_text.style.font = 'default-bold'
        wave_defense_text.style.font_color = { r = 0.33, g = 0.66, b = 0.9 }

        if mvp.killscore then
            local fighter_label = t.add({ type = 'label', caption = 'Fighter >> ' })
            fighter_label.style.font = 'default-listbox'
            fighter_label.style.font_color = { r = 0.22, g = 0.77, b = 0.44 }
            local fighter_label_text = t.add({ type = 'label', caption = mvp.killscore.name .. ' with a killing score of ' .. mvp.killscore.score .. ' kills!' })
            fighter_label_text.style.font = 'default-bold'
            fighter_label_text.style.font_color = { r = 0.33, g = 0.66, b = 0.9 }
        end

        if mvp.built_entities then
            local builder_label = t.add({ type = 'label', caption = 'Builder >> ' })
            builder_label.style.font = 'default-listbox'
            builder_label.style.font_color = { r = 0.22, g = 0.77, b = 0.44 }
            local builder_label_text = t.add({ type = 'label', caption = mvp.built_entities.name .. ' built ' .. mvp.built_entities.score .. ' things!' })
            builder_label_text.style.font = 'default-bold'
            builder_label_text.style.font_color = { r = 0.33, g = 0.66, b = 0.9 }
        end

        if mvp.mined_entities then
            local miners_label = t.add({ type = 'label', caption = 'Miners >> ' })
            miners_label.style.font = 'default-listbox'
            miners_label.style.font_color = { r = 0.22, g = 0.77, b = 0.44 }
            local miners_label_text = t.add({ type = 'label', caption = mvp.mined_entities.name .. ' mined a total of  ' .. mvp.mined_entities.score .. ' entities!' })
            miners_label_text.style.font = 'default-bold'
            miners_label_text.style.font_color = { r = 0.33, g = 0.66, b = 0.9 }
        end
    end
end

local function save_game_before_end()
    local wave = WD.get_wave()
    local date = Server.get_start_time()
    game.server_save('Final_Mtn_v3_' .. tostring(date) .. '_wave' .. tostring(wave))
end

local function check_rpg_progression()
    if Public.get('prestige_system_enabled') then
        RPG_Progression.save_all_players()
    end
end

local function post_mvp_to_discord(mvp)
    if not mvp then
        mvp = get_mvps('player')
    end

    if mvp then
        local server_name_matches = Server.check_server_name(Public.discord_name)
        local message =
        {
            title = 'MVPs',
            description = 'Player statistics is below',
            color = 'success',
            fields = {}
        }
        if mvp.killscore then
            message.fields[#message.fields + 1] =
            {
                title = 'MVP Fighter:',
                description = mvp.killscore.name .. ' with a killing score of ' .. mvp.killscore.score .. ' kills!',
                inline = 'false'
            }
        end
        if mvp.built_entities then
            message.fields[#message.fields + 1] =
            {
                title = 'MVP Builder:',
                description = mvp.built_entities.name .. ' built ' .. mvp.built_entities.score .. ' things!',
                inline = 'false'
            }
        end
        if mvp.mined_entities then
            message.fields[#message.fields + 1] =
            {
                title = 'MVP Miners:',
                description = mvp.mined_entities.name .. ' mined a total of ' .. mvp.mined_entities.score .. ' entities!',
                inline = 'false'
            }
        end

        local wave = WD.get_wave()
        if wave >= 100 then
            if server_name_matches then
                Server.to_discord_named_parsed_embed(send_ping_to_channel, message)
            else
                Server.to_discord_embed_parsed(message)
            end
        else
            Server.output_script_data('Wave is less than 100, not sending MVPs to discord.')
        end
    end
end

local function notify_game_lost_to_discord(mvp)
    if mvp then
        local sent_to_discord = Public.get('sent_to_discord')
        local server_name_matches = Server.check_server_name(Public.discord_name)

        if not sent_to_discord then
            local wave = WD.get_wave()
            local threat = WD.get('threat')
            local collapse_speed = Collapse.get_speed()
            local collapse_amount = Collapse.get_amount()

            local diff = Diff.get()
            if not diff then
                return
            end
            local time_played = Core.format_time(game.ticks_played)
            local total_players = #game.players
            local total_connected_players = #game.connected_players
            local pickaxe_upgrades = Public.pickaxe_upgrades
            local upgrades = Public.get('upgrades')
            local pick_tier = pickaxe_upgrades[upgrades.pickaxe_tier]

            local stateful = Public.get_stateful()
            local text =
            {
                title = 'Game lost!',
                description = 'Game statistics from the game is below',
                color = 'failure',
                fields =
                {

                    {
                        title = 'Time played:',
                        description = time_played,
                        inline = 'false'
                    },
                    {
                        title = 'Season:',
                        description = stateful.season,
                        inline = 'false'
                    },
                    {
                        title = 'Rounds survived:',
                        description = stateful.rounds_survived,
                        inline = 'false'
                    },
                    {
                        title = 'Best winning streak:',
                        description = stateful.best_streak,
                        inline = 'false'
                    },
                    {
                        title = 'Highest wave:',
                        description = format_number(wave, true),
                        inline = 'false'
                    },
                    {
                        title = 'Total connected players:',
                        description = total_players,
                        inline = 'false'
                    },
                    {
                        title = 'Threat:',
                        ---@descriptioniagnostic disable-next-line: param-type-mismatch
                        text2 = format_number(threat, true),
                        inline = 'false'
                    },
                    {
                        title = 'Pickaxe Upgrade:',
                        description = pick_tier .. ' (' .. upgrades.pickaxe_tier .. ')',
                        inline = 'false'
                    },
                    {
                        title = 'Collapse Speed:',
                        description = collapse_speed,
                        inline = 'false'
                    },
                    {
                        title = 'Collapse Amount:',
                        description = collapse_amount,
                        inline = 'false'
                    },
                    {
                        title = 'Connected players:',
                        description = total_connected_players,
                        inline = 'false'
                    }
                }
            }
            if server_name_matches then
                if wave >= 500 then
                    Server.to_discord_named_parsed_embed(send_ping_to_channel, text)
                end
            else
                Server.to_discord_embed_parsed(text)
            end

            Public.set('sent_to_discord', true)
        end
    end
end


function Public.game_is_over()
    Public.set('game_lost', true)
    Public.loco_died()
end

function Public.unstuck_player(index)
    if not index then
        return
    end
    local player = game.get_player(index)
    if not player or not player.valid then
        return
    end

    local surface = player.physical_surface
    local position = surface.find_non_colliding_position('character', player.physical_position, 32, 1)
    if not position then
        return
    end
    player.teleport(position, surface)
end

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

    if not Public.is_task_done() then return end

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

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

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

    local surface = game.surfaces[active_surface_index]

    Collapse.start_now(false, true)

    Public.set_stateful('current_streak', 0)

    local this = Public.get()

    this.locomotive_health = 0
    if this.health_text and this.health_text.valid and this.locomotive and this.locomotive.valid then
        this.locomotive.color = { 0.49, 0, 255, 1 }
        this.health_text.text = 'HP: ' .. round(this.locomotive_health) .. ' / ' .. round(this.locomotive_max_health)
    end
    WD.set('game_lost', true)
    WD.set('target', nil)
    local msg = defeated_messages[random(1, #defeated_messages)]

    local p = this.locomotive_position or { x = 0, y = 0 }

    local pos =
    {
        position = p,
    }
    Alert.alert_all_players_location(pos, msg)
    game.forces.enemy.set_friend('player', true)
    game.forces.aggressors.set_friend('player', true)
    game.forces.aggressors_frenzy.set_friend('player', true)

    game.forces.player.set_friend('enemy', true)
    game.forces.player.set_friend('aggressors', true)
    game.forces.player.set_friend('aggressors_frenzy', true)

    local fake_shooter = surface.create_entity({ name = 'character', position = p, force = 'enemy' })
    surface.create_entity(
        {
            name = 'atomic-rocket',
            position = p,
            force = 'enemy',
            speed = 1,
            max_range = 1200,
            target = p,
            source = fake_shooter
        }
    )

    surface.spill_item_stack({ position = p, stack = { name = 'coin', count = 512, quality = 'normal' } })
    this.game_reset_tick = 5400
    local mvp = get_mvps('player')

    save_game_before_end()
    check_rpg_progression()

    Public.set_stateful('current_streak', 0)

    if not Public.get('game_won') then
        notify_game_lost_to_discord(mvp)
        post_mvp_to_discord(mvp)
    end

    Core.iter_connected_players(function (player)
        player.play_sound { path = 'utility/game_lost', volume_modifier = 0.75 }
        show_mvps_ingame(player, mvp)
    end)

    Public.set('notified_game_over', true)
end

local function on_entity_spawned(event)
    local enemy_spawners = Public.get('enemy_spawners')
    if not enemy_spawners or not enemy_spawners.enabled then
        return
    end

    local spawner = event.spawner
    if not spawner or not spawner.valid then
        return
    end

    local unit_number = spawner.unit_number
    if not enemy_spawners.spawners[unit_number] then
        enemy_spawners.spawners[unit_number] =
        {
            entity = spawner,
            count = 0
        }
    end
end

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

    local starting_planet = Public.get_planet()

    if string.sub(entity.surface.name, 0, #starting_planet) == Public.init_name then
        entity.destroy()
        return
    end

    if string.sub(entity.surface.name, 0, #starting_planet) ~= starting_planet then
        return
    end

    local position = entity.position
    local player = game.get_player(event.player_index)

    if entity.name == 'radar' then
        if entity.surface.count_entities_filtered({ type = 'radar', position = position, radius = 64 }) > 1 then
            player.create_local_flying_text(
                {
                    position = entity.position,
                    text = ({ 'entity.radar_limit' }),
                    color = { 255, 0, 0 },
                }
            )

            player.physical_surface.spill_item_stack({ position = position, stack = { name = entity.name, count = 1, quality = 'normal' } })

            entity.destroy()
            return
        end
    end

    local valid_drills =
    {
        ['burner-mining-drill'] = true,
        ['electric-mining-drill'] = true
    }

    if valid_drills[entity.name] then
        Task.set_timeout_in_ticks(30, change_force_for_drills_token, { entity = entity })
        return
    end

    local upgrades = Public.get('upgrades')

    local upg = upgrades
    local built =
    {
        ['land-mine'] = upg.landmine.built,
        ['flamethrower-turret'] = upg.flame_turret.built
    }

    local limit =
    {
        ['land-mine'] = upg.landmine.limit,
        ['flamethrower-turret'] = upg.flame_turret.limit
    }

    local validator =
    {
        ['land-mine'] = 'landmine',
        ['flamethrower-turret'] = 'flame_turret'
    }

    local name = validator[entity.name]

    if built[entity.name] and entity.force.index == 1 then
        if built[entity.name] < limit[entity.name] then
            upgrades[name].built = built[entity.name] + 1
            upgrades.unit_number[name][entity] = entity
            upgrades.showed_text = false

            player.create_local_flying_text(
                {
                    position = entity.position,
                    text = upgrades[name].built .. ' / ' .. limit[entity.name] .. ' ' .. entity.name,
                    color = { r = 0.82, g = 0.11, b = 0.11 },
                }
            )
        else
            if not upgrades.showed_text then
                player.create_local_flying_text(
                    {
                        position = entity.position,
                        text = ({ 'entity.entity_limit_reached', entity.name }),
                        color = { r = 0.82, g = 0.11, b = 0.11 },
                    }
                )

                upgrades.showed_text = true
            end
            player.insert({ name = entity.name, count = 1 })
            entity.destroy()
        end
    end
end

local function on_robot_built_entity(event)
    local entity = event.entity
    if not entity.valid then
        return
    end
    local starting_planet = Public.get_planet()
    if string.sub(entity.surface.name, 0, #starting_planet) ~= starting_planet then
        return
    end

    local position = entity.position

    if entity.name == 'radar' then
        if entity.surface.count_entities_filtered({ type = 'radar', position = position, radius = 64 }) > 1 then
            entity.surface.create_entity(
                {
                    name = 'compi-speech-bubble',
                    position = entity.position,
                    text = ({ 'entity.radar_limit' }),
                    source = entity,
                    lifetime = 30
                }
            )

            entity.surface.spill_item_stack({ position = entity.position, stack = { name = entity.name, count = 1 } })
            entity.destroy()
            return
        end
    end

    local valid_drills =
    {
        ['burner-mining-drill'] = true,
        ['electric-mining-drill'] = true
    }

    if valid_drills[entity.name] then
        Task.set_timeout_in_ticks(30, change_force_for_drills_token, { entity = entity })
        return
    end

    local upgrades = Public.get('upgrades')

    local upg = upgrades
    local surface = entity.surface

    local built =
    {
        ['land-mine'] = upg.landmine.built,
        ['flamethrower-turret'] = upg.flame_turret.built
    }

    local limit =
    {
        ['land-mine'] = upg.landmine.limit,
        ['flamethrower-turret'] = upg.flame_turret.limit
    }

    local validator =
    {
        ['land-mine'] = 'landmine',
        ['flamethrower-turret'] = 'flame_turret'
    }

    local name = validator[entity.name]

    if built[entity.name] and entity.force.index == 1 then
        if built[entity.name] < limit[entity.name] then
            upgrades[name].built = built[entity.name] + 1
            upgrades.unit_number[name][entity] = entity
            upgrades.showed_text = false

            surface.create_entity(
                {
                    name = 'compi-speech-bubble',
                    position = entity.position,
                    text = upgrades[name].built .. ' / ' .. limit[entity.name] .. ' ' .. entity.name,
                    source = entity,
                    lifetime = 30
                }
            )
        else
            if not upgrades.showed_text then
                surface.create_entity(
                    {
                        name = 'compi-speech-bubble',
                        position = entity.position,
                        text = ({ 'entity.entity_limit_reached', entity.name }),
                        source = entity,
                        lifetime = 30
                    }
                )

                upgrades.showed_text = true
            end
            local inventory = event.robot.get_inventory(defines.inventory.robot_cargo)
            inventory.insert({ name = entity.name, count = 1 })
            entity.destroy()
        end
    end
end

local on_player_or_robot_built_tile = function (event)
    local surface = game.surfaces[event.surface_index]
    local starting_planet = Public.get_planet()
    if string.sub(surface.name, 0, #starting_planet) ~= starting_planet then
        return
    end

    local tiles = event.tiles
    if not tiles then
        return
    end
    for _, v in pairs(tiles) do
        local old_tile = v.old_tile
        if old_tile.name == 'black-refined-concrete' then
            surface.set_tiles({ { name = 'black-refined-concrete', position = v.position } }, true)
        end
        if old_tile.name == 'blue-refined-concrete' then
            surface.set_tiles({ { name = 'blue-refined-concrete', position = v.position } }, true)
        end
        if old_tile.name == 'cyan-refined-concrete' then
            surface.set_tiles({ { name = 'cyan-refined-concrete', position = v.position } }, true)
        end
        if old_tile.name == 'hazard-concrete-right' then
            surface.set_tiles({ { name = 'hazard-concrete-right', position = v.position } }, true)
        end
        if old_tile.name == 'lab-dark-2' then
            surface.set_tiles({ { name = 'lab-dark-2', position = v.position } }, true)
        end
    end
end

Public.get_random_weighted = get_random_weighted
Public.post_mvp_to_discord = post_mvp_to_discord

Event.add_event_filter(defines.events.on_entity_damaged, { filter = 'final-damage-amount', comparison = '>', value = 0 })
Event.add(defines.events.on_entity_damaged, on_entity_damaged)
Event.add(defines.events.on_entity_spawned, on_entity_spawned)
Event.add(defines.events.on_player_repaired_entity, on_player_repaired_entity)
Event.add(defines.events.on_player_mined_entity, on_player_mined_entity)
Event.add(defines.events.on_robot_mined_entity, on_robot_mined_entity)
Event.add(defines.events.on_entity_died, on_entity_died)
Event.add(defines.events.on_built_entity, on_built_entity)
Event.add(defines.events.on_robot_built_entity, on_robot_built_entity)
Event.add(defines.events.on_player_built_tile, on_player_or_robot_built_tile)
Event.add(defines.events.on_robot_built_tile, on_player_or_robot_built_tile)

return Public
