--[[	*** DataStore_Crafts ***
Written by : Thaoky, EU-Marécages de Zangar
June 23rd, 2009
--]]
if not DataStore then return end

local addonName = "DataStore_Crafts"

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

local addon = _G[addonName]

local THIS_ACCOUNT = "Default"
local L = LibStub("AceLocale-3.0"):GetLocale("DataStore_Crafts")
local PT = LibStub("LibPeriodicTable-3.1")

local AddonDB_Defaults = {
	global = {
		Characters = {
			['*'] = {				-- ["Account.Realm.Name"] 
				lastUpdate = nil,
				Professions = {
					['*'] = {
						FullLink = nil,		-- Tradeskill link
						NumCrafts = 0,			-- total number of crafts for this tradeskill
						Crafts = { ['*'] = nil },
						Cooldowns = { ['*'] = nil },		-- list of active cooldowns
					}
				},
			}
		}
	}
}

-- ** Mixins **
local function _GetProfession(character, name)
	return character.Professions[name]
end
	
local function _GetProfessions(character)
	return character.Professions
end

local function _GetProfessionInfo(profession)
	-- accepts either a pointer (type == table)to the profession table, as returned by addon:GetProfession()
	-- or a link (type == string)
	local link
	if type(profession) == "table" then
		link = profession.FullLink
	elseif type(profession) == "string" then
		link = profession
	end
	
	if link then
		local spellID, currentLevel, maxLevel = link:match("trade:(%d+):(%d+):(%d+):")
		return tonumber(currentLevel), tonumber(maxLevel), tonumber(spellID)
	end
end

local function _GetNumCraftLines(profession)
	return #profession.Crafts
end
	
local function _GetCraftLineInfo(profession, index)
	local craft = profession.Crafts[index]
	local color, info = strsplit("|", craft)
	
	color = tonumber(color)
	if color ~= 0 then			-- not a header ?
		info = tonumber(info)	-- it's a spellID, return a number
	end
	return (color == 0), color, info		-- 1st return value = isHeader
end

local function _GetCraftCooldownInfo(profession, index)
	local cooldown = profession.Cooldowns[index]
	local name, reset, lastCheck = strsplit("|", cooldown)
	
	reset = tonumber(reset)
	lastCheck = tonumber(lastCheck)
	local expiresIn = reset - (time() - lastCheck)
	
	return name, expiresIn, reset, lastCheck
end

local function _GetNumActiveCooldowns(profession)
	assert(type(profession) == "table")		-- this is the pointer to a profession table, obtained through addon:GetProfession()
	return #profession.Cooldowns
end

local function _ClearExpiredCooldowns(profession)
	assert(type(profession) == "table")		-- this is the pointer to a profession table, obtained through addon:GetProfession()
	
	for i = #profession.Cooldowns, 1, -1 do		-- from last to first, to avoid messing up indexes when removing entries
		local _, expiresIn = _GetCraftCooldownInfo(profession, i)
		if expiresIn <= 0 then		-- already expired ? remove it
			table.remove(profession.Cooldowns, i)
		end
	end
end

local function _GetCraftInfo(spellID)
	-- get the id of the item that can be crafted by this spellID
	local itemID = PT:ItemInSet("-"..spellID, "Tradeskill.RecipeLinks")
	local reagents
	
	if itemID then
		itemID = tonumber(itemID)

		-- ex: itemID 10046 is made with reagents : "2996x2;2318x1;2320x1"
		reagents = PT:ItemInSet(itemID, "TradeskillResultMats.Forward")

		if itemID == -spellID then		-- enchants that do not yield  an item will return this, ex: enchant 7420 will return itemID -7420
			itemID = nil
		end
	end
	
	return itemID, reagents
end

local function _GetCraftLevels(spellID)
	-- get the id of the item that can be crafted by this spellID
	local itemID = PT:ItemInSet("-"..spellID, "Tradeskill.RecipeLinks")
	
	if itemID then
		itemID = tonumber(itemID)

		-- ex: itemID 10046 : levels = "20/50/67/85", the item turns yellow at 50, green at 67, grey at 85
		local levels = PT:ItemInSet(itemID, "TradeskillLevels")

		if levels then
			local orange, yellow, green, grey = strsplit("/", levels)
			return tonumber(orange), tonumber(yellow), tonumber(green), tonumber(grey)
		end
	end
end

