local _, L = ...
local Frame, TalkBox, API, GetTime = {}, {}, ImmersionAPI, GetTime

----------------------------------
-- Event handler
----------------------------------
function Frame:OnEvent(event, ...)
	self:ResetElements(event)
	self:HandleGossipQuestOverlap(event)
	if self[event] then
		event = self[event](self, ...) or event
	end
	self.TalkBox.lastEvent = event
	self.lastEvent = event
	self.timeStamp = GetTime()
	self:UpdateItems()
	self:UpdateBackground()
	return event
end

function Frame:OnHide()
	self:ClearImmersionFocus()
	self.TalkBox.BackgroundFrame.OverlayKit:Hide()
end

----------------------------------
-- Content handler (gossip & quest)
----------------------------------
function Frame:AddQuestInfo(template)
	local elements = self.TalkBox.Elements
	local content = elements.Content
	local height = elements:Display(template, 'Stone')

	-- hacky fix to stop a content frame that only contains a spacer from showing.
	if height > 20 then
		elements:Show()
		content:Show()
		elements:UpdateBoundaries()
	else
		elements:Hide()
		content:Hide()
	end
	-- Extra: 32 px padding
	self.TalkBox:SetExtraOffset((height + 32) * L('elementscale'))
	self.TalkBox.NameFrame.FadeIn:Play()
end

function Frame:IsGossipAvailable(ignoreAutoSelect)
	-- if there is only a non-gossip option, then go to it directly
	if 	(API:GetNumGossipAvailableQuests() == 0) and
		(API:GetNumGossipActiveQuests() == 0) and
		(API:GetNumGossipOptions() == 1) and
		not API:ForceGossip() then
		----------------------------
		if API:CanAutoSelectGossip(ignoreAutoSelect) then
			return false
		end
	end
	return true
end

function Frame:IsQuestAutoAccepted(questStartItemID)
	-- Auto-accepted quests need to be treated differently from other quests,
	-- and different from eachother depending on the source of the quest.
	-- Handling here is prone to cause bugs/weird behaviour, update with caution.

	local questID = GetQuestID()
	local isFromAdventureMap = API:QuestIsFromAdventureMap()
	local isFromAreaTrigger = API:QuestGetAutoAccept() and API:QuestIsFromAreaTrigger()
	local isFromItem = (questStartItemID ~= nil and questStartItemID ~= 0)

	-- the quest came from an adventure map, so user has already seen and accepted it.
	if isFromAdventureMap then
		return true
	end

	-- an item pickup by loot caused this quest to show up, don't intrude on the user.
	if isFromItem then
		-- add a new quest tracker popup and close the quest dialog
		if AddAutoQuestPopUp(questID, 'OFFER') then
			PlayAutoAcceptQuestSound()
		end
		API:CloseQuest()
		return true
	end

	-- triggered from entering an area, but also from forced campaign quests.
	-- let's not intrude on the user; just add a tracker popup.
	if isFromAreaTrigger then
		-- add a new quest tracker popup and close the quest dialog
		if AddAutoQuestPopUp(questID, 'OFFER') then
			PlayAutoAcceptQuestSound()
		end
		API:CloseQuest()
		return true
	end
end

-- Iterate through gossip options and simulate a click on the best option.
function Frame:SelectBestOption()
	local button = self.TitleButtons:GetBestOption()
	if button then
		button.Hilite:SetAlpha(1)
		button:Click()
		button:OnLeave()
		PlaySound(SOUNDKIT.IG_QUEST_LIST_SELECT)
	end
end

function Frame:GetRemainingSpeechTime()
	return self.TalkBox.TextFrame.Text:GetTimeRemaining()
end

function Frame:IsSpeechFinished()
	return self.TalkBox.TextFrame.Text:IsFinished()
end

-- hack to figure out if event is related to quests
function Frame:IsObstructingQuestEvent(forceEvent)
	local event = forceEvent or self.lastEvent or ''
	return ( event:match('^QUEST') and event ~= 'QUEST_ACCEPTED' )
end

