-- ============================================================
--  rOrbs – Orb Template Engine  (rewrite)
-- ============================================================
--
--  ARCHITECTURE – one source of truth per orb:
--
--    orb._tmpl      = the active template table (raw, never mutated)
--    orb._orbType   = "HEALTH" / "POWER" / ...
--
--  When the model is loaded, OnModelLoaded stores the model's
--  natural camera target as _natX/Y/Z (read once, never changed).
--
--  ApplyParams() is the single function that writes cam+position
--  to the model.  It reads posZ/camScale directly from the template
--  table every time – no caching, no drift.
--
--  For Preview the GUI simply mutates tmpl.camScale / tmpl.posAdjustZ
--  directly and then calls engine:Refresh(orbType).  That's it.
-- ============================================================

local _, ns = ...

local PAN_X = 1.75
local PAN_Z = 1.95

local engine = {}
ns.OrbTemplateEngine = engine

-----------------------------------------------------------------
-- helpers
-----------------------------------------------------------------
local function GetOrb(t)
  if t == "HEALTH"        then return ns.HealthOrb
  elseif t == "POWER"         then return ns.PowerOrb
  elseif t == "TARGET_HEALTH" then return ns.TargetHealthOrb
  elseif t == "TOT_HEALTH"    then return ns.ToTHealthOrb
  end
end

local function clean(v)
  local n = tonumber(v)
  if not n then return 0 end
  return n
end

-----------------------------------------------------------------
-- ApplyParams: write cam+position to the model RIGHT NOW
-- called after model loads AND after any slider change
-----------------------------------------------------------------
local function ApplyParams(orb, model, tmpl, orbType)
  -- effective cam = template base * live scale multiplier (Template Adjustment)
  local db          = rOrbsDB or {}
  local liveScale   = db["tmplScale_"  ..orbType] or 1.0
  local liveOffZ    = db["tmplOffsetZ_"..orbType] or 0.0
  local liveOffX    = db["tmplOffsetX_"..orbType] or 0.0
  local liveOpacity = db["tmplOpacity_"..orbType]

  -- For preview templates we bypass live overrides so sliders are absolute
  local isPreview = tmpl._isPreview
  local cam     = (tmpl.camScale   or 1.0) * (isPreview and 1.0 or liveScale)
  local posX    = (tmpl.posAdjustX or 0.0) + (isPreview and 0.0 or liveOffX)
  local posY    = (tmpl.posAdjustY or 0.0)
  local posZ    = (tmpl.posAdjustZ or 0.0) + (isPreview and 0.0 or liveOffZ)
  local opacity = isPreview and (tmpl.modelOpacity or 1.0)
                            or (liveOpacity or tmpl.modelOpacity or 1.0)

  local scale = (orb and orb:GetScale()) or 1.0

  -- absolute position = natural origin + user offset
  local ax = (model._natX or 0) + posX * scale
  local ay = (model._natY or 0) + posY * scale
  local az = (model._natZ or 0) + posZ * scale

  -- Store for fill-level panning (AdjustByValue uses these)
  model._baseX   = ax
  model._baseY   = ay
  model._baseZ   = az
  model._cam     = cam
  model._opacity = opacity

  model:SetCamDistanceScale(cam)
  model:SetPosition(ax, ay, az)
  model:SetRotation(tmpl.rotation or 0)
  model:SetPortraitZoom(0)
  model:SetAlpha(opacity)
end

-----------------------------------------------------------------
-- AdjustByValue: pan model with fill level (called on HP change)
-----------------------------------------------------------------
function engine:AdjustByValue(orb, ratio)
  local model = orb and orb.model
  if not model or model._loading then return end
  if not orb._tmpl then return end

  if ratio <= 0 then
    model:SetAlpha(0)
    return
  end
  model:SetAlpha(model._opacity or 1)

  local h = orb:GetHeight()
  if h <= 0 then h = 256 end
  local diff = h * (1 - ratio)
  local cam  = model._cam or 1

  model:SetPosition(
    (model._baseX or 0) + diff * cam * PAN_X / h,
    (model._baseY or 0),
    (model._baseZ or 0) + diff * cam * PAN_Z / h
  )
end

-- legacy alias used by AdjustModelByValue calls elsewhere
engine.AdjustModelByValue = engine.AdjustByValue

