local Inventory = require("storage.inventory")
local Sentinel = require("storage.sentinel")
local Logging = require("logging")
local Util = require("util")
local Logger = Logging.getGlobalLogger()

local ItemStack = Sentinel:tag({}, Sentinel.ITEMSTACK)
ItemStack.__index = ItemStack

function ItemStack:fromDetail(inv, detail, slot)
    local obj = {
        slot = slot,
        inv = inv
    }

    if detail == nil then
        obj.maxCount = Util.getItemLimit(inv, slot)
    else
        obj.name = detail.name
        obj.damage = detail.damage
        obj.maxDamage = detail.maxDamage
        obj.count = detail.count
        obj.maxCount = detail.maxCount or 64
        obj.enchantments = detail.enchantments
        obj.displayName = detail.displayName
        obj.nbt = detail.nbt
    end

    setmetatable(obj, self)
    obj.__index = obj

    return obj
end

function ItemStack:clone(withSlot)
    local obj = {
        slot = withSlot or self:getSlot(),
        inv = self:getInventory(),
        name = self:getName(),
        damage = self:getDamage(),
        maxDamage = self:getMaxDamage(),
        count = self:getCount(),
        maxCount = self:getMaxCount(),
        enchantments = self:getEnchantments(),
        displayName = self:getDisplayName(),
        nbt = self:getNBT()
    }

    setmetatable(obj, self)
    obj.__index = obj

    return obj
end

function ItemStack:toSerializable()
  local ser = {
    slot = self:getSlot(),
    maxCount = self:getMaxCount()
  }

  if not self:isEmpty() then
    -- Not empty
    ser.name = self:getName()
    ser.damage = self:getDamage()
    ser.maxDamage = self:getMaxDamage()
    ser.count = self:getCount()
    ser.enchantments = self:getEnchantments()
    ser.displayName = self:getDisplayName()
    ser.nbt = self:getNBT()
  end

  return ser
end

function ItemStack:fromSerializable(ser, inv)
    ser.inv = inv

    setmetatable(ser, self)
    ser.__index = ser

    return ser
end

function ItemStack:getCount()
  return self.count or 0
end

function ItemStack:getName()
  return self.name
end

function ItemStack:getDamage()
  return self.damage
end

function ItemStack:getMaxDamage()
  return self.maxDamage
end

function ItemStack:getMaxCount()
  return self.maxCount
end

function ItemStack:getEnchantments()
  return self.enchantments
end

function ItemStack:getSlot()
  return self.slot
end

function ItemStack:getInventory()
  return self.inv
end

function ItemStack:getNBT()
  return self.nbt
end

function ItemStack:isEmpty()
  local count = self:getCount()
  return count == nil or count == 0
end

function ItemStack:getDisplayName()
  return self.displayName
end

function ItemStack:getSimpleName()
  local displayName = self:getDisplayName()
  if displayName ~= nil then
    return displayName
  end

  local name = self:getName()
  if name ~= nil then
    local _, e = name:find(":")
    if e == nil then
      return name
    end

    local simpleName = name:sub(e + 1)
    if #simpleName == 0 then
      return name
    end

    return simpleName
  end

  return Inventory.getSimpleName(self:getInventory()).."["..(self:getSlot() or "NO_SLOT").."]"
end

function ItemStack:hasChanged(listObj, thorough)
  local listItem = listObj[self:getSlot()]
  if listItem == nil or listItem.name ~= self:getName() or listItem.count ~= self:getCount() then
    return true
  end

  local inv = self:getInventory()
  local slot = self:getSlot()
  return thorough and (self ~= self:fromDetail(inv, inv.getItemDetail(slot), slot))
end