function Frame:HandleGossipQuestOverlap(event)
	-- Since Blizzard handles this transition by mutually exclusive gossip/quest frames,
	-- and their visibility to determine whether to close gossip or quest interaction,
	-- events need to be checked so that an NPC interaction is correctly transitioned.
	if (type(event) == 'string') then
		if ( event == 'GOSSIP_SHOW' ) then
		--	API:CloseQuest()
		elseif self:IsObstructingQuestEvent(event) then
			API:CloseGossip(true)
		end
	end
end

function Frame:HandleGossipOpenEvent(kit)
	if not self.gossipHandlers[kit] then
		self:SetBackground(kit)
		self:UpdateTalkingHead(API:GetUnitName('npc'), API:GetGossipText(), 'GossipGossip')
		if self:IsGossipAvailable() then
			self:PlayIntro('GOSSIP_SHOW')
		end
	end
end

function Frame:SetBackground(kit)
	local backgroundFrame = self.TalkBox.BackgroundFrame;
	local overlay = backgroundFrame.OverlayKit;

	if kit and not L('disablebgtextures') then
		local backgroundAtlas = GetFinalNameFromTextureKit('QuestBG-%s', kit)
		local atlasInfo = C_Texture.GetAtlasInfo(backgroundAtlas)
		if atlasInfo then
			local minColor = CreateColor(1, 1, 1, 0)
			local maxColor = CreateColor(1, 1, 1, 0.5)

			overlay:Show()
			L.SetGradient(overlay, 'HORIZONTAL', minColor, maxColor)

			overlay:SetSize(atlasInfo.width, atlasInfo.height)
			overlay:SetTexture(atlasInfo.file)
			overlay:SetTexCoord(
				atlasInfo.leftTexCoord, atlasInfo.rightTexCoord,-- + 0.035,
				atlasInfo.topTexCoord, atlasInfo.bottomTexCoord)-- + 0.035)
			return
		end
	end
end

function Frame:UpdateBackground()
	local theme = API:GetQuestDetailsTheme(GetQuestID())
	local kit = theme and theme.background and theme.background:gsub('QuestBG%-', '')
	if kit then
		self:SetBackground(kit)
	end
end

function Frame:ResetElements(event)
	if ( self.IgnoreResetEvent[event] ) then return end

	self.Inspector:Hide()
	self.TalkBox.Elements:Reset()
	self:SetBackground(nil)
end

function Frame:UpdateTalkingHead(title, text, npcType, explicitUnit, isToastPlayback)
	local unit = explicitUnit
	if not unit then
		if ( UnitExists('questnpc') and not UnitIsUnit('questnpc', 'player') and not UnitIsDead('questnpc') ) then
			unit = 'questnpc'
		elseif ( UnitExists('npc') and not UnitIsUnit('npc', 'player') and not UnitIsDead('npc') ) then
			unit = 'npc'
		else
			unit = npcType
		end
	end
	local talkBox = self.TalkBox
	talkBox:SetExtraOffset(0)
	talkBox.ReputationBar:Update()
	talkBox.MainFrame.Indicator:SetTexture('Interface\\GossipFrame\\' .. npcType .. 'Icon')
	talkBox.MainFrame.Model:SetUnit(unit)
	local textFrame = talkBox.TextFrame
	-- Handle secret NPC names in the title, if applicable.
	-- The gossip text can't determine truncation since it is normally anchored
	-- to the name frame; so we adjust the name frame and text frame positions
	-- based on whether the title is secret or not.
	if API:IsSecretValue(title) then
		talkBox.NameFrame.Name:SetMaxLines(1)
		textFrame.Text:SetPoint('TOPLEFT', talkBox.MainFrame.Model, 'TOPRIGHT', 14, -30)
	else
		talkBox.NameFrame.Name:SetMaxLines(2)
		textFrame.Text:SetPoint('TOPLEFT', talkBox.NameFrame.Name, 'BOTTOMLEFT', 0, -3)
	end
	talkBox.NameFrame.Name:SetText(title)
	textFrame.Text:SetText(text)
	-- Add contents to toast.
	if not isToastPlayback then
		if L('onthefly') then
			self:QueueToast(title, text, npcType, unit)
		elseif L('supertracked') then
			self:QueueQuestToast(title, text, npcType, unit)
		end
	end
	if L('showprogressbar') and not L('disableprogression') then
		talkBox.ProgressionBar:Show()
	end
