--[[	*** DataStore_Containers ***
Written by : Thaoky, EU-Marécages de Zangar
June 21st, 2009
--]]
if not DataStore then return end

local addonName = "DataStore_Containers"

_G[addonName] = LibStub("AceAddon-3.0"):NewAddon(addonName, "AceConsole-3.0", "AceEvent-3.0")

local addon = _G[addonName]

local THIS_ACCOUNT = "Default"
local BI = LibStub("LibBabble-Inventory-3.0"):GetLookupTable()

local AddonDB_Defaults = {
	global = {
		Guilds = {
			['*'] = {			-- ["Account.Realm.Name"] 
				['*'] = {		-- tabID = table index [1] to [6]
					name = nil,
					visitedBy = "",
					ClientTime = 0,				-- since epoch
					ClientDate = nil,
					ClientHour = nil,
					ClientMinute = nil,
					ServerHour = nil,
					ServerMinute = nil,
					ids = {},
					links = {},
					counts = {}
				}
			}
		},
		Characters = {
			['*'] = {					-- ["Account.Realm.Name"] 
				lastUpdate = nil,
				numBagSlots = 0,
				numFreeBagSlots = 0,
				numBankSlots = 0,
				numFreeBankSlots = 0,
				Containers = {
					['*'] = {					-- Containers["Bag0"]
						icon = nil,				-- Containers's texture
						link = nil,				-- Containers's itemlink
						size = 0,
						freeslots = 0,
						bagtype = 0,
						ids = {},
						links = {},
						counts = {},
						cooldowns = {}
					}
				}
			}
		}
	}
}

-- *** Utility functions ***
local function IsEnchanted(link)
	if not link then return end
	
	if not string.find(link, "0:0:0:0:0:0:0") then
		-- enchants/jewels store values instead of zeroes in the link, if this string can't be found, there's at least one enchant/jewel
		return true
	end
end

local BAGS			= 1		-- All bags, 0 to 11, and keyring ( id -2 )
local BANK			= 2		-- 28 main slots
local GUILDBANK	= 3		-- 98 main slots

local ContainerTypes = {
	[BAGS] = {
		GetSize = function(self, bagID)
				return GetContainerNumSlots(bagID)
			end,
		GetFreeSlots = function(self, bagID)
				local freeSlots, bagType = GetContainerNumFreeSlots(bagID)
				return freeSlots, bagType
			end,
		GetLink = function(self, slotID, bagID)
				return GetContainerItemLink(bagID, slotID)
			end,
		GetCount = function(self, slotID, bagID)
				local _, count = GetContainerItemInfo(bagID, slotID)
				return count
			end,
		GetCooldown = function(self, slotID, bagID)
				local startTime, duration, isEnabled = GetContainerItemCooldown(bagID, slotID)
				return startTime, duration, isEnabled
			end,
	},
	[BANK] = {
		GetSize = function(self)
				return NUM_BANKGENERIC_SLOTS		-- 28
			end,
		GetFreeSlots = function(self)
				local freeSlots, bagType = GetContainerNumFreeSlots(-1)		-- -1 = player bank
				return freeSlots, bagType
			end,
		GetLink = function(self, slotID)
				return GetInventoryItemLink("player", slotID)
			end,
		GetCount = function(self, slotID)
				return GetInventoryItemCount("player", slotID)
			end,
		GetCooldown = function(self, slotID)
				local startTime, duration, isEnabled = GetInventoryItemCooldown("player", slotID)
				return startTime, duration, isEnabled
			end,
	},
	[GUILDBANK] = {
		GetSize = function(self)
				return MAX_GUILDBANK_SLOTS_PER_TAB		-- 98
			end,
		GetFreeSlots = function(self)
				return nil, nil
			end,
		GetLink = function(self, slotID, tabID)
				return GetGuildBankItemLink(tabID, slotID)
			end,
		GetCount = function(self, slotID, tabID)
				local _, count = GetGuildBankItemInfo(tabID, slotID)
				return count
			end,
		GetCooldown = function(self, slotID)
				return nil
			end,
	}
}

