QuestHubber = LibStub("AceAddon-3.0"):NewAddon("QuestHubber", "AceConsole-3.0", "AceEvent-3.0", "AceTimer-3.0", "AceHook-3.0");
local core = QuestHubber;
local L = LibStub("AceLocale-3.0"):GetLocale("QuestHubber");
local G = LibStub:GetLibrary("LibGratuity-3.0");

-- upvalues for globals used often
local ipairs, pairs, tonumber, select, table, bit = ipairs, pairs, tonumber, select, table, bit
local GetCurrentMapAreaID, UnitLevel, GetNumQuestLogEntries, GetQuestLogTitle = GetCurrentMapAreaID, UnitLevel, GetNumQuestLogEntries, GetQuestLogTitle
local UIParent, WorldMapFrame, WorldMapDetailFrame = UIParent, WorldMapFrame, WorldMapDetailFrame

-- constants for clarity and order in selects
local SHOW_ALWAYS = 0
local SHOW_COND = 1
local SHOW_NEVER = 2

local options_setter = function(info, v) local t=core.db.profile for k=1,#info-1 do t=t[info[k]] end t[info[#info]]=v core:UpdatePins(true) end
local options_getter = function(info) local t=core.db.profile for k=1,#info-1 do t=t[info[k]] end return t[info[#info]] end
local options = {
	name = "QuestHubber",
	type = 'group',
	set = options_setter,
	get = options_getter,
	args = {
		displayMap = {
			name = L["World Map Display"],
			type = 'toggle',
			desc = L["Display a quick toggle and quest count in the corner of the world map"],
			order = 10,
			set = function(info, v) options_setter(info, v) core:SetMapModuleDisplay(v) end,
		},
		quests = {
			name = L["Normal Quests"],
			type = 'group',
			inline = true,
			order = 20,
			args = {
                display = {
					name = L["Display on map"],
					type = 'toggle',
					order = 10,
				},
				lowLevel = {
					name = L["Show Low Level Quests"],
					type = 'select',
					desc = L["Choose how to handle quests below your character's level range"],
					values = {
						[SHOW_ALWAYS] = L["Always"],
						[SHOW_COND] = L["When tracking"],
						[SHOW_NEVER] = L["Never"],
					},
					order = 20,
				},
				highLevel = {
					name = L["Show High Level Quests"],
					type = 'select',
					desc = L["Choose how to handle quests above your character's level range"],
					values = {
						[SHOW_ALWAYS] = L["Always"],
						[SHOW_NEVER] = L["Never"],
					},
					order = 30,
				},
				br = {
					name = '',
					type = 'description',
					order = 32,
					width = 'full',
				},
				hidePreBreadcrumb = {
					name = L["Hide Skipped Quests"],
					type = 'toggle',
					desc = ("|cffff0000%s|r\n%s\n\n|cff9999ff%s|r"):format(L["Warning: Experimental"], L["Do not show pins on the map for quests that were optional lead-ins to quests you have already completed."], L["Note: Because data on Wowhead is limited, the full quest chains are not always available, so this option may incorrectly show some unavailable quests"]),
					order = 35,
				},
				hideBreadcrumb = {
					name = L["Hide Unavailable Chain Quests"],
					type = 'toggle',
					desc = ("|cffff0000%s|r\n%s\n\n|cff9999ff%s|r"):format(L["Warning: Experimental"], L["Do not show pins on the map that require another quest to be completed."], L["Note: Because data on Wowhead is limited, the full quest chains are not always available, so this option may incorrectly show some unavailable or hide some available quests"]),
					order = 40,
					width = 'double',
				},
			},
		},
		daily = {
			name = L["Daily Quests"],
			type = 'group',
			inline = true,
			order = 30,
			args = {
				display = {
					name = L["Display on map"],
					type = 'toggle',
					order = 10,
				},
				range = {
					name = L["Use level filters"],
					type = 'toggle',
					desc = L["The filters set in the Normal Quests section for high and low level quests will apply to dailies as well"],
					order = 20,
				},
			},
		},
		grouping = {
			name = L["Quest Pin Grouping"],
			type = 'group',
			inline = true,
			hidden = true,
			args = {
				enable = {
					name = L["Enable"],
					type = 'toggle',
					desc = L["If enabled, pins within a certain range of each other will be grouped on your world map as a single pin"],
					order = 10,
				},
				group = {
					name = L["Radius"],
					type = 'range',
					desc = L["Group any pins within this many number of yards"],
					min = 0,
					max = 100,
					step = 1,
					order = 20,
				},
			},
			order = 40,
		},
		status = {
			name = '',
			type = 'description',
			order = 90,
		},
	},
};
local defaults = {
	profile = {
		debug = 0,
		display = true, -- Master switch shown on minimap; is not accounted for if displayMap=false

		displayMap = true,
		quests = {
			display = true,
			lowLevel = SHOW_COND,
			highLevel = SHOW_NEVER,
			hidePreBreadcrumb = true,
			hideBreadcrumb = false,
		},
		daily = {
			display = false,
			range = false,
		},
		grouping = {
			enable = true,
			group = 10,
		},
	},
};
local FACTION_MAP = {
	Alliance = 1,
	Horde = 2,
};
local IS_ENGLISH = (GetLocale()=="enUS" or GetLocale()=="enGB")
local RACE_ID = {
	["Human"] 		= 1,
	["Orc"] 		= 2,
	["Dwarf"] 		= 4,
	["Night Elf"]	= 8,
	["Scourge"]		= 16,
	["Tauren"]		= 32,
	["Gnome"]		= 64,
	["Troll"]		= 128,
	["Goblin"]		= 256,
	["Blood Elf"]	= 512,
	["Draenei"]		= 1024,
	["Worgen"]		= 2048,
}
local CLASS_ID = {
	["WARRIOR"] 	= 1,
	["PALADIN"] 	= 2,
	["HUNTER"] 		= 4,
	["ROGUE"] 		= 8,
	["PRIEST"] 		= 16,
	["DEATH KNIGHT"]= 32,
	["MAGE"] 		= 128,
	["SHAMAN"] 		= 64,
	["WARLOCK"] 	= 256,
	["DRUID"] 		= 1024,
}
local FLAG_STR = {
	[0x002] = "  |TInterface\\MINIMAP\\TRACKING\\Banker:0|t",
	[0x040] = L["Daily"],
	[0x080] = L["Escort"],
	[0x100] = L["Dungeon"],
	[0x200] = L["Raid"],
	[0x400] = L["PvP"],
}

