-- Portal Pro :: UI/MainFrame.lua
-- The shell: header strip, icon rail, content router. Tabs register themselves and are
-- built lazily on first open.

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

UI.tabs = {}

--- @param def table { key, label, icon, order, build = fun(parent):Frame, refresh = fun(frame) }
function UI:RegisterTab(def)
	table.insert(self.tabs, def)
	table.sort(self.tabs, function(a, b) return (a.order or 99) < (b.order or 99) end)
end

--------------------------------------------------------------------------------
-- Compat: resize bounds moved API in 10.0
--------------------------------------------------------------------------------

local function setResizeBounds(frame, minW, minH, maxW, maxH)
	if frame.SetResizeBounds then
		pcall(frame.SetResizeBounds, frame, minW, minH, maxW, maxH)
	else
		if frame.SetMinResize then pcall(frame.SetMinResize, frame, minW, minH) end
		if frame.SetMaxResize then pcall(frame.SetMaxResize, frame, maxW, maxH) end
	end
end

--------------------------------------------------------------------------------
-- Header
--------------------------------------------------------------------------------

local function buildChip(parent, labelText)
	local chip = CreateFrame("Frame", nil, parent)
	-- Wide enough for a full "12g34s56c" balance rather than a rounded "12.3g", and no
	-- wider: three chips plus the brand and the Broadcast controls have to coexist in the
	-- header at the minimum window width.
	chip:SetSize(92, 32)

	local label = UI:Label(chip, labelText)
	label:SetPoint("TOPLEFT")

	local value = UI:Text(chip, "-", T.fontSize.md, "", "textPrimary")
	value:SetPoint("BOTTOMLEFT", 0, 1)

	chip.valueText = value
	function chip:Set(text, colorName)
		self.valueText:SetText(text)
		self.valueText:SetTextColor(T:C(colorName or "textPrimary"))
	end
	return chip
end

