local Event = require 'utils.event'
local Global = require 'utils.global'
local Color = require 'utils.color_presets'
local Gui = require 'utils.gui'
local m_gui = require 'mod-gui'
local mod = m_gui.get_button_flow
local SpamProtection = require 'utils.spam_protection'

local this =
{
    inf_chests = {},
    inf_storage = {},
    inf_gui = {},
    player_chests = {},
    viewing_player = {},
    editor = {},
    ores_only = false,
    allow_barrels = true,
    total_slots = {},
    stack_size = {}
}

local ore_names =
{
    ['coal'] = true,
    ['stone'] = true,
    ['iron-ore'] = true,
    ['copper-ore'] = true,
    ['uranium-ore'] = true,
    ['wood'] = true
}

local format = string.format
local size = 35
local main_button_name = Gui.uid_name()
local main_frame_name = Gui.uid_name()
local stack_slider_name = Gui.uid_name()
local delete_mode_name = Gui.uid_name()

local Public = {}

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

function Public.get_table()
    return this
end

local function clear_gui(player)
    local data = this.inf_gui[player.index]
    if not data then
        return
    end
    if data.frame and data.frame.valid then
        Gui.remove_data_recursively(data.frame)
        data.frame.destroy()
    end
    this.inf_gui[player.index] = nil
    if this.viewing_player[player.index] then
        this.viewing_player[player.index] = nil
    end
end

local function create_button(player)
    mod(player).add(
        {
            type = 'sprite-button',
            sprite = 'item/logistic-chest-requester',
            name = main_button_name,
            tooltip = 'Portable inventory stash!',
            style = m_gui.button_style
        }
    )
end

local function validate_player(player)
    if not player then
        return false
    end
    if not player.valid then
        return false
    end
    if not player.character then
        return false
    end
    if not player.connected then
        return false
    end
    if not game.players[player.index] then
        return false
    end
    return true
end

local function item(item_name, item_count, player, storage)
    local stack_size = this.stack_size[player.index]
    local item_stack
    if stack_size then
        item_stack = prototypes.item[item_name].stack_size * stack_size
    else
        item_stack = prototypes.item[item_name].stack_size
    end

    local diff = item_count - item_stack

    if diff > 0 then
        local count = player.remove({ name = item_name, count = diff })
        if not storage[item_name] then
            storage[item_name] = count
        else
            storage[item_name] = storage[item_name] + count
        end
    elseif diff < 0 then
        if not storage[item_name] or storage[item_name] <= 0 then
            goto continue
        end
        if storage[item_name] > (diff * -1) then
            local inserted = player.insert({ name = item_name, count = (diff * -1) })
            storage[item_name] = storage[item_name] - inserted
        else
            player.insert({ name = item_name, count = storage[item_name] })
            storage[item_name] = 0
        end
    end
    ::continue::
end

local function update_chest()
    for chest_id, player in pairs(this.inf_chests) do
        if not player.valid then
            goto continue
        end
        local storage = this.inf_storage[chest_id]
        if not storage then
            goto continue
        end

        local inv = player.get_inventory(defines.inventory.character_main)
        if not inv or not inv.valid then
            goto continue
        end
        local content = inv.get_contents()

        for item_name, item_count in pairs(content) do
            if storage[item_name] then
                item(item_name, item_count, inv, storage)
            end
        end

        for item_name, _ in pairs(storage) do
            if not content[item_name] then
                item(item_name, 0, inv, storage)
            end
        end

        ::continue::
    end
end