end


----------------------------------
-- Content handler (items)
----------------------------------
function Frame:SetItemTooltip(tooltip, item)
	local objType = item.objectType
	if objType == 'item' then
		tooltip:SetQuestItem(item.type, item:GetID())
	elseif objType == 'currency' then
		tooltip:SetQuestCurrency(item.type, item:GetID())
	end
	if item.rewardContextLine then
		GameTooltip_AddBlankLineToTooltip(tooltip)
		GameTooltip_AddColoredLine(tooltip, item.rewardContextLine, QUEST_REWARD_CONTEXT_FONT_COLOR)
	end
	tooltip.Icon.Texture:SetTexture(item.itemTexture or item.Icon:GetTexture())
	tooltip:Show()
end

function Frame:GetItemColumn(owner, id)
	local columns = owner and owner.Columns
	if columns and id then
		local column = columns[id]
		local anchor = columns[id - 1]
		if not column then
			column = CreateFrame('Frame', nil, owner)
			column:SetSize(1, 1) -- set size to make sure children are drawn
			column:SetScript('OnHide', function(self) self.lastItem = nil end)
			column:SetFrameStrata("FULLSCREEN_DIALOG")
			L.Mixin(column, L.AdjustToChildren)
			columns[id] = column
		end
		if anchor then
			column:SetPoint('TOPLEFT', anchor, 'TOPRIGHT', 30, 0)
		else
			column:SetPoint('TOPLEFT', owner, 0, -30)
		end
		column:Show()
		return column
	end
end

function Frame:ShowItems()
	local inspector = self.Inspector
	local items, hasChoice, hasExtra = inspector.Items
	local active, extras, choices = inspector.Active, inspector.Extras, inspector.Choices
	inspector:Show()
	for id, item in ipairs(items) do
		local tooltip = inspector.tooltipFramePool:Acquire()
		local owner = item.type == 'choice' and choices or extras
		local columnID = ( id % 3 == 0 ) and 3 or ( id % 3 )
		local column = self:GetItemColumn(owner, columnID)

		hasChoice = hasChoice or item.type == 'choice'
		hasExtra = hasExtra or item.type ~= 'choice'

		-- Set up tooltip
		tooltip:SetParent(column)
		tooltip:SetOwner(column, "ANCHOR_NONE")
		tooltip.owner = owner

		active[id] = tooltip.Button
		tooltip.Button:SetID(id)

		-- Mixin the tooltip button functions
		L.Mixin(tooltip.Button, L.TooltipMixin)
		tooltip.Button:SetReferences(item, inspector)

		self:SetItemTooltip(tooltip, item, inspector)

		-- Anchor the tooltip to the column
		tooltip:SetPoint('TOP', column.lastItem or column, column.lastItem and 'BOTTOM' or 'TOP', 0, 0)
		column.lastItem = tooltip
	end

	-- Text display:
	local elements = self.TalkBox.Elements
	local progress = elements.Progress
	local rewardsFrame = elements.Content.RewardsFrame
	-- Choice text:
	if rewardsFrame.ItemChooseText:IsVisible() then
		choices.Text:Show()
		choices.Text:SetText(rewardsFrame.ItemChooseText:GetText())
	else
		choices.Text:Hide()
	end
	-- Extra text:
	if progress.ReqText:IsVisible() then
		extras.Text:Show()
		extras.Text:SetText(progress.ReqText:GetText())
	elseif rewardsFrame.ItemReceiveText:IsVisible() and hasExtra then
		extras.Text:Show()
		extras.Text:SetText(rewardsFrame.ItemReceiveText:GetText())
	else
		extras.Text:Hide()
	end
	------------------------------
	inspector.Threshold = #active
	inspector:AdjustToChildren()
	if inspector.SetFocus then
		inspector:SetFocus(1)
	end
end

