local Event = require 'utils.event'
local Global = require 'utils.global'
local Gui = require 'utils.gui'
local Task = require 'utils.task_token'
local Config = require 'utils.gui.config'
local CustomEvents = require 'utils.created_events'

local this =
{
    players = {},
    storage = {},
    activate_custom_buttons = false,
    bottom_quickbar_button = {}
}

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

--- Events generated by the bottom frame module.
-- @table events
-- @field bottom_quickbar_respawn_raise The event triggered when the bottom quickbar is respawned or raised.
-- @field bottom_quickbar_location_changed The event triggered when the location of the bottom quickbar is changed.
local Public = {}

local set_location
local destroy_frame
local remove_player
local get_player_data

local main_frame_name = Gui.uid_name()

local sections =
{
    [1] = 1,
    [2] = 1,
    [3] = 2,
    [4] = 2,
    [5] = 3,
    [6] = 3,
    [7] = 4,
    [8] = 4,
    [9] = 5,
    [10] = 5,
    [11] = 6,
    [12] = 6
}

Config.register_scenario_module(
    {
        id = "bottom_frame",
        admin_only = false,
        gui_rows = Config.register_token(
            function (player, frame)
                local switch_state

                local autostash = is_loaded('modules.autostash')
                if autostash then
                    switch_state = 'right'
                    local bottom_frame = Public.get_player_data(player)
                    if bottom_frame and bottom_frame.top then
                        switch_state = 'left'
                    end
                    Config.add_switch(frame, switch_state, 'top_location', 'Position - top', 'Toggle to select if you want the bottom buttons at the top or the bottom.')
                    frame.add({ type = 'line' })
                end

                switch_state = 'right'
                local bottom_frame = Public.get_player_data(player)
                if bottom_frame and bottom_frame.bottom_state == 'bottom_left' then
                    switch_state = 'left'
                end
                Config.add_switch(frame, switch_state, 'bottom_location', 'Position - bottom', 'Toggle to select if you want the bottom button on the left side or the right side.')

                frame.add({ type = 'line' })

                switch_state = 'right'
                if bottom_frame and bottom_frame.above then
                    switch_state = 'left'
                end
                Config.add_switch(frame, switch_state, 'middle_location', 'Position - middle', 'Toggle to select if you want the bottom button above the quickbar or the side of the quickbar.')

                frame.add({ type = 'line' })

                switch_state = 'right'
                if bottom_frame and bottom_frame.portable then
                    switch_state = 'left'
                end
                Config.add_switch(frame, switch_state, 'portable_button', 'Position - portable', 'Toggle to select if you want the bottom button to be portable or not.')
                frame.add({ type = 'line' })
            end),
        handlers =
        {
            ['top_location'] = Config.register_token(
                function (player, _)
                    local data = Public.get_player_data(player)
                    if data and data.state and not data.top then
                        Public.set_top(player, true)
                    else
                        Public.set_top(player, false)
                    end
                end),
            ['bottom_location'] = Config.register_token(
                function (player, event)
                    if event.element.switch_state == 'left' then
                        Public.set_location(player, 'bottom_left')
                    else
                        Public.set_location(player, 'bottom_right')
                    end
                end),
            ['middle_location'] = Config.register_token(
                function (player, event)
                    local data = Public.get_player_data(player)
                    if event.element.switch_state == 'left' then
                        data.above = true
                        data.portable = false
                    else
                        data.above = false
                        data.portable = false
                    end
                    if not data.bottom_state then
                        data.bottom_state = 'bottom_right'
                    end

                    Public.set_location(player, data.bottom_state)
                end),
            ['portable_button'] = Config.register_token(
                function (player, event)
                    local data = Public.get_player_data(player)
                    if event.element.switch_state == 'left' then
                        data.above = false
                        data.portable = true
                    else
                        data.portable = false
                        data.above = false
                    end

                    if not data.bottom_state then
                        data.bottom_state = 'bottom_right'
                    end

                    Public.set_location(player, data.bottom_state)
                end),
        }
    })

local check_bottom_buttons_token =
    Task.register(
        function (event)
            local player_index = event.player_index
            local player = game.get_player(player_index)
            if not player or not player.valid then
                return
            end

            local player_data, storage_data = get_player_data(player)
            if not player_data or not storage_data or not next(storage_data) then
                destroy_frame(player)
                remove_player(player.index)
                return
            end
        end
    )

