-- Portal Pro :: UI/Toast.lua
-- Transient lead alert. Independently positioned from the main window, because the
-- whole point is to see it while the main window is closed.
--
-- Never shown during combat. A popup you can't safely read is worse than no popup, so
-- leads that arrive mid-fight are held and replayed the moment you drop out of combat.
-- The alert sound still plays immediately — that's the part that's useful while fighting.

local ADDON_NAME, PP = ...
local UI, T, U = PP.UI, PP.Theme, PP.U
local L = PP.L

local Toast = PP:NewModule("Toast")
PP.Toast = Toast

local DISPLAY_TIME = 12

local frame
local deferred = {}   -- lead IDs held back while in combat, oldest first

--------------------------------------------------------------------------------
-- Combat state
--------------------------------------------------------------------------------

local function inCombat()
	-- InCombatLockdown covers the protected-action window; UnitAffectingCombat is the
	-- broader "still swinging" state. Either one means don't put a popup on screen.
	return InCombatLockdown() or UnitAffectingCombat("player") or false
end

--------------------------------------------------------------------------------
-- Frame
--------------------------------------------------------------------------------

local function build()
	if frame then return frame end

	frame = UI:Panel(UIParent, "overlay")
	frame:SetSize(300, 92)
	frame:SetFrameStrata("HIGH")
	frame:Hide()
	UI:Border(frame, "accent")
	UI:Shadow(frame, 6)

	UI:RestorePosition(frame, PP.profile.ui, "toastPoint",
		{ "TOP", "UIParent", "TOP", 0, -200 })
	UI:MakeDraggable(frame, frame, function(f)
		UI:SavePosition(f, PP.profile.ui, "toastPoint")
	end)

	frame.icon = frame:CreateTexture(nil, "ARTWORK")
	frame.icon:SetSize(28, 28)
	frame.icon:SetPoint("TOPLEFT", T.space.md, -T.space.md)
	frame.icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)

	frame.name = UI:Text(frame, "", T.fontSize.md, "", "textPrimary")
	frame.name:SetPoint("TOPLEFT", frame.icon, "TOPRIGHT", T.space.sm, 0)

	frame.dest = UI:Text(frame, "", T.fontSize.sm, "", "accent")
	frame.dest:SetPoint("TOPLEFT", frame.name, "BOTTOMLEFT", 0, -2)

	frame.message = UI:Text(frame, "", T.fontSize.sm, "", "textSecondary")
	frame.message:SetPoint("TOPLEFT", frame.icon, "BOTTOMLEFT", 0, -T.space.sm)
	frame.message:SetPoint("RIGHT", frame, "RIGHT", -T.space.md, 0)

	local dismiss = UI:Button(frame, "X", function() Toast:Hide() end, "ghost", 22, 22)
	dismiss:SetPoint("TOPRIGHT", -T.space.xs, -T.space.xs)

	frame.inviteBtn = UI:Button(frame, L["Invite"], function()
		if frame.lead then PP.Leads:Invite(frame.lead) end
		Toast:Next()
	end, "primary", 62, 22)
	frame.inviteBtn:SetPoint("BOTTOMRIGHT", -T.space.md, T.space.sm)

	frame.whisperBtn = UI:Button(frame, L["Whisper"], function()
		if frame.lead then PP.Leads:Whisper(frame.lead) end
		Toast:Next()
	end, "secondary", 68, 22)
	frame.whisperBtn:SetPoint("RIGHT", frame.inviteBtn, "LEFT", -T.space.xs, 0)

	-- "held while you were fighting" indicator
	frame.extra = UI:Text(frame, "", T.fontSize.xs, "", "warning")
	frame.extra:SetPoint("BOTTOMLEFT", T.space.md, T.space.md)
	frame.extra:SetPoint("RIGHT", frame.whisperBtn, "LEFT", -T.space.sm, 0)

	frame.countdown = frame:CreateTexture(nil, "OVERLAY")
	frame.countdown:SetHeight(2)
	frame.countdown:SetPoint("BOTTOMLEFT")
	frame.countdown:SetColorTexture(T:C("accent"))

	return frame
end

--------------------------------------------------------------------------------
-- Deferral queue
--------------------------------------------------------------------------------