local function draw_main_frame(player, target, chest_id)
    chest_id = chest_id or this.player_chests[player.index].chest_id
    if not chest_id then
        return
    end
    local p = target or player
    local frame =
        player.gui.screen.add
        {
            type = 'frame',
            caption = p.name .. '´s private portable stash',
            direction = 'vertical',
            name = main_frame_name
        }
    frame.auto_center = true

    local data = {}

    local controls = frame.add { type = 'flow', direction = 'horizontal' }
    local items = frame.add { type = 'flow', direction = 'vertical' }

    local tbl = controls.add { type = 'table', column_count = 1 }
    local btn =
        tbl.add
        {
            type = 'sprite-button',
            tooltip = '[color=blue]Info![/color]\nYou can easily remove an item by left/right-clicking it.\n\nItems selected in the table below will remove all stacks except one from the player inventory.\nIf the stack-size is bigger in the personal stash than the players inventory stack then the players inventory will automatically refill from the personal stash.\n\n[color=red]Usage[/color]\nPressing the following keys will do the following actions:\nCTRL: Retrieves all stacks from clicked item\nSHIFT:Retrieves a stack from clicked item.\nStack-Size slider will always ensure that you have <x> amounts of stacks in your inventory.\n\n[color=red]Deleting[/color]\nDelete Mode: Will delete the clicked item instantly.',
            sprite = 'utility/questionmark'
        }
    btn.style.height = 20
    btn.style.width = 20
    btn.enabled = false
    btn.focus()

    if not player.admin and this.ores_only then
        this.total_slots[player.index] = 6
    end

    local amount_and_types
    if this.ores_only then
        amount_and_types = this.total_slots[player.index] .. ' different ore'
    else
        amount_and_types = this.total_slots[player.index] .. ' different item'
    end

    local text =
        tbl.add
        {
            type = 'label',
            caption = format('Stores unlimited quantity of items (up to ' .. amount_and_types .. ' types).\nRead the tooltip by hovering the question-mark above!')
        }
    text.style.single_line = false

    local tbl_2 = tbl.add { type = 'table', column_count = 4 }
    local stack_size = this.stack_size[player.index]

    local stack_value = tbl_2.add({ type = 'label', caption = 'Stack Size: ' .. stack_size .. ' ' })
    stack_value.style.font = 'default-bold'
    data.stack_value = stack_value

    local slider =
        tbl_2.add(
            {
                type = 'slider',
                minimum_value = 1,
                maximum_value = 10,
                name = stack_slider_name,
                value = stack_size
            }
        )
    data.slider = slider
    slider.style.width = 115
    Gui.set_data(slider, data)

    local delete_mode = tbl_2.add({ type = 'label', caption = '  Delete Mode: ' })
    delete_mode.style.font = 'default-bold'
    local checkbox = tbl_2.add({ type = 'checkbox', name = delete_mode_name, state = false })
    data.checkbox = checkbox

    Gui.set_data(checkbox, data)

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

    player.opened = frame
    if target and target.valid then
        this.viewing_player[player.index] = true
    else
        if this.viewing_player[player.index] then
            this.viewing_player[player.index] = nil
        end
    end
    this.inf_gui[player.index] =
    {
        item_frame = items,
        frame = frame,
        updated = false,
        delete_mode = false
    }
end

local function update_gui()
    for _, player in pairs(game.connected_players) do
        local chest_gui_data = this.inf_gui[player.index]
        if not chest_gui_data then
            goto continue
        end
        local frame = chest_gui_data.item_frame
        if not frame or not frame.valid then
            clear_gui(player)
            goto continue
        end

        local data = this.player_chests[player.index]
        if not data then
            goto continue
        end

        local chest_id = data.chest_id
        if not chest_id then
            goto continue
        end
        if this.inf_gui[player.index].updated then
            goto continue
        end
        frame.clear()

        local tbl = frame.add { type = 'table', column_count = 10, name = 'personal_inventory' }
        local total = 0
        local items = {}

        local storage = this.inf_storage[chest_id]

        if not storage then
            goto no_storage
        end
        for item_name, item_count in pairs(storage) do
            total = total + 1
            items[item_name] = item_count
        end
        ::no_storage::

        local delete_mode = this.inf_gui[player.index].delete_mode

        local btn
        for item_name, item_count in pairs(items) do
            btn =
                tbl.add
                {
                    type = 'sprite-button',
                    sprite = 'item/' .. item_name,
                    style = 'slot_button',
                    number = item_count,
                    name = item_name
                }
            btn.enabled = true
            btn.style.height = size
            btn.style.width = size
            if delete_mode then
                btn.tooltip = 'Press to delete this item.'
            end
            btn.focus()
        end

        while total < this.total_slots[player.index] do
            local btns = tbl.add { type = 'choose-elem-button', style = 'slot_button', elem_type = 'item' }
            btns.enabled = true
            btns.style.height = size
            btns.style.width = size
            btns.focus()
            if this.viewing_player[player.index] then
                btns.enabled = false
            end
            total = total + 1
        end

        this.inf_gui[player.index].updated = true
        ::continue::
    end