-----------------------------------------------------------------
-- LoadModel: trigger async model load, apply params on callback
-----------------------------------------------------------------
local function LoadModel(orb, orbType, tmpl)
  local model = orb.model
  if not model then return end

  model._loading = true
  model._tmpl    = tmpl
  model._orbType = orbType

  -- reset natural origin (captured fresh in OnModelLoaded)
  model._natX, model._natY, model._natZ = 0, 0, 0

  model:SetAlpha(0)
  model:ClearModel()
  model:SetDisplayInfo(tmpl.displayInfoID)

  -- Hook once per model frame
  if not model._engineHooked then
    model._engineHooked = true
    model:HookScript("OnModelLoaded", function(self)
      local myTmpl = self._tmpl
      if not myTmpl then return end
      -- skip the ClearModel() callback (displayInfo == 0)
      if (self:GetDisplayInfo() or 0) == 0 then return end

      local parentOrb = self:GetParent()
      if parentOrb then ns.ApplyOrbModelAnchors(self, parentOrb) end

      -- Capture the model's natural camera origin ONCE.
      -- GetCameraTarget() here returns the model-space look-at point
      -- BEFORE we call SetPosition, so it is always (0,0,0) for
      -- standard creature models, and non-zero for vehicle/object models.
      local nx, ny, nz = 0, 0, 0
      if self.GetCameraTarget then nx, ny, nz = self:GetCameraTarget() end
      self._natX = nx
      self._natY = ny
      self._natZ = nz
      self._loading = false

      local ot = self._orbType or ""
      ApplyParams(parentOrb, self, myTmpl, ot)

      -- apply fill panning once
      local fill = parentOrb and parentOrb.fill
      if fill then
        local ok1, ratio1 = pcall(function()
          local _, rawMax = fill:GetMinMaxValues()
          local maxN = tonumber(rawMax) or 0
          local valN = tonumber(fill:GetValue()) or 0
          if maxN > 0 then return valN / maxN end
          return 1
        end)
        engine:AdjustByValue(parentOrb, ok1 and ratio1 or 1)
      end
    end)
  end

  -- Fallback: OnModelLoaded sometimes doesn't fire for already-cached models.
  -- After 0.5s, if still loading, apply params using (0,0,0) as natural origin.
  C_Timer.After(0.5, function()
    if not (model._loading and model._tmpl == tmpl) then return end
    model._loading = false
    local parentOrb = model:GetParent()
    if parentOrb then ns.ApplyOrbModelAnchors(model, parentOrb) end
    model._natX, model._natY, model._natZ = 0, 0, 0
    ApplyParams(orb, model, tmpl, orbType)
    local fill = orb.fill
    if fill then
      local ok2, ratio2 = pcall(function()
        local _, rawMax = fill:GetMinMaxValues()
        local maxN = tonumber(rawMax) or 0
        local valN = tonumber(fill:GetValue()) or 0
        if maxN > 0 then return valN / maxN end
        return 1
      end)
      engine:AdjustByValue(orb, ok2 and ratio2 or 1)
    end
  end)
end

-----------------------------------------------------------------
-- HookFill: wire fill bar → AdjustByValue (once per orb)
-----------------------------------------------------------------
local function HookFill(orb)
  if orb._fillHooked then return end
  orb._fillHooked = true
  local fill = orb.fill
  if not fill then return end
  fill:HookScript("OnValueChanged", function(bar, value)
    if not orb._tmpl then return end
    local ok, ratio = pcall(function()
      local _, rawMax = bar:GetMinMaxValues()
      local maxN = tonumber(rawMax) or 0
      local valN = tonumber(value)  or 0
      if maxN > 0 then return valN / maxN end
      return 1
    end)
    engine:AdjustByValue(orb, ok and ratio or 1)
  end)
end

-----------------------------------------------------------------
-- PUBLIC API
-----------------------------------------------------------------

-- Apply a template key to an orb. Key can be in ns.orbTemplates
-- or rOrbsDB.customTemplates.  Pass "none" to remove.
function engine:Apply(orbType, templateKey, applyColors)
  local orb = GetOrb(orbType)
  if not orb then return end

  local tmpl = (ns.orbTemplates and ns.orbTemplates[templateKey])
            or (rOrbsDB and rOrbsDB.customTemplates and rOrbsDB.customTemplates[templateKey])

  if rOrbsDB then rOrbsDB["orbTemplate_"..orbType] = templateKey end

  if not tmpl or templateKey == "none" or (tmpl.displayInfoID or 0) == 0 then
    self:RemoveTemplate(orb)
    return
  end

  orb._tmpl    = tmpl
  orb._orbType = orbType
  -- legacy compat fields
  orb._templateConfig = tmpl
  orb._templateKey    = templateKey

  HookFill(orb)
  if orb.model then LoadModel(orb, orbType, tmpl) end
  if applyColors then self:ApplyColors(orb, orbType, tmpl) end
end

