--luacheck: ignore
local math_random = math.random
local Global = require 'utils.global'
local visuals_delay = 1800
local level_up_floating_text_color = { 0, 205, 0 }
local xp_floating_text_color = { 157, 157, 157 }
local experience_levels = { 0 }

local xp_t = {}
local last_built_entities = {}

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

local Public = {}

function Public.get_table()
    return xp_t
end

function Public.lost_xp(player, amount)
    xp_t[player.index].xp = xp_t[player.index].xp - amount
end

local xp_yield = {
    ['behemoth-biter'] = 16,
    ['behemoth-spitter'] = 16,
    ['behemoth-worm-turret'] = 64,
    ['big-biter'] = 8,
    ['big-spitter'] = 8,
    ['big-worm-turret'] = 48,
    ['biter-spawner'] = 64,
    ['character'] = 16,
    ['gun-turret'] = 8,
    ['laser-turret'] = 16,
    ['medium-biter'] = 4,
    ['medium-spitter'] = 4,
    ['medium-worm-turret'] = 32,
    ['small-biter'] = 1,
    ['small-spitter'] = 1,
    ['small-worm-turret'] = 16,
    ['spitter-spawner'] = 64
}

local function gain_xp(player, amount)
    if not player.force == 'spectator' then
        amount = math.round(amount, 2)
        xp_t[player.index].xp = xp_t[player.index].xp + amount
        xp_t[player.index].xp_since_last_floaty_text = xp_t[player.index].xp_since_last_floaty_text + amount
        if xp_t[player.index].last_floaty_text > game.tick then
            return
        end
        player.create_local_flying_text {
            text = '+' .. xp_t[player.index].xp_since_last_floaty_text .. ' xp',
            position = player.position,
            color = xp_floating_text_color,
            time_to_live = 120,
            speed = 2
        }
        xp_t[player.index].xp_since_last_floaty_text = 0
        xp_t[player.index].last_floaty_text = game.tick + visuals_delay
    end
end
function Public.xp_reset_player(player)
    if not player.character then
        player.set_controller({ type = defines.controllers.god })
        player.create_character()
    end
    xp_t[player.index] = {
        xp = 0,
        points_to_distribute = 0,
        last_floaty_text = visuals_delay,
        xp_since_last_floaty_text = 0,
        rotated_entity_delay = 0,
        last_mined_entity_position = { x = 0, y = 0 }
    }
end

function Public.xp_reset_all_players()
    for _, p in pairs(game.players) do
        rpg_t[p.index] = nil
    end
    for _, p in pairs(game.connected_players) do
        Public.xp_reset_player(p)
    end
end

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

    --Grant XP for hand placed land mines
    if event.entity.last_user then
        if event.entity.type == 'land-mine' then
            if event.cause then
                if event.cause.valid then
                    if event.cause.force.index == event.entity.force.index then
                        return
                    end
                end
            end
            gain_xp(event.entity.last_user, 1)
            return
        end
    end

    if not event.cause then
        return
    end
    if not event.cause.valid then
        return
    end
    if event.cause.force.index == event.entity.force.index then
        return
    end
    if not get_cause_player[event.cause.type] then
        return
    end

    local players = get_cause_player[event.cause.type](event.cause)
    if not players then
        return
    end
    if not players[1] then
        return
    end

    --Grant modified XP for health boosted units
    if storage.biter_health_boost then
        if event.entity.type == 'unit' then
            for _, player in pairs(players) do
                if xp_yield[event.entity.name] then
                    gain_xp(player, xp_yield[event.entity.name] * storage.biter_health_boost)
                else
                    gain_xp(player, 0.5 * storage.biter_health_boost)
                end
            end
            return
        end
    end

    --Grant normal XP
    for _, player in pairs(players) do
        if xp_yield[event.entity.name] then
            gain_xp(player, xp_yield[event.entity.name])
        else
            gain_xp(player, 0.5)
        end
    end
