local Lib = LibStub:NewLibrary('LibItemCache-2.0', 20)
if not Lib then
	return
end

local FACTION = UnitFactionGroup('player')
local COMPLETE_LINK = '|c.+|H.+|h.+|h|r'
local PET_LINK = '|c%s|Hbattlepet:%sx0|h[%s]|h|r'
local PET_STRING = '^' .. strrep('%d+:', 6) .. '%d+$'
local EMPTY_FUNC = function() end
local PLAYER, REALM, REALMS

local FindRealms = function()
	if not REALM then
		PLAYER, REALM = UnitFullName('player')
		REALMS = GetAutoCompleteRealms()

		if not REALMS or #REALMS == 0 then
			REALMS = {REALM}
		end
	end
end

local Caches = {}
local AccessInterfaces = function(self, key)
	for name, lib in LibStub:IterateLibraries() do
		if lib.IsItemCache and lib[key] then
			self[key] = lib[key]
			return self[key]
		end
	end

	return EMPTY_FUNC
end

setmetatable(Caches, { __index = AccessInterfaces })
LibStub('AceEvent-3.0'):Embed(Lib)

Lib:RegisterEvent('BANKFRAME_OPENED', function() Lib.AtBank = true; Lib:SendMessage('CACHE_BANK_OPENED') end)
Lib:RegisterEvent('BANKFRAME_CLOSED', function() Lib.AtBank = false; Lib:SendMessage('CACHE_BANK_CLOSED') end)

if CanUseVoidStorage then
	Lib:RegisterEvent('VOID_STORAGE_OPEN', function() Lib.AtVault = true; Lib:SendMessage('CACHE_VAULT_OPENED') end)
	Lib:RegisterEvent('VOID_STORAGE_CLOSE', function() Lib.AtVault = false; Lib:SendMessage('CACHE_VAULT_CLOSED') end)
end

if CanGuildBankRepair then
	Lib:RegisterEvent('GUILDBANKFRAME_OPENED', function() Lib.AtGuild = true; Lib:SendMessage('CACHE_GUILD_OPENED') end)
	Lib:RegisterEvent('GUILDBANKFRAME_CLOSED', function() Lib.AtGuild = false; Lib:SendMessage('CACHE_GUILD_CLOSED') end)
end


--[[ Owners ]]--

function Lib:GetOwnerInfo(owner)
	local realm, name, isguild = self:GetOwnerAddress(owner)
	local cached = self:IsOwnerCached(realm, name, isguild)

	local api = isguild and 'GetGuild' or 'GetPlayer'
	local owner = cached and Caches[api](Caches, realm, name) or {}
	if not cached then
		owner.faction = FACTION

		if isguild then
			-- what information should go here?
		else
			owner.money = (GetMoney() or 0) - GetCursorMoney() - GetPlayerTradeMoney()
			owner.class = select(2, UnitClass('player'))
			owner.race = select(2, UnitRace('player'))
			owner.guild = GetGuildInfo('player')
			owner.gender = UnitSex('player')
		end
	end

	owner.guild = owner.guild and ('® ' .. owner.guild .. ' - ' .. realm)
	owner.name, owner.realm, owner.isguild = name, realm, isguild
	owner.cached = cached

	return owner
end

function Lib:DeleteOwnerInfo(owner)
	local realm, name, isguild = self:GetOwnerAddress(owner)
	local cached = self:IsOwnerCached(realm, name, isguild)

	if cached then
		if isguild then
			Caches:DeleteGuild(realm, name)
		else
			Caches:DeletePlayer(realm, name)
		end
	end
end

function Lib:IterateOwners()
	FindRealms()

	local i, players, guilds, suffix = 0
	return function()
		while i <= #REALMS do
			local owner = players and players()
			if owner then
				return owner .. suffix
			else
				players = nil
			end

			local owner = guilds and guilds()
			if owner then
				return '® ' .. owner .. suffix
			end

			i = i + 1
			if i <= #REALMS then
				players = Caches:GetPlayers(REALMS[i])
				guilds = Caches:GetGuilds(REALMS[i])
				suffix = REALMS[i] ~= REALM and ' - ' .. REALMS[i] or ''
			end
		end
	end
end


--[[ Items and Bags ]]--

function Lib:GetBagInfo(owner, bag)
	local realm, name, isguild = self:GetOwnerAddress(owner)
	local cached = self:IsBagCached(realm, name, isguild, bag)

	local api = isguild and 'GetGuildTab' or 'GetBag'
	local query = cached or isguild and bag ~= GetCurrentGuildBankTab()
	local item = query and Caches[api](Caches, realm, name, bag) or {}

	if isguild then
		item.count = 98
	elseif bag == 'vault' then
		item.count = 160
	elseif bag == 'equip' then
		item.count = INVSLOT_LAST_EQUIPPED
	else
		if bag == REAGENTBANK_CONTAINER or bag == BACKPACK_CONTAINER or bag == BANK_CONTAINER then
			item.count = GetContainerNumSlots(bag)
		end

		item.owned = item.owned or (bag >= KEYRING_CONTAINER and bag <= NUM_BAG_SLOTS) or item.id or item.link
	end

	if cached then
		item.cached = true
	elseif isguild then
		local name, icon, view, deposit, withdraw, remaining = GetGuildBankTabInfo(bag)
		if not query then
			item.deposit, item.withdraw, item.remaining = deposit, withdraw, remaining
		end

		item.name, item.icon, item.viewable = name, icon, view
	elseif tonumber(bag) then
		item.free = GetContainerNumFreeSlots(bag)

		if bag == REAGENTBANK_CONTAINER then
			item.cost = GetReagentBankCost()
			item.owned = IsReagentBankUnlocked()
		elseif bag ~= BACKPACK_CONTAINER and bag ~= BANK_CONTAINER then
			item.slot = ContainerIDToInventoryID(bag)
			item.link = GetInventoryItemLink('player', item.slot)
			item.icon = GetInventoryItemTexture('player', item.slot)
			item.count = GetContainerNumSlots(bag)

			if bag > NUM_BAG_SLOTS then
				item.owned = (bag - NUM_BAG_SLOTS) <= GetNumBankSlots()
				item.cost = GetBankSlotCost()
			end
		end
	end

	return self:RestoreItemData(item)
