local Gui = require "expcore.gui" --- @dep expcore.gui
local PlayerData = require 'expcore.player_data' --- @dep expcore.player_data

-- Used to store the state of the toolbar when a player leaves
local ToolbarState = PlayerData.Settings:combine('ToolbarState')
ToolbarState:set_metadata{
    stringify = function(value)
        local buttons, favourites = 0, 0
        for _, state in ipairs(value) do
            buttons = buttons + 1
            if state.favourite then
                favourites = favourites + 1
            end
        end
        return string.format("Buttons: %d, Favourites: %d", buttons, favourites)
    end
}

-- Styles used for sprite buttons
local button_size = 20
local Styles = {
    header = Gui.sprite_style(22),
    item = Gui.sprite_style(button_size)
}

--- Set the style of the fake toolbar element
local function copy_style(src, dst)
    dst.style = src.style.name
    dst.style.height = button_size
    dst.style.width = button_size
    dst.style.padding = -2
end

local toolbar_container, move_up, move_down, toggle_toolbar

--- Reorder the buttons relative to each other, this will update the datastore
local function move_toolbar_button(player, item, offset)
    local old_index = item.get_index_in_parent()
    local new_index = old_index + offset

    -- Ideally the following would all happen in on_update but this had too much latency
    -- Swap the position in the list
    local list = item.parent
    local other_item = list.children[new_index]
    list.swap_children(old_index, new_index)

    -- Swap the position in the top flow, offset by 1 because of settings button
    local top_flow = Gui.get_top_flow(player)
    top_flow.swap_children(old_index+1, new_index+1)

    -- Check if the element has a left element to move
    local element_define = Gui.defines[item.tags.top_element_uid]
    local other_define = Gui.defines[other_item.tags.top_element_uid]
    if element_define.left_flow_element and other_define.left_flow_element then
        local left_element = Gui.get_left_element(player, element_define.left_flow_element)
        local other_left_element = Gui.get_left_element(player, other_define.left_flow_element)
        local left_index = left_element.get_index_in_parent()
        local other_index = other_left_element.get_index_in_parent()
        left_element.parent.swap_children(left_index, other_index)
    end

    -- If we are moving in/out of first/last place we need to update the move buttons
    local last_index = #list.children
    if old_index == 1 then -- Moving out of index 1
        other_item.move[move_up.name].enabled = false
        item.move[move_up.name].enabled = true
    elseif new_index == 1 then -- Moving into index 1
        other_item.move[move_up.name].enabled = true
        item.move[move_up.name].enabled = false
    elseif old_index == last_index then -- Moving out of the last index
        other_item.move[move_down.name].enabled = false
        item.move[move_down.name].enabled = true
    elseif new_index == last_index then -- Moving into the last index
        other_item.move[move_down.name].enabled = true
        item.move[move_down.name].enabled = false
    end

    -- Update the datastore state
    ToolbarState:update(player, function(_, order)
        local tmp = order[old_index]
        order[old_index] = order[new_index]
        order[new_index] = tmp
    end)
end

--- Reorder the toolbar buttons
local function reorder_toolbar_menu(player)
    local frame = Gui.get_left_element(player, toolbar_container)
    local list = frame.container.scroll.list
    local order = ToolbarState:get(player)
    local last_index = #order

    -- Reorder the buttons
    for index, state in ipairs(order) do
        local element_define = Gui.defines[state.element_uid]

        -- Switch item order
        local item = list[element_define.name]
        list.swap_children(index, item.get_index_in_parent())

        -- Check if the player is allowed to see the button
        local allowed = element_define.authenticator
        if type(allowed) == 'function' then allowed = allowed(player) end

        -- Update the checkbox state and item visibility
        local toolbar_button = Gui.get_top_element(player, element_define)
        toolbar_button.visible = allowed and state.favourite or false
        item.checkbox.state = state.favourite

        -- Update the state if the move buttons
        item.move[move_up.name].enabled = index ~= 1
        item.move[move_down.name].enabled = index ~= last_index
    end

    -- Update the state of the toggle button
    local button = frame.container.header.alignment[toggle_toolbar.name]
    button.enabled = Gui.top_flow_has_visible_elements(player)
    button.toggled = Gui.get_top_flow(player).parent.visible
