itemstore.containers = {}
itemstore.containers.Active = {}

local Container = {}

AccessorFunc(Container, "Owner", "Owner")
AccessorFunc(Container, "Suppressed", "Suppressed", FORCE_BOOL)
AccessorFunc(Container, "Width", "Width", FORCE_NUMBER)
AccessorFunc(Container, "Height", "Height", FORCE_NUMBER)
AccessorFunc(Container, "Pages", "Pages", FORCE_NUMBER)

function Container:GetID()
	return self.ID
end

function Container:IsValid()
	return self:GetID() and itemstore.containers.Active[self:GetID()] == self
end

function Container:Remove()
	itemstore.containers.Remove(self:GetID())
end

function Container:GetPageSize()
	return self:GetWidth() * self:GetHeight()
end

function Container:GetPageFromSlot(slot)
	return math.ceil(slot / self:GetPageSize())
end

function Container:GetSize()
	return self:GetPageSize() * self:GetPages()
end

function Container:CoordsToSlot(x, y, p)
	return ((p - 1) * self:GetPageSize()) + ((y - 1) * self:GetWidth() + x)
end

function Container:GetItems()
	return self.Items
end

function Container:GetItem(slot)
	return self.Items[slot]
end

function Container:SetItem(slot, item)
	if item and !item:IsValid() then return end
	slot = math.floor(slot)
	if slot >= 1 then
		if self:RunCallbacks("set", slot, item) == false then return end
		self.Items[slot] = item
		if item then
			item.Container = self
			item.Slot = slot
		end
		if SERVER then
			self:QueueSync()
		end
	end
end

function Container:FirstEmptySlot()
	for i = 1, self:GetSize() do
		if !self:GetItem(i) then
			return i
		end
	end
	return false
end

function Container:CanFit(item)
	for i = 1, self:GetSize() do
		local merge_item = self:GetItem(i)
		if merge_item and merge_item:CanMerge(item) then
			return true
		end
	end
	return self:FirstEmptySlot() != false
end

function Container:AddItem(item, dontmerge)
	if !dontmerge then
		for i = 1, self:GetSize() do
			local merge_item = self:GetItem(i)
			if merge_item and merge_item:CanMerge(item) then
				merge_item:Merge(item)
				self:QueueSync()
				return i, merge_item
			end
		end
	end
	local slot = self:FirstEmptySlot()
	if slot then
		self:SetItem(slot, item)
		return slot
	end
	return false
end

function Container:HasItem(item_class)
	for k, v in pairs(self:GetItems()) do
		if v:GetClass() == item_class then
			return k
		end
	end
	return false
end

function Container:CountItems(item_class)
	local amount = 0
	for _, item in pairs(self:GetItems()) do
		if item:GetClass() == item_class then
			amount = amount + item:GetAmount()
		end
	end
	return amount
end

function Container:TakeItems(item_class, amount)
	local amount_taken = 0
	self:Suppress(function()
		for k, v in pairs(self:GetItems()) do
			if v:GetClass() == item_class then
				local amount_to_take = v:GetAmount()
				amount_to_take = math.Clamp(amount_to_take, 0, amount)
				v:SetAmount(v:GetAmount() - amount_to_take)
				if v:GetAmount() <= 0 then
					self:SetItem(k, nil)
				end
				amount_taken = amount_taken + amount_to_take
				amount = amount - amount_to_take
			end
		end
		return true
	end)
	return amount_taken
end

function Container:GetDefaultPermissions()
	return self.DefaultPermissions
end

function Container:GetPermissions(ply)
	return self.Permissions[ply] or self:GetDefaultPermissions()
end

function Container:SetDefaultPermissions(read, write)
	self.DefaultPermissions = {Read = read, Write = write}
end

function Container:SetPermissions(ply, read, write)
	self.Permissions[ply] = {Read = read, Write = write}
end

function Container:CanRead(ply, ...)
	local res = hook.Run("ItemStoreCanRead", self, ply, ...)
	if res != nil then
		return res
	end
	if self.Permissions[ply] then
		return self.Permissions[ply].Read
	end
	local res = self:RunCallbacks("read", ply, ...)
	if res != nil then
		return res
	end
	return self.DefaultPermissions.Read
end

