--[[--------------------------------------------------------------------------
	Boom - Modern Healing & Support Raid Frames
	UI/Skin.lua

	The visual language of the addon, in one place.

	Design rules Boom follows everywhere:
	  * flat fills, no bevels, no gloss except one very faint bar highlight
	  * exactly 1px borders, drawn with textures so they stay crisp at any scale
	  * a single accent colour carries all interactive emphasis
	  * information density over decoration - nothing is drawn that cannot be read
	    at raid-frame size during a pull

	Every widget in Options/ and every frame in UI/ builds from these primitives,
	which is what makes a twenty-file addon look like one product.
----------------------------------------------------------------------------]]

local ADDON_NAME, Boom = ...

local Skin = Boom:NewModule("Skin")

local Util = Boom.Util
local Media = Boom.Media

--------------------------------------------------------------------------------
-- Palette
--------------------------------------------------------------------------------

Skin.colors = {
	-- Surfaces, darkest to lightest.
	shadow    = { 0.00, 0.00, 0.00, 0.55 },
	bgDeep    = { 0.055, 0.063, 0.078, 0.97 },
	bg        = { 0.086, 0.094, 0.114, 0.96 },
	bgRaised  = { 0.118, 0.129, 0.153, 1.00 },
	bgHover   = { 0.160, 0.176, 0.208, 1.00 },

	border    = { 0.169, 0.188, 0.227, 1.00 },
	borderLit = { 0.259, 0.290, 0.345, 1.00 },

	-- Interaction.
	accent    = { 0.000, 0.831, 1.000, 1.00 },
	accentDim = { 0.000, 0.831, 1.000, 0.25 },

	-- Text.
	text      = { 0.902, 0.914, 0.937, 1.00 },
	textDim   = { 0.604, 0.639, 0.698, 1.00 },
	textFaint = { 0.400, 0.427, 0.478, 1.00 },

	-- Semantic.
	good      = { 0.239, 0.863, 0.518, 1.00 },
	warn      = { 1.000, 0.690, 0.180, 1.00 },
	bad       = { 1.000, 0.341, 0.341, 1.00 },
}

--- The accent colour is user-configurable; read it live.
---
--- The `appearance` step is checked separately from `profile`: this is called
--- while widgets are being built, which can happen in the window between a
--- profile existing and its defaults having been back-filled into it.
function Skin:Accent()
	local app = Boom.profile and Boom.profile.appearance
	local c = app and app.accentColor
	if type(c) == "table" then return c[1] or 0, c[2] or 0.83, c[3] or 1, c[4] or 1 end
	return unpack(Skin.colors.accent)
end

local WHITE = "Interface\\Buttons\\WHITE8X8"

--------------------------------------------------------------------------------
-- Backdrops and borders
--------------------------------------------------------------------------------

--- Give a frame a flat fill plus a 1px border, without the BackdropTemplate
--- (which brings tiling artefacts at fractional UI scales).
--- opts = { color = {r,g,b,a}, borderColor = {r,g,b,a}, borderSize = 1, inset = 0 }
function Skin:CreateBackdrop(frame, opts)
	opts = opts or {}

	if not frame.boomFill then
		frame.boomFill = frame:CreateTexture(nil, "BACKGROUND", nil, -7)
		frame.boomFill:SetTexture(WHITE)
	end

	local inset = opts.inset or 0
	frame.boomFill:ClearAllPoints()
	frame.boomFill:SetPoint("TOPLEFT", frame, "TOPLEFT", inset, -inset)
	frame.boomFill:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -inset, inset)
	frame.boomFill:SetVertexColor(Util.Unpack(opts.color or Skin.colors.bg))

	if opts.borderSize ~= 0 then
		if not frame.boomBorder then
			frame.boomBorder = Util.CreateBorder(frame, "BORDER")
		end
		frame.boomBorder:SetSize(opts.borderSize or 1)
		frame.boomBorder:SetColor(Util.Unpack(opts.borderColor or Skin.colors.border))
		frame.boomBorder:Show()
	elseif frame.boomBorder then
		frame.boomBorder:Hide()
	end

	return frame
end

--- Soft drop shadow behind a frame. Uses a plain dark texture bled outwards;
--- at raid-frame sizes this reads better than a real blurred shadow and costs
--- one texture instead of nine.
function Skin:CreateShadow(frame, size, alpha)
	size = size or 3
	local shadow = CreateFrame("Frame", nil, frame)
	shadow:SetFrameLevel(math.max(0, frame:GetFrameLevel() - 1))
	shadow:SetPoint("TOPLEFT", frame, "TOPLEFT", -size, size)
	shadow:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", size, -size)

	local tex = shadow:CreateTexture(nil, "BACKGROUND", nil, -8)
	tex:SetAllPoints()
	tex:SetTexture(WHITE)
	tex:SetVertexColor(0, 0, 0, alpha or 0.45)

	shadow.texture = tex
	return shadow