local function _GetNumRecipesByColor(profession)
	-- counts the number of orange, yellow, green and grey recipes.
	local counts = { 0, 0, 0, 0 }
	
	for i = 1, _GetNumCraftLines(profession) do
		local isHeader, color = _GetCraftLineInfo(profession, i)
		
		if not isHeader then
			counts[color] = counts[color] + 1
		end
	end
	return counts[1], counts[2], counts[3], counts[4]		-- orange, yellow, green, grey
end

local function _IsCraftKnown(profession, spellID)
	-- returns true if a given spell ID is known in the profession passed as first argument
	for i = 1, _GetNumCraftLines(profession) do
		local isHeader, _, info = _GetCraftLineInfo(profession, i)
		
		if not isHeader then
			if info == spellID then
				return true
			end
		end
	end
end

local PublicMethods = {
	GetProfession = _GetProfession,
	GetProfessions = _GetProfessions,
	GetProfessionInfo = _GetProfessionInfo,
	GetNumCraftLines = _GetNumCraftLines,
	GetCraftLineInfo = _GetCraftLineInfo,
	GetCraftCooldownInfo = _GetCraftCooldownInfo,
	GetNumActiveCooldowns = _GetNumActiveCooldowns,
	ClearExpiredCooldowns = _ClearExpiredCooldowns,
	GetCraftInfo = _GetCraftInfo,
	GetCraftLevels = _GetCraftLevels,
	GetNumRecipesByColor = _GetNumRecipesByColor,
	IsCraftKnown = _IsCraftKnown,
}

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

	DataStore:RegisterModule(addonName, addon, PublicMethods)
	DataStore:SetCharacterBasedMethod("GetProfession")
	DataStore:SetCharacterBasedMethod("GetProfessions")
end

function addon:OnEnable()
	addon:RegisterEvent("PLAYER_ALIVE")
	addon:RegisterEvent("TRADE_SKILL_SHOW")
	addon:RegisterEvent("CHAT_MSG_SKILL")
end

function addon:OnDisable()
	addon:UnregisterEvent("PLAYER_ALIVE")
	addon:UnregisterEvent("TRADE_SKILL_SHOW")
	addon:UnregisterEvent("CHAT_MSG_SKILL")
end

-- *** Utility functions ***

-- *** Scanning functions ***

local selectedTradeSkillIndex
local subClasses, subClassID
local invSlots, invSlotID
local haveMats

local function SaveActiveFilters()
	selectedTradeSkillIndex = GetTradeSkillSelectionIndex()
	
	subClasses = { GetTradeSkillSubClasses() }
	invSlots = { GetTradeSkillInvSlots() }
	subClassID = UIDropDownMenu_GetSelectedID(TradeSkillSubClassDropDown)
	invSlotID = UIDropDownMenu_GetSelectedID(TradeSkillInvSlotDropDown)
	
	-- Subclasses
	SetTradeSkillSubClassFilter(0, 1, 1)	-- this checks "All subclasses"
	UIDropDownMenu_SetSelectedID(TradeSkillSubClassDropDown, 1)
	
	-- Inventory slots
	SetTradeSkillInvSlotFilter(0, 1, 1)		-- this checks "All slots"
	UIDropDownMenu_SetSelectedID(TradeSkillInvSlotDropDown, 1)
	
	-- Have Materials
	haveMats = TradeSkillFrameAvailableFilterCheckButton:GetChecked()	-- nil or true
	TradeSkillFrameAvailableFilterCheckButton:SetChecked(false)
	TradeSkillOnlyShowMakeable(false)
end

local function RestoreActiveFilters()
	-- Subclasses
	SetTradeSkillSubClassFilter(subClassID-1, 1, 1)	-- this checks the previously checked value
	UIDropDownMenu_SetSelectedID(TradeSkillSubClassDropDown, subClassID)
	if subClassID == 1 then
		UIDropDownMenu_SetText(TradeSkillSubClassDropDown, ALL_SUBCLASSES);
	else
		UIDropDownMenu_SetText(TradeSkillSubClassDropDown, subClasses[subClassID-1]);
	end	
	
	subClassID = nil
	wipe(subClasses)
	subClasses = nil
	
	-- Inventory slots
	invSlotID = invSlotID or 1
	SetTradeSkillInvSlotFilter(invSlotID-1, 1, 1)	-- this checks the previously checked value
	UIDropDownMenu_SetSelectedID(TradeSkillInvSlotDropDown, invSlotID)
	if invSlotID == 1 then
		UIDropDownMenu_SetText(TradeSkillInvSlotDropDown, ALL_INVENTORY_SLOTS);
	else
		UIDropDownMenu_SetText(TradeSkillInvSlotDropDown, invSlots[invSlotID-1]);
	end
	
	invSlotID = nil
	wipe(invSlots)
	invSlots = nil
	
	-- Have Materials
	TradeSkillFrameAvailableFilterCheckButton:SetChecked(haveMats or false)
	TradeSkillOnlyShowMakeable(haveMats or false)
	haveMats = nil
	
	SelectTradeSkill(selectedTradeSkillIndex)
	selectedTradeSkillIndex = nil
