--[[
	Enchantrix Addon for World of Warcraft(tm).
	Version: 8.2.6506 (SwimmingSeadragon)
	Revision: $Id: EnxStorage.lua 6506 2019-11-02 14:38:37Z none $
	URL: http://enchantrix.org/

	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 GPL.txt); if not, write to the Free Software
		Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

	Note:
		This AddOn's source code is specifically designed to work with
		World of Warcraft's interpreted AddOn system.
		You have an implicit license to use this AddOn with these facilities
		since that is its designated purpose as per:
		http://www.fsf.org/licensing/licenses/gpl-faq.html#InterpreterIncompat
]]
Enchantrix_RegisterRevision("$URL: Enchantrix/EnxStorage.lua $", "$Rev: 6506 $")

--[[
Usages:
  Enchantrix.Storage["4:2:4:1234"] = { [5432] = { 10, 20 } }
  print(Enchantrix.Storage["4:2:4"])
]]


-- Global functions
local getItemDisenchants			-- Enchantrix.Storage.GetItemDisenchants()
local getItemDisenchantTotals		-- Enchantrix.Storage.GetItemDisenchantTotals()
local getItemDisenchantFromTable	-- Enchantrix.Storage.GetItemDisenchantFromTable()
local getItemDisenchantFromTableForOneMaterial	-- Enchantrix.Storage.GetItemDisenchantFromTableForOneMaterial()
local saveDisenchant				-- Enchantrix.Storage.SaveDisenchant()
local addonLoaded					-- Enchantrix.Storage.AddonLoaded()
local saveNonDisenchantable			-- Enchantrix.Storage.SaveNonDisenchantable()
local removeFromNonDisenchantable	-- Enchantrix.Storage.RemoveNonDisenchantable()

local saveProspect					-- Enchantrix.Storage.SaveProspect()
local getItemProspects				-- Enchantrix.Storage.GetItemProspects()
local getItemProspectTotals			-- Enchantrix.Storage.GetItemProspectTotals()

local saveMilling					-- Enchantrix.Storage.SaveMilling
local getItemMilling				-- Enchantrix.Storage.GetItemMilling()
local getItemMillingTotals			-- Enchantrix.Storage.GetItemMillingTotals()

local constants = Enchantrix.Constants

-- Local functions
local unserialize
local serialize
local normalizeDisenchant
local mergeDisenchant
local mergeDisenchantLists
local writeToNonDisenchantable

local tooltip = LibStub("nTipHelper:1")

-- Database

