--[[
	Enchantrix Addon for World of Warcraft(tm).
	Version: 3.5.0.0917 (Platypus)
	Revision: $Id: EnxStorage.lua 899 2006-06-05 11:03:30Z aradan $

	Database functions and saved variables.

	License:
		This program is free software; you can redistribute it and/or
		modify it under the terms of the GNU General Public License
		as published by the Free Software Foundation; either version 2
		of the License, or (at your option) any later version.

		This program is distributed in the hope that it will be useful,
		but WITHOUT ANY WARRANTY; without even the implied warranty of
		MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
		GNU General Public License for more details.

		You should have received a copy of the GNU General Public License
		along with this program(see GLP.txt); if not, write to the Free Software
		Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
]]

-- Global functions
local addonLoaded			-- Enchantrix.Storage.AddonLoaded()
local saveDisenchant		-- Enchantrix.Storage.SaveDisenchant()
local getItemDisenchants	-- Enchantrix.Storage.GetItemDisenchants()

-- Local functions
local unserialize
local serialize
local mergeDisenchant
local normalizeDisenchant
local cleanupDisenchant
local disenchantTotal
local disenchantListHash
local mergeDisenchantLists
local saveLocal
local getLocal

-- Database
local LocalBaseItems = {} -- EnchantedLocal merged by item id
local EnchantedItemTypes = {} -- LocalBaseItems and EnchantedBaseItems merged by type

local N_DISENCHANTS = 1
local N_REAGENTS = 2

Enchantrix.State.MAX_ITEM_ID = 26000

local const = Enchantrix.Constants

function addonLoaded()
	-- Create and setup saved variables
	if not EnchantConfig then EnchantConfig = {} end
	if not EnchantConfig.filters then EnchantConfig.filters = {} end
	if not EnchantConfigChar then EnchantConfigChar = {} end
	if not EnchantConfigChar.filters then EnchantConfigChar.filters = {} end

	if not EnchantConfig.cache then EnchantConfig.cache = {} end
	if not EnchantConfig.cache.reagentinfo then EnchantConfig.cache.reagentinfo = {} end
	if not EnchantConfig.cache.names then EnchantConfig.cache.names = {} end

	if not EnchantedLocal then EnchantedLocal = {} end
	if not EnchantedBaseItems then EnchantedBaseItems = {} end

	if not EnchantConfig.zomgBlizzardAreMeanies then
		-- Push disenchant reagents into cache if needed
		for id in Enchantrix.Constants.StaticPrices do
			if not (Enchantrix.Util.GetReagentInfo(id)) then
				EnchantConfig.zomgBlizzardAreMeanies = true
				GameTooltip:SetHyperlink(string.format("item:%d:0:0:0", id))
				GameTooltip:Hide()
				EnchantConfig.zomgBlizzardAreMeanies = nil
			end
		end
	else
		-- We were kicked from the server last time, better not try that again for a while
		EnchantConfig.zomgBlizzardAreMeanies = nil
	end

	mergeDisenchantLists()
end

function unserialize(str)
	-- Break up a disenchant string to a table for easy manipulation
	local tbl = {}
	if type(str) == "string" then
		for de in Enchantrix.Util.Spliterator(str, ";") do
			local _, _, id, d, r = string.find(de, "(%d+):(%d+):(%d+)")
			id, d, r = tonumber(id), tonumber(d), tonumber(r)
			if (id and d > 0 and r > 0) then
				tbl[id] = {[N_DISENCHANTS] = d, [N_REAGENTS] = r}
			end
		end
	end
	return tbl
end

function serialize(tbl)
	-- Serialize a table into a string
	if type(tbl) == "table" then
		local str
		for id, counts in pairs(tbl) do
			if (type(id) == "number" and counts[N_DISENCHANTS] > 0 and counts[N_REAGENTS] > 0) then
				if (str) then
					str = string.format("%s;%d:%d:%d:0", str, id, counts[N_DISENCHANTS], counts[N_REAGENTS])
				else
					str = string.format("%d:%d:%d:0", id, counts[N_DISENCHANTS], counts[N_REAGENTS])
				end
			end
		end
		return str
	end
end

