features/market_chest.lua
-- This feature replaces placed linked-chests with buffer chests that can automatically trade items

local Buckets = require 'utils.buckets'
local Event = require 'utils.event'
local Global = require 'utils.global'
local Gui = require 'utils.gui'
local Retailer = require 'features.retailer'
local config = require 'config'.market_chest

local floor = math.floor
local b_get = Buckets.get
local b_add = Buckets.add
local b_remove = Buckets.remove
local b_bucket = Buckets.get_bucket
local register_on_object_destroyed = script.register_on_object_destroyed

local relative_frame_name = Gui.uid_name()
local offer_tag_name = Gui.uid_name()
local request_tag_name = Gui.uid_name()
local standard_market_name = 'fish_market'

-- What market provides
local DEFAULT_OFFERS = {
  ['coal'] = 2,
  ['copper-ore'] = 2,
  ['iron-ore'] = 2,
  ['stone'] = 2,
  ['uranium-ore'] = 10,
}
-- What market requests
local DEFAULT_REQUESTS = {
  ['coal'] = 1,
  ['copper-ore'] = 1,
  ['iron-ore'] = 1,
  ['stone'] = 1,
  ['uranium-ore'] = 5,
}

local this = {
  chests = Buckets.new(),
  enabled = config.enabled or false,
  offers = config.offers or DEFAULT_OFFERS,
  requests = config.requests or DEFAULT_REQUESTS,
  relative_gui = {},
}

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

local function update_entity(entity)
  if not (entity and entity.valid) then
    return
  end

  local data = b_get(this.chests, entity.unit_number)
  local offer, request, ratio = data.offer, data.request, data.ratio
  if not offer or not request or not ratio then
    return
  end

  local inv = entity.get_inventory(defines.inventory.chest)
  if not (inv and inv.valid) then
    return
  end

  local r_count = inv.get_item_count(request)
  local o_count = floor(r_count * ratio)
  if o_count == 0 or r_count == 0 then
    return
  end

  local removed = inv.remove {
    name = request,
    quality = request.quality,
    count = o_count / ratio,
  }
  if removed > 0 then
    local inserted = inv.insert {
      name = offer,
      count = o_count,
    }
    if inserted < o_count then
      inv.insert {
        name = request,
        count = floor((o_count - inserted) / ratio),
      }
    end
  end
end

local function update_market(enabled, price)
  if enabled then
    if not price then
      local chest = Retailer.get_items(standard_market_name)['linked-chest']
      price = chest and chest.price or 3000
    end
    Retailer.set_item(standard_market_name, { name = 'linked-chest', price = price })
  else
    Retailer.remove_item(standard_market_name, 'linked-chest')
  end
end

Event.on_built(function(event)
  local entity = event.entity
  if not (entity and entity.valid and entity.name == 'linked-chest') then
    return
  end

  -- Replace with buffer chest
  local force = entity.force
  local position = entity.position
  local surface = entity.surface
  entity.destroy()

  local chest = surface.create_entity{
    name = 'buffer-chest',
    position = position,
    force = force,
  }

  chest.destructible = false
  b_add(this.chests, chest.unit_number, { entity = chest })
  register_on_object_destroyed(chest)
  update_entity(chest)
  rendering.draw_sprite {
    sprite = 'entity.market',
    surface = chest.surface,
    only_in_alt_mode = true,
    target = {
      entity = chest,
      offset = { 0, 0 },
    },
  }
end)

Event.on_destroyed(function(event)
  local id = event.useful_id or (event.entity and event.entity.valid and event.entity.unit_number)
  if not id then
    return
  end

  local data = b_get(this.chests, id)
  local inv = event.buffer
  if data and inv and inv.valid and inv.get_item_count { name = 'buffer-chest' } > 0 then
    update_entity(data.entity)
    b_remove(this.chests, id)
    inv.remove { name = 'buffer-chest', count = 1 }
    inv.insert { name = 'linked-chest', count = 1 }
  end
end)

Event.add(defines.events.on_tick, function(event)
  if not this.enabled then
    return
  end

  for unit_number, data in pairs(b_bucket(this.chests, event.tick)) do
    local entity = data.entity
    if entity.valid then
      update_entity(entity)
    else
      b_remove(this.chests, unit_number)
    end
  end
end)

