--[[ UI/CardGrid.lua - virtualized card grid (spec 5.1: recycle frames, zero
     per-entry frames for the whole dataset; M2 target 60fps @ 500 entries). ]]

local _, ns = ...
local S = ns.Style
local W = ns.Widgets
ns.UI = ns.UI or {}
local UI = ns.UI

local pool = {}          -- reusable card frames
local view = {}          -- filtered+sorted entries currently shown
local scroll, inner
local selectedId

------------------------------------------------------------------------
-- Score resolution (lazy, session-cached; live payload wins over baked)
------------------------------------------------------------------------
local scoreCache = {}
local liveRequested = {}   -- one live request per entry per session
local function scoreFor(entry)
  local hit = scoreCache[entry.id]
  if hit ~= nil then return hit end
  local live = ns.API.Cached and ns.API.Cached(entry.code)
  local result
  if live then
    result = ns.Score.FromLive(live, ns.Sources and ns.Sources.Get)
  elseif entry.mockPct then
    result = { pct = entry.mockPct, missing = {} }
  else
    result = ns.Score.Compute(entry.manifest, function(rid)
      return ns.Catalog.OwnedByRecord(rid)
    end, ns.Sources and ns.Sources.Get)
    -- unknown ownership -> ask the server for the authoritative score once.
    -- QUEUED, not fired all at once: a library of sixteen builds used to send
    -- sixteen simultaneous requests the moment the window opened, and the ones
    -- the server dropped left their cards stuck on "?" forever.
    if result.pct == nil and entry.code and not liveRequested[entry.id]
      and ns.Guard.Has("contents") then
      liveRequested[entry.id] = true
      UI.QueueScoreRequest(entry)
    end
  end
  scoreCache[entry.id] = result
  return result