core.failedQueries = 0
core.dataLoaded = false;		-- flag set after LoadStep() is done
core.currentZone = -1;			-- current World Map zone id
core.pinReg = {};				-- registry of all used pins
core.frameReg = {};				-- registry of all unused pin frmaes
core.playerQuests = {};			-- quests the player has completed, queried from server
core.questLink = {				-- table linking optional quests to the mandatory quests they lead to which take away the initial
	[25985] = 27874,
	[27727] = 27203,
};
core.questLog = {};				-- current quest log, used to see when quests are accepted or completed
core.questInit = false;			-- flag set after the quest log has been initially recorded
core.abandonQuest = -1;			-- when "Abandon" is clicked, sets the quest that was selected at the time; used after confirmation

core.cache = {
	quests = {},				-- [questid] = "Localized Name",
	npcs = {},					-- [npcid] = "Localized Name",
	invalid = {},				-- [npcid] = true, -- flag set so you don't get double debug warnings about a single npc
};
core.data = {
	zones = {},					-- each zone has a table of quests available
	npcs = {}, 					-- npcs that drop a quest item
};

-- LibDataBroker-1.1 support
local ldb = LibStub:GetLibrary("LibDataBroker-1.1");
local dataObj = ldb:NewDataObject("QuestHubber", {
	type = "data source",
	text = "0",
	value = "0",
	icon = "Interface\\AddOns\\QuestHubber\\QuestIcon.tga",
	OnClick = function(self, button)
		if button == "LeftButton" then
			if IsControlKeyDown() then
				core:ToggleTracking()
			else
				ToggleFrame(WorldMapFrame)
			end
		elseif button == "RightButton" then
			InterfaceOptionsFrame_OpenToCategory("QuestHubber")
		end
	end,
	OnTooltipShow = function(self)
		local shown, total = core.mapModule.shown, core.mapModule.total
		self:ClearLines()
		self:AddLine(("QuestHubber - %s"):format(GetRealZoneText()))
		self:AddLine(L["%d quests available in current zone"]:format(shown))
		self:AddLine(L["%d additional quests not shown due to filter restrictions"]:format(total-shown));
		self:AddLine(" ")
		self:AddLine("|cFFEDA55FClick|r to open the World Map", 0.2, 1, 0.2, 1)
		self:AddLine("|cFFEDA55FRight-Click|r to open the QuestHubber options", 0.2, 1, 0.2, 1)
		self:AddLine("|cFFEDA55FCtrl-Click|r to toggle low level quest tracking", 0.2, 1, 0.2, 1)
	end,
})
function core:UpdateLDB(text)
	dataObj.text = text