-- *** Scanning functions ***
local function ScanContainer(bagID, containerType)
	local Container = ContainerTypes[containerType]
	
	local bag
	if containerType == GUILDBANK then
		local guildName = GetGuildInfo("player")
		if not guildName then return end		-- to do, add an error message here, should not happen, since a scan is triggered when opening the guild bank
		
		local key = format("%s.%s.%s", THIS_ACCOUNT, GetRealmName(), guildName)
		bag = addon.db.global.Guilds[key][bagID]	-- bag is actually the current tab
	else
		bag = addon.ThisCharacter.Containers["Bag" .. bagID]
		wipe(bag.cooldowns)		-- does not exist for a guild bank
	end

	wipe(bag.ids)				-- clean existing bag data
	wipe(bag.counts)
	wipe(bag.links)
	
	local link, count
	local startTime, duration, isEnabled
	
	bag.size = Container:GetSize(bagID)
	bag.freeslots, bag.bagtype = Container:GetFreeSlots(bagID)
	
	-- Scan from 1 to bagsize for normal bags or guild bank tabs, but from 40 to 67 for main bank slots
	local baseIndex = (containerType == BANK) and 39 or 0
	local index
	
	for slotID = baseIndex + 1, baseIndex + bag.size do
		index = slotID - baseIndex
		link = Container:GetLink(slotID, bagID)
		if link then
			bag.ids[index] = tonumber(link:match("item:(%d+)"))

			if IsEnchanted(link) then
				bag.links[index] = link
			end
		
			count = Container:GetCount(slotID, bagID)
			if count and count > 1  then
				bag.counts[index] = count	-- only save the count if it's > 1 (to save some space since a count of 1 is extremely redundant)
			end
		end
		
		startTime, duration, isEnabled = Container:GetCooldown(slotID, bagID)
		if startTime and startTime > 0 then
			bag.cooldowns[index] = startTime .."|".. duration .. "|" .. 1
		end
	end
	
	addon.ThisCharacter.lastUpdate = time()
end

local function ScanBagSlotsInfo()
	local char = addon.ThisCharacter

	local numBagSlots = 0
	local numFreeBagSlots = 0

	for bagID = 0, NUM_BAG_SLOTS do
		local bag = char.Containers["Bag" .. bagID]
		numBagSlots = numBagSlots + bag.size
		numFreeBagSlots = numFreeBagSlots + bag.freeslots
	end
	
	char.numBagSlots = numBagSlots
	char.numFreeBagSlots = numFreeBagSlots
end

local function ScanBankSlotsInfo()
	local char = addon.ThisCharacter
	
	local numBankSlots = NUM_BANKGENERIC_SLOTS
	local numFreeBankSlots = char.Containers["Bag100"].freeslots

	for bagID = NUM_BAG_SLOTS, NUM_BAG_SLOTS + NUM_BANKBAGSLOTS do		-- 5 to 11
		local bag = char.Containers["Bag" .. bagID]
		
		numBankSlots = numBankSlots + bag.size
		numFreeBankSlots = numFreeBankSlots + bag.freeslots
	end
	
	char.numBankSlots = numBankSlots
	char.numFreeBankSlots = numFreeBankSlots
end

local function ScanGuildBankInfo()
	-- only the current tab can be updated
	local guildName = GetGuildInfo("player")
	local tabID = GetCurrentGuildBankTab()
	
	local key = format("%s.%s.%s", THIS_ACCOUNT, GetRealmName(), guildName)
	local t = addon.db.global.Guilds[key][tabID]	-- t = current tab

	t.name = GetGuildBankTabInfo(tabID)
	t.visitedBy = UnitName("player")
	t.ClientTime = time()
	if GetLocale() == "enUS" then				-- adjust this test if there's demand
		t.ClientDate = date("%m/%d/%Y")
	else
		t.ClientDate = date("%d/%m/%Y")
	end
	t.ClientHour = tonumber(date("%H"))
	t.ClientMinute = tonumber(date("%M"))
	t.ServerHour, t.ServerMinute = GetGameTime()
