--[[--------------------------------------------------------------------------
	Boom - Modern Healing & Support Raid Frames
	Click/Presets.lua

	"It just works" lives here.

	The usual complaint about a raid frame addon is that a new user faces a wall
	of options before a single spell is bound. Boom inverts that: it reads your
	specialization, looks up which of its spells you actually know, and lays them
	onto the mouse in a sensible order. One click, done. Everything remains fully
	editable afterwards.

	Two rules keep the result safe:
	  * right click always opens the unit menu
	  * middle click always targets
	Those two are reserved before anything else is assigned, so a preset can
	never leave you unable to target a player or open their menu.
----------------------------------------------------------------------------]]

local ADDON_NAME, Boom = ...

local Presets = Boom:NewModule("Presets")

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

local ipairs, pairs, tsort = ipairs, pairs, table.sort

--------------------------------------------------------------------------------
-- Slot order
--
-- Filled in this sequence. Earlier slots are the ones the hand reaches fastest,
-- so the most-used spell of the spec lands on a bare left click.
--------------------------------------------------------------------------------

local SLOT_ORDER = {
	"1",
	"shift-1", "ctrl-1", "alt-1",
	"shift-2", "ctrl-2", "alt-2",
	"4", "5",
	"shift-3", "ctrl-3", "alt-3",
	"shift-4", "ctrl-4",
	"shift-5", "ctrl-5",
}

-- Reserved before any spell is placed.
local RESERVED = {
	["2"] = { type = "menu" },
	["3"] = { type = "target" },
}

--------------------------------------------------------------------------------
-- Category preference per role
--
-- The order a spec's spells are consumed in. A category the spec does not have
-- is simply skipped, so a Restoration Druid and an Augmentation Evoker both get
-- a full, sensible set from the same table.
--------------------------------------------------------------------------------

-- Both lists must name EVERY bindable category, or the specs that own the
-- missing ones quietly lose spells from their preset. purge_offensive and
-- damage are deliberately absent: they target enemies and can never be bound to
-- a friendly unit frame.
local CATEGORY_ORDER = {
	HEALER = {
		"heal_fast", "heal_efficient", "heal_big", "heal_hot", "shield",
		"support_buff", "heal_emergency", "dispel", "external_defensive",
		"heal_aoe", "raid_cooldown", "battle_res", "utility",
	},

	-- DPS and tanks: their support toolkit is the point, so it comes first -
	-- but the ones who also carry a heal or two must still get them bound.
	SUPPORT = {
		"support_buff", "external_defensive", "dispel", "shield",
		"heal_emergency", "heal_fast", "heal_efficient", "heal_hot",
		"heal_aoe", "heal_big", "battle_res", "raid_cooldown", "utility",
	},
}

--------------------------------------------------------------------------------
-- Building
--------------------------------------------------------------------------------

local function roleKeyFor(specID)
	local ClassData = Boom.ClassData
	if ClassData and ClassData.IsHealerSpec and ClassData:IsHealerSpec(specID) then
		return "HEALER"
	end
	return "SUPPORT"
end