end

	
function core:OnInitialize()
	self.db = LibStub("AceDB-3.0"):New("QuestHubberDB", defaults, "Default");
	self:RegisterChatCommand("qh", "SlashCommand")
	self:RegisterChatCommand("qhub", "SlashCommand")
	self:RegisterChatCommand("questhubber", "SlashCommand")
	
	LibStub("AceConfigRegistry-3.0"):RegisterOptionsTable("QuestHubber", options);
	local ACD = LibStub("AceConfigDialog-3.0");
	ACD:AddToBlizOptions("QuestHubber", "QuestHubber");
	
	self:SetupMapModule();
	self:SetupTooltip();

end

function core:SlashCommand()
	InterfaceOptionsFrame_OpenToCategory("QuestHubber")
end

function core:OnEnable()
	self:QueryQuestsCompleted();

	self:RegisterEvent("WORLD_MAP_UPDATE"); 			-- map change
	
	self:RegisterEvent("QUEST_LOG_UPDATE");				-- scan for quest completion/pickup
	
	self:RegisterEvent("MINIMAP_UPDATE_TRACKING"); 		-- for toggling Low Level Quests
	
	self:Hook("SetAbandonQuest", true);					-- hook for abandoned quests
	self:Hook("AbandonQuest", true);					-- hook for confirmation
	
	self.mapModule:Show();
end

function core:OnDisable()
	self.mapModule:Hide();	
end

function core:SlashHandle(message)

end

function core:QueryQuestsCompleted()
	-- We don't need to query anymore
	self.playerQuests = GetQuestsCompleted();
  -- Some people are having no result here, and I don't know why
  if not next(self.playerQuests) then
    -- self:Print("Failed to load quests, trying again...")
    self:ScheduleTimer(function()
      self:QueryQuestsCompleted()
    end, 1)
  else
    self:UpdatePins(true);
  end
end

local numScans = 0
local curScans = {}
function core:QuestScan(id)
	-- self:Debug("Scanning", id)
	if (id and id > 0) then
		-- check if we're scanning too many
		if (numScans < 100 or curScans[id]) then
			G:SetHyperlink(("quest:%d"):format(id));
			local text = G:GetText();
			if (text and text[1] and text[1][1]) then
				-- success, remove from scan list if it was queried
				if (curScans[id]) then
					curScans[id] = nil;
					numScans = numScans - 1;
					-- self:Debug("Removing a query", numScans);
				end
				self.cache.quests[id] = text[1][1];
				local mouse = GetMouseFocus()
				if (mouse and mouse.questHubberID == id) then
					self:ShowTooltip(mouse)
				end
				-- self:Debug(format("Quest id %d = %q", q, text[1][1]))
			else
				-- self:Debug("No name for quest "..id);
				-- no result, so the server has been queried
				if (not curScans[id]) then
					numScans = numScans + 1;
					curScans[id] = true;
					-- self:Debug("Adding another query", numScans);
				end
				self:ScheduleTimer(function()
					self:QuestScan(id)
				end, 2)
			end
		else
			-- self:Debug("NOT QUERYING, TOO HIGH")
			self:ScheduleTimer(function()
				self:QuestScan(id)
			end, 2.5)
		end
	end
end

function core:NPCScan(id)
	G:SetHyperlink(("unit:0xF53%05X00000000"):format(id));
	local text = G:GetText();
	if not text and not self.cache.invalid[id] then
		-- self:Debug("Invalid NPC id:", id, "- perhaps not cached?");
		self.cache.invalid[id] = true;
	end
	if (text and text[1] and text[1][1]) then
		self.cache.npcs[id] = text[1][1];
	end
end

function core:WORLD_MAP_UPDATE()
	if (self.dataLoaded) then
		self:UpdatePins();
	end
end

-----------------------------------------------
-- Pin management
-----------------------------------------------

function core:UpdatePins(force)
	local id = GetCurrentMapAreaID();
	local filtered, shown = 0, 0;
	if (id ~= self.currentZone or force) then
		-- update all pins
		self.currentZone = id;
		self:HideAllPins();
		if (self.data.zones[id]) then
			for q,v in pairs(self.data.zones[id]) do
				if (self:QuestAvailable(q, v)) then	-- if we're not on the quest or have completed it
					if (self:QuestUnfiltered(q, v)) then
						if (self.db.profile.display or not self.db.profile.displayMap) then -- don't want to restrict if we hid the map overlay
							self:ShowPin(q, v);
						end
						shown = shown + 1;
						-- self:Debug("Showing pin",q,"on map",id)
					else
						filtered = filtered + 1;
					end
				else
					-- self:Debug("Not showing pin",q,"on map",id)
				end
			end
			-- self:Debug("Showing",shown,"pins");
		end
		self.mapModule:SetCount(shown, shown+filtered);
	end
