--
--	Grail
--	Written by scott@mithrandir.com
--
--	Version History
--		001	Initial version.
--		002	Converted to using a hooked function to register completed quests.
--			Made it so quests that never appear in the quest log can be marked completed assuming the quest data is up to date.
--			Condensed the debug statements.
--			Changed the architecture so extra information can be returned for failure conditions.
--			Switched ProfessionExceeds to be able to use localized names of professions.
--		003	Made it so Darkmoon Faire NPCs return the location based on where the Darkmoon Faire currently is.
--			Removed the QUEST_AUTOCOMPLETE event handling since it seems to be unneeded.
--			Added specialZones which allow mapping of GetZoneText() to things we prefer.
--			Removed the check for IsDaily() and IsWeekly() from the Status routine since they are marked as non-complete when reset happens.
--			Added IsYearly() because there are holiday quests that can be completed only once.
--			Resettable quests (daily/weekly/yearly) are now recorded specially so quests can be queried as to whether they have ever been completed using HasQuestEverBeenCompleted().
--			Added a notification system for accepting and completing quests.
--			Added API to get quests that are available during an event (holiday).
--		004	Corrected a problem where resettable quests could not be saved for initial use.
--			Augmented level checking to maximum level is checked as well.
--			Added a targetLevel parameter to filtering quests.
--			Made it so "Near" NPCs can have a specific zone associated with them which makes their return location table entry have the zone name and the word "Near".
--			Removed the need for specialZones since GetRealZoneText() does what we need.  Switched the use of GetZoneText() to GetRealZoneText().
--			ProfessionExceeds() now returns success and skill level, where skill level can be Grail.NO_SKILL if the player does not have that skill at all.
--			LocationNPC() now has more parameters to refine the locations returned.
--			LocationQuest() now makes use of LocationNPC() changes and can return the NPC name as well.
--		005	Quest titles that do not match our internal database are recorded, which helpfully gives us localizations as well.
--			Made it so repeatable quests are also recorded in the resettable quests list.
--			Did a little optimization by declaring some LUA functions local.
--			Made some quest traversal routines take an optional argument to force garbage collection, which greatly increases the time to return the desired data, but brings the footprint back down.
--			Added a routine to get the riding skill level.
--			Made it so QueryQuestsCompleted() is called at startup because the earlier assumption did not take into account that there was still another add-on that did it.
--			Made it so we call QueryQuestsCompleted() if GetQuestResetTime() indicates that quests have been reset.  LIMITATION: The check that triggers this only happens upon accepting or completing a quest.
--			Corrected a problem in ProfessionExceeds() where the comparison was incorrect.  Also made sure the skill exists before API is called.  Changed the value of Grail.NO_SKILL.
--			IsNPCAvailable() now can work with heroic NPCs in their instances.
--		006	Corrected a problem where the questResetTime variable was misspelled.
--			Made it so the SpecialQuests are cleaned out of the GrailDatabase properly.
--			Switched City of Ironforge to Ironforge to match GetRealZoneText() return value.
--			Added a table that contains the quests per zone to allow QuestsInZone() to return the cached information immediately.
--			Made it so a callback can be registered for quest abandoning.
--		007	Corrected a problem where a mismatch in title would cause a LUA error when attempting to record bad quest data.
--			Made it so the GrailDatabase gets its NewQuests and NewNPCs cleaned out properly.
--			Added a QuestName function so the name can be gotten without need for internal data structure knowledge.
--			Added Quest and NPC localizations for French, German, Russian and Spanish.
--		008	Changed hooking quest completing to actually get the current script associated instead of the global name for the script.
--			Corrected some problems where LUA errors would occur if the internal database did not know about a quest.
--			Added support for a quest to have a prerequisite quest in the quest log and not complete.
--			Added support for automatic quests to indicate the NPC that needs to be killed to initiate quest acceptance.
--			Added support for automatic quests that are obtained from entering a zone.
--			Added a zoneMapping table that maps zone IDs returned by GetCurrentMapAreaID() to those used internally.
--			Made it so Status can ignore prerequisite requirements of a quest.
--			Made LocationQuest also return the NPC ID as well as the other information it returns.
--			Restructured the posting of the abandon notification to be about 0.75 seconds after the button click because it seems the quest log does not actually remove it immediately.
--			Added capability to handle indirect items where another NPC drops the one that starts the quest.  The NPC name returned is an NPC that drops the item followed by the item name in parentheses.
--		009	Added more mappings from GetCurrentMapAreaID().
--			Corrected a problem where some parameter names in Status() were not the same as in the implementation, thereby ignoring their values.
--			Added a QuestLevel() function.
--			MeetsRequirementLevel() now returns the levels used to determine success.
--			Added some quest interrogation routines IsEscort(), IsDungeon(), IsRaid(), IsPVP(), IsGroup() and IsHeroic().
--			Added a QuestsInMap() function that uses a map ID.
--			Added a convenience function SingleMapLocationQuest().
--		010	Added an NPCName() function.
--			Added an IsTooltipNPC() which indicates what type of NPC we are dealing with for those that modify tooltips.
--			Added an AvailableBreadcrumbs() which returns breadcrumb quests available to be gotten for the specified quest.
--		011	Corrected an issue where ensuring all prerequisite quests were confirmed could have been inaccurate.
--			Added AncestorStatus() and made Status() call it so prerequisite quests are checked to ensure they can be completed otherwise the Status will be false.  For example, this makes
--			an entire quest chain unavailable if the race does not permit the first quest to be accepted.
--			Debugging has now been turned off by default.
--			A new feature called tracking has been provided to keep a little history of basic quest activity, but is off by default.
--			Changed the posting of abandon notifications to be about 1.0 seconds after the button click since there were times when 0.75 seconds was not enough, and made it a variable.
--			Added some clearing out of BadQuestData that has been added to the database.
--			Made clearing out of NewQuest data more robust.
--			Changed to have NPC locations use map area IDs.
--			Removed the zoneMapping and zones tables as part of the move to using map area IDs for all locations.
--			Made the quest index per map area computed at runtime for the latest most accurate data.
--		012	Made it so marking a quest complete only does so if the quest is not already complete.  This is just a precaution to handle an edge case.
--			Added the "/grail backup" and "/grail compare" commands to help find quest IDs for quests that do not enter the quest log.
--			Made it so special quests that never appear in the quest log can be recorded as complete when there is more than one quest with the same name as long as the NPC ID is different.
--			Made it so NPC locations return the dungeon level and the alias map ID.
--			Added a lot of quest/NPC information for the Midsummer Fire Festival.
--		013	Updated quests and NPCs for Firelands.
--			Did a bunch of localization.
--		014	Corrected some localization issues.
--			Updates quests and NPCs for Mount Hyjal and Firelands.
--			Corrected quest prerequisite information to remove cycles to make a DAG.
--			Added AncestorQuests() which returns a complicated table structure of prerequisite quests.
--		015	Made low level comparisons use Blizzard's own routine so grey quests appear properly.
--			Added more quest level information.
--			Updates to quests/NPCs for Firelands, Alliance Grizzly Hills, and Kezan.
--			Added processing to have holiday quests stored in their own map areas (besides where the quest givers are) so they can be viewed as a group.
--			Added the ability to handle PH: prerequisites that require a quest to have been completed sometime in the past (used with dailies that are triggers).
--			Added the ability to handle Xc codes which exclude classes (basically the opposite of Cc codes).
--		016	Updates to quests/NPCs for Gilneas, and Durotar.
--			Corrected a problem in MultipleUniqueMapLocationQuest() where the accept or turn in parameter was not being passed along properly.
--		017	Updated quest and NPC data to minimize problems with stack overflows, etc.
--			Made it so questgiver locations with NearXXX codes are ignored like Near.
--			Made it so AncestorStatus is now passed the same ignore flags as Status so any subsequent calls to Status will get passed them as well.
--			Added tables for the five continents' dungeons.
--		018	Set up basic structures to start support for ptBR localization when Brazilian version comes on line.
--			Updated a large number of localizations for quest names.
--			Updates to some Firelands quest/NPC information, as well as Zul'Drak, Tirisfal Glades, Redridge Mountains, Duskwood and Northern Stranglethorn.
--			Implemented support for Sx quest codes which are the logical opposite of Rx codes.
--			Corrected the implementation of Xx codes.
--			Made it so a nil value we sometimes get will no longer crash, but output something helpful.
--
--	Known Issues
--
--			The use of GetQuestResetTime() is not adequate, nor is the API good enough to provide us accurate information for weeklies (and possibly yearlies depending on when they actually reset compared to dailies).
--				The check is only made when a quest is accepted or completed, and this means the reset could happen during play and the Blizzard-provided data would be out of date until a restart or one of our
--				monitored events occurs.  This is the price one pays for not using something like OnUpdate.
--
--			Need to clean up the "BadQuestData" data when our internal database gets information that is found in it.
--			Need to make it so special quests with the same name AND same NPC ID can be handled.  For the Consortium gem quests I believe we will have to check the levels of the Consortium rep to know how to distinguish.
--
--	UTF-8 file
--

local Grail_File_Version = 018

