--[[ ---------------------------------------------------------------------------
Created:		7/29/2010 5:42:47 AM
Last Modified:	2010-08-09T23:38:30Z
Author:			orgevo
Version:		v1.3a

LootTableTree:	Tree data structure for loot table data

AtlasLootAdvancedSearch 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 3 of the License, or
(at your option) any later version.

AtlasLootAdvancedSearch is distributed 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 AtlasLootAdvancedSearch.	If not, see <http://www.gnu.org/licenses/>.

----------------------------------------------------------------------------- ]]

local ADDON_TAG					= "AtlasLootAdvancedSearch"
local AtlasLootAdvancedSearch	= LibStub("AceAddon-3.0"):GetAddon(ADDON_TAG);
local alas_addon				= AtlasLootAdvancedSearch;
local al_addon					= AtlasLoot;
local LOOTTABLE_DEBUG			= false
local ENABLE_LOGGING			= alas_addon.ENABLE_LOGGING and LOOTTABLE_DEBUG
local ALLOW_SLOW_FUNCS			= alas_addon.ALLOW_SLOW_FUNCS;

-- localization
local L						= LibStub("AceLocale-3.0"):GetLocale(ADDON_TAG, true)
local AL					= LibStub("AceLocale-3.0"):GetLocale("AtlasLoot");
local ADDON_NAME			= L["ADDON_NAME"]

-- cached globals
local tinsert, tconcat = table.insert, table.concat
local strlen, strmatch, strfind, strsub, strformat, strlower, strupper, spad = string.len, string.match, string.find, string.sub, string.format, string.lower, string.upper, string.rep
local pairs, next, type = pairs, next, type
local tostring = tostring

-- print wrappers
-- t_print (testing): what I use while developing....most of these will be commented out in the repository versions
-- dev_print (development): outputs only when AtlasLootAdvancedSearch.dev_mode is true
-- print (release): messages that should be displayed to end-users, shadows the global versions
local function r_print(str) alas_addon:Print(str); end
local function r_printf(str,...) alas_addon:Print(strformat(str,...)); end

local dbgProxy = alas_addon.DebugProxy;
local function isDevMode()			return dbgProxy and dbgProxy:isDevMode() end
local function isInternal()			return dbgProxy and dbgProxy:isInternal() end
local function t_print(str)			if dbgProxy and ENABLE_LOGGING then dbgProxy:t_print(str) end end
local function t_printf(str,...)	if dbgProxy and ENABLE_LOGGING then dbgProxy:t_printf(str, ...) end end
local function dev_print(str)		if dbgProxy and ENABLE_LOGGING then dbgProxy:dev_print(str) end end
local function dev_printf(str, ...)	if dbgProxy and ENABLE_LOGGING then dbgProxy:dev_printf(str,...) end end
local function d_print(str)			if dbgProxy and ENABLE_LOGGING then dbgProxy:d_print(str) end end
local function d_printf(str,...)	if dbgProxy and ENABLE_LOGGING then dbgProxy:d_printf(str,...) end end
local function funcEnter(obj, funcName, formatStr, ...)
	if alas_addon.DebugProxy.isEnabled and ENABLE_LOGGING then
		if formatStr then alas_addon.DebugProxy:TraceEnter(obj,funcName,strformat(formatStr,...));
		else alas_addon.DebugProxy:TraceEnter(obj,funcName); end
	end
end
local function funcExit(obj, funcName, formatStr, ...)
	if alas_addon.DebugProxy.isEnabled and ENABLE_LOGGING then
		if formatStr then alas_addon.DebugProxy:TraceExit(obj,funcName,strformat(formatStr,...));
		else alas_addon.DebugProxy:TraceExit(obj,funcName); end
	end
end
local function funcEnterSlow(obj, funcName, formatStr, ...)
	if ALLOW_SLOW_FUNCS then funcEnter(obj, funcName, formatStr, ...); end
end
local function funcExitSlow(obj, funcName, formatStr, ...)
	if ALLOW_SLOW_FUNCS then funcExit(obj, funcName, formatStr, ...); end
end

-- state vars
--[===[@debug@
cachedZoneTable = nil;
flatZoneList = nil;
--@end-debug@]===]
--@non-debug@
local cachedZoneTable = nil;
local flatZoneList = nil;
--@end-non-debug@

local indent_char = " "
local indent_increment = 2;



local TAG_ALLIANCE		= "_A";
local TAG_HORDE			= "_H";
local TAG_25MAN			= "25Man";
local TAG_HEROIC		= "HEROIC";
local TAG_25MANHEROIC	= TAG_25MAN .. TAG_HEROIC;