end

--- Resets the toolbar to its default state when pressed
-- @element reset_toolbar
local reset_toolbar =
Gui.element {
    type = "sprite-button",
    sprite = "utility/reset",
    style = "shortcut_bar_button_red",
    tooltip = {"toolbar.reset"},
    name = Gui.unique_static_name
}
:style(Gui.sprite_style(Styles.header.width, -1))
:on_click(function(player)
    ToolbarState:set(player, nil)
    Gui.toggle_top_flow(player, true)
    reorder_toolbar_menu(player)
end)

--- Replaces the default method for opening and closing the toolbar
-- @element toggle_toolbar
toggle_toolbar =
Gui.element {
    type = "sprite-button",
    sprite = "utility/bookmark",
    tooltip = {"toolbar.toggle"},
    style = "tool_button",
    auto_toggle = true,
    name = Gui.unique_static_name
}
:style(Styles.header)
:on_click(function(player, element)
    Gui.toggle_top_flow(player, element.toggled)
end)

--- Move an element up the list
-- @element move_up
move_up =
Gui.element {
    type = "sprite-button",
    sprite = "utility/speed_up",
    tooltip = {"toolbar.move-up"},
    name = Gui.unique_static_name
}
:style(Styles.item)
:on_click(function(player, element)
    local item = element.parent.parent
    move_toolbar_button(player, item, -1)
end)

--- Move an element down the list
-- @element move_down
move_down =
Gui.element {
    type = "sprite-button",
    sprite = "utility/speed_down",
    tooltip = {"toolbar.move-down"},
    name = Gui.unique_static_name
}
:style(Styles.item)
:on_click(function(player, element)
    local item = element.parent.parent
    move_toolbar_button(player, item, 1)
end)

--- A flow which represents one item in the toolbar list
-- @element toolbar_list_item
local toolbar_list_item =
Gui.element(function(definition, parent, element_define)
    local flow = parent.add {
        type = "frame",
        style = "shortcut_selection_row",
        name = element_define.name,
        tags = {
            top_element_uid = element_define.uid
        }
    }
    flow.style.horizontally_stretchable = true
    flow.style.vertical_align = "center"

    -- Add the button and the icon edit button
    local element = element_define(flow)
    local player = Gui.get_player_from_element(parent)
    local top_element = Gui.get_top_element(player, element_define)
    copy_style(top_element, element)

    -- Add the checkbox that can toggle the visibility
    local checkbox = flow.add{
        type = "checkbox",
        name = "checkbox",
        caption = element_define.tooltip or element_define.caption or "None",
        state = top_element.visible or false,
        tags = {
            top_element_name = element_define.name
        }
    }
    definition:triggers_events(checkbox)
    checkbox.style.width = 180

    -- Add the buttons used to move the flow up and down
    local move_flow = flow.add{ type = "flow", name = "move" }
    move_flow.style.horizontal_spacing = 0
    move_up(move_flow)
    move_down(move_flow)

    return definition:no_events(flow)
end)
:on_checked_changed(function(player, element)
    local top_flow = Gui.get_top_flow(player)
    local top_element = top_flow[element.tags.top_element_name]
    local had_visible = Gui.top_flow_has_visible_elements(player)
    top_element.visible = element.state

    -- Check if we are on the edge case between 0 and 1 visible elements
    if element.state and not had_visible then
        Gui.toggle_top_flow(player, true)
        local container = element.parent.parent.parent.parent
        local button = container.header.alignment[toggle_toolbar.name]
        button.toggled = true
        button.enabled = true
    elseif not element.state and not Gui.top_flow_has_visible_elements(player) then
        Gui.toggle_top_flow(player, false)
        local container = element.parent.parent.parent.parent
        local button = container.header.alignment[toggle_toolbar.name]
        button.toggled = false
        button.enabled = false
    end

    -- Update the datastore state
    ToolbarState:update(player, function(_, order)
        local index = element.parent.get_index_in_parent()
        order[index].favourite = element.state
    end)
end)