--- Ordered list of the spells this character can actually cast right now, as
--- { id, cat, prio, ground } entries with no duplicates.
function Presets:GatherSpells(specID)
	local SpellDB = Boom.SpellDB
	local out, seen = {}, {}
	if not SpellDB or not specID then return out end

	local order = CATEGORY_ORDER[roleKeyFor(specID)]

	for _, category in ipairs(order) do
		local list = SpellDB:GetKnownSpells(specID, category)

		-- Within a category, lower prio number wins.
		tsort(list, function(a, b) return (a.prio or 99) < (b.prio or 99) end)

		for _, entry in ipairs(list) do
			if not seen[entry.id] and Util.GetSpellName(entry.id) then
				seen[entry.id] = true
				out[#out + 1] = entry
			end
		end
	end

	return out
end

--- Produce the complete bindings table a preset would install.
function Presets:BuildForSpec(specID)
	local bindings = {}

	for key, binding in pairs(RESERVED) do
		bindings[key] = { type = binding.type }
	end

	local spells = self:GatherSpells(specID)
	local slot = 1

	for _, entry in ipairs(spells) do
		local key = SLOT_ORDER[slot]
		if not key then break end

		bindings[key] = {
			type   = "spell",
			id     = entry.id,
			ground = entry.ground or false,
		}
		slot = slot + 1
	end

	-- Some specs genuinely have nothing to cast on a friendly player - all three
	-- Demon Hunter specs, for instance, whose whole kit is self-cast or aimed at
	-- enemies. Left click would still target, but only by falling through to the
	-- header's "*type1" wildcard. Say it explicitly instead, so the binding list
	-- shows the user what their left click does rather than looking empty.
	if slot == 1 then
		bindings["1"] = { type = "target" }
	end

	return bindings
end

--- How many actual spells a spec's preset would bind. Lets the wizard explain
--- itself when the answer is zero rather than presenting a bare list.
function Presets:CountSpells(specID)
	local n = 0
	for _, binding in pairs(self:BuildForSpec(specID)) do
		if binding.type == "spell" then n = n + 1 end
	end
	return n
end

--- Human readable preview, for the wizard and the options panel.
--- Returns array of { key, keyLabel, spellName, icon }
function Presets:Preview(specID)
	local bindings = self:BuildForSpec(specID)
	local rows = {}

	local ordered = { "1", "2", "3" }
	for _, key in ipairs(SLOT_ORDER) do
		if key ~= "1" then ordered[#ordered + 1] = key end
	end

	for _, key in ipairs(ordered) do
		local binding = bindings[key]
		if binding then
			rows[#rows + 1] = {
				key      = key,
				keyLabel = Boom.ClickCast:DescribeKey(key),
				text     = Boom.ClickCast:DescribeBinding(binding),
				icon     = binding.type == "spell" and Util.GetSpellIcon(binding.id) or nil,
			}
		end
	end

	return rows
end

--------------------------------------------------------------------------------
-- Applying
--------------------------------------------------------------------------------

function Presets:Apply(specID)
	specID = specID or Boom.Config:GetPlayerSpecID()
	if not specID then
		Boom:Print(L["Could not detect your specialization."])
		return false
	end

	local bindings = self:BuildForSpec(specID)

	local store = Boom.profile.clickcast.bindings
	wipe(store)
	for key, binding in pairs(bindings) do
		store[key] = binding
	end

	Boom.global.presetApplied = Boom.global.presetApplied or {}
	Boom.global.presetApplied[specID] = true

	Boom:SendMessage("BOOM_BINDINGS_CHANGED")

	local specName = Boom.Config:GetPlayerSpecName() or ""
	Boom:Print(format(L["Recommended bindings applied for %s."], specName))
	return true
end

function Presets:HasRunFor(specID)
	return Boom.global.presetApplied and Boom.global.presetApplied[specID] or false
end

--------------------------------------------------------------------------------
-- First-run wizard
--------------------------------------------------------------------------------

local wizard

local function buildWizard()
	if wizard then return wizard end

	-- Wide enough for a portrait beside the binding list. The portrait is the
	-- first thing the player sees, so it gets real space rather than being
	-- squeezed in as decoration.
	local PORTRAIT_W = 196
	local GUTTER = 16

	wizard = Skin:CreatePanel(UIParent, {
		name = "BoomWizard",
		title = L["Welcome to Boom"],
		icon = Boom.Media.LOGO,
		width = 700,
		-- 470 of content plus the 22 the emblem header added, so the binding
		-- preview keeps the room it had.
		height = 492,
		closeButton = true,
	})
	wizard:SetPoint("CENTER")
	wizard:SetFrameStrata("DIALOG")
	wizard:Hide()
	tinsert(UISpecialFrames, "BoomWizard")

	-- Everything in the body hangs below the title bar, whose height depends on
	-- whether the panel carries an emblem. Read it rather than assuming.
	local BODY_TOP = -(wizard.titleBarHeight + 10)

	------------------------------------------------------------------ portrait
	local portrait = Boom.SpecArt:CreatePortrait(wizard, { width = PORTRAIT_W })
	portrait:SetPoint("TOPLEFT", wizard, "TOPLEFT", GUTTER, BODY_TOP)
	wizard.portrait = portrait

	-- Everything textual starts after the portrait when there is one, and at the
	-- left margin when there is not.
	local textLeft = GUTTER + PORTRAIT_W + GUTTER

	local intro = Skin:CreateFontString(wizard, "OVERLAY", 13, "NONE", "LEFT")
	intro:SetPoint("TOPLEFT", wizard, "TOPLEFT", textLeft, BODY_TOP - 2)
	intro:SetPoint("RIGHT", wizard, "RIGHT", -GUTTER, 0)
	intro:SetTextColor(Util.Unpack(Skin.colors.text))
	wizard.intro = intro

	local spec = Skin:CreateFontString(wizard, "OVERLAY", 16, "OUTLINE", "LEFT")
	spec:SetPoint("TOPLEFT", intro, "BOTTOMLEFT", 0, -10)
	spec:SetTextColor(Skin:Accent())
	wizard.spec = spec

	local hint = Skin:CreateFontString(wizard, "OVERLAY", 11, "NONE", "LEFT")
	hint:SetPoint("TOPLEFT", spec, "BOTTOMLEFT", 0, -8)
	hint:SetPoint("RIGHT", wizard, "RIGHT", -GUTTER, 0)
	hint:SetText(L["This binds your most used healing and support spells to the mouse buttons."])
	hint:SetTextColor(Util.Unpack(Skin.colors.textDim))
	hint:SetHeight(30)
	wizard.hint = hint

	-- Preview of exactly what will be bound. Showing the user the outcome before
	-- they commit is the whole difference between a wizard that feels helpful
	-- and one that feels like it took something away.
	local list = CreateFrame("Frame", nil, wizard)
	list:SetPoint("TOPLEFT", hint, "BOTTOMLEFT", 0, -8)
	list:SetPoint("BOTTOMRIGHT", wizard, "BOTTOMRIGHT", -GUTTER, 64)
	Skin:CreateBackdrop(list, { color = Skin.colors.bgDeep, borderColor = Skin.colors.border })
	list.rows = {}
	wizard.list = list

	--- Reclaim the portrait's space when there is no artwork for this spec (or
	--- the user switched portraits off), so the wizard never shows a gap. Called
	--- from ShowWizard once the spec is known.
	function wizard:LayoutForPortrait(hasArt)
		local left = hasArt and textLeft or GUTTER
		intro:ClearAllPoints()
		intro:SetPoint("TOPLEFT", self, "TOPLEFT", left, BODY_TOP - 2)
		intro:SetPoint("RIGHT", self, "RIGHT", -GUTTER, 0)

		self.apply:ClearAllPoints()
		self.apply:SetPoint("BOTTOMLEFT", left, 20)

		self:SetWidth(hasArt and 700 or 480)
	end

	local apply = CreateFrame("Button", nil, wizard)
	apply:SetSize(210, 30)
	apply:SetPoint("BOTTOMLEFT", textLeft, 20)
	Skin:CreateBackdrop(apply, { color = { 0, 0.55, 0.68, 1 }, borderColor = Skin.colors.borderLit })
	local applyText = Skin:CreateFontString(apply, "OVERLAY", 13, "OUTLINE")
	applyText:SetPoint("CENTER")
	apply:SetScript("OnEnter", function(self) self.boomFill:SetVertexColor(0, 0.72, 0.88, 1) end)
	apply:SetScript("OnLeave", function(self) self.boomFill:SetVertexColor(0, 0.55, 0.68, 1) end)
	apply:SetScript("OnClick", function()
		Presets:Apply()
		wizard:Hide()
		if Boom.Options then Boom.Options:Open("clickcast") end
	end)
	wizard.apply = apply

	local skip = CreateFrame("Button", nil, wizard)
	skip:SetSize(190, 30)
	skip:SetPoint("BOTTOMRIGHT", -16, 20)
	local skipText = Skin:CreateFontString(skip, "OVERLAY", 12, "NONE")
	skipText:SetPoint("CENTER")
	skipText:SetTextColor(Util.Unpack(Skin.colors.textDim))
	skip:SetScript("OnEnter", function() skipText:SetTextColor(Util.Unpack(Skin.colors.text)) end)
	skip:SetScript("OnLeave", function() skipText:SetTextColor(Util.Unpack(Skin.colors.textDim)) end)
	skip:SetScript("OnClick", function()
		wizard:Hide()
		if Boom.Options then Boom.Options:Open() end
	end)

	local footer = Skin:CreateFontString(wizard, "OVERLAY", 10, "NONE")
	footer:SetPoint("BOTTOM", 0, 6)
	footer:SetTextColor(Util.Unpack(Skin.colors.textFaint))
	wizard.footer = footer

	wizard.applyText = applyText
	wizard.skipText  = skipText

	--- Re-resolve every fixed string on the wizard.
	---
	--- Called from ShowWizard rather than from a language-change handler: the
	--- wizard is built once and shown rarely, so the cheapest place to be correct
	--- is the moment before it appears. Without this it kept whichever language
	--- was active the first time it was built - which, since it is built on first
	--- login, is the one the user is most likely to be about to change.
	function wizard:Relocalize()
		if self.titleBar and self.titleBar.title then
			self.titleBar.title:SetText(L["Welcome to Boom"])
		end
		self.intro:SetText(L["Boom is ready. Pick a setup and you are done."])
		self.applyText:SetText(L["Use Recommended Setup"])
		self.skipText:SetText(L["I'll set it up myself"])
		self.footer:SetText(L["Open settings any time with /boom"])
	end

	wizard:Relocalize()
	return wizard
end

local function fillWizardList()
	local list = wizard.list
	local rows = Presets:Preview(Boom.Config:GetPlayerSpecID())

	for i, data in ipairs(rows) do
		local row = list.rows[i]
		if not row then
			row = CreateFrame("Frame", nil, list)
			row:SetHeight(22)
			row:SetPoint("LEFT", list, "LEFT", 6, 0)
			row:SetPoint("RIGHT", list, "RIGHT", -6, 0)

			row.icon = row:CreateTexture(nil, "ARTWORK")
			row.icon:SetSize(16, 16)
			row.icon:SetPoint("LEFT", 0, 0)
			row.icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)

			row.key = Skin:CreateFontString(row, "OVERLAY", 11, "NONE", "LEFT")
			row.key:SetPoint("LEFT", row.icon, "RIGHT", 6, 0)
			row.key:SetWidth(110)
			row.key:SetTextColor(Util.Unpack(Skin.colors.textDim))

			row.text = Skin:CreateFontString(row, "OVERLAY", 12, "NONE", "LEFT")
			row.text:SetPoint("LEFT", row.key, "RIGHT", 6, 0)
			row.text:SetPoint("RIGHT", row, "RIGHT", 0, 0)

			list.rows[i] = row
		end

		row:SetPoint("TOPLEFT", list, "TOPLEFT", 6, -6 - (i - 1) * 22)
		row.icon:SetTexture(data.icon or "")
		row.icon:SetShown(data.icon ~= nil)
		row.key:SetText(data.keyLabel)
		row.text:SetText(data.text)
		row:Show()
	end

	for i = #rows + 1, #list.rows do
		list.rows[i]:Hide()
	end
