-- TOG Tools — Log Graphs (Graphs sub-tab engine + per-log graph specs)
--
-- Registers the "graphs" log sub-category (forced first in the Logs strip
-- via `order`) and owns the mapping from each graphable log to a LogGraph
-- spec (see GUI/LogGraph.lua for the spec contract). Keeping the specs here
-- — rather than in each log's own module — leaves the capture engines
-- untouched and collects all "how this log becomes a chart" logic in one
-- place. v0.6.2 graphs Gathering, Mail, and Trade.
--
-- The Graphs view is read-only: it derives its series on the fly from each
-- log's existing entries, so there's nothing to capture, enable, or clear —
-- it has no Settings toggle and no Clear Data button.

local _, addon = ...

local LogGraphs = {}
addon.logCategories = addon.logCategories or {}
addon.logCategories["graphs"] = LogGraphs

LogGraphs.subKey = "graphs"
LogGraphs.label  = "Graphs"
-- Sort ahead of every capture sub-tab (those have no `order`, treated as
-- 100 by LogsTab). Graphs is the overview, so it leads the strip.
LogGraphs.order  = 0
LogGraphs.help = {
    title = "Logs > Graphs",
    lines = {
        "Line charts of your log data over time, powered by LibGraph. Pick which log to chart with the |cffffd700Log|r dropdown.",
        "|cffffd700Period:|r choose the window — last 24 hours (hourly), 7 / 14 / 30 days (daily), or all time.",
        "|cffffd700Filters:|r each log has its own dropdown filters (Gathering: Character / Profession / Zone / Source / Expansion). Narrowing them cuts which lines are charted; \"All\" disables a filter.",
        "|cffffd700Legend:|r the coloured totals line below the chart names each charted series and its total over the period.",
        "|cffffd700Hover the chart|r to see a tooltip for the day (or hour) under your cursor — it lists each series' value for just that point.",
        "All filters and the period are remembered per log.",
    },
}

-- ---------------------------------------------------------------------------
-- Item-expansion resolution (for the Gathering chart)
-- ---------------------------------------------------------------------------
-- Entries logged from v0.6.2 on carry `expac` (captured at loot time). Older
-- entries don't, so fall back to GetItemInfo's expansionID (15th return),
-- cached per itemID. Returns nil when the item isn't cached yet.
local _expacCache = {}
local function entryExpac(e)
    if e.expac ~= nil then return e.expac end
    local id = e.itemID
    if not id then return nil end
    if _expacCache[id] ~= nil then return _expacCache[id] end
    local expac = GetItemInfo and select(15, GetItemInfo(id)) or nil
    if expac ~= nil then _expacCache[id] = expac end
    return expac
end

local function expacName(id)
    if id == nil then return "Unknown" end
    local nm = _G["EXPANSION_NAME" .. id]
    return nm or ("Expansion " .. tostring(id))
end

local MAX_GRAPH_ITEMS = 12   -- cap per-item series; LogGraph caps too