function mergeDisenchant(str1, str2)
	-- Merge two disenchant strings into a single string
	local tbl1, tbl2 = unserialize(str1), unserialize(str2)
	for id, counts in pairs(tbl2) do
		if (not tbl1[id]) then
			tbl1[id] = counts
		else
			tbl1[id][N_DISENCHANTS] = tbl1[id][N_DISENCHANTS] + counts[N_DISENCHANTS]
			tbl1[id][N_REAGENTS] = tbl1[id][N_REAGENTS] + counts[N_REAGENTS]
		end
	end
	return serialize(tbl1)
end

function normalizeDisenchant(str)
	-- Divide all counts in disenchant string by gcd
	local div = 0
	local count = 0
	local tbl = unserialize(str)
	for id, counts in pairs(tbl) do
		div = Enchantrix.Util.GCD(div, counts[N_DISENCHANTS])
		div = Enchantrix.Util.GCD(div, counts[N_REAGENTS])
		count = count + 1
	end
	-- Only normalize if there's more than one kind of reagent
	if count > 1 then
		for id, counts in pairs(tbl) do
			counts[N_DISENCHANTS] = counts[N_DISENCHANTS] / div
			counts[N_REAGENTS] = counts[N_REAGENTS] / div
		end
		return serialize(tbl)
	end
	return str
end

function cleanupDisenchant(str, id)
	-- Remove reagents that don't appear in level rules table
	if (type(str) == "string") and (id ~= nil) then
		local _, _, quality, level, _, _, _, equip = GetItemInfo(id)
		local itype = const.InventoryTypes[equip]
		if quality and itype then
			local tbl = unserialize(str)
			local clean = {}
			level = Enchantrix.Util.RoundUp(level, 5)
			for id, counts in pairs(tbl) do
				if const.LevelRules[itype][level][id] then
					if quality == 2 then
						-- Uncommon item, remove nexus crystal
						if (const.LevelRules[itype][level][id] < const.CRYSTAL) then
							clean[id] = counts
						end
					else
						-- Rare or epic item, remove dusts and essences
						if (const.LevelRules[itype][level][id] > const.ESSENCE_GREATER) then
							clean[id] = counts
						end
					end
				end
			end
			return serialize(clean)
		end
	end
	return str
end

function disenchantTotal(str)
	-- Return total number of disenchants
	local tot = 0
	local tbl = unserialize(str)
	for id in tbl do
		tot = tot + tbl[id][N_DISENCHANTS]
	end
	return tot
end

function disenchantListHash()
	-- Generate a hash for DisenchantList
	local hash = 1
	local id
	for sig, val in pairs(DisenchantList) do
		id = Enchantrix.Util.GetItemIdFromSig(sig)
		Enchantrix.State.MAX_ITEM_ID = math.max(Enchantrix.State.MAX_ITEM_ID, id or 0)
		hash = math.mod(3 * hash + 2 * (id or 0) + string.len(val), 16777216)
	end
	return hash
end

function mergeDisenchantLists()
	-- Merge DisenchantList by base item, i.e. all "Foobar of <x>" are merged into "Foobar"
	-- This can be rather time consuming so we store this in a saved variable and use a hash
	-- signature to determine if we need to update the table
	local hash = disenchantListHash()
	if (not EnchantedBaseItems.hash) then
		EnchantedBaseItems.hash = -hash
	end
	if (EnchantedBaseItems.hash ~= hash) then
		-- Hash has changed, update EnchantedBaseItems
		EnchantedBaseItems = {}
		for sig, disenchant in pairs(DisenchantList) do
			local item = Enchantrix.Util.GetItemIdFromSig(sig)
			if (Enchantrix.Util.IsDisenchantable(item)) then
				EnchantedBaseItems[item] = mergeDisenchant(EnchantedBaseItems[item],
					normalizeDisenchant(disenchant))
			end
		end
		EnchantedBaseItems.hash = hash
	end

	-- We don't need DisenchantList anymore
	DisenchantList = nil

	-- Merge items from EnchantedLocal
	for sig, disenchant in pairs(EnchantedLocal) do
		local item = Enchantrix.Util.GetItemIdFromSig(sig)
		if type(disenchant) == "table" then
			saveLocal(sig, disenchant)
			disenchant = EnchantedLocal[sig]
		end
		if Enchantrix.Util.IsDisenchantable(item) and (type(disenchant) == "string") then
			LocalBaseItems[item] = mergeDisenchant(LocalBaseItems[item], disenchant)
		end
	end

	-- Merge by item type
	for id, disenchant in pairs(EnchantedBaseItems) do
		local itype = Enchantrix.Util.GetItemType(id)
		if itype then
			EnchantedItemTypes[itype] = mergeDisenchant(EnchantedItemTypes[itype], disenchant)
		end
	end
	for id, disenchant in pairs(LocalBaseItems) do
		local itype = Enchantrix.Util.GetItemType(id)
		if itype then
			EnchantedItemTypes[itype] = mergeDisenchant(EnchantedItemTypes[itype], disenchant)
		end
	end

	-- Take out the trash
	collectgarbage()
