--[[
Name: PeriodicTable-3.1
Revision: $Rev: 669 $
Author: Nymbia (nymbia@gmail.com)
Many thanks to Tekkub for writing PeriodicTable 1 and 2, and for permission to use the name PeriodicTable!
Website: http://www.wowace.com/wiki/PeriodicTable-3.1
Documentation: http://www.wowace.com/wiki/PeriodicTable-3.1/API
SVN: http://svn.wowace.com/wowace/trunk/PeriodicTable-3.1/PeriodicTable-3.1/
Description: Library of compressed itemid sets.
License: LGPL v2.1
Copyright (C) 2007 Nymbia

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

This library 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
Lesser General Public License for more details.

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

---@class LibPeriodicTable-3.1
---@field ItemSearch function(item : number|string) : table
---@field GetSetTable function(set : string) : table
---@field GetSetString function(set : string) : string
---@field IsSetMulti function(set : string) : boolean
---@field IterateSet function(set : string) : function
---@field AddData function(arg1 : string, arg2 : string|table, arg3 : table|nil)
---@field ItemInSet function(item : number|string, set : string) : any, string
---@field sets table
---@field embedversions table

local PT3, _oldminor = LibStub:NewLibrary("LibPeriodicTable-3.1", tonumber(("$Revision: 669 $"):match("(%d+)")) + 90000)
if not PT3 then
	return
end

-- local references to oft-used global functions.
local type = type
local rawget = rawget
local tonumber = tonumber
local pairs = pairs
local ipairs = ipairs
local next = next
local assert = assert
local table_concat = table.concat

local iternum, iterpos, cache, sets, embedversions

local ptGetNumAddOns = (C_AddOns and C_AddOns.GetNumAddOns) or GetNumAddOns
local ptGetAddOnMetadata = (C_AddOns and C_AddOns.GetAddOnMetadata) or GetAddOnMetadata
local ptGetAddOnInfo = (C_AddOns and C_AddOns.GetAddOnInfo) or GetAddOnInfo
local ptLoadAddOn = (C_AddOns and C_AddOns.LoadAddOn) or LoadAddOn


---------------------------------------------
--       Internal / Local Functions        --
---------------------------------------------
local getItemID, makeNonPresentMultiSet, shredCache, setiter, multisetiter, fixSetName

function getItemID(item)
	-- accepts either an item string ie "item:12345:0:0:0:2342:123324:12:1", hyperlink, or an itemid.
	-- returns a number'ified itemid.
	return tonumber(item) or tonumber(item:match("item:(%d+)")) or (-1 * ((item:match("enchant:(%d+)") or item:match("spell:(%d+)")) or 0))
end

do
	local tables = setmetatable({},{__mode = 'k'})
	function makeNonPresentMultiSet(parentname)
		-- makes an implied multiset, ie if you define only the set "a.b.c",
		-- a request to "a.b" will come through here for a.b to be built.
		-- an expensive function because it needs to iterate all active sets,
		-- moreso for invalid sets.

		-- store some temp tables with weak keys to reduce garbage churn
		local temp = next(tables)
		if temp then
			tables[temp] = nil
		else
			temp = {}
		end
		-- Escape characters that will screw up the name matching.
		local escapedparentname = parentname:gsub("([%,%.%(%)%%%+%-%*%?%[%]%^%$])", "%%%1")
		-- Check all the sets to see if they start with this name.
		for k in pairs(sets) do
			if k:match("^"..escapedparentname.."%.") then
				temp[#temp+1] = k
			end
		end
		if #temp == 0 then
			sets[parentname] = false
		else
			sets[parentname] = "m,"..table_concat(temp, ',')
		end
		-- clear the temp table then feed it back into the recycler
		for k in pairs(temp) do
			temp[k] = nil
		end
		tables[temp] = true
	end
end

function shredCache(setname)
	-- If there's a cache for this set, delete it, since we just added a new copy.
	if rawget(cache, setname) then
		cache[setname] = nil
	end
	local parentname = setname:match("^(.+)%.[^%.]+$")
	if parentname then
		-- Recurse and do the same for the parent set if we find one.
		shredCache(parentname)
	end
end

function setiter(t)
	local k,v
	if iterpos then
		-- We already have a position that we're at in the iteration, grab the next value up.
		k,v = next(t,iterpos)
	else
		-- We havent yet touched this set, grab the first value.
		k,v = next(t)
	end
	if k == "set" then
		k,v = next(t, k)
	end
	if k then
		iterpos = k
		return k,v,t.set
	end
end

function multisetiter(t)
	local k,v
	repeat
		if iterpos then
			-- We already have a position that we're at in the iteration, grab the next value up.
			k,v = next(t[iternum],iterpos)
		else
			-- We havent yet touched this set, grab the first value.
			k,v = next(t[iternum])
		end
		if k == "set" then
			k,v = next(t[iternum], k)
		end
		if k then
			-- There's an entry here, no need to move on to the next table yet.
			iterpos = k
			return k,v,t[iternum].set
		else
			-- No entry, time to check for a new table.
			iternum = iternum + 1
			iterpos = nil
		end
	until not t[iternum]
end

function fixSetName(set)
	-- strip comma from set names since it's used as special char in multisets
	return (set:gsub(",", ""))
end

do
	-- Handle the initial scan of LoD data modules, storing in this local table so the sets metatable can find em
	local lodmodules = {}
	for i = 1, ptGetNumAddOns() do
		local metadata = ptGetAddOnMetadata(i, "X-PeriodicTable-3.1-Module")
		if metadata then
			local name, _, _, enabled = ptGetAddOnInfo(i)
			if enabled then
				lodmodules[metadata] = name
			end
		end
	end

	PT3.sets = setmetatable(PT3.sets or {}, {
		__index = function(self, key)
			local base = key:match("^([^%.]+)%.") or key
			if lodmodules[base] then
				ptLoadAddOn(lodmodules[base])
				lodmodules[base] = nil -- don't try to load again
				-- still may need to generate multiset or something like that, so re-call the metamethod if need be
				return self[key]
			end
			makeNonPresentMultiSet(key) -- this will store it as empty if this is an invalid set.
			return self[key]
		end
	})
end
PT3.embedversions = PT3.embedversions or {}

sets = PT3.sets
embedversions = PT3.embedversions

cache = setmetatable({}, {
	__mode = 'v', -- weaken this table's values.
	__index = function(self, key)
		-- Get the setstring in question.  This call does most of the hairy stuff
		-- like putting together implied but absent multisets and finding child sets
		local setstring = sets[key]
		if not setstring then
			return
		end
		if setstring:sub(1,2) == "m," then
			-- This table is a list of references to the members of this set.
			self[key] = {}
			local working = self[key]
			for childset in setstring:sub(3):gmatch("([^,]+)") do
				if childset ~= key then -- infinite loops is bad
					local pointer = cache[childset]
					if pointer then
						local _, firstv = next(pointer)
						if type(firstv) == "table" then
							-- This is a multiset, copy its references
							for _,v in ipairs(pointer) do
								working[#working+1] = v
							end
						elseif firstv then
							-- This is not a multiset, just stick a reference in.
							working[#working+1] = pointer
						end
					end
				end
			end
			return working
		else
			-- normal ol' set.  Well, maybe not, but close enough.
			self[key] = {}
			local working = self[key]
			for itemstring in setstring:gmatch("([^,]+)") do
				-- for each item (comma seperated)..
				-- ...check to see if we have a value set (ie "14543:1121")
				local id, value = itemstring:match("^([^:]+):(.+)$")
				-- if we don't, (ie "14421,12312"), then set the value to true.
				id, value = tonumber(id) or tonumber(itemstring), value or true
				assert(id, 'malformed set? '..key)
				working[id] = value
			end
			-- stick the set name in there so that we can find out which set an item originally came from.
			working.set = key
			return working
		end
	end
})
LPTcache = cache
---------------------------------------------
--                  API                    --
---------------------------------------------
-- These three are pretty simple.  Note that non-present chunks will be generated by the metamethods.
function PT3:GetSetTable(set)
	assert(type(set) == "string", "Invalid arg1: set must be a string")
	set = fixSetName(set)
	return cache[set]
end

function PT3:GetSetString(set)
	assert(type(set) == "string", "Invalid arg1: set must be a string")
	set = fixSetName(set)
	return sets[set]
end

function PT3:IsSetMulti(set)
	assert(type(set) == "string", "Invalid arg1: set must be a string")
	set = fixSetName(set)
	-- Check if this set's a multiset by checking if its table contains tables instead of strings/booleans
	local pointer = cache[set]
	if not pointer then
		return
	end
	local _, firstv = next(pointer)
	if type(firstv) == "table" then
		return true
	else
		return false
	end
end

function PT3:IterateSet(set)
	assert(type(set) == "string", "Invalid arg1: set must be a string")
	set = fixSetName(set)
	-- most of the work here is handled by the local functions above.
	--!! this could maybe use some improvement...
	local t = cache[set]
	assert(t, "Invalid set: "..set)
	if self:IsSetMulti(set) then
		iternum, iterpos = 1, nil
		return multisetiter, t
	else
		iterpos = nil
		return setiter, t
	end
end

-- Check if the item is contained in this set or any of its child sets.  If it is, return the value
-- (which is true for items with no value set) and the set where the item is contained in data.
function PT3:ItemInSet(item, set)
	assert(type(item) == "number" or type(item) == "string", "Invalid arg1: item must be a number or item link")
	assert(type(set) == "string", "Invalid arg2: set must be a string")
	set = fixSetName(set)
	-- Type the passed item out to an itemid.
	item = getItemID(item)
	assert(item ~= 0,"Invalid arg1: invalid item.")
	local pointer = cache[set]
	if not pointer then
		return
	end
	local _, firstv = next(pointer)
	if type(firstv) == "table" then
		-- The requested set is a multiset, iterate its children.  Return the first matching item.
		for _,v in ipairs(pointer) do
			if v[item] then
				return v[item], v.set
			end
		end
	elseif pointer[item] then
		-- Not a multiset, just return the value and set name.
		return pointer[item], pointer.set
	end
end

function PT3:AddData(arg1, arg2, arg3)
	assert(type(arg1) == "string", "Invalid arg1: name must be a string")
	assert(type(arg2) == "string" or type(arg2) == "table", "Invalid arg2: must be set contents string or table, or revision string")
	assert((arg3 and type(arg3) == "table") or not arg3, "Invalid arg3: must be a table")
	if not arg3 and type(arg2) == "string" then
		-- Just a string.
		local replacing
		if rawget(sets, arg1) then
			replacing = true
		end
		sets[arg1] = arg2
		-- Clear the cache of this set's data if it exists, avoiding invoking the metamethod.
		-- No sense generating data if we're just gonna nuke it anyway ;)
		if replacing then
			shredCache(arg1)
		end
	else
		-- Table of sets passed.
		if arg3 then
			-- Woot, version numbers and everything.
			assert(type(arg2) == "string", "Invalid arg2: must be revision string")
			local version = tonumber(arg2:match("(%d+)"))
			if embedversions[arg1] and embedversions[arg1] >= version then
				-- The loaded version is newer than this one.
				return
			end
			embedversions[arg1] = version
			for k,v in pairs(arg3) do
				-- Looks good, throw 'em in there one by one
				self:AddData(k,v)
			end
		else
			-- Boo, no version numbers.  Just overwrite all these sets.
			for k,v in pairs(arg2) do
				self:AddData(k,v)
			end
		end
	end
end

function PT3:ItemSearch(item)
	assert(type(item) == "number" or type(item) == "string", "Invalid arg1: item must be a number or item link")
	item = tonumber(item) or tonumber(item:match("item:(%d+)"))
	if item == 0 then
		self:error("Invalid arg1: invalid item.")
	end
	local matches = {}
	for k, _v in pairs(self.sets) do
		local _, set = self:ItemInSet(item, k)
		if set then
			local have
			for _,v in ipairs(matches) do
				if v == set then
					have = true
				end
			end
			if not have then
				table.insert(matches, set)
			end
		end
	end
	if #matches > 0 then
		return matches
	end
end

-- ie, LibStub('PeriodicTable-3.1')('InstanceLoot') == LibStub('LibPeriodicTable-3.1'):GetSetTable('InstanceLoot')
setmetatable(PT3, { __call = PT3.GetSetTable })
