-- Portal Pro :: Core/Util.lua
-- Money is copper, always, as an integer. Formatting happens at the UI edge and nowhere else.

local ADDON_NAME, PP = ...

local U = {}
PP.U = U

local GOLD    = "|cffF0C674"
local SILVER  = "|cffC7CDD6"
local COPPER  = "|cffCD7F32"
local R       = "|r"

--------------------------------------------------------------------------------
-- Money
--------------------------------------------------------------------------------

--- Full precision: "12g 34s 56c", zero components dropped unless the whole thing is zero.
function U.Money(copper, forceSign)
	copper = math.floor(tonumber(copper) or 0)
	local neg = copper < 0
	local abs = math.abs(copper)

	local g = math.floor(abs / 10000)
	local s = math.floor((abs % 10000) / 100)
	local c = abs % 100

	local parts = {}
	if g > 0 then parts[#parts + 1] = GOLD .. g .. "g" .. R end
	if s > 0 then parts[#parts + 1] = SILVER .. s .. "s" .. R end
	if c > 0 or #parts == 0 then parts[#parts + 1] = COPPER .. c .. "c" .. R end

	local sign = neg and "-" or (forceSign and "+" or "")
	return sign .. table.concat(parts, " ")
end

--- Full precision with no spaces: "12g34s56c". Zero components are dropped, but nothing is
--- rounded away — this is the formatter for anywhere a figure has to be both exact and fit
--- a narrow chip. MoneyShort collapses 12g34s56c to "12.3g", which is fine for a chart axis
--- and misleading for a balance.
function U.MoneyFull(copper, forceSign)
	copper = math.floor(tonumber(copper) or 0)
	local neg = copper < 0
	local abs = math.abs(copper)

	local g = math.floor(abs / 10000)
	local s = math.floor((abs % 10000) / 100)
	local c = abs % 100

	local parts = {}
	if g > 0 then parts[#parts + 1] = GOLD .. g .. "g" .. R end
	if s > 0 then parts[#parts + 1] = SILVER .. s .. "s" .. R end
	if c > 0 or #parts == 0 then parts[#parts + 1] = COPPER .. c .. "c" .. R end

	local sign = neg and "-" or (forceSign and "+" or "")
	return sign .. table.concat(parts, "")
end

--- Compact, for tight columns: "12.3g", "340s", "56c".
function U.MoneyShort(copper, forceSign)
	copper = math.floor(tonumber(copper) or 0)
	local neg = copper < 0
	local abs = math.abs(copper)
	local sign = neg and "-" or (forceSign and "+" or "")

	if abs >= 10000 then
		local g = abs / 10000
		local text = (g >= 100) and ("%d"):format(g + 0.5) or ("%.1f"):format(g)
		return sign .. GOLD .. text .. "g" .. R
	elseif abs >= 100 then
		return sign .. SILVER .. math.floor(abs / 100) .. "s" .. R
	end
	return sign .. COPPER .. abs .. "c" .. R
end

--- Uncoloured, for chat messages and text templates.
function U.MoneyPlain(copper)
	copper = math.floor(tonumber(copper) or 0)
	local abs = math.abs(copper)
	local g, s, c = math.floor(abs / 10000), math.floor((abs % 10000) / 100), abs % 100
	local parts = {}
	if g > 0 then parts[#parts + 1] = g .. "g" end
	if s > 0 then parts[#parts + 1] = s .. "s" end
	if c > 0 or #parts == 0 then parts[#parts + 1] = c .. "c" end
	return (copper < 0 and "-" or "") .. table.concat(parts, " ")
end

--- Parse "5g", "5g50s", "50s", "12" (bare number = gold) into copper.
function U.ParseMoney(str)
	if not str then return nil end
	str = tostring(str):lower():gsub("%s+", "")
	if str:match("^%d+$") then
		return tonumber(str) * 10000
	end
	local total, found = 0, false
	for num, unit in str:gmatch("(%d+)([gsc])") do
		found = true
		local n = tonumber(num)
		if unit == "g" then total = total + n * 10000
		elseif unit == "s" then total = total + n * 100
		else total = total + n end
	end
	return found and total or nil
end

--------------------------------------------------------------------------------
-- Time
--------------------------------------------------------------------------------

--- "just now", "40s", "12m", "3h", "2d"
function U.Ago(timestamp, now)
	local delta = (now or time()) - (timestamp or 0)
	if delta < 5 then return "just now" end
	if delta < 60 then return ("%ds"):format(delta) end
	if delta < 3600 then return ("%dm"):format(delta / 60) end
	if delta < 86400 then return ("%dh"):format(delta / 3600) end
	return ("%dd"):format(delta / 86400)
end

--- "1h 24m" / "24m 05s" — for session clocks.
function U.Duration(seconds)
	seconds = math.max(0, math.floor(seconds or 0))
	local h = math.floor(seconds / 3600)
	local m = math.floor((seconds % 3600) / 60)
	local s = seconds % 60
	if h > 0 then return ("%dh %02dm"):format(h, m) end
	if m > 0 then return ("%dm %02ds"):format(m, s) end
	return ("%ds"):format(s)
end

--- Midnight-aligned day index, for grouping the ledger into days.
function U.DayKey(timestamp)
	return date("%Y-%m-%d", timestamp or time())
end

function U.Clock(timestamp)
	return date("%H:%M", timestamp or time())
end

--------------------------------------------------------------------------------
-- Strings
--------------------------------------------------------------------------------

function U.Trim(s)
	return (tostring(s or ""):gsub("^%s+", ""):gsub("%s+$", ""))
end

--- Lowercased, punctuation flattened to spaces. Used by the lead parser so that
--- "port to sw?" and "PORT TO SW!!!" normalise to the same token stream.
function U.Normalize(s)
	s = tostring(s or ""):lower()
	s = s:gsub("[%p]", " ")
	s = s:gsub("%s+", " ")
	return U.Trim(s)
end

--- Whole-word match against a normalised string. Critical for aliases like "if"
--- (Ironforge) that would otherwise match inside ordinary English.
function U.HasWord(normalized, word)
	return (" " .. normalized .. " "):find(" " .. word .. " ", 1, true) ~= nil
end

function U.Truncate(s, maxLen)
	s = tostring(s or "")
	if #s <= maxLen then return s end
	return s:sub(1, math.max(0, maxLen - 1)) .. "..."
end

--- Strip colour codes, texture escapes and hyperlinks — for length counting.
function U.StripEscapes(s)
	s = tostring(s or "")
	s = s:gsub("|c%x%x%x%x%x%x%x%x", ""):gsub("|r", "")
	s = s:gsub("|T.-|t", "")
	s = s:gsub("|H.-|h(.-)|h", "%1")
	return s
end

function U.Capitalize(s)
	s = tostring(s or "")
	return s:sub(1, 1):upper() .. s:sub(2)
end

--------------------------------------------------------------------------------
-- Tables
--------------------------------------------------------------------------------

function U.Count(t)
	local n = 0
	for _ in pairs(t or {}) do n = n + 1 end
	return n
end

function U.CopyDefaults(dst, src)
	for k, v in pairs(src) do
		if type(v) == "table" then
			if type(dst[k]) ~= "table" then dst[k] = {} end
			U.CopyDefaults(dst[k], v)
		elseif dst[k] == nil then
			dst[k] = v
		end
	end
	return dst
end

function U.DeepCopy(src)
	if type(src) ~= "table" then return src end
	local out = {}
	for k, v in pairs(src) do out[k] = U.DeepCopy(v) end
	return out
end

--- Stable sort by a key function, ascending unless `desc`.
function U.SortBy(list, keyFn, desc)
	table.sort(list, function(a, b)
		local ka, kb = keyFn(a), keyFn(b)
		if ka == kb then return tostring(a) < tostring(b) end
		if desc then return ka > kb end
		return ka < kb
	end)
	return list
end

--------------------------------------------------------------------------------
-- Misc
--------------------------------------------------------------------------------

local idCounter = 0
function U.NextID()
	idCounter = idCounter + 1
	return ("%d-%d"):format(time(), idCounter)
end

function U.ClassColor(class)
	local c = RAID_CLASS_COLORS and RAID_CLASS_COLORS[class or ""]
	if c then return c.r, c.g, c.b end
	return 0.91, 0.92, 0.93
end

function U.ClassColorHex(class)
	local c = RAID_CLASS_COLORS and RAID_CLASS_COLORS[class or ""]
	if c then return ("|cff%02x%02x%02x"):format(c.r * 255, c.g * 255, c.b * 255) end
	return "|cffE8EAED"
end

function U.Round(n, places)
	local mult = 10 ^ (places or 0)
	return math.floor((n or 0) * mult + 0.5) / mult
end

function U.Clamp(n, lo, hi)
	if n < lo then return lo end
	if n > hi then return hi end
	return n
end
