--[[--------------------------------------------------------------------------
	Boom - Modern Healing & Support Raid Frames
	Boom.lua

	Bootstrap: slash commands, the minimap button, the addon compartment entry,
	and the handful of things that only make sense once every other module has
	loaded.

	Loaded last.
----------------------------------------------------------------------------]]

local ADDON_NAME, Boom = ...

local Bootstrap = Boom:NewModule("Bootstrap")

local Util  = Boom.Util
local Skin  = Boom.Skin
local L     = Boom.L

local format, lower, trim = string.format, string.lower, strtrim

--------------------------------------------------------------------------------
-- Slash commands
--------------------------------------------------------------------------------

local function printHelp()
	Boom:Print("|cff00d4ffBoom " .. Boom.VERSION .. "|r")
	local lines = {
		{ "/boom",         L["opens the settings window"] },
		{ "/boom lock",    L["toggles frame lock"] },
		{ "/boom test",    L["toggles test mode"] },
		{ "/boom setup",   L["applies the recommended setup for your specialization"] },
		{ "/boom spells",  L["lists configured spells that could not be found"] },
		{ "/boom errors",  L["reports errors Boom caught and recovered from"] },
		{ "/boom blocked", L["reports captured blocked-action errors"] },
		{ "/boom blizz",   L["reports whether Blizzard's own frames are hidden by Boom"] },
		{ "/boom safemode", L["disables everything that touches Blizzard frames"] },
		{ "/boom lang",    L["switches language"] },
		{ "/boom reset",   L["resets the current profile"] },
	}
	for _, line in ipairs(lines) do
		DEFAULT_CHAT_FRAME:AddMessage(format("   |cff00d4ff%s|r  %s", line[1], line[2]))
	end
end

--- Report the live state of the Blizzard frames Boom can suppress.
---
--- Exists to settle one question that cannot be answered from the code: when the
--- raid manager tab is missing, is Boom hiding it, or is the game itself not
--- showing it for the current group type? "parent: BoomHiddenParent" means us.
--- "parent: UIParent" with shown: false means Blizzard, and no Boom setting will
--- bring it back.
local function reportBlizzardFrames()
	local frames = {
		{ "CompactRaidFrameManager",   _G.CompactRaidFrameManager,   "general.hideBlizzardRaidManager" },
		{ "CompactRaidFrameContainer", _G.CompactRaidFrameContainer, "general.hideBlizzardRaid"        },
		{ "PartyFrame",                _G.PartyFrame,                "general.hideBlizzardParty"       },
		{ "CompactPartyFrame",         _G.CompactPartyFrame,         "general.hideBlizzardParty"       },
	}

	Boom:Print("Blizzard frames:")

	for _, entry in ipairs(frames) do
		local name, frame, path = entry[1], entry[2], entry[3]

		if not frame then
			DEFAULT_CHAT_FRAME:AddMessage(format(
				"   |cff888888%s|r  |cffff8080does not exist yet|r", name))
		else
			local parent = frame:GetParent()
			local parentName = parent and (parent:GetName() or "<unnamed>") or "none"
			local ours = parentName == "BoomHiddenParent"

			DEFAULT_CHAT_FRAME:AddMessage(format(
				"   |cff888888%s|r  shown: %s  visible: %s  parent: %s%s|r  (%s = %s)",
				name,
				tostring(frame:IsShown()),
				tostring(frame:IsVisible()),
				ours and "|cffff8080" or "|cff80ff80",
				parentName,
				path,
				tostring(Boom.Config:Get(path))))
		end
	end

	DEFAULT_CHAT_FRAME:AddMessage("   |cff888888group: " ..
		(IsInRaid() and "raid" or IsInGroup() and "party" or "solo") ..
		"  -  a frame parented to UIParent but not shown is Blizzard's own choice, not Boom's.|r")
end

local function reportBrokenSpells()
	local broken = Boom.ClickCast:GetBrokenBindings()
	local missing = Boom.SpellDB and Boom.SpellDB.GetMissingSpellIDs
		and Boom.SpellDB:GetMissingSpellIDs() or {}

	if #broken == 0 and #missing == 0 then
		Boom:Print(L["All configured spells resolved correctly."])
		return
	end

	Boom:Print(L["The following spells could not be found:"])
	for _, entry in ipairs(broken) do
		DEFAULT_CHAT_FRAME:AddMessage(format("   |cffff6060%s|r -> spell %d",
			Boom.ClickCast:DescribeKey(entry.key), entry.id))
	end
	for _, id in ipairs(missing) do
		DEFAULT_CHAT_FRAME:AddMessage(format("   |cffaaaaaadatabase|r -> spell %d", id))
	end
