-- TOG Tools — Whisper Log sub-tab (UI)
-- Renders inside the Logs outer tab's nested AceGUI TabGroup. Layout:
--   Filter row:  Direction ▾   Type ▾   GM ▾   Date range ▾   [🔍 search]
--   Data table:  Time · Character · Other · Message
--
-- Character + partner + message filtering now lives in the column-header menus
-- and the single search box (case-insensitive substring across every column).
-- Direction / Type / GM aren't columns, so they stay as dropdowns. Filter state
-- lives on the module table so it survives Draw rebuilds, but resets on /reload.

local _, addon = ...
local AceGUI = LibStub("AceGUI-3.0")

local WhisperLog = addon.logCategories["whispers"]

WhisperLog._filterDir          = WhisperLog._filterDir          or nil
WhisperLog._filterType         = WhisperLog._filterType         or nil
WhisperLog._filterGM           = WhisperLog._filterGM           or nil
WhisperLog._filterRange        = WhisperLog._filterRange        or addon.DateRangePicker.DEFAULT_KEY
WhisperLog._filterCustomFrom   = WhisperLog._filterCustomFrom   or nil
WhisperLog._filterCustomTo     = WhisperLog._filterCustomTo     or nil
WhisperLog._filterCalendarOpen = WhisperLog._filterCalendarOpen or false
WhisperLog._search             = WhisperLog._search             or ""

-- ---------------------------------------------------------------------------
-- Cell formatters
-- ---------------------------------------------------------------------------
local function fmtTime(ts)
    if not ts or ts <= 0 then return "—" end
    local now = (GetServerTime and GetServerTime()) or time()
    local diff = now - ts
    if diff < 60      then return string.format("%ds ago", math.floor(diff)) end
    if diff < 3600    then return string.format("%dm ago", math.floor(diff / 60)) end
    if diff < 86400   then return string.format("%dh ago", math.floor(diff / 3600)) end
    if diff < 604800  then return string.format("%dd ago", math.floor(diff / 86400)) end
    return date("%m/%d/%y", ts)
end

-- Other column folds direction + isBN + isGM annotation into a single cell
-- to keep the table compact. Green arrow = incoming, orange = outgoing,
-- purple [BN] tag for Battle.net whispers, red [GM] for Blizzard staff.
--
-- For in-game whispers, the partner name is wrapped in a |Hplayer:NAME|h
-- hyperlink so left-click opens a whisper edit box and right-click pops
-- the standard chat-name context menu (whisper / invite / inspect /
-- ignore / report). RowList.lua routes player links through SetItemRef
-- which has version-specific dispatch baked in. Skipped for BN whispers
-- (different link type — |HBNplayer:|h — and cross-version BN routing
-- is fragile enough that v1 just renders BN partners as plain text) and
-- for unknown senders ("?").
local function fmtOther(e)
    local other = e.other or "?"
    -- Legacy BN entries (logged before we resolved names at capture time) stored
    -- the raw Kstring token (e.g. "|Kq2|k"); rendered now it re-resolves to
    -- whichever presence currently holds that slot — the "wrong person" bug. Mask
    -- it with a neutral label so old entries stop showing a mislabelled name.
    if e.isBN and other:find("|K", 1, true) then
        other = "Battle.net friend"
    end
    local prefix
    if e.dir == "in"  then prefix = "|cff66ff66<-|r "
    elseif e.dir == "out" then prefix = "|cffffaa55->|r "
    else                       prefix = "" end
    local tags = ""
    if e.isBN then tags = tags .. "|cffaa44ff[BN]|r " end
    if e.isGM then tags = tags .. "|cffff5555[GM]|r " end
    local nameCell
    if not e.isBN and other ~= "?" and other ~= "" then
        nameCell = "|Hplayer:" .. other .. "|h[" .. other .. "]|h"
    else
        nameCell = other
    end
    return prefix .. tags .. nameCell
end