--[[
Takes a table ID such as TrialoftheCrusaderLordJaraxxus25ManHEROIC_H, and removes any additional tags from the name, leaving
only e.g. TrialoftheCrusaderLordJaraxxus
--]]
local function ALAS_StripLootTableKeyModifiers(lootTableID)
	local result;
	if lootTableID and type(lootTableID) == "string" then
		-- strip any faction or dungeon-difficulty tags
		result = lootTableID:gsub(
					TAG_25MANHEROIC, ""):gsub(
					TAG_HEROIC, ""):gsub(
					TAG_25MAN, ""):gsub(
					TAG_HORDE .. "$", ""):gsub(
					TAG_ALLIANCE .. "$", ""
				);
				
		-- strip any page number suffixes
		--@TODO: hmmm, for now we'll do this seperately, but it might make sense to move it here in the future.
		--result = string.gsub(result, "^(%a+)%d$", "%1");
	end
	return result;
end

--[[
	Creates a localized description for a loot table extension, such as a 25-man loot table id.
	i.e. "The Twin Val'kyr" becomes "The Twin Val'kyr (25 Man)" or "The Twin Val'kyr (25 Man - Heroic)"
--]]
local function ALAS_GenerateModifiedDisplayName( altLootTableKey, tableFlags, normalLootTable )
--[===[@debug@
--funcEnter(alas_addon, "ALAS_GenerateModifiedDisplayName", "  %s   %s   [isRaid25:%s   isHeroic:%s  IsHorde:%s  IsAlliance:%s]", tostring(altLootTableKey), tostring(normalLootTable:GetCaption()), tostring(tableFlags.isRaid25), tostring(tableFlags.isHeroic), tostring(tableFlags.isHorde), tostring(tableFlags.isAlliance));
--@end-debug@]===]

	if normalLootTable and tableFlags then
		local mid, start, finish = "", "", "";
		
		if tableFlags.isRaid25 then
			start, mid, finish = " (", AL["25 Man"], ")";
			if tableFlags.isHeroic then
				mid = mid .. "-" .. AL["Heroic"];
			end
		elseif tableFlags.isHeroic then
			start, mid, finish = " (", AL["Heroic"], ")";
		end
		
		local currentName = normalLootTable:GetCaption();
		if (currentName == nil or currentName == "") and tableFlags.DisplayName ~= "" then
			currentName = tableFlags.DisplayName;
		end

		tableFlags.DisplayName = currentName .. start .. mid .. finish;
	end

--[===[@debug@
--funcExit(alas_addon, "ALAS_GenerateModifiedDisplayName",  " [%s =>  name:%s]", altLootTableKey, tostring(tableFlags.DisplayName));
--@end-debug@]===]
end

--[[
Given a loot table ID, determines if it has any extensions (25man or heroic), and sets flags in result accordingly.
--]]
local function ALAS_ParseAlternateLootTableID( altLootTableKey )
--funcEnter(alas_addon, "ALAS_ParseAlternateLootTableID", " [%s]", altLootTableKey);
	local flags = {
		hasFaction = false, isAlliance = false, isHorde = false,
		isRaid25 = false, isHeroic = false,
	}
	
	if altLootTableKey and type(altLootTableKey) == "string" then
		if strmatch(altLootTableKey, TAG_ALLIANCE .. "$") ~= nil then
			flags.hasFaction = true;
			flags.isAlliance = true;
			flags.isHorde = false;
		elseif strmatch(altLootTableKey, TAG_HORDE .. "$") ~= nil then
			flags.hasFaction = true;
			flags.isAlliance = false;
			flags.isHorde = true;
		end
		if strmatch(altLootTableKey, TAG_25MAN) ~= nil then
			flags.isRaid25 = true;
		end
		if strmatch(altLootTableKey, TAG_HEROIC) ~= nil then
			flags.isHeroic = true;
		end
	end
	
--funcExit(alas_addon,"ALAS_ParseAlternateLootTableID", " [%s]=hasFaction:%s isAlliance:%s isHorde:%s isRaid25:%s isHeroic:%s",
--	altLootTableKey, tostring(flags.hasFaction), tostring(flags.isAlliance), tostring(flags.isHorde), tostring(flags.isRaid25), tostring(flags.isHeroic));
	return flags;
end


--[[
Creates a matrix of loot table IDs containing all combinations of faction and raid difficulty codes.
--]]
local function CreateFactionIDComboTable( dataID, dataSource )
	local baseID = ALAS_StripLootTableKeyModifiers(dataID);
	if not dataSource then dataSource = AtlasLoot_Data end;
	local result = {
		Default = {
			ID_Normal = baseID,
			ID_Heroic = baseID .. TAG_HEROIC,
			ID_25Man = baseID .. TAG_25MAN,
			ID_25ManHeroic = baseID .. TAG_25MANHEROIC,
		},
		
		Alliance = {
			ID_Normal = baseID .. TAG_ALLIANCE,
			ID_Heroic = baseID .. TAG_HEROIC .. TAG_ALLIANCE,
			ID_25Man = baseID .. TAG_25MAN .. TAG_ALLIANCE,
			ID_25ManHeroic = baseID .. TAG_25MANHEROIC .. TAG_ALLIANCE,
		},
		
		Horde = {
			ID_Normal = baseID .. TAG_HORDE,
			ID_Heroic = baseID .. TAG_HEROIC .. TAG_HORDE,
			ID_25Man = baseID .. TAG_25MAN .. TAG_HORDE,
			ID_25ManHeroic = baseID .. TAG_25MANHEROIC .. TAG_HORDE,
		},
	}
	return result;
