local ADDON_NAME, ns = ...
local WF = ns.WF

local Util = WF.Util

local function EscapePattern(s)
  return (s:gsub("([%(%)%.%+%-%*%?%[%]%^%$])", "%%%1"))
end

local function AddCommas(n)
  local s = tostring(math.floor(tonumber(n) or 0))
  local result = s:reverse():gsub("(%d%d%d)", "%1,"):reverse():gsub("^,", "")
  return result
end

function Util:FormatMoney(copper)
  copper = tonumber(copper) or 0
  if copper <= 0 then
    return "0g 0s 0c"
  end
  local g = math.floor(copper / 10000)
  local s = math.floor((copper % 10000) / 100)
  local c = copper % 100
  return string.format("%sg %ds %dc", AddCommas(g), s, c)
end

function Util:Round(n)
  n = tonumber(n) or 0
  return math.floor(n + 0.5)
end

function Util:SafeCall(fn, ...)
  if type(fn) ~= "function" then
    return
  end
  local ok, err = pcall(fn, ...)
  if not ok then
    WF:Debug(err)
  end
end

function Util:PatternFromFormat(fmt)
  if type(fmt) ~= "string" then
    return nil
  end
  local tmp = fmt
  tmp = tmp:gsub("%%d", "\1D")
  tmp = tmp:gsub("%%s", "\1S")
  tmp = EscapePattern(tmp)
  tmp = tmp:gsub("\1D", "(%%d+)")
  tmp = tmp:gsub("\1S", "(.+)")
  return "^" .. tmp .. "$"
end

function Util:AmountFromFormat(msg, fmt)
  if type(msg) ~= "string" or type(fmt) ~= "string" then
    return 0
  end
  local tmp = fmt:gsub("%%d", "\1D")
  tmp = EscapePattern(tmp)
  tmp = tmp:gsub("\1D", "(%%d+)")
  local n = msg:match(tmp)
  if n then
    return tonumber(n) or 0
  end
  return 0
end

function Util:GetPlayerMapXY()
  local mapId = C_Map.GetBestMapForUnit("player")
  if not mapId then
    return nil, nil, nil
  end
  local pos = C_Map.GetPlayerMapPosition(mapId, "player")
  if not pos then
    return mapId, nil, nil
  end

  -- Track last known outdoor position so instance sessions can still waypoint/promote.
  local inInstance = IsInInstance()
  if not inInstance then
    local lw = self._lastWorldPos
    if not lw then
      lw = {}
      self._lastWorldPos = lw
    end
    lw.mapId = mapId
    lw.x = pos.x
    lw.y = pos.y
    lw.t = time()
  end

  return mapId, pos.x, pos.y
end

function Util:GetLastWorldMapXY()
  local lw = self._lastWorldPos
  if not lw then
    return nil, nil, nil
  end
  if not lw.mapId or lw.x == nil or lw.y == nil then
    return nil, nil, nil
  end
  return lw.mapId, lw.x, lw.y
end

function Util:GetZoneLabel()
  local zone = GetZoneText()
  if zone and zone ~= "" then
    return zone
  end
  local sub = GetSubZoneText()
  if sub and sub ~= "" then
    return sub
  end
  return "Unknown Zone"
end

function Util:GetInstanceContext()
  local inInstance, instanceType = IsInInstance()
  if not inInstance then
    return false, nil
  end
  return true, instanceType
end

function Util:IsFarmLocationAllowed()
  local inInstance, instanceType = self:GetInstanceContext()
  if not inInstance then
    return true
  end

  if WF.DB and WF.DB.GetSetting and WF.DB:GetSetting("allowInstanceFarms") then
    return true
  end

  -- default: do not allow saving farms inside dungeon/raid instances
  if instanceType == "raid" or instanceType == "party" then
    return false
  end

  return false
end