end

local function gui_click(event)
    local element = event.element
    local player = game.get_player(event.player_index)
    if not validate_player(player) then
        return
    end
    if not element.valid then
        return
    end
    local parent = element.parent
    if not parent then
        return
    end
    if parent.name ~= 'personal_inventory' then
        return
    end
    local chest_id = this.player_chests[player.index].chest_id
    if not chest_id then
        return
    end

    if this.viewing_player[player.index] then
        goto update
    end

    local shift = event.shift
    local ctrl = event.control
    local name = element.name
    local storage = this.inf_storage[chest_id]
    local delete_mode = this.inf_gui[player.index].delete_mode

    if not storage then
        return
    end

    if delete_mode then
        storage[name] = nil
        this.inf_gui[player.index].updated = false
        return
    end

    if this.editor[player.index] then
        if not storage[name] then
            return
        end
        if ctrl then
            storage[name] = storage[name] + 5000000
            this.inf_gui[player.index].updated = false
            goto update
        elseif shift then
            storage[name] = storage[name] - 5000000
            this.inf_gui[player.index].updated = false
            if storage[name] <= 0 then
                storage[name] = nil
            end
            goto update
        end
    end

    if storage[name] and storage[name] <= 0 then
        storage[name] = nil
        if this.inf_gui[player.index] then
            this.inf_gui[player.index].updated = false
        end
        goto update
    end

    if ctrl then
        local count = storage[name]
        if not count then
            return
        end
        local inserted = player.insert { name = name, count = count }
        if not inserted then
            return
        end
        if inserted == count then
            storage[name] = nil
        else
            storage[name] = storage[name] - inserted
        end
        if this.inf_gui[player.index] then
            this.inf_gui[player.index].updated = false
        end
    elseif shift then
        local count = storage[name]
        local stack = prototypes.item[name].stack_size
        if not count then
            return
        end
        if not stack then
            return
        end
        if count > stack then
            local inserted = player.insert { name = name, count = stack }
            storage[name] = storage[name] - inserted
        else
            player.insert { name = name, count = count }
            storage[name] = nil
        end
        if this.inf_gui[player.index] then
            this.inf_gui[player.index].updated = false
        end
    end

    ::update::
end

local function gui_closed(event)
    local player = game.get_player(event.player_index)
    local type = event.gui_type

    if type == defines.gui_type.custom then
        clear_gui(player)
    end
end

local function on_gui_elem_changed(event)
    local element = event.element
    local player = game.get_player(event.player_index)
    if not validate_player(player) then
        return
    end

    if not element.valid then
        return
    end
    local parent = element.parent
    if not parent then
        return
    end
    if parent.name ~= 'personal_inventory' then
        return
    end

    local chest_id = this.player_chests[player.index].chest_id
    if not chest_id then
        return
    end
    local storage = this.inf_storage[chest_id]
    if not storage then
        this.inf_storage[chest_id] = {}
        storage = this.inf_storage[chest_id]
    end
    local name = element.elem_value

    if not name then
        return
    end

    if this.ores_only then
        if not ore_names[name] then
            player.print('You can only stash ores and wood.', { color = Color.warning })
            goto update
        end
    end

    if this.allow_barrels then
        if string.match(name, 'barrel') then
            player.print('You can´t stash barrels.', { color = Color.warning })
            goto update
        end
    end

    storage[name] = 0
    if this.editor[player.index] then
        storage[name] = 5000000
    end

    ::update::

    if this.inf_gui[player.index] then
        this.inf_gui[player.index].updated = false
    end
end

