features/infinite_storage_chest.lua
local Module = {}

local Event = require 'utils.event'
local Token = require 'utils.token'
local Gui = require 'utils.gui'
local Task = require 'utils.task'
local Global = require 'utils.global'
local Table = require 'utils.table'

local format = string.format

local chests = {}
local chests_next = {}
local config = storage.config.infinite_storage_chest


Global.register(
    {chests = chests, chests_next = chests_next},
    function(tbl)
        chests = tbl.chests
        chests_next = tbl.chests_next
    end
)

local chest_gui_frame_name = Gui.uid_name()
local chest_content_table_name = Gui.uid_name()

function Module.create_chest(surface, position, storage)
    local entity = surface.create_entity {name = 'infinity-chest', position = position, force = 'player'}
    chests[entity.unit_number] = {entity = entity, storage = storage}
    return entity
end

local function built_entity(event)
    local entity = event.entity
    if not entity or not entity.valid or entity.name ~= 'infinity-chest' then
        return
    end

    entity.active = false

    chests[entity.unit_number] = {entity = entity, storage = {}}
end

local function get_stack_size(name)
    local proto = prototypes.item[name]
    if not proto then
        log('item prototype ' .. name .. ' not found')
        return 1
    end

    return proto.stack_size
end

local function do_item(name, count, inv, storage)
    local size = get_stack_size(name)
    local diff = count - size

    if diff == 0 then
        return
    end

    local new_amount = 0

    if diff > 0 then
        inv.remove({name = name, count = diff})
        local prev = storage[name] or 0
        new_amount = prev + diff
    elseif diff < 0 then
        local prev = storage[name]
        if not prev then
            return
        end

        diff = math.min(prev, -diff)
        local inserted = inv.insert({name = name, count = diff})
        new_amount = prev - inserted
    end

    if new_amount == 0 then
        storage[name] = nil
    else
        storage[name] = new_amount
    end
end

local function tick()
    local chest_id, chest_data = next(chests, chests_next[1])

    chests_next[1] = chest_id

    if not chest_id then
        return
    end

    local entity = chest_data.entity
    if not entity or not entity.valid then
        chests[chest_id] = nil
    else
        local storage = chest_data.storage
        local inv = entity.get_inventory(defines.inventory.chest)
        local contents = inv.get_contents()

        for _, item_stack in pairs(contents) do
            do_item(item_stack.name, item_stack.count, inv, storage)
        end

        local dict = Table.array_to_dict(contents, 'name')
        for name, _ in pairs(storage) do
            if not dict[name] then
                do_item(name, 0, inv, storage)
            end
        end
    end
end

local function create_chest_gui_content(frame, player, chest)
    local storage = chest.storage
    local inv = Table.array_to_dict(chest.entity.get_inventory(defines.inventory.chest).get_contents(), 'name')

    local grid = frame[chest_content_table_name]

    if grid then
        grid.clear()
    else
        grid = frame.add {type = 'table', name = chest_content_table_name, column_count = 10, style = 'slot_table'}
    end

    for name, count in pairs(storage) do
        local number = count + (inv[name] and inv[name].count or 0)
        grid.add {
            type = 'sprite-button',
            sprite = 'item/' .. name,
            number = number,
            tooltip = name,
            --style = 'slot_button'
            enabled = false
        }
    end

    for name, item_stack in pairs(inv) do
        if not storage[name] then
            grid.add {
                type = 'sprite-button',
                sprite = 'item/' .. name,
                number = item_stack.count,
                tooltip = name,
                --style = 'slot_button'
                enabled = false
            }
        end
    end

    player.opened = frame
end

local chest_gui_content_callback
chest_gui_content_callback =
    Token.register(
    function(data)
        local player = data.player

        if not player or not player.valid then
            return
        end

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

        local entity = data.chest.entity
        if not entity.valid then
            player.opened = nil
            opened.destroy()
            return
        end

        if not player.connected then
            player.opened = nil
            opened.destroy()
            return
        end

        create_chest_gui_content(opened, player, data.chest)

        Task.set_timeout_in_ticks(60, chest_gui_content_callback, data)
    end
)

local function gui_opened(event)
    if not event.gui_type == defines.gui_type.entity then
        return
    end

    local entity = event.entity
    if not entity or not entity.valid or entity.name ~= 'infinity-chest' then
        return
    end

    local chest = chests[entity.unit_number]

    if not chest then
        return
    end

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

    local frame =
        player.gui.center.add {
        type = 'frame',
        name = chest_gui_frame_name,
        caption = 'Infinite Storage Chest',
        direction = 'vertical'
    }

    local text =
        frame.add {
        type = 'label',
        caption = format('This chest stores unlimited quantity of items (up to 48 different item types).\nThe chest is best used with an inserter to add / remove items.\nIf the chest is mined or destroyed the items are lost.\nYou can buy the chest at the market for %s coins.', config.cost)
    }
    text.style.single_line = false

    local content_header = frame.add {type = 'label', caption = 'Content'}
    content_header.style.font = 'default-listbox'

    create_chest_gui_content(frame, player, chest)

    Task.set_timeout_in_ticks(60, chest_gui_content_callback, {player = player, chest = chest, opened = frame})
end

Event.add(defines.events.on_built_entity, built_entity)
Event.add(defines.events.on_robot_built_entity, built_entity)
Event.add(defines.events.on_tick, tick)
Event.add(defines.events.on_gui_opened, gui_opened)

Event.add(
    defines.events.on_player_died,
    function(event)
        local player = game.get_player(event.player_index or 0)

        if not player or not player.valid then
            return
        end

        local element = player.gui.center

        if element and element.valid then
            element = element[chest_gui_frame_name]
            if element and element.valid then
                element.destroy()
            end
        end
    end
)

Gui.on_custom_close(
    chest_gui_frame_name,
    function(event)
        event.element.destroy()
    end
)

local market_items = require 'resources.market_items'
table.insert(
    market_items,
    {
        price = config.cost,
        name = 'infinity-chest',
        description = 'Stores unlimited quantity of items for up to 48 different item types'
    }
)
return Module