local function defer(lead)
	for _, id in ipairs(deferred) do
		if id == lead.id then return end
	end
	deferred[#deferred + 1] = lead.id
	PP:Debug("toast deferred (in combat):", lead.name)
end

--- Deferred IDs resolved back to leads, dropping any that expired or were handled while
--- we were fighting. Holding IDs rather than lead tables is what makes that check possible.
local function liveDeferred()
	local out = {}
	for _, id in ipairs(deferred) do
		local lead = PP.Leads:Get(id)
		if lead then out[#out + 1] = lead end
	end
	return out
end

--------------------------------------------------------------------------------
-- Show / hide
--------------------------------------------------------------------------------

--- @param extraCount number|nil how many other leads are still queued behind this one
function Toast:Show(lead, extraCount)
	if not PP.profile.detection.toast then return end
	if not lead then return end

	if inCombat() then
		defer(lead)
		return
	end

	build()

	frame.lead = lead
	frame.name:SetText(U.ClassColorHex(lead.class) .. lead.name .. "|r")

	if lead.dest then
		frame.icon:SetTexture(PP.Dest:Icon(lead.dest))
		frame.icon:SetDesaturated(false)
		frame.dest:SetText(("%s · %s"):format(lead.dest.name, U.MoneyPlain(PP.Dest:Price(lead.dest))))
	else
		frame.icon:SetTexture(134400)
		frame.icon:SetDesaturated(true)
		frame.dest:SetText(L["Unknown destination"])
	end

	frame.message:SetText(U.Truncate(lead.message or "", 58))
	frame:SetBorderColor(lead.vip and "gold" or "accent")

	if extraCount and extraCount > 0 then
		frame.extra:SetText(("+%d more waiting"):format(extraCount))
	else
		frame.extra:SetText("")
	end

	frame:Show()
	UI:Fade(frame, 0, 1, 0.15)
	UI:SlideIn(frame, 0, -8, 0.18)

	-- Countdown bar drains over the display window.
	local width = frame:GetWidth()
	frame.countdown:SetWidth(width)
	local started = GetTime()
	frame:SetScript("OnUpdate", function(self)
		-- Combat starting while the toast is up: pull it and hold the lead for later.
		if inCombat() then
			self:SetScript("OnUpdate", nil)
			if self.lead then defer(self.lead) end
			Toast:Hide()
			return
		end
		local remaining = 1 - ((GetTime() - started) / DISPLAY_TIME)
		if remaining <= 0 then
			self:SetScript("OnUpdate", nil)
			Toast:Hide()
			return
		end
		self.countdown:SetWidth(width * remaining)
	end)
end

function Toast:Hide()
	if not frame or not frame:IsShown() then return end
	frame:SetScript("OnUpdate", nil)
	UI:FadeOut(frame, 0.15)
end

--- Dismiss the current toast and immediately surface the next held lead, if any.
function Toast:Next()
	local current = frame and frame.lead
	if current then
		for i = #deferred, 1, -1 do
			if deferred[i] == current.id then table.remove(deferred, i) end
		end
	end
	self:Hide()

	local queued = liveDeferred()
	if #queued == 0 then return end
	-- Small delay so the outgoing fade completes before the next one slides in.
	C_Timer.After(0.18, function() Toast:Flush() end)
end

--- Show the most recent held lead and report how many others are behind it.
function Toast:Flush()
	if inCombat() then return end

	local queued = liveDeferred()
	wipe(deferred)

	if #queued == 0 then return end

	-- Newest first: if several came in during a long fight, the freshest request is the
	-- one most likely to still be worth answering.
	local newest = queued[#queued]
	for i = 1, #queued - 1 do
		deferred[#deferred + 1] = queued[i].id
	end

	Toast:Show(newest, #queued - 1)
end

function Toast:DeferredCount()
	return #liveDeferred()
end

--------------------------------------------------------------------------------
-- Wiring
--------------------------------------------------------------------------------

function Toast:OnEnable()
	PP:On("LEAD_ADDED", function(lead)
		-- Don't cover the main window when it's already showing the queue.
		if PP.UI.main and PP.UI.main:IsShown() and PP.UI.currentTab == "leads" then return end
		Toast:Show(lead)
	end)

	PP:On("LEAD_REMOVED", function(lead)
		for i = #deferred, 1, -1 do
			if deferred[i] == lead.id then table.remove(deferred, i) end
		end
		if frame and frame.lead and frame.lead.id == lead.id then Toast:Hide() end
	end)

	PP:RegisterEvent("PLAYER_REGEN_DISABLED", function()
		-- Entering combat: clear the screen, keep the lead.
		if frame and frame:IsShown() then
			if frame.lead then defer(frame.lead) end
			Toast:Hide()
		end
	end)

	PP:RegisterEvent("PLAYER_REGEN_ENABLED", function()
		-- Out of combat. Small delay: PLAYER_REGEN_ENABLED can fire a moment before the
		-- fight is really over, and re-entering combat immediately would just hide it again.
		C_Timer.After(1.0, function() Toast:Flush() end)
	end)
end
