local STORAGE_NODE_NAME = "minecraft:trapped_chest"
local STORAGE_STORE_PREFIX = "minecraft:chest_"
local node = peripheral.find(STORAGE_NODE_NAME)
local store = {}
for _,v in ipairs(peripheral.getNames()) do
  if #v >= #STORAGE_STORE_PREFIX and v:sub(1,#STORAGE_STORE_PREFIX) == STORAGE_STORE_PREFIX then
    store[v] = peripheral.wrap(v)
  end
end

local function countNamed(tbl)
  local count = 0
  for _,_ in pairs(tbl) do
    count = count + 1
  end
  return count
end

local function cacheFromItemDetail(detail, chest, slot)
  if detail == nil then
    return {
      maxCount = chest.getItemLimit(slot)
    }
  end

  return {
    name = detail.name,
    enchantments = detail.enchantments,
    count = detail.count,
    maxCount = detail.maxCount,
    displayName = detail.displayName,
    damage = detail.damage,
    maxDamage = detail.maxDamage
  }
end

local function cacheFromSlot(chest, slot)
  return cacheFromItemDetail(chest.getItemDetail(slot), chest, slot)
end

local function scanChest(chest)
  local entry = {}
  for slot=1,chest.size() do
    table.insert(
      entry,
      cacheFromSlot(chest, slot)
    )
  end
  return entry
end

local Cache = {}