end

--------------------------------------------------------------------------------
-- Gradients
--------------------------------------------------------------------------------

--- Apply a two-stop gradient to a texture across API generations.
function Skin:SetGradient(texture, orientation, r1, g1, b1, a1, r2, g2, b2, a2)
	if texture.SetGradient and CreateColor then
		texture:SetGradient(orientation or "VERTICAL",
			CreateColor(r1, g1, b1, a1), CreateColor(r2, g2, b2, a2))
	elseif texture.SetGradientAlpha then
		texture:SetGradientAlpha(orientation or "VERTICAL", r1, g1, b1, a1, r2, g2, b2, a2)
	end
end

--- The faint top-to-bottom sheen laid over health bars.
function Skin:CreateGloss(parent, alpha)
	local gloss = parent:CreateTexture(nil, "ARTWORK", nil, 2)
	gloss:SetTexture(WHITE)
	gloss:SetAllPoints()
	Skin:SetGradient(gloss, "VERTICAL", 0, 0, 0, 0, 1, 1, 1, alpha or 0.16)
	return gloss
end

--------------------------------------------------------------------------------
-- Glow (used for dispellable debuffs and expiring support buffs)
--------------------------------------------------------------------------------

function Skin:CreateGlow(frame, thickness)
	thickness = thickness or 4

	local glow = CreateFrame("Frame", nil, frame)
	glow:SetPoint("TOPLEFT", frame, "TOPLEFT", -thickness, thickness)
	glow:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", thickness, -thickness)
	glow:SetFrameLevel(frame:GetFrameLevel() + 1)
	glow:Hide()

	local tex = glow:CreateTexture(nil, "OVERLAY")
	tex:SetAllPoints()
	tex:SetTexture("Interface\\SpellActivationOverlay\\IconAlert")
	tex:SetTexCoord(0.00781250, 0.50781250, 0.27734375, 0.52734375)
	tex:SetBlendMode("ADD")
	glow.texture = tex

	local anim = glow:CreateAnimationGroup()
	anim:SetLooping("BOUNCE")
	local pulse = anim:CreateAnimation("Alpha")
	pulse:SetFromAlpha(1)
	pulse:SetToAlpha(0.35)
	pulse:SetDuration(0.55)
	glow.anim = anim

	function glow:SetColor(r, g, b)
		tex:SetVertexColor(r, g, b)
	end

	function glow:Start()
		self:Show()
		if not self.anim:IsPlaying() then self.anim:Play() end
	end

	function glow:Stop()
		if self.anim:IsPlaying() then self.anim:Stop() end
		self:Hide()
	end

	return glow
end

--------------------------------------------------------------------------------
-- Building blocks
--------------------------------------------------------------------------------

function Skin:CreateStatusBar(parent, layer)
	local bar = CreateFrame("StatusBar", nil, parent)
	bar:SetStatusBarTexture(Media:GetBarTexture())
	bar:SetMinMaxValues(0, 1)
	bar:SetValue(1)

	local tex = bar:GetStatusBarTexture()
	if tex and layer then tex:SetDrawLayer(layer) end

	bar.bg = bar:CreateTexture(nil, "BACKGROUND")
	bar.bg:SetAllPoints()
	bar.bg:SetTexture(Media:GetBackgroundTexture())
	bar.bg:SetVertexColor(Util.Unpack(Skin.colors.bgDeep))

	return bar
end

function Skin:CreateFontString(parent, layer, size, outline, justify)
	local fs = parent:CreateFontString(nil, layer or "OVERLAY")
	Util.SetFont(fs, Media:GetFont(), size or 12, outline or "OUTLINE")
	fs:SetTextColor(Util.Unpack(Skin.colors.text))
	if justify then fs:SetJustifyH(justify) end
	return fs
end

function Skin:CreateSeparator(parent, horizontal)
	local line = parent:CreateTexture(nil, "ARTWORK")
	line:SetTexture(WHITE)
	line:SetVertexColor(Util.Unpack(Skin.colors.border))
	if horizontal == false then line:SetWidth(1) else line:SetHeight(1) end
	return line
end