end

function Lib:GetItemInfo(owner, bag, slot)
	local realm, name, isguild = self:GetOwnerAddress(owner)
	local cached = self:IsBagCached(realm, name, isguild, bag)

	local api = isguild and 'GetGuildItem' or 'GetItem'
	local item = cached and Caches[api](Caches, realm, name, bag, slot) or {}

	if cached then
		item.cached = true
	elseif isguild then
		item.link = GetGuildBankItemLink(bag, slot)
		item.icon, item.count, item.locked = GetGuildBankItemInfo(bag, slot)
	elseif bag == 'equip' then
		item.link = GetInventoryItemLink('player', slot)
	elseif bag == 'vault' then
		item.id, item.icon, item.locked, item.recent, item.filtered, item.quality = GetVoidItemInfo(1, slot)
	else
		item.icon, item.count, item.locked, item.quality, item.readable, item.lootable, item.link, item.filtered, item.worthless, item.id = GetContainerItemInfo(bag, slot)
	end

	return self:RestoreItemData(item)
end

function Lib:GetItemID(owner, bag, slot)
	local realm, name, isguild = self:GetOwnerAddress(owner)
	local cached = self:IsBagCached(realm, name, isguild, bag)

	if cached then
		local api = isguild and 'GetGuildItem' or 'GetItem'
		local item = Caches[api](Caches, realm, name, bag, slot)

		return item and (item.id or item.link and tonumber(item.link:match('(%d+)')))
	elseif isguild then
		local link = GetGuildBankItemLink(bag, slot)
		return link and tonumber(link:match('item:(%d+)'))
	elseif bag == 'equip' then
		return GetInventoryItemID('player', slot)
	elseif bag == 'vault' then
		return GetVoidItemInfo(1, slot)
	else
		return GetContainerItemID(bag, slot)
	end
end

function Lib:PickupItem(owner, bag, slot)
	local realm, name, isguild = self:GetOwnerAddress(owner)
	local cached = self:IsBagCached(realm, name, isguild, bag)

	if not cached then
		if isguild then
			PickupGuildBankItem(slot)
		elseif bag == 'equip' then
			PickupInventoryItem(slot)
		elseif bag == 'vault' then
			ClickVoidStorageSlot(1, slot)
		else
			PickupContainerItem(bag, slot)
		end
	end
end


--[[ Advanced ]]--

function Lib:GetOwnerID(owner)
	local realm, name, isguild = self:GetOwnerAddress(owner)
	return (isguild and '® ' or '') .. name .. ' - ' .. realm
end

function Lib:GetOwnerAddress(owner)
	FindRealms()

	if not owner then
		return REALM, PLAYER
	end

	local first, realm = strmatch(owner, '^(.-) *%- *(.+)$')
	local isguild, name = strmatch(first or owner, '^(®) *(.+)')
	return realm or REALM, name or first or owner, isguild and true
end

function Lib:IsOwnerCached(realm, name, isguild)
	if isguild then
		return realm ~= REALM or name ~= GetGuildInfo('player')
	else
		return realm ~= REALM or name ~= PLAYER
	end
end

function Lib:IsBagCached(realm, name, isguild, bag)
	if self:IsOwnerCached(realm, name, isguild) then
		return true
	end

	if isguild then
		return not self.AtGuild
	end

	local isBankBag = bag == BANK_CONTAINER or bag == REAGENTBANK_CONTAINER or type(bag) == 'number' and bag > NUM_BAG_SLOTS
	return isBankBag and not self.AtBank or bag == 'vault' and not self.AtVault
end

function Lib:RestoreItemData(item)
	local link, id, quality, icon = self:RestoreLinkData(item.link or item.id)
	local query = link or item.id or id

	item.icon = item.icon or icon or query and GetItemIcon(query)
	item.quality = (not item.quality or item.quality < 0) and quality or item.quality
	item.id = item.id or id
	item.link = link
	return item
end

function Lib:RestoreLinkData(partial)
	if type(partial) == 'string' and not partial:find(COMPLETE_LINK) then
		if partial:sub(1,9) == 'battlepet' or partial:find(PET_STRING) then
			local id, quality = partial:match('(%d+):%d+:(%d+)')
			local id, quality = tonumber(id), tonumber(quality)
			local name, icon = C_PetJournal.GetPetInfoBySpeciesID(id)
			local color = select(4, GetItemQualityColor(quality))

			return PET_LINK:format(color, partial, name), id, quality, icon
		elseif partial:sub(1,5) ~= 'item:' then
			partial = 'item:' .. partial
		end
	end

	if partial then
		local _, link, quality, _, _, _, _, _, _, icon = GetItemInfo(partial)
		local id = tonumber(partial) or tonumber(partial:match('^(%d+)') or partial:match('item:(%d+)'))
		return link, id, quality, icon
	end
end