local N_DISENCHANTS = 1
local N_REAGENTS = 2


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 = de:match("(%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 = ("%s;%d:%d:%d:0"):format(str, id, counts[N_DISENCHANTS], counts[N_REAGENTS])
				else
					str = ("%d:%d:%d:0"):format(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 mergeDisenchantLists()

	-- now we need to merge the user non-disenchantables with the default non-disenchantables
	if not NonDisenchantablesLocal then NonDisenchantablesLocal = {} end
	for sig, value in pairs(NonDisenchantablesLocal) do
		NonDisenchantables[sig] = value;
	end

	-- Take out the trash
	collectgarbage("collect")

end


function saveDisenchant(sig, reagentID, count, itemLink)
	-- Update tables after a disenchant has been detected

	--Enchantrix.Util.DebugPrint("saveDisenchant", ENX_INFO, "saveDisenchant", sig, reagentID, count, type(sig) )	-- debugging
	assert(type(sig) == "string");
	assert(tonumber(reagentID));
	assert(tonumber(count));

	local itype = Enchantrix.Util.GetIType(itemLink)	-- must use full link to get correct quality result, thanks to quality boosts/bonuses

	local disenchant = ("%d:1:%d:0"):format(reagentID, count)
	--Enchantrix.Util.DebugPrint("saveDisenchant", ENX_INFO, "saveDisenchant", sig, reagentID, count, type(sig), itype, disenchant )	-- debugging
	EnchantedLocal[sig] = mergeDisenchant(EnchantedLocal[sig], disenchant)
	if itype then
		EnchantedItemTypes[itype] = mergeDisenchant(EnchantedItemTypes[itype], disenchant)
	end

	-- if we disenchanted successfully, then make sure it isn't on the non-disenchantable list
	removeFromNonDisenchantable(sig)
end



-- for this, we need to pass in a list of reagents
function saveProspect(sig, reagentList )
	-- Update tables after a prospect has been detected
	assert(type(sig) == "string");

	local id = Enchantrix.Util.GetItemIdFromSig(sig)
	if (not ProspectedLocal[id]) then
		ProspectedLocal[id] = {}
		ProspectedLocal[id].total = 0;
	end

	ProspectedLocal[id].total = ProspectedLocal[id].total + 1;
	for reagentID, quantity in pairs( reagentList ) do
		if (not ProspectedLocal[id][reagentID]) then
			ProspectedLocal[id][reagentID] = 0;
		end
		ProspectedLocal[id][reagentID] = ProspectedLocal[id][reagentID] + quantity;
	end
end

-- this will return nil for anything that is not prospectable
function getItemProspects(link)
    if (constants.Classic) then return end

	local itemType, itemID = tooltip:DecodeLink(link)
	if (itemType ~= "item") then return end

	return Enchantrix.Constants.ProspectableItems[ itemID ];
end



-- for this, we need to pass in a list of reagents
function saveMilling(sig, reagentList )
	-- Update tables after a prospect has been detected
	assert(type(sig) == "string");

	local id = Enchantrix.Util.GetItemIdFromSig(sig)
	if (not MillingLocal[id]) then
		MillingLocal[id] = {}
		MillingLocal[id].total = 0;
	end

	MillingLocal[id].total = MillingLocal[id].total + 1;
	for reagentID, quantity in pairs( reagentList ) do
		if (not MillingLocal[id][reagentID]) then
			MillingLocal[id][reagentID] = 0;
		end
		MillingLocal[id][reagentID] = MillingLocal[id][reagentID] + quantity;
	end
end

-- this will return nil for anything that is not millable

--- ccox - WOTLK - this needs to use item level not just item->result
-- similar code in EnxTooltip.lua / millingTooltip

function getItemMilling(link)
    if (constants.Classic) then return end

	local itemType, itemID = tooltip:DecodeLink(link)
	if (itemType ~= "item") then return end

	local resultGroup = Enchantrix.Constants.MillableItems[ itemID ];
	if not resultGroup then
		return nil
	end
	return Enchantrix.Constants.MillGroupYields[ resultGroup ];
end


function getItemDisenchants(link)
	local sig
	local iType = Enchantrix.Util.GetIType(link)

	if (not iType) then
		-- NOTE - ccox - GetIType can return nil for items that are not disenchantable
		-- a nil result does not mean that we could not find the IType
		return nil
	end

	-- see if it is on our non-disenchantable list
	if type(link) == "string" then
		sig = Enchantrix.Util.GetSigFromLink(link);
	else
		local _, sLink = GetItemInfo(link);
		sig = Enchantrix.Util.GetSigFromLink(sLink);
	end

	if (NonDisenchantables[sig]) then
		return nil
	end

	local data = Enchantrix.Storage[iType]
	if not data then
		-- we really should have data
		Enchantrix.Util.DebugPrint("getItemDisenchants", ENX_INFO, "No data", "No data returned for iType:", iType, link)
		return nil
	end
	return data
end



-- NOTE - Rabbitbunny - copied from getItemDisenchantTotals directly below
-- NOTE - ccox - calculation copied from itemTooltip, I couldn't easily reuse the code
-- TODO - REVISIT - ccox - share the code with itemTooltip
-- NOTE - RockSlice - this function now returns value for one ore
function getItemProspectTotals(link)
	local data = Enchantrix.Storage.GetItemProspects(link)
	if not data then
		-- error message would have been printed inside GetItemProspects
		return
	end
	local totalHSP, totalMed, totalMkt, totalFive = 0,0,0,0

	for result, resProb in pairs(data) do
		local style, extra = Enchantrix.Util.GetPricingModel()
		local hsp, med, mkt, five = Enchantrix.Util.GetReagentPrice(result,extra)
		local resHSP, resMed, resMkt, resFive = (hsp or 0)*resProb, (med or 0)*resProb, (mkt or 0)*resProb, (five or 0)*resProb
		totalHSP = totalHSP + resHSP
		totalMed = totalMed + resMed
		totalMkt = totalMkt + resMkt
		totalFive = totalFive + resFive
	end
	--so far, we've been calculating per prospect.  We need price per unit
	totalHSP, totalMed, totalMkt, totalFive = totalHSP/5, totalMed/5, totalMkt/5, totalFive/5
	return totalHSP, totalMed, totalMkt, totalFive
end



-- NOTE - ccox - calculation copied from itemTooltip, I couldn't easily reuse the code
-- TODO - REVISIT - ccox - share the code with itemTooltip
function getItemDisenchantTotals(link)
	local data = Enchantrix.Storage.GetItemDisenchants(link)
	if not data then
		-- error message would have been printed inside GetItemDisenchants
		return
	end

	local total = data.total
	local totalHSP, totalMed, totalMkt, totalFive = 0,0,0,0

	if (total and total[1] > 0) then
		local totalNumber, totalQuantity = unpack(total)
		for result, resData in pairs(data) do
			if (result ~= "total") then

				local resNumber, resQuantity = unpack(resData)
				local style, extra = Enchantrix.Util.GetPricingModel()
				local hsp, med, mkt, five = Enchantrix.Util.GetReagentPrice(result,extra)
				local resProb, resCount = resNumber/totalNumber, resQuantity/resNumber
				local resYield = resProb * resCount;	-- == resQuantity / totalNumber;
				local resHSP, resMed, resMkt, resFive = (hsp or 0)*resYield, (med or 0)*resYield, (mkt or 0)*resYield, (five or 0)*resYield
				totalHSP = totalHSP + resHSP
				totalMed = totalMed + resMed
				totalMkt = totalMkt + resMkt
				totalFive = totalFive + resFive
			end
		end
	else
		return
	end

	return totalHSP, totalMed, totalMkt, totalFive
end


-- NOTE - ccox - calculation copied from itemTooltip, I couldn't easily reuse the code
-- TODO - REVISIT - ccox - share the code with itemTooltip
-- NOTE - RockSlice - this function now returns value for one herb
function getItemMillingTotals(link)
	local data = Enchantrix.Storage.GetItemMilling(link)
	if not data then
		-- error message would have been printed inside GetItemMilling
		return
	end
	local totalHSP, totalMed, totalMkt, totalFive = 0,0,0,0

	for result, resProb in pairs(data) do
		local style, extra = Enchantrix.Util.GetPricingModel()
		local hsp, med, mkt, five = Enchantrix.Util.GetReagentPrice(result,extra)
		local resHSP, resMed, resMkt, resFive = (hsp or 0)*resProb, (med or 0)*resProb, (mkt or 0)*resProb, (five or 0)*resProb
		totalHSP = totalHSP + resHSP
		totalMed = totalMed + resMed
		totalMkt = totalMkt + resMkt
		totalFive = totalFive + resFive
	end
	--so far, we've been calculating per milling.  We need price per unit
	totalHSP, totalMed, totalMkt, totalFive = totalHSP/5, totalMed/5, totalMkt/5, totalFive/5
	return totalHSP, totalMed, totalMkt, totalFive
end


-- NOTE - ccox - calculation copied from itemTooltip, but not remotely shareable
-- this version takes a table of pre-calculated reagent prices
-- this simplifies the inner loop of some calculations, and allow for custom pricing

function getItemDisenchantFromTable(link, reagentTable)
	local data = Enchantrix.Storage.GetItemDisenchants(link)
	if not data then
		-- error message would have been printed inside GetItemDisenchants
		return
	end

	local total = data.total
	local priceTotal = 0;

	if (total and total[1] > 0) then
		local totalNumber, totalQuantity = unpack(total)
		for result, resData in pairs(data) do
			if (result ~= "total") then
				local resNumber, resQuantity = unpack(resData)
				local reagentPrice = reagentTable[ result ];
				if (not reagentPrice) then
					Enchantrix.Util.DebugPrint("reagentTable", ENX_INFO, "No data", "No data in reagent table for ", result, reagentTable )
				end
				local resYield = resQuantity / totalNumber;
				local resPrice = (reagentPrice or 0) * resYield;
				priceTotal = priceTotal + resPrice;
			end
		end
	else
		return
	end

	return priceTotal
end


function getItemDisenchantFromTableForOneMaterial(link, reagentTable, material)
	local data = Enchantrix.Storage.GetItemDisenchants(link)
	if not data then
		-- error message would have been printed inside GetItemDisenchants
		return
	end

	local total = data.total
	local priceTotal = 0;

	if (total and total[1] > 0) then
		local totalNumber, totalQuantity = unpack(total)
		for result, resData in pairs(data) do
			if (result ~= "total" and result == material) then

				local resNumber, resQuantity = unpack(resData)
				local reagentPrice = reagentTable[ result ];
				if (not reagentPrice) then
					Enchantrix.Util.DebugPrint("reagentTable", ENX_INFO, "No data", "No data in reagent table for ", result, reagentTable )
				end
				local resYield = resQuantity / totalNumber;
				local resPrice = (reagentPrice or 0) * resYield;
				local percentage = resNumber / totalNumber;
				local simpleYield = resQuantity/resNumber;
				return resPrice, percentage, simpleYield;
			end
		end
	end

	-- material not matched
	return
end


local _G
local lib = Enchantrix.Storage
lib.data = {}


local function addResults(data, ...)
	if not data then return end
	local result, number, quantity
	local n = select("#", ...)
	local stats
	if (not data.total) then data.total = { 0, 0 } end
	for i = 1, n do
		stats = select(i, ...)
		result, number, quantity = strsplit(":", stats)
		result = tonumber(result)
		if (result) then
			number = tonumber(number) or 0
			quantity = tonumber(quantity) or 0
			if (not data[result]) then data[result] = { 0, 0 } end
			data[result][1] = data[result][1] + number
			data[result][2] = data[result][2] + quantity
			data.total[1] = data.total[1] + number
			data.total[2] = data.total[2] + quantity
		end
	end
end


-- take an ilevel and round it up to the corresponding bracket
local function roundupLevel(level, level_list)
	for _, bracket in ipairs(level_list) do
		if bracket >= level then
			return bracket
		end
	end
	return nil
end

-- get entry from disenchant table (or nil if nothing found)
local function getBaseTableDisenchants(level, quality, type, item)

	-- heirlooms, legendaries, etc. are not disenchantable
	if (quality > 4) then
		return nil
	end

	if Enchantrix.Constants.baseDisenchantTable[quality] then
		local baseTable = Enchantrix.Constants.baseDisenchantTable[quality][type];

		-- so we don't have to keep armor and weapon tables when both are the same
		-- if the weapon table is missing, use the armor table
		if (not baseTable) then
			baseTable = Enchantrix.Constants.baseDisenchantTable[quality][Enchantrix.Constants.ARMOR];
		end

		if baseTable then
			-- find level bracket
			local rLevel = roundupLevel(level, baseTable.bounds);
			if rLevel then
				return baseTable[rLevel]
			end
		end
	end

	-- no matching entry found, this is bad!!
	Enchantrix.Util.DebugPrint("disenchantTable", ENX_INFO, "No data", "No match found in base disenchant table for", quality, type, level, item )
	return nil
end


-- normal (history) data is material, number of times disenchanted, number of items returned
-- base data is material, percentage given, number returned
-- this will work as-is, but return a total count of 1
-- we have to multiply this to get a reasonable result after the confidence function
-- TODO - ccox - clean this up now that the confidence function is gone
local BASE_SCALE = 1000

local function addResultFromBaseTable(data, baseData)
	if not data then return end
	local result, number, quantity
	if (not data.total) then data.total = { 0, 0 } end
    for _, stats in pairs(baseData) do
		result, number, quantity = stats[1], stats[2], stats[3];
		result = tonumber(result)
		if (result) then
			number = tonumber(number) or 0
			quantity = tonumber(quantity) or 0
			number = BASE_SCALE * number
			quantity = number * quantity
			if (not data[result]) then data[result] = { 0, 0 } end
			data[result][1] = number
			data[result][2] = quantity
			data.total[1] = data.total[1] + number
			data.total[2] = data.total[2] + quantity
		end
	end
end


local compactres = {}
local function compact(data)
	while #compactres > 0 do
		table.remove(compactres)
	end
	for item, value in pairs(data) do
		table.insert(compactres, strjoin(":", item, value[1], value[2]))
	end
	return strjoin(",", unpack(compactres))
end

local function index(self, key)
	local iLevel,iQual,iType,iItem
	if (type(key) == "string") then
		iLevel,iQual,iType,iItem = strsplit(":", key)
	end
	if (iQual) then
		local data = {}
		iLevel = tonumber(iLevel) or 0
		iQual = tonumber(iQual) or 0
		iType = tonumber(iType) or 0

		if (iLevel > 0 and iQual >= 2
--		and (iType == 2 or iType == 4)
		) then

			local baseData = getBaseTableDisenchants(iLevel,iQual,iType,iItem);
			if (baseData) then
				addResultFromBaseTable(data,baseData);
			end
		end
		return data
	end
	local val = rawget(self, key)
	if (val) then
		return val
	end
	return nil
end

local function newindex(self, key, value)
	local iLevel,iQual,iType,iItem
	if (type(key) == "string") then
		iLevel,iQual,iType,iItem = strsplit(":", key)
	end
	if (iQual) then
		if (type(value) ~= "table") then return end

		local data = {}
		iLevel = tonumber(iLevel) or 0
		iQual = tonumber(iQual) or 0
		iType = tonumber(iType) or 0
		iItem = tonumber(iItem) or 0

		if (iLevel > 0 and iQual >= 2
--		and (iType == 2 or iType == 4)
		and iItem > 0) then
			local key = strjoin(":", iLevel, iQual, iType)
			if (not EnchantrixData) then EnchantrixData = {} end
			if (not EnchantrixData.disenchants) then EnchantrixData.disenchants = {} end
			if (not EnchantrixData.disenchants[key]) then EnchantrixData.disenchants[key] = {}
			else
				for itemId, itemData in pairs(EnchantrixData.disenchants[key]) do
					if (itemId == iItem) then
						addResults(data, strsplit(",", itemData))
						break
					end
				end
			end
			for resultId, resultData in pairs(value) do
				if (not data[resultId]) then data[resultId] = { 0, 0 } end
				local node = data[resultId]
				node[1] = node[1] + resultData[1]
				node[2] = node[2] + resultData[2]
			end

			EnchantrixData.disenchants[key][iItem] = compact(data)
		end
		return
	end
	if (self.locked) then return end
	rawset(self, key, value)
end



function saveNonDisenchantable(itemLink)
	if not NonDisenchantablesLocal then NonDisenchantablesLocal = {} end
	local sig = Enchantrix.Util.GetSigFromLink(itemLink);
	-- put this in the local and combined list
	-- only the local list will be saved in SavedVariables
	if (not NonDisenchantables[sig]) then
		if (Enchantrix.Settings.GetSetting('chatShowFindings')) then
			Enchantrix.Util.ChatPrint(_ENCH("FrmtFoundNotDisenchant"):format(itemLink))
			--Enchantrix.Util.DebugPrintQuick( itemLink, sig, NonDisenchantables[sig] )
		end
		NonDisenchantablesLocal[sig] = true;
		NonDisenchantables[sig] = true;
	end
end

function removeFromNonDisenchantable(sig)
	if not NonDisenchantablesLocal then NonDisenchantablesLocal = {} end
	-- put this in the local and combined list
	-- only the local list will be saved in SavedVariables
	if (NonDisenchantables[sig]) then
		NonDisenchantablesLocal[sig] = NIL;
		NonDisenchantables[sig] = NIL;
	end
end


function addonLoaded()
	-- Create and setup saved variables
	if not EnchantedLocal then EnchantedLocal = {} end
	if not EnchantedBaseItems then EnchantedBaseItems = {} end
	if not EnchantedItemTypes then EnchantedItemTypes = {} end
	if not NonDisenchantables then NonDisenchantables = {} end
	if not NonDisenchantablesLocal then NonDisenchantablesLocal = {} end
	if not ProspectedLocal then ProspectedLocal = {} end
	if not MillingLocal then MillingLocal = {} end

	mergeDisenchantLists()
end


Enchantrix.Storage = {
	data={},
	locked=false,
	AddonLoaded			= addonLoaded,

	GetItemDisenchants	= getItemDisenchants,
	GetItemDisenchantTotals = getItemDisenchantTotals,
	GetItemDisenchantFromTable = getItemDisenchantFromTable,
	GetItemDisenchantFromTableForOneMaterial = getItemDisenchantFromTableForOneMaterial,
	SaveDisenchant = saveDisenchant,
	SaveNonDisenchantable = saveNonDisenchantable,
	RemoveNonDisenchantable = removeFromNonDisenchantable,

	SaveProspect = saveProspect,
	GetItemProspects = getItemProspects,
	GetItemProspectTotals = getItemProspectTotals,

	SaveMilling = saveMilling,
	GetItemMilling = getItemMilling,
	GetItemMillingTotals = getItemMillingTotals,
}

-- Make all globals local to this file
_G = getfenv(0)
local metatable = {__index = index, __newindex = newindex}
setmetatable(Enchantrix.Storage, metatable)
setfenv(1, Enchantrix.Storage)

-- Stops any other addon from modifying our stuff.
loaded = true