-- Refresh visual after any parameter change (slider, Apply button).
-- Does NOT reload the model – just recalculates cam+position from
-- the current values in orb._tmpl and writes them to the model.
function engine:Refresh(orbType)
  local orb = GetOrb(orbType)
  if not orb then return end
  local tmpl  = orb._tmpl or orb._templateConfig
  local model = orb.model
  if not tmpl or not model or model._loading then return end
  -- keep both field names in sync
  orb._tmpl           = tmpl
  orb._templateConfig = tmpl
  model._tmpl         = tmpl
  ApplyParams(orb, model, tmpl, orbType)
  -- re-apply fill panning
  local fill = orb.fill
  if fill then
    local ok, ratio = pcall(function()
      local _, rawMax = fill:GetMinMaxValues()
      local rawVal    = fill:GetValue()
      local maxN  = tonumber(rawMax) or 0
      local valN  = tonumber(rawVal) or 0
      if maxN > 0 then return valN / maxN end
      return 1
    end)
    self:AdjustByValue(orb, ok and ratio or 1)
  end
end

-- Legacy alias used by Template Adjustment sliders
engine.RefreshModel = engine.Refresh

-- Remove template, revert to standard model.
function engine:RemoveTemplate(orb)
  if not orb then return end
  orb._tmpl = nil; orb._templateConfig = nil
  orb._templateKey = nil; orb._orbType = nil
  if orb.model then
    local m = orb.model
    m._tmpl = nil; m._templateConfig = nil; m._loading = false
    if m.Update then m:Update() end
  end
end

-- Restore all persisted templates on login / reload.
function engine:RestoreAll()
  for _, orbType in ipairs({"HEALTH","POWER","TARGET_HEALTH","TOT_HEALTH"}) do
    local key  = rOrbsDB and rOrbsDB["orbTemplate_"..orbType]
    local show = rOrbsDB and rOrbsDB["tmplShow_"..orbType]
    if key and key ~= "none" and key ~= "" and show then
      self:Apply(orbType, key, false)
    end
  end
end

-- Apply fill texture + colours.
function engine:ApplyColors(orb, orbType, tmpl)
  local fill = orb.fill
  if not fill then return end
  if tmpl.statusBarColor then
    local c = tmpl.statusBarColor
    fill:SetStatusBarColor(c[1], c[2], c[3], c[4] or 1)
  end
  if tmpl.statusBarTexture then fill:SetStatusBarTexture(tmpl.statusBarTexture) end
  if tmpl.sparkColor and orb.spark then
    local s = tmpl.sparkColor
    orb.spark:SetVertexColor(s[1], s[2], s[3], s[4] or 1)
  end
  if tmpl.statusBarColor then
    local c = tmpl.statusBarColor
    if orb.galaxies then for _, g in pairs(orb.galaxies) do g:SetVertexColor(c[1],c[2],c[3]) end end
    if orb.bubbles  then for _, b in pairs(orb.bubbles)  do b:SetVertexColor(c[1],c[2],c[3]) end end
  end
end

-----------------------------------------------------------------
-- Events
-----------------------------------------------------------------
do
  -- Helper: re-apply all persisted templates (used by multiple events below)
  local function ReApplyAll()
    if not (ns and ns.OrbTemplateEngine) then return end
    ns.OrbTemplateEngine:RestoreAll()
  end

  -- PLAYER_ENTERING_WORLD fires on login AND reload.
  -- We schedule three passes (0.5 / 2.0 / 4.0 s) because the model cache
  -- may not be warmed yet on the first tick after a /reload.
  local f = CreateFrame("Frame")
  f:RegisterEvent("PLAYER_ENTERING_WORLD")
  f:RegisterEvent("LOADING_SCREEN_DISABLED")   -- fires when the loading screen fades out
  f:SetScript("OnEvent", function(self, event)
    C_Timer.After(0.5,  ReApplyAll)
    C_Timer.After(2.0,  ReApplyAll)  -- second pass for slow model cache
    C_Timer.After(4.0,  ReApplyAll)  -- third pass as last-resort safety net
  end)

  -- Refresh after UI fade-in (e.g. dialogue UI)
  local w = CreateFrame("Frame", nil, UIParent)
  w:SetScript("OnShow", function()
    C_Timer.After(0.1, function()
      if not (ns and ns.OrbTemplateEngine) then return end
      for _, orbType in ipairs({"HEALTH","POWER","TARGET_HEALTH","TOT_HEALTH"}) do
        local orb = GetOrb(orbType)
        local key = rOrbsDB and rOrbsDB["orbTemplate_"..orbType]
        if orb and orb._tmpl and key and key ~= "none" then
          ns.OrbTemplateEngine:Apply(orbType, key, false)
        end
      end
    end)
  end)
end