end
local function on_player_repaired_entity(event)
    if math_random(1, 4) ~= 1 then
        return
    end
    local player = game.players[event.player_index]
    if not player.character then
        return
    end
    gain_xp(player, 0.40)
end

local function on_player_rotated_entity(event)
    local player = game.players[event.player_index]
    if not player.character then
        return
    end
    if xp_t[player.index].rotated_entity_delay > game.tick then
        return
    end
    xp_t[player.index].rotated_entity_delay = game.tick + 20
    gain_xp(player, 0.20)
end

local function on_player_changed_position(event)
    if math_random(1, 64) ~= 1 then
        return
    end
    local player = game.players[event.player_index]
    if not player.character then
        return
    end
    if player.character.driving then
        return
    end
    gain_xp(player, 1.0)
end

local building_and_mining_blacklist = {
    ['tile-ghost'] = true,
    ['entity-ghost'] = true,
    ['item-entity'] = true
}

local function is_replaced_entity(entity)
    if not last_built_entities[entity.position.x .. '_' .. entity.position.y] then
        return
    end
    for key, tick in pairs(last_built_entities) do
        if tick < game.tick then
            last_built_entities[key] = nil
        end
    end
    return true
end

local function on_pre_player_mined_item(event)
    local entity = event.entity
    if not entity.valid then
        return
    end
    if building_and_mining_blacklist[entity.type] then
        return
    end

    local player = game.players[event.player_index]

    if is_replaced_entity(entity) then
        gain_xp(player, -0.1)
        return
    end

    if xp_t[player.index].last_mined_entity_position.x == event.entity.position.x and xp_t[player.index].last_mined_entity_position.y == event.entity.position.y then
        return
    end
    xp_t[player.index].last_mined_entity_position.x = entity.position.x
    xp_t[player.index].last_mined_entity_position.y = entity.position.y
    if entity.type == 'resource' then
        gain_xp(player, 0.5)
        return
    end
    if entity.force.name == 'neutral' then
        gain_xp(player, 1.5 + event.entity.max_health * 0.0035)
        return
    end
    gain_xp(player, 0.1 + event.entity.max_health * 0.0005)
end

local function on_built_entity(event)
    local created_entity = event.entity
    if not created_entity.valid then
        return
    end
    if building_and_mining_blacklist[created_entity.type] then
        return
    end
    last_built_entities[created_entity.position.x .. '_' .. created_entity.position.y] = game.tick + 1800
    local player = game.players[event.player_index]
    gain_xp(player, 0.1)
end

local function on_player_crafted_item(event)
    if not event.recipe.energy then
        return
    end
    local player = game.players[event.player_index]
    gain_xp(player, event.recipe.energy * 0.20)
end

local function on_player_respawned(event)
    local player = game.players[event.player_index]
    if not xp_t[player.index] then
        Public.xp_reset_player(player)
        return
    end
end

local function on_player_joined_game(event)
    local player = game.players[event.player_index]
    if not xp_t[player.index] then
        Public.xp_reset_player(player)
    end
end

local function on_init(event)
end

local event = require 'utils.event'
event.on_init(on_init)
event.add(defines.events.on_built_entity, on_built_entity)
event.add(defines.events.on_entity_damaged, on_entity_damaged)
event.add(defines.events.on_entity_died, on_entity_died)
event.add(defines.events.on_gui_click, on_gui_click)
event.add(defines.events.on_player_changed_position, on_player_changed_position)
event.add(defines.events.on_player_crafted_item, on_player_crafted_item)
event.add(defines.events.on_player_joined_game, on_player_joined_game)
event.add(defines.events.on_player_repaired_entity, on_player_repaired_entity)
event.add(defines.events.on_player_respawned, on_player_respawned)
event.add(defines.events.on_player_rotated_entity, on_player_rotated_entity)
event.add(defines.events.on_pre_player_mined_item, on_pre_player_mined_item)

return Public