-- cacheLoader?: function(periph_name, periph_inv): { [number]: { maxCount: number } } | nil
function Cache.makeCache(cacheLoader)
  local count = countNamed(store)
  local state = {}
  local progress = 0
  for name,chest in pairs(store) do
    progress = progress + 1
    local percent = ""..((progress / count) * 100)
    print(percent:sub(1, math.min(#percent, 6))..("0"):rep(6 - #percent).."%  ("..progress.."/"..count..")")

    local entry = nil
    if #chest.list() == 0 and type(cacheLoader) == "function" then
      print("Using cacheloader")
      entry = cacheLoader(name, chest)
    end

    if entry == nil then
      entry = scanChest(chest)
    end
    entry.index = name
    state[name] = entry
  end

  --[[
    state: {
      [str]: {
        index: str,

        [number]: {
          name?: str,
          enchantments?: {
            [str]: str
          },
          count?: number,    -- if nil, then slot is empty
          maxCount: number,
          displayName?: str,
          damage?: number,
          maxDamage?: number
        }
        ...
      }
    }
  --]]

  return Cache:from(state)
end

function Cache:reloadSlot(chestName, slot)
  local chest = self[chestName]
  if chest == nil then
    return false
  end

  chest[slot] = cacheFromSlot(store[chestName], slot)
  return true
end

function Cache:from(o)
  setmetatable(o, self)
  self.__index = self
  return o
end

function Cache:save(fileName)
  local file = io.open(fileName, "w+")
  if file == nil then
    return false
  end
  local x = self
  x.__index = nil
  setmetatable(x, nil)
  file:write(textutils.serialize(x, { compact = true }))
  file:close()
  Cache:from(x)
  return true
end

-- cacheLoader?: function(periph_name, periph_inv): { [number]: { maxCount: number } } | nil
function Cache.loadOrBuild(fileName, cacheLoader)
  local file = fs.open(fileName, "r")
  if file == nil then
    print("Building cache...")
    return Cache.makeCache(cacheLoader)
  end
  
  local obj = textutils.unserialise(file.read(fs.getSize(fileName)))
  if obj ~= nil then
    return Cache:from(obj)
  else
    print("Malformed cache file! Building cache...")
    return Cache.makeCache(cacheLoader)
  end
end

-- predicate: function(itemDetail) -> bool
function Cache:find(predicate)
  local slots = {}

  if type(predicate) ~= "function" then
    return slots
  end

  for name,chest in pairs(self) do
    for slot,detail in ipairs(chest) do
      if predicate(detail) then
        -- Flat to make sorting is easier
        table.insert(slots, {
          chest = name,
          slot = slot,
          name = detail.name,
          enchantments = detail.enchantments,
          count = detail.count == nil and 0 or detail.count,
          maxCount = detail.maxCount,
          displayName = detail.displayName,
          damage = detail.damage,
          maxDamage = detail.maxDamage
        })
      end
    end
  end
  return slots
end

local function checkField(base, check)
  if type(base) == "function" then
    return base(check)
  end

  local baseType = type(base)
  if baseType == "nil" then
    return true
  elseif baseType ~= type(check) then
    return false
  elseif baseType == "table" then
    for i,v in ipairs(base) do
      if not checkField(v, check[i]) then
        return false
      end
    end
    for k,v in pairs(base) do
      if not checkField(v, check[k]) then
        return false
      end
    end
    return true
  else
    return base == check
  end
end

function FILTER_MATCHING_SLOTS(target)
  return function(detail)
    return detail ~= nil and (target == nil or (checkField(target.name, detail.name) and
            checkField(target.displayName, detail.displayName) and
            checkField(target.damage, detail.damage) and
            checkField(target.enchantments, detail.enchantments)))
  end
end

function Cache:findEmptySlots()
  return self:find(function(detail)
    return detail.count == nil and detail.maxCount > 0
  end)
end

function Cache:findMatchingSlots(target)
  return self:find(FILTER_MATCHING_SLOTS(target))
end

function Cache:findAllowedSlots(target)
  local matchingSlotsFunc = FILTER_MATCHING_SLOTS(target)
  local slots = self:find(function(detail)
    return detail.count == nil or (detail.count < detail.maxCount and matchingSlotsFunc(detail))
  end)
  table.sort(slots, function (a, b)
    return (a.count ~= nil and (b.count == nil or a.maxCount > b.maxCount or (a.maxCount == b.maxCount and a.count > b.count)))
  end)
  return slots
end

function Cache:insertStack(nodeSlot)
  local detail = node.getItemDetail(nodeSlot)
  if type(detail) ~= "table" then
    return true, 0
  end

  local count = 0
  local allowedSlots = self:findAllowedSlots(detail)
  for _,slot in ipairs(allowedSlots) do
    if count == detail.count then
      break
    end

    local slotCap = math.min(slot.maxCount - slot.count, detail.count - count)
    local result, change = pcall(node.pushItems, slot.chest, nodeSlot, slotCap, slot.slot)
    if not result then
      print("Slot insert error for chest \""..slot.chest.."\"! Make sure it's still attached to the controller: "..change)
    else
      if change == slot.count then
        self:reloadSlot(slot.chest, slot.slot)
      else
        slot.count = slot.count - change
      end
      count = count + change
    end
  end

  return count == detail.count, count
end

function Cache:extractStack(detail)
  local foundSlots = self:findMatchingSlots(detail)
  -- Prioritize smaller stacks in order to create more empty slots when possible
  table.sort(foundSlots, function (a, b)
    return a.count < b.count
  end)

  local count = 0
  for index,slot in ipairs(foundSlots) do
    if detail ~= nil and count == detail.count then
      break
    end

    local slotCap = (detail == nil or detail.count == nil) and slot.count or math.min(slot.count, detail.count)
    local result, change = pcall(node.pullItems, slot.chest, slot.slot, slotCap)
    if not result then
      print("Slot extract error for chest \""..slot.chest.."\"! Make sure it's still attached to the controller: "..change)
    else
      if change == slot.count then
        self:reloadSlot(slot.chest, slot.slot)
      else
        slot.count = slot.count - change
      end
      count = count + change
    end
  end

  return (detail == nil or detail.count == nil) or (detail.count == count), count
end

local function clone(o)
  if type(o) == "table" then
    local copy = {}
    for k,v in pairs(o) do
      copy[clone(k)] = clone(v)
    end

    for i,v in ipairs(o) do
      copy[i] = clone(v)
    end

    return copy
  end

  return o
end


--[[
Generates blank chest cache entries for predefined chest types
Current definitions:
  - minecraft:chest
  - minecraft:trapped_chest
--]]
local function QUICKLOAD(name, chest)
  local DEFS = {
    {
      prefix = "minecraft:chest",
      entry = { maxCount = 64 }
    },
    {
      prefix = "minecraft:trapped_chest",
      entry = { maxCount = 64 }
    }
  }

  for _,def in ipairs(DEFS) do
    if #name >= #def.prefix and name:sub(1,#def.prefix) == def.prefix then
      local cacheState = {}
      for _=1,chest.size() do
        table.insert(cacheState, clone(def.entry))
      end
      return cacheState
    end
  end

  -- No quickload def found. Controller must slow-scan inventory
  return nil
end

return Cache, QUICKLOAD