end

function saveDisenchant(sig, reagentID, count)
	-- Update tables after a disenchant has been detected
	assert(type(sig) == "string"); assert(tonumber(reagentID)); assert(tonumber(count))

	local id = Enchantrix.Util.GetItemIdFromSig(sig)
	local itype = Enchantrix.Util.GetItemType(id)
	local disenchant = string.format("%d:1:%d:0", reagentID, count)
	EnchantedLocal[sig] = mergeDisenchant(EnchantedLocal[sig], disenchant)
	LocalBaseItems[id] = mergeDisenchant(LocalBaseItems[id], disenchant)
	if itype then
		EnchantedItemTypes[itype] = mergeDisenchant(EnchantedItemTypes[itype], disenchant)
	end
end

function saveLocal(sig, lData)
	local str = "";
	for eResult, eData in lData do
		eResult = tonumber(eResult)
		if not eResult then
			return
		end
		if (eData and type(eData) == "table") then
			local iCount = tonumber(eData.i) or 0;
			local dCount = tonumber(eData.d) or 0;
			local oCount = tonumber(eData.o) or 0;
			local serial = string.format("%d:%d:%d:%d", eResult, iCount, dCount, oCount);
			if (str == "") then str = serial else str = str..";"..serial end
		else
			eData = nil;
		end
	end
	EnchantedLocal[sig] = str;
end

function getLocal(sig)
	local enchantItem = {};
	if (EnchantedLocal and EnchantedLocal[sig]) then
		if (type(EnchantedLocal[sig]) == "table") then
			-- Time to convert it into the new serialized format
			saveLocal(sig, EnchantedLocal[sig]);
		end

		-- Get the string and break it apart
		for enchantResult in Enchantrix.Util.Spliterator(EnchantedLocal[sig], ";") do
			local enchantBreak = Enchantrix.Util.Split(enchantResult, ":");
			local rSig = tonumber(enchantBreak[1]) or 0;
			local iCount = tonumber(enchantBreak[2]) or 0;
			local dCount = tonumber(enchantBreak[3]) or 0;
			local oCount = tonumber(enchantBreak[4]) or 0;

			enchantItem[rSig] = { i=iCount, d=dCount, o=oCount };
		end
	end
	return enchantItem;
end