end

local headersState = {}

local function SaveHeaders()
	local headerCount = 0		-- use a counter to avoid being bound to header names, which might not be unique.
	
	for i = GetNumTradeSkills(), 1, -1 do		-- 1st pass, expand all categories
		local _, skillType, _, isExpanded  = GetTradeSkillInfo(i)
		 if (skillType == "header") then
			headerCount = headerCount + 1
			if not isExpanded then
				ExpandTradeSkillSubClass(i)
				headersState[headerCount] = true
			end
		end
	end
end

local function RestoreHeaders()
	local headerCount = 0
	for i = GetNumTradeSkills(), 1, -1 do
		local _, skillType  = GetTradeSkillInfo(i)
		if (skillType == "header") then
			headerCount = headerCount + 1
			if headersState[headerCount] then
				CollapseTradeSkillSubClass(i)
			end
		end
	end
	wipe(headersState)
end

local function ScanCooldowns()
	local tradeskillName = GetTradeSkillLine()
	local char = addon.ThisCharacter
	local profession = char.Professions[tradeskillName]
	
	wipe(profession.Cooldowns)
	for i = 1, GetNumTradeSkills() do
		local skillName, skillType = GetTradeSkillInfo(i)
		
		if skillType ~= "header" then
			local cooldown = GetTradeSkillCooldown(i)
			if cooldown then
				table.insert(profession.Cooldowns, skillName .. "|" .. cooldown .. "|" .. time())
			end
		end
	end
end

local function ScanProfessionLinks()
	local char = addon.ThisCharacter
	
	if not char then return end

	for i = GetNumSkillLines(), 1, -1 do		-- 1st pass, expand all categories
		local _, isHeader = GetSkillLineInfo(i)
		if isHeader then
			ExpandSkillHeader(i)
		end
	end

	local category
	for i = 1, GetNumSkillLines() do
		local skillName, isHeader, _, skillRank, _, _, skillMaxRank = GetSkillLineInfo(i)
		
		if isHeader then
			category = skillName
		else
			if category and skillName then
				if category == L["Professions"] or category == L["Secondary Skills"] then
					-- should be nil anyway for fishing, mining, etc..
					local newLink = select(2, GetSpellLink(skillName))
					if newLink then		-- sometimes a nil value may be returned, so keep the old one if nil
						char.Professions[skillName].FullLink = newLink
					end
				end
			end
		end
	end
end

local SkillTypeToColor = {
	["header"] = 0,
	["optimal"] = 1,		-- orange
	["medium"] = 2,		-- yellow
	["easy"] = 3,			-- green
	["trivial"] = 4,		-- grey
}

local function ScanRecipes()
	local tradeskillName = GetTradeSkillLine()
	if not tradeskillName or tradeskillName == "UNKNOWN" then return end		-- may happen after a patch, or under extreme lag, so do not save anything to the db !

	local char = addon.ThisCharacter
	local profession = char.Professions[tradeskillName]
	local crafts = profession.Crafts
	wipe(crafts)

	profession.FullLink = select(2, GetSpellLink(tradeskillName))

	local NumCrafts = 0
	local color, craftInfo, link

	for i = 1, GetNumTradeSkills() do
		local skillName, skillType = GetTradeSkillInfo(i)
		color = SkillTypeToColor[skillType]
		
		if skillType == "header" then
			craftInfo = skillName or ""
		else
			link = GetTradeSkillRecipeLink(i)
			craftInfo = tonumber(link:match("enchant:(%d+)"))		-- this actually extracts the spellID
			NumCrafts = NumCrafts + 1
		end
		crafts[i] = color .. "|" .. craftInfo
	end
	
	profession.NumCrafts = NumCrafts
end