end

local function ScanBag(bagID)
	if bagID < 0 then return end

	local char = addon.ThisCharacter
	local bag = char.Containers["Bag" .. bagID]
	
	if bagID == 0 then	-- Bag 0	
		bag.icon = "Interface\\Buttons\\Button-Backpack-Up";
		bag.link = nil;
	else						-- Bags 1 through 11
		bag.icon = GetInventoryItemTexture("player", ContainerIDToInventoryID(bagID))
		bag.link = GetInventoryItemLink("player", ContainerIDToInventoryID(bagID))
	end
	ScanContainer(bagID, BAGS)
	ScanBagSlotsInfo()
end

local function ScanKeyRing()
	local char = addon.ThisCharacter
	local bag = char.Containers["Bag" .. KEYRING_CONTAINER]

	bag.icon = "Interface\\Icons\\INV_Misc_Key_14";
	bag.link = nil
	ScanContainer(KEYRING_CONTAINER, BAGS)
end

-- ** Mixins **
local function _GetContainer(character, containerID)
	-- containerID can be number or string
	return character.Containers["Bag" .. containerID]
end

local function _GetContainers(character)
	return character.Containers
end

local BagTypeStrings = {
	[1] = BI["Quiver"],
	[2] = BI["Ammo Pouch"],
	[4] = BI["Soul Bag"],
	[8] = BI["Leatherworking Bag"],
	[16] = BI["Inscription Bag"],
	[32] = BI["Herb Bag"],
	[64] = BI["Enchanting Bag"],
	[128] = BI["Engineering Bag"],
	[512] = BI["Gem Bag"],
	[1024] = BI["Mining Bag"],
}

local function _GetContainerInfo(character, containerID)
	local bag = _GetContainer(character, containerID)
	return bag.icon, bag.link, bag.size, bag.freeslots, BagTypeStrings[bag.bagtype]
end

local function _GetContainerSize(character, containerID)
	-- containerID can be number or string
	return character.Containers["Bag" .. containerID].size
end

local function _GetSlotInfo(bag, slotID)
	assert(type(bag) == "table")		-- this is the pointer to a bag table, obtained through addon:GetContainer()
	assert(type(slotID) == "number")

	-- return itemID, itemLink, itemCount
	return bag.ids[slotID], bag.links[slotID], bag.counts[slotID] or 1
end

local function _GetContainerCooldownInfo(bag, slotID)
	assert(type(bag) == "table")		-- this is the pointer to a bag table, obtained through addon:GetContainer()
	assert(type(slotID) == "number")

	local cd = bag.cooldowns[slotID]
	if cd then
		local startTime, duration, isEnabled = strsplit("|", bag.cooldowns[slotID])
		local remaining = duration - (GetTime() - startTime)
		
		if remaining > 0 then		-- valid cd ? return it
			return tonumber(startTime), tonumber(duration), tonumber(isEnabled)
		end
		-- cooldown expired ? clean it from the db
		bag.cooldowns[slotID] = nil
	end
end

local function _GetContainerItemCount(character, searchedID)
	local bagCount = 0
	local bankCount = 0
	local id
	
	for containerName, container in pairs(character.Containers) do
		for slotID=1, container.size do
			id = container.ids[slotID]
			
			if (id) and (id == searchedID) then
				local itemCount = container.counts[slotID] or 1
				
				if (containerName == "Bag100") then
					bankCount = bankCount + itemCount
				elseif (containerName == "Bag-2") then
					bagCount = bagCount + itemCount
				else
					local bagNum = tonumber(string.sub(containerName, 4))
					if (bagNum >= 0) and (bagNum <= 4) then
						bagCount = bagCount + itemCount
					else
						bankCount = bankCount + itemCount
					end
				end
			end
		end
	end

	return bagCount, bankCount
end

local function _GetNumBagSlots(character)
	return character.numBagSlots
end

local function _GetNumFreeBagSlots(character)
	return character.numFreeBagSlots
end