end

--- Print what the blocked-action watcher captured. The first line of each stack
--- is the one that matters: it names the file and line that reached a protected
--- function.
local function reportBlockedActions()
	local log = Boom.global and Boom.global.blockedLog
	if not log or #log == 0 then
		Boom:Print("No blocked actions recorded. |cff888888(Reproduce the error first, then run this again.)|r")
		return
	end

	Boom:Print(format("%d blocked action(s) recorded:", #log))

	for i = 1, #log do
		local entry = log[i]
		DEFAULT_CHAT_FRAME:AddMessage(format(
			"|cffffd100%d.|r %s  |cff888888(blamed: %s, %s, %s, v%s)|r",
			i, entry.func or "?", entry.addon or "?", entry.event or "?",
			entry.combat and "in combat" or "out of combat", entry.version or "?"))

		-- Stacks are long; show the frames that name a Boom file.
		local shown = 0
		for line in tostring(entry.stack or ""):gmatch("[^\r\n]+") do
			if line:find("Boom", 1, true) and shown < 6 then
				shown = shown + 1
				DEFAULT_CHAT_FRAME:AddMessage("     |cff88ccff" .. line:gsub("^%s+", "") .. "|r")
			end
		end
		if shown == 0 then
			DEFAULT_CHAT_FRAME:AddMessage("     |cff888888(no Boom frame in the stack - taint reached Blizzard code)|r")
		end
	end
end

--- Print everything Boom caught and recovered from. Each entry names the module
--- or control that failed, so a report can be acted on without a reproduction.
local function reportErrors()
	local log = Boom.global and Boom.global.errorLog
	if not log or #log == 0 then
		Boom:Print("No errors recorded.")
		return
	end

	-- The log survives across sessions and upgrades, so most of what it holds is
	-- usually history rather than a live problem. Without saying so, a fixed bug
	-- keeps being reported as current - and the line numbers in an old entry
	-- point at code that no longer exists, which sends the reader somewhere the
	-- fault is not.
	-- Notices are counted apart from errors. A notice means Boom saw something
	-- worth mentioning and carried on working; lumping it into the error total
	-- makes a healthy session look like a broken one.
	local current, old, notices = 0, 0, 0
	for i = 1, #log do
		if log[i].notice then
			notices = notices + 1
		elseif log[i].version == Boom.VERSION then
			current = current + 1
		else
			old = old + 1
		end
	end

	if current == 0 then
		Boom:Print(format(L["No errors from this version. %d older entr(ies) kept below - type /boom errors clear to discard them."], old))
	else
		Boom:Print(format(L["%d error(s) from this version (v%s), %d from older ones."],
			current, Boom.VERSION, old))
	end

	if notices > 0 then
		Boom:Print(format(L["%d notice(s) - things Boom handled. Not errors."], notices))
	end

	for i = 1, #log do
		local entry = log[i]
		local isCurrent = entry.version == Boom.VERSION

		if entry.notice then
			DEFAULT_CHAT_FRAME:AddMessage(format("|cffffd100%d.|r %s |cff888888(v%s) - %s|r",
				i, entry.context or "?", entry.version or "?", L["notice, not an error"]))
			DEFAULT_CHAT_FRAME:AddMessage("     |cffffb020" .. tostring(entry.err) .. "|r")
		else
			DEFAULT_CHAT_FRAME:AddMessage(format("|cffffd100%d.|r %s |cff888888(v%s)%s|r",
				i, entry.context or "?", entry.version or "?",
				isCurrent and "" or (" - " .. L["older version, already replaced"])))
			DEFAULT_CHAT_FRAME:AddMessage("     |cff" ..
				(isCurrent and "ff8080" or "886666") .. tostring(entry.err) .. "|r")
		end
	end
end

--- Turn off every feature that reaches outside Boom's own frames.
---
--- "Forbidden action" errors come from touching something the client does not
--- let addons touch at all. Boom only reaches outside itself in two places -
--- suppressing Blizzard's own raid frames, and anchoring Blizzard's private
--- aura display - so switching both off isolates the cause in one step while
--- leaving every other feature working.
local function enableSafeMode()
	Boom.Config:Set("general.hideBlizzardRaid", false)
	Boom.Config:Set("general.hideBlizzardParty", false)
	Boom.Config:Set("general.hideBlizzardRaidManager", false)
	Boom.Config:Set("indicators.privateAuras.enabled", false)

	Boom:Print("Safe mode on:")
	DEFAULT_CHAT_FRAME:AddMessage("   |cff888888- Blizzard raid/party frame hiding: off|r")
	DEFAULT_CHAT_FRAME:AddMessage("   |cff888888- private aura anchors: off|r")
	Boom:Print("Type |cff00d4ff/reload|r, then check whether the error is gone.")
end

--- Throw the recorded log away, so a fresh reproduction is unambiguous.
local function clearBlockedActions()
	if Boom.global then Boom.global.blockedLog = {} end
	Boom.seenBlocked = nil
	Boom:Print("Blocked-action log cleared.")
end

local function handleSlash(input)
	input = lower(trim(input or ""))
	local command, rest = input:match("^(%S*)%s*(.-)$")

	if command == "" then
		if Boom.Options then Boom.Options:Toggle() end

	elseif command == "lock" or command == "unlock" then
		local locked = not Boom.profile.general.lockFrames
		Boom.Frames:SetLocked(locked)
		Boom:Print(locked and L["Frames locked."] or L["Frames unlocked - drag them into place."])

	elseif command == "test" then
		local enabled = not Boom.profile.general.testMode
		Boom.Config:Set("general.testMode", enabled)
		Boom:Print(enabled and L["Test mode on - these are preview frames."] or L["Test mode off."])

	elseif command == "setup" or command == "wizard" then
		Boom.Presets:ShowWizard()

	elseif command == "spells" then
		reportBrokenSpells()

	elseif command == "blocked" then
		if rest == "clear" then clearBlockedActions() else reportBlockedActions() end

	elseif command == "blizz" or command == "blizzard" then
		reportBlizzardFrames()

	elseif command == "errors" then
		if rest == "clear" then
			if Boom.global then Boom.global.errorLog = {} end
			Boom:Print("Error log cleared.")
		else
			reportErrors()
		end

	elseif command == "safemode" then
		enableSafeMode()

	elseif command == "lang" or command == "dil" then
		local code = (rest == "en" or rest == "enus") and "enUS"
			or (rest == "tr" or rest == "trtr") and "trTR"
			or (Boom.profile.general.locale == "trTR" and "enUS" or "trTR")

		-- Config:Set alone. It raises BOOM_CONFIG_CHANGED, which this module
		-- turns into Boom:SetLocale below - calling SetLocale here as well would
		-- run the whole relocalisation twice, and the settings window would build
		-- a second copy of the current page for nothing.
		Boom.Config:Set("general.locale", code)
		Boom:Print(format(L["Language changed to %s."], Boom.LOCALES[code]))

	elseif command == "reset" then
		Boom.Config:ResetProfile()
		Boom:Print(L["Profile reset."])

	elseif command == "debug" then
		Boom.debugMode = not Boom.debugMode
		Boom.global.debugMode = Boom.debugMode
		Boom:Print("debug: " .. tostring(Boom.debugMode))

	else
		printHelp()
	end
end

--------------------------------------------------------------------------------
-- Minimap button
--
-- Hand rolled rather than pulled in through LibDBIcon, because Boom ships no
-- libraries. It is about sixty lines and behaves the same.
--------------------------------------------------------------------------------

local minimapButton

local function positionMinimapButton()
	if not minimapButton or not Minimap then return end

	local angle = math.rad(Boom.global.minimap.minimapPos or 205)
	local radius = (Minimap:GetWidth() / 2) + 10

	minimapButton:ClearAllPoints()
	minimapButton:SetPoint("CENTER", Minimap, "CENTER",
		math.cos(angle) * radius, math.sin(angle) * radius)
end

local function createMinimapButton()
	if minimapButton or not Minimap then return end

	minimapButton = CreateFrame("Button", "BoomMinimapButton", Minimap)
	minimapButton:SetSize(31, 31)
	minimapButton:SetFrameStrata("MEDIUM")
	minimapButton:SetFrameLevel(8)
	minimapButton:RegisterForClicks("LeftButtonUp", "RightButtonUp")
	minimapButton:RegisterForDrag("LeftButton")

	-- The emblem IS the button. Deliberately no MiniMap-TrackingBorder ring
	-- around it: that ring exists to give a square spell icon a round frame, and
	-- this artwork already is a round frame - ornate metal with four gems. Laying
	-- Blizzard's ring over it produced a ring inside a ring, and the outer one
	-- cropped the gems off the inner one. Dropping it is also what lets the
	-- emblem fill the whole 31px button instead of the ~20px hole in the ring.
	--
	-- No SetTexCoord crop either, for the same reason: that trims the stock
	-- border off a spell icon, and here it would cut the design.
	local icon = minimapButton:CreateTexture(nil, "ARTWORK")
	icon:SetSize(30, 30)
	icon:SetPoint("CENTER", minimapButton, "CENTER", 0, 0)
	icon:SetTexture(Boom.Media.LOGO)

	-- Minimap buttons are expected to light up under the cursor; without this the
	-- only feedback is the tooltip. Sized to the emblem, not the button, so the
	-- glow follows the art rather than framing empty corners.
	local highlight = minimapButton:CreateTexture(nil, "HIGHLIGHT")
	highlight:SetSize(30, 30)
	highlight:SetPoint("CENTER")
	highlight:SetTexture("Interface\\Minimap\\UI-Minimap-ZoomButton-Highlight")
	highlight:SetBlendMode("ADD")

	minimapButton:SetScript("OnEnter", function(self)
		GameTooltip:SetOwner(self, "ANCHOR_LEFT")
		GameTooltip:AddLine("|cff00d4ffBoom|r " .. Boom.VERSION)
		GameTooltip:AddLine(L["Modern Healing & Support Raid Frames"], 0.8, 0.8, 0.8, true)
		GameTooltip:AddLine(" ")
		GameTooltip:AddLine(L["Left Click"] .. ": " .. L["Settings"], 1, 1, 1)
		GameTooltip:AddLine(L["Right Click"] .. ": " .. L["Lock Frames"], 1, 1, 1)
		GameTooltip:Show()
	end)
	minimapButton:SetScript("OnLeave", function() GameTooltip:Hide() end)

	minimapButton:SetScript("OnClick", function(_, button)
		if button == "RightButton" then
			handleSlash("lock")
		else
			if Boom.Options then Boom.Options:Toggle() end
		end
	end)

	-- Dragging orbits the button around the minimap.
	minimapButton:SetScript("OnDragStart", function(self)
		self:SetScript("OnUpdate", function()
			local mx, my = Minimap:GetCenter()
			local cx, cy = GetCursorPosition()
			local scale = Minimap:GetEffectiveScale()
			cx, cy = cx / scale, cy / scale
			Boom.global.minimap.minimapPos = math.deg(math.atan2(cy - my, cx - mx)) % 360
			positionMinimapButton()
		end)
	end)
	minimapButton:SetScript("OnDragStop", function(self)
		self:SetScript("OnUpdate", nil)
	end)

	positionMinimapButton()
end

function Bootstrap:UpdateMinimapButton()
	if Boom.global.minimap.hide then
		if minimapButton then minimapButton:Hide() end
	else
		createMinimapButton()
		if minimapButton then
			minimapButton:Show()
			positionMinimapButton()
		end
	end
end

--------------------------------------------------------------------------------
-- Addon compartment (the dropdown next to the minimap in modern clients)
--------------------------------------------------------------------------------

function _G.Boom_OnAddonCompartmentClick(_, mouseButton)
	if mouseButton == "RightButton" then
		handleSlash("lock")
	else
		if Boom.Options then Boom.Options:Toggle() end
	end
end

--------------------------------------------------------------------------------
-- Lifecycle
--------------------------------------------------------------------------------

function Bootstrap:OnInitialize()
	-- Locale must be applied as early as possible so every other module's
	-- OnInitialize sees the right strings.
	Boom:SetLocale(Boom.profile.general.locale or "enUS")
end

function Bootstrap:OnEnable()
	SLASH_BOOM1 = "/boom"
	SLASH_BOOM2 = "/bm"
	SlashCmdList["BOOM"] = handleSlash

	self:UpdateMinimapButton()

	self:RegisterMessage("BOOM_CONFIG_CHANGED", function(_, _, path, value)
		if path == "general.locale" then
			Boom:SetLocale(value)
		end
	end)

	-- Announce once per new version so the user knows the update landed.
	if Boom.global.lastVersion ~= Boom.VERSION then
		Boom.global.lastVersion = Boom.VERSION
		Boom:After(5, function()
			Boom:Print(L["Boom is loaded. Type /boom for settings."])
		end)
	end
end