local function ScanTradeSkills()
	SaveActiveFilters()
	SaveHeaders()
	ScanRecipes()
	ScanCooldowns()
	RestoreHeaders()
	RestoreActiveFilters()
	
	addon.ThisCharacter.lastUpdate = time()
end

local ProfessionSpellID = {
	-- GetSpellInfo with this value will return localized spell name
	["Alchemy"] = 2259,
	["Blacksmithing"] = 3100,
	["Cooking"] = 2550,
	["Enchanting"] = 7411,
	["Engineering"] = 4036,
	["First Aid"] = 3273,
	["Inscription"] = 45357,
	["Jewelcrafting"] = 25229,
	["Leatherworking"] = 2108,
	["Smelting"] = 2656,
	["Tailoring"] = 3908,
	["Skinning"] = 8613,
	["Mining"] = 2575,
	["Herbalism"] = 2366,
	["Fishing"] = 7733,
}

function addon:GetSource(searchedID)
	local PT = LibStub("LibPeriodicTable-3.1")
	
	-- Returns "Profession, level"		ex: "Alchemy", "180"
	local level, data = PT:ItemInSet(searchedID, "Tradeskill.Crafted")
	if level and data then
		local _, _, profession = strsplit(".", data)		-- ex: "Tradeskill.Crafted.Inscription"
		local localizedProfession
		if ProfessionSpellID[profession] then
			localizedProfession = GetSpellInfo(ProfessionSpellID[profession])
		end
			
		return localizedProfession or profession, level
	end
end

function addon:IsTradeSkillWindowOpen()
	-- note : maybe there's a function in the WoW API to test this, but I did not find it :(
	return self.isOpen
end

-- *** Hooks ***
-- todo : change the hooks, do them the Ace way
local Orig_DoTradeSkill = DoTradeSkill

function DoTradeSkill(index, repeatCount, ...)		-- this hook is necessary to get cooldown information after a craft
	Orig_DoTradeSkill(index, repeatCount, ...)
	addon:RegisterEvent("TRADE_SKILL_UPDATE")
end

local Orig_AbandonSkill = AbandonSkill

function AbandonSkill(index, ...)
	local skillName = GetSkillLineInfo(index)				-- get the name of the profession that is being abandonned

	Orig_AbandonSkill(index, ...)
	
	-- clear data from the db
	local char = addon.ThisCharacter
	
	wipe(char.Professions[skillName])			-- and the list of recipes
	char.Professions[skillName] = nil
end


-- *** EVENT HANDLERS ***
function addon:PLAYER_ALIVE()
	ScanProfessionLinks()
end

function addon:TRADE_SKILL_SHOW()
	if IsTradeSkillLinked() then return end
	
	self.isOpen = true
	addon:RegisterEvent("TRADE_SKILL_CLOSE")
	ScanTradeSkills()
end

function addon:TRADE_SKILL_UPDATE()
	-- The hook in DoTradeSkill will register this event so that we only update skills once.
	-- unregister it before calling the update, or the event will be called recursively (due to expand/collapse)
	addon:UnregisterEvent("TRADE_SKILL_UPDATE")
	ScanCooldowns()	-- only cooldowns need to be refreshed
end

function addon:TRADE_SKILL_CLOSE()
	addon:UnregisterEvent("TRADE_SKILL_CLOSE")
	addon:UnregisterEvent("TRADE_SKILL_UPDATE")
	self.isOpen = nil
end

-- this turns
--	"Your skill in %s has increased to %d."
-- into
--	"Your skill in (.+) has increased to (%d+)."
local arg1pattern, arg2pattern
if GetLocale() == "deDE" then		
	-- ERR_SKILL_UP_SI = "Eure Fertigkeit '%1$s' hat sich auf %2$d erhöht.";
	arg1pattern = "'%%1%$s'"
	arg2pattern = "%%2%$d"
else
	arg1pattern = "%%s"
	arg2pattern = "%%d"
end

local skillUpMsg = gsub(ERR_SKILL_UP_SI, arg1pattern, "(.+)")
skillUpMsg = gsub(skillUpMsg, arg2pattern, "(%%d+)")

function addon:CHAT_MSG_SKILL(self, msg)
	if msg and addon.isOpen then	-- point gained while ts window is open ? rescan
		local skill = msg:match(skillUpMsg)
		if skill and skill == GetTradeSkillLine() then	-- if we gained a skill point in the currently opened profession pane, rescan
			ScanTradeSkills()
		end
	end
end
