-- TOG Tools — Date range picker (shared)
--
-- Dropdown over preset lookback ranges + an optional inline calendar
-- grid for the Custom range. Returns (fromTs, toTs) bounds for
-- filtering log entries. Used by every Logs sub-tab so adding a new
-- sub-tab doesn't reimplement filter math or the calendar widget.

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

addon.DateRangePicker = addon.DateRangePicker or {}
local DRP = addon.DateRangePicker

-- Range presets. `seconds` is the lookback in seconds; nil = unbounded;
-- "custom" is a sentinel — Resolve() needs customFrom/customTo args to
-- compute bounds for it (range from the picked start day to the picked
-- end day, inclusive midnight-to-midnight).
DRP.RANGES = {
    { key = "1d",     label = "Last 24 hours", seconds = 86400 },
    { key = "7d",     label = "Last 7 days",   seconds = 7  * 86400 },
    { key = "30d",    label = "Last 30 days",  seconds = 30 * 86400 },
    { key = "all",    label = "All",           seconds = nil },
    { key = "custom", label = "Custom...",     seconds = nil, custom = true },
}

DRP.DEFAULT_KEY = "7d"

--- Resolve a preset key to (fromTs, toTs). For "custom":
---   * `customFrom` is the start-day timestamp the user picked
---   * `customTo` is the end-day timestamp (nil for single-day picks
---     — treated as same-as-customFrom)
--- Returns a midnight-to-end-of-day-inclusive window:
---   fromTs = start of customFrom's day (00:00:00)
---   toTs   = end   of customTo's   day (23:59:59) — entries are
---            filtered with `<= toTs`, so this includes everything
---            that happened on the end day.
--- For other keys, customFrom/customTo are ignored.
function DRP:Resolve(key, customFrom, customTo)
    local now = (GetServerTime and GetServerTime()) or time()
    if key == "custom" then
        if not customFrom or customFrom <= 0 then
            -- No range picked yet — match nothing so the table goes
            -- empty and the calendar prompts the user to pick.
            return now + 1, now + 1
        end
        local fromDay = customFrom - (customFrom % 86400)
        local toDay
        if customTo and customTo > 0 and customTo > customFrom then
            toDay = customTo - (customTo % 86400)
        else
            toDay = fromDay  -- single-day range
        end
        return fromDay, toDay + 86399
    end
    for _, r in ipairs(self.RANGES) do
        if r.key == key then
            if r.seconds then return now - r.seconds, now end
            return nil, now
        end
    end
    return now - 7 * 86400, now
end

--- Construct an AceGUI Dropdown bound to one of the preset ranges. opts:
---   default   string  initial selection (defaults to DEFAULT_KEY)
---   onChange  func    fires with the new key on every selection change
---   width     number  dropdown width (default 160)
---   label     string  label above the dropdown (default "Date range")
---
--- Returns the AceGUI Dropdown widget.
function DRP:Create(opts)
    opts = opts or {}
    local dd = AceGUI:Create("Dropdown")
    dd:SetLabel(opts.label or "Date range")
    dd:SetWidth(opts.width or 160)
    local list, order = {}, {}
    for _, r in ipairs(self.RANGES) do
        list[r.key] = r.label
        tinsert(order, r.key)
    end
    dd:SetList(list, order)
    dd:SetValue(opts.default or self.DEFAULT_KEY)
    if opts.onChange then
        dd:SetCallback("OnValueChanged", function(_, _, key) opts.onChange(key) end)
    end
    return dd
end

-- ---------------------------------------------------------------------------
-- Inline calendar grid
-- ---------------------------------------------------------------------------

local MONTH_NAMES = { "Jan","Feb","Mar","Apr","May","Jun",
                      "Jul","Aug","Sep","Oct","Nov","Dec" }
local WEEKDAY_NAMES = { "Sun","Mon","Tue","Wed","Thu","Fri","Sat" }
local LAST_DAY = { 31,28,31,30,31,30,31,31,30,31,30,31 }

local function isLeap(y)
    return (y % 4 == 0 and y % 100 ~= 0) or y % 400 == 0
end

local function lastDayOf(y, m)
    if m == 2 and isLeap(y) then return 29 end
    return LAST_DAY[m] or 31
end

-- "Weekday of day 1 of (y, m)" returned as 0 (Sun) ... 6 (Sat). Used to
-- decide how many blank cells to leave before the 1st of the month.
local function firstWeekday(y, m)
    local ts = time({ year = y, month = m, day = 1, hour = 12 })
    return tonumber(date("%w", ts)) or 0