function Frame:UpdateItems()
	local items = self.Inspector.Items
	wipe(items)
	-- count item rewards
	for _, item in ipairs(self.TalkBox.Elements.Content.RewardsFrame.Buttons) do
		if item:IsVisible() then
			items[#items + 1] = item
		end
	end
	-- count necessary quest progress items
	for _, item in ipairs(self.TalkBox.Elements.Progress.Buttons) do
		if item:IsVisible() then
			items[#items + 1] = item
		end
	end
	self.hasItems = #items > 0

	if self.hasItems then
		self:AddHint('CIRCLE', INSPECT)
	else
		self:RemoveHint('CIRCLE')
	end

	return items, #items
end

----------------------------------
-- Animation players
----------------------------------
function Frame:PlayIntro(event, freeFloating)
	local isShown = self:IsVisible()
	local shouldAnimate = not isShown and not L('disableglowani')
	self.playbackEvent = event

	if freeFloating then
		self:ClearImmersionFocus()
	else
		self:SetImmersionFocus()
		self:AddHint('TRIANGLE', GOODBYE)
	end

	self:Show()

	if IsOptionFrameOpen() then
		self:ForceClose(true)
	else
		self:EnableKeyboard(not freeFloating)
		self:FadeIn(nil, shouldAnimate, freeFloating)

		local box = self.TalkBox
		local x, y = L('boxoffsetX'), L('boxoffsetY')
		box:ClearAllPoints()
		box:SetOffset(box.offsetX or x, box.offsetY or y)

		if not shouldAnimate and not L('disableglowani') then
			self.TalkBox.MainFrame.SheenOnly:Play()
		end

	end
end

-- This will also hide the frames after the animation is done.
function Frame:PlayOutro(optionFrameOpen)
	self:EnableKeyboard(false)
	self:FadeOut(0.5)
	self:PlayToasts(optionFrameOpen)
end

function Frame:ForceClose(optionFrameOpen)
	API:CloseGossip()
	API:CloseQuest()
	API:CloseItemText()
	self:PlayOutro(optionFrameOpen)
end

----------------------------------
-- Key input handler
----------------------------------
local inputs, modifierStates = L.Inputs, L.ModifierStates;

function Frame:IsInspectModifier(button)
	return button and button:match(L('inspect')) and true
end

function Frame:IsModifierDown(modifier)
	return modifierStates[modifier or L('inspect')]()
end

function Frame:OnKeyDown(button)
	if (button == 'ESCAPE' or GetBindingAction(button) == 'TOGGLEGAMEMENU') then
		self:ForceClose()
		return
	elseif self:ParseControllerCommand(button) then
		if not InCombatLockdown() then
			self:SetPropagateKeyboardInput(false)
		end
		return
	elseif self:IsInspectModifier(button) and self.hasItems then
		if not InCombatLockdown() then
			self:SetPropagateKeyboardInput(false)
		end
		self:ShowItems()
		return
	end
	local input
	for action, func in pairs(inputs) do
		-- run through input handlers and check if button matches a configured key.
		if L.cfg[action] == button then
			input = func
			break
		end
	end
	if input then
		input(self)
		if not InCombatLockdown() then
			self:SetPropagateKeyboardInput(false)
		end
	elseif L.cfg.enablenumbers and tonumber(button) then
		inputs.number(self, tonumber(button))
		if not InCombatLockdown() then
			self:SetPropagateKeyboardInput(false)
		end
	else
		if not InCombatLockdown() then
			self:SetPropagateKeyboardInput(true)
		end
	end
end

function Frame:OnKeyUp(button)
	local inspector = self.Inspector
	if ( inspector.ShowFocusedTooltip and ( self:IsInspectModifier(button) or button:match('SHIFT') ) ) then
		inspector:ShowFocusedTooltip(false)
	elseif ( self:IsInspectModifier(button) and inspector:IsVisible() ) then
		inspector:Hide()
	end
end

Frame.OnGamePadButtonDown = Frame.OnKeyDown;
Frame.OnGamePadButtonUp   = Frame.OnKeyUp;

----------------------------------
-- Mixin with scripts
----------------------------------
L.Mixin(L.frame, Frame)