remove_player = function (index)
    this.players[index] = nil
    this.storage[index] = nil
    this.bottom_quickbar_button[index] = nil
end

get_player_data = function (player, remove_user_data)
    if remove_user_data then
        this.players[player.index] = nil
        this.storage[player.index] = nil
        return
    end
    if not this.players[player.index] then
        this.players[player.index] =
        {
            state = 'bottom_right',
            section = {},
            direction = 'vertical',
            row_index = 1,
            row_selection = 1,
            row_selection_added = 1
        }
        this.storage[player.index] = {}
    end
    return this.players[player.index], this.storage[player.index]
end

--- Refreshes all inner frames for a given player
local function refresh_inner_frames(player)
    if not player or not player.valid then
        return
    end
    local player_data, storage_data = get_player_data(player)
    if not player_data or not storage_data or not player_data.frame or not player_data.frame.valid then
        return
    end

    local main_frame = player_data.frame

    local horizontal_flow = main_frame.add { type = 'flow', direction = 'horizontal' }
    horizontal_flow.style.horizontal_spacing = 0

    for row_index, row_index_data in pairs(storage_data) do
        if row_index_data and type(row_index_data) == 'table' then
            local section_row_index = player_data.section[row_index]
            local vertical_flow = horizontal_flow.add { type = 'flow', direction = 'vertical' }
            vertical_flow.style.vertical_spacing = 0

            if not section_row_index then
                player_data.section[row_index] = {}
                section_row_index = player_data.section[row_index]
            end

            if not section_row_index.inside_frame or not section_row_index.inside_frame.valid then
                section_row_index.inner_frame = vertical_flow
            end

            for row_selection, row_selection_data in pairs(row_index_data) do
                if section_row_index[row_selection] and section_row_index[row_selection].valid then
                    section_row_index[row_selection].destroy()
                end

                section_row_index[row_selection] =
                    section_row_index.inner_frame.add
                    {
                        type = 'sprite-button',
                        sprite = row_selection_data.sprite,
                        name = row_selection_data.name,
                        tooltip = row_selection_data.tooltip or '',
                        style = 'quick_bar_page_button'
                    }
            end
        end
    end
end

local refresh_inner_frames_token =
    Task.register(
        function (event)
            local player_index = event.player_index
            local player = game.get_player(player_index)
            if not player or not player.valid then
                return
            end

            refresh_inner_frames(player)
        end
    )

---Adds a new inner frame to the bottom frame
-- local BottomFrame = require 'utils.gui.bottom_frame'
-- BottomFrame.add_inner_frame({player = player, element_name = Gui.uid_name(), tooltip = 'Some tooltip', sprite = 'item/raw-fish' })
---@param data any
local function add_inner_frame(data)
    if not data then
        return
    end
    local player = data.player
    local element_name = data.element_name
    local tooltip = data.tooltip
    local sprite = data.sprite
    if not player or not player.valid then
        return error('Given player was not valid', 2)
    end
    if not element_name then -- the element_name to pick from the row_selection
        return error('Element name is missing', 2)
    end
    if not sprite then
        return error('Sprite is missing', 2)
    end

    local player_data, storage_data = get_player_data(player)
    if not player_data or not storage_data or not player_data.frame or not player_data.frame.valid then
        return
    end

    if player_data.row_index > 6 then
        return error('Having more than 6 rows is currently not supported.', 2)
    end

    local found = false
    for _, row_index_data in pairs(storage_data) do
        if row_index_data and type(row_index_data) == 'table' then
            for _, row_selection_data in pairs(row_index_data) do
                if row_selection_data and row_selection_data.name == element_name then
                    found = true
                end
            end
        end
    end

    if found then
        return
    end

    player_data.row_index = sections[player_data.row_selection_added]

    if not storage_data[player_data.row_index] then
        storage_data[player_data.row_index] = {}
    end

    local storage_data_section = storage_data[player_data.row_index]
    storage_data_section[player_data.row_selection] =
    {
        name = element_name,
        sprite = sprite,
        tooltip = tooltip
    }

    player_data.row_selection = player_data.row_selection + 1
    player_data.row_selection_added = player_data.row_selection_added + 1
    player_data.row_selection = player_data.row_selection > 2 and 1 or player_data.row_selection
    Task.priority_delay(2, refresh_inner_frames_token, { player_index = player.index })
end