end
-- One score request in flight at a time, the next fired when the previous
-- answers (or times out via the API's own 10s timer). Retries once: a dropped
-- request gets a second chance before its card settles on "?".
local scoreQueue, queueBusy = {}, false
local function pumpScoreQueue()
  if queueBusy then return end
  local job = table.remove(scoreQueue, 1)
  if not job then return end
  queueBusy = true
  ns.API.Request(job.entry.code, function(ok)
    queueBusy = false
    if ok then
      ns.bus:Fire("SCORE_INVALIDATE", job.entry.id)
    elseif not job.retried then
      job.retried = true
      table.insert(scoreQueue, job)   -- back of the line, one more try
    end
    pumpScoreQueue()
  end)
end

function UI.QueueScoreRequest(entry)
  table.insert(scoreQueue, { entry = entry })
  pumpScoreQueue()
end

ns.bus:On("OWNERSHIP_CHANGED", function() wipe(scoreCache); if UI.RefreshGrid then UI.RefreshGrid() end end)
ns.bus:On("SCORE_INVALIDATE", function(id)
  if id then scoreCache[id] = nil else wipe(scoreCache) end
  if UI.RefreshGrid then UI.RefreshGrid() end
end)
UI.ScoreFor = scoreFor

------------------------------------------------------------------------
-- Filtering + sorting (spec F2)
------------------------------------------------------------------------
local function passes(entry, flt)
  if flt.search and flt.search ~= "" then
    local hay = (entry.title or "") .. " " .. (entry.author and entry.author.name or "")
      .. " " .. table.concat(entry.tags or {}, " ")
    -- searchable manifests (judge/provenance use case): find builds by the
    -- decor items they contain, e.g. "tusked fireplace"
    for _, m in ipairs(entry.manifest or {}) do
      if m.name then hay = hay .. " " .. m.name end
    end
    if not hay:lower():find(flt.search, 1, true) then return false end
  end
  if flt.type and flt.type ~= "all" and entry.type ~= flt.type then return false end
  if flt.buildable then
    local sc = scoreFor(entry)
    if not (sc.pct and sc.pct >= 0.8) then return false end
  end
  if UI.activeTab == "favs" and not ns.db.favorites[entry.id] then return false end
  if ns.db.hidden[entry.id] then return false end
  return true
end

local sorters = {
  featured = nil,   -- curated file order
  newest = function(a, b) return (a.addedIn or "") > (b.addedIn or "") end,
  budgetAsc = function(a, b)
    return ((a.budget and a.budget.interior) or 0) < ((b.budget and b.budget.interior) or 0) end,
  budgetDesc = function(a, b)
    return ((a.budget and a.budget.interior) or 0) > ((b.budget and b.budget.interior) or 0) end,
  buildability = function(a, b)
    local pa = scoreFor(a).pct or -1
    local pb = scoreFor(b).pct or -1
    return pa > pb
  end,
}

local function rebuildView()
  wipe(view)
  local flt = ns.db.settings.lastFilters or {}
  local entries = ns.Library and ns.Library.entries or {}
  for _, e in ipairs(entries) do
    if passes(e, flt) then view[#view + 1] = e end
  end
  local sorter = sorters[flt.sort or "featured"]
  if sorter then table.sort(view, sorter) end
end

------------------------------------------------------------------------
-- Card frame factory + layout
------------------------------------------------------------------------
local function makeCard(parent)
  local c = W.Panel(parent, true)
  c:SetSize(S.card.w, S.card.h)
  c:EnableMouse(true)

  -- preview image across the top of the card
  c.img = c:CreateTexture(nil, "ARTWORK")
  c.img:SetPoint("TOPLEFT", 1, -1)
  c.img:SetPoint("TOPRIGHT", -1, -1)
  c.img:SetHeight(S.card.imgH)

  -- shown instead of the image when a build has no screenshot yet
  c.imgEmpty = W.Text(c, S.size.tiny, S.textDim)
  c.imgEmpty:SetPoint("TOP", c.img, "TOP", 0, -S.card.imgH / 2 + 6)
  c.imgEmpty:SetText("no preview yet")

  -- Page through a build's pictures without opening it. Arrows only appear on
  -- hover, and only when there is more than one picture.
  local function showThumb(card, index)
    local thumbs = card.entry and card.entry.thumbs
    if not (thumbs and #thumbs > 0) then return end
    card.imgIndex = ((index - 1) % #thumbs) + 1
    card.img:SetTexture("Interface\\AddOns\\BlueprintBazaar\\Media\\"
      .. thumbs[card.imgIndex]:gsub("%.tga$", ""))
    card.imgCount:SetText(card.imgIndex .. "/" .. #thumbs)
  end
  c.ShowThumb = showThumb

  c.imgPrev = W.Button(c, "<", nil)
  c.imgPrev:SetPoint("LEFT", c.img, "LEFT", 2, 0)
  c.imgPrev:SetSize(18, 20)
  c.imgPrev:SetFrameLevel(c:GetFrameLevel() + 3)
  c.imgPrev:SetScript("OnClick", function(self)
    local card = self:GetParent()
    showThumb(card, (card.imgIndex or 1) - 1)
  end)
  c.imgPrev:Hide()

  c.imgNext = W.Button(c, ">", nil)
  c.imgNext:SetPoint("RIGHT", c.img, "RIGHT", -2, 0)
  c.imgNext:SetSize(18, 20)
  c.imgNext:SetFrameLevel(c:GetFrameLevel() + 3)
  c.imgNext:SetScript("OnClick", function(self)
    local card = self:GetParent()
    showThumb(card, (card.imgIndex or 1) + 1)
  end)
  c.imgNext:Hide()

  c.imgCount = W.Text(c, S.size.tiny, S.text)
  c.imgCount:SetPoint("BOTTOMRIGHT", c.img, "BOTTOMRIGHT", -4, 4)

  c.title = W.Text(c, S.size.body, S.gold)
  c.title:SetPoint("TOPLEFT", 8, -S.card.imgH - 8)
  c.title:SetPoint("RIGHT", -52, 0)
  c.title:SetWordWrap(false)

  c.author = W.Text(c, S.size.tiny, S.textDim)
  c.author:SetPoint("TOPLEFT", 8, -S.card.imgH - 26)

  c.info = W.Text(c, S.size.small, S.textDim)
  c.info:SetPoint("BOTTOMLEFT", 8, 8)

  c.chip = W.ScoreChip(c)
  c.chip:SetPoint("TOPRIGHT", -6, -6)

  c.fav = W.Text(c, S.size.body, S.amber)
  c.fav:SetPoint("BOTTOMRIGHT", -8, 6)

  c:SetScript("OnMouseDown", function(self)
    if self.entry then
      selectedId = self.entry.id
      if UI.ShowDetail then UI.ShowDetail(self.entry) end
      UI.RefreshGrid(true)
    end
  end)
  c:SetScript("OnEnter", function(self)
    self:SetBackdropBorderColor(S.gold[1], S.gold[2], S.gold[3], 0.8)
    local thumbs = self.entry and self.entry.thumbs
    if thumbs and #thumbs > 1 then
      self.imgPrev:Show()
      self.imgNext:Show()
    end
  end)
  c:SetScript("OnLeave", function(self)
    -- the arrows sit inside the card, so leaving onto one is not leaving
    if self:IsMouseOver() then return end
    self.imgPrev:Hide()
    self.imgNext:Hide()
    if self.entry and self.entry.id == selectedId then return end
    self:SetBackdropBorderColor(unpack(S.border))
  end)
  return c
end

local function layout()
  if not scroll then return end
  local width = scroll:GetWidth()
  if width < 10 then return end
  local cols = math.max(1, math.floor((width + S.card.gap) / (S.card.w + S.card.gap)))
  local rows = math.ceil(#view / cols)
  local rowH = S.card.h + S.card.gap
  inner:SetHeight(math.max(1, rows * rowH))

  local offset = scroll:GetVerticalScroll()
  local viewH = scroll:GetHeight()
  -- Never let the viewport point below the content: filtering while scrolled
  -- deep used to leave the window staring at empty space under a now-short
  -- list, rendering nothing at all.
  local maxOffset = math.max(0, rows * rowH - viewH)
  if offset > maxOffset then
    scroll:SetVerticalScroll(maxOffset)
    offset = maxOffset
  end
  local firstRow = math.max(0, math.floor(offset / rowH))
  local lastRow = math.min(rows - 1, math.ceil((offset + viewH) / rowH))

  local used = 0
  for row = firstRow, lastRow do
    for col = 0, cols - 1 do
      local idx = row * cols + col + 1
      local entry = view[idx]
      if entry then
        used = used + 1
        local card = pool[used]
        if not card then
          card = makeCard(inner)
          pool[used] = card
        end
        card.entry = entry
        card:ClearAllPoints()
        card:SetPoint("TOPLEFT", inner, "TOPLEFT",
          col * (S.card.w + S.card.gap), -(row * rowH))
        local thumbs = entry.thumbs
        if thumbs and #thumbs > 0 then
          card.img:SetTexCoord(0, 1, 0, 1)
          card.img:Show()
          card.imgEmpty:Hide()
          card.ShowThumb(card, 1)       -- pooled cards must reset to page one
          card.imgCount:SetShown(#thumbs > 1)
        else
          card.img:Hide()
          card.imgEmpty:Show()
          card.imgCount:Hide()
          card.imgPrev:Hide()
          card.imgNext:Hide()
        end
        card.title:SetText(entry.title or "?")
        card.author:SetText("by " .. ((entry.author and entry.author.name) or "?"))
        local sc = scoreFor(entry)
        card.chip:SetScore(sc.pct)
        -- "0 budget" is not information, it means we have not baked it yet
        local budget = entry.budget and entry.budget.interior
        card.info:SetText(W.TypeLabel(entry.type)
          .. ((budget and budget > 0) and ("  -  " .. budget .. " budget") or ""))
        card.fav:SetText(ns.db.favorites[entry.id] and "*" or "")
        if entry.id == selectedId then
          card:SetBackdropBorderColor(S.gold[1], S.gold[2], S.gold[3], 1)
        else
          card:SetBackdropBorderColor(unpack(S.border))
        end
        card:Show()
      end
    end
  end
  for i = used + 1, #pool do pool[i]:Hide() end

  if UI.emptyText then
    if #view == 0 then
      -- An EMPTY LIBRARY is not a filter problem: telling a day-one user to
      -- "clear filters" when there is nothing to clear reads as broken.
      local libCount = #((ns.Library and ns.Library.entries) or {})
      if UI.activeTab == "favs" then
        UI.emptyText:SetText("No favorites yet - click a card, then the * button.")
      elseif libCount == 0 then
        UI.emptyText:SetText("The library fills up during launch week.\n\n"
          .. "Meanwhile: paste any blueprint code into |cffffd100/bb code <code>|r\n"
          .. "to inspect it, see what it would cost you, and import it.")
      else
        UI.emptyText:SetText("No results - try clearing filters.")
      end
      UI.emptyText:Show()
    else
      UI.emptyText:Hide()
    end
  end
end

function UI.BuildGrid(host)
  scroll = CreateFrame("ScrollFrame", nil, host)
  scroll:SetAllPoints()
  UI.gridScroll = scroll
  inner = CreateFrame("Frame", nil, scroll)
  inner:SetPoint("TOPLEFT")
  inner:SetWidth(1)
  scroll:SetScrollChild(inner)
  scroll:EnableMouseWheel(true)
  scroll:SetScript("OnMouseWheel", function(self, delta)
    local max = math.max(0, inner:GetHeight() - self:GetHeight())
    local target = math.min(max, math.max(0, self:GetVerticalScroll() - delta * 60))
    self:SetVerticalScroll(target)
    layout()
  end)
  scroll:SetScript("OnSizeChanged", function() layout() end)

  UI.emptyText = W.Text(host, S.size.body, S.textDim)
  UI.emptyText:SetPoint("CENTER")
  UI.emptyText:SetWidth(380)          -- the empty-library message is multi-line
  UI.emptyText:SetJustifyH("CENTER")
  UI.emptyText:SetSpacing(3)
  UI.emptyText:Hide()

  ns.bus:On("WINDOW_RESIZED", layout)
end

--- Jump to the top of the grid (deliberate filter/tab changes)
function UI.ResetGridScroll()
  if scroll then scroll:SetVerticalScroll(0) end
end

--- softRefresh = keep the current view list (selection change only)
function UI.RefreshGrid(softRefresh)
  if not scroll then return end
  if UI.activeTab == "mine" then return end   -- grid + empty text stay hidden there
  if not softRefresh then rebuildView() end
  layout()
end