function Util:GetQualityColor(itemLinkOrId)
  local quality = nil

  -- If the link already contains a color wrapper (|cAARRGGBB...|r),
  -- trust it. This handles cases where the instance's rarity differs
  -- from the base itemID (e.g. Remix / upgraded variants) and avoids
  -- relying on GetItemInfo cache timing.
  if type(itemLinkOrId) == "string" then
    local hex = itemLinkOrId:match("^|c(%x%x%x%x%x%x%x%x)") or itemLinkOrId:match("|c(%x%x%x%x%x%x%x%x)|Hitem:")
    if hex then
      local r = tonumber(hex:sub(3, 4), 16)
      local g = tonumber(hex:sub(5, 6), 16)
      local b = tonumber(hex:sub(7, 8), 16)
      if r and g and b then
        return r / 255, g / 255, b / 255
      end
    end
  end

  if type(itemLinkOrId) == "number" then
    quality = select(3, GetItemInfo(itemLinkOrId))
  elseif type(itemLinkOrId) == "string" then
    quality = select(3, GetItemInfo(itemLinkOrId))
  end

  if not quality then
    return 0.92, 0.92, 0.92
  end

  local r, g, b = GetItemQualityColor(quality)
  if not r then
    return 0.92, 0.92, 0.92
  end

  return r, g, b
end

-- -------------------------------------------------------
-- Pretty display helpers (UI only)
-- -------------------------------------------------------
do
  local PRETTY = {
    ["any"] = "Any",
    ["filtered"] = "Filtered",

    ["tsm"] = "TSM",
    ["gph"] = "GPH",
    ["liv"] = "LIV",

    ["vendor"] = "Vendor",
    ["auctionator"] = "Auctionator",

    ["openworld"] = "Open World",
  }

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

  local function HasUpper(s)
    return s:find("%u") ~= nil
  end

  local function IsShortAcronymLike(s)
    -- e.g. MoP, WoD, BfA
    return s:match("^%u%l%u%u?$") ~= nil
  end

  local function TitleCaseWords(s)
    -- Lowercase then capitalize each word start. (Keeps apostrophes.)
    local low = s:lower()
    return (low:gsub("(%a)([%w']*)", function(a, rest)
      return a:upper() .. rest
    end))
  end

  --- PrettyToken
  -- UI-friendly labels for internal enum-ish strings.
  -- Does NOT mutate inputs or affect stored values.
  function Util:PrettyToken(v)
    if v == nil then
      return "—"
    end

    local s = Trim(v)
    if s == "" then
      return "—"
    end

    local k = s:lower()
    local mapped = PRETTY[k]
    if mapped then
      return mapped
    end

    -- Preserve already-nice strings (spaces + uppercase letters)
    if s:find("%s") and HasUpper(s) then
      return s
    end

    -- Preserve short acronym-like tokens (MoP, WoD, BfA)
    if IsShortAcronymLike(s) then
      return s
    end

    -- Preserve pure acronyms/punct
    if s:match("^[%u%d%p]+$") then
      return s
    end

    -- Split simple CamelCase words into spaced words (OpenWorld -> Open World)
    local camel = s:gsub("(%l)(%u)", "%1 %2")
    if camel ~= s then
      s = camel
    end

    return TitleCaseWords(s)
  end
end