function ItemStack:_modify(countDelta, stack)
  local newCount = self:getCount() + countDelta
  if newCount < 0 then
    error("ERROR: New stack count is negative: "..newCount)
  end

  if newCount == 0 then
    -- Clear data
    self.maxCount = Util.getItemLimit(self:getInventory(), self:getSlot())
    self.name = nil
    self.damage = nil
    self.maxDamage = nil
    self.count = nil
    self.enchantments = nil
    self.displayName = nil
    self.nbt = nil
  else
    -- If stack is empty, copy stack data from source
    if self:isEmpty() then
      self.name = stack:getName()
      self.damage = stack:getDamage()
      self.maxDamage = stack:getMaxDamage()
      self.maxCount = stack:getMaxCount()
      self.enchantments = stack:getEnchantments()
      self.displayName = stack:getDisplayName()
      self.nbt = stack:getNBT()
    end

    self.count = newCount
  end
end

function ItemStack:transferTo(target, count)
  local cap = math.min(count or self:getCount(), target:getMaxCount() - target:getCount(), self:getCount())

  -- If we can't transfer any data, then 
  if cap == 0 then
    return count == 0, 0
  end

  local errC = 0
  local result
  repeat
    result = { pcall(self:getInventory().pushItems, peripheral.getName(target:getInventory()), self:getSlot(), cap, target:getSlot()) }
    errC = errC + 1
  until (not result[1]) or (result[2] ~= nil) or (errC > 8)

  if not result[1] then
    return false, result[2]
  end

  target:_modify(result[2], self)
  self:_modify(-result[2], self)

  return result[2] == count, result[2]
end

local function objEquals(o1, o2)
  local objType = type(o1)
  if objType ~= type(o2) then
    return false
  end

  if objType == "table" then
    if #o1 ~= #o2 then
        return false
    end

    for i,v in ipairs(o1) do
        if not objEquals(v, o2[i]) then
            return false
        end
    end

    for k,v in pairs(o1) do
        if not objEquals(v, o2[k]) then
            return false
        end
    end

    return true
  else
    return o1 == o2
  end
end

local function runGetter(obj, name)
  local fun = obj[name]
  if type(fun) ~= "function" then
    return false, ("Function '%s' cannot be found in object"):format(name)
  end
  return pcall(fun, obj)
end

local function checkEqual(obj1, obj2, funcName)
  local result1 = { runGetter(obj1, funcName) }
  local result2 = { runGetter(obj2, funcName) }
  return result1[1] and result2[1] and (result1[2] == result2[2])
end

local function checkEnchantments(obj1, obj2)
  local GETTER_FUNC_NAME = "getEnchantments"
  local result1 = { runGetter(obj1, GETTER_FUNC_NAME) }
  local result2 = { runGetter(obj2, GETTER_FUNC_NAME) }
  return result1[1] and result2[1] and objEquals(result1[2], result2[2])
end

function ItemStack:__eq(other)
  return type(other) == "table" and
          checkEqual(self, other, "getCount") and
          checkEqual(self, other, "getMaxCount") and
          self:canTransfer(other)
end

-- Determines if two stacks can be transferred to eachother
-- Empty stacks are always valid transfer nodes
function ItemStack:canTransfer(stack)
  return self:isEmpty() or stack:isEmpty() or (
    checkEqual(self, stack, "getName") and
    checkEqual(self, stack, "getDamage") and
    checkEqual(self, stack, "getMaxDamage") and
    checkEqual(self, stack, "getDisplayName") and
    checkEqual(self, stack, "getNBT") and
    checkEnchantments(self, stack)
  )
end

local function queryField(query, field)
  local queryType = type(query)
  if queryType == "nil" then
    return true
  elseif queryType == "function" then
    return query(field)
  end

  return objEquals(query, field)
end

--- Matches a query object/function(s)
--- @param query table | function
function ItemStack:matches(query)
  if query == nil then
    return true
  elseif type(query) == "function" then
    return query(self)
  end

  return queryField(query.name, self:getName()) and
          queryField(query.damage, self:getDamage()) and
          queryField(query.count, self:getCount()) and
          queryField(query.maxDamage, self:getMaxDamage()) and
          queryField(query.enchantments, self:getEnchantments()) and
          queryField(query.maxCount, self:getMaxCount()) and
          queryField(query.nbt, self:getNBT()) and
          queryField(query.displayName, self:getDisplayName())
end

return ItemStack