Event.add(defines.events.on_gui_opened, function(event)
  if event.gui_type ~= defines.gui_type.entity then
    return
  end

  local old = this.relative_gui[event.player_index]
  if old and old.valid then
    Gui.destroy(old)
  end

  if not this.enabled then
    return
  end

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

  local data = b_get(this.chests, entity.unit_number)
  if not data then
    return
  end

  local player = game.get_player(event.player_index)
  local frame = player.gui.relative.add {
    type = 'frame',
    name = relative_frame_name,
    direction = 'vertical',
    anchor = {
      gui = defines.relative_gui_type.container_gui,
      position = defines.relative_gui_position.right,
    }
  }
  Gui.set_style(frame, { horizontally_stretchable = false, padding = 3 })

  local flow = frame.add { type = 'flow', direction = 'horizontal' }
  flow.add { type = 'label', style = 'frame_title' }

  local canvas = frame.add { type = 'frame', style = 'entity_frame', direction = 'vertical' }

  local info = canvas.add { type = 'frame', style = 'deep_frame_in_shallow_frame_for_description', direction = 'vertical' }
  info.add { type = 'label', caption = '[img=entity/market]  Market chest', style = 'tooltip_heading_label_category' }
  info.add { type = 'line', direction = 'horizontal', style = 'tooltip_category_line' }
  local description = info.add { type = 'label', caption = {'market_chest.description'} }
  Gui.set_style(description, { single_line = false, maximal_width = 184 })

  local tables = {}

  canvas.add { type = 'label', style = 'bold_label', caption = 'Requests [img=info]', tooltip = {'market_chest.requests_tooltip'} }
  tables.requests = canvas
    .add { type = 'frame', style = 'slot_button_deep_frame' }
    .add { type = 'table', style = 'filter_slot_table', column_count = 5 }
  for name, value in pairs(this.requests) do
    local button = tables.requests.add {
      type = 'sprite-button',
      sprite = 'item/'..name,
      number = value,
      tooltip = {'market_chest.item_tooltip', value},
      tags = { name = request_tag_name, item = name, id = entity.unit_number },
      toggled = data.request and data.request == name,
    }
    Gui.set_data(button, tables)
  end

  canvas.add { type = 'line', direction = 'horizontal' }
  canvas.add { type = 'label', style = 'bold_label', caption = 'Offers [img=info]', tooltip = {'market_chest.offers_tooltip'} }
  tables.offers = canvas
    .add { type = 'frame', style = 'slot_button_deep_frame' }
    .add { type = 'table', style = 'filter_slot_table', column_count = 5 }
  for name, value in pairs(this.offers) do
    local button = tables.offers.add {
      type = 'sprite-button',
      sprite = 'item/'..name,
      number = value,
      tooltip = {'market_chest.item_tooltip', value},
      tags = { name = offer_tag_name, item = name, id = entity.unit_number },
      toggled = data.offer and data.offer == name,
    }
    Gui.set_data(button, tables)
  end

  this.relative_gui[event.player_index] = frame
end)

Event.add(defines.events.on_gui_click, function(event)
  local element = event.element
  if not (element and element.valid) then
    return
  end

  local tag = element.tags and element.tags.name
  if not tag or not (tag == request_tag_name or tag == offer_tag_name) then
    return
  end

  local toggled = not element.toggled
  for _, button in pairs(element.parent.children) do
    button.toggled = false
  end
  element.toggled = toggled

  local item_name = element.tags.item
  local data = b_get(this.chests, element.tags.id)

  if tag == request_tag_name then
    data.request = toggled and item_name or nil
  elseif tag == offer_tag_name then
    data.offer = toggled and item_name or nil
  end

  if data.request == data.offer then
    data.request, data.offer = nil, nil
    for _, t in pairs(Gui.get_data(element)) do
      for _, button in pairs(t.children) do
        button.toggled = false
      end
    end
  end

  if data.request and data.offer then
    data.ratio = this.requests[data.request] / this.offers[data.offer]
  else
    data.ratio = nil
  end
end)

Event.on_init(function()
  update_market(config.market_provides_chests, 3000)
end)

local Public = {}

Public.get = function(key)
  return this[key]
end

Public.set = function(key, value)
  this[key] = value
end

Public.distribute_linked_chests = function(enabled, price)
  update_market(enabled, price)
end

Public.spread = function(ticks)
  Buckets.reallocate(this.chests, ticks)
end

return Public