local function fmtText(text)
    if not text or text == "" then return "|cff888888—|r" end
    return text
end

-- ---------------------------------------------------------------------------
-- Filter widget builders
-- ---------------------------------------------------------------------------
local function buildDirDropdown(onChange)
    local dd = AceGUI:Create("Dropdown")
    dd:SetLabel("Direction")
    dd:SetWidth(120)
    dd:SetList({ __both = "Both", ["in"] = "Received", ["out"] = "Sent" },
               { "__both", "in", "out" })
    dd:SetValue(WhisperLog._filterDir or "__both")
    dd:SetCallback("OnValueChanged", function(_, _, key)
        WhisperLog._filterDir = (key == "__both") and nil or key
        onChange()
    end)
    return dd
end

local function buildTypeDropdown(onChange)
    local dd = AceGUI:Create("Dropdown")
    dd:SetLabel("Type")
    dd:SetWidth(130)
    dd:SetList({ __both = "Both", ingame = "In-game", bn = "Battle.net" },
               { "__both", "ingame", "bn" })
    dd:SetValue(WhisperLog._filterType or "__both")
    dd:SetCallback("OnValueChanged", function(_, _, key)
        WhisperLog._filterType = (key == "__both") and nil or key
        onChange()
    end)
    return dd
end

local function buildGMDropdown(onChange)
    local dd = AceGUI:Create("Dropdown")
    dd:SetLabel("GM")
    dd:SetWidth(110)
    dd:SetList({ __all = "All", nogm = "Hide GM", gm = "GM only" },
               { "__all", "nogm", "gm" })
    dd:SetValue(WhisperLog._filterGM or "__all")
    dd:SetCallback("OnValueChanged", function(_, _, key)
        WhisperLog._filterGM = (key == "__all") and nil or key
        onChange()
    end)
    return dd
end