end

-- my version of AtlasLoot's AtlasLoot_GetLoottableHeroic
function ALAS_GetLootTableKeyVariations(dataID, dataSource, filterFaction)
--[===[@debug@
--funcEnter(alas_addon, "ALAS_GetLootTableKeyVariations", "  dataID:%s", dataID);
--@end-debug@]===]
	if not dataSource then dataSource = AtlasLoot_Data; end
	local isHorde = (UnitFactionGroup("player") == "Horde");
	local id_tbl = CreateFactionIDComboTable(dataID, dataSource);
	local result_tbl = {}
	
	local function InsertValidKeys( keyName ) 
		local num_inserted = 0;
		if dataSource[id_tbl.Default[keyName]] or dataSource[id_tbl.Horde[keyName]] or dataSource[id_tbl.Alliance[keyName]] then
			if filterFaction then
				local newID = id_tbl.Default[keyName];
				if isHorde and dataSource[id_tbl.Horde[keyName]] then
					newID = id_tbl.Horde[keyName];
				elseif not isHorde and dataSource[id_tbl.Alliance[keyName]] then
					newID = id_tbl.Alliance[keyName];
				end
				tinsert(result_tbl, newID);
				num_inserted = num_inserted + 1;
			else
				local addedDefault = false;
				if dataSource[id_tbl.Horde[keyName]] then
					-- add the horde version
					tinsert(result_tbl, id_tbl.Horde[keyName]);
					num_inserted = num_inserted + 1;
				else
					addedDefault = true;
					tinsert(result_tbl, id_tbl.Default[keyName]);
					num_inserted = num_inserted + 1;
				end
				if dataSource[id_tbl.Alliance[keyName]] then
					-- add the alliance version too 
					tinsert(result_tbl, id_tbl.Alliance[keyName]);
					num_inserted = num_inserted + 1;
				elseif not addedDefault then
					addedDefault = true;
					tinsert(result_tbl, id_tbl.Default[keyName]);
					num_inserted = num_inserted + 1;
				end
			end
		end
		return num_inserted;
	end
	
	local num_added = 0;
	num_added = InsertValidKeys("ID_Normal");
--t_printf("** found %i additional keys based on %s.ID_Normal", num_added, dataID);
	num_added = InsertValidKeys("ID_Heroic");
--t_printf("** found %i additional keys based on %s.ID_Heroic", num_added, dataID);
	num_added = InsertValidKeys("ID_25Man");
--t_printf("** found %i additional keys based on %s.ID_25Man", num_added, dataID);
	num_added = InsertValidKeys("ID_25ManHeroic");
--t_printf("** found %i additional keys based on %s.ID_25ManHeroic", num_added, dataID);
	
	-- strip any page number suffixes to see if this key might belong to a series of pages.
	local baseDataID = ALAS_StripLootTableKeyModifiers(dataID);
	local sequencePageBase, pageIdx = strmatch(baseDataID, "^(%a+)(%d)$");
	-- sigh....ok, so there are a few key sequences where the first page does NOT have a number, but the subsequent pages do....so now we check for that
	if not sequencePageBase then
		local lootTable = dataSource[dataID];
		if lootTable then 
			local nextTableKey = lootTable["Next"];
			if nextTableKey then
				sequencePageBase, pageIdx = strmatch(ALAS_StripLootTableKeyModifiers(nextTableKey), "^(%a+)(%d)$");
				if sequencePageBase and sequencePageBase == baseDataID then
					if dataSource[nextTableKey] then
						-- ok this is one mother-of-a-hack.....we only care about cases where the first loot table
						-- did not have a number, and the next one had the number 2.  But the next block of code will
						-- ignore sequential tables if this isn't the first one in the series....so we need to pretend
						-- that the pageIdx is 1
						if pageIdx == "2" then pageIdx = "1"; end;
					end
				end
			end
		end
	end
			
	if sequencePageBase and pageIdx then
		pageIdx = tonumber(pageIdx);
		if pageIdx == 1 then
			pageIdx = 2
			while pageIdx < 10 do
				local altDataID = sequencePageBase .. pageIdx;
					id_tbl = CreateFactionIDComboTable(altDataID, dataSource)
					num_added = InsertValidKeys("ID_Normal");
					--t_printf("** found %i additional keys based on %s.ID_Normal", num_added, altDataID);
					num_added = InsertValidKeys("ID_Heroic");
					--t_printf("** found %i additional keys based on %s.ID_Heroic", num_added, altDataID);
					num_added = InsertValidKeys("ID_25Man");
					--t_printf("** found %i additional keys based on %s.ID_25Man", num_added, altDataID);
					num_added = InsertValidKeys("ID_25ManHeroic");
					--t_printf("** found %i additional keys based on %s.ID_25ManHeroic", num_added, altDataID);