--------------------------------------------------------------------------------
-- Panels
--------------------------------------------------------------------------------

-- How tall the title bar is. A panel carrying an emblem gets a proper header
-- rather than a strip: the emblem is 48px and would otherwise hang out of a
-- 34px bar, either poking above the window edge or down over the content.
local TITLE_BAR_HEIGHT      = 34
local TITLE_BAR_HEIGHT_ICON = 56
local EMBLEM_SIZE           = 48

--- A titled container.
--- opts = { title, icon, width, height, closeButton, movable }
--- `icon` is a texture path shown as an emblem at the left of the title bar; the
--- title text steps aside for it and the bar grows to fit.
---
--- Callers that anchor anything under the title bar must read
--- `panel.titleBarHeight` rather than assuming a number - it depends on whether
--- there is an emblem.
function Skin:CreatePanel(parent, opts)
	opts = opts or {}

	local panel = CreateFrame("Frame", opts.name, parent or UIParent)
	panel:SetSize(opts.width or 400, opts.height or 300)
	Skin:CreateBackdrop(panel, { color = Skin.colors.bg, borderColor = Skin.colors.border })

	if opts.shadow ~= false then
		panel.shadow = Skin:CreateShadow(panel, 4, 0.5)
	end

	panel.titleBarHeight = 0

	if opts.title then
		local barHeight = opts.icon and TITLE_BAR_HEIGHT_ICON or TITLE_BAR_HEIGHT
		panel.titleBarHeight = barHeight

		local bar = CreateFrame("Frame", nil, panel)
		bar:SetPoint("TOPLEFT", 1, -1)
		bar:SetPoint("TOPRIGHT", -1, -1)
		bar:SetHeight(barHeight)
		Skin:CreateBackdrop(bar, { color = Skin.colors.bgRaised, borderSize = 0 })

		local accentLine = bar:CreateTexture(nil, "ARTWORK")
		accentLine:SetTexture(WHITE)
		accentLine:SetPoint("BOTTOMLEFT")
		accentLine:SetPoint("BOTTOMRIGHT")
		accentLine:SetHeight(1)
		accentLine:SetVertexColor(Skin:Accent())
		bar.accentLine = accentLine

		-- Emblem first, so the title knows how far to step aside for it.
		local titleX, titleSize = 12, 14
		if opts.icon then
			local emblem = bar:CreateTexture(nil, "ARTWORK")
			emblem:SetSize(EMBLEM_SIZE, EMBLEM_SIZE)
			emblem:SetPoint("LEFT", 10, 0)
			emblem:SetTexture(opts.icon)
			bar.emblem = emblem

			titleX = 10 + EMBLEM_SIZE + 12
			-- A 14pt title next to a 48px emblem reads as an afterthought.
			titleSize = 18
		end

		local title = Skin:CreateFontString(bar, "OVERLAY", titleSize, "NONE")
		title:SetPoint("LEFT", titleX, 0)
		title:SetText(opts.title)
		bar.title = title

		panel.titleBar = bar

		if opts.closeButton then
			panel.closeButton = Skin:CreateCloseButton(bar, panel)
			panel.closeButton:SetPoint("RIGHT", -6, 0)
		end

		if opts.movable ~= false then
			panel:SetMovable(true)
			panel:EnableMouse(true)
			bar:EnableMouse(true)
			bar:RegisterForDrag("LeftButton")
			bar:SetScript("OnDragStart", function() panel:StartMoving() end)
			bar:SetScript("OnDragStop", function() panel:StopMovingOrSizing() end)
		end
	end

	return panel
end

function Skin:CreateCloseButton(parent, target)
	local btn = CreateFrame("Button", nil, parent)
	btn:SetSize(22, 22)

	local x = Skin:CreateFontString(btn, "OVERLAY", 16, "NONE")
	x:SetPoint("CENTER")
	x:SetText("x")
	x:SetTextColor(Util.Unpack(Skin.colors.textDim))
	btn.label = x

	btn:SetScript("OnEnter", function() x:SetTextColor(Skin:Accent()) end)
	btn:SetScript("OnLeave", function() x:SetTextColor(Util.Unpack(Skin.colors.textDim)) end)
	btn:SetScript("OnClick", function()
		if target then target:Hide() end
	end)

	return btn
end

--------------------------------------------------------------------------------
-- Refresh on settings change
--------------------------------------------------------------------------------

function Skin:OnEnable()
	self:RegisterMessage("BOOM_APPEARANCE_CHANGED", function()
		Boom:SendMessage("BOOM_SKIN_REFRESH")
	end)
end