end

function core:ShowPin(id, data)
	local flags, class, race, level, minlevel, xcoord, ycoord, npc, name = self:GetQuestData(data);
	if (not flags or not tonumber(xcoord) or not tonumber(ycoord)) then
		-- self:Debug("Bad quest data:", id, data, ";;;", flags, level, xcoord, ycoord, npc, name)
		return
	end
	
	if (bit.band(flags, 0x02) > 0) then
		self.data.npcs[tonumber(npc)] = id;
	end
	
	-- check the cache for quest name; scan tooltip if not
	if (not self.cache.quests[id]) then
		self:QuestScan(id);
	end
	-- check the cache for npc name; scan tooltip if not (and id belongs to an npc and not an object)
	if (not self.cache.npcs[npc] and bit.band(flags, 0x010)>0) then
		self:NPCScan(npc);
	end

	-- check level and set pin type
	local texType, level, minlevel = nil, tonumber(level), tonumber(minlevel)
	if bit.band(flags, 0x040) > 0 then
		texType = "daily"
	elseif UnitLevel("player") < minlevel then
		texType = "high"
	elseif UnitLevel("player") > level + 10 then
		texType = "low"
	end
	
	local pin = self:GetPin();
	
	pin:SetType(texType);
	pin:ClearAllPoints();
	pin.questHubberID = id;
	-- pin:SetParent(WorldMapDetailFrame);
	pin:SetFrameLevel(WorldMapPOIFrame:GetFrameLevel()-1);
	pin:SetPoint("CENTER", WorldMapDetailFrame, "TOPLEFT", xcoord/10000*WorldMapDetailFrame:GetWidth(), -ycoord/10000*WorldMapDetailFrame:GetHeight());
	-- pin:SetFrameStrata("FULLSCREEN");
	pin:Show();
	
	self.pinReg[id] = pin;
end

function core:HidePin(id)
	if (self.pinReg[id]) then
		self.pinReg[id]:Hide();
		table.insert(self.frameReg, self.pinReg[id]);
		self.pinReg[id] = nil;
	end
end

function core:HideAllPins()
	for i,v in pairs(self.pinReg) do
		self:HidePin(i);
	end
end