if nil == Grail or Grail.versionNumber < Grail_File_Version then

	local tinsert, tContains = tinsert, tContains
	local strsplit, strfind, strformat, strsub = strsplit, string.find, string.format, string.sub
	local pairs = pairs

	--	Even though it is documented that UNIT_QUEST_LOG_CHANGED is preferable to QUEST_LOG_UPDATE, in practice UNIT_QUEST_LOG_CHANGED fails
	--	to do what it is supposed to do.  In fact, processing cannot properly happen using it and not QUEST_LOG_UPDATE, even with proper
	--	priming of the data structures.  Therefore, this addon makes use of QUEST_LOG_UPDATE instead.  Actually, this has proven to be a
	--	little unreliable as well, so a hooked function is now used instead.

	--	It would be really convenient to be able not to store the localized names of the quests and the NPCs.  However, the only real way
	--	to get any arbitrary one (that is not in the quest log) is to populate the tooltip with a hyperlink.  However, that will not normally
	--	return results immediately from a server query, so another attempt at tooltip population is needed.  In the case of quests, this
	--	works pretty well.  However, with NPCs the results are less than satisfactory.  In reality, we want the information to be readily
	--	available for when someone needs it, so polling the server is not convenient.  Therefore, we will continue to store the localized
	--	names of these objects so they are available immediately to the caller.  This means the size of the add-on in memory is going to
	--	be constant and not growing overtime if we were to attempt to populate the information in the background (which we would want to do
	--	to make the information available).

	--	Instead of trying to deal with the concept of having NPCs who have unique IDs to be associated with each other but only be available
	--	in specific "phases", the availability of an NPC should probably be checked through the use of determining whether a quest can be
	--	obtained.  Normally, the prerequisite structure of the quests will indicate specific quests cannot yet be obtained, and those are
	--	likely to be associated with the NPCs that will be in new "phases".  Therefore, nothing special needs be done in this library, but
	--	the onus can be put on the user of this library to ensure only quest givers for available quests are listed/shown.

	--	Database of stored information per character.
	GrailDatabase = { }
	--	The completedQuests is a table of 32-bit integers.  The index in the table indicates which set of 32 bits are being used and the value at that index
	--	is a bit representation of completed quests in that 32 quest range.  For example, quest 7 being the only one completed in the quests from 1 to 32
	--	would mean table entry 0 would have a value of 64.  Quest 33 being done would mean [1] = 1, while quests 33 and 35 would mean [1] = 5.  The user need
	--	not know any of this since the API to access this information takes care of the dirty work.
	--	The completedResettableQuests is just like completedQuests except it records only those quests that Blizzard resets like dailies and weeklies.  This
	--	is used for API that can determine if a quest has ever been completed (since a daily could have been completed in the past, but Blizzard's API would
	--	indicate that it is currently not completed (because it has been reset)).
	--	There are four possible tables of interest:  NewNPCs, NewQuests, SpecialQuests and BadQuestData.
	--	These tables could be used to provide feedback which can be used to update the internal database to provide more accurate quest information.

	Grail = {
		versionNumber = Grail_File_Version,
		questsVersionNumber = 0,
		npcsVersionNumber = 0,
		zonesVersionNumber = 0,
		zonesIndexedVersionNumber = 0,
		INFINITE_LEVEL = 100000,
		NO_SKILL = -1,
		NPC_TYPE_BY = 'BY',
		NPC_TYPE_DROP = 'DROP',
		NPC_TYPE_KILL = 'KILL',
		abandonPostNotificationDelay = 1.0,
		abandonedQuestId = nil,
		abandoningQuestIndex = nil,
		classMapping = { ['K'] = 'DEATH KNIGHT', ['D'] = 'DRUID', ['H'] = 'HUNTER', ['M'] = 'MAGE', ['P'] = 'PALADIN', ['T'] = 'PRIEST', ['R'] = 'ROGUE', ['S'] = 'SHAMAN', ['L'] = 'WARLOCK', ['W'] = 'WARRIOR', },
		completingQuest = nil,
		completingQuestTitle = nil,
		continents = {},
		currentlyVerifying = false,
		currentQuestIndex = nil,
		darkmoonFaireLocation = nil,
		debug = false,
		emergencyOutlet = 0,
		eventDispatch = {			-- table of functions whose keys are the events
			['PLAYER_LOGIN'] = function(self, frame)
				frame:RegisterEvent("QUEST_ACCEPTED")
				frame:RegisterEvent("QUEST_COMPLETE")
				frame:RegisterEvent("QUEST_QUERY_COMPLETE")
				self:CleanDatabase()
				self:UpdateQuestResetTime()
			end,
			['QUEST_ACCEPTED'] = function(self, frame, questIndex)
				local questTitle, level, questTag, suggestedGroup, isHeader, isCollapsed, isComplete, isDaily, questId = GetQuestLogTitle(questIndex)
				local npcId = nil
				local version = self.versionNumber.."/"..self.questsVersionNumber.."/"..self.npcsVersionNumber.."/"..self.zonesVersionNumber

				-- Get the target information to ensure the target exists in the database of NPCs
				local targetName, npcId, coordinates = self:TargetInformation()
				self:UpdateTargetDatabase(targetName, npcId, coordinates, version)

				--	If this quest is not in our internal database attempt to record some information about it so we have a chance the
				--	user can provide this to us to update the database.
				if not isHeader then
					self:UpdateQuestDatabase(questId, questTitle, npcId, isDaily, 'A:', version)
				end

				--	If we think we should not have been able to accept this quest we should record some information that may help us update our faulty database.
				local canAccept, reason, failures = self:Status(questId, false, false, false, false, true)
				if not canAccept and reason ~= "Nonexistent" then
					-- look at the reason and record the reason and contrary information for that reason
					if nil == GrailDatabase["BadQuestData"] then GrailDatabase["BadQuestData"] = { } end
					if nil == GrailDatabase["BadQuestData"][questId] then GrailDatabase["BadQuestData"][questId] = { } end
					if reason == "Completed" then
						tinsert(GrailDatabase["BadQuestData"][questId], version.." "..reason)
					elseif reason == "Nonexistent" then
						-- ignore this since it should be added to NewQuests above
					elseif reason == "Level" then
						tinsert(GrailDatabase["BadQuestData"][questId], version.." "..reason.." actual: "..UnitLevel('player'))
					elseif reason == "Prerequisites" then
						tinsert(GrailDatabase["BadQuestData"][questId], { version.." "..reason, failures })
					elseif reason == "Invalidated" then
						tinsert(GrailDatabase["BadQuestData"][questId], { version.." "..reason, failures })
					elseif reason == "Class" then
						tinsert(GrailDatabase["BadQuestData"][questId], version.." "..reason.." actual: "..self.playerClass)
					elseif reason == "Race" then
						tinsert(GrailDatabase["BadQuestData"][questId], version.." "..reason.." actual: "..self.playerRace)
					elseif reason == "Gender" then
						tinsert(GrailDatabase["BadQuestData"][questId], version.." "..reason.." actual: "..self.playerGender)
					elseif reason == "Faction" then
						tinsert(GrailDatabase["BadQuestData"][questId], version.." "..reason.." actual: "..self.playerFaction)
					elseif reason == "Profession" then
						tinsert(GrailDatabase["BadQuestData"][questId], { version.." "..reason, failures })
					elseif reason == "Reputation" then
						tinsert(GrailDatabase["BadQuestData"][questId], { version.." "..reason, failures })
					elseif reason == "Holiday" then
						tinsert(GrailDatabase["BadQuestData"][questId], { version.." "..reason, failures })
					end
				end

				--	If the questTitle is different from what we have recorded, note that as BadQuestData (even though it could just be a localization issue)
				if self:DoesQuestExist(questId) and questTitle ~= self:QuestName(questId) then
					if nil == GrailDatabase["BadQuestData"] then GrailDatabase["BadQuestData"] = { } end
					if nil == GrailDatabase["BadQuestData"][questId] then GrailDatabase["BadQuestData"][questId] = { } end
					tinsert(GrailDatabase["BadQuestData"][questId], { version.." Quest title mismatch: "..self.playerLocale.." "..questTitle })
				end

				--	If the level as reported by Blizzard API does not match our internal database we should note that fact
				if self:DoesQuestExist(questId) and (self:QuestLevel(questId) or 1) ~= level then
					if nil == GrailDatabase["BadQuestData"] then GrailDatabase["BadQuestData"] = { } end
					if nil == GrailDatabase["BadQuestData"][questId] then GrailDatabase["BadQuestData"][questId] = { } end
					tinsert(GrailDatabase["BadQuestData"][questId], { version.." Quest level mismatch: " .. level })
				end

				self:PostNotification("Accept", questId)
				if self.debug then
					local debugMessage = "Grail Debug: Accepted quest: ".. questTitle .. " (" .. questId .. ") from "
					if nil ~= targetName then debugMessage = debugMessage .. targetName .. " (" .. npcId .. ") " .. coordinates else debugMessage = debugMessage .. "no target" end
					if not canAccept and reason ~= "Nonexistent" then debugMessage = debugMessage .. " but should not accept because of: " .. reason else debugMessage = debugMessage .. " without problems" end
					DEFAULT_CHAT_FRAME:AddMessage(debugMessage)
				end
				self:UpdateQuestResetTime()
			end,
			['QUEST_COMPLETE'] = function(self, frame)
				local titleText = GetTitleText()
				self.completingQuest = self:QuestInQuestLogMatchingTitle(titleText)
				self.completingQuestTitle = titleText
				if nil == self.completingQuest then	-- if we still do not have it, mark it in the saved variables for possible future inclusion
					if nil == GrailDatabase["SpecialQuests"] then GrailDatabase["SpecialQuests"] = { } end
					if nil == GrailDatabase["SpecialQuests"][titleText] then GrailDatabase["SpecialQuests"][titleText] = true end
				end
				self:UpdateQuestResetTime()
			end,
			['QUEST_QUERY_COMPLETE'] = function(self, frame, arg1)
				DEFAULT_CHAT_FRAME:AddMessage("Grail starting to process completed query results")
				local completedQuests = { }
				local db = GrailDatabase[self.playerRealm][self.playerName]
				GetQuestsCompleted(completedQuests)
				local hour, minute = GetGameTime()
				local weekday, month, day, year = CalendarGetDate()
				db["serverUpdated"] = strformat("%4d-%02d-%02d %02d:%02d", year, month, day, hour, minute)
				db["completedQuests"] = { }
				for v,_ in pairs(completedQuests) do
					self:MarkQuestComplete(v)
				end
				DEFAULT_CHAT_FRAME:AddMessage("Grail finished processing completed query results")
			end,
			['VARIABLES_LOADED'] = function(self, frame)

				-- We have loaded GrailDatabase at this point, but we need to ensure the structure is set up for first-time players as we rely on at least an empty structure existing
				if nil == GrailDatabase[self.playerRealm] then GrailDatabase[self.playerRealm] = { } end
				if nil == GrailDatabase[self.playerRealm][self.playerName] then GrailDatabase[self.playerRealm][self.playerName] = { } end
				if nil == GrailDatabase[self.playerRealm][self.playerName]["completedQuests"] then GrailDatabase[self.playerRealm][self.playerName]["completedQuests"] = { } end
				if nil == GrailDatabase[self.playerRealm][self.playerName]["completedResettableQuests"] then GrailDatabase[self.playerRealm][self.playerName]["completedResettableQuests"] = { } end

				--	Ensure the tooltip is not messed up
				if not com_mithrandir_grailTooltip:IsOwned(UIParent) then
					com_mithrandir_grailTooltip:SetOwner(UIParent, "ANCHOR_NONE")
				end

				frame:RegisterEvent("PLAYER_LOGIN")
			end,
			},
		factionMapping = { ['A'] = 'Alliance', ['H'] = 'Horde', },
		genderMapping = { ['M'] = 2, ['F'] = 3, },
		holidayMapping = { ['A'] = 'Love is in the Air', ['B'] = 'Brewfest', ['C'] = "Children's Week", ['D'] = 'Day of the Dead', ['F'] = 'Darkmoon Faire',
				['H'] = 'Harvest Festival', ['L'] = 'Lunar Festival', ['M'] = 'Midsummer Fire Festival', ['N'] = 'Noblegarden', ['P'] = "Pirates' Day",
				['Q'] = "Ahn'Qiraj War Effort", ['U'] = 'New Year', ['V'] = 'Feast of Winter Veil', ['W'] = "Hallow's End", ['Y'] = "Pilgrim's Bounty", },
		holidayToMapAreaMapping = { ['HA'] = 100001, ['HB'] = 100002, ['HC'] = 100003, ['HD'] = 100004, ['HF'] = 100006, ['HH'] = 100008, ['HL'] = 100012, ['HM'] = 100013,
				['HN'] = 100014, ['HP'] = 100016, ['HQ'] = 100017, ['HU'] = 100021, ['HV'] = 100022, ['HW'] = 100023, ['HY'] = 100025, },
		indexedQuests = {},
		indexedQuestsExtra = {},
		mapAreaMapping = {},
		observers = { },
		origAbandonQuestFunction,
		origConfirmAbandonQuestFunction,
		origHookFunction,
		playerClass,
		playerFaction,
		playerGender,
		playerLocale,
		playerName,
		playerRace,
		playerRealm,
		professionMapping = { ['A'] = 'Alchemy', ['B'] = 'Blacksmithing', ['C'] = 'Cooking', ['E'] = 'Enchanting', ['F'] = 'Fishing', ['H'] = 'Herbalism',
				['I'] = 'Inscription', ['J'] = 'Jewelcrafting', ['L'] = 'Leatherworking', ['M'] = 'Mining', ['N'] = 'Engineering', ['P'] = 'Lockpicking',
				['R'] = 'Riding', ['S'] = 'Skinning', ['T'] = 'Tailoring', ['U'] = 'Runeforging', ['X'] = 'Archaeology', ['+'] = 'First Aid', },
		questNPCId = nil,
		questResetTime = 0,
		raceMapping = { ['H'] = 'Human', ['F'] = 'Dwarf', ['E'] = 'NightElf', ['N'] = 'Gnome', ['D'] = 'Draenei', ['W'] = 'Worgen', ['O'] = 'Orc', ['U'] = 'Scourge', ['T'] = 'Tauren', ['L'] = 'Troll', ['B'] = 'BloodElf', ['G'] = 'Goblin', },
		reputationMapping = {
			[01] = 'Darnassus', [02] = 'Gnomeregan Exiles', [03] = 'Ironforge', [04] = 'Stormwind', [05] = 'Exodar', [06] = 'Gilneas', [07] = 'Darkspear Trolls', [08] = 'Orgrimmar',
			[09] = 'Thunder Bluff', [10] = 'Undercity', [11] = 'Silvermoon City', [12] = 'Bilgewater Cartel', [13] = 'The League of Arathor', [14] = 'Silverwing Sentinels', [15] = 'Stormpike Guard', [16] = 'The Defilers',
			[17] = 'Frostwolf Clan', [18] = 'Warsong Outriders', [19] = 'Booty Bay', [20] = 'Everlook', [21] = 'Gadgetzan', [22] = 'Ratchet', [23] = 'Cenarion Expedition', [24] = 'Honor Hold',
			[25] = 'Kurenai', [26] = "The Mag'har", [27] = "Ogri'la", [28] = 'Netherwing', [29] = 'Sporeggar', [30] = 'The Consortium', [31] = 'Thrallmar', [32] = 'Lower City',
			[33] = "Sha'tari Skyguard", [34] = 'Shattered Sun Offensive', [35] = 'The Aldor', [36] = 'The Scryers', [37] = "The Sha'tar", [38] = 'Alliance Vanguard', [39] = 'Argent Crusade', [40] = "Explorers' League",
			[41] = 'Frenzyheart Tribe', [42] = 'The Frostborn', [43] = 'The Hand of Vengeance', [44] = 'Horde Expedition', [45] = "The Kalu'ak", [46] = 'Knights of the Ebon Blade', [47] = 'The Oracles', [48] = 'The Sons of Hodir',
			[49] = 'The Taunka', [50] = 'Valiance Expedition', [51] = 'Warsong Offensive', [52] = 'The Wyrmrest Accord', [53] = 'Kirin Tor', [54] = 'The Silver Covenant', [55] = 'The Sunreavers', [56] = 'Guardians of Hyjal',
			[57] = 'The Earthen Ring', [58] = 'Therazane', [59] = 'The Ashen Verdict', [60] = 'Ashtongue Deathsworn', [61] = 'Brood of Nozdormu', [62] = 'Cenarion Circle', [63] = 'Hydraxian Waterlords', [64] = 'The Scale of the Sands',
			[65] = 'Zandalar Tribe', [66] = 'Argent Dawn', [67] = 'Bloodsail Buccaneers', [68] = 'Darkmoon Faire', [69] = 'Gelkis Clan Centaur', [70] = 'Keepers of Time', [71] = 'Magram Clan Centaur', [72] = 'Ravenholdt',
			[73] = "Shen'dralar", [74] = 'Syndicate', [75] = 'Thorium Brotherhood', [76] = 'Timbermaw Hold', [77] = 'Tranquillien', [78] = 'The Violet Eye', [79] = 'Wintersaber Trainers', [80] = 'Wildhammer Clan',
			[81] = 'Ramkahen',
			},
		specialQuests = { },
		statusMapping = { ['C'] = "Completed", ['F'] = 'Faction', ['G'] = 'Gender', ['H'] = 'Holiday', ['I'] = 'Invalidated', ['L'] = "InLog",
			['P'] = 'Profession', ['Q'] = 'Prerequisites', ['R'] = 'Race', ['S'] = 'Class', ['T'] = 'Reputation', ['V'] = "Level", },
		timeSinceLastUpdate = 0,
		tooltip = nil,
		tracking = false,
		trackingStarted = false,
		useAncestor = true,
		verifyTable = {},
		verifyTableCount = 0,
		zoneNameMapping = {},	-- maps zone names into map IDs

		---
		--	Internal Use.
		--	This routine is registered to be called when any of the notifications this addon can post are posted.
		--	It formats a message that is stored in the tracking system.
		--	@param callbackType The string representing the type of callback as posted by the notification system.
		--	@param questId The standard questId posted by the notification system.
		AddTrackingCallback = function(callbackType, questId)
			local message = strformat("%s %s(%d)", callbackType, Grail:QuestName(questId), questId)
			if "Accept" == callbackType or "Complete" == callbackType then
				local targetName, npcId, coordinates = Grail:TargetInformation()
				if nil ~= targetName then
					message = strformat("%s %s %s(%d) %s", message, ("Accept" == callbackType) and "from" or "to", targetName, npcId, coordinates)
				end
			end
			Grail:AddTrackingMessage(message)
		end,

		---
		--	Internal Use.
		--	This adds the provided message to the tracking system.  The first time this is called, a timestamp with some player
		--	information is logged into the tracking system as well.
		--	@param msg The string that will be added to the tracking system.
		AddTrackingMessage = function(self, msg)
			if nil == GrailDatabase["Tracking"] then GrailDatabase["Tracking"] = {} end
			if not self.trackingStarted then
				local hour, minute = GetGameTime()
				local weekday, month, day, year = CalendarGetDate()
				tinsert(GrailDatabase["Tracking"], strformat("%4d-%02d-%02d %02d:%02d %s/%s/%s/%s/%s/%s", year, month, day, hour, minute, self.playerRealm, self.playerName, self.playerFaction, self.playerClass, self.playerRace, self.playerGender))
				self.trackingStarted = true
			end
			tinsert(GrailDatabase["Tracking"], msg)
		end,

		---
		--	This returns a list of all the quests that are prerequisites of the one specified.
		--	If there are none, nil is returned.
		--	@param questId The standard numeric questId representing a quest.
		--	@return The questId
		--	@return A table of the prerequisite quests, or nil if there are none.
		AncestorQuests = function(self, questId)
			if nil == questId or not tonumber(questId) then return nil, nil end
			questId = tonumber(questId)
			local retval = {}

			--[[ First gather all the quests that make up the prerequisite list ]]--
			local allCodes = ""
			local pCodes = self:CodesWithPrefixQuest(questId, "P:")
			if nil ~= pCodes then
				for i = 1, #(pCodes), 1 do
					local colonPosition = (strfind(pCodes[i], ':') or 0) + 1
					if strlen(allCodes) > 0 then allCodes = allCodes .. "," end
					allCodes = allCodes .. strsub(pCodes[i], colonPosition)
				end
			end
			local plCodes = self:CodesWithPrefixQuest(questId, "PL:")
			if nil ~= plCodes then
				for i = 1, #(plCodes), 1 do
					local colonPosition = (strfind(plCodes[i], ':') or 0) + 1
					if strlen(allCodes) > 0 then allCodes = allCodes .. "," end
					allCodes = allCodes .. strsub(plCodes[i], colonPosition)
				end
			end
			local phCodes = self:CodesWithPrefixQuest(questId, "PH:")
			if nil ~= phCodes then
				for i = 1, #(phCodes), 1 do
					local colonPosition = (strfind(phCodes[i], ':') or 0) + 1
					if strlen(allCodes) > 0 then allCodes = allCodes .. "," end
					allCodes = allCodes .. strsub(phCodes[i], colonPosition)
				end
			end

			--[[ Now process the list by splitting it apart by the OR clauses symbolized by commas, and within the AND clauses symbolized by pluses ]]--
			if 0 < strlen(allCodes) then
				local questArray = { strsplit(",", allCodes) }
				if nil ~= questArray then
					for i = 1, #questArray, 1 do
						local orArray = {}
						local eachQuest = { strsplit("+", questArray[i]) }
						for j = 1, #eachQuest do
							local currentQuestId = tonumber(eachQuest[j])
							tinsert(orArray, { self:AncestorQuests(currentQuestId) })
						end
						if 0 < #orArray then
							tinsert(retval, orArray)
						end
					end
				end
			end

			if 0 == #retval then retval = nil end
			return questId, retval
		end,

		---
		--	Returns whether a quest can possibly be accepted based on the quest's prerequisites.
		--	In a similar manner to Status, the reason and failures are also returned.  However, the status is based on whether the
		--	prerequisite chain could possibly be accepted by the player in its current state.  For example, if the initial quest
		--	in the prerequisite chain cannot be accepted because of Race this would return false and its reason should be Race with
		--	a number of asterisks appended based on how many prerequisites were followed to get to the quest that failed.
		--	@param questId The standard numeric questId representing a quest.
		--	@return True to indicate that the quest can be accepted, or false if it cannot.
		--	@return The reason why the quest cannot be accepted from the values Completed, InLog, Nonexistent, Level, Class, Race, Gender, Faction, Prerequisites, Invalidated, Profession, Reputation, Holiday.
		--	@return A table of specific information about failures or nil if no failures.
		AncestorStatus = function(self, questId, ignoreLevel, ignoreProfession, ignoreReputation, ignoreHoliday, ignorePresenceInLog, ignorePrerequisites)
			local retval = false
			local reason = "Error"
			local failures = nil

			--[[ First gather all the quests that make up the prerequisite list ]]--
			local allCodes = ""
			local pCodes = self:CodesWithPrefixQuest(questId, "P:")
			if nil ~= pCodes then
				for i = 1, #(pCodes), 1 do
					local colonPosition = (strfind(pCodes[i], ':') or 0) + 1
					if strlen(allCodes) > 0 then allCodes = allCodes .. "," end
					allCodes = allCodes .. strsub(pCodes[i], colonPosition)
				end
			end
			local plCodes = self:CodesWithPrefixQuest(questId, "PL:")
			if nil ~= plCodes then
				for i = 1, #(plCodes), 1 do
					local colonPosition = (strfind(plCodes[i], ':') or 0) + 1
					if strlen(allCodes) > 0 then allCodes = allCodes .. "," end
					allCodes = allCodes .. strsub(plCodes[i], colonPosition)
				end
			end
			local phCodes = self:CodesWithPrefixQuest(questId, "PH:")
			if nil ~= phCodes then
				for i = 1, #(phCodes), 1 do
					local colonPosition = (strfind(phCodes[i], ':') or 0) + 1
					if strlen(allCodes) > 0 then allCodes = allCodes .. "," end
					allCodes = allCodes .. strsub(phCodes[i], colonPosition)
				end
			end

			--[[ Now process the list by splitting it apart by the OR clauses symbolized by commas, and within the AND clauses symbolized by pluses ]]--
			local questArray = { strsplit(",", allCodes) }
			if nil ~= questArray then
				for i = 1, #(questArray), 1 do
					-- each of these elements is considered part of an OR clause, so any that match would qualify as a success
					local eachQuest = { strsplit("+", questArray[i]) }
					local anyFailure = false
					for j = 1, #(eachQuest), 1 do
						-- each of these elements is considered part of an AND clause, so all must match to be true
						local currentQuestId = tonumber(eachQuest[j])
						local subRetval, subReason, subFailures = self:Status(currentQuestId, true, ignoreProfession, ignoreReputation, ignoreHoliday, ignorePresenceInLog, ignorePrerequisites)
						if subRetval or subReason == 'Completed' or subReason == 'InLog' or subReason == 'Prerequisites' or subReason == 'Level' then
							subRetval = true
						else
							anyFailure = true
							reason = subReason.."*"	-- each level of the chain will append an asterisk, so if needed, one can tell how far back the ancestor failure is
							failures = subFailures
						end
					end
					if not anyFailure then
						retval = true
						reason = nil
						failures = nil
						break
					end
				end
			end

			return retval, reason, failures
		end,

		---
		--	Internal Use.
		--	This looks at the code with appropriate prefix from the specified log and analyzes it to determine if any of the quests
		--	the code contains have been completed, or if checkLog is true, are in the quest log.  The format for the code is a comma
		--	separated list of single questIds that match or if more than one is required to match, they are separated by a plus.  So:
		--	<br>123,456,789+1122,3344<br>means and of the following quests would match:<br>123<br>456<br>789 and 1122<br>3344<br>
		--	@param questId The standard numeric questId representing a quest.
		--	@param codePrefix An prefix used to determine which type of internal code to process.
		--	@param checkLog If true, quests in the quest log are also checked, otherwise they are not.
		--	@param checkEver If true, quests that have ever been completed are checked, otherwise they are not.
		--	@return True if any of the codes quests are completed (or appropriately in the quest log), false otherwise.
		--	@return True is there actually is a code that needed checking, false otherwise.
		--	@return A table of failures, or nil if there are none.
		AnyEvaluateTrue = function(self, questId, codePrefix, checkLog, checkEver)
			if nil == questId or not tonumber(questId) then return false end
			questId = tonumber(questId)
			local anyEvaluateTrue = false
			local requirementPresent = false
			local codeValues = self:CodesWithPrefixQuest(questId, codePrefix)
			local failures = { }
			if nil ~= codeValues then
				for x = 1, #(codeValues), 1 do
					requirementPresent = true
					local colonPosition = (strfind(codeValues[x], ':') or 0) + 1
					local requirementArray = { strsplit(",", strsub(codeValues[x], colonPosition)) }
					local eachRequirement
					local stillGood
					local questCompleted
					local questInLog
					local questEverCompleted
					local requirementQuest
					for i = 1, #(requirementArray), 1 do
						eachRequirement = { strsplit("+", requirementArray[i]) }
						stillGood = true
						local plusArray = { }
						for j = 1, #(eachRequirement), 1 do
							requirementQuest = tonumber(eachRequirement[j])
							questCompleted = self:IsQuestCompleted(requirementQuest)
							questInLog = self:IsQuestInQuestLog(requirementQuest)
							questEverCompleted = self:HasQuestEverBeenCompleted(requirementQuest)
							stillGood = stillGood and (questCompleted or (checkLog and questInLog) or (checkEver and questEverCompleted))
							if questInLog then tinsert(plusArray, "L"..requirementQuest)
							elseif questCompleted then tinsert(plusArray, "C"..requirementQuest)
							elseif questEverCompleted then tinsert(plusArray, "E"..requirementQuest)
							else tinsert(plusArray, "N"..requirementQuest)
							end
						end
						if stillGood then
							anyEvaluateTrue = true
						end
						if 0 < #(plusArray) then
							tinsert(failures, table.concat(plusArray, "+"))
						end
					end
				end
			end
			if 0 == #(failures) then failures = nil end
			return anyEvaluateTrue, requirementPresent, failures
		end,

		---
		--	Internal Use.
		--	This is a support routine used to determine whether specific patterns associated with the BadQuestData are
		--	found and whether they are currently satisfied with the current database contents.
		--	@param questId The standard numeric questId representing a quest.
		--	@param victim The string to search for the pattern.
		--	@param which The number of the type of string we are searching.  It determines the pattern and API to use.
		--	@retval True if the pattern is now satisfied with the current database, or false otherwise.
		AttemptToClean = function(self, questId, victim, which)
			retval = false
			local format
			if 1 == which then format = strformat(" Quest level mismatch: (.*)")
			elseif 2 == which then format = strformat(" Quest title mismatch: %s (.*)", self.playerLocale)
			elseif 3 == which then format = strformat(" Class actual: (.*)")
			elseif 4 == which then format = strformat(" Faction actual: (.*)")
			elseif 5 == which then format = strformat(" Race actual: (.*)")
			end
			local first, last, ofInterest = strfind(victim, format)
			if nil ~= first then
				if 1 == which then retval = (tonumber(ofInterest) == self:QuestLevel(questId))
				elseif 2 == which then retval = (ofInterest == self:QuestName(questId))
				elseif 3 == which then retval = self:MeetsRequirementClass(questId, ofInterest)
				elseif 4 == which then retval = self:MeetsRequirementFaction(questId, ofInterest)
				elseif 5 == which then retval = self:MeetsRequirementRace(questId, ofInterest)
				end
			end
			return retval
		end,

		---
		--	Returns a table of questIds that are available breadcrumb quests for the specified quest.
		--	@param questId The standard numeric questId representing a quest.
		--	@return A table of questIds for available breadcrumb quests for this quest, or nil if there are none.
		AvailableBreadcrumbs = function(self, questId)
			if nil == questId or not tonumber(questId) then return nil end
			questId = tonumber(questId)
			local retval = {}
			local codes = self:CodesWithPrefixQuest(questId, 'O:') -- technically we should only ever have zero or one O: for each quest, but this returns a table
			if nil ~= codes then
				for i = 1, #(codes), 1 do
					local _, commaList = strsplit(":", codes[i])
					local quests = { strsplit(",", commaList) }
					if nil ~= quests then
						for j = 1, #(quests), 1 do
							if self:Status(quests[j]) then
								tinsert(retval, tonumber(quests[j]))
							end
						end
					end
				end
			end
			if 0 == #(retval) then retval = nil end
			return retval
		end,

		---
		--	Determines whether the soughtHolidayName is currently being celebrated.
		--	Has the side effect of setting the current location for Darkmoon Faire NPCs.
		--	@param soughtHolidayName The localized name of a holiday, like Brewfest or Darkmoon Faire.
		--	@return true if the holiday is being celebrated currently, or false otherwise
		CelebratingHoliday = function(self, soughtHolidayName)
			local retval = false
			local weekday, month, day, year = CalendarGetDate()
			local i = 1
			local darkmoonFaireFound = false
			while CalendarGetDayEvent(0, day, i) do
				local title, hour, minute, calendarType, sequenceType, eventType, texture, modStatus, inviteStatus, invitedBy, difficulty, inviteType = CalendarGetDayEvent(0, day, i)
				if eventType == 0 and calendarType == 'HOLIDAY' then
					if title == soughtHolidayName then
						retval = true
					end
					if title == self.holidayMapping['F'] then	-- Darkmoon Faire is special because its NPCs change locations based on where it is
						darkmoonFaireFound = true
						if nil == self.darkmoonFaireLocation then
							local _, description, _ = CalendarGetHolidayInfo(0, day, i)
							if nil ~= strfind(description, self.mapAreaMapping[481]) then		-- Shattrath City
								self.darkmoonFaireLocation = 478				-- Terokkar Forest
							elseif nil ~= strfind(description, self.mapAreaMapping[9]) then		-- Mulgore
								self.darkmoonFaireLocation = 9					-- Mulgore
							elseif nil ~= strfind(description, self.mapAreaMapping[30]) then	-- Elwynn Forest
								self.darkmoonFaireLocation = 30					-- Elwynn Forest
							end
						end
					end
				end
				i = i + 1
			end

			-- The location of the Darkmoon Faire is cleared if it is not found in the calendar, because someone
			-- running Grail over the boundary of when the Faire disappears could have wrong information otherwise.
			if not darkmoonFaireFound then
				self.darkmoonFaireLocation = nil
			end

			return retval
		end,

		---
		--	Internal Use.
		--	This routine attempts to remove items from the special tables that are stored in the GrailDatabase table
		--	when they have been added to the internal database.  These special tables are populated when Grail discovers
		--	something lacking in its internal database as game play proceeds.  This routine is called upon startup.
		CleanDatabase = function(self)
			-- Remove quests from SpecialQuests that have been marked as special in our internal database.
			if nil ~= GrailDatabase["SpecialQuests"] then
				for questName, _ in pairs(GrailDatabase["SpecialQuests"]) do
					local questId = self:QuestWithName(questName)
					if nil ~= questId and self:HasCode(questId, "SP") then
						GrailDatabase["SpecialQuests"][questName] = nil
					end
				end
			end

			-- Remove quests from NewQuests that have been added to our internal database
			-- If the name matches and all the codes are in our internal database we remove
			if nil ~= GrailDatabase["NewQuests"] then
				for questId, q in pairs(GrailDatabase["NewQuests"]) do
					if self:DoesQuestExist(questId) then
						if q[self.playerLocale] == self:QuestName(questId) or q[self.playerLocale] == "No Title Stored" then
							local allCodesGood = true
							if nil ~= q[1] then
								local codeArray = { strsplit(" ", q[1]) }
								for i = 1, #(codeArray), 1 do
									if codeArray[i] ~= "" and not self:HasAliasCode(questId, codeArray[i]) then
										allCodesGood = false
									end
								end
							end
							if allCodesGood then GrailDatabase["NewQuests"][questId] = nil end
						end
					end
				end
			end

			-- Remove NPCs from NewNPCs that have been added to our internal database
			-- Basically, if the name matches and we have a location in our internal database we remove
			if nil ~= GrailDatabase["NewNPCs"] then
				for npcId, n in pairs(GrailDatabase["NewNPCs"]) do
					local locations, npcName = self:LocationNPC(npcId)
					if nil ~= npcName and n[self.playerLocale] == npcName and nil ~= locations then
						GrailDatabase["NewNPCs"][npcId] = nil
					end
				end
			end

-- TODO: BadQuestData still need to do error types: completed, nonexistent, level, prerequisites, invalidated, gender, profession, reputation, holiday
			-- Remove bad quest data that has been corrected in our internal database
			if nil ~= GrailDatabase["BadQuestData"] then
				local maxCleanTypesHandled = 5
				for questId, problems in pairs(GrailDatabase["BadQuestData"]) do
					for i = 1, #problems do
						if "string" == type(problems[i]) then
							for k = 1, maxCleanTypesHandled do
								if self:AttemptToClean(questId, problems[i], k) then problems[i] = nil break end
							end
						elseif "table" == type(problems[i]) then
							for j = 1, #(problems[i]) do
								if "string" == type(problems[i][j]) then
									for k = 1, maxCleanTypesHandled do
										if self:AttemptToClean(questId, problems[i][j], k) then problems[i][j] = nil break end
									end
									if nil ~= problems[i][j] and " Prerequisites" == strsub(problems[i][j], -14) then
										if nil == problems[i][j + 1] then
											problems[i][j] = nil
										else
											
										end
									end
								end
							end
						end
						if "table" == type(problems[i]) and 0 == #(problems[i]) then
							problems[i] = nil
						end
					end
					if 0 == #problems then
						GrailDatabase["BadQuestData"][questId] = nil
					end
				end
			end

		end,

		---
		--	Internal Use.
		--	Returns a table of codes from the victim string that match the sought prefix.
		--	@param victim The string that contains codes separated by spaces.
		--	@param soughtPrefix The prefix of the desired matching codes.
		--	@return A table of the matching codes or nil if there are none.
		CodesWithPrefix = function(self, victim, soughtPrefix)
			local retval = { }
			local codeArray = { strsplit(" ", victim) }
			if nil ~= codeArray then
				for i = 1, #(codeArray), 1 do
					if 1 == strfind(codeArray[i], soughtPrefix) then
						tinsert(retval, codeArray[i])
					end
				end
			end
			-- return nil if there is nothing in the table of matching codes
			if 0 == #(retval) then
				retval = nil
			end
			return retval
		end,

		---
		--	Internal Use.
		--	Returns a table of codes from the NPC that match the sought prefix.
		--	@param npcId The standard numeric npcId representing an NPC.
		--	@param soughtPrefix The prefix of the desired matching codes.
		--	@return A table of the matching codes or nil if there are none.
		CodesWithPrefixNPC = function(self, npcId, soughtPrefix)
			local retval = nil
			if nil == npcId or not tonumber(npcId) then return nil end
			npcId = tonumber(npcId)
			if nil ~= self.npcs[npcId] then
				retval = self:CodesWithPrefix(self.npcs[npcId][1], soughtPrefix)
			end
			return retval
		end,

		---
		--	Internal Use.
		--	Returns a table of codes from the quest that match the sought prefix.
		--	@param questId The standard numeric questId representing a quest.
		--	@param soughtPrefix The prefix of the desired matching codes.
		--	@return A table of the matching codes or nil if there are none.
		CodesWithPrefixQuest = function(self, questId, soughtPrefix)
			if nil == self.quests[questId] then return nil end
			return self:CodesWithPrefix(self.quests[questId][1], soughtPrefix)
		end,

		---
		--	Internal Use.
		--	Populates internal quest lists based on location of NPCs that can start
		--	the quests.  In normal mode, the API will use this indexed list instead
		--	of accessing the NPC information, thereby speeding up queries.
		CreateIndexedQuestList = function(self)
			if self.debug then debugprofilestart() end
			self.indexedQuests = {}
			self.indexedQuestsExtra = {}
			local locations
			local mapId
			local codes
			for questId in pairs(self.quests) do
				locations = self:LocationQuestInMap(questId, 'A')
				if nil ~= locations then
					for i = 1, #locations do
						mapId = tonumber(locations[i][1])
						if nil ~= mapId then
							if 4 == #(locations[i]) then
								if nil == self.indexedQuestsExtra[mapId] then self.indexedQuestsExtra[mapId] = {} end
								if not tContains(self.indexedQuestsExtra[mapId], questId) then tinsert(self.indexedQuestsExtra[mapId], questId) end
							else
								if nil == self.indexedQuests[mapId] then self.indexedQuests[mapId] = {} end
								if not tContains(self.indexedQuests[mapId], questId) then tinsert(self.indexedQuests[mapId], questId) end
							end
						else
							if self.debug then print("Quest ", questId, " has nil mapId for location ", i, "/", #locations) end
						end
					end
				end

				-- Now holiday quests are also put into special map areas that are used to access them altogether in one nice list
				codes = self:CodesWithPrefixQuest(questId, 'H')
				if nil ~= codes then
					for i = 1, #codes do
						local whackyMapId = tonumber(self.holidayToMapAreaMapping[codes[i]])
						if nil == self.indexedQuests[whackyMapId] then self.indexedQuests[whackyMapId] = {} end
						if not tContains(self.indexedQuests[whackyMapId], questId) then tinsert(self.indexedQuests[whackyMapId], questId) end
						if nil == self.mapAreaMapping[whackyMapId] then self.mapAreaMapping[whackyMapId] = self.holidayMapping[strsub(codes[i], 2, 2)] end
					end
				end

			end
			if self.debug then print("Done creating indexed quest list with elapsed milliseconds: "..debugprofilestop()) end
		end,

		---
		--	Determines whether the internal database contains the NPC specified by npcId.
		--	@param npcId The standard numeric npcId representing an NPC.
		--	@return true if the NPC is known to the internal database, false otherwise
		DoesNPCExist = function(self, npcId)
			if nil == npcId or not tonumber(npcId) then return false end
			npcId = tonumber(npcId)
			local retval = false
			if self.npcs[npcId] ~= nil then
				retval = true
			end
			return retval
		end,

		---
		--	Determines whether the internal database contains the quest specified by questId.
		--	@param questId The standard numeric questId representing a quest.
		--	@return true if the quest is known to the internal database, false otherwise
		DoesQuestExist = function(self, questId)
			if nil == questId or not tonumber(questId) then return false end
			questId = tonumber(questId)
			local retval = false
			if self.quests[questId] ~= nil then
				retval = true
			end
			return retval
		end,

		---
		--	Debug Use.
		--	Routine used to test registering a callback that dumps the callback and basic quest information to DEFAULT_CHAT_FRAME.
		--	Note that lack of self as a parameter.
		--	@param callbackType The type of the callback depends on what notification is being posted.
		--	@param questId The quest the notification concerns.
		DebugCallback = function(callbackType, questId)
			DEFAULT_CHAT_FRAME:AddMessage("Callback "..callbackType.." "..questId.." "..Grail:QuestName(questId))
		end,

		---
		--	Debug Use.
		--	Dumps out internal information about the specified NPC to DEFAULT_CHAT_FRAME.
		--	@param npcId The standard numeric npcId representing an NPC.
		DebugDumpOneNPCInfo = function(self, npcId)
			if nil == npcId or not tonumber(npcId) then return end
			npcId = tonumber(npcId)
			local locations, name = self:LocationNPC(npcId)
			if nil ~= name then
				DEFAULT_CHAT_FRAME:AddMessage("NPC id " .. npcId .. " " .. name .. " => " .. self.npcs[npcId][1])
				if nil ~= locations then
					for i = 1, #(locations), 1 do
						if 1 == #(locations[i]) then
							DEFAULT_CHAT_FRAME:AddMessage(locations[i][1])
						else
							DEFAULT_CHAT_FRAME:AddMessage(locations[i][1] .. " " .. locations[i][2])
						end
					end
				else
					DEFAULT_CHAT_FRAME:AddMessage("No Locations")
				end
			else
				DEFAULT_CHAT_FRAME:AddMessage("There is no NPC with id "..npcId)
			end
		end,

		---
		--	Debug Use.
		--	Dumps out internal information about the specified quest and either accept or turnin locations.  Dumps to DEFAULT_CHAT_FRAME.
		--	@param questId The standard numeric questId representing a quest.
		--	@param aOrT Either 'A' for accept or 'T' for turnin.
		DebugDumpOneQuestInfo = function(self, questId, aOrT)
			if nil == questId or not tonumber(questId) then return end
			questId = tonumber(questId)
			local locations, name, npcId, mustKill = self:LocationQuest(questId, aOrT)
			if nil ~= name then
				DEFAULT_CHAT_FRAME:AddMessage("Quest id " .. questId .. " " .. name .. " => " .. self.quests[questId][1])
				if mustKill then DEFAULT_CHAT_FRAME:AddMessage("This is an NPC that must be killed") end
				if nil ~= locations then
					for i = 1, #(locations), 1 do
						if 1 == #(locations[i]) then
							DEFAULT_CHAT_FRAME:AddMessage(locations[i][1])
						else
							DEFAULT_CHAT_FRAME:AddMessage(locations[i][1] .. " " .. locations[i][2])
						end
					end
				else
					DEFAULT_CHAT_FRAME:AddMessage("No Locations")
				end
			else
				DEFAULT_CHAT_FRAME:AddMessage("There is no Quest with id ".. questId)
			end
		end,

		---
		--	Debug Use.
		--	Dumps out internal database information about the quests in the specified table.
		--	@param idTable A table of quest IDs to be dumped to the DEFAULT_CHAT_FRAME
		DebugDumpQuestInfo = function(self, idTable)
			if nil ~= idTable then
				local questId
				for i = 1, #(idTable), 1 do
					questId = idTable[i]
					DEFAULT_CHAT_FRAME:AddMessage(questId .. " " .. self:QuestName(questId) .. " => " .. self.quests[questId][1])
				end
			else
				DEFAULT_CHAT_FRAME:AddMessage("Grail Debug: No quests")
			end
		end,

		---
		--	Internal Use.
		--	Returns whether the quest has the sought code, or in the case of A: or T: codes
		--	returns whether the quest has the equivalent of one of them or an A:0 or T:0
		--	which is assumed to match.
		--	@param questId The standard numeric questId representing a quest.
		--	@param soughtCode An internal code to the database.
		--	@return True is the quest has the sought code or its A:/T: equivalent, otherwise false.
		HasAliasCode = function(self, questId, soughtCode)
			local retval = self:HasCode(questId, soughtCode)
			if false == retval then
				local first, last, kind, npcId = strfind(soughtCode, "^([AT]):(.*)")
				if nil ~= first then
					npcId = tonumber(npcId)
					local allCodes = {}
					self:HasAliasCodeHelper(allCodes, questId, kind..":")
					self:HasAliasCodeHelper(allCodes, questId, kind.."A:")
					self:HasAliasCodeHelper(allCodes, questId, kind.."H:")
					for i = 1, #allCodes do
						local comparison = tonumber(allCodes[i])
						if npcId == comparison or 0 == comparison then
							retval = true
						else
							local aliasCodes = self:CodesWithPrefixNPC(comparison, "A:")
							if nil ~= aliasCodes then	-- if there are any, there should only be 1
								local _, aliasNPCId = strsplit(":", aliasCodes[1])
								if tonumber(aliasNPCId) == npcId then
									retval = true
								end
							end
						end
					end
				end
			end
			return retval
		end,

		---
		--	Internal Use.
		--	Returns the table passed in with the comma-separated values associated with the soughtPrefix codes
		--	added to the table.
		--	@param resultTable A table that will have values added to it.
		--	@param questId The standard numeric questId representing a quest.
		--	@param soughtPrefix The prefix to seek for internal codes used in the database.
		--	@return The modified input resultTable.
		HasAliasCodeHelper = function(self, resultTable, questId, soughtPrefix)
			local codes = self:CodesWithPrefixQuest(questId, soughtPrefix)
			if nil ~= codes then
				for i = 1, #codes do
					local _, commaList = strsplit(":", codes[i])
					local theTable = { strsplit(",", commaList) }
					for j = 1, #theTable do
						tinsert(resultTable, theTable[j])
					end
				end
			end
			return resultTable
		end,

		---
		--	Internal Use.
		--	Returns whether the quest has the sought code.  It requires an exact match.
		--	@param questId The standard numeric questId representing a quest.
		--	@param soughtCode An internal code to the database.
		--	@return True if the quest has the sought code, otherwise false.
		HasCode = function(self, questId, soughtCode)
			if nil == questId or not tonumber(questId) then return false end
			questId = tonumber(questId)
			if nil == self.quests[questId] then return false end
			local retval = false
			local codeArray = { strsplit(" ", self.quests[questId][1]) }
			if nil ~= codeArray then
				for i = 1, #(codeArray), 1 do
					if codeArray[i] == soughtCode then
						retval = true
					end
				end
			end
			return retval
		end,

		---
		--	Indicates whether the character has ever completed the specified quest.
		--	@param questId The standard numeric questId representing a quest.
		--	@return true if the quest has been marked complete, or if the quest has been completed and is one that Blizzard resets daily/weekly/yearly, otherwise false
		HasQuestEverBeenCompleted = function(self, questId)
			return self:IsQuestCompleted(questId) or self:IsResettableQuestCompleted(questId)
		end,

		---
		--	Indicates whether the character is in a heroic instance with the specified NPC.
		--	@param npcId The standard numeric npcId representing an NPC.
		--	@return true if the character is in a heroic instance where the NPC is located, otherwise false
		InWithHeroicNPC = function(self, npcId)
			local retval = false
			local isHeroic, instanceName = self:IsInHeroicInstance()
			if isHeroic then
				local locations = self:LocationNPCInMap(npcId, false, false, true)	-- only return things that match the current zone
				if nil ~= locations and 0 < #(locations) then
					retval = true
				end
			end
			return retval
		end,

		---
		--	Indicates whether the quest is a daily quest as indicated by the internal database.
		--	@param questId The standard numeric questId representing a quest.
		--	@return true if the quest is a daily quest, otherwise false
		IsDaily = function(self, questId)
			return self:HasCode(questId, '+D')
		end,

		---
		--	Indicates whether the quest is a dungeon quest.
		--	@param questId The standard numeric questId representing a quest.
		--	@return true if the quest is a dungeon quest, otherwise false
		IsDungeon = function(self, questId)
			return self:IsType(questId, 64)
		end,

		---
		--	Indicates whether the quest is an escort quest.
		--	@param questId The standard numeric questId representing a quest.
		--	@return true if the quest is an escort quest, otherwise false
		IsEscort = function(self, questId)
			return self:IsType(questId, 32)
		end,

		---
		--	Indicates whether the quest is a group quest.
		--	@param questId The standard numeric questId representing a quest.
		--	@return true if the quest is a group quest, otherwise false
		IsGroup = function(self, questId)
			return self:IsType(questId, 512)
		end,

		---
		--	Indicates whether the quest is a heroic quest.
		--	@param questId The standard numeric questId representing a quest.
		--	@return true if the quest is a heroic quest, otherwise false
		IsHeroic = function(self, questId)
			return self:IsType(questId, 1024)
		end,

		---
		--	Indicates whether the NPC is only to be found in heroic instances.
		--	@param npcId The standard numeric npcId representing an NPC.
		--	@return True if the NPC is only found in heroic instances, false otherwise.
		IsHeroicNPC = function(self, npcId)
			retval = false
			local codes = self:CodesWithPrefixNPC(npcId, 'X')
			if nil ~= codes and 0 < #(codes) then
				retval = true
			end
			return retval
		end,

		---
		--	Indicates whether the character is in a heroic instance.
		--	@return true if the character is in a heroic instance, otherwise false
		--	@return the name of the instance the character is in
		--	@usage isHeroic, instanceName = Grail:IsInHeroicInstance()
		IsInHeroicInstance = function(self)
			local retval = false
			local name, type, difficultyIndex, difficultyName, maxPlayers, dynamicDifficulty, isDynamic = GetInstanceInfo()
			if "none" ~= type then
				if 3 == difficultyIndex or 4 == difficultyIndex or (2 == difficultyIndex and "raid" ~= type) then
					retval = true
				end
			end
			return retval, name
		end,

		---
		--	Indicates whether the quest is invalidated (meaning it cannot be accepted based on other completed quests or those in the quest log).
		--	@param questId The standard numeric questId representing a quest.
		--	@return True if the quest cannot be accepted because of a quest in the log or one already completed, false otherwise.
		--	@return A table of failure reasons, or nil if there are none.
		IsInvalidated = function(self, questId)
			local retval = false
			local any, present, failures = self:AnyEvaluateTrue(questId, "I:", true)
			if present then
				retval = any
			end
			return retval, failures
		end,

		---
		--	Returns whether the quest is a low level quest in comparison to the specified level
		--	or that of the player if none specified.
		--	@param questId The standard numeric questId representing a quest.
		--	@param comparisonLevel The level used to make a comparison against the quest level.
		--	@return True if the comparisonLevel (or player level) is more than the quest's level plus Blizzard's green range comparison
		IsLowLevel = function(self, questId, comparisonLevel)
			local retval = false
			comparisonLevel = tonumber(comparisonLevel) or UnitLevel("player")
			retval = (comparisonLevel > ((self:QuestLevel(questId) or 1) + (GetQuestGreenRange() or 8)))	-- 8 is the return value from GetQuestGreenRange() for anyone level 60 or higher (at least)
			return retval
		end,

		---
		--	Returns whether the NPC should be available to the character.
		--	@param npcId The standard numeric npcId representing an NPC.
		--	@return True if the NPC is available based on holidays currently celebrated and presence in a heroic instance, false otherwise.
		IsNPCAvailable = function(self, npcId)
			if nil == npcId or not tonumber(npcId) then return false end
			npcId = tonumber(npcId)
			local retval = true
			local codes = self:CodesWithPrefixNPC(npcId, 'H')
			if nil ~= codes then
				local holidayGood = true
				for i = 1, #(codes), 1 do
					if holidayGood then
						holidayGood = self:CelebratingHoliday(self.holidayMapping[strsub(codes[i], 2, 2)])
					end
				end
				retval = holidayGood
			end
			if retval and self:IsHeroicNPC(npcId) then
				retval = self:InWithHeroicNPC(npcId)
			end
			return retval
		end,

		---
		--	Indicates whether the quest is a PVP quest.
		--	@param questId The standard numeric questId representing a quest.
		--	@return true if the quest is a PVP quest, otherwise false
		IsPVP = function(self, questId)
			return self:IsType(questId, 256)
		end,

		---
		--	Returns whether the quest is considered completed.
		--	Note that certain types of quests can be reset (e.g., dailies) and when they are, this routine will return false.  These types of
		--	quests can be completed and this routine will return true until they are once again reset.
		--	@param questId The standard numeric questId representing a quest.
		--	@return True if the quest is completed, false otherwise.
		--	@see HasQuestEverBeenCompleted
		--	@see IsResettableQuestCompleted
		IsQuestCompleted = function(self, questId)
			if nil == questId or not tonumber(questId) then return false end
			questId = tonumber(questId)
			local retval = false
			local index = math.floor((questId - 1) / 32)
			local offset = questId - (index * 32) - 1
			local db = GrailDatabase[self.playerRealm][self.playerName]["completedQuests"]
			if (nil ~= db[index]) then
				if bit.band(db[index], 2^offset) > 0 then
					retval = true
				end
			end
			return retval
		end,

		---
		--	Returns whether the quest is in the quest log.
		--	@param questId The standard numeric questId representing a quest.
		--	@return True if the quest is in the quest log, false otherwise.
		--	@return True if the quest is marked as complete in the quest log, false otherwise.
		--	@use inLog, isComplete = Grail:IsQuestInQuestLog(11)
		IsQuestInQuestLog = function(self, questId)
			if nil == questId or not tonumber(questId) then return false end
			questId = tonumber(questId)
			local retval = false
			local retvalComplete = false
			local i = 1
			while GetQuestLogTitle(i) and false == retval do
				local questTitle, level, questTag, suggestedGroup, isHeader, isCollapsed, isComplete, isDaily, questID = GetQuestLogTitle(i)
				if (not isHeader) then
					if questID == questId then
						retval = true
						retvalComplete = isComplete
					end
				end
				i = i + 1
			end
			return retval, retvalComplete
		end,

		---
		--	Indicates whether the quest is a raid quest.
		--	@param questId The standard numeric questId representing a quest.
		--	@return true if the quest is a raid quest, otherwise false
		IsRaid = function(self, questId)
			return self:IsType(questId, 128)
		end,

		---
		--	Returns whether the quest is a repeatable quest.
		--	@param questId The standard numeric questId representing a quest.
		--	@return True if the quest is a repeatable quest, false otherwise.
		IsRepeatable = function(self, questId)
			return self:HasCode(questId, '++')
		end,

		---
		--	Returns whether the quest is a resettable quest and has been completed in the past.
		--	This routine can return true and IsQuestCompleted() can return false as the quest can be reset.
		--	@param questId The standard numeric questId representing a quest.
		--	@return True if the quest is resettable and has ever been completed, false otherwise.
		--	@see HasQuestEverBeenCompleted
		--	@see IsQuestCompleted
		IsResettableQuestCompleted = function(self, questId)
			if nil == questId or not tonumber(questId) then return false end
			questId = tonumber(questId)
			local retval = false
			local index = math.floor((questId - 1) / 32)
			local offset = questId - (index * 32) - 1
			local db = GrailDatabase[self.playerRealm][self.playerName]["completedResettableQuests"]
			if (nil ~= db[index]) then
				if bit.band(db[index], 2^offset) > 0 then
					retval = true
				end
			end
			return retval
		end,

		---
		--	Returns whether this is a special type of NPC that has information useful for tooltips and a table of that information
		--	where each item in the table is a table with the type of NPC and the associated NPC/quest/item ID.
		--	@param npcId The standard numeric npcId representing an NPC.
		--	@return True if there is any table data being returned, false otherwise
		--	@return Table data containing tables of NPC type and associated ID.
		IsTooltipNPC = function(self, npcId)
			local retval = {}
			local dropCodes = self:CodesWithPrefixNPC(npcId, 'D:')
			if nil ~= dropCodes then
				for i = 1, #(dropCodes), 1 do
					local npcs = { strsplit(",", strsub(dropCodes[i], 3)) }
					if nil ~= npcs then
						for j = 1, #(npcs), 1 do
							tinsert(retval, { self.NPC_TYPE_BY, npcs[j] } )
						end
					end
				end
			end
			local killCodes = self:CodesWithPrefixNPC(npcId, 'K:')
			if nil ~= killCodes then
				for i = 1, #(killCodes), 1 do
					local quests = { strsplit(",", strsub(killCodes[i], 3)) }
					if nil ~= quests then
						for j = 1, #(quests), 1 do
							tinsert(retval, { self.NPC_TYPE_KILL, quests[j] } )
						end
					end
				end
			end
			local hasCodes = self:CodesWithPrefixNPC(npcId, 'H:')
			if nil ~= hasCodes then
				for i = 1, #(hasCodes), 1 do
					local items = { strsplit(",", strsub(hasCodes[i], 3)) }
					if nil ~= items then
						for j = 1, #(items), 1 do
							tinsert(retval, { self.NPC_TYPE_DROP, items[j] } )
						end
					end
				end
			end
			return (0 < #(retval)), retval
		end,

		---
		--	Internal Use.
		--	Returns whether the specified quest has a quest type marker containing the sought bit value.
		--	@param questId The standard numeric questId representing a quest.
		--	@param soughtBitValue The bit value to use to compare against the quest type information.
		--	@return True if the quest has the sought bits, false otherwise.
		IsType = function(self, questId, soughtBitValue)
			local retval = false
			local codes = self:CodesWithPrefixQuest(questId, 'K')
			if nil ~= codes and strlen(codes[1]) > 4 then
				retval = (bit.band(tonumber(strsub(codes[1], 5)), soughtBitValue) > 0)
			end
			return retval
		end,

		---
		--	Returns whether the quest is a weekly quest.
		--	@param questId The standard numeric questId representing a quest.
		--	@return True if the quest is a weekly quest, false otherwise.
		IsWeekly = function(self, questId)
			return self:HasCode(questId, '+W')
		end,

		---
		--	Returns whether the quest is a yearly quest.
		--	@param questId The standard numeric questId representing a quest.
		--	@return True if the quest is a yearly quest, false otherwise.
		IsYearly = function(self, questId)
			return self:HasCode(questId, '+Y')
		end,

		---
		--	Returns a table of locations for the NPC and its name.
		--	Each table element is itself a table with one or two entries.  Those with two entries will have as their first entry the localized
		--	name of the zone.  The second entry will either be the x,y pair of coordinates in the zone or the special word Near which represents
		--	something that should be near the character (from a recent summoning for example) or the special word Self which represents automatic
		--	acceptance of the quest.  Those with one entry are special and can have either the localized name of the zone in which an object is
		--	found or one of the special keywords:<br>
		--	Preowned (the character must already own the object since it has been removed from the game)<br>
		--	Created (this item can be created)<br>
		--	Mailbox (this item will be mailed by Blizzard)<br>
		--	Self (this will be used by quests that are automatically accepted or have no NPC)<br>
		--	Near (the NPC should be nearby (from a recent summons for example))<br>
		--	@param npcId The standard numeric npcId representing an NPC.
		--	@param requiresNPCAvailable If true, the NPC must be available.
		--	@param onlySingleReturn If true, only one location will be in the returned table, otherwise all matching locations will be there.
		--	@param onlyZoneReturn If true, only locations matching the appropriate zone will be returned.
		--	@param preferredZoneName The localized name of the zone to use, and if nil the character's current zone is used.
		--	@return A table of locations where the NPC can be found or nil if there are none.
		--	@return The localized name of the NPC.
		--	@see IsNPCAvailable
		LocationNPC = function(self, npcId, requiresNPCAvailable, onlySingleReturn, onlyZoneReturn, preferredZoneName)
			local preferredMapId = nil
			if nil ~= preferredZoneName then
				preferredMapId = self.zoneNameMapping[preferredZoneName]
				if nil == preferredMapId and self.debug then print("Grail:LocationNPC could not find zone name ", preferredZoneName, " so is using current zone.") end
			end
			local retval, retvalName = self:LocationNPCInMap(npcId, requiresNPCAvailable, onlySingleReturn, onlyZoneReturn, preferredMapId)
			if nil ~= retval then
				for i = 1, #retval do
					if 2 == #(retval[i]) or (1 == #(retval[i]) and tonumber(retval[i][1])) then retval[i][1] = self.mapAreaMapping[tonumber(retval[i][1])] end
				end
			end
			return retval, retvalName
		end,

		---
		--	Returns a table of locations for the NPC and its name.
		--	Each table element is itself a table with one or two entries.  Those with two entries will have as their first entry the map area ID.
		--	The second entry will either be the x,y pair of coordinates in the zone or the special word Near which represents something that
		--	should be near the character (from a recent summoning for example) or the special word Self which represents automatic acceptance of
		--	the quest.  Those with one entry are special and can have either the map area ID in which an object is found or one of the special keywords:<br>
		--	Preowned (the character must already own the object since it has been removed from the game)<br>
		--	Created (this item can be created)<br>
		--	Mailbox (this item will be mailed by Blizzard)<br>
		--	Self (this will be used by quests that are automatically accepted or have no NPC)<br>
		--	Near (the NPC should be nearby (from a recent summons for example))<br>
		--	@param npcId The standard numeric npcId representing an NPC.
		--	@param requiresNPCAvailable If true, the NPC must be available.
		--	@param onlySingleReturn If true, only one location will be in the returned table, otherwise all matching locations will be there.
		--	@param onlyMapAreaReturn If true, only locations matching the appropriate map area will be returned.
		--	@param preferredMapId The map area to use, and if nil the character's current zone is used.
		--	@return A table of locations where the NPC can be found or nil if there are none.
		--	@return The localized name of the NPC.
		--	@see IsNPCAvailable
		LocationNPCInMap = function(self, npcId, requiresNPCAvailable, onlySingleReturn, onlyMapAreaReturn, preferredMapId)
			if nil == npcId or not tonumber(npcId) then return nil, nil end
			npcId = tonumber(npcId)
			if nil == self.npcs[npcId] then return nil, nil end
			local retval = {}
			local retvalName = self:NPCName(npcId)
			local codes = self.npcs[npcId][1]
			local npcIsDarkmoonFaire = false
			local mapIdToUse = tonumber(preferredMapId) or GetCurrentMapAreaID()
			if nil ~= codes and (not requiresNPCAvailable or self:IsNPCAvailable(npcId)) then
				local codeArray = { strsplit(" ", codes) }

				-- First we check for the Darkmoon Faire code to see if we must limit the returned locations to the current Darkmoon Faire location
				for i = 1, #codeArray do
					if codeArray[i] == 'HF' then
						npcIsDarkmoonFaire = true
					end
				end

				for i = 1, #(codeArray), 1 do
					local controlCode = strsub(codeArray[i], 1, 1)
					local mapAreaId
					if 'Z' == controlCode then
						mapAreaId = tonumber(strsub(codeArray[i], 2))
						if not onlyMapAreaReturn or (onlyMapAreaReturn and mapIdToUse == mapAreaId) then
							tinsert(retval, { mapAreaId })
						end
					elseif 'H' == controlCode then
						-- ignore this because it indicates the NPC is only present during a holiday, or if H: has the specified item that starts a quest
					elseif 'K' == controlCode then
						-- ignore this because it indicates the NPC is killed to start the specified quest
					elseif 'A' == controlCode then
						-- ignore this because it is an alias marker indicating the real NPC id
					elseif 'X' == controlCode then
						-- ignore this because it indicates the NPC is only present in heroic mode
					elseif 'P' == controlCode
					    or 'C' == controlCode
					    or 'M' == controlCode
					    or 'S' == controlCode then
						tinsert(retval, { codeArray[i] })	-- Preowned, Created, Mailbox, Self
					elseif 'N' == controlCode then
						if strlen(codeArray[i]) > 4 then	--	NearXXXX where XXXX is the only map area where this Near can appear
							mapAreaId = tonumber(strsub(codeArray[i], 5))
							if not onlyMapAreaReturn or (onlyMapAreaReturn and mapIdToUse == mapAreaId) then
								tinsert(retval, { mapAreaId, "Near" })
							end
						else
							tinsert(retval, { codeArray[i] })	-- Near
						end
					elseif 'D' == controlCode then
						-- these are NPCs that can drop the item that we are processing, therefore add the location for each NPC to the table
						local modifiedName = false
						if strlen(codeArray[i]) > 2 and ':' == strsub(codeArray[i], 2, 2) then
							local realNPCs = { strsplit(',', strsub(codeArray[i], 3)) }
							for j = 1, #realNPCs do
								local subnpclocations, subnpcname = self:LocationNPCInMap(realNPCs[j], requiresNPCAvailable, onlySingleReturn, onlyMapAreaReturn, preferredMapId)
								if nil ~= subnpclocations then
									for k = 1, #subnpclocations do
										tinsert(retval, subnpclocations[k])
									end
								end
								if subnpcname and not modifiedName then
									retvalName = subnpcname .. " (" .. retvalName .. ")" -- an NPC that drops the item needed for the quest in parentheses
									modifiedName = true
								end
							end
						end
					else	-- a real coordinate
						local mapId, rest = strsplit(":", codeArray[i])
						local mapLevel = 0
						local mapLevelString
						mapId, mapLevelString = strsplit("[", mapId)
						mapId = tonumber(mapId)
						if nil ~= mapLevelString then
							mapLevel = tonumber(strsub(mapLevelString, 1, strlen(mapLevelString) - 1))
						end
						local coord, realArea = nil, nil
						if nil ~= rest then
							coord, realArea = strsplit(">", rest)
						end
						if not npcIsDarkmoonFaire or (npcIsDarkmoonFaire and nil ~= self.darkmoonFaireLocation and mapId == self.darkmoonFaireLocation) then
							if not onlyMapAreaReturn or (onlyMapAreaReturn and mapIdToUse == mapId) then
								if nil ~= realArea then
									tinsert(retval, { mapId, coord, mapLevel, tonumber(realArea) })
								else
									tinsert(retval, { mapId, coord, mapLevel })
								end
							end
						end
					end
				end
			end
			if onlySingleReturn and 1 < #retval then
				retval = { retval[1] }		-- pick the first item for no better algorithm to use to decide
			end
			if 0 == #retval then
				retval = nil
			end
			return retval, retvalName
		end,

		---
		--	Returns a table of locations for the quest's NPC for accepting the quest or turning it in and the NPC name.
		--	@param questId The standard numeric questId representing a quest.
		--	@param acceptOrTurnin 'A' for the NPC from which the quest is accepted or 'T' for the NPC to which one turns in the quest.  If nil, 'A' is used.
		--	@param requiresNPCAvailable If true, the NPC must be available.
		--	@param onlySingleReturn If true, only one location will be in the returned table, otherwise all matching locations will be there.
		--	@param onlyZoneReturn If true, only locations matching the appropriate zone will be returned.
		--	@param preferredZoneName The localized name of the zone to use, and if nil the character's current zone is used.
		--	@return A table of locations where the NPC can be found or nil if there are none.
		--	@return The localized name of the NPC.
		--	@return Whether the NPC must be killed.
		--	@see LocationNPC
		LocationQuest = function(self, questId, acceptOrTurnin, requiresNPCAvailable, onlySingleReturn, onlyZoneReturn, preferredZoneName)
			local preferredMapId = nil
			if nil ~= preferredZoneName then
				preferredMapId = self.zoneNameMapping[preferredZoneName]
				if nil == preferredMapId and self.debug then print("Grail:LocationNPC could not find zone name ", preferredZoneName, " so is using current zone.") end
			end
			local retval, npcNameRetval, npcIdRetval, npcMustDie = self:LocationQuestInMap(questId, acceptOrTurnin, requiresNPCAvailable, onlySingleReturn, onlyZoneReturn, preferredMapId)
-- TODO: Determine whether we need to change map IDs to zone names like we did for LocationNPC
			return retval, npcNameRetval, npcIdRetval, npcMustDie
		end,

		---
		--	Returns a table of locations for the quest's NPC for accepting the quest or turning it in and the NPC name, NPC ID and whether the NPC needs to
		--	be killed to satisfy the quest.  Note that the two parameters onlySingleReturn and acceptsMultipleUniques can be used to get specific location
		--	information as follows.  E.g., an NPC may wander and therefore have more than one location.  To get only one of those locations (instead of all
		--	of them) specify true for onlySingleReturn.  However, if there is more than one NPC that provides a quest (like the Hero Boards in Stormwind)
		--	and you are specifying true for onlySingleReturn, you need to specify true for acceptsMultipleUniques to get both boards, otherwise only one of
		--	them will appear in the results.
		--	@param questId The standard numeric questId representing a quest.
		--	@param acceptOrTurnin 'A' for the NPC from which the quest is accepted or 'T' for the NPC to which one turns in the quest.  If nil, 'A' is used.
		--	@param requiresNPCAvailable If true, the NPC must be available.
		--	@param onlySingleReturn If true, only one location will be in the returned table, otherwise all matching locations will be there.
		--	@param onlyMapAreaReturn If true, only locations matching the appropriate map area will be returned.
		--	@param preferredMapId The map area to use, and if nil the character's current zone is used.
		--	@param acceptsMultipleUniques If true, returns locations for multiple NPCs that satisfy the many criteria.
		--	@return A table of locations where the NPC can be found or nil if there are none.
		--	@return The localized name of the NPC.
		--	@return Whether the NPC must be killed.
		--	@see LocationNPCInMap
		LocationQuestInMap = function(self, questId, acceptOrTurnin, requiresNPCAvailable, onlySingleReturn, onlyMapAreaReturn, preferredMapId, acceptsMultipleUniques)
			local retval, npcMustDie = self:LocationQuestInMapRaw(questId, acceptOrTurnin, requiresNPCAvailable, onlySingleReturn, onlyMapAreaReturn, preferredMapId, acceptsMultipleUniques)
			local newRetval, npcNameRetval, npcIdRetval = {}, nil, nil

			if 0 == #retval then
				retval = nil
			else
				for i = 1, #retval do
					tinsert(newRetval, retval[i][1])
					if nil == npcNameRetval then npcNameRetval = retval[i][2] end
					if nil == npcIdRetval then npcIdRetval = retval[i][3] end
				end
				retval = newRetval
			end

			return retval, npcNameRetval, npcIdRetval, npcMustDie
		end,


		---
		--	Returns a table of locations for the quest's NPC for accepting the quest or turning it in and whether the NPC needs to be killed to satisfy the quest.
		--	The returned table has location, name, and ID.  Note that the two parameters onlySingleReturn and acceptsMultipleUniques can be used to get specific location
		--	information as follows.  E.g., an NPC may wander and therefore have more than one location.  To get only one of those locations (instead of all
		--	of them) specify true for onlySingleReturn.  However, if there is more than one NPC that provides a quest (like the Hero Boards in Stormwind)
		--	and you are specifying true for onlySingleReturn, you need to specify true for acceptsMultipleUniques to get both boards, otherwise only one of
		--	them will appear in the results.
		--	@param questId The standard numeric questId representing a quest.
		--	@param acceptOrTurnin 'A' for the NPC from which the quest is accepted or 'T' for the NPC to which one turns in the quest.  If nil, 'A' is used.
		--	@param requiresNPCAvailable If true, the NPC must be available.
		--	@param onlySingleReturn If true, only one location will be in the returned table, otherwise all matching locations will be there.
		--	@param onlyMapAreaReturn If true, only locations matching the appropriate map area will be returned.
		--	@param preferredMapId The map area to use, and if nil the character's current zone is used.
		--	@param acceptsMultipleUniques If true, returns locations for multiple NPCs that satisfy the many criteria.
		--	@return A table of locations where the NPC can be found or nil if there are none.
		--	@return The localized name of the NPC.
		--	@return Whether the NPC must be killed.
		--	@see LocationNPCInMap
		LocationQuestInMapRaw = function(self, questId, acceptOrTurnin, requiresNPCAvailable, onlySingleReturn, onlyMapAreaReturn, preferredMapId, acceptsMultipleUniques)
			if nil == questId or not tonumber(questId) then return nil, nil, nil end
			questId = tonumber(questId)
			local retval, npcMustDie = {}, false
			local codeToUse = acceptOrTurnin or 'A'
			if 'A' ~= codeToUse and 'T' ~= codeToUse then codeToUse = 'A' end
			local factionCode = 'A'
			if 'Horde' == self.playerFaction then factionCode = 'H' end
			local factionSpecificValue = codeToUse..factionCode
			local npcCodes = self:CodesWithPrefixQuest(questId, factionSpecificValue..':') or self:CodesWithPrefixQuest(questId, codeToUse..':')
			if nil == npcCodes then
				npcCodes = self:CodesWithPrefixQuest(questId, codeToUse..'K:')
				if nil ~= npcCodes then npcMustDie = true end
			end
			if nil ~= npcCodes then
				for i = 1, #npcCodes do
					local _, npcs = strsplit(":", npcCodes[i])
					local npcTable = { strsplit(",", npcs) }
					for j = 1, #npcTable do
						local npcLocations, npcName = self:LocationNPCInMap(npcTable[j], requiresNPCAvailable, onlySingleReturn, onlyMapAreaReturn, preferredMapId)
						if nil ~= npcLocations then
							for _, value in pairs(npcLocations) do
								tinsert(retval, { value, npcName, npcTable[j] })
							end
						end
					end
				end
			else
				local zoneCodes = self:CodesWithPrefixQuest(questId, codeToUse..'Z:')
				if nil ~= zoneCodes then
					local mapId = tonumber(preferredMapId) or GetCurrentMapAreaID()
					for i = 1, #zoneCodes do
						local _, zones = strsplit(":", zoneCodes[i])
						local zoneTable = { strsplit(",", zones) }
						for j = 1, #zoneTable do
							if not onlyMapAreaReturn or (onlyMapAreaReturn and tonumber(zoneTable[j]) == mapId) then
								tinsert(retval, { {mapId}, "Self", 0 })
							end
						end
					end
				end
			end
			-- Since the return values from LocationNPCMapInfo will process things like onlySingleReturn properly, that means the retval should only have one location value per unique NPC
			-- and that means we can make use of acceptsMultipleUniques to ignore the onlySingleReturn value at this point.
			if onlySingleReturn and not acceptsMultipleUniques and 1 < #retval then retval = { retval[1] } end		-- pick the first item for no better algorithm to use to decide

			return retval, npcMustDie
		end,

		---
		--	Internal Use.
		LogNameIssue = function(self, npcOrQuest, id, properTitle)
			if GrailDatabase[npcOrQuest] == nil then GrailDatabase[npcOrQuest] = {} end
			if GrailDatabase[npcOrQuest][self.playerLocale] == nil then GrailDatabase[npcOrQuest][self.playerLocale] = {} end
			GrailDatabase[npcOrQuest][self.playerLocale][id] = properTitle
		end,

		---
		--	Internal Use.
		--	This marks the specified quest as complete in the internal database.  Optionally it will attempt to update the NewNPCs and NewQuests.
		--	@param questId The standard numeric questId representing a quest.
		--	@param updateDatabase If true the NewNPCs and NewQuests will be updated as well as posting the Complete notification.
		MarkQuestComplete = function(self, questId, updateDatabase)
			local v = tonumber(questId)
			local index = math.floor((v - 1) / 32)
			local offset = v - (index * 32) - 1
			local db = GrailDatabase[self.playerRealm][self.playerName]["completedQuests"]
			if (nil == db[index]) then
				db[index] = 0
			end
			if bit.band(db[index], 2^offset) == 0 then
				db[index] = db[index] + (2^offset)
			else
				if self.debug then print(format("Quest %d is already marked completed", v)) end
			end

			if updateDatabase then

				if not self:IsResettableQuestCompleted(questId) and (self:IsDaily(questId) or self:IsWeekly(questId) or self:IsYearly(questId) or self:IsRepeatable(questId)) then
					local rdb = GrailDatabase[self.playerRealm][self.playerName]["completedResettableQuests"]
					if (nil == rdb[index]) then
						rdb[index] = 0
					end
					rdb[index] = rdb[index] + (2^offset)
				end

				-- Get the target information to ensure the target exists in the database of NPCs
				local version = self.versionNumber.."/"..self.questsVersionNumber.."/"..self.npcsVersionNumber.."/"..self.zonesVersionNumber
				local targetName, npcId, coordinates = self:TargetInformation()
				self:UpdateTargetDatabase(targetName, npcId, coordinates, version)
				if self.debug then
					if nil ~= targetName then
						DEFAULT_CHAT_FRAME:AddMessage("Grail Debug: Marked questId "..questId.." complete, turned in to: "..targetName.."("..npcId..") "..coordinates)
					else
						DEFAULT_CHAT_FRAME:AddMessage("Grail Debug: Turned in quest "..questId.." with no target")
					end
				end
				self:UpdateQuestDatabase(questId, 'No Title Stored', npcId, false, 'T:', version)
				self:PostNotification("Complete", questId)
			end

		end,

		---
		--	Returns whether the character meets prerequisites for the specified quest.
		--	@param questId The standard numeric questId representing a quest.
		--	@return True if the character meets the prerequisites for the specified quest or false otherwise.
		--	@return A table of failures if any, nil otherwise.
		MeetsPrerequisites = function(self, questId)
			local retval = true
			local any, present, failures = self:AnyEvaluateTrue(questId, "P:", false)
			if present then
				retval = any
			else
				any, present, failures = self:AnyEvaluateTrue(questId, "PL:", true)
				if present then
					retval = any
				else
					any, present, failures = self:AnyEvaluateTrue(questId, "PH:", false, true)
					if present then
						retval = any
					end
				end
			end
			return retval, failures
		end,

		---
		--	Internal Use.
		--	Returns whether the character meets the requirements indicated by the code.
		--	@param questId The standard numeric questId representing a quest.
		--	@param requirementCode An internal code that indicates what type of requirement should be checked.
		--	@param soughtParameter A value to be checked against, or if nil a reasonable default based on requirementCode will be used.
		--	@return True if the requirement is met by the character, false otherwise.
		--	@return A table of failures specific to each type of requirement or nil if there are no failures.
		MeetsRequirement = function(self, questId, requirementCode, soughtParameter)
			if nil == questId or not tonumber(questId) then return false end
			questId = tonumber(questId)
			local retval = true
			local failures = { }
			local foundAnyRequirement = false
			local foundAnyMatchingRequirement = false
			local codeArray = { strsplit(" ", self.quests[questId][1]) }
			local possibleFailures = { }
			if nil ~= codeArray then
				for i = 1, #(codeArray), 1 do
					local controlCode = strsub(codeArray[i], 1, 1)
					local controlValue = strsub(codeArray[i], 2, 2)
					if requirementCode == controlCode then
						if 'G' == controlCode then
							if self.genderMapping[controlValue] ~= self.playerGender then
								retval = false
							end
						elseif 'F' == controlCode then
							if self.factionMapping[controlValue] ~= (soughtParameter or self.playerFaction) then
								retval = false
							end
						elseif 'C' == controlCode or 'X' == controlCode then
							foundAnyRequirement = true
							if self.classMapping[controlValue] == (soughtParameter or self.playerClass) then
								foundAnyMatchingRequirement = true
							end
						elseif 'H' == controlCode then
							foundAnyRequirement = true
							if self:CelebratingHoliday(self.holidayMapping[controlValue]) then
								foundAnyMatchingRequirement = true
							else
								tinsert(possibleFailures, codeArray[i])
							end
						elseif 'R' == controlCode  or 'S' == controlCode then
							foundAnyRequirement = true
							if self.raceMapping[controlValue] == (soughtParameter or self.playerRace) then
								foundAnyMatchingRequirement = true
							end
						elseif 'V' == controlCode then
							local repIndex = tonumber(strsub(codeArray[i], 2, 3))
							local repValue = tonumber(strsub(codeArray[i], 4, 8))
							local exceeds, earnedValue = self:ReputationExceeds(self.reputationMapping[repIndex], repValue)
							if not exceeds then
								retval = false
								if nil ~= earnedValue then
									tinsert(failures, codeArray[i].." actual: "..earnedValue)
								end
							end
						elseif 'W' == controlCode then
							local repIndex = tonumber(strsub(codeArray[i], 2, 3))
							local repValue = tonumber(strsub(codeArray[i], 4, 8))
							local exceeds, earnedValue = self:ReputationExceeds(self.reputationMapping[repIndex], repValue)
							if exceeds then
								retval = false
								if nil ~= earnedValue then
									tinsert(failures, codeArray[i].." actual: "..earnedValue)
								end
							end
						elseif 'P' == controlCode then
							local colonCheck = strsub(codeArray[i], 3, 3)
							if ':' == controlValue or ('L' == controlValue and ':' == colonCheck) or ('H' == controlValue and ':' == colonCheck) then
								-- we ignore these because they are really P:xxx or PL:xxx which are prerequisites and not profession requirements.  This has to be done
								-- because we could have a PLxxx code requiring a Leatherworking skill level
								-- Now we support PH: as well.
							else
								local profValue = tonumber(strsub(codeArray[i], 3, 5))
								local exceeds, skillLevel = self:ProfessionExceeds(controlValue, profValue)
								if not exceeds then
									retval = false
									tinsert(failures, codeArray[i].." actual: "..skillLevel)
								end
							end
						end
					end
				end
			end
			--	In the case where the requirementCode is X then if we have foundAnyRequirement if we have
			--	also foundMatchingRequirement we have failed, which is the opposite of something like C.
			if foundAnyRequirement then
				if 'X' == requirementCode or 'S' == requirementCode then
					retval = not foundAnyMatchingRequirement
				else
					retval = foundAnyMatchingRequirement
				end
				if false == retval and 0 < #(possibleFailures) then
					tinsert(failures, table.concat(possibleFailures, " "))
				end
			end
			if 0 == #(failures) then failures = nil end
			return retval, failures
		end,

		---
		--	Returns whether the character meets class requirements for the specified quest.
		--	@param questId The standard numeric questId representing a quest.
		--	@soughtClass The desired class to be matched, or if nil the player's class will be used
		--	@return True if the character meets the class requirements for the specified quest or false otherwise.
		--	@return A table of failures if any, nil otherwise.
		MeetsRequirementClass = function(self, questId, soughtClass)
			return self:MeetsRequirement(questId, 'C', soughtClass) and self:MeetsRequirement(questId, 'X', soughtClass)
		end,

		---
		--	Returns whether the character meets faction requirements for the specified quest.
		--	@param questId The standard numeric questId representing a quest.
		--	@soughtFaction The desired faction to be matched, or if nil the player's faction will be used
		--	@return True if the character meets the faction requirements for the specified quest or false otherwise.
		--	@return A table of failures if any, nil otherwise.
		MeetsRequirementFaction = function(self, questId, soughtFaction)
			return self:MeetsRequirement(questId, 'F', soughtFaction)
		end,

		---
		--	Returns whether the character meets gender requirements for the specified quest.
		--	@param questId The standard numeric questId representing a quest.
		--	@return True if the character meets the gender requirements for the specified quest or false otherwise.
		--	@return A table of failures if any, nil otherwise.
		MeetsRequirementGender = function(self, questId)
			return self:MeetsRequirement(questId, 'G')
		end,

		---
		--	Returns whether the character meets holiday requirements for the specified quest.
		--	@param questId The standard numeric questId representing a quest.
		--	@return True if the character meets the holiday requirements for the specified quest or false otherwise.
		--	@return A table of failures if any, nil otherwise.
		MeetsRequirementHoliday = function(self, questId)
			return self:MeetsRequirement(questId, 'H')
		end,

		---
		--	Returns whether the level requirements are met for the specified quest.  This handles both minimum and maximum level requirements.
		--	@param questId The standard numeric questId representing a quest.
		--	@param optionalComparisonLevel A comparison level to use.  If nil the character's level is used.
		--	@return True if the level requirements for the specified quest are met or false otherwise.
		--	@return The level used in making comparisons to the requirements of the quest.
		--	@return The minimum level required for the quest.
		--	@return The maximum level permitted for the quest or Grail.INFINITE_LEVEL if there is none.
		MeetsRequirementLevel = function(self, questId, optionalComparisonLevel)
			if nil == questId or not tonumber(questId) then return false end
			questId = tonumber(questId)
			local retval = true
			local levelToCompare = optionalComparisonLevel or UnitLevel('player')
			local levelCodes = self:CodesWithPrefixQuest(questId, "L")
			local levelRequired = 1
			if nil ~= levelCodes then	-- there should only be one level code or nil returned
				levelRequired = tonumber(strsub(levelCodes[1], 2))
			end
			local levelNotToExceed = self.INFINITE_LEVEL
			levelCodes = self:CodesWithPrefixQuest(questId, "M")
			if nil ~= levelCodes then
				levelNotToExceed = tonumber(strsub(levelCodes[1], 2))
			end
			if levelToCompare < levelRequired or levelToCompare > levelNotToExceed then
				retval = false
			end
			return retval, levelToCompare, levelRequired, levelNotToExceed
		end,

		---
		--	Returns whether the character meets profession requirements for the specified quest.
		--	@param questId The standard numeric questId representing a quest.
		--	@return True if the character meets the profession requirements for the specified quest or false otherwise.
		--	@return A table of failures if any, nil otherwise.
		MeetsRequirementProfession = function(self, questId)
			return self:MeetsRequirement(questId, 'P')
		end,

		---
		--	Returns whether the character meets race requirements for the specified quest.
		--	@param questId The standard numeric questId representing a quest.
		--	@soughtRace The desired race to be matched, or if nil the player's race will be used
		--	@return True if the character meets the race requirements for the specified quest or false otherwise.
		--	@return A table of failures if any, nil otherwise.
		MeetsRequirementRace = function(self, questId, soughtRace)
			return self:MeetsRequirement(questId, 'R', soughtRace) and self:MeetsRequirement(questId, 'S', soughtRace)
		end,

		---
		--	Returns whether the character meets reputation requirements for the specified quest.
		--	@param questId The standard numeric questId representing a quest.
		--	@return True if the character meets the reputation requirements for the specified quest or false otherwise.
		--	@return A table of failures if any, nil otherwise.
		MeetsRequirementReputation = function(self, questId)
			local first, failures = self:MeetsRequirement(questId, 'V')
			local second, failures2 = self:MeetsRequirement(questId, 'W')
			local retval = first and second
			if nil == failures then
				failures = failures2
			else
				if nil ~= failures2 then
					for i = 1, #(failures2), 1 do
						tinsert(failures, failures2[i])
					end
				end
			end
			return retval, failures
		end,

		---
		--	Returns a table of NPC information, and whether the NPC must be killed.  Each element in the table is a table with the x-coordinate,
		--	y-coordinate, NPC name, and NPC ID in it.
		--	@param questId The standard numeric questId representing a quest.
		--	@param acceptOrTurnin Either "A" or "T" depending on which type of NPC is sought, one that gives the quest, or one to whom the quest is turned in.
		--	@param mapId The desired map area where the NPC should appear.
		--	@return A table of NPC information
		--	@return	Whether the NPC must be killed.
		MultipleUniqueMapLocationQuest = function(self, questId, acceptOrTurnin, mapId, dungeonLevel, allowAnyMap)
			local retval = {}
			local onlyInSpecifiedMap = true
			if allowAnyMap then onlyInSpecifiedMap = false end
			local locations, mustKill = self:LocationQuestInMapRaw(questId, acceptOrTurnin, false, true, onlyInSpecifiedMap, mapId, true)
			if nil ~= locations then
				for i = 1, #locations do	-- each location should be { { mapId, coords, dungeonLevel [,realArea] }, NPCname, NPCID }
					if locations[i][2] ~= self:NPCName(0) and nil == strfind(locations[i][2], 'Near') and 2 <= #(locations[i][1]) then
						if nil == locations[i][1][2] then
							print("Grail error: Not expecting nil for coords from Quest", questId, acceptOrTurnin, i)
						else
							local coords = { strsplit(',', locations[i][1][2]) }
							if 2 >= #coords and (nil == dungeonLevel or dungeonLevel == locations[i][1][3]) then
								tinsert(retval, { tonumber(coords[1]), tonumber(coords[2]), locations[i][2], locations[i][3], locations[i][1][1], locations[i][1][3], locations[i][1][4] } )
							end
						end
					end
				end
			end
			if 0 == #retval then retval = nil end
			return retval, mustKill
		end,	

		---
		--	Returns the localized name of the NPC represented by the specified NPC ID.
		--	@param npcId The standard numeric npcId representing an NPC.
		--	@return The localized string for the specific NPC, or nil if the NPC is not found in the database.
		NPCName = function(self, npcId)
			local retval = nil
			if nil ~= npcId and tonumber(npcId) then
				npcId = tonumber(npcId)
				if nil ~= self.npcs[npcId] then retval = self.npcs[npcId][0] end
			end
			return retval
		end,

		---
		--	Internal Use.
		--	This routine is used to post notifications to observers.
		--	@param eventName The name of the notification.
		--	@param questId The questId associated with the notification.
		PostNotification = function(self, eventName, questId)
			if nil ~= self.observers[eventName] then
				for i = 1, #(self.observers[eventName]), 1 do
					self.observers[eventName][i](eventName, questId)
				end
			end
		end,

		---
		--	Internal Use.
		--	Returns whether the character has the profession specified by the code exceeding the specified level.
		--	@param professionCode The code representing the profession as used in Grail.professionMapping
		--	@param professionValue The skill level to use in comparison.
		--	@return True when the character possesses the skill in excess of the indicated value, false otherwise.
		--	@return The actual skill level the character posseses or Grail.NO_SKILL if the character does not have the specified skill.
		--	@use hasSkill, skillLevel = Grail:ProfessionExceeds('+', 125)
		ProfessionExceeds = function(self, professionCode, professionValue)
			local retval = false
			local skillLevel = self.NO_SKILL
			local skillName = nil
			local prof1, prof2, archaeology, fishing, cooking, firstAid = GetProfessions();

			if "X" == professionCode and nil ~= archaeology then
				_, _, skillLevel = GetProfessionInfo(archaeology)
			elseif "F" == professionCode and nil ~= fishing then
				_, _, skillLevel = GetProfessionInfo(fishing)
			elseif "C" == professionCode and nil ~= cooking then
				_, _, skillLevel = GetProfessionInfo(cooking)
			elseif "+" == professionCode and nil ~= firstAid then
				_, _, skillLevel = GetProfessionInfo(firstAid)
			elseif "R" == professionCode then
				skillLevel = self:RidingSkillLevel()
			else
				professionName = self.professionMapping[professionCode]
				if nil ~= prof1 then
					skillName, _, skillLevel = GetProfessionInfo(prof1)
				end
				if skillName ~= professionName then
					if nil ~= prof2 then
						skillName, _, skillLevel = GetProfessionInfo(prof2)
					end
					if skillName ~= professionName then
						skillLevel = self.NO_SKILL
					end
				end
			end
			if skillLevel >= professionValue then
				retval = true
			end
			return retval, skillLevel
		end,

		---
		--	Internal Use.
		--	Routine used for OnUpdate when abandoning a quest.  This is needed because the events that Blizzard issues
		--	are not adequate for our desired tasks.  One of three needed for abandoning.
		QuestAbandonPost = function(self, elapsed)
			self.timeSinceLastUpdate = self.timeSinceLastUpdate + elapsed
			if self.timeSinceLastUpdate > self.abandonPostNotificationDelay then
				self:PostNotification("Abandon", self.abandonedQuestId)
				self.tooltip:SetScript("OnUpdate", nil)
				self.timeSinceLastUpdate = 0
			end
		end,

		---
		--	Internal Use.
		--	Routine used to hook the function for abandoning a quest.  This is needed because the events that Blizzard issues
		--	are not adequate for our desired tasks.  One of three needed for abandoning.
		QuestAbandonStart = function(self)
			self.abandoningQuestIndex = GetQuestLogSelection()
			self.origAbandonQuestFunction()
		end,

		---
		--	Internal Use.
		--	Routine used to hook the function for abandoning a quest.  This is needed because the events that Blizzard issues
		--	are not adequate for our desired tasks.  One of three needed for abandoning.
		QuestAbandonStop = function(self)
			local questTitle, level, questTag, suggestedGroup, isHeader, isCollapsed, isComplete, isDaily, questId = GetQuestLogTitle(self.abandoningQuestIndex)
			self.abandonedQuestId = questId
			self.origConfirmAbandonQuestFunction()
			self.tooltip:SetScript("OnUpdate", function(myself, elapsed) self:QuestAbandonPost(elapsed) end)
			-- the following is done because the tooltip is hidden and will not call the OnUpdate script otherwise
			self.tooltip:ClearLines()
			self.tooltip:SetHyperlink(format("quest:%d", self.abandonedQuestId))
		end,

		---
		--	Returns the questId of the quest in the quest log with the sought title.
		--	@param soughtTitle The localized name of the quest sought in the quest log.
		--	@return The questId of the quest in the quest log matching the sought name or nil if none match.
		QuestInQuestLogMatchingTitle = function(self, soughtTitle)
			local retval = nil
			local i = 1
			local cleanedTitle = strtrim(soughtTitle)
			while GetQuestLogTitle(i) and nil == retval do
				local questTitle, level, questTag, suggestedGroup, isHeader, isCollapsed, isComplete, isDaily, questId = GetQuestLogTitle(i)
				if not isHeader and questTitle == cleanedTitle then
					retval = questId
				end
				i = i + 1
			end
			return retval
		end,

		---
		--	Returns the level of the quest with the specified questId.
		--	@param questId The standard numeric questId representing a quest.
		--	@return The level of the quest matching the questId or nil if none found.
		QuestLevel = function(self, questId)
			local retval = nil
			if nil ~= questId and tonumber(questId) then
				questId = tonumber(questId)
				local codes = self:CodesWithPrefixQuest(questId, "K")
				-- there should be only one K code
				if nil ~= codes then
					retval = tonumber(strsub(codes[1], 2, 4))
				end
			end
			return retval
		end,

		---
		--	Returns the name of the quest with the specified questId.
		--	@param questId The standard numeric questId representing a quest.
		--	@return The localized name of the quest matching the questId or nil if none found.
		QuestName = function(self, questId)
			local retval = nil
			if nil ~= questId and tonumber(questId) then
				questId = tonumber(questId)
				if nil ~= self.quests[questId] then
					retval = self.quests[questId][0]
				else
					retval = "NOT IN DATABASE"
				end
			end
			return retval
		end,

		---
		--	Internal Use.
		--	Routine used to hook the function for completing a quest.  This is needed because the events that Blizzard issues
		--	are not adequate for our desired tasks.
		QuestRewardCompleteButton_OnClick = function(self)
			local used = "npc"
			local targetName = UnitName(used)
			if nil == targetName then used = "questnpc" targetName = UnitName(used) end
			local npcId = nil
			if nil ~= targetName then
				local guid = UnitGUID(used)
				if nil ~= guid then
					local targetType = tonumber(guid:sub(5,5), 16) % 8
					npcId = tonumber(guid:sub(7,10), 16)
					if 1 == targetType then npcId = npcId + 1000000 end	-- world object
				end
			end
			self.questNPCId = npcId
			if nil == self.completingQuest then
				local questGivers = self.specialQuests[self.completingQuestTitle]
				if nil ~= questGivers then
					for i = 1, #questGivers do
						if tonumber(questGivers[i][1]) == self.questNPCId then
							self.completingQuest = questGivers[i][2]
						end
					end
				end
			end

			if self.completingQuest then
				self:MarkQuestComplete(self.completingQuest, true)
				self.completingQuest = nil
			end
			self.origHookFunction()
		end,

		---
		--	Returns a table of quest IDs for quests that take place during the specified holiday.
		--	@param eventName The localized name of the holiday.
		--	@param forceGarbageCollection If true, garbage collection is performed after searching the internal database.
		--	@return A table of questIds for quests taking place in the specified holiday or nil if there are none.
		--	@see QuestsDuringEventCode
		QuestsDuringEvent = function(self, eventName, forceGarbageCollection)
			local retval = nil
			local desiredEventCode = nil
			for holidayCode, holidayName in pairs(self.holidayMapping) do
				if eventName == holidayName then desiredEventCode = holidayCode end
			end
			if nil ~= desiredEventCode then retval = self:QuestsDuringEventCode(desiredEventCode, forceGarbageCollection) end
			return retval
		end,

		---
		--	Returns a table of quest IDs for quests that take place during the holiday specified by the code.
		--	@param eventNameCode The code representing the holiday taken from Grail.holidayMapping.  This is not a localized code.
		--	@param forceGarbageCollection If true, garbage collection is performed after searching the internal database.
		--	@return A table of questIds for quests taking place in the specified holiday or nil if there are none.
		--	@see QuestsDuringEvent
		QuestsDuringEventCode = function(self, eventNameCode, forceGarbageCollection)
			return self:QuestsWithCode("H"..eventNameCode, forceGarbageCollection)
		end,

		---
		--	Returns the table of questIds filtered by character's specifics.
		--	@param questTable The master table of questIds to be filtered.
		--	@param faction If true, the character's faction must match that of the quest.
		--	@param class If true, the character's class must match that of the quest.
		--	@param race If true, the character's race must match that of the quest.
		--	@param gender If true, the character's gender must match that of the quest.
		--	@param targetLevel If present, the quest must be acceptable for a character of this level.
		--	@return A table of filtered quests or nil if there are none.
		QuestsFiltered = function(self, questTable, faction, class, race, gender, targetLevel)
			local retval = { }
			local stillGood
			if nil ~= questTable then
				for _, questId in pairs(questTable) do
					stillGood = true
					if faction and not self:MeetsRequirementFaction(questId) then stillGood = false end
					if class and not self:MeetsRequirementClass(questId) then stillGood = false end
					if race and not self:MeetsRequirementRace(questId) then stillGood = false end
					if gender and not self:MeetsRequirementGender(questId) then stillGood = false end
					if targetLevel and not self:MeetsRequirementLevel(questId, targetLevel) then stillGood = false end
					if stillGood then tinsert(retval, questId) end
				end
			end
			if 0 == #(retval) then retval = nil end
			return retval
		end,

		---
		--	Returns a table of quest IDs for quests that can start in the specified map area.
		--	@param mapId The map area to use, or if nil, the map area the character is currently in will be used.
		--	@param forceGarbageCollection If true, garbage collection is performed after searching the internal database.
		--	@param forceSearch If true, a search is performed using starting NPCs instead of using the cached indexed, resulting in slower and more memory intensive use.
		--	@return A table of questIds for quests that start in the map area or nil if none.
		QuestsInMap = function(self, mapId, forceGarbageCollection, forceSearch, useDungeonAlso)
			local retval = {}
			local mapIdToUse = mapId or GetCurrentMapAreaID()

			if nil ~= mapIdToUse and not forceSearch then	-- take the easy way out and use the precomputed list of quests in the map area
				if useDungeonAlso then
					if nil == self.indexedQuestsExtra[mapIdToUse] then
						retval = self.indexedQuests[mapIdToUse]
					elseif nil == self.indexedQuests[mapIdToUse] then
						retval = self.indexedQuestsExtra[mapIdToUse]
					else
						for k,v in pairs(self.indexedQuests[mapIdToUse]) do
							tinsert(retval, v)
						end
						for k, v in pairs(self.indexedQuestsExtra[mapIdToUse]) do
							if not tContains(retval, v) then
								tinsert(retval, v)
							end
						end
					end
				else
					retval = self.indexedQuests[mapIdToUse]
				end
			else	-- do a manual search through all the quests to find where their A NPCs are, and whether they match the mapIdToUse
				local locations
				local shouldAdd
				for questId, _ in pairs(self.quests) do
					locations = self:LocationQuestInMap(questId, 'A')
					shouldAdd = false
					if nil ~= locations then
						for i = 1, #locations do
							if tonumber(locations[i][1]) == mapIdToUse then shouldAdd = true end
						end
					end
					if shouldAdd then tinsert(retval, questId) end
				end
			end

			if forceGarbageCollection then collectgarbage() end

			if nil ~= retval and 0 == #retval then retval = nil end
			return retval
		end,

		---
		--	Returns a table of quest IDs for the quests in the character's quest log.
		--	@return A table of questIds of quests in the character's quest log or an empty table if there are none.
		QuestsInQuestLog = function(self)
			local retval = { }
			local i = 1
			while GetQuestLogTitle(i) do
				local questTitle, level, questTag, suggestedGroup, isHeader, isCollapsed, isComplete, isDaily, questId = GetQuestLogTitle(i)
				if not isHeader then
					tinsert(retval, questId)
				end
				i = i + 1
			end
			return retval
		end,

		---
		--	Returns a table of quest IDs for quests that can start in the specified zone.
		--	@param zoneText The localized zone name, or if nil is used the zone the character is currently in.
		--	@param forceGarbageCollection If true, garbage collection is performed after searching the internal database.
		--	@param forceSearch If true, a search is performed using starting NPCs instead of using the cached indexed, resulting in slower and more memory intensive use.
		--	@return A table of questIds for quests that start in the zone or nil if none.
		QuestsInZone = function(self, zoneText, forceGarbageCollection, forceSearch)
			local mapIdToUse = nil
			if nil ~= zoneText then
				mapIdToUse = self.zoneNameMapping[zoneText]
				if nil == mapIdToUse and self.debug then print("Grail:QuestsInZone could not find zone name ", zoneText, " so is using current zone.") end
			end
			return self:QuestsInMap(mapIdToUse, forceGarbageCollection, forceSearch)
		end,

		---
		--	Internal Use.
		--	Returns a table of quest IDs for those with the sought code.
		--	@param soughtCode A code sought from the internal database.  If nil this will raise.
		--	@param forceGarbageCollection If true, garbage collection is performed after searching the internal database.
		--	@return A table of questIds for matching quests or nil if none found.
		QuestsWithCode = function(self, soughtCode, forceGarbageCollection)
			assert((nil ~= soughtCode), "Grail Error: sought code cannot be nil")
			local retval = { }
			for questId, _ in pairs(self.quests) do
				if self:HasCode(questId, soughtCode) then tinsert(retval, questId) end
			end
			if forceGarbageCollection then collectgarbage() end
			if 0 == #(retval) then retval = nil end
			return retval
		end,

		---
		--	Returns the questId for the quest with the specified name.
		--	@param soughtName The localized name of the quest.  If nil this will raise.
		--	@return The questId of the quest or nil if no quest with that name found.
		QuestWithName = function(self, soughtName)
			assert((nil ~= soughtName), "Grail Error: sought name cannot be nil")
			local retval = nil
			for questId, _ in pairs(self.quests) do
				if self:QuestName(questId) == soughtName then
					retval = questId
				end
			end
			return retval
		end,

		---
		--	Internal Use.
		--	Adds the callback to the observer queue for eventName.  Should not be called directly, but through the use of convenience API.
		--	@see RegisterObserverQuestAbandon
		--	@see RegisterObserverQuestAccept
		--	@see RegisterObserverQuestComplete
		--	@param eventName The name of the event to which the callback should be added.
		--	@param callback The callback that is to be added.
		RegisterObserver = function(self, eventName, callback)
			assert((nil ~= callback), "Grail Error: cannot register a nil callback")
			if nil == self.observers[eventName] then self.observers[eventName] = { } end
			tinsert(self.observers[eventName], callback)
		end,

		---
		--	Add the callback to receive quest Abandon notifications.
		--	When the notification is posted the callback will be called with two parameters, "Abandon" and the questId.
		--	@param callback The callback that is to be added.
		RegisterObserverQuestAbandon = function(self, callback)
			self:RegisterObserver("Abandon", callback)
		end,

		---
		--	Add the callback to receive quest Accept notifications.
		--	When the notification is posted the callback will be called with two parameters, "Accept" and the questId.
		--	@param callback The callback that is to be added.
		RegisterObserverQuestAccept = function(self, callback)
			self:RegisterObserver("Accept", callback)
		end,

		---
		--	Add the callback to receive quest Completion notifications.
		--	When the notification is posted the callback will be called with two parameters, "Complete" and the questId.
		--	@param callback The callback that is to be added.
		RegisterObserverQuestComplete = function(self, callback)
			self:RegisterObserver("Complete", callback)
		end,

		---
		--	Internal Use.
		--	Returns whether the character has a reputation that exceeds the value specified for the reputation specified.
		--	@param reputationName The localized name of the sought reputation.
		--	@param reputationValue The reputation value that needs to be exceeded.  Note that internally all reputation values are the earned reputation value + 42000.
		--	@return True if the character has more reputation than was sought, or false otherwise.
		--	@return The reputation value the character actually has (earned value + 42000).
		--	@usage doesExceed, reputationValue = Grail:ReputationExceeds("Lower City", 41999)
		ReputationExceeds = function(self, reputationName, reputationValue)
			local retval = false
			local actualEarnedValue = nil
			local factionIndex = 1
			reputationValue = tonumber(reputationValue)
			while GetFactionInfo(factionIndex) and false == retval do
				local name, description, standingId, bottomValue, topValue, earnedValue, atWarWith, canToggleAtWar, isHeader, isCollapsed, hasRep, isWatched, isChild = GetFactionInfo(factionIndex)
				if not isHeader then
					if name == reputationName then
						earnedValue = earnedValue + 42000	-- the reputationValue is stored with 42000 added to it so we do not have to deal with negative numbers, so we normalize here
						retval = (earnedValue > reputationValue)
						actualEarnedValue = earnedValue
					end
				end
				factionIndex = factionIndex + 1
			end
			return retval, actualEarnedValue
		end,

		---
		--	Returns the riding skill level of the character.
		--	@return The riding skill level of the character or Grail.NO_SKILL if no skill exists.
		RidingSkillLevel = function(self)
			-- Need to search the spell book for the Riding skill
			local retval = self.NO_SKILL
			local spellIdMapping = { [33388] = 75, [33391] = 150, [34090] = 225, [34091] = 300, [90265] = 375 }
			local _, _, _, numberSpells = GetSpellTabInfo(1)
			for i = 1, numberSpells, 1 do
				local name = GetSpellBookItemName(i, BOOKTYPE_SPELL)
				local link = GetSpellLink(name)
				if link then
					local spellId = tonumber(link:match("^|c%x+|H(.+)|h%[.+%]"):match("(%d+)"))
					if spellId then
						local newLevel = spellIdMapping[spellId]
						if newLevel and newLevel > retval then
							retval = newLevel
						end
					end
				end
			end
			return retval
		end,

		---
		--	Returns a single x-coordinate, y-coordinate, NPC name, NPC ID, and whether the NPC must be killed for the specified information.
		--	@param questId The standard numeric questId representing a quest.
		--	@param acceptOrTurnin Either "A" or "T" depending on which type of NPC is sought, one that gives the quest, or one to whom the quest is turned in.
		--	@param mapId The desired map area where the NPC should appear.
		--	@return The x-coordinate of the NPC.
		--	@return The y-coordinate of the NPC.
		--	@return The localized name of the NPC.
		--	@return The ID of the NPC.
		--	@return	Whether the NPC must be killed.
		SingleMapLocationQuest = function(self, questId, acceptOrTurnin, mapId)
			local locations, npcName, npcId, mustKill = self:LocationQuestInMap(questId, 'A', false, true, true, mapId)
			if nil ~= locations and 0 < #(locations) and 2 == #(locations[1]) and locations[1][2] ~= self:NPCName(0) and nil == strfind(locations[1][2], 'Near') then
				local coords = { strsplit(',', locations[1][2]) }
				return tonumber(coords[1]), tonumber(coords[2]), npcName, npcId, mustKill
			else
				return nil
			end
		end,

		---
		--	Internal Use.
		--	The routine called when the /grail slash command is used.  For the most part the currently implemented commands are for testing only.
		--	@param frame The tooltip frame.
		--	@param msg The rest of the command line used with the /grail slash command.
		SlashCommand = function(self, frame, msg)
			msg = strlower(msg)
			if "debug" == msg then
				self.debug = not self.debug
				local statusMessage = self.debug and "ON" or "OFF"
				DEFAULT_CHAT_FRAME:AddMessage("Grail Debug now " .. statusMessage)
			elseif "compare" == msg then
				local db = GrailDatabase[self.playerRealm][self.playerName]
				for index, value in pairs(db["completedQuests"]) do
					if value ~= nil then
						if db["completedQuests"][index] ~= db["backupCompletedQuests"][index] then
							local backup = db["backupCompletedQuests"][index] or 0
							local current = db["completedQuests"][index] or 0
							if current > backup then
								local diff = current - backup
								-- index 0 covers 1 - 32
								-- index 1 covers 33 - 64
								-- index 2 covers 65 - 96
								local base = index * 32

								for i = 0, 31 do
									if bit.band(diff, 2^i) > 0 then
										local message = strformat("New quest completed %d", base + i + 1)
										print(message)
										self:AddTrackingMessage(message)
									end
								end
							else
								print("Index", index, "has a backup bigger than the current, which is not expected")
								local diff = backup - current
								-- index 0 covers 1 - 32
								-- index 1 covers 33 - 64
								-- index 2 covers 65 - 96
								local base = index * 32

								for i = 0, 31 do
									if bit.band(diff, 2^i) > 0 then
										local message = strformat("New quest LOST %d", base + i + 1)
										print(message)
										self:AddTrackingMessage(message)
									end
								end
							end
						end
					end
				end
			elseif "backup" == msg then
				GrailDatabase[self.playerRealm][self.playerName]["backupCompletedQuests"] = {}
				for i, v in pairs(GrailDatabase[self.playerRealm][self.playerName]["completedQuests"]) do
					GrailDatabase[self.playerRealm][self.playerName]["backupCompletedQuests"][i] = v
				end
				print("A backup of the completed quests has been made")
			elseif "tracking" == msg then
				self.tracking = not self.tracking
				print(format("Grail Tracking now %s", self.tracking and "ON" or "OFF"))
				if self.tracking then
					self:RegisterObserverQuestAbandon(Grail.AddTrackingCallback)
					self:RegisterObserverQuestAccept(Grail.AddTrackingCallback)
					self:RegisterObserverQuestComplete(Grail.AddTrackingCallback)
				else
					self:UnregisterObserverQuestAbandon(Grail.AddTrackingCallback)
					self:UnregisterObserverQuestAccept(Grail.AddTrackingCallback)
					self:UnregisterObserverQuestComplete(Grail.AddTrackingCallback)
				end
			elseif "c " == strsub(msg, 1, 2) then
				self:AddTrackingMessage(strsub(msg, 3))
			elseif "comment" == strsub(msg, 1, 7) then
				self:AddTrackingMessage(strsub(msg, 9))
			elseif "help" == msg then
				DEFAULT_CHAT_FRAME:AddMessage("Grail commands:")
				DEFAULT_CHAT_FRAME:AddMessage("/grail debug  => toggles debug on and off, printing new value")
				DEFAULT_CHAT_FRAME:AddMessage("/grail tracking  => toggles tracking on and off, printing new value")
				DEFAULT_CHAT_FRAME:AddMessage("/grail c msg => adds the msg to the tracking data")
				DEFAULT_CHAT_FRAME:AddMessage("/grail comment msg => adds the msg to the tracking data")
				DEFAULT_CHAT_FRAME:AddMessage("/grail target  => gets target information (NPC ID and your current location)")
				DEFAULT_CHAT_FRAME:AddMessage("/grail npc ###  => dumps NPC information for NPC with ID ###")
				DEFAULT_CHAT_FRAME:AddMessage("/grail holiday code  => dumps quest information for quests with the code (see Grail.holidayMapping)")
				DEFAULT_CHAT_FRAME:AddMessage("/grail quest ###  => outputs the tooltip for the quest with ID ### (use again if not populated the first time)")
				DEFAULT_CHAT_FRAME:AddMessage("/grail completed ###  => indicates whether quest ID ### is already completed")
				DEFAULT_CHAT_FRAME:AddMessage("/grail riding  => prints the riding skill of the character")
				DEFAULT_CHAT_FRAME:AddMessage("/grail zone  => outputs a list of all the (unfiltered) quests in the current zone")
				DEFAULT_CHAT_FRAME:AddMessage("/grail zonesearch  => outputs a list of all the (unfiltered) quests in the current zone searching the database")
				DEFAULT_CHAT_FRAME:AddMessage("/grail abandon  => sets up a notification to observe abandoned quests")
				DEFAULT_CHAT_FRAME:AddMessage("/grail  => initiates a database query to get completed quests [this happens at startup normally]")
			elseif "target" == msg then
				local targetName, npcId, coordinates = self:TargetInformation()
				if targetName == nil then targetName = 'nil target' end
				if npcId == nil then npcId = -1 end
				if coordinates == nil then coordinates = 'no coords' end
				DEFAULT_CHAT_FRAME:AddMessage(targetName.."("..npcId..") "..coordinates)
			elseif "npc " == strsub(msg, 1, 4) then
				debugprofilestart()
				self:DebugDumpOneNPCInfo(strsub(msg, 5))
				DEFAULT_CHAT_FRAME:AddMessage("Elapsed milliseconds: "..debugprofilestop())
			elseif "holiday" == strsub(msg, 1, 7) then
				debugprofilestart()
				self:DebugDumpQuestInfo(self:QuestsDuringEventCode(strupper(strsub(msg, 9))))
				DEFAULT_CHAT_FRAME:AddMessage("Elapsed milliseconds: "..debugprofilestop())
			elseif "quest" == strsub(msg, 1, 5) then
				local questToUse = strsub(msg, 7)
				if not frame:IsOwned(UIParent) then frame:SetOwner(UIParent, "ANCHOR_NONE") end
				frame:ClearLines()
				frame:SetHyperlink(format("quest:%d", questToUse))
				DEFAULT_CHAT_FRAME:AddMessage("Quest "..questToUse)
				local numLines = frame:NumLines()
				if nil == numLines then numLines = 0 end
				DEFAULT_CHAT_FRAME:AddMessage("Tooltip has "..numLines.." lines")
				for i = 1, numLines, 1 do
					local text = _G["com_mithrandir_grailTooltipTextLeft"..i]
					if text then
						DEFAULT_CHAT_FRAME:AddMessage(text:GetText())
					end
				end
			elseif "npctip" == strsub(msg, 1, 6) then
				local npcToUse = tonumber(strsub(msg, 8))
				if not frame:IsOwned(UIParent) then frame:SetOwner(UIParent, "ANCHOR_NONE") end
				frame:ClearLines()
				if npcToUse > 1000000 and npcToUse < 100000000 then
					frame:SetHyperlink(format("unit:0xF510%04X00000000", npcToUse - 1000000))
				else
					frame:SetHyperlink(format("unit:0xF530%04X00000000", npcToUse))
				end
				DEFAULT_CHAT_FRAME:AddMessage("NPC "..npcToUse)
				local numLines = frame:NumLines()
				if nil == numLines then numLines = 0 end
				DEFAULT_CHAT_FRAME:AddMessage("Tooltip has "..numLines.." lines")
				for i = 1, numLines, 1 do
					local text = _G["com_mithrandir_grailTooltipTextLeft"..i]
					if text then
						DEFAULT_CHAT_FRAME:AddMessage(text:GetText())
					end
				end
			elseif "completed" == strsub(msg, 1, 9) then
				local questToUse = strsub(msg, 11)
				local normal = self:IsQuestCompleted(questToUse) and "YES" or "NO"
				local resettable = self:IsResettableQuestCompleted(questToUse) and "YES" or "NO"
				DEFAULT_CHAT_FRAME:AddMessage("Quest " .. questToUse .. " completed: " .. normal .. " and resettable completed: " .. resettable)
			elseif "riding" == msg then
				DEFAULT_CHAT_FRAME:AddMessage("Riding skill is "..self:RidingSkillLevel())
			elseif "zone" == msg then
				DEFAULT_CHAT_FRAME:AddMessage("All quests in current zone")
				debugprofilestart()
				local questsInZone = self:QuestsInZone() or {}
				for i = 1, #(questsInZone), 1 do
					DEFAULT_CHAT_FRAME:AddMessage(questsInZone[i].." "..self.quests[questsInZone[i]][0])
				end
				DEFAULT_CHAT_FRAME:AddMessage("Elapsed milliseconds: "..debugprofilestop())
			elseif "zonesearch" == msg then
				DEFAULT_CHAT_FRAME:AddMessage("All quests in current zone")
				debugprofilestart()
				local questsInZone = self:QuestsInZone(GetRealZoneText(), false, true)
				for i = 1, #(questsInZone), 1 do
					DEFAULT_CHAT_FRAME:AddMessage(questsInZone[i].." "..self.quests[questsInZone[i]][0])
				end
				DEFAULT_CHAT_FRAME:AddMessage("Elapsed milliseconds: "..debugprofilestop())
			elseif "abandon" == msg then
				self:RegisterObserverQuestAbandon(Grail.DebugCallback)
			elseif "verifyquests" == strsub(msg, 1, 12) then
				if self.currentlyVerifying then print("Already verifying...please wait until completed") return end
				self.currentlyVerifying = true
				self.currentQuestIndex = 2
				self.timeSinceLastUpdate = 0
				self.tooltip:SetScript("OnUpdate", function(myself, elapsed) self:VerifyQuestList(elapsed) end)
				print("Starting total quest name verification")
			elseif "verifyquest" == strsub(msg, 1, 11) then
				local idToUse = tonumber(strsub(msg, 13))
				if not frame:IsOwned(UIParent) then frame:SetOwner(UIParent, "ANCHOR_NONE") end
				frame:ClearLines()
				frame:SetHyperlink(format("quest:%d", idToUse))
				local numLines = frame:NumLines()
				if nil == numLines or 0 == numLines then
					print("Query returned nothing for", idToUse)
				else
					local text = _G["com_mithrandir_grailTooltipTextLeft1"]
					if text then
						text = text:GetText()
						if text ~= self:QuestName(idToUse) then
							self:LogNameIssue('QUEST_ISSUES', idToUse, text)
							print("Added entry for", idToUse)
						else
							print("Title seems fine", idToUse)
						end
					else
						print("Query returned, but no text for", idToUse)
					end
				end
			elseif "verifynpcs" == strsub(msg, 1, 10) then
				if self.currentlyVerifying then print("Already verifying...please wait until completed") return end
				self.currentlyVerifying = true
				self.currentQuestIndex = nil	-- reusing this variable
				self.timeSinceLastUpdate = 0
				self.emergencyOutlet = 0
				self.tooltip:SetScript("OnUpdate", function(myself, elapsed) self:VerifyNPCList(elapsed) end)
				print("Starting total NPC verification")
			elseif "verifynpc" == strsub(msg, 1, 9) then
				local idToUse = tonumber(strsub(msg, 11))
				if not frame:IsOwned(UIParent) then frame:SetOwner(UIParent, "ANCHOR_NONE") end
				frame:ClearLines()
				if idToUse > 1000000 and idToUse < 100000000 then
					frame:SetHyperlink(format("unit:0xF51%05X00000000", idToUse - 1000000))
				else
					frame:SetHyperlink(format("unit:0xF53%05X00000000", idToUse))
				end
				local numLines = frame:NumLines()
				if nil == numLines or 0 == numLines then
					print("Query returned nothing for", idToUse)
				else
					local text = _G["com_mithrandir_grailTooltipTextLeft1"]
					if text then
						text = text:GetText()
						if text ~= self:NPCName(idToUse) then
							self:LogNameIssue('NPC_ISSUES', idToUse, text)
							print("Added entry for", idToUse)
						else
							print("Title seems fine", idToUse)
						end
					else
						print("Query returned, but no text for", idToUse)
					end
				end
			elseif "verifyitems" == strsub(msg, 1, 11) then
				if self.currentlyVerifying then print("Already verifying...please wait until completed") return end
				if self.playerLocale == "enUS" or self.playerLocale == "enGB" then
					self.retrievingString = "Retrieving item information"
				else
					self.retrievingString = "Unknown"
				end
				self.currentlyVerifying = true
				self.currentQuestIndex = nil	-- reusing this variable
				self.timeSinceLastUpdate = 0
				self.emergencyOutlet = 0
				self.tooltip:SetScript("OnUpdate", function(myself, elapsed) self:VerifyItemList(elapsed) end)
				print("Starting total NPC item verification")
			elseif "verifyitem" == strsub(msg, 1, 10) then
				local idToUse = tonumber(strsub(msg, 12))
				if not frame:IsOwned(UIParent) then frame:SetOwner(UIParent, "ANCHOR_NONE") end
				frame:ClearLines()
				frame:SetHyperlink(format("item:%d:0:0:0:0:0:0:0", idToUse))
				local numLines = frame:NumLines()
				if nil == numLines or 0 == numLines then
					print("Query returned nothing for", idToUse)
				else
					local text = _G["com_mithrandir_grailTooltipTextLeft1"]
					if text then
						text = text:GetText()
						if text ~= self:NPCName(idToUse + 100000000) then
							self:LogNameIssue('NPC_ISSUES', 100000000 + idToUse, text)
							print("Added entry for", idToUse)
						else
							print("Title seems fine", idToUse)
						end
					else
						print("Query returned, but no text for", idToUse)
					end
				end
			else
				DEFAULT_CHAT_FRAME:AddMessage("Grail initiating server database query")
				QueryQuestsCompleted()
			end
		end,

		---
		--	Returns whether the specified quest can be accepted.
		--	If the quest cannot be accepted, specific information is returned indicating the problem.  Specific types of checks can be ignored with the proper parameters.
		--	@param questId The standard numeric questId representing a quest.
		--	@param ignoreLevel If true, the level requirement for the quest is ignored.
		--	@param ignoreProfession If true, the profession requirements for the quest are ignored.
		--	@param ignoreReputation If true, the reputation requirements for the quest are ignored.
		--	@param ignoreHoliday If true, the holiday requirements for the quest are ignored.
		--	@param ignorePresenceInLog If true, the presence of the quest in the log is ignored.
		--	@param ignorePrerequisites If true, the prerequisite requirements of the quest are ignored.
		--	@return True to indicate that the quest can be accepted, or false if it cannot.
		--	@return The reason why the quest cannot be accepted from the values Completed, InLog, Nonexistent, Level, Class, Race, Gender, Faction, Prerequisites, Invalidated, Profession, Reputation, Holiday.
		--	@return A table of specific information about failures or nil if no failures.
		--	@usage canAccept, failureReason, failureTable = Grail:Status(2)
		Status = function(self, questId, ignoreLevel, ignoreProfession, ignoreReputation, ignoreHoliday, ignorePresenceInLog, ignorePrerequisites)
			local retval = false
			local reason = "Error"
			local success
			local failures = { }
			if questId ~= nil and tonumber(questId) then
				questId = tonumber(questId)
				if self:IsQuestCompleted(questId) and not self:IsRepeatable(questId) then
					reason = "Completed"
				elseif not ignorePresenceInLog and self:IsQuestInQuestLog(questId) then
					reason = "InLog"
				elseif not self:DoesQuestExist(questId) then
					reason = "Nonexistent"
				elseif not ignoreLevel and not self:MeetsRequirementLevel(questId) then
					reason = "Level"
				elseif not self:MeetsRequirementClass(questId) then
					reason = "Class"
				elseif not self:MeetsRequirementRace(questId) then
					reason = "Race"
				elseif not self:MeetsRequirementGender(questId) then
					reason = "Gender"
				elseif not self:MeetsRequirementFaction(questId) then
					reason = "Faction"
				else
					success, failures = self:MeetsPrerequisites(questId)
					if not success and self.useAncestor then
						local isPossible, subReason, subFailures, failureCode = self:AncestorStatus(questId, ignoreLevel, ignoreProfession, ignoreReputation, ignoreHoliday, ignorePresenceInLog, ignorePrerequisites)
						if not isPossible then
							reason = subReason
							failures = subFailures
						end
					end
					if "Error" == reason and not ignorePrerequisites and not success then
						reason = "Prerequisites"
					end
					if "Error" == reason then
						success, failures = self:IsInvalidated(questId)
						if success then		-- note that the word success here is a misnomer.  if IsInvalidated returns true we have failed
							reason = "Invalidated"
						else
							success, failures = self:MeetsRequirementProfession(questId)
							if not ignoreProfession and not success then
								reason = "Profession"
							else
								success, failures = self:MeetsRequirementReputation(questId)
								if not ignoreReputation and not success then
									reason = "Reputation"
								else
									success, failures = self:MeetsRequirementHoliday(questId)
									if not ignoreHoliday and not success then
										reason = "Holiday"
									else
										retval = true
										reason = ""
									end
								end
							end
						end
					end
				end
			end
			if nil ~= failures and 0 == #(failures) then failures = nil end
			return retval, reason, failures
		end,

		---
		--	Returns information about the currently selected target.
		--	@return The localized name of the target or nil if no target.
		--	@return The npcId of the target unless the target is a world object in which one million is added to its value.
		--	@return The coordinates of the player (since the target coordinates cannot be determined) in the format mapId*:xx.xx,yy.yy, where * is nothing or the dungeon level in []
		--	@usage targetName, npcId, coordinates = Grail:TargetInformation()
		TargetInformation = function(self)
			local used = "target"
			local targetName = UnitName(used)
			if nil == targetName then used = "npc" targetName = UnitName(used) end
			if nil == targetName then used = "questnpc" targetName = UnitName(used) end
			local npcId = nil
			local coordinates = nil
			if nil ~= targetName then
				local guid = UnitGUID(used)
				if nil ~= guid then
					local targetType = tonumber(guid:sub(5,5), 16) % 8
					npcId = tonumber(guid:sub(7,10), 16)
					if 1 == targetType then npcId = npcId + 1000000 end	-- world object
					local x, y = GetPlayerMapPosition("player")	-- cannot get target x,y since Blizzard disabled that and returns 0,0 all the time for it
					local dungeonLevel = GetCurrentMapDungeonLevel()
					local dungeonIndicator = (dungeonLevel > 0) and "["..dungeonLevel.."]" or ""
					coordinates = strformat("%d%s:%.2f,%.2f", GetCurrentMapAreaID(), dungeonIndicator, x*100, y*100)
				end
			end
			return targetName, npcId, coordinates
		end,

		---
		--	Internal Use.
		--	The routine called for event processing associated with the hidden tooltip.
		--	@param frame The tooltip frame.
		--	@param event The name of the event.
		--	@param ... Various parameters depending on the event.
		Tooltip_OnEvent = function(self, frame, event, ...)
			if self.eventDispatch[event] then
				self.eventDispatch[event](self, frame, ...)
			end
		end,

		---
		--	Internal Use.
		--	The routine called by the hidden tooltip used to receive events when it is loaded into the system.
		--	It is used to set up information about the player, register the /grail slash command, hook the routines
		--	for handling accepting and abandoning quests, updating database values, and registering for the
		--	VARIABLES_LOADED event.
		--	@param frame The tooltip frame.
		Tooltip_OnLoad = function(self, frame)
			self.playerRealm = GetRealmName()
			self.playerName = UnitName('player')
			_, self.playerClass = UnitClass('player')
			_, self.playerRace = UnitRace('player')
			self.playerFaction = UnitFactionGroup('player')
			self.playerGender = UnitSex('player')
			self.playerLocale = GetLocale()
			SlashCmdList["GRAIL"] = function(msg)
				self:SlashCommand(frame, msg)
			end
			SLASH_GRAIL1 = "/grail"

			if not frame:IsOwned(UIParent) then
				frame:SetOwner(UIParent, "ANCHOR_NONE")
			end

			-- Now to hook the QuestRewardCompleteButton_OnClick function
			self.origHookFunction = QuestFrameCompleteQuestButton:GetScript("OnClick")
			QuestFrameCompleteQuestButton:SetScript("OnClick", function() self:QuestRewardCompleteButton_OnClick() end);

			self.origAbandonQuestFunction = SetAbandonQuest
			SetAbandonQuest = function() self:QuestAbandonStart() end

			self.origConfirmAbandonQuestFunction = AbandonQuest
			AbandonQuest = function() self:QuestAbandonStop() end

			-- Populate our special list of quests that are gained and completed without ever entering the quest log
			for questId, _ in pairs(self.quests) do
				if self:HasCode(questId, "SP") or self:IsRepeatable(questId) then
					local questName = self:QuestName(questId)
					if nil == self.specialQuests[questName] then self.specialQuests[questName] = {} end
					-- Now we go through and get the NPCs that give this quest and add them to the name table matching this quest
					local codeToUse = 'A'
					local factionCode = ('Horde' == self.playerFaction) and 'H' or 'A'
					local factionSpecificValue = codeToUse..factionCode
					local npcCodes = self:CodesWithPrefixQuest(questId, factionSpecificValue..':') or self:CodesWithPrefixQuest(questId, codeToUse..':') or self:CodesWithPrefixQuest(questId, codeToUse..'K:')
					if nil ~= npcCodes then
						for i = 1, #npcCodes do
							local _, npcs = strsplit(":", npcCodes[i])
							local npcTable = { strsplit(",", npcs) }
							for j = 1, #npcTable do
								tinsert(self.specialQuests[questName], { npcTable[j], questId })
							end
						end
					end
				end
			end

			-- Load Continent and Zone information
			local continentNames = { GetMapContinents() }
			local L
			for i = 1, #(continentNames), 1 do
				L = {}
				L["name"] = continentNames[i]
				L["zones"] = {}
				local zoneNames = { GetMapZones(i) }
				for j = 1, #(zoneNames), 1 do
					L["zones"][j] = {}
					L["zones"][j]["name"] = zoneNames[j]
					SetMapZoom(i, j)
					L["zones"][j]["mapID"] = GetCurrentMapAreaID()
					self.zoneNameMapping[zoneNames[j]] = L["zones"][j]["mapID"]
				end
				self.continents[i] = L
			end

			-- Now we need to make a reverse mapping table that maps map area IDs into localized zone names.
			for zoneName, mapId in pairs(self.zoneNameMapping) do
				if nil == self.mapAreaMapping[mapId] then self.mapAreaMapping[mapId] = zoneName end
			end

			-- Create the indexed quest list up front so future requests are much faster
			self:CreateIndexedQuestList()

			-- Find out where the Darkmoon Faire is
			self:CelebratingHoliday('Can Be Ignored')

			self.tooltip = frame
			frame:SetScript("OnEvent", function(frame, event, ...) self:Tooltip_OnEvent(frame, event, ...) end)
			frame:RegisterEvent("VARIABLES_LOADED")
		end,

		---
		--	Internal Use.
		--	Removes the callback from the observer queue for eventName.  Should not be called directly, but through the use of convenience API.
		--	@see UnregisterObserverQuestAbandon
		--	@see UnregisterObserverQuestAccept
		--	@see UnregisterObserverQuestComplete
		--	@param eventName The name of the event from which the callback should be removed.
		--	@param callback The callback that is to be removed.
		UnregisterObserver = function(self, eventName, callback)
			if nil ~= callback and nil ~= self.observers[eventName] then
				for i = 1, #(self.observers[eventName]), 1 do
					if callback == self.observers[eventName][i] then
						tremove(self.observers[eventName], i)
						break
					end
				end
			end
		end,

		---
		--	Remove the callback from receiving quest Abandon notifications.
		--	@param callback The callback that is to be removed.
		UnregisterObserverQuestAbandon = function(self, callback)
			self:UnregisterObserver("Abandon", callback)
		end,

		---
		--	Remove the callback from receiving quest Accept notifications.
		--	@param callback The callback that is to be removed.
		UnregisterObserverQuestAccept = function(self, callback)
			self:UnregisterObserver("Accept", callback)
		end,

		---
		--	Remove the callback from receiving quest Completion notifications.
		--	@param callback The callback that is to be removed.
		UnregisterObserverQuestComplete = function(self, callback)
			self:UnregisterObserver("Complete", callback)
		end,

		---
		--	Internal Use.
		--	Updates the NewQuests with data if the quest does not already exist in the internal database or adds the npcCode to the NewQuests data if it does not already exist.
		--	@param questId The standard numeric questId representing a quest.
		--	@param questTitle The localized name of the quest.
		--	@param npcId The standard numeric npcId representing an NPC.
		--	@param isDaily Indicates whether the quest is a daily quest.
		--	@param npcCode A string value 'A:' or 'T:' indicating whether the NPC is for accepting a quest or turning one in.
		--	@param version A version string based on the current internal database versions.
		UpdateQuestDatabase = function(self, questId, questTitle, npcId, isDaily, npcCode, version)
			if nil == questId or not tonumber(questId) then return end
			questId = tonumber(questId)
			if not self:DoesQuestExist(questId) or nil == self:CodesWithPrefixQuest(questId, npcCode) then
				if nil == GrailDatabase["NewQuests"] then GrailDatabase["NewQuests"] = { } end
				if nil == GrailDatabase["NewQuests"][questId] then
					GrailDatabase["NewQuests"][questId] = { }
					GrailDatabase["NewQuests"][questId][self.playerLocale] = questTitle
					local codes = nil
					if isDaily then codes = "+D" end
					if nil ~= npcId then
						if nil == codes then codes = npcCode..npcId else codes = codes.." "..npcCode..npcId end
					end
					if nil ~= codes then GrailDatabase["NewQuests"][questId][1] = codes end
					GrailDatabase["NewQuests"][questId][2] = version
				else
					local codes = GrailDatabase["NewQuests"][questId][1] or ''
					if nil == self:CodesWithPrefix(codes, npcCode) then
						if nil ~= npcId then
							if nil == codes then codes = npcCode..npcId else codes = codes.." "..npcCode..npcId end
						end
						if nil ~= codes then GrailDatabase["NewQuests"][questId][1] = codes end
					end
				end
			end
		end,

		---
		--	Internal Use.
		--	Updates the time until quests reset based on the GetQuestResetTime() API.  A side-effect is that if the reset time is past QueryQuestsCompleted() will be called.
		UpdateQuestResetTime = function(self)
			local seconds = GetQuestResetTime()
			if seconds > self.questResetTime then
				QueryQuestsCompleted()
			end
			self.questResetTime = seconds
		end,

		---
		--	Internal Use.
		--	Updates the NewNPCs with data if the NPC does not already exist in the internal database.
		--	@param targetName The localized name of the NPC.
		--	@param npcId The standard numeric npcId representing an NPC.
		--	@param coordinates The zone coordinates of the player.
		--	@param version A version string based on the current internal database versions.
		UpdateTargetDatabase = function(self, targetName, npcId, coordinates, version)
			if nil ~= npcId then
				if nil == self.npcs[npcId] then
					if nil == GrailDatabase["NewNPCs"] then	GrailDatabase["NewNPCs"] = { } end
					if nil == GrailDatabase["NewNPCs"][npcId] then GrailDatabase["NewNPCs"][npcId] = { } end
					GrailDatabase["NewNPCs"][npcId][self.playerLocale] = targetName
					GrailDatabase["NewNPCs"][npcId][1] = coordinates
					GrailDatabase["NewNPCs"][npcId][2] = version
				end
			end
		end,

		VerifyItem = function(self, npcId)
			local retval = true
			local frame = com_mithrandir_grailTooltip
			npcId = tonumber(npcId)
			if not frame:IsOwned(UIParent) then frame:SetOwner(UIParent, "ANCHOR_NONE") end
			frame:ClearLines()
			frame:SetHyperlink(format("item:%d:0:0:0:0:0:0:0", npcId - 100000000))
			local numLines = frame:NumLines()
			if nil == numLines or 0 == numLines then
				retval = false
			else
				local text = _G["com_mithrandir_grailTooltipTextLeft1"]
				if text then
					text = text:GetText()
					if text == self.retrievingString then
						retval = false
					elseif text ~= self:NPCName(npcId) then
						self:LogNameIssue('NPC_ISSUES', npcId, text)
						print("Added entry for", npcId)
					else
						print("NPC item seems fine", npcId)
					end
				else
					retval = false
				end
			end
			return retval
		end,

		VerifyItemList = function(self, elapsed)
			self.timeSinceLastUpdate = self.timeSinceLastUpdate + elapsed
			if self.timeSinceLastUpdate > 2.0 then

				for npcId in pairs(self.verifyTable) do
					if nil ~= npcId and self:VerifyItem(npcId) then self.verifyTable[npcId] = nil self.verifyTableCount = self.verifyTableCount - 1 end
				end

				if self.verifyTableCount <= 100 then
					self.currentQuestIndex = next(self.npcs, self.currentQuestIndex)
					while nil ~= self.currentQuestIndex and self.verifyTableCount <= 100 do
						self.currentQuestIndex = tonumber(self.currentQuestIndex)
						if self.npcs[self.currentQuestIndex] and self.currentQuestIndex >= 100000000 then
							if not self:VerifyItem(self.currentQuestIndex) then
								self.verifyTable[self.currentQuestIndex] = true
								self.verifyTableCount = self.verifyTableCount + 1
							end
						end
						if self.verifyTableCount <=100 then
							self.currentQuestIndex = next(self.npcs, self.currentQuestIndex)
						end
					end
				else
					self.emergencyOutlet = self.emergencyOutlet + 1
				end

				self.timeSinceLastUpdate = 0
				print("Verify NPC item status:", self.verifyTableCount, "waiting and current npc index:", self.currentQuestIndex)

				if 10 == self.emergencyOutlet or (0 == self.verifyTableCount and nil == self.currentQuestIndex) then
					self.tooltip:SetScript("OnUpdate", nil)
					self.currentlyVerifying = false
					if 10 == self.emergencyOutlet then print("Abandoning NPC item verification due to lack of response") end
					print("Ended total NPC item verification")
				end

			end

		end,

		VerifyNPC = function(self, npcId)
			local retval = true
			local frame = com_mithrandir_grailTooltip
			npcId = tonumber(npcId)
			if not frame:IsOwned(UIParent) then frame:SetOwner(UIParent, "ANCHOR_NONE") end
			frame:ClearLines()
			if npcId > 1000000 and npcId < 100000000 then
				frame:SetHyperlink(format("unit:0xF510%04X00000000", npcId - 1000000))
			else
				frame:SetHyperlink(format("unit:0xF530%04X00000000", npcId))
			end
			local numLines = frame:NumLines()
			if nil == numLines or 0 == numLines then
				retval = false
			else
				local text = _G["com_mithrandir_grailTooltipTextLeft1"]
				if text then
					text = text:GetText()
					if text ~= self:NPCName(npcId) then
						self:LogNameIssue('NPC_ISSUES', npcId, text)
						print("Added entry for", npcId)
					else
						print("NPC item seems fine", npcId)
					end
				else
					retval = false
				end
			end
			return retval
		end,

		VerifyNPCList = function(self, elapsed)
			self.timeSinceLastUpdate = self.timeSinceLastUpdate + elapsed
			if self.timeSinceLastUpdate > 2.0 then

				for npcId in pairs(self.verifyTable) do
					if nil ~= npcId and self:VerifyNPC(npcId) then self.verifyTable[npcId] = nil self.verifyTableCount = self.verifyTableCount - 1 end
				end

				if self.verifyTableCount <= 100 then
					self.currentQuestIndex = next(self.npcs, self.currentQuestIndex)
					while nil ~= self.currentQuestIndex and self.verifyTableCount <= 100 do
						self.currentQuestIndex = tonumber(self.currentQuestIndex)
						if self.npcs[self.currentQuestIndex] and self.currentQuestIndex < 100000000 and self.currentQuestIndex > 6 then
							if not self:VerifyItem(self.currentQuestIndex) then
								self.verifyTable[self.currentQuestIndex] = true
								self.verifyTableCount = self.verifyTableCount + 1
							end
						end
						if self.verifyTableCount <=100 then
							self.currentQuestIndex = next(self.npcs, self.currentQuestIndex)
						end
					end
				else
					self.emergencyOutlet = self.emergencyOutlet + 1
				end

				self.timeSinceLastUpdate = 0
				print("Verify NPC status:", self.verifyTableCount, "waiting and current npc index:", self.currentQuestIndex)

				if 10 == self.emergencyOutlet or (0 == self.verifyTableCount and nil == self.currentQuestIndex) then
					self.tooltip:SetScript("OnUpdate", nil)
					self.currentlyVerifying = false
					if 10 == self.emergencyOutlet then print("Abandoning NPC verification due to lack of response") end
					print("Ended total NPC verification")
				end

			end

		end,

		VerifyQuest = function(self, questId)
			local retval = true
			local frame = com_mithrandir_grailTooltip
			questId = tonumber(questId)
			if not frame:IsOwned(UIParent) then frame:SetOwner(UIParent, "ANCHOR_NONE") end
			frame:ClearLines()
			frame:SetHyperlink(format("quest:%d", questId))
			local numLines = frame:NumLines()
			if nil == numLines or 0 == numLines then
				retval = false
			else
				local text = _G["com_mithrandir_grailTooltipTextLeft1"]
				if text then
					text = text:GetText()
					if text ~= self:QuestName(questId) then
						self:LogNameIssue('QUEST_ISSUES', questId, text)
						print("Added entry for", questId)
					else
						print("Title seems fine", questId)
					end
				else
					retval = false
				end
			end
			return retval
		end,

		VerifyQuestList = function(self, elapsed)
			-- the first quest to process is set before this script is set, and its value is 2
			-- the last quest to process is currently 29453
			self.timeSinceLastUpdate = self.timeSinceLastUpdate + elapsed
			if self.timeSinceLastUpdate > 2.0 then

				for questId in pairs(self.verifyTable) do
					if nil ~= questId and self:VerifyQuest(questId) then self.verifyTable[questId] = nil self.verifyTableCount = self.verifyTableCount - 1 end
				end

				local emergencyTableCount = 0
				while 29453 >= self.currentQuestIndex and self.verifyTableCount <= 100 and emergencyTableCount <= 100 do
					if self.quests[self.currentQuestIndex] then
						if not self:VerifyQuest(self.currentQuestIndex) then
							self.verifyTable[self.currentQuestIndex] = true
							self.verifyTableCount = self.verifyTableCount + 1
							emergencyTableCount = emergencyTableCount + 1
						end
					end
					self.currentQuestIndex = self.currentQuestIndex + 1
				end

				self.timeSinceLastUpdate = 0
				print("Verify quest status:", self.verifyTableCount, "waiting and current quest index:", self.currentQuestIndex, "emergency:", emergencyTableCount)

				if 0 == self.verifyTableCount and 29453 < self.currentQuestIndex then
					self.tooltip:SetScript("OnUpdate", nil)
					self.currentlyVerifying = false
					print("Ended total quest name verification")
				end

			end
		end,

		}

end