local function _GetNumBankSlots(character)
	return character.numBankSlots
end

local function _GetNumFreeBankSlots(character)
	return character.numFreeBankSlots
end

local function _GetGuild(name, realm, account)
	realm = realm or GetRealmName()
	account = account or THIS_ACCOUNT
	
	local key = format("%s.%s.%s", account, realm, name)
	return addon.db.global.Guilds[key]
end

local function _GetGuilds(realm, account)
	-- get a list of guilds on a given realm/account
	realm = realm or GetRealmName()
	account = account or THIS_ACCOUNT
	
	local out = {}
	local accountKey, realmKey, guildKey
	for k, guildTable in pairs(addon.db.global.Guilds) do
		accountKey, realmKey, guildKey = strsplit(".", k)
		
		if accountKey and realmKey then
			if accountKey == account and realmKey == realm then
				out[guildKey] = guildTable	
				-- this allows to iterate with this kind of loop:
					-- for guildName, guild in pairs(DS:GetGuilds(realm, account)) do
						-- do stuff with guildName only
						-- or do stuff with the "guild" key to pass to other DataStore functions
					-- end
			end
		end
	end
	
	return out
end
	
local function _DeleteGuild(name, realm, account)
	realm = realm or GetRealmName()
	account = account or THIS_ACCOUNT
	
	local key = format("%s.%s.%s", account, realm, name)
	addon.db.global.Guilds[key] = nil
end

local function _GetGuildBankItemCount(searchedID, guildName, realm, account)
	local count = 0
	for _, container in pairs(_GetGuild(guildName, realm, account)) do
	   for slotID, id in pairs(container.ids) do
	      if (id == searchedID) then
	         count = count + (container.counts[slotID] or 1)
	      end
	   end
	end
	return count
end
	
local function _GetGuildBankTab(guild, tabID)
	assert(type(guild) == "table")		-- this is the pointer to a guild table, obtained through addon:GetGuild()
	return guild[tabID]
end
	
local function _GetGuildBankTabName(guild, tabID)
	assert(type(guild) == "table")		-- this is the pointer to a guild table, obtained through addon:GetGuild()
	return guild[tabID].name
end

local function _GetGuildBankTabItemCount(guild, tabID, searchedID)
	assert(type(guild) == "table")		-- this is the pointer to a guild table, obtained through addon:GetGuild()
	
	local count = 0
	local container = guild[tabID]
	
	for slotID, id in pairs(container.ids) do
		if (id == searchedID) then
			count = count + (container.counts[slotID] or 1)
		end
	end
	return count
end

local function _GetGuildBankTabLastUpdate(guild, tabID)
	assert(type(guild) == "table")		-- this is the pointer to a guild table, obtained through addon:GetGuild()
	return guild[tabID].ClientTime
end

local function _ImportGuildBankTab(guild, tabID, data)
	assert(type(guild) == "table")		-- this is the pointer to a guild table, obtained through addon:GetGuild()

	wipe(guild[tabID])							-- clear existing data
	guild[tabID] = data
end

local PublicMethods = {
	GetContainer = _GetContainer,
	GetContainers = _GetContainers,
	GetContainerInfo = _GetContainerInfo,
	GetContainerSize = _GetContainerSize,
	GetSlotInfo = _GetSlotInfo,
	GetContainerCooldownInfo = _GetContainerCooldownInfo,
	GetContainerItemCount = _GetContainerItemCount,
	GetNumBagSlots = _GetNumBagSlots,
	GetNumFreeBagSlots = _GetNumFreeBagSlots,
	GetNumBankSlots = _GetNumBankSlots,
	GetNumFreeBankSlots = _GetNumFreeBankSlots,
	GetGuild = _GetGuild,
	GetGuilds = _GetGuilds,
	DeleteGuild = _DeleteGuild,
	GetGuildBankItemCount = _GetGuildBankItemCount,
	GetGuildBankTab = _GetGuildBankTab,
	GetGuildBankTabName = _GetGuildBankTabName,
	GetGuildBankTabItemCount = _GetGuildBankTabItemCount,
	GetGuildBankTabLastUpdate = _GetGuildBankTabLastUpdate,
	ImportGuildBankTab = _ImportGuildBankTab,
}