-- -------------------------------------------------------
-- Tooltip snapshot cache (reuse rendered hyperlink text to avoid
-- rebuilding native item tooltips on every hover)
-- -------------------------------------------------------
do
  local MAX_TOOLTIP_SNAPSHOTS = 200
  local TOOLTIP_SNAPSHOT_TTL = 900

  local function _GetTooltipLineObjects(tip, index)
    if not tip or not tip.GetName then return nil, nil end
    local name = tip:GetName()
    if not name or name == "" then return nil, nil end
    return _G[name .. "TextLeft" .. tostring(index)], _G[name .. "TextRight" .. tostring(index)]
  end

  local function _CaptureTooltipSnapshot(tip)
    if not tip or not tip.NumLines then return nil end
    local num = tonumber(tip:NumLines()) or 0
    if num <= 0 then return nil end

    local lines = {}
    for i = 1, num do
      local leftFS, rightFS = _GetTooltipLineObjects(tip, i)
      local leftText = leftFS and leftFS.GetText and leftFS:GetText() or nil
      local rightText = rightFS and rightFS.GetText and rightFS:GetText() or nil
      if leftText ~= nil or rightText ~= nil then
        local lr, lg, lb = 1, 1, 1
        local rr, rg, rb = 1, 1, 1
        if leftFS and leftFS.GetTextColor then
          lr, lg, lb = leftFS:GetTextColor()
        end
        if rightFS and rightFS.GetTextColor then
          rr, rg, rb = rightFS:GetTextColor()
        end
        lines[#lines + 1] = {
          left = leftText,
          right = rightText,
          lr = lr or 1,
          lg = lg or 1,
          lb = lb or 1,
          rr = rr or 1,
          rg = rg or 1,
          rb = rb or 1,
        }
      end
    end

    if #lines <= 0 then
      return nil
    end

    return {
      lines = lines,
      capturedAt = GetTime and GetTime() or 0,
    }
  end

  local function _RenderTooltipSnapshot(tip, snapshot)
    if not tip or type(snapshot) ~= "table" or type(snapshot.lines) ~= "table" then
      return false
    end

    tip:ClearLines()
    for i = 1, #snapshot.lines do
      local line = snapshot.lines[i]
      local left = line and line.left or nil
      local right = line and line.right or nil
      local lr = (line and line.lr) or 1
      local lg = (line and line.lg) or 1
      local lb = (line and line.lb) or 1
      local rr = (line and line.rr) or 1
      local rg = (line and line.rg) or 1
      local rb = (line and line.rb) or 1

      if right and right ~= "" and tip.AddDoubleLine then
        tip:AddDoubleLine(left or " ", right, lr, lg, lb, rr, rg, rb)
      elseif left and left ~= "" then
        tip:AddLine(left, lr, lg, lb, true)
      else
        tip:AddLine(" ", lr, lg, lb, true)
      end
    end

    return true
  end

  local function _TooltipSnapshotLooksCombatRestricted(snapshot)
    if type(snapshot) ~= "table" or type(snapshot.lines) ~= "table" then
      return false
    end

    for i = 1, #snapshot.lines do
      local line = snapshot.lines[i]
      local left = type(line and line.left) == "string" and line.left or nil
      local right = type(line and line.right) == "string" and line.right or nil

      if left and left ~= "" then
        local low = left:lower()
        if low:find("tsm tooltip", 1, true) and low:find("in combat", 1, true) then
          return true
        end
      end

      if right and right ~= "" then
        local low = right:lower()
        if low:find("tsm tooltip", 1, true) and low:find("in combat", 1, true) then
          return true
        end
      end
    end

    return false
  end

  function Util:ClearTooltipSnapshotCache(key)
    local cache = self._tooltipSnapshotCache
    if type(cache) ~= "table" then
      return
    end

    if key ~= nil then
      key = tostring(key or "")
      if key == "" then return end
      cache[key] = nil
      if type(self._tooltipSnapshotSeen) == "table" then
        self._tooltipSnapshotSeen[key] = nil
      end
      if type(self._tooltipSnapshotOrder) == "table" then
        for i = #self._tooltipSnapshotOrder, 1, -1 do
          if self._tooltipSnapshotOrder[i] == key then
            table.remove(self._tooltipSnapshotOrder, i)
          end
        end
      end
      return
    end

    self._tooltipSnapshotCache = {}
    self._tooltipSnapshotOrder = {}
    self._tooltipSnapshotSeen = {}
  end

  function Util:_TooltipSnapshotCacheGet(key)
    key = tostring(key or "")
    if key == "" then return nil end

    local cache = self._tooltipSnapshotCache
    if type(cache) ~= "table" then
      cache = {}
      self._tooltipSnapshotCache = cache
      self._tooltipSnapshotOrder = {}
      self._tooltipSnapshotSeen = {}
    end

    local entry = cache[key]
    if type(entry) ~= "table" or type(entry.snapshot) ~= "table" then
      return nil
    end

    local now = GetTime and GetTime() or 0
    if (tonumber(entry.expiresAt) or 0) > 0 and now > (tonumber(entry.expiresAt) or 0) then
      self:ClearTooltipSnapshotCache(key)
      return nil
    end

    if (not (InCombatLockdown and InCombatLockdown())) and _TooltipSnapshotLooksCombatRestricted(entry.snapshot) then
      self:ClearTooltipSnapshotCache(key)
      return nil
    end

    entry.lastAccess = now
    return entry.snapshot
  end

  function Util:_TooltipSnapshotCachePut(key, snapshot)
    key = tostring(key or "")
    if key == "" or type(snapshot) ~= "table" then
      return
    end

    if InCombatLockdown and InCombatLockdown() then
      return
    end

    if _TooltipSnapshotLooksCombatRestricted(snapshot) then
      return
    end

    local cache = self._tooltipSnapshotCache
    local order = self._tooltipSnapshotOrder
    local seen = self._tooltipSnapshotSeen
    if type(cache) ~= "table" then
      cache = {}
      order = {}
      seen = {}
      self._tooltipSnapshotCache = cache
      self._tooltipSnapshotOrder = order
      self._tooltipSnapshotSeen = seen
    end
    if type(order) ~= "table" then
      order = {}
      self._tooltipSnapshotOrder = order
    end
    if type(seen) ~= "table" then
      seen = {}
      self._tooltipSnapshotSeen = seen
    end

    local now = GetTime and GetTime() or 0
    cache[key] = {
      snapshot = snapshot,
      expiresAt = now + TOOLTIP_SNAPSHOT_TTL,
      lastAccess = now,
    }

    if not seen[key] then
      order[#order + 1] = key
      seen[key] = true
    end

    while #order > MAX_TOOLTIP_SNAPSHOTS do
      local victim = table.remove(order, 1)
      if victim then
        seen[victim] = nil
        cache[victim] = nil
      end
    end
  end

  function Util:TrySetHyperlinkCached(tip, link)
    tip = tip or GameTooltip
    if not (tip and link) then return false, false end

    local key = tostring(link)
    local inCombat = InCombatLockdown and InCombatLockdown()

    if not inCombat then
      local cached = self:_TooltipSnapshotCacheGet(key)
      if cached then
        local ok = _RenderTooltipSnapshot(tip, cached)
        return ok, ok
      end
    end

    if tip.SetHyperlink then
      local ok = pcall(tip.SetHyperlink, tip, link)
      if ok then
        local snapshot = _CaptureTooltipSnapshot(tip)
        if snapshot and not inCombat and not _TooltipSnapshotLooksCombatRestricted(snapshot) then
          self:_TooltipSnapshotCachePut(key, snapshot)
        end
        return true, false
      end
    end

    if C_TooltipInfo and C_TooltipInfo.GetHyperlink and tip.AddLine then
      local ok, info = pcall(C_TooltipInfo.GetHyperlink, link)
      if ok and type(info) == "table" and type(info.lines) == "table" then
        tip:ClearLines()
        local moneyType = Enum and Enum.TooltipDataLineType and Enum.TooltipDataLineType.Money
        for i = 1, #info.lines do
          local line = info.lines[i]
          if line and not line.money and not (moneyType and line.type == moneyType) then
            local lt = line.leftText
            local rt = line.rightText
            local lc = line.leftColor
            local rc = line.rightColor
            local lr, lg, lb = 1, 1, 1
            local rr, rg, rb = 1, 1, 1
            if lc then lr, lg, lb = lc.r or 1, lc.g or 1, lc.b or 1 end
            if rc then rr, rg, rb = rc.r or 1, rc.g or 1, rc.b or 1 end

            if lt and lt ~= "" and rt and rt ~= "" and tip.AddDoubleLine then
              tip:AddDoubleLine(lt, rt, lr, lg, lb, rr, rg, rb)
            elseif lt and lt ~= "" then
              tip:AddLine(lt, lr, lg, lb, true)
            end
          end
        end

        local sellPrice = nil
        if GetItemInfo then
          sellPrice = select(11, GetItemInfo(link))
        end
        if type(sellPrice) == "number" and sellPrice > 0 and GetCoinTextureString then
          tip:AddLine(" ")
          tip:AddLine("Vendor: " .. GetCoinTextureString(sellPrice), 0.85, 0.85, 0.85, true)
        end

        local snapshot = _CaptureTooltipSnapshot(tip)
        if snapshot and not inCombat and not _TooltipSnapshotLooksCombatRestricted(snapshot) then
          self:_TooltipSnapshotCachePut(key, snapshot)
        end
        return true, false
      end
    end

    return false, false
  end
end