function Container:CanWrite(ply, action, ...)
	local res = hook.Run("ItemStoreCanWrite", self, ply, action, ...)
	if res != nil then
		return res
	end
	if self.Permissions[ply] then
		return self.Permissions[ply].Write
	end
	local res = self:RunCallbacks("write", ply, action, ...)
	if res != nil then
		return res
	end
	return self.DefaultPermissions.Write
end

function Container:AddCallback(name, func)
	if !self.Callbacks[name] then
		self.Callbacks[name] = {}
	end
	return table.insert(self.Callbacks[name], func)
end

function Container:RemoveCallback(name, id)
	if self.Callbacks[name] then
		self.Callbacks[name][id] = nil
	end
end

function Container:RunCallbacks(name, ...)
	if self.Callbacks[name] then
		for _, func in pairs(self.Callbacks[name]) do
			local res = func(self, ...)
			if res != nil then
				return res
			end
		end
	end
end

function Container:GetSyncTargets()
	local players = {}
	for _, ply in ipairs(player.GetAll()) do
		if self:CanRead(ply) then
			table.insert(players, ply)
		end
	end
	return players
end

function Container:Suppress(func)
	self:SetSuppressed(true)
	local sync = func()
	self:SetSuppressed(false)
	if sync then
		self:QueueSync()
	end
end

function Container:QueueSync()
	self.ShouldSync = true
end

function Container:Sync(ply)
	if SERVER then
		itemstore.containers.Sync(self:GetID(), ply)
	end
end

function itemstore.Container(w, h, pages, dontnetwork)
	local con = {
		ShouldSync = false,
		Width = w or 4,
		Height = h or 4,
		Pages = pages or 1,
		Owner = nil,
		Callbacks = {},
		Permissions = {},
		DefaultPermissions = {Read = false, Write = false},
		Items = {}
	}
	setmetatable(con, {__index = Container})
	if !dontnetwork then
		con.ID = table.insert(itemstore.containers.Active, con)
		con:Sync()
	end
	return con
end

function itemstore.containers.Get(id)
	return itemstore.containers.Active[id]
end

function itemstore.containers.Remove(id)
	itemstore.containers.Active[id] = nil
end

if SERVER then
	AddCSLuaFile()

	util.AddNetworkString("ItemStore_Sync")
	function itemstore.containers.Sync(id, ply)
		local con = itemstore.containers.Active[id]
		if !con then return end
		if con:GetSuppressed() then return end
		net.Start("ItemStore_Sync")
			net.WriteUInt(con:GetID(), 32)
			net.WriteUInt(con:GetWidth(), 8)
			net.WriteUInt(con:GetHeight(), 8)
			net.WriteUInt(con:GetPages(), 8)
			net.WriteUInt(table.Count(con.Items), 8)
			for k, v in pairs(con.Items) do
				net.WriteUInt(k, 8)
				local id = util.NetworkStringToID(v.Class)
				if id == 0 then
					if v.Class then
						error(string.format("[Inventory] Tried to send data for unnetworked item %s", v.Class))
					else
						error("[Inventory] Tried to send data for a classless item")
					end
				end
				net.WriteUInt(util.NetworkStringToID(v.Class), 16)
				v:WriteNetworkData()
			end
		net.Send(ply or con:GetSyncTargets())
	end

	hook.Add("Tick", "ItemStore_SyncContainers", function()
		for _,v in pairs(itemstore.containers.Active) do
			if v.ShouldSync then
				v.ShouldSync = false
				v:Sync()
			end
		end
	end)
else
	itemstore.containers.Panels = {}

	function itemstore.containers.UpdatePanels(id)
		for k, v in pairs(itemstore.containers.Panels) do
			if IsValid(v) then
				if v:GetContainerID() == id then
					v:Refresh()
				end
			else
				itemstore.containers.Panels[k] = nil
			end
		end
	end

	net.Receive("ItemStore_Sync", function()
		local id = net.ReadUInt(32)
		local w, h = net.ReadUInt(8), net.ReadUInt(8)
		local pages = net.ReadUInt(8)
		local con = itemstore.Container(w, h, pages, true)
		for i = 1, net.ReadUInt(8) do
			local slot = net.ReadUInt(8)
			local class = util.NetworkIDToString(net.ReadUInt(16))
			local item = itemstore.Item(class)
			item:ReadNetworkData()
			con:SetItem(slot, item)
		end
		con.ID = id
		itemstore.containers.Active[id] = con
		itemstore.containers.UpdatePanels(id)
	end)
end