local function get_frame_by_element_name(player, element_name)
    local player_data, storage_data = get_player_data(player)
    if not player_data or not storage_data or not player_data.frame or not player_data.frame.valid then
        return
    end

    for _, row_index_data in pairs(storage_data) do
        if row_index_data and type(row_index_data) == 'table' then
            for _, row_selection_data in pairs(row_index_data) do
                if row_selection_data and row_selection_data.name == element_name then
                    return row_selection_data
                end
            end
        end
    end
end

destroy_frame = function (player)
    local gui = player.gui
    local frame = gui.screen[main_frame_name]
    if frame and frame.valid then
        frame.destroy()
    end
end

--- Creates a new frame
---@param player LuaPlayer
---@param alignment string
---@param location table
---@param data any
---@return unknown
local function create_frame(player, alignment, location, data)
    local gui = player.gui
    local frame = gui.screen[main_frame_name]
    if frame and frame.valid then
        destroy_frame(player)
    end

    alignment = alignment or 'vertical'

    frame =
        player.gui.screen.add
        {
            type = 'frame',
            name = main_frame_name,
            direction = alignment
        }

    if data.visible ~= nil then
        if data.visible then
            frame.visible = true
        else
            frame.visible = false
        end
    end

    frame.style.padding = 3
    frame.style.top_padding = 4
    if alignment == 'vertical' then
        frame.style.minimal_height = 96
    end

    local inner_frame =
        frame.add
        {
            type = 'frame',
            direction = alignment
        }
    inner_frame.style = 'quick_bar_inner_panel'

    frame.location = location
    if data.portable then
        frame.caption = '•'
    end

    if data.top then
        frame.visible = false
    else
        frame.visible = true
    end

    data.frame = inner_frame
    data.parent = frame
    data.section = data.section or {}
    data.section_data = data.section_data or {}
    data.alignment = alignment

    Task.priority_delay(5, check_bottom_buttons_token, { player_index = player.index })

    return frame
end

set_location = function (player, state)
    local data = get_player_data(player)
    local alignment = 'vertical'

    local location
    local resolution = player.display_resolution
    local scale = player.display_scale

    state = state or data.state

    if state == 'bottom_left' then
        if data.above then
            location =
            {
                x = (resolution.width / 2) - ((259) * scale),
                y = (resolution.height - (-12 + (40 * 5) * scale))
            }
            alignment = 'horizontal'
        else
            location =
            {
                -- x = (resolution.width / 2) - ((54 + 528 - 44) * scale),
                x = (resolution.width / 2) - ((455 + (data.row_index * 40)) * scale),
                y = (resolution.height - (96 * scale))
            }
        end
        data.bottom_state = 'bottom_left'
    elseif state == 'bottom_right' then
        if data.above then
            location =
            {
                -- x = (resolution.width / 2) - ((-262 - (40 * t[data.row_index])) * scale),
                x = (resolution.width / 2) - ((-460 + (data.row_index * 40)) * scale),
                y = (resolution.height - (-12 + (40 * 5) * scale))
            }
            alignment = 'horizontal'
        else
            location =
            {
                x = (resolution.width / 2) - ((54 + -689) * scale),
                y = (resolution.height - (96 * scale))
            }
        end
        data.bottom_state = 'bottom_right'
    else
        location =
        {
            x = (resolution.width / 2) - ((54 + -528) * scale),
            y = (resolution.height - (96 * scale))
        }
    end

    Event.raise(CustomEvents.events.bottom_quickbar_location_changed, { player_index = player.index, data = data })

    data.state = state
    create_frame(player, alignment, location, data)
    refresh_inner_frames(player)
end

--- Sets then frame location of the given player
---@param player LuaPlayer?
---@param value boolean
local function set_top(player, value)
    local data = get_player_data(player)
    data.top = value or false
    Public.set_location(player, 'bottom_right')
end

--- Returns the current frame location of the given player
---@param player LuaPlayer
---@return table|nil
local function get_location(player)
    local data = get_player_data(player)
    return data and data.state or nil
end

--- Activates the custom buttons
---@param value boolean
function Public.activate_custom_buttons(value)
    this.activate_custom_buttons = value or false
end

--- Checks if custom buttons are enabled.
--- @return boolean: True if custom buttons are enabled, false otherwise.
function Public.is_custom_buttons_enabled()
    return this.activate_custom_buttons
end