local function on_player_joined_game(event)
    local player = game.get_player(event.player_index)
    if not (player and player.valid) then
        return
    end
    local chest_id = Gui.uid()
    if not this.player_chests[player.index] then
        this.player_chests[player.index] =
        {
            chest_id = chest_id
        }
    end

    chest_id = this.player_chests[player.index].chest_id

    if not this.inf_chests[chest_id] then
        this.inf_chests[chest_id] = player
    end

    if not this.stack_size[player.index] then
        this.stack_size[player.index] = 1
    end

    if not this.total_slots[player.index] then
        this.total_slots[player.index] = 50
    end

    if not mod(player)[main_button_name] then
        create_button(player)
    end
end

local function tick()
    update_chest()
    update_gui()
end

local function on_pre_player_left_game(event)
    local player = game.get_player(event.player_index)

    if not player or not player.valid then
        return
    end

    clear_gui(player)
end

local function on_player_died(event)
    local player = game.get_player(event.player_index)

    if not player or not player.valid then
        return
    end

    clear_gui(player)
end

Gui.on_click(
    main_button_name,
    function (event)
        local is_spamming = SpamProtection.is_spamming(event.player, nil, 'Portable Chest Main Button')
        if is_spamming then
            return
        end
        local player = game.get_player(event.player_index)
        if not player or not player.valid or not player.character then
            return
        end

        local screen = player.gui.screen
        local main_frame = screen[main_frame_name]
        if main_frame and main_frame.valid then
            clear_gui(player)
        else
            draw_main_frame(player)
        end
    end
)

Gui.on_value_changed(
    stack_slider_name,
    function (event)
        local player = event.player
        local element = event.element

        local data = Gui.get_data(element)
        local stack_value = data.stack_value
        if not stack_value or not stack_value.valid then
            return
        end

        local slider = data.slider
        if not slider or not slider.valid then
            return
        end

        local screen = player.gui.screen
        local main_frame = screen[main_frame_name]
        if main_frame and main_frame.valid then
            this.stack_size[player.index] = element.slider_value
            stack_value.caption = 'Stack Size: ' .. this.stack_size[player.index] .. ' '
            this.inf_gui[player.index].updated = false
        end
    end
)

Gui.on_checked_state_changed(
    delete_mode_name,
    function (event)
        local player = event.player
        local element = event.element

        local data = Gui.get_data(element)
        local checkbox = data.checkbox
        if not checkbox or not checkbox.valid then
            return
        end

        local screen = player.gui.screen
        local main_frame = screen[main_frame_name]
        if main_frame and main_frame.valid then
            this.inf_gui[player.index].delete_mode = element.state
            this.inf_gui[player.index].updated = false
        end
    end
)

commands.add_command(
    'open_stash',
    'Opens a players private stash!',
    function (cmd)
        local player = game.player

        if not validate_player(player) then
            return
        end

        if not cmd.parameter then
            return
        end
        local target_player = game.players[cmd.parameter]

        if target_player == player then
            return player.print('Cannot open self.', { color = Color.warning })
        end

        if target_player.admin then
            return
        end

        if target_player and target_player.valid then
            local chest_id = this.player_chests[target_player.index].chest_id
            if not chest_id then
                return
            end
            draw_main_frame(player, target_player, chest_id)
        else
            player.print('Please type a valid player name.', { color = Color.warning })
        end
    end
)

function Public.ores_only(value)
    if value then
        this.ores_only = value
    else
        this.ores_only = false
    end
    return this.ores_only
end

function Public.allow_barrels(value)
    if value then
        this.allow_barrels = value
    else
        this.allow_barrels = false
    end
    return this.allow_barrels
end

Event.on_nth_tick(10, tick)
Event.add(defines.events.on_gui_click, gui_click)
Event.add(defines.events.on_gui_closed, gui_closed)
Event.add(defines.events.on_player_joined_game, on_player_joined_game)
Event.add(defines.events.on_gui_elem_changed, on_gui_elem_changed)
Event.add(defines.events.on_pre_player_left_game, on_pre_player_left_game)
Event.add(defines.events.on_player_died, on_player_died)

return Public