-- ---------------------------------------------------------------------------
-- Filter helpers — build a single-select {value,text} option list (with an
-- "All" row) of the distinct values a field takes across `entries`. Used by the
-- per-log graph filters; the spec supplies how to read + label the field.
-- ---------------------------------------------------------------------------
local function distinctOptions(entries, allLabel, valueOf, textOf)
    local seen, list = {}, {}
    for _, e in ipairs(entries) do
        local v = valueOf(e)
        if v ~= nil and v ~= "" and not seen[v] then
            seen[v] = true
            list[#list + 1] = v
        end
    end
    table.sort(list, function(a, b)
        return tostring(textOf and textOf(a) or a) < tostring(textOf and textOf(b) or b)
    end)
    local opts = { { value = "__all", text = allLabel } }
    for _, v in ipairs(list) do
        opts[#opts + 1] = { value = v, text = textOf and textOf(v) or tostring(v) }
    end
    return opts
end

-- ---------------------------------------------------------------------------
-- Spec builders (lazy category lookups so load order doesn't matter)
-- ---------------------------------------------------------------------------
local function gatheringSpec()
    local cat = addon.logCategories["gathering"]
    if not cat then return nil end
    local function entries() return cat:GetEntries(nil, nil, nil, nil, nil) end

    return {
        key  = "gathering",
        unit = "items",
        getEntries = entries,
        -- getOptions(scoped): `scoped` is the entry set already narrowed by the
        -- OTHER active filters (cascading), passed by the renderer. Falling back
        -- to entries() keeps each builder usable un-scoped. So picking Mining
        -- trims the Item / Zone / Source lists to only mining gathers, etc.
        filters = {
            { key = "player", label = "Character",
              getOptions = function(scoped) return distinctOptions(scoped or entries(), "All characters", function(e) return e.player end) end,
              match = function(e, v) return e.player == v end },
            { key = "prof", label = "Profession",
              getOptions = function(scoped)
                  local seen = {}
                  for _, e in ipairs(scoped or entries()) do if e.prof then seen[e.prof] = true end end
                  local opts = { { value = "__all", text = "All professions" } }
                  for _, p in ipairs(cat.PROF_ORDER or {}) do
                      if seen[p] then opts[#opts + 1] = { value = p, text = cat.PROF_LABEL[p] or p } end
                  end
                  return opts
              end,
              match = function(e, v) return e.prof == v end },
            { key = "zone", label = "Zone",
              getOptions = function(scoped) return distinctOptions(scoped or entries(), "All zones", function(e) return e.zone end) end,
              match = function(e, v) return e.zone == v end },
            { key = "source", label = "Source",
              getOptions = function(scoped) return distinctOptions(scoped or entries(), "All sources", function(e) return e.source end) end,
              match = function(e, v) return e.source == v end },
            { key = "item", label = "Item",
              -- Keyed by itemID (not name) and labelled with the item LINK, so
              -- two items sharing a name (the two copper-ore tiers) are listed
              -- and selectable separately, matching the Gathering log.
              getOptions = function(scoped)
                  local seen, list = {}, {}
                  for _, e in ipairs(scoped or entries()) do
                      if e.itemID and not seen[e.itemID] then
                          seen[e.itemID] = true
                          list[#list + 1] = { id = e.itemID, name = e.name or "",
                              text = e.link or e.name or ("item:" .. e.itemID) }
                      end
                  end
                  table.sort(list, function(a, b)
                      if a.name ~= b.name then return a.name < b.name end
                      return a.id < b.id
                  end)
                  local opts = { { value = "__all", text = "All items" } }
                  for _, it in ipairs(list) do opts[#opts + 1] = { value = it.id, text = it.text } end
                  return opts
              end,
              match = function(e, v) return e.itemID == v end },
            { key = "expac", label = "Expansion",
              getOptions = function(scoped)
                  local seen = {}
                  for _, e in ipairs(scoped or entries()) do local x = entryExpac(e); if x then seen[x] = true end end
                  local ids = {}
                  for id in pairs(seen) do tinsert(ids, id) end
                  table.sort(ids, function(a, b) return a > b end)  -- newest first
                  local opts = { { value = "__all", text = "All expansions" } }
                  for _, id in ipairs(ids) do opts[#opts + 1] = { value = id, text = expacName(id) } end
                  return opts
              end,
              match = function(e, v) return entryExpac(e) == v end },
        },
        -- Rank the items in the (already filtered) set by total gathered and
        -- chart the top MAX_GRAPH_ITEMS as one line each.
        getSeries = function(ents)
            ents = ents or entries()
            local agg = {}
            for _, e in ipairs(ents) do
                if e.itemID then
                    local a = agg[e.itemID]
                    -- Keep the item LINK (quality colour + the reagent-tier
                    -- "pentagon" icon) so two items sharing a name — e.g. the
                    -- two Refulgent Copper Ore tiers — render distinctly, same
                    -- as the Gathering log's Item column (fmtItem = e.link).
                    if not a then a = { id = e.itemID, name = e.name, link = e.link, total = 0 }; agg[e.itemID] = a end
                    a.total = a.total + (e.count or 1)
                end
            end
            local list = {}
            for _, a in pairs(agg) do tinsert(list, a) end
            table.sort(list, function(a, b)
                if a.total ~= b.total then return a.total > b.total end
                return (a.name or "") < (b.name or "")
            end)

            local series = {}
            for i = 1, math.min(#list, MAX_GRAPH_ITEMS) do
                local it = list[i]
                series[i] = {
                    key   = "item:" .. it.id,
                    label = it.link or it.name or ("item:" .. it.id),
                    match = function(e) return e.itemID == it.id end,
                    value = function(e) return e.count or 1 end,
                }
            end
            return series
        end,
    }
end

local function mailSpec()
    local cat = addon.logCategories["mail"]
    if not cat then return nil end
    local function entries() return cat:GetEntries(nil, nil, nil, nil) end
    return {
        key  = "mail",
        unit = "mails",
        getEntries = entries,
        filters = {
            { key = "player", label = "Character",
              getOptions = function() return distinctOptions(entries(), "All characters", function(e) return e.player end) end,
              match = function(e, v) return e.player == v end },
            { key = "dir", label = "Direction",
              getOptions = function()
                  return { { value = "__all", text = "All mail" },
                           { value = "in",  text = "Received" },
                           { value = "out", text = "Sent" } }
              end,
              match = function(e, v) return e.dir == v end },
        },
        getSeries = function()
            return {
                { key = "in",  label = "Received", color = {0.30, 1.00, 0.30, 0.9},
                  match = function(e) return e.dir == "in"  end },
                { key = "out", label = "Sent",     color = {0.00, 0.80, 1.00, 0.9},
                  match = function(e) return e.dir == "out" end },
            }
        end,
    }
end

local function tradeSpec()
    local cat = addon.logCategories["trade"]
    if not cat then return nil end
    local function entries() return cat:GetEntries(nil, nil, nil) end
    return {
        key  = "trade",
        unit = "items",
        getEntries = entries,
        filters = {
            { key = "player", label = "Character",
              getOptions = function() return distinctOptions(entries(), "All characters", function(e) return e.player end) end,
              match = function(e, v) return e.player == v end },
            { key = "other", label = "Partner",
              getOptions = function() return distinctOptions(entries(), "All partners", function(e) return e.other end) end,
              match = function(e, v) return e.other == v end },
        },
        getSeries = function()
            return {
                { key = "recv", label = "Items received", color = {0.30, 1.00, 0.30, 0.9},
                  match = function(e) return e.received and #e.received > 0 end,
                  value = function(e) return #(e.received or {}) end },
                { key = "give", label = "Items given",    color = {1.00, 0.55, 0.30, 0.9},
                  match = function(e) return e.given and #e.given > 0 end,
                  value = function(e) return #(e.given or {}) end },
            }
        end,
    }
end

-- Ordered, gated list of graphable logs for the picker. Each is included only
-- when its category exists AND its capture is enabled (no point charting a log
-- the user has turned off). label comes from the source category.
local GRAPHABLE = {
    { key = "gathering", build = gatheringSpec },
    { key = "mail",      build = mailSpec },
    { key = "trade",     build = tradeSpec },
}

function LogGraphs:GetGraphableLogs()
    local out = {}
    for _, g in ipairs(GRAPHABLE) do
        local cat = addon.logCategories[g.key]
        if cat and addon:IsLogCategoryEnabled(g.key) then
            local spec = g.build()
            if spec then
                tinsert(out, { key = g.key, label = cat.label or g.key, spec = spec })
            end
        end
    end
    return out
end

-- Per-log graph preference table (period / series visibility / sub-filter),
-- created on demand under db.global.logGraphs.byLog.
function LogGraphs:GetPrefs(logKey)
    local lg = addon.DB and addon.DB.global and addon.DB.global.logGraphs
    if not lg then return { period = 1, series = {} } end
    lg.byLog = lg.byLog or {}
    lg.byLog[logKey] = lg.byLog[logKey] or { period = 1, series = {} }
    return lg.byLog[logKey]
end