--				if dataSource[altDataID] then
--					tinsert(result_tbl, altDataID);
--				end
				pageIdx = pageIdx + 1;
			end
		end
	end
	
--[===[@debug@
--funcExit(alas_addon, "ALAS_GetLootTableKeyVariations");
--@end-debug@]===]
	return result_tbl;
end


local function CreateChildren(parentZone)
	local num_children_created = 0;
	
	-- it seems that generally, if a loot page is registered as a table, that means that it will have child keys
	-- If so, they will be listed in the AtlasLoot_Data array if the corresponding module is loaded.....
	--[===[@debug@
	funcEnterSlow(alas_addon,"CreateChildren","parentZone:%s",tostring(parentZone));
	--@end-debug@]===]
	
	local childKey = parentZone:GetDataID();

	local moduleLootTable;
	
	-- first, check for cases where the data for this table represents a menu header
	if AtlasLoot_Data[childKey] and type(AtlasLoot_Data[childKey]) == "table" then
		local childMenuData = AtlasLoot_Data[childKey];
		for dataIdx, sourceData in pairs(childMenuData) do
			local dataID = sourceData[2];
			if type(dataID) == "string" then
				if strmatch(dataID, "^s%d+") == nil then
					local normalName, formattedName, description = AtlasLootAdvancedSearch_GetFormattedName(dataID, sourceData[4]);
					local newChild = parentZone:AddLootTable(dataID, normalName, true, true);
					if newChild ~= nil then
						num_children_created = num_children_created + 1;
					end
				end
			end
		end
	end
	
--[===[@debug@
funcExitSlow(alas_addon,"CreateChildren","parentZone:%s   children created:%i", tostring(parentZone), num_children_created);
--@end-debug@]===]
	return num_children_created;
end


-- Class declarations
LootTableBase = newclass("LootTableBase");
	ZoneRootTable = newclass("ZoneRootTable",	LootTableBase);
	ZoneLootTable = newclass("ZoneLootTable",	LootTableBase);
		ZoneTable = newclass("ZoneTable",		ZoneLootTable);


function NewZone(zoneClass, parentZone, zoneID, zoneCaption)
	local uniqueID = zoneClass:name() .. "." .. zoneID;
	local newzone = flatZoneList[uniqueID];
	if not newzone then
		newzone = zoneClass:new(parentZone, zoneID, zoneCaption);
		flatZoneList[uniqueID] = newzone;
	end
	return newzone;
end

function LootTableBase:GetClassName()
	return self:class():name();
end
LootTableBase:virtual("GetClassName");

--@todo: override metatables to create familiar iterator interface 

--[[==============================================================================
	ZoneRootTable
==========================================================================--]]
function ZoneRootTable:init()
	self.ZoneArray = {};
	self.isZoneRootTable = true;