function addon:OnInitialize()
	addon.db = LibStub("AceDB-3.0"):New(addonName .. "DB", AddonDB_Defaults)

	DataStore:RegisterModule(addonName, addon, PublicMethods)
	DataStore:SetCharacterBasedMethod("GetContainer")
	DataStore:SetCharacterBasedMethod("GetContainers")
	DataStore:SetCharacterBasedMethod("GetContainerInfo")
	DataStore:SetCharacterBasedMethod("GetContainerSize")
	DataStore:SetCharacterBasedMethod("GetContainerItemCount")
	DataStore:SetCharacterBasedMethod("GetNumBagSlots")
	DataStore:SetCharacterBasedMethod("GetNumFreeBagSlots")
	DataStore:SetCharacterBasedMethod("GetNumBankSlots")
	DataStore:SetCharacterBasedMethod("GetNumFreeBankSlots")
end

function addon:OnEnable()
	-- manually update bags 0 to 4, then register the event, this avoids reacting to the flood of BAG_UPDATE events at login
	for bagID = 0, NUM_BAG_SLOTS do
		ScanBag(bagID)
	end
	ScanKeyRing()
	
	addon:RegisterEvent("BAG_UPDATE")
	addon:RegisterEvent("BANKFRAME_OPENED")
	addon:RegisterEvent("GUILDBANKFRAME_OPENED")
end

function addon:OnDisable()
	addon:UnregisterEvent("BAG_UPDATE")
	addon:UnregisterEvent("BANKFRAME_OPENED")
	addon:UnregisterEvent("GUILDBANKFRAME_OPENED")
end


-- *** EVENT HANDLERS ***
function addon:BAG_UPDATE(event, bag)
	if bag < 0 then
		return
	end
	
	if (bag >= 5) and (bag <= 11) and not addon.isBankOpen then
		return
	end
	
	if bag == 0 then					-- bag is 0 for both the keyring and the original backpack
		ScanKeyRing()
	end
	ScanBag(bag)
end

function addon:BANKFRAME_OPENED()
	addon.isBankOpen = true
	for bagID = NUM_BAG_SLOTS, NUM_BAG_SLOTS + NUM_BANKBAGSLOTS do		-- 5 to 11
		ScanBag(bagID)
	end
	ScanContainer(100, BANK)
	ScanBankSlotsInfo()
	addon:RegisterEvent("BANKFRAME_CLOSED")
	addon:RegisterEvent("PLAYERBANKSLOTS_CHANGED")
end

function addon:BANKFRAME_CLOSED()
	addon.isBankOpen = nil
	addon:UnregisterEvent("BANKFRAME_CLOSED")
	addon:UnregisterEvent("PLAYERBANKSLOTS_CHANGED")
end

function addon:PLAYERBANKSLOTS_CHANGED(event, slotID)
	-- from top left to bottom right, slotID = 1 to 28for main slots, and 29 to 35 for the additional bags
	if (slotID >= 29) and (slotID <= 35) then
		ScanBag(slotID - 24)		-- bagID for bank bags goes from 5 to 11, so slotID - 24
	else
		ScanContainer(100, BANK)
		ScanBankSlotsInfo()
	end
end

function addon:GUILDBANKFRAME_OPENED()
	addon:RegisterEvent("GUILDBANKFRAME_CLOSED")
	addon:RegisterEvent("GUILDBANKBAGSLOTS_CHANGED")
end

function addon:GUILDBANKBAGSLOTS_CHANGED()
	ScanContainer(GetCurrentGuildBankTab(), GUILDBANK)
	ScanGuildBankInfo()
end

function addon:GUILDBANKFRAME_CLOSED()
	addon:UnregisterEvent("GUILDBANKFRAME_CLOSED")
	addon:UnregisterEvent("GUILDBANKBAGSLOTS_CHANGED")
end