-- tile sizes and coord starts for Interface\\MINIMAP\\ObjectIcons.blp
local tileX, tileY = 1/8, 1/8
local dailyX, dailyY = 3, 1
local questX, questY = 1, 1
function core:GetPin()
	if (#self.frameReg > 0) then
		return table.remove(self.frameReg);
	end
	
	-- create new
	local pin = CreateFrame("Frame", nil, WorldMapDetailFrame);
	pin:SetWidth(16);
	pin:SetHeight(16);
	pin:EnableMouse(true);
	pin:SetScript("OnEnter", function(pin)
		self:ShowTooltip(pin);
	end);
	pin:SetScript("OnLeave", function()
		self:HideTooltip();
	end);
	pin.SetType = function(self, texType)
		if self.texType == texType then return end -- don't need to make changes

		-- 256x64, 8x4
		self.texture:SetDesaturated(false);
		self.texture:SetVertexColor(1, 1, 1);
		if texType == "daily" then
            self.texture:SetTexCoord(dailyX*tileX, (dailyX+1)*tileX, dailyY*tileY, (dailyY+1)*tileY)
		elseif texType == "low" then
			-- I don't like this one, it's too hard to see.
			-- self.texture:SetTexCoord(4*0.125, 5*0.125, 3*0.250, 4*0.250);
            self.texture:SetTexCoord(questX*tileX, (questX+1)*tileX, questY*tileY, (questY+1)*tileY)
			self.texture:SetVertexColor(0.75, 0.75, 0.75);
		elseif texType == "high" then
			-- Note: not all systems can desaturate. If they don't, I don't really care.
            self.texture:SetTexCoord(questX*tileX, (questX+1)*tileX, questY*tileY, (questY+1)*tileY)
			self.texture:SetDesaturated(1);
		else
            self.texture:SetTexCoord(questX*tileX, (questX+1)*tileX, questY*tileY, (questY+1)*tileY)
		end

		self.texType = texType
	end

	pin.texture = pin:CreateTexture();
	pin.texture:SetTexture("Interface\\MINIMAP\\ObjectIcons.blp");
	pin.texture:SetAllPoints();
	pin:SetType("default")
	
	return pin;
end

-- QuestAvailable() returns whether or not the quest is available for pickup right now
function core:QuestAvailable(id, data)
	if data then
		local flags, class, race, _, _, _, _, _, _, pre, post, final = self:GetQuestData(data);
		class, race = tonumber(class), tonumber(race)
		
		-- check race
		local urace = RACE_ID[select(2, UnitRace("player"))]
		if (race > 0 and urace and bit.band(race, urace) == 0) then
			-- self:Debug("Hid", id, "because of race restriction")
			return false
		end			
		
		-- check class
		local uclass = CLASS_ID[select(2, UnitClass("player"))]
		if (class > 0 and uclass and bit.band(class, uclass) == 0) then
			-- self:Debug("Hid", id, "because of class restriction")
			return false
		end		

		-- check daily - we don't want it to add to total if we have it turned off
		if (bit.band(flags, 0x040) > 0 and not self.db.profile.daily.display) then
			return false
		end

		-- check if this is a pre-quest that cannot be completed
		if (self.db.profile.quests.hidePreBreadcrumb) then
			-- check post and final of this quest
			if (post ~= "0" and self:CompletedBreadcrumbs(post)) or (final ~= "0" and self:CompletedBreadcrumbs(final)) then
				return false
			end
		end

		-- check if this is a future quest that cannot be picked up yet
		if (self.db.profile.quests.hideBreadcrumb) then
			-- check pre of this quest
			if pre ~= "0" and not self:CompletedBreadcrumbs(pre) then
				return false
			end
		end
	end
	
	-- check based on chain - TODO: delete
	if (not self.questLog[id] and not self.playerQuests[id] and self.questLink[id]) then
		return not self.playerQuests[self.questLink[id]];
	end
	
	-- now only return true if its not in your logs or completed list
	return not self.questLog[id] and not self.playerQuests[id];
end

-- QuestUnfiltered() returns whether or not the quest is being filtered (i.e., level requirement)
-- add daily flag?
function core:QuestUnfiltered(id, data)
	local db = self.db.profile

	if data then
		local flags, _, _, level, minlevel = self:GetQuestData(data)
		level, minlevel = tonumber(level), tonumber(minlevel)

		-- check daily quest
		-- I am taking advantage of the fact that there are only two filters. This will need to change if I add more
		-- We definitely want to display if we have range off
		if (bit.band(flags, 0x040) > 0) then
			if db.daily.display and not db.daily.range then
				return true
			end
		else
			-- check if we're displaying normal quests
			if not db.quests.display then 
				return false 
			end
		end

		-- check level requirement
		if  (db.quests.lowLevel == SHOW_NEVER and UnitLevel("player") > level + 10) 							or
			(db.quests.lowLevel == SHOW_COND and UnitLevel("player") > level + 10 and not self:TrackingLow()) 	or
			(db.quests.highLevel == SHOW_NEVER and UnitLevel("player") < minlevel)    							then
			-- self:Debug("Hid", id, "because of level restriction")
			return false
		end
	end

	return true
end

-- checks whether the player is tracking low level quests
local trackIndex
local trackTexture = "Interface\\Minimap\\Tracking\\TrivialQuests"
function core:TrackingLow()
	if not trackIndex or select(2, GetTrackingInfo(trackIndex)) ~= trackTexture then
		trackIndex = nil -- if the second condition fired, let's discard the old value just in case
		for i=1,GetNumTrackingTypes() do
			local _, texture = GetTrackingInfo(i)
			if texture == trackTexture then
				trackIndex = i
				break
			end
		end
	end
	
	if trackIndex then
		return select(3, GetTrackingInfo(trackIndex))
	end
end

-- when minimap tracking changes, see if we need to fire a map update
local oldTrack = core:TrackingLow()
function core:MINIMAP_UPDATE_TRACKING()
	if self:TrackingLow() ~= oldTrack and self.db.profile.quests.lowLevel == SHOW_COND then
		self:UpdatePins(true)
		oldTrack = not oldTrack
	end
end

-- toggles the low level quest tracking
function core:ToggleTracking()
	-- force refresh to make sure our vars are correct
	local current = self:TrackingLow()

	if trackIndex then
		SetTracking(trackIndex, not current)
	end
end

-- CompletedBreadcrumbs() takes a quest id string joined with | (or) or & (and) and returns the result respectively
function core:CompletedBreadcrumbs(qidStr)
	local qid, delim, cdr = qidStr:match("(%d+)([|&]*)(%s*)")
	local completed = self.playerQuests[tonumber(qid) or 0]
	
	if delim == "" then
		return completed
	elseif delim == "|" then
		return completed or self:CompletedBreadcrumbs(cdr)
	elseif delim == "&" then
		return completed and self:CompletedBreadcrumbs(cdr)
	end
end

-----------------------------------------------
-- Tooltip management
-----------------------------------------------

function core:GetMousePosition()
	local left, top = WorldMapDetailFrame:GetLeft(), WorldMapDetailFrame:GetTop();
	local width, height = WorldMapDetailFrame:GetWidth(), WorldMapDetailFrame:GetHeight();
	local scale = WorldMapDetailFrame:GetEffectiveScale();

	local x, y = GetCursorPosition();
	local cx = (x/scale - left) / width;
	local cy = (top - y/scale) / height;
	
	return math.min(math.max(cx, 0), 1), math.min(math.max(cy, 0), 1);
end

function core:Distance(x1, y1, x2, y2)
	-- gets distance in map units TODO: use yards?
	local distRatio = WorldMapDetailFrame:GetHeight() / WorldMapDetailFrame:GetWidth();
	
	return math.sqrt( (x1 - x2)^2 + ((y1 - y2)/distRatio)^2 );
end

local listedQuests = {} -- this will be wiped and filled with names that are added to the tooltip so there are no dupes
function core:ShowTooltip(pin)
	-- check to make sure the tooltip can be shown because of the parent
	if not UIParent:IsShown() then
		if self.tooltip:GetParent() == UIParent then
			self.tooltip:SetParent(WorldMapFrame);
			self.tooltip:SetFrameStrata("TOOLTIP");
		end
	elseif self.tooltip:GetParent() ~= UIParent then
		self.tooltip:SetParent(UIParent);
		self.tooltip:SetFrameStrata("TOOLTIP");
	end

	self.tooltip:SetOwner(pin, "ANCHOR_RIGHT");
	self.tooltip:ClearLines();
	
	-- find all quests in range of hover
	local mx, my = self:GetMousePosition();
	local npcList = {};
	local npcNames = {};
	for q,v in pairs(self.data.zones[self.currentZone]) do
		if (self:QuestAvailable(q, v) and self:QuestUnfiltered(q, v)) then
			local _, _, _, _, _, xcoord, ycoord, npc, npcname = self:GetQuestData(v);
			if (tonumber(xcoord) and tonumber(ycoord)) then
				local dist = self:Distance(mx, my, xcoord/10000, ycoord/10000);
				-- self:Print("d:",dist,v.begin.x,v.begin.y);
				if (dist <= 0.02) then
					if (not npcList[npc]) then
						npcList[npc] = {};
						npcNames[npc] = npcname;
					end
					table.insert(npcList[npc], q);
				end
			end
		end
	end
	
	local first = true;	
	local cZone = self.data.zones[self.currentZone];
	for npc,questList in pairs(npcList) do
		wipe(listedQuests)
		if (not first) then
			self.tooltip:AddLine(" ");
		else
			first = false;
		end
		table.sort(questList);
		for _,q in ipairs(questList) do
			local flags, class, race, level, minlevel, _, _, _, npcname = self:GetQuestData(cZone[q]);
			local questName = self.cache.quests[q]
			local leftStr = ("[%d] %s"):format(level, questName or "(Querying server...)")
			local rightStr = "";
			
			-- checking flags for rightStr text
			-- rightStr = "|TInterface\\CURSOR\\CURSORICONSNEW:16:16:0:0:128:256:32:63:128:159|t" -- -64:-96
			for flg, text in pairs(FLAG_STR) do
				if bit.band(flags, flg) > 0 then
					if text:match("%s.+") then
						leftStr = leftStr .. text
					else
						rightStr = text
					end
				end
			end

			-- check if already printed - this is for spam quests like the human starting area that haven't been labeled correctly
			if not questName or not listedQuests[questName] then
				self.tooltip:AddDoubleLine(leftStr, rightStr, 255/255, 210/255, 0/255, 255/255, 210/255, 0/255);
				-- self.tooltip:AddLine(leftStr .. rightStr, 255/255, 210/255, 0/255, 1);
				self.tooltip:SetLastFont(self.tooltip.large);
				self.tooltip:SetLastFont(self.tooltip.small, true);
				if questName then
					listedQuests[questName] = true
				end
			end
		end
		-- if the npc name is cached, display it (will be localized); if not, if we are on enUS or enGB, display the hard-coded name as it should be localized; if on a different locale, display the hard-coded name in parenthesis to denote that it has not been discovered yet
		self.tooltip:AddLine(self.cache.npcs[npc] or (IS_ENGLISH and npcNames[npc]) or ("(%s)"):format(npcNames[npc]), 1, 1, 1, 1);
		self.tooltip:SetLastFont(self.tooltip.small);
	end
	
	self.tooltip:Show();
end

function core:HideTooltip()
	self.tooltip:Hide();
end

-----------------------------------------------
-- Quest log management
-----------------------------------------------

function core:QUEST_LOG_UPDATE()
	-- get closed tabs
	local closed = {}
	for i=1,GetNumQuestLogEntries() do
		local title, _, _, _, isHeader, isCollapsed = GetQuestLogTitle(i);
		if (isCollapsed) then
			closed[title] = true
		end
	end
	ExpandQuestHeader(0)
	
	if (not self.questInit) then
		for i=1,GetNumQuestLogEntries() do
			local _, _, _, _, _, _, _, _, id = GetQuestLogTitle(i);
			if (id and id > 0) then
				self.questLog[id] = true;
			end
		end
		self:Debug("Quest Log snapshot saved");
		-- self:UnregisterEvent("QUEST_LOG_UPDATE");
		self.questInit = true;
	else
		-- self:Debug("QUEST_LOG_UPDATE");
		local tempLog = {};
		for i=1,GetNumQuestLogEntries() do
			local name, _, _, _, _, _, _, _, id = GetQuestLogTitle(i);
			-- self:Debug("   ", name, id);
			if (id and id > 0) then
				tempLog[id] = name;
			end
		end
		-- self:Debug("_END_");
		
		for i,v in pairs(self.questLog) do
			if (not tempLog[i]) then
				-- quest removed; hopefully completed
				self:Debug("Quest completed:",i);
				self.questLog[i] = nil;
				tempLog[i] = nil;
				self.playerQuests[i] = true;
				-- force a full update in case any optional pre-reqs should be turned off
				self:UpdatePins(true);
			end
		end
		
		-- merge added
		for i,v in pairs(tempLog) do
			if (not self.questLog[i]) then
				self:Debug("Quest accepted:", v);
				self.questLog[i] = true;
				self:HidePin(i);
			end
		end
	end
	
	-- restore closed tabs
	for i=1,GetNumQuestLogEntries() do
		local title, _, _, _, isHeader = GetQuestLogTitle(i);
		if closed[title] then
			CollapseQuestHeader(i)
		end
	end
end

function core:SetAbandonQuest()
	self.abandonQuest = GetQuestLogSelection();
end

function core:AbandonQuest()
	-- saved quest was abandoned
	-- self:Debug("AbandonQuest");
	local name, _, _, _, _, _, _, _, id = GetQuestLogTitle(self.abandonQuest);
	self:Debug("Quest abandoned:",name);
	self.questLog[id] = nil;
	self:UpdatePins(true)
end

function core:Debug(level, ...)
	if (type(level) ~= "number") then
		self:Debug(1, level, ...);
		return;
	end
	if self.db.profile.debug >= level then 
		self:Print(...);
	end
end

local loader  -- timer that runs UpdatePins after everything is loaded
function core:RegisterQuests(zones)
	for i,v in pairs(zones) do
		if (not self.data.zones[i]) then
			self.data.zones[i] = {};
		end
		
		for q,qv in pairs(v) do
			self.data.zones[i][q] = qv;
		end
	end
	self.dataLoaded = true;
	if (not loader) then
		loader = self:ScheduleTimer(function()
			self:UpdatePins(true);
			self:SecureHookScript(GameTooltip, "OnTooltipSetUnit", "CheckNPCTooltip"); -- used for adding notes to mobs' tooltips
		end, 1);
	end
end

-- returns flags, class, race, level, xcoord, ycoord, npc, npcname
function core:GetQuestData(questStr)
	return questStr:match("(%d+):(%d+):(%d+):(%d+):(%d+):(%d+):(%d+):(%d+):(.-)::([0-9|&]+):([0-9|&]+):([0-9|&]+)")
end

-- local oprint = print
-- local function print(...)
-- 	local toprint = {...}
-- 	core:ScheduleTimer(function()
-- 		oprint(unpack(toprint))
-- 	end, 1)
-- end

function core:SetupMapModule()
	-- frame contains checkbox and text label
	local frame = CreateFrame("Frame", nil, WorldMapDetailFrame);
	frame:SetSize(250, 20);
	frame:SetPoint("TOPLEFT", WorldMapDetailFrame, "TOPLEFT", 5, -2);
	frame:Hide();
	frame:EnableMouse(1);
	frame:SetFrameStrata("HIGH");
	frame:SetFrameLevel(5);
	frame:SetScript("OnEnter", function(self)
		core.tooltip:ClearLines();
		core.tooltip:SetOwner(self);
		core.tooltip:AddLine(L["%d quests available in current zone"]:format(self.shown))
		if self.total > self.shown then
			core.tooltip:AddLine(L["%d additional quests not shown due to filter restrictions"]:format(self.total-self.shown));
			core.tooltip:SetLastFont(core.tooltip.small);
		end
		core.tooltip:Show();
		core.tooltip:ClearAllPoints();
		core.tooltip:SetPoint("TOPLEFT", self, "BOTTOMLEFT", 20, 0);
	end);
	frame:SetScript("OnLeave", function(self)
		core.tooltip:Hide();
	end);
	
	frame.check = CreateFrame("CheckButton", nil, frame, "OptionsBaseCheckButtonTemplate");
	frame.check:SetSize(18, 18);
	frame.check:SetHitRectInsets(0, 0, 0, 0);
	frame.check:SetPoint("LEFT");
	frame.check:SetChecked(self.db.profile.display);
	frame.check:SetFrameLevel(6);
	frame.check:SetScript("OnClick", function(self)
		if (self:GetChecked()) then
			core.db.profile.display = true
		else
			core.db.profile.display = false
		end
		core:UpdatePins(true)
	end)
	frame.check:SetScript("OnEnter", function(self)
		-- core:Print("Enter");
		core.tooltip:ClearLines();
		core.tooltip:SetOwner(self, "ANCHOR_RIGHT");
		core.tooltip:AddLine(L["Show/Hide all pins"]);
		core.tooltip:Show();
	end);
	frame.check:SetScript("OnLeave", function(self)
		-- core:Print("Leave");
		core.tooltip:Hide();
	end);
	
	local font, size = GameTooltipHeader:GetFont();
	frame.text = frame:CreateFontString();
	frame.text:SetFont(font, size, "OUTLINE");
	frame.text:SetTextColor(255/255, 210/255, 0/255, 1);
	frame.text:SetText("QuestHubber");
	frame.text:SetPoint("LEFT", frame.check, "RIGHT");
	
	core.mapModule = frame;
	core.mapModule.SetCount = function(self, m, n)
		-- sets the text to QuestHubber (m/n) and shows a tooltip saying how many are hidden
		self.shown, self.total = m, n or m
		local suffix = ""
		if (m and (n or m)>0) then
			if not n or n <= m then
				suffix = ("(%d)"):format(m)
			else
				suffix = ("(%d/%d)"):format(m, n)
			end
		end
		self.text:SetText("QuestHubber " .. suffix)

		-- also update our ldb object
		core:UpdateLDB(suffix=="" and 0 or suffix:sub(2, -2))
	end
end

function core:SetMapModuleDisplay(shown)
	if shown then
		core.mapModule:Show()
	else
		core.mapModule:Hide()
	end
end

	

function core:SetupTooltip()
	self.tooltip = CreateFrame("GameTooltip", "QuestHubberTooltip", UIParent, "GameTooltipTemplate");
	self.tooltip:SetFrameStrata("TOOLTIP");
	self.tooltip.large = QuestHubberTooltipTextLeft1:GetFontObject();
	self.tooltip.small = QuestHubberTooltipTextLeft2:GetFontObject();
	self.tooltip.SetLastFont = function(self, fontObj, rightText)
		local txt = "Left"
		if rightText then
			txt = "Right"
		end
		_G[("QuestHubberTooltipText%s%d"):format(txt, self:NumLines())]:SetFont(fontObj:GetFont());
		-- _G["QuestHubberTooltipTextRight"..self:NumLines()]:SetFont(fontObj:GetFont());
	end
	
	-- if the UI panel disappears (maximized WorldMapFrame) we need to change parents
	self:HookScript(UIParent, "OnHide", function()
		self.tooltip:SetParent(WorldMapFrame);
		self.tooltip:SetFrameStrata("TOOLTIP");
	end);
	self:HookScript(UIParent, "OnShow", function()
		self.tooltip:SetParent(UIParent);
		self.tooltip:SetFrameStrata("TOOLTIP");
	end);
end

function core:CheckNPCTooltip(tooltip)
	if (not UnitIsPlayer("mouseover") or true) then
		-- check if this npc drops a quest item
		local guid = UnitGUID("mouseover");
		if ( guid ) then
			local id = tonumber(guid:sub(7, 10), 16);
			local q = self.data.npcs[id];
			if (q and self:QuestAvailable(q) and self:QuestUnfiltered(q)) then
				local leftStr = ("|TInterface\\MINIMAP\\ObjectIcons:17:9:0:0:256:128:40:56:32:63|t %s"):format(L["Drops an item which starts [%s]"]:format(self.cache.quests[q] or ("(%s)"):format(L["Querying server..."])))
				tooltip:AddLine(leftStr);
				tooltip:Show();
			end
		end
	end
end