end

-- Walk `timestamps` and build a nested set: available[year][month][day] = true.
-- Iterates the entire list once per calendar build. For tens of thousands of
-- entries this could be slow; if it becomes a problem we can cache the set
-- on the picker and dirty-flag it from the sub-tabs.
local function buildAvailable(timestamps)
    local out = {}
    for _, ts in ipairs(timestamps or {}) do
        if ts and ts > 0 then
            local y = tonumber(date("%Y", ts))
            local m = tonumber(date("%m", ts))
            local d = tonumber(date("%d", ts))
            if y and m and d then
                out[y] = out[y] or {}
                out[y][m] = out[y][m] or {}
                out[y][m][d] = true
            end
        end
    end
    return out
end

-- Sorted descending list of years that have entries. Falls back to the
-- current year if there are none, so the dropdown always has something.
local function yearsFrom(available)
    local years = {}
    for y in pairs(available) do tinsert(years, y) end
    table.sort(years, function(a, b) return a > b end)
    if #years == 0 then tinsert(years, tonumber(date("%Y")) or 2026) end
    return years
end

--- Build an inline calendar grid widget with two-click range selection.
--- opts:
---   defaultFrom number  start-day ts to pre-select (nil = none)
---   defaultTo   number  end-day ts to pre-select (nil = same as
---                       defaultFrom → single-day range)
---   timestamps  array   entry timestamps used to populate the year
---                       dropdown (years with data + current year)
---   onApply     func    fires when user clicks Apply; receives
---                       (fromTs, toTs). toTs is nil for a single-day
---                       pick (no end day selected). Both ts values
---                       are at hour=12 of the picked day — the
---                       sub-tab's Resolve expands to day-bounds.
---
--- Click logic:
---   * 1st click  — sets start day
---   * 2nd click later than start  — sets end day (range complete)
---   * 2nd click earlier than start — treated as a new start (per user
---     preference; simpler than auto-swapping start/end)
---   * 3rd click anywhere — starts a fresh range from that click
---   * Apply enabled iff a start day is set
function DRP:CreateCalendar(opts)
    opts = opts or {}
    local available = buildAvailable(opts.timestamps)
    local years     = yearsFrom(available)

    -- Decompose initial defaults into (y, m, d) tuples. defaultTo is
    -- optional — nil means "single-day range at defaultFrom".
    local function decompose(ts)
        if not ts or ts <= 0 then return nil, nil, nil end
        return tonumber(date("%Y", ts)), tonumber(date("%m", ts)), tonumber(date("%d", ts))
    end
    local startY, startM, startD = decompose(opts.defaultFrom)
    local endY,   endM,   endD   = decompose(opts.defaultTo)

    -- Initial displayed year/month: from defaultFrom if provided, else
    -- newest year with entries + that year's newest month, else current.
    local defaultY, defaultM
    if startY then
        defaultY, defaultM = startY, startM
    else
        defaultY = years[1]
        if available[defaultY] then
            local maxM = 0
            for m in pairs(available[defaultY]) do if m > maxM then maxM = m end end
            defaultM = maxM > 0 and maxM or (tonumber(date("%m")) or 1)
        else
            defaultM = tonumber(date("%m")) or 1
        end
    end

    -- AceGUI SimpleGroup as the outer wrapper so the calendar slots into
    -- any AceGUI container (Flow, List, Fill). All raw frames parent to
    -- `group.content` — the canonical place for children inside an
    -- AceGUI container.
    --
    -- Critical: SimpleGroup's `LayoutFinished` auto-sets its own height
    -- to the measured AceGUI children's height — and we have NO AceGUI
    -- children (only raw frames parented to .content), so without
    -- intervention LayoutFinished collapses height to 0. The parent's
    -- Flow then places the next sibling (body) right on top of our
    -- (zero-height) calendar group. AceGUI's escape hatch is the
    -- `noAutoHeight` flag, which makes LayoutFinished no-op. Set it
    -- BEFORE any layout pass that could fire LayoutFinished, then
    -- SetHeight survives.
    local group = AceGUI:Create("SimpleGroup")
    group.noAutoHeight = true
    group:SetLayout("Fill")
    group:SetFullWidth(true)
    group:SetHeight(210)   -- dropdowns row + weekday header + 6 grid rows + a small bottom margin

    local host = group.content

    -- Widget-pool detach: AceGUI recycles SimpleGroup widgets, so the
    -- underlying .content frame survives Release() and gets re-handed-
    -- out on the next CreateCalendar. Without explicit detachment, our
    -- 42 day buttons + dropdowns + weekday fontstrings ride along on
    -- the recycled .content — each new render layers another set on
    -- top, visually stacking the buttons and pushing later AceGUI
    -- siblings (the empty-state label inside `body`) up into the
    -- button area. Track every raw child we create and detach on
    -- OnRelease. UI.DetachChildren is keyed-name-based and doesn't fit
    -- a 42-element loop; this is the array equivalent.
    local rawChildren = {}
    local function track(frame) tinsert(rawChildren, frame); return frame end

    group:SetCallback("OnRelease", function()
        for _, f in ipairs(rawChildren) do
            if f.Hide          then f:Hide()             end
            if f.SetParent     then f:SetParent(UIParent) end
            if f.ClearAllPoints then f:ClearAllPoints()  end
        end
        rawChildren = {}
    end)

    -- state.y / state.m         — currently DISPLAYED year + month
    -- state.startY/M/D          — first click of the range (or
    --                             defaultFrom). Highlighted distinctly.
    -- state.endY/M/D            — second click of the range. nil means
    --                             "single-day pick" — Apply commits
    --                             one day, not a range.
    -- All picked-day state persists across year/month navigation so
    -- Apply commits the actually-chosen range even after the user
    -- scrolls to look at other months.
    local state = {
        y      = defaultY,
        m      = defaultM,
        startY = startY, startM = startM, startD = startD,
        endY   = endY,   endM   = endM,   endD   = endD,
    }

    -- Sortable integer key for a (year, month, day) tuple — lets us
    -- compare two days as plain numbers.
    local function dayKey(y, m, d) return y * 10000 + m * 100 + d end

    -- ---- Year / Month dropdowns -----------------------------------------
    -- Native CreateFrame dropdowns; AceGUI's are heavier and harder to
    -- position precisely in a fixed-width header.
    local function dropdown(parent, x, width, list, onSelect, current)
        local dd = track(CreateFrame("Frame", nil, parent, "UIDropDownMenuTemplate"))
        dd:SetPoint("TOPLEFT", parent, "TOPLEFT", x, -2)
        UIDropDownMenu_SetWidth(dd, width)
        UIDropDownMenu_Initialize(dd, function()
            for _, entry in ipairs(list) do
                local info = UIDropDownMenu_CreateInfo()
                info.text = entry.label
                info.checked = (entry.value == current())
                info.func = function()
                    onSelect(entry.value)
                    UIDropDownMenu_SetText(dd, entry.label)
                end
                UIDropDownMenu_AddButton(info)
            end
        end)
        local label = current()
        for _, entry in ipairs(list) do
            if entry.value == label then UIDropDownMenu_SetText(dd, entry.label) break end
        end
        return dd
    end

    -- Year list — descending years with data; current always included.
    local yearItems = {}
    local thisYear = tonumber(date("%Y")) or 2026
    local seenYears = {}
    for _, y in ipairs(years) do
        tinsert(yearItems, { value = y, label = tostring(y) })
        seenYears[y] = true
    end
    if not seenYears[thisYear] then
        tinsert(yearItems, 1, { value = thisYear, label = tostring(thisYear) })
    end

    local monthItems = {}
    for i = 1, 12 do
        tinsert(monthItems, { value = i, label = MONTH_NAMES[i] })
    end

    local dayButtons = {}   -- 42 cells, 7x6 grid
    local applyBtn          -- forward-declared so redraw() can toggle its enable state

    -- True iff (state.y, state.m, dayNum) falls inside the picked
    -- range. With only start set, only the start day matches; with
    -- start + end, every day between the two (inclusive) matches.
    local function inRange(dayNum)
        if not state.startY then return false end
        local k  = dayKey(state.y, state.m, dayNum)
        local sk = dayKey(state.startY, state.startM, state.startD)
        if not state.endY then return k == sk end
        local ek = dayKey(state.endY, state.endM, state.endD)
        return k >= sk and k <= ek
    end

    local function redraw()
        local first = firstWeekday(state.y, state.m)   -- 0..6 (Sun..Sat)
        local last  = lastDayOf(state.y, state.m)

        for i = 1, 42 do
            local btn = dayButtons[i]
            local dayNum = i - first
            if dayNum < 1 or dayNum > last then
                btn:Hide()
            else
                btn:Show()
                btn:Enable()
                btn:SetText(tostring(dayNum))
                if inRange(dayNum) then
                    btn:LockHighlight()
                else
                    btn:UnlockHighlight()
                end
            end
        end
        -- Apply enabled iff a start day is set. A single click (start
        -- only, no end) commits as a one-day range.
        if applyBtn then
            if state.startY then applyBtn:Enable() else applyBtn:Disable() end
        end
    end

    -- Build dropdowns. Return values are unused — the widgets live on
    -- `host` and respond to their own callbacks. We don't need to talk
    -- to them again after construction.
    dropdown(host, -10, 60, yearItems,
        function(v) state.y = v; redraw() end,
        function() return state.y end)

    dropdown(host, 70, 70, monthItems,
        function(v) state.m = v; redraw() end,
        function() return state.m end)

    -- ---- Weekday header --------------------------------------------------
    -- Fontstrings are parented to `host` so they're kept alive by the
    -- frame; no local refs needed after positioning.
    local CELL_W, CELL_H = 36, 22
    local GRID_TOP = -34  -- below the dropdowns
    for i = 1, 7 do
        local fs = track(host:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall"))
        fs:SetPoint("TOPLEFT", host, "TOPLEFT", (i - 1) * CELL_W + 4, GRID_TOP)
        fs:SetSize(CELL_W, CELL_H)
        fs:SetText(WEEKDAY_NAMES[i])
        fs:SetJustifyH("CENTER")
    end

    -- ---- Day grid --------------------------------------------------------
    for row = 0, 5 do
        for col = 0, 6 do
            local idx = row * 7 + col + 1
            local btn = track(CreateFrame("Button", nil, host, "UIPanelButtonTemplate"))
            btn:SetSize(CELL_W - 2, CELL_H - 2)
            btn:SetPoint("TOPLEFT", host, "TOPLEFT",
                col * CELL_W + 4,
                GRID_TOP - CELL_H - row * CELL_H)
            btn:SetScript("OnClick", function()
                local first = firstWeekday(state.y, state.m)
                local dayNum = idx - first
                if dayNum < 1 then return end
                local last = lastDayOf(state.y, state.m)
                if dayNum > last then return end
                -- Two-click range pick:
                --   * No start set       → 1st click: set start
                --   * Start set, no end  → 2nd click: set end iff later
                --                          than start; otherwise treat
                --                          as a new start (per user
                --                          preference — simpler than
                --                          auto-swapping endpoints).
                --   * Both set           → 3rd click: fresh range
                local clicked = dayKey(state.y, state.m, dayNum)
                if not state.startY then
                    state.startY, state.startM, state.startD = state.y, state.m, dayNum
                    state.endY, state.endM, state.endD = nil, nil, nil
                elseif not state.endY then
                    local sk = dayKey(state.startY, state.startM, state.startD)
                    if clicked < sk then
                        -- Earlier than start → new start, no end
                        state.startY, state.startM, state.startD = state.y, state.m, dayNum
                    else
                        -- Same day or later → set end (single-day range
                        -- if clicked == start)
                        state.endY, state.endM, state.endD = state.y, state.m, dayNum
                    end
                else
                    -- Fresh range starting at this click
                    state.startY, state.startM, state.startD = state.y, state.m, dayNum
                    state.endY, state.endM, state.endD = nil, nil, nil
                end
                redraw()
            end)
            dayButtons[idx] = btn
        end
    end

    -- ---- Apply button ---------------------------------------------------
    -- Sits below the day grid. Disabled until a day is picked. Clicking
    -- Apply commits the picked date via opts.onApply and (by sub-tab
    -- convention) collapses the calendar.
    applyBtn = track(CreateFrame("Button", nil, host, "UIPanelButtonTemplate"))
    applyBtn:SetSize(80, 24)
    applyBtn:SetPoint("TOPRIGHT", host, "TOPRIGHT", -10, -2)
    applyBtn:SetText("Apply")
    applyBtn:SetScript("OnClick", function()
        if not state.startY then return end
        local fromTs = time({
            year = state.startY, month = state.startM,
            day  = state.startD, hour  = 12,
        })
        local toTs
        if state.endY then
            toTs = time({
                year = state.endY, month = state.endM,
                day  = state.endD, hour  = 12,
            })
        end
        -- toTs is nil for single-day picks; sub-tab's Resolve treats
        -- nil end as "same day as start" and produces a one-day window.
        if opts.onApply then opts.onApply(fromTs, toTs) end
    end)

    redraw()
    return group
end