end
function ZoneRootTable:__tostring()
	return strformat("[ZoneRootTable:%i children]", #self.ZoneArray);
end
function ZoneRootTable:GetClassName()
	return self.class():name();
end

-- @return	[ZoneTable]
function ZoneRootTable:CreateZoneTable(zoneID, zoneCaption)
--[===[@debug@
d_printf(self:GetClassName() .. ": " .. "Creating zone table with zoneID:%s  Caption:%s", tostring(zoneID), tostring(zoneCaption));
--@end-debug@]===]

	local newZone = self:GetZone(zoneID);
	if newZone == nil and zoneID ~= nil then
		newZone = NewZone(ZoneTable, self, zoneID, zoneCaption);
		if newZone ~= nil then
			tinsert(self.ZoneArray, newZone);
		end
	end
	return newZone;
end

function ZoneRootTable:AddLootTable(zoneID, zoneCaption, recursive, checkAdditionalLootTables)
	local newZoneTbl = self:CreateZoneTable(zoneID, zoneCaption);
	if newZoneTbl then
		if recursive then
			CreateChildren(newZoneTbl);
		end
		
		if checkAdditionalLootTables then
			newZoneTbl:AddLootTableVariations(newZoneTbl);
		end
	end
	return newZoneTbl;
end

function ZoneRootTable:GetZones()
	--@todo: this should really return a copy of the ZoneArray table.
	return self.ZoneArray;
end
function ZoneRootTable:HasLootTable(lootTableID, zoneID)
	return self:GetLootTable(lootTableID, zoneID) ~= nil;
end
function ZoneRootTable:GetNumZones()
	local result = 0;
	if self.ZoneArray ~= nil then result = #self.ZoneArray; end
	return result;
end

-- specify TRUE for ignoreSubZones to indicate that boss names should not be checked (i.e. search only dungeon names)
-- there are very few circumstances where you need to limit searches to dungeon names, however...
function ZoneRootTable:GetLootTable(lootTableID, zoneID, ignoreSubZones)
--[===[@debug@
funcEnterSlow(alas_addon, "ZoneRootTable:GetLootTable", "  dataID:%s", lootTableID);
--@end-debug@]===]
	local catObj = nil;
	if zoneID ~= nil and type(zoneID) == "number" then
		local zone = self:GetZone(zoneID);
		if zone then
			catObj = zone:GetLootTable(lootTableID,ignoreSubZones);
		end
	elseif lootTableID ~= nil then
		-- first, try to look in the flat array (much faster)
		if flatZoneList then
			catObj =	flatZoneList["ZoneLootTable." .. lootTableID]
				or		flatZoneList["ZoneTable." .. lootTableID];
		end
		if not catObj then
--			r_printf("Failed to find loot table %s in quick lookup table.  Performing slow search...", lootTableID);
			for zoneIdx = 1, #self.ZoneArray do
				catObj = self.ZoneArray[zoneIdx]:GetLootTable(lootTableID,ignoreSubZones);
				if catObj then break; end
			end
		end
	else
		t_print("ZoneRootTable:GetLootTable - invalid value passed in for lootTableID");
	end
--[===[@debug@
funcExitSlow(alas_addon,"ZoneRootTable:GetLootTable","catObj:" .. tostring(catObj));
--@end-debug@]===]
	return catObj;
end
function ZoneRootTable:GetZone(zoneID)
	if zoneID ~= nil then
		for zoneIdx = 1, #self.ZoneArray do
			if self.ZoneArray[zoneIdx]:GetDataID() == zoneID then
				return self.ZoneArray[zoneIdx];
			end
		end
	end
	return nil;
end
function ZoneRootTable:DumpZoneTableTree( zoneID )
	if zoneID then
		local zoneTable = self:GetZone(zoneID);
		if zoneTable then
			zoneTable:DumpZoneTableTree();
		else
			r_print("No zone tables found with zoneID '" .. tostring(zoneID) .. "'");
		end
	else
		for zoneIdx = 1, #self.ZoneArray do
			local zoneTable = self.ZoneArray[zoneIdx];
			zoneTable:DumpZoneTableTree("", tostring(zoneIdx) .. ") ");
		end
	end
end

--[[==============================================================================
	ZoneLootTable - merge ZoneTable & ZoneLootTable
	==========================================================================--]]
function ZoneLootTable:init( parentTbl, tableID, tableCaption )
	self.super:init();
	self.parentLootTable = parentTbl;
	self.parentZoneTable = nil;
	local nextParent = parentTbl;
	while nextParent ~= nil do
		if nextParent.isZoneTable then
			self.parentZoneTable = nextParent;
			break;
		end
		nextParent = nextParent.parentLootTable;
	end
	
	self:SetDataID(tableID);
	self:SetCaption(tableCaption);
	self.alternateLootTables = {};
	self.childLootTables = {};
	
	self.isZoneLootTable = true;
end
function ZoneLootTable:__tostring()
	return strformat("[%s %s (%s): %i children, %i alts]",
		self:GetClassName(), tostring(self:GetCaption()),tostring(self:GetDataID()), self:GetNumChildren(), self:GetNumAlternates());
end
function ZoneLootTable:GetClassName()
	return self:class():name();
end

function ZoneLootTable:SetDataID(value)
	assert(value, "Null value passed to " .. self:GetClassName() .. ":SetDataID()");

	local cacheID, cachedItem;
	if self:GetDataID() ~= nil then
		cacheID = self:GetClassName() .. "." .. self:GetDataID();
		cachedItem = flatZoneList[cacheID];
	end
	self.DataID = value;
	if cacheID ~= nil and cachedItem ~= nil then
		flatZoneList[cacheID] = nil;
		flatZoneList[self:GetClassName() .. "." .. value] = self;
	end
end		ZoneLootTable:virtual("SetDataID");

function ZoneLootTable:SetCaption(value)
	assert(value, "Null value passed to " .. self:GetClassName() .. ":SetCaption()");
	self.Caption = value;
end		ZoneLootTable:virtual("SetCaption");

function ZoneLootTable:GetDataID()	return	self.DataID;		end
function ZoneLootTable:GetCaption()	return	self.Caption or ""	end			
function ZoneLootTable:HasCaption()	return	self:GetCaption() ~= "" end
ZoneLootTable:virtual("GetDataID");
ZoneLootTable:virtual("GetCaption");
ZoneLootTable:virtual("HasCaption");
									

function ZoneLootTable:GetZoneID()
	if self.parentZoneTable then return self.parentZoneTable:GetZoneID(); end
	return self:GetDataID();
end		ZoneLootTable:virtual("GetZoneID");

function ZoneLootTable:GetZoneCaption()
	if self.parentZoneTable and self.parentZoneTable.GetCaption then return self.parentZoneTable:GetCaption(); end
	return self:GetCaption();
end		ZoneLootTable:virtual("GetZoneCaption");

function ZoneLootTable:HasLootTable(lootTableID,ignoreSubZones)
--t_printf("ZoneLootTable[%s]:GetLootTable(%s): %s[%s]   ...%i children", self:GetDataID(), lootTableID, result:GetDataID(), result:GetCaption(), #self.child
	local lootTbl = self:GetLootTable(lootTableID,ignoreSubZones);
	return lootTbl ~= nil and lootTbl ~= self;
end		ZoneLootTable:virtual("HasLootTable");

function ZoneLootTable:GetLootTable(lootTableID, ignoreSubZones)
--[===[@debug@
funcEnterSlow(alas_addon,tostring(self).." => ZoneLootTable:GetLootTable","lootTableID:%s",lootTableID);
--@end-debug@]===]
	-- in ZoneLootTable's version of GetLootTable, we never check child tables
	local result_tbl;
	if lootTableID then
		if lootTableID == self:GetDataID() then
			result_tbl = self;
		else
			if not result_tbl then
				result_tbl = self:GetAlternateLootTable(lootTableID);
			end
			if not result_tbl and not ignoreSubZones then
				result_tbl = self:GetChildTable(lootTableID);
			end
		end
	end
--[===[@debug@
funcExitSlow(alas_addon,"ZoneLootTable:GetLootTable","found:%s",tostring(result_tbl));
--@end-debug@]===]
	return result_tbl;
end		ZoneLootTable:virtual("GetLootTable");

function ZoneLootTable:GetZoneLootTables( destTable )
	local result = destTable or {};
	
	if self.alternateLootTables then
		for idx, tbl in pairs(self.alternateLootTables) do
			tinsert(result, tbl);
		end 
	end
	if self.childLootTables then
		for idx, tbl in pairs(self.childLootTables) do
			tinsert(result, tbl);
		end
	end
	return result;
end		ZoneLootTable:virtual("GetZoneLootTables");

function ZoneLootTable:GetChildTable( lootTableID )
	local result_tbl;
	if self.childLootTables and lootTableID then
		for idx, tbl in pairs(self.childLootTables) do
			result_tbl = tbl:GetLootTable(lootTableID);
			if result_tbl then break; end
		end 
	end
	return result_tbl;
end		ZoneLootTable:virtual("GetChildTable");

function ZoneLootTable:GetAlternateLootTable( lootTableID )
	local result_tbl;
	if self.alternateLootTables and lootTableID then
		for idx, tbl in pairs(self.alternateLootTables) do
			result_tbl = tbl:GetLootTable(lootTableID);
			if result_tbl then break; end
		end 
	end
	return result_tbl;
end		ZoneLootTable:virtual("GetAlternateLootTable");

function ZoneLootTable:GetNumChildren()
	if self.childLootTables then return #self.childLootTables
	else return 0; end
end		ZoneLootTable:virtual("GetNumChildren");

function ZoneLootTable:GetNumAlternates()
	if self.alternateLootTables then return #self.alternateLootTables
	else return 0; end
end		ZoneLootTable:virtual("GetNumAlternates");

function ZoneLootTable:HasChildTable( lootTableID )
	return self:GetChildTable(lootTableID) ~= nil;
end		ZoneLootTable:virtual("HasChildTable");

function ZoneLootTable:HasAlternateLootTable( lootTableID )
	return self:GetAlternateLootTable(lootTableID) ~= nil;
end		ZoneLootTable:virtual("HasAlternateLootTable");

function ZoneLootTable:AddLootTable(lootTableID, displayName, recursive, checkAdditionalLootTables)
	return self:AddChildLootTable(lootTableID, displayName, recursive, checkAdditionalLootTables);
end

function ZoneLootTable:AddChildLootTable(lootTableID, displayName, recursive, checkAdditionalLootTables)
--[===[@debug@
funcEnterSlow(alas_addon, "["..self:GetDataID() .. "] " .. self:GetClassName() .. ":AddChildLootTable ","%s (%s): hasExistingChild:%s  recursive:%s  checkAlts:%s",
	tostring(lootTableID), tostring(displayName), tostring(self:HasChildTable(lootTableID)), tostring(recursive), tostring(checkAdditionalLootTables));
--@end-debug@]===]
	local childLootTable;
	if not self:HasChildTable(lootTableID) then
		if lootTableID == self:GetDataID() then
			debugstack();
		else
			childLootTable = NewZone(ZoneLootTable,self,lootTableID,displayName);
			assert(childLootTable ~= self.parentLootTable,"Attempted to add loot table to one of its children - self:"..tostring(self:GetDataID()).."  lootTableID:"..tostring(lootTableID).."  displayName:"..tostring(displayName));
			
			tinsert(self.childLootTables, childLootTable);
			if recursive == true then
				CreateChildren(childLootTable);
			end
			--@todo here is where we should probably add sub-categories, similar to AtlasLoot_CategorizeWishList
			if checkAdditionalLootTables then
				self:AddLootTableVariations(childLootTable);
			end
		end
	end
--[===[@debug@
funcExitSlow(alas_addon, self:GetClassName() .. ":AddChildLootTable", tostring(childLootTable));
--@end-debug@]===]
	return childLootTable;
end		ZoneLootTable:virtual("AddChildLootTable");

function ZoneLootTable:AddAlternateLootTable(lootTableID, displayName)
--[===[@debug@
funcEnterSlow(alas_addon,"["..self:GetDataID() .. "] " ..self:GetClassName() .. ":AddAlternateLootTable", "%s (%s)", tostring(lootTableID), tostring(displayName));
--@end-debug@]===]
	local newLootTable;
	if lootTableID then
		if not self:HasAlternateLootTable(lootTableID) then
			newLootTable = NewZone(ZoneLootTable, self, lootTableID, displayName);
			
			-- now, there is something peculiar about alternate loot tables...if an alternate version of a loot
			-- table was seen before the original version (i.e. if we came across SomeLootTable2 before SomeLootTable1), then
			-- we might be attempting to add our own parent here.....that's no good.
			if newLootTable ~= self.parentLootTable then
				--assert(newLootTable ~= self.parentLootTable,"Attempted to add alternate loot table to one of its children - self:"..tostring(self:GetDataID()).."  lootTableID:"..tostring(lootTableID).."  displayName:"..tostring(displayName));
				tinsert(self.alternateLootTables, newLootTable);
				CreateChildren(newLootTable);
			end
		end
	end
--[===[@debug@
funcExitSlow(alas_addon, self:GetClassName() .. ":AddAlternateLootTable", tostring(newLootTable));
--@end-debug@]===]
	return newLootTable;
end		ZoneLootTable:virtual("AddAlternateLootTable");

function ZoneLootTable:AddLootTableVariations( baseLootTable )
--[===[@debug@
	dev_printf("Searching for table ID variations for %s", baseLootTable:GetDataID());
--@end-debug@]===]
	-- will return 4 values
	if baseLootTable and baseLootTable:GetDataID() then
		local potentialVariations = ALAS_GetLootTableKeyVariations(baseLootTable:GetDataID());
		local l = potentialVariations;
		--[===[@debug@
		local openStr, outputStr, closeStr = "", "", "";
		for i = 1, #l do if outputStr:len() > 0 then outputStr = outputStr .. ", "; end outputStr = outputStr .. tostring(l[i]) end
		if outputStr:len() > 0 then openStr = "["; closeStr = "]"; end
		dev_printf("%s%s%s", openStr, outputStr, closeStr);
		--@end-debug@]===]
		for _idx, altLootTableKey in pairs(potentialVariations) do
			if altLootTableKey then
				local alt_data = ALAS_ParseAlternateLootTableID(altLootTableKey, baseLootTable);
				if AtlasLoot_TableNames[altLootTableKey] == nil then
					-- this means that the AtlasLoot_Data and AtlasLoot_TableNames arrays are out of sync
					-- hmmm, try to recover gracefully by simply skipping it....thereby simulating as though
					-- we never found that version of the lookup key ?
					-- give it a default value.....this can be replaced later.
					assert(false, strformat("ALAS_GetLootTableKeyVariations found an entry for %s but now it's gone...(base key ID:%s   name:%s)", altLootTableKey, tostring(baseLootTable:GetDataID()), tostring(baseLootTable:GetCaption())));
				else
					alt_data.DisplayName = AtlasLoot_TableNames[altLootTableKey][1];
					-- if the variant has the same ID as us, it's probably safe to be ignored as an artifact of the code handling so many different cases
					-- effectively, the fact that it's now finding every permutation table names for existing data means this means the code is working correctly
					if altLootTableKey ~= baseLootTable:GetDataID() then
						ALAS_GenerateModifiedDisplayName(altLootTableKey, alt_data, baseLootTable);
--[===[@debug@
						dev_printf("Adding zone variation table for %s: %s (%s)", tostring(baseLootTable:GetDataID()), tostring(altLootTableKey), tostring(alt_data.DisplayName));
--@end-debug@]===]
						local altTable = baseLootTable:AddAlternateLootTable(altLootTableKey, alt_data.DisplayName);
--[[						
						if altTable and ALAS_StripLootTableKeyModifiers(baseLootTable:GetDataID()) ~= ALAS_StripLootTableKeyModifiers(altLootTableKey) then
							-- this alternate table might have alternate versions of its own (i.e. TableName2 might have TableName225Man - this is the case for the VoA tables)
							-- cancelled; instead, I do this directly in the ALAS_GetLootTableKeyVariations() function, to avoid the overhead incurred by recursing this function
							self:AddLootTableVariations(altTable)
						end
--]]
					elseif baseLootTable:GetCaption() == nil or baseLootTable:GetCaption() == "" then
						if alt_data.DisplayName ~= nil and alt_data.DisplayName ~= "" then
							-- do I need to set the inital category's caption again?  i.e.
							--d_printf("----- Updating caption for loot table %s", alt_data.DisplayName);
							baseLootTable:SetCaption(alt_data.DisplayName);
						end
					end
				end
			end
		end
	end
end		ZoneLootTable:virtual("AddLootTableVariations");

function ZoneLootTable:DumpZoneTableTree(indent_string, prefix_string)
	if not indent_string then indent_string = "" end
	if not prefix_string then prefix_string = "" end
	r_printf("%s%s%s (%s)", indent_string, prefix_string, self:GetDataID(), self:GetCaption());
	if self.childLootTables and #self.childLootTables > 0 then
		r_printf("%s%sChildren:", spad(indent_char, strlen(prefix_string)), indent_string);
		for idx, lootTbl in pairs(self.childLootTables) do
			lootTbl:DumpZoneTableTree(indent_string .. spad(indent_char, indent_increment), tostring(idx) .. ") ");
		end
	end
	if self.alternateLootTables and #self.alternateLootTables > 0 then
		r_printf("%s%sAlternate:", spad(indent_char, strlen(prefix_string)), indent_string);
		for idx, lootTbl in pairs(self.alternateLootTables) do
			lootTbl:DumpZoneTableTree(indent_string .. spad(indent_char, indent_increment) , tostring(idx) .. ") ");
		end
	end
end		ZoneLootTable:virtual("DumpZoneTableTree");

--[[==============================================================================
	ZoneTable
===============================================================================--]]
function ZoneTable:init(parentTbl, zoneID, zoneCaption)
	self.super:init(parentTbl, zoneID, zoneCaption);
	
	self.isZoneTable = true;
end
function ZoneTable:__tostring()
	return strformat("[%s %s (%s): %i children, %i alts]",
		self:GetClassName(), tostring(self:GetCaption()),tostring(self:GetDataID()), self:GetNumChildren(), self:GetNumAlternates());
end
function ZoneTable:GetClassName()
	return self:class():name();
end

function ZoneTable:AddLootTable(lootTableID, displayName, recursive, checkAdditionalLootTables)
	local newLootTable;	
	local hasChildZone = self:HasLootTable(lootTableID);
--[===[@debug@
funcEnterSlow(alas_addon,tostring(self) .. " => AddLootTable","new child ID:%s  recursive:%s  checkAlts:%s   hasTable:%s",
	lootTableID, tostring(recursive), tostring(checkAdditionalLootTables), tostring(hasChildZone));
--@end-debug@]===]
	if not hasChildZone then
		newLootTable = NewZone(ZoneLootTable,self,lootTableID,displayName);
		
		tinsert(self.childLootTables, newLootTable);
		if recursive == true then
			CreateChildren(newLootTable);
		end
		--@todo here is where we should probably add sub-categories, similar to AtlasLoot_CategorizeWishList
		if checkAdditionalLootTables then
			self:AddLootTableVariations(newLootTable);
		end
	end
--[===[@debug@
funcExitSlow(alas_addon,"AddLootTable","new child: %s",tostring(newLootTable));
--@end-debug@]===]
	return newLootTable;
end

-- Accessor for creating a table which allows loot tables to be indexed by their ID instead of searching
-- through the array in LUA
function ZoneTable:GenerateLookupTable()
	local result_tbl = {}
	for idx, lootTbl in pairs(self:GetZoneLootTables()) do
		result_tbl[lootTbl:GetDataID()] = lootTbl;
	end
	return result_tbl;
end

	
function AtlasLootAdvancedSearch:CreateZoneTableRoot( ignoreCachedValue )
	local tbl;
	if cachedZoneTable and not ignoreCachedValue then
		tbl = cachedZoneTable;
	end
	
	if not tbl then
		tbl = ZoneRootTable();
		cachedZoneTable = tbl;
		if not flatZoneList then
			flatZoneList = {};
		else
			wipe(flatZoneList)
		end
	end
		
	return tbl;
end

function AtlasLootAdvancedSearch:ACE_DumpZoneTableTree(info, val)
	self:DumpZoneTableTree(val, true);
end
function AtlasLootAdvancedSearch:DumpZoneTableTree(rootTableID,createIfNecessary )
	if createIfNecessary then self:BuildZoneTable() end
	if cachedZoneTable then
	
		local rootNode = cachedZoneTable;
		if rootTableID then
			if flatZoneList then
				for tableID, tbl in pairs(flatZoneList) do
					if strfind(tableID, rootTableID) then
						rootNode = tbl;
						break;
					end
				end
			else
				local all_zones = cachedZoneTable:GetZones();
				local processed_zones = {};
				
				for idx = 1, #all_zones do
					local subZone = all_zones[idx];
					if not processed_zones[subZone:GetDataID()] then
						if strfind(subZone:GetDataID(),rootTableID) then
							rootNode = tbl;
							break;
						end

						processed_zones[subZone:GetDataID()] = 1;
						subZone:GetZoneLootTables(all_zones);
					else
						r_printf("ENCOUNTERED A NODE WE'D ALREADY PROCESSED:"..subZone:GetDataID());
					end
				end
			end
		end
		r_print("Dumping tree of zone tables...");
		rootNode:DumpZoneTableTree();
	else
		r_print("DisplayZoneTableTree - no zone table cached.");
	end
end

function AtlasLootAdvancedSearch:CleanupZoneNameSearch()
	d_printf("Clearing zone loot table cache.");
	cachedZoneTable = nil;
	flatZoneList = nil;
end
