local Inventory = require("storage.inventory")
local Sentinel = require("storage.sentinel")
local Logging = require("logging")
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 = inv.getItemLimit(slot)
    else
        obj.name = detail.name
        obj.damage = detail.damage
        obj.maxDamage = detail.maxDamage
        obj.count = detail.count
        obj.maxCount = detail.maxCount
        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.slot,
        inv = self.inv,
        name = self.name,
        damage = self.damage,
        maxDamage = self.maxDamage,
        count = self.count,
        maxCount = self.maxCount,
        enchantments = self.enchantments,
        displayName = self.displayName,
        nbt = self.nbt
    }

    setmetatable(obj, self)
    obj.__index = obj

    return obj
end

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

  if not self:isEmpty() then
    -- Not empty
    ser.name = self.name
    ser.damage = self.damage
    ser.maxDamage = self.maxDamage
    ser.count = self.count
    ser.enchantments = self.enchantments
    ser.displayName = self.displayName
    ser.nbt = self.nbt
  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()
  return self.count == nil or self.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.slot]
  if listItem == nil or listItem.name ~= self.name or listItem.count ~= self.count then
    return true
  end

  return thorough and (self ~= self:fromDetail(self.inv, self.inv.getItemDetail(self.slot), self.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 = self.inv.getItemLimit(self.slot)
    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.maxCount = stack.maxCount
      self.name = stack.name
      self.damage = stack.damage
      self.maxDamage = stack.maxDamage
      self.maxCount = stack.maxCount
      self.enchantments = stack.enchantments
      self.displayName = stack.displayName
      self.nbt = stack.nbt
    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

function ItemStack:__eq(other)
  return type(other) == "table" and
          self.count == other.count and
          self.maxCount == other.maxCount 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 (self.name == stack.name and
          self.damage == stack.damage and
          self.maxDamage == stack.maxDamage and
          self.displayName == stack.displayName and
          self.nbt == stack.nbt and
          objEquals(self.enchantments, stack.enchantments))
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.name) and
          queryField(query.damage, self.damage) and
          queryField(query.count, self.count) and
          queryField(query.maxDamage, self.maxDamage) and
          queryField(query.enchantments, self.enchantments) and
          queryField(query.maxCount, self.maxCount) and
          queryField(query.nbt, self.nbt) and
          queryField(query.displayName, self.displayName)
end

return ItemStack