-- ---------------------------------------------------------------------------
-- Draw
-- ---------------------------------------------------------------------------
function WhisperLog:Draw(container)
    container:SetLayout("Flow")

    local function rebuild()
        if addon.MainWindow and addon.MainWindow.Refresh then
            addon.MainWindow:Refresh()
        end
    end

    local rl   -- data table; forward-declared so the search box can reach it

    -- ---- Filter row -----------------------------------------------------
    local filterRow = AceGUI:Create("SimpleGroup")
    filterRow:SetFullWidth(true)
    filterRow:SetLayout("Flow")
    container:AddChild(filterRow)

    filterRow:AddChild(buildDirDropdown(rebuild))
    filterRow:AddChild(buildTypeDropdown(rebuild))
    filterRow:AddChild(buildGMDropdown(rebuild))
    filterRow:AddChild(addon.DateRangePicker:Create({
        default  = self._filterRange,
        onChange = function(key)
            local enteringCustom = (key == "custom" and self._filterRange ~= "custom")
            self._filterRange = key
            if enteringCustom then self._filterCalendarOpen = true end
            rebuild()
        end,
    }))

    local search = AceGUI:Create("EditBox")
    search:SetWidth(240)
    search:DisableButton(true)
    search:SetText(self._search or "")
    search:SetCallback("OnTextChanged", function(_, _, text)
        self._search = text
        if rl then rl:SetSearch(text) end
    end)
    addon.UI.StyleSearchBox(search, true)
    filterRow:AddChild(search)

    if self._filterRange == "custom" and self._filterCustomFrom
       and not self._filterCalendarOpen then
        local pickBtn = AceGUI:Create("Button")
        pickBtn:SetWidth(140)
        pickBtn:SetText("Pick range...")
        pickBtn:SetCallback("OnClick", function()
            self._filterCalendarOpen = true
            rebuild()
        end)
        filterRow:AddChild(pickBtn)
    end

    if self._filterRange == "custom"
       and (not self._filterCustomFrom or self._filterCalendarOpen) then
        local unbounded = WhisperLog:GetEntries(nil, self._filterDir,
            self._filterType, self._filterGM, nil, nil, nil, nil)
        local timestamps = {}
        for _, e in ipairs(unbounded) do tinsert(timestamps, e.ts) end
        container:AddChild(addon.DateRangePicker:CreateCalendar({
            defaultFrom = self._filterCustomFrom,
            defaultTo   = self._filterCustomTo,
            timestamps  = timestamps,
            onApply     = function(fromTs, toTs)
                self._filterCustomFrom   = fromTs
                self._filterCustomTo     = toTs
                self._filterCalendarOpen = false
                rebuild()
            end,
        }))
        if not self._filterCustomFrom then return end
    end

    -- Spacer between the filters and the data table
    local sp = AceGUI:Create("Label")
    sp:SetFullWidth(true); sp:SetText(" ")
    container:AddChild(sp)

    -- ---- Data table -----------------------------------------------------
    local body = AceGUI:Create("SimpleGroup")
    body:SetFullWidth(true)
    body:SetFullHeight(true)
    body:SetLayout("List")
    container:AddChild(body)

    local fromTs, toTs = addon.DateRangePicker:Resolve(
        self._filterRange, self._filterCustomFrom, self._filterCustomTo)
    local entries = WhisperLog:GetEntries(nil, self._filterDir,
        self._filterType, self._filterGM, nil, nil, fromTs, toTs)

    if #entries == 0 then
        local empty = AceGUI:Create("Label")
        empty:SetFullWidth(true)
        empty:SetText("|cff888888No whisper entries match the current filters.|r\n\nWhisper Log captures every whisper sent or received (in-game and Battle.net). Widen the |cffffd700Date range|r, clear the |cffffd700search|r box, or check the column-header filters.")
        body:AddChild(empty)
        return
    end

    rl = addon.RowList:New(body.content, {
        rowHeight = 16,
        columns = {
            { key = "ts", header = "Time", autoFit = true, minWidth = 50, justify = "LEFT",
              sortKey = "ts", sortDescDefault = true,
              headerTip = "Time",
              headerTipDesc = "When the whisper was sent or received. Recent entries show as \"Nm/h/d ago\"; older entries fall back to MM/DD/YY. Click to sort by time. Column auto-sizes to fit the longest captured timestamp.",
              format = function(e) return fmtTime(e.ts) end },

            { key = "player", header = "Character", autoFit = true, minWidth = 60, justify = "LEFT",
              headerTip = "Character",
              headerTipDesc = "The alt that sent or received this whisper. Whisper Log is account-wide; click this column header to filter by character, or use the search box. Column auto-sizes to fit the longest captured name.",
              format = function(e) return e.player or "?" end },

            { key = "other", header = "Other", autoFit = true, minWidth = 80, justify = "LEFT",
              headerTip = "Other",
              headerTipDesc = "The other side of the whisper. Green |cff66ff66<-|r prefix = received from this player; orange |cffffaa55->|r prefix = sent to this player. Purple |cffaa44ff[BN]|r tag = Battle.net whisper; red |cffff5555[GM]|r tag = Blizzard GM whisper. For in-game partners, |cffffd700left-click|r the bracketed name to open a whisper edit box, |cffffd700right-click|r to pop the standard chat-name menu (whisper / invite / inspect / ignore / report). Battle.net partners render as plain text in this version. Use the search box above for substring search across every column.",
              format = function(e) return fmtOther(e) end },

            { key = "text", header = "Message", justify = "LEFT",
              headerTip = "Message",
              headerTipDesc = "The whisper body. This is the rightmost stretchy column — the only column that may truncate when the window is too narrow; every other column auto-sizes to fit its content. Use the search box above to find specific phrases (case-insensitive substring match across every column).",
              format = function(e) return fmtText(e.text) end },
        },
    })
    rl:SetSort("ts", true)
    rl:SetData(entries)
    if self._search and self._search ~= "" then rl:SetSearch(self._search) end

    body:SetCallback("OnRelease", function() rl:Detach() end)
end