end

function Presets:ShowWizard()
	buildWizard()
	wizard:Relocalize()

	local specID   = Boom.Config:GetPlayerSpecID()
	local specName = Boom.Config:GetPlayerSpecName()
	local _, class = UnitClass("player")
	local classColor = class and ("|cff" .. Util.ToHex(Util.GetClassColorByToken(class))) or ""

	wizard.spec:SetText(format(L["Detected: %s"],
		classColor .. (specName or "?") .. "|r"))

	-- The portrait decides the layout: with art the wizard is a two-column
	-- panel, without it a plain one.
	wizard:LayoutForPortrait(wizard.portrait:SetSpec(specID))

	-- Be honest when there is nothing to bind. A Demon Hunter has no spell that
	-- targets a friendly player, so an unexplained near-empty list reads as the
	-- addon having failed to detect anything.
	if specID and Presets:CountSpells(specID) == 0 then
		wizard.hint:SetText(L["This specialization has no spells that target a friendly player, so the frames are set up for targeting instead."])
	else
		wizard.hint:SetText(L["This binds your most used healing and support spells to the mouse buttons."])
	end

	fillWizardList()
	wizard:Show()
end

function Presets:HideWizard()
	if wizard then wizard:Hide() end
end

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

function Presets:OnEnable()
	-- Spellbook data is not reliably available the instant we log in, so give
	-- the client a moment before deciding what the player knows.
	Boom:After(3, function()
		if Boom.global.firstRun then
			Boom.global.firstRun = false
			Presets:ShowWizard()
		end
	end)
end