--- Toggles the player frame.
--- @param player LuaPlayer: The player entity.
--- @param state boolean: The state to set for the player frame.
function Public.toggle_player_frame(player, state)
    local gui = player.gui
    local frame = gui.screen[main_frame_name]
    if frame and frame.valid then
        local data = get_player_data(player)
        if state then
            data.visible = true
            frame.visible = true
        else
            data.visible = false
            frame.visible = false
        end
    end
end

--- Returns the current frame of the given player
---@param player LuaPlayer
---@param section_name string
---@return table|boolean|nil
function Public.get_section(player, section_name)
    local data = get_player_data(player)
    local section = data.section
    if not section then
        return false
    end

    for _, section_tbl in pairs(section) do
        if not section_tbl or not next(section_tbl) then
            break
        end

        for _, section_data in pairs(section_tbl) do
            if section_data and section_data.valid and section_data.name == section_name then
                return section_data
            end
        end
    end
end

--- Retrieves the value associated with the specified key.
--- @param key any The key to retrieve the value for.
--- @return any The value associated with the key.
function Public.get(key)
    if key then
        return this[key]
    else
        return this
    end
end

--- Sets the value of a given key.
--- @param key any The key to set.
--- @param value any The value to set for the key.
function Public.set(key, value)
    if key and (value or value == false) then
        this[key] = value
        return this[key]
    elseif key then
        return this[key]
    else
        return this
    end
end

--- Resets the bottom frame.
function Public.reset()
    local players = game.players
    for i = 1, #players do
        local player = players[i]
        if player and player.valid then
            if not player.connected then
                this.players[player.index] = nil
                this.storage[player.index] = nil
            end
        end
    end
end

Event.add(
    defines.events.on_player_joined_game,
    function (event)
        if this.activate_custom_buttons then
            local player = game.get_player(event.player_index)
            local data = get_player_data(player)
            set_location(player, data.state)
        end
    end
)

Event.add(
    defines.events.on_player_display_resolution_changed,
    function (event)
        if this.activate_custom_buttons then
            local player = game.get_player(event.player_index)
            local data = get_player_data(player)
            set_location(player, data.state)
        end
    end
)

Event.add(
    defines.events.on_player_display_scale_changed,
    function (event)
        local player = game.get_player(event.player_index)
        if this.activate_custom_buttons then
            local data = get_player_data(player)
            set_location(player, data.state)
        end
    end
)

Event.add(
    defines.events.on_pre_player_left_game,
    function (event)
        local player = game.get_player(event.player_index)
        destroy_frame(player)
        if this.activate_custom_buttons then
            get_player_data(player, true)
        end
    end
)

Event.add(
    defines.events.on_player_left_game,
    function (event)
        local player = game.get_player(event.player_index)
        destroy_frame(player)
        if this.activate_custom_buttons then
            get_player_data(player, true)
        end
    end
)

Event.add(
    defines.events.on_pre_player_died,
    function (event)
        if this.activate_custom_buttons then
            local player = game.get_player(event.player_index)
            destroy_frame(player)
        end
    end
)

Event.add(
    defines.events.on_player_respawned,
    function (event)
        if this.activate_custom_buttons then
            local player = game.get_player(event.player_index)
            local data = get_player_data(player)
            set_location(player, data.state)
        end
    end
)

Event.add(
    defines.events.on_player_removed,
    function (event)
        remove_player(event.player_index)
    end
)

Event.add(
    CustomEvents.events.bottom_quickbar_respawn_raise,
    function (event)
        if not event or not event.player_index then
            return
        end

        if this.activate_custom_buttons then
            local player = game.get_player(event.player_index)
            local data = get_player_data(player)
            set_location(player, data.state)
        end
    end
)

Event.add(
    CustomEvents.events.bottom_quickbar_location_changed,
    function (event)
        if not event or not event.player_index then
            return
        end

        if this.activate_custom_buttons then
            local player = game.get_player(event.player_index)
            local data = get_player_data(player)
            if data.frame and data.frame.valid then
                if data.top then
                    data.frame.visible = false
                else
                    data.frame.visible = true
                end
            end
        end
    end
)

Public.main_frame_name = main_frame_name
Public.refresh_inner_frames = refresh_inner_frames
Public.get_player_data = get_player_data
Public.remove_player = remove_player
Public.set_location = set_location
Public.get_location = get_location
Public.set_top = set_top
Public.add_inner_frame = add_inner_frame
Public.get_frame_by_element_name = get_frame_by_element_name
Gui.screen_to_bypass(main_frame_name)

return Public