local function buildHeader(main)
	local header = UI:Panel(main, "raised")
	header:SetPoint("TOPLEFT")
	header:SetPoint("TOPRIGHT")
	header:SetHeight(T.size.headerHeight)

	local underline = UI:Divider(header)
	underline:SetPoint("BOTTOMLEFT")
	underline:SetPoint("BOTTOMRIGHT")

	-- Brand
	local logo = header:CreateTexture(nil, "ARTWORK")
	logo:SetSize(22, 22)
	logo:SetPoint("LEFT", T.space.md, 0)
	logo:SetTexture("Interface\\ICONS\\Spell_Arcane_PortalStormWind")
	logo:SetTexCoord(0.08, 0.92, 0.08, 0.92)

	local title = UI:Text(header, L["Portal Pro"], T.fontSize.md, "", "textPrimary")
	title:SetPoint("LEFT", logo, "RIGHT", T.space.sm, 0)

	-- Stat chips
	main.chips = {}
	local anchor = title
	for _, spec in ipairs({
		{ key = "net",     label = L["Session"] .. " " .. L["Net"] },
		{ key = "portals", label = L["Portals"] },
		{ key = "runes",   label = L["Runes"] },
	}) do
		local chip = buildChip(header, spec.label)
		chip:SetPoint("LEFT", anchor, "RIGHT", T.space.lg, 0)
		main.chips[spec.key] = chip
		anchor = chip
	end

	-- Close
	local close = UI:Button(header, "X", function() UI:Hide() end, "ghost", 28, 28)
	close:SetPoint("RIGHT", -T.space.sm, 0)

	-- Compact-mode switch
	local compact = UI:Button(header, "_", function()
		PP.profile.ui.compact = true
		UI:Hide()
		PP.CompactBar:Show()
	end, "ghost", 28, 28)
	compact:SetPoint("RIGHT", close, "LEFT", -2, 0)
	UI:Tooltip(compact, "Compact mode", { "Collapse to the slim header bar." })

	-- Broadcast
	local broadcast = UI:Button(header, L["Broadcast"], function(self, button)
		if button == "RightButton" then
			PP.Broadcaster:ToggleArm()
		else
			-- This OnClick IS the hardware event that makes a CHANNEL send legal.
			PP.Broadcaster:Broadcast()
		end
	end, "primary", 110, 28)
	broadcast:RegisterForClicks("LeftButtonUp", "RightButtonUp")
	broadcast:SetPoint("RIGHT", compact, "LEFT", -T.space.md, 0)
	main.broadcastButton = broadcast

	UI:Tooltip(broadcast, L["Broadcast"], function()
		return {
			"Left-click: post the current ad now.",
			"Right-click: arm it - the next key you press sends it.",
			" ",
			"Trade/General/Say/Yell are protected by Blizzard and can only be",
			"sent from a real click or keypress. There is no AFK timer for this.",
		}
	end)

	-- Cooldown progress under the button
	local cdTrack = header:CreateTexture(nil, "ARTWORK")
	cdTrack:SetHeight(2)
	cdTrack:SetPoint("BOTTOMLEFT", broadcast, "BOTTOMLEFT", 0, -3)
	cdTrack:SetPoint("BOTTOMRIGHT", broadcast, "BOTTOMRIGHT", 0, -3)
	cdTrack:SetColorTexture(T:C("overlay"))

	local cdFill = header:CreateTexture(nil, "OVERLAY")
	cdFill:SetHeight(2)
	cdFill:SetPoint("BOTTOMLEFT", cdTrack, "BOTTOMLEFT")
	cdFill:SetColorTexture(T:C("accent"))
	main.cdTrack, main.cdFill = cdTrack, cdFill

	-- Tracking-suspended badge.
	--
	-- Sits under the title, where the space either side is fixed and can't be squeezed by
	-- the stat chips. The original bug was not the position but the absence of a width
	-- bound combined with a long reason string: "IN INSTANCE - NOT LOGGING" is roughly
	-- 150px and ran straight under the Session Net figure. Fixed short text plus an
	-- explicit width means it can never outgrow its slot; the reason lives in the tooltip.
	local freeBadge = UI:Text(header, "NOT LOGGING", T.fontSize.xs, "", "warning")
	freeBadge:SetPoint("TOPLEFT", title, "BOTTOMLEFT", 0, -2)
	freeBadge:SetWidth(96)
	freeBadge:Hide()
	main.freeBadge = freeBadge
	main.titleText = title
	main.logoTexture = logo

	UI:Tooltip(freeBadge, "Not logging portals", function()
		local reasons = {}
		if PP.CastTracker.freeMode then
			reasons[#reasons + 1] = "Free-cast mode is on (/pp free)."
		end
		if PP.profile.tracking.ignoreInInstances and IsInInstance and IsInInstance() then
			reasons[#reasons + 1] = "You're in a dungeon or raid."
		end
		reasons[#reasons + 1] = " "
		reasons[#reasons + 1] = "Portals cast now won't be recorded and their runes won't"
		reasons[#reasons + 1] = "be charged against your margin."
		return reasons
	end)

	main.header = header
	return header
end

--------------------------------------------------------------------------------
-- Rail
--------------------------------------------------------------------------------

local function buildRail(main)
	local rail = UI:Panel(main, "raised")
	rail:SetPoint("TOPLEFT", main.header, "BOTTOMLEFT")
	rail:SetPoint("BOTTOMLEFT")
	rail:SetWidth(T.size.railWidth)

	local edge = UI:Divider(rail, true)
	edge:SetPoint("TOPRIGHT")
	edge:SetPoint("BOTTOMRIGHT")

	main.railButtons = {}
	local y = -T.space.sm

	for _, tab in ipairs(UI.tabs) do
		local b = CreateFrame("Button", nil, rail)
		b:SetSize(T.size.railWidth, 44)
		b:SetPoint("TOPLEFT", 0, y)

		local indicator = b:CreateTexture(nil, "OVERLAY")
		indicator:SetSize(2, 24)
		indicator:SetPoint("LEFT")
		indicator:SetColorTexture(T:C("accent"))
		indicator:Hide()

		local icon = b:CreateTexture(nil, "ARTWORK")
		icon:SetSize(20, 20)
		icon:SetPoint("CENTER", 0, 5)
		icon:SetTexture(tab.icon)
		icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
		icon:SetDesaturated(true)

		local label = UI:Text(b, tab.label, T.fontSize.xs, "", "textMuted", "CENTER")
		label:SetPoint("BOTTOM", 0, 4)
		label:SetPoint("LEFT")
		label:SetPoint("RIGHT")

		-- Unread badge, used by the Leads tab.
		local badge = UI:Text(b, "", T.fontSize.xs, "", "accent", "CENTER")
		badge:SetPoint("TOPRIGHT", -6, -4)

		b.tabKey, b.icon, b.label, b.indicator, b.badge = tab.key, icon, label, indicator, badge

		b:SetScript("OnClick", function(self) UI:SelectTab(self.tabKey) end)
		b:SetScript("OnEnter", function(self)
			if PP.UI.currentTab ~= self.tabKey then
				self.label:SetTextColor(T:C("textSecondary"))
				self.icon:SetDesaturated(false)
			end
		end)
		b:SetScript("OnLeave", function(self)
			if PP.UI.currentTab ~= self.tabKey then
				self.label:SetTextColor(T:C("textMuted"))
				self.icon:SetDesaturated(true)
			end
		end)

		main.railButtons[tab.key] = b
		y = y - 46
	end

	main.rail = rail
	return rail
end

--------------------------------------------------------------------------------
-- Shell
--------------------------------------------------------------------------------

function UI:GetMain()
	if self.main then return self.main end

	local ui = PP.profile.ui

	local main = CreateFrame("Frame", "PortalProMainFrame", UIParent)
	main:SetSize(ui.width or 720, ui.height or 480)
	main:SetFrameStrata("MEDIUM")
	main:SetToplevel(true)
	main:Hide()

	UI:Fill(main, "base")
	UI:Border(main, "borderStrong")
	UI:Shadow(main, 6)

	main:SetScale(ui.scale or 1)
	UI:RestorePosition(main, ui, "point")

	main:SetResizable(true)
	-- 700 rather than 560: the header genuinely needs that much to fit the brand, three
	-- stat chips and the Broadcast controls without them overlapping. A minimum the layout
	-- can't honour isn't a minimum, it's a bug you can drag yourself into.
	setResizeBounds(main, 700, 360, 1400, 1000)

	buildHeader(main)
	UI:MakeDraggable(main, main.header, function(frame)
		UI:SavePosition(frame, PP.profile.ui, "point")
	end)

	buildRail(main)

	-- Content host
	local content = CreateFrame("Frame", nil, main)
	content:SetPoint("TOPLEFT", main.rail, "TOPRIGHT", 0, 0)
	content:SetPoint("BOTTOMRIGHT", main, "BOTTOMRIGHT", 0, 0)
	main.content = content
	main.tabFrames = {}

	-- Resize grip
	local grip = CreateFrame("Button", nil, main)
	grip:SetSize(14, 14)
	grip:SetPoint("BOTTOMRIGHT", -2, 2)
	for i = 1, 3 do
		local line = grip:CreateTexture(nil, "OVERLAY")
		line:SetColorTexture(T:C("borderStrong"))
		line:SetSize(2 + (i - 1) * 3, 2)
		line:SetPoint("BOTTOMRIGHT", 0, (i - 1) * 3)
	end
	grip:SetScript("OnMouseDown", function()
		if PP.profile.ui.locked then return end
		main:StartSizing("BOTTOMRIGHT")
	end)
	grip:SetScript("OnMouseUp", function()
		main:StopMovingOrSizing()
		PP.profile.ui.width = math.floor(main:GetWidth() + 0.5)
		PP.profile.ui.height = math.floor(main:GetHeight() + 0.5)
	end)

	-- ESC closes it, like every other window in the game.
	tinsert(UISpecialFrames, "PortalProMainFrame")

	main:SetScript("OnShow", function()
		UI:RefreshHeader()
		UI:RefreshCurrentTab()
	end)

	self.main = main
	return main
end

--------------------------------------------------------------------------------
-- Tab routing
--------------------------------------------------------------------------------

function UI:SelectTab(key)
	local main = self:GetMain()

	for tabKey, frame in pairs(main.tabFrames) do
		frame:Hide()
	end

	local def
	for _, tab in ipairs(self.tabs) do
		if tab.key == key then def = tab break end
	end
	if not def then return end

	local frame = main.tabFrames[key]
	if not frame then
		frame = def.build(main.content)
		frame:SetAllPoints(main.content)
		main.tabFrames[key] = frame
		frame.__def = def
	end

	self.currentTab = key
	frame:Show()
	if def.refresh then def.refresh(frame) end
	UI:Fade(frame, 0, 1, 0.10)

	for tabKey, button in pairs(main.railButtons) do
		local active = (tabKey == key)
		button.indicator:SetShown(active)
		button.icon:SetDesaturated(not active)
		button.label:SetTextColor(T:C(active and "textPrimary" or "textMuted"))
	end
end

function UI:RefreshCurrentTab()
	local main = self.main
	if not main or not self.currentTab then return end
	local frame = main.tabFrames[self.currentTab]
	if frame and frame.__def and frame.__def.refresh then
		frame.__def.refresh(frame)
	end
end

--------------------------------------------------------------------------------
-- Header refresh
--------------------------------------------------------------------------------

function UI:RefreshHeader()
	local main = self.main
	if not main or not main:IsShown() then return end

	local totals = PP.Stats:Totals("session")
	main.chips.net:Set(U.MoneyFull(totals.net, true), totals.net >= 0 and "positive" or "negative")

	if main.freeBadge then
		-- Text is fixed; the reason lives in the tooltip so the badge can't outgrow the
		-- space it was given. Only touch the layout when the state actually flips, since
		-- this runs on a quarter-second ticker.
		local suspended = (PP.CastTracker.freeMode
			or (PP.profile.tracking.ignoreInInstances and IsInInstance and IsInInstance()))
			and true or false

		if suspended ~= main.__badgeShown then
			main.__badgeShown = suspended
			main.freeBadge:SetShown(suspended)
			-- Lift the title to make room for the badge, and re-centre it when it goes.
			main.titleText:ClearAllPoints()
			main.titleText:SetPoint("LEFT", main.logoTexture, "RIGHT", T.space.sm,
				suspended and 7 or 0)
		end
	end

	local portals, teleports = PP.CastTracker:CountSince(PP.Stats:SessionStart())
	main.chips.portals:Set(tostring(portals + teleports))

	local runes = PP.Reagents:PortalCount()
	local runeColor = runes <= (PP.profile.reagents.lowWarning or 5) and "warning" or "textPrimary"
	main.chips.runes:Set(tostring(runes), runeColor)

	-- Broadcast state
	local remaining, throttle = PP.Broadcaster:Cooldown()
	local button = main.broadcastButton
	if PP.Broadcaster:IsArmed() then
		button:SetTextValue(L["ARMED"])
		main.cdFill:SetWidth(main.cdTrack:GetWidth())
		main.cdFill:SetColorTexture(T:C("warning"))
	elseif remaining > 0 then
		button:SetTextValue(("%ds"):format(math.ceil(remaining)))
		local pct = 1 - (remaining / math.max(1, throttle))
		main.cdFill:SetWidth(math.max(1, main.cdTrack:GetWidth() * pct))
		main.cdFill:SetColorTexture(T:C("accent"))
	else
		button:SetTextValue(L["Broadcast"])
		main.cdFill:SetWidth(main.cdTrack:GetWidth())
		main.cdFill:SetColorTexture(T:C("positive"))
	end

	-- Lead count badge on the rail
	local leadsButton = main.railButtons and main.railButtons.leads
	if leadsButton then
		local n = PP.Leads:Count()
		leadsButton.badge:SetText(n > 0 and tostring(n) or "")
	end
end

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

function UI:Show()
	local main = self:GetMain()
	main:Show()
	if not self.currentTab then
		self:SelectTab(self.tabs[1] and self.tabs[1].key or "leads")
	end
	self:RefreshHeader()
end

function UI:Hide()
	if self.main then self.main:Hide() end
end

function UI:Toggle()
	if self.main and self.main:IsShown() then
		self:Hide()
	else
		self:Show()
	end
end

function UI:ApplyScale()
	if not self.main then return end
	self.main:SetScale(PP.profile.ui.scale or 1)
end

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

local UIModule = PP:NewModule("UI")

function UIModule:OnEnable()
	-- Header ticks for the broadcast cooldown; cheap because it early-outs when hidden.
	C_Timer.NewTicker(0.25, function() UI:RefreshHeader() end)

	local function refreshAll()
		UI:RefreshHeader()
		UI:RefreshCurrentTab()
	end

	-- Every event that changes something on screen must be listed here. A module firing an
	-- event nothing subscribes to looks exactly like a dead button: the data changes and
	-- the view never redraws.
	PP:On("LEADS_CHANGED", refreshAll)
	PP:On("LEDGER_CHANGED", refreshAll)
	PP:On("REAGENTS_CHANGED", refreshAll)
	PP:On("ADS_CHANGED", refreshAll)
	-- The library is edited in Settings and consumed by the Ads tab's dropdown, so a change
	-- has to reach a tab that isn't the one you're on.
	PP:On("GREETINGS_CHANGED", refreshAll)
	PP:On("CUSTOMERS_CHANGED", refreshAll)
	PP:On("MARKET_UPDATED", refreshAll)
	PP:On("PRICES_CHANGED", refreshAll)
	PP:On("FREEMODE_CHANGED", refreshAll)
	PP:On("PROFILE_CHANGED", function()
		T:InvalidateFont()
		refreshAll()
	end)

	if PP.profile.ui.compact then
		C_Timer.After(1, function() PP.CompactBar:Show() end)
	end
end