function getItemDisenchants(sig, name, useCache)
	local disenchantsTo = {};

	if (not Enchantrix.Storage.Price_Cache) or (time()-Enchantrix.Storage.Price_Cache.t > 300) then
		Enchantrix.Storage.Price_Cache = {t=time()};
	end

	-- Automatically convert any named EnchantedLocal items to new items
	if (name and EnchantedLocal[name]) then
		local iData = getLocal(name)
		for dName, count in iData do
			local link = Enchantrix.Util.GetLinkFromName(dName);
			local dSig = Enchantrix.Util.GetItemIdFromLink(link)
			if (dSig ~= nil) then
				if (not iData[dSig]) then iData[dSig] = {}; end
				local oCount = tonumber(iData[dSig].o);
				if (oCount == nil) then oCount = 0; end
				iData[dSig].o = ""..count;
			end
		end
		EnchantedLocal[name] = nil;
		saveLocal(sig, iData);
	end

	local item = Enchantrix.Util.GetItemIdFromSig(sig)
	if (not (item and Enchantrix.Util.IsDisenchantable(item))) then
		-- Item is not disenchantable
		return {}
	end

	-- If there is data, then work out the disenchant data
	if (EnchantedBaseItems[item] or LocalBaseItems[item]) then
		local biTotal = 0;
		local bdTotal = 0;
		local iTotal = 0;
		local dTotal = 0;

		local baseDisenchant = EnchantedBaseItems[item]

		local itype = Enchantrix.Util.GetItemType(item)
		if (itype and EnchantedItemTypes[itype]) then
			if (disenchantTotal(EnchantedItemTypes[itype]) > disenchantTotal(baseDisenchant)) then
				baseDisenchant = EnchantedItemTypes[itype]
			end
		end

		baseDisenchant = cleanupDisenchant(baseDisenchant, item)

		if (baseDisenchant) then
			-- Base Disenchantments are now serialized
			local baseResults = unserialize(baseDisenchant)
			for dSig, counts in pairs(baseResults) do
				if (dSig > 0) then
					disenchantsTo[dSig] = {
						biCount = counts[N_DISENCHANTS],
						bdCount = counts[N_REAGENTS],
						iCount = 0,
						dCount = 0,
					}
					biTotal = biTotal + counts[N_DISENCHANTS]
					bdTotal = bdTotal + counts[N_REAGENTS]
				end
			end
		end

		if (LocalBaseItems[item]) then
			local enchantedLocal = unserialize(LocalBaseItems[item])
			for dSig, counts in pairs(enchantedLocal) do
				if (dSig and dSig > 0) then
					if (not disenchantsTo[dSig]) then
						disenchantsTo[dSig] = {biCount = 0, bdCount = 0, iCount = 0, dCount = 0}
					end
					disenchantsTo[dSig].dCount = counts[N_REAGENTS]
					disenchantsTo[dSig].iCount = counts[N_DISENCHANTS]

					dTotal = dTotal + counts[N_REAGENTS]
					iTotal = iTotal + counts[N_DISENCHANTS]
				end
			end
		end

		local total = biTotal + iTotal;

		local hspGuess = 0;
		local medianGuess = 0;
		local marketGuess = 0;
		if (total > 0) then
			for dSig, counts in pairs(disenchantsTo) do
				local item = 0;
				if (dSig) then item = tonumber(dSig); end
				local dName = Enchantrix.Util.GetReagentInfo(item);
				if (not dName) then dName = "Item "..dSig; end
				local count = (counts.biCount or 0) + (counts.iCount or 0);
				local countI = (counts.biCount or 0) + (counts.iCount or 0);
				local countD = (counts.bdCount or 0) + (counts.dCount or 0);
				local pct = tonumber(string.format("%0.1f", count / total * 100));
				local rate
				if (countI > 0) then
					rate = countD / countI
				end

				local count = 1;
				if (rate and rate > 0) then count = rate; end
				disenchantsTo[dSig].name = dName;
				disenchantsTo[dSig].pct = pct;
				disenchantsTo[dSig].rate = count;

				local hsp, med, mkt = Enchantrix.Util.GetReagentPrice(item)

				local hspValue = (hsp or 0) * pct * count / 100
				local medValue = (med or 0) * pct * count / 100
				local mktValue = (mkt or 0) * pct * count / 100

				disenchantsTo[dSig].hspValue = hspValue;
				disenchantsTo[dSig].medValue = medValue;
				disenchantsTo[dSig].mktValue = mktValue;

				hspGuess = hspGuess + hspValue;
				medianGuess = medianGuess + medValue;
				marketGuess = marketGuess + mktValue;
			end
		end

		local confidence = math.log(math.min(total, 19)+1)/3;

		disenchantsTo.totals = {};
		disenchantsTo.totals.hspValue = hspGuess or 0;
		disenchantsTo.totals.medValue = medianGuess or 0;
		disenchantsTo.totals.mktValue = marketGuess or 0;
		disenchantsTo.totals.biCount = biTotal or 0;
		disenchantsTo.totals.bdCount = bdTotal or 0;
		disenchantsTo.totals.dCount = dTotal or 0;
		disenchantsTo.totals.iCount = iTotal or 0;
		disenchantsTo.totals.total = total or 0;
		disenchantsTo.totals.conf = confidence or 0;
	end

	return disenchantsTo;
end

Enchantrix.Storage = {
	Revision			= "$Revision: 899 $",
	AddonLoaded			= addonLoaded,

	GetItemDisenchants	= getItemDisenchants,
	SaveDisenchant		= saveDisenchant,
}
