local Global = require 'utils.global'
local Event = require 'utils.event'
local error = error
local format = string.format
local raise_event = script.raise_event

local score_metadata = {}

local memory_players = {}
local memory_global = {}

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

local Public = {}

Public.events = {
    -- Event {
    --     score_name = score_name
    --     player_index = player_index
    --  }
    on_player_score_changed = Event.generate_event_name('on_player_score_changed'),
    -- Event {
    --     score_name = score_name
    --  }
    on_global_score_changed = Event.generate_event_name('on_global_score_changed'),
}

local on_player_score_changed = Public.events.on_player_score_changed
local on_global_score_changed = Public.events.on_global_score_changed

---Register a specific score.
---
--- This function must be called in the control stage, i.e. not inside an event.
---
---@param name string
---@param locale_string table
---@param icon string
function Public.register(name, locale_string, icon)
    if _LIFECYCLE ~= _STAGE.control then
        error(format('You can only register score types in the control stage, i.e. not inside events. Tried setting "%s".', name), 2)
    end

    if score_metadata[name] then
        error(format('Trying to register score type for "%s" while it has already been registered.', name), 2)
    end

    local score = {
        name = name,
        icon = icon,
        locale_string = locale_string
    }

    score_metadata[name] = score

    return score
end

---Sets a setting to a specific value for a player.
---
---@param player_index number
---@param score_name string
---@param value number to subtract or add
function Public.change_for_player(player_index, score_name, value)
    if value == 0 then
        return
    end

    local setting = score_metadata[score_name]
    if not setting then
        if _DEBUG then
            error(format('Trying to change score "%s" while it has was never registered.', score_name), 2)
        end
        return
    end

    local player_score = memory_players[player_index]
    if not player_score then
        player_score = {}
        memory_players[player_index] = player_score
    end

    player_score[score_name] = (player_score[score_name] or 0) + value

    raise_event(on_player_score_changed, {
        score_name = score_name,
        player_index = player_index
    })
end

---Sets a setting to a specific value for a player.
---
---@param score_name string
---@param value number to subtract or add
function Public.change_for_global(score_name, value)
    if value == 0 then
        return
    end

    local setting = score_metadata[score_name]
    if not setting then
        if _DEBUG then
            error(format('Trying to change score "%s" while it has was never registered.', score_name), 2)
        end
        return
    end

    memory_global[score_name] = (memory_global[score_name] or 0) + value

    raise_event(on_global_score_changed, {
        score_name = score_name
    })
end

---Sets a setting to a specific value for a player.
---
---@param score_name string
---@param value number to subtract or add
function Public.set_for_global(score_name, value)
    if not value then
        value = 0
    end

    local setting = score_metadata[score_name]
    if not setting then
        if _DEBUG then
            error(format('Trying to set score "%s" while it has was never registered.', score_name), 2)
        end
        return
    end

    local previous_value = Public.get_for_global(score_name)
    if value == previous_value then
        return
    end

    memory_global[score_name] = value

    raise_event(on_global_score_changed, {
        score_name = score_name
    })
end

---Sets a setting to a specific value for a player.
---
---@param player_index number
---@param score_name string
---@param value number to subtract or add
function Public.set_for_player(player_index, score_name, value)
    if not value then
        value = 0
    end

    local setting = score_metadata[score_name]
    if not setting then
        if _DEBUG then
            error(format('Trying to change score "%s" while it has was never registered.', score_name), 2)
        end
        return
    end

    local player_score = memory_players[player_index]
    if not player_score then
        player_score = {}
        memory_players[player_index] = player_score
    end

    local previous_value = Public.get_for_player(player_index, score_name)
    if value == previous_value then
        return
    end

    player_score[score_name] = (player_score[score_name] or 0) + value

    raise_event(on_player_score_changed, {
        score_name = score_name,
        player_index = player_index
    })
end

--- Resets all scores
function Public.reset()
    for score_name, _ in pairs(memory_global) do
        Public.set_for_global(score_name, 0)
    end
    for player_index, player_memory in pairs(memory_players) do
        for score_name, _ in pairs(player_memory) do
            Public.set_for_player(player_index, score_name, 0)
        end
    end
end

---Returns the value for this player of a specific score.
---
---@param player_index number
---@param score_name string
function Public.get_for_player(player_index, score_name)
    local setting = score_metadata[score_name]
    if not setting then
        return nil
    end

    local player_scores = memory_players[player_index]
    if not player_scores then
        return 0
    end

    return player_scores[score_name] or 0
end

---Returns the value of the game score.
---
---@param score_name string
function Public.get_for_global(score_name)
    local setting = score_metadata[score_name]
    if not setting then
        return 0
    end

    return memory_global[score_name] or 0
end

---Returns all metadata merged with the values for this player for the given score names.
---
---@param player_index number
---@param score_names table
function Public.get_player_scores_with_metadata(player_index, score_names)
    local scores = {}
    local size = 0
    for i = 1, #score_names do
        local score_name = score_names[i]
        local metadata = score_metadata[score_name]
        if metadata then
            local player_scores = memory_players[player_index]
            if player_scores then
                size = size + 1
                scores[size] = {
                    name = metadata.name,
                    locale_string = metadata.locale_string,
                    icon = metadata.icon,
                    value = player_scores[score_name] or 0
                }
            end
        end
    end

    return scores
end

---Returns all metadata merged with the values of the global scores for the given score names.
---
---@param score_names table
function Public.get_global_scores_with_metadata(score_names)
    local scores = {}
    local size = 0
    for i = 1, #score_names do
        local score_name = score_names[i]
        local metadata = score_metadata[score_name]
        if metadata then
            size = size + 1
            scores[size] = {
                name = metadata.name,
                locale_string = metadata.locale_string,
                icon = metadata.icon,
                value = memory_global[score_name] or 0
            }
        end
    end

    return scores
end

---Returns the full score metadata, note that this is a reference, do not modify
---this data unless you know what you're doing!
function Public.get_score_metadata()
    return score_metadata
end

return Public