--- Scrollable list of all toolbar buttons
-- @element toolbar_list
local toolbar_list =
Gui.element(function(_, parent)
    -- This is a scroll pane for the list
    local scroll_pane = parent.add {
        name = "scroll",
        type = "scroll-pane",
        direction = "vertical",
        horizontal_scroll_policy = "never",
        vertical_scroll_policy = "auto",
        style = "scroll_pane_under_subheader"
    }
    scroll_pane.style.horizontally_stretchable = true
    scroll_pane.style.padding = 0
    scroll_pane.style.maximal_height = 224

    -- This flow is the list, we need a linear list because of get_index_in_parent
    local flow = scroll_pane.add {
        name = "list",
        type = "flow",
        direction = "vertical"
    }
    flow.style.vertical_spacing = 0
    flow.style.horizontally_stretchable = true

    return flow
end)

--- Main toolbar container for the left flow
-- @element toolbar_container
toolbar_container =
Gui.element(function(definition, parent)
    -- Draw the internal container
    local container = Gui.container(parent, definition.name, 268)
    container.style.maximal_width = 268
    container.style.minimal_width = 268

    -- Draw the header
    local player = Gui.get_player_from_element(parent)
    local header = Gui.header(container, {"toolbar.main-caption"}, {"toolbar.main-tooltip"}, true)

    -- Draw the toolbar control buttons
    local toggle_element = toggle_toolbar(header)
    toggle_element.toggled = Gui.get_top_flow(player).visible
    reset_toolbar(header)

    -- Draw toolbar list element
    local list_element = toolbar_list(container)
    local flow_order = Gui.get_top_flow_order(player)

    for _, element_define in ipairs(flow_order) do
        -- Ensure the element exists
        local element = list_element[element_define.name]
        if not element then
            element = toolbar_list_item(list_element, element_define)
        end

        -- Set the visible state
        local allowed = element_define.authenticator
        if type(allowed) == 'function' then allowed = allowed(player) end
        element.visible = allowed or false
    end

    -- Set the state of the move buttons for the first and last element
    local children = list_element.children
    children[1].move[move_up.name].enabled = false
    children[#children].move[move_down.name].enabled = false

    -- Return the external container
    return container.parent
end)
:static_name(Gui.unique_static_name)
:add_to_left_flow(false)

--- Set the default value for the datastore
local datastore_id_map = {}
local toolbar_default_state = {}
ToolbarState:set_default(toolbar_default_state)

--- Get the datastore id for this element define, to best of ability it should be unique between versions
local function to_datastore_id(element_define)
    -- First try to use the tooltip locale string
    local tooltip = element_define.tooltip
    if type(tooltip) == "table" then
        return tooltip[1]:gsub("%.(.+)", "")
    end

    -- Then try to use the caption or sprite
    return element_define.caption or element_define.sprite
end

--- For all top element, register an on click which will copy their style
for index, element_define in ipairs(Gui.top_elements) do
    -- This is a bit hacky, the gui system cant have multiple handlers registered
    local prev_handler = element_define[Gui.events.on_toolbar_button_toggled]

    -- Add the handler for when the button is toggled
    element_define:on_event(Gui.events.on_toolbar_button_toggled, function(player, element, event)
        if prev_handler then prev_handler(player, element, event) end -- Kind of hacky but works
        local frame = Gui.get_left_element(player, toolbar_container)
        if not frame then return end -- Gui might not be loaded yet
        local button = frame.container.scroll.list[element_define.name][element_define.name]
        local toolbar_button = Gui.get_top_element(player, element_define)
        copy_style(toolbar_button, button)
    end)

    -- Insert the element into the id map
    local id = to_datastore_id(element_define)
    if datastore_id_map[id] then
        error(string.format("All toolbar elements need a unique id to be saved correctly, %d (%s) and %d (%s) share the id %s",
            datastore_id_map[id].uid, datastore_id_map[id].defined_at, element_define.uid, element_define.defined_at, id
        ))
    end
    datastore_id_map[id] = element_define

    -- Add the element to the default state
    table.insert(toolbar_default_state, {
        element_uid = element_define.uid,
        favourite = true,
    })
end

--- Get the top order based on the players settings
Gui.inject_top_flow_order(function(player)
    local order = ToolbarState:get(player)

    local elements = {}
    for index, state in ipairs(order) do
        elements[index] = Gui.defines[state.element_uid]
    end

    return elements
end)

--- Get the left order based on the player settings, with toolbar menu first, and all remaining after
Gui.inject_left_flow_order(function(player)
    local order = Gui.get_top_flow_order(player)
    local elements, element_map = { toolbar_container }, { [toolbar_container] = true }

    -- Add the flows that have a top element
    for _, element_define in ipairs(order) do
        if element_define.left_flow_element then
            table.insert(elements, element_define.left_flow_element)
            element_map[element_define.left_flow_element] = true
        end
    end

    -- Add the flows that dont have a top element
    for _, element_define in ipairs(Gui.left_elements) do
        if not element_map[element_define] then
            table.insert(elements, element_define)
        end
    end

    return elements
end)

--- Overwrite the default toggle behaviour and instead toggle this menu
Gui.core_defines.hide_top_flow:on_click(function(player, _, _)
    Gui.toggle_left_element(player, toolbar_container)
end)

--- Overwrite the default toggle behaviour and instead toggle this menu
Gui.core_defines.show_top_flow:on_click(function(player, _, _)
    Gui.toggle_left_element(player, toolbar_container)
end)

--- Overwrite the default update top flow
local _update_top_flow = Gui.update_top_flow
function Gui.update_top_flow(player)
    _update_top_flow(player) -- Call the original

    local order = ToolbarState:get(player)
    for index, state in ipairs(order) do
        local element_define = Gui.defines[state.element_uid]
        local top_element = Gui.get_top_element(player, element_define)
        top_element.visible = top_element.visible and state.favourite or false
    end
end

--- Uncompress the data to be more useable
ToolbarState:on_load(function(player_name, value)
    -- If there is no value, do nothing
    if value == nil then return end

    -- Create a hash map of the favourites
    local favourites = {}
    for _, id in ipairs(value[2]) do
        favourites[id] = true
    end

    -- Read the order from the value
    local elements = {}
    local element_hash = {}
    for index, id in ipairs(value[1]) do
        local element = datastore_id_map[id]
        if element and not element_hash[element.uid] then
            element_hash[element.uid] = true
            elements[index] = {
                element_uid = element.uid,
                favourite = favourites[id] or false,
            }
        end
    end

    -- Add any in the default state that are missing
    for _, state in ipairs(toolbar_default_state) do
        if not element_hash[state.element_uid] then
            table.insert(elements, table.deep_copy(state))
        end
    end

    -- Create a hash map of the open left flows
    local left_flows = {}
    for _, id in ipairs(value[3]) do
        local element = datastore_id_map[id]
        if element.left_flow_element then
            left_flows[element.left_flow_element] = true
        end
    end

    -- Set the visible state of all left flows
    local player = game.get_player(player_name)
    for _, left_element in ipairs(Gui.left_elements) do
        Gui.toggle_left_element(player, left_element, left_flows[left_element] or false)
    end

    -- Set the toolbar visible state
    Gui.toggle_top_flow(player, value[4])

    -- Set the data now and update now, ideally this would be on_update but that had too large of a latency
    ToolbarState:raw_set(player_name, elements)
    Gui.reorder_top_flow(player)
    Gui.reorder_left_flow(player)
    reorder_toolbar_menu(player)

    return elements
end)

--- Save the current state of the players toolbar menu
ToolbarState:on_save(function(player_name, value)
    if value == nil then return nil end -- Don't save default
    local order, favourites, left_flows = {}, {}, {}

    local player = game.get_player(player_name)
    local top_flow_open = Gui.get_top_flow(player).parent.visible

    for index, state in ipairs(value) do
        -- Add the element to the order array
        local element_define = Gui.defines[state.element_uid]
        local id = to_datastore_id(element_define)
        order[index] = id

        -- If its a favourite then insert it
        if state.favourite then
            table.insert(favourites, id)
        end

        -- If it has a left flow and its open then insert it
        if element_define.left_flow_element then
            local left_element = Gui.get_left_element(player, element_define.left_flow_element)
            if left_element.visible then
                table.insert(left_flows, id)
            end
        end
    end

    return { order, favourites, left_flows, top_flow_open }
end)