--[[
    UI/PetList.lua

    The virtualized list widget. Knows how to draw rows; knows nothing about
    pets, stables or sightings — it is handed a flat list of element tables and
    a set of callbacks, which is what lets all three tabs share it.

    Built on Blizzard's WowScrollBoxList stack, which recycles a small pool of
    frames instead of creating one per record. With a few thousand entries the
    number of live frames stays proportional to the visible area.

    ---------------------------------------------------------------------------
    Element contract
    ---------------------------------------------------------------------------

        { kind = "header",
          name, counterText, icon, collapsible, collapsed, token }

        { name, subtitle, icon, portraitDisplayID, catalogText, desaturated,
          statusText, statusColor, selectionID }

    Everything except `name` is optional. A row with no `selectionID` cannot be
    selected, which is how the stable and sighting tabs get read-only lists out
    of the same widget.

    ---------------------------------------------------------------------------
    How the recycling contract works
    ---------------------------------------------------------------------------

    `SetElementInitializer` takes ONE frame type for every element, so a frame
    that drew a header a moment ago may be handed a row next. Child regions are
    therefore created once and guarded, every visual property is re-set on every
    call, and `ResetRegions` hides everything first so each layout only has to
    show what it actually uses.
]]

local _, ns = ...

local PetList = ns.NewModule("PetList")
ns.UI = ns.UI or {}
ns.UI.PetList = PetList

local ROW_HEIGHT = 58
local HEADER_HEIGHT = 32
local ROW_INSET = 5

local ICON_SIZE = 42
local HEADER_ICON_SIZE = 18

local FAVORITE_COLOR = { r = 1.00, g = 0.82, b = 0.10 }
local HEADER_COLOR = { r = 1.00, g = 0.82, b = 0.10 }
local NAME_COLOR = { r = 0.95, g = 0.95, b = 0.95 }
local SUBTITLE_COLOR = { r = 0.55, g = 0.55, b = 0.55 }
local Theme = ns.UI.Theme

--=============================================================================
-- Row regions
--=============================================================================

local function EnsureRegions(frame)
    if frame.Name then
        return
    end

    frame.Background = frame:CreateTexture(nil, "BACKGROUND")
    frame.Background:SetPoint("TOPLEFT", frame, "TOPLEFT", ROW_INSET, -3)
    frame.Background:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -ROW_INSET, 3)

    frame.Selection = frame:CreateTexture(nil, "BORDER")
    frame.Selection:SetWidth(3)
    frame.Selection:SetPoint("TOPLEFT", frame.Background, "TOPLEFT", 0, 0)
    frame.Selection:SetPoint("BOTTOMLEFT", frame.Background, "BOTTOMLEFT", 0, 0)

    frame.PortraitRing = frame:CreateTexture(nil, "BORDER")
    frame.PortraitRing:SetAtlas("footer_inactive-ring")

    frame.Icon = frame:CreateTexture(nil, "ARTWORK")

    frame.IconMask = frame:CreateMaskTexture()
    frame.IconMask:SetTexture("Interface\\CharacterFrame\\TempPortraitAlphaMask",
        "CLAMPTOBLACKADDITIVE", "CLAMPTOBLACKADDITIVE")
    frame.Icon:AddMaskTexture(frame.IconMask)

    -- A font glyph rendered as a square on the Spanish client.  This is the
    -- first cell of the client-shipped raid marker sheet: the gold star.
    frame.Favorite = frame:CreateTexture(nil, "OVERLAY")
    frame.Favorite:SetTexture("Interface\\TargetingFrame\\UI-RaidTargetingIcons")
    frame.Favorite:SetTexCoord(0, 0.25, 0, 0.25)
    frame.Favorite:SetSize(16, 16)

    frame.Name = frame:CreateFontString(nil, "ARTWORK", "GameFontNormal")
    frame.Name:SetJustifyH("LEFT")
    frame.Name:SetWordWrap(false)

    frame.Subtitle = frame:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
    frame.Subtitle:SetJustifyH("LEFT")
    frame.Subtitle:SetWordWrap(false)

    frame.Status = frame:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
    frame.Status:SetJustifyH("RIGHT")
    frame.Status:SetWordWrap(false)

    frame.Catalog = frame:CreateFontString(nil, "ARTWORK", "GameFontDisableSmall")
    frame.Catalog:SetJustifyH("RIGHT")
    frame.Catalog:SetWordWrap(false)

    frame.Toggle = frame:CreateFontString(nil, "ARTWORK", "GameFontNormalLarge")
    frame.Toggle:SetJustifyH("CENTER")
end

local function ResetRegions(frame)
    frame.Background:Hide()
    frame.Selection:Hide()
    frame.PortraitRing:Hide()
    frame.Icon:Hide()
    frame.Favorite:Hide()
    frame.Subtitle:Hide()
    frame.Toggle:Hide()
    frame.Catalog:Hide()

    frame.Name:ClearAllPoints()
    frame.Status:ClearAllPoints()
    frame.Catalog:ClearAllPoints()
    frame.Icon:ClearAllPoints()
    frame.Icon:SetDesaturated(false)
    frame.Icon:SetVertexColor(1, 1, 1, 1)
    frame.PortraitRing:ClearAllPoints()
    frame.IconMask:ClearAllPoints()
end

--=============================================================================
-- Layouts
--=============================================================================

local function LayoutAsHeader(frame, element)
    local anchor = frame
    local anchorPoint, anchorX = "LEFT", ROW_INSET + 2

    if element.collapsible then
        frame.Toggle:SetPoint("LEFT", frame, "LEFT", ROW_INSET + 2, 0)
        frame.Toggle:SetWidth(14)
        -- A plain +/- rather than an atlas: no expander atlas name has been
        -- verified on this client, and an unverified texture draws nothing.
        frame.Toggle:SetText(element.collapsed and "+" or "-")
        frame.Toggle:SetTextColor(HEADER_COLOR.r, HEADER_COLOR.g, HEADER_COLOR.b)
        frame.Toggle:Show()

        anchor, anchorPoint, anchorX = frame.Toggle, "RIGHT", 4
    end

    if element.icon then
        frame.Icon:SetSize(HEADER_ICON_SIZE, HEADER_ICON_SIZE)
        frame.Icon:SetPoint("LEFT", anchor, anchorPoint, anchorX, 0)
        frame.Icon:SetTexture(element.icon)
        frame.Icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
        frame.Icon:Show()

        anchor, anchorPoint, anchorX = frame.Icon, "RIGHT", 6
    end

    frame.Status:SetPoint("RIGHT", frame, "RIGHT", -ROW_INSET - 4, 0)
    frame.Status:SetText(element.counterText or "")
    frame.Status:SetTextColor(0.65, 0.65, 0.65)

    frame.Name:SetPoint("LEFT", anchor, anchorPoint, anchorX, 0)
    frame.Name:SetPoint("RIGHT", frame.Status, "LEFT", -6, 0)
    frame.Name:SetFontObject(GameFontNormal)
    frame.Name:SetText(element.name)
    frame.Name:SetTextColor(HEADER_COLOR.r, HEADER_COLOR.g, HEADER_COLOR.b)

    frame.Background:SetColorTexture(0.055, 0.040, 0.032, 0.94)
    frame.Background:Show()

    frame:EnableMouse(element.collapsible and true or false)
end

local function LayoutAsRow(frame, element, isSelected)
    local card = isSelected and Theme.color.cardHover or Theme.color.card
    frame.Background:SetColorTexture(card[1], card[2], card[3], card[4])
    frame.Background:Show()

    if isSelected then
        frame.Selection:SetColorTexture(Theme.color.gold[1], Theme.color.gold[2],
            Theme.color.gold[3], Theme.color.gold[4])
        frame.Selection:Show()
    end

    if element.icon or element.portraitDisplayID then
        frame.PortraitRing:SetSize(ICON_SIZE + 6, ICON_SIZE + 6)
        frame.PortraitRing:SetPoint("LEFT", frame, "LEFT", ROW_INSET + 8, 0)
        frame.PortraitRing:SetAtlas(isSelected and "footer_active-ring"
            or "footer_inactive-ring")
        frame.PortraitRing:SetVertexColor(1, 1, 1, 1)
        frame.PortraitRing:Show()

        frame.Icon:SetSize(ICON_SIZE, ICON_SIZE)
        frame.Icon:SetPoint("CENTER", frame.PortraitRing, "CENTER", 0, 0)
        frame.IconMask:SetAllPoints(frame.Icon)

        local portraitApplied = false
        if element.portraitDisplayID and SetPortraitTextureFromCreatureDisplayID then
            portraitApplied = pcall(SetPortraitTextureFromCreatureDisplayID,
                frame.Icon, element.portraitDisplayID)
        end
        if not portraitApplied then
            frame.Icon:SetTexture(element.icon)
            frame.Icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
        else
            frame.Icon:SetTexCoord(0, 1, 0, 1)
        end
        frame.Icon:SetDesaturated(element.desaturated == true)
        local portraitBrightness = element.desaturated and 0.58 or 1
        frame.Icon:SetVertexColor(portraitBrightness, portraitBrightness,
            portraitBrightness, 1)
        frame.Icon:Show()

        if element.favorite then
            frame.Favorite:ClearAllPoints()
            frame.Favorite:SetPoint("TOPRIGHT", frame.PortraitRing, "TOPRIGHT", 4, 4)
            frame.Favorite:Show()
        end
    end

    if element.catalogText then
        frame.Status:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -ROW_INSET - 9, -10)
        frame.Catalog:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -ROW_INSET - 9, 10)
        frame.Catalog:SetText(element.catalogText)
        frame.Catalog:SetTextColor(Theme.color.muted[1], Theme.color.muted[2],
            Theme.color.muted[3], Theme.color.muted[4])
        frame.Catalog:Show()
    else
        frame.Status:SetPoint("RIGHT", frame, "RIGHT", -ROW_INSET - 4, 0)
    end
    frame.Status:SetText(element.statusText or "")

    local statusColor = element.statusColor or SUBTITLE_COLOR
    frame.Status:SetTextColor(statusColor.r, statusColor.g, statusColor.b)

    -- A row with a subtitle stacks name over subtitle and so anchors from the
    -- top; one without is vertically centred. Times whether there is an icon to
    -- sit beside, that is four cases, spelled out rather than folded into
    -- conditional anchor arguments.
    if element.subtitle then
        if element.icon or element.portraitDisplayID then
            frame.Name:SetPoint("TOPLEFT", frame.Icon, "TOPRIGHT", 10, -4)
        else
            frame.Name:SetPoint("TOPLEFT", frame, "TOPLEFT", ROW_INSET + 10, -6)
        end
    else
        if element.icon or element.portraitDisplayID then
            frame.Name:SetPoint("LEFT", frame.Icon, "RIGHT", 10, 0)
        else
            frame.Name:SetPoint("LEFT", frame, "LEFT", ROW_INSET + 10, 0)
        end
    end

    frame.Name:SetPoint("RIGHT", frame.Status, "LEFT", -6, 0)
    frame.Name:SetFontObject(GameFontNormal)
    frame.Name:SetText(element.name)

    local color = element.desaturated and SUBTITLE_COLOR
        or (element.favorite and FAVORITE_COLOR or NAME_COLOR)
    frame.Name:SetTextColor(color.r, color.g, color.b)

    if element.subtitle then
        frame.Subtitle:ClearAllPoints()
        frame.Subtitle:SetPoint("TOPLEFT", frame.Name, "BOTTOMLEFT", 0, -3)
        frame.Subtitle:SetPoint("RIGHT", frame.Status, "LEFT", -6, 0)
        frame.Subtitle:SetText(element.subtitle)
        frame.Subtitle:SetTextColor(SUBTITLE_COLOR.r, SUBTITLE_COLOR.g, SUBTITLE_COLOR.b)
        frame.Subtitle:Show()
    end

    frame:EnableMouse(element.selectionID ~= nil)
end

--=============================================================================
-- Construction
--=============================================================================

--- Creates a virtualized list.
-- @param parent Frame The owner frame.
-- @param callbacks table|nil { onSelect(id), onToggleHeader(token) }
-- @return table A list handle with SetElements / SetSelected / Refresh.
function PetList:Create(parent, callbacks)
    callbacks = callbacks or {}

    local list = {
        elements = {},
        selectedID = nil,
    }

    -- A frame with no anchor points has zero size, and everything parented to
    -- it inherits that, silently and without a Lua error.
    local container = CreateFrame("Frame", nil, parent)
    container:SetAllPoints(parent)
    list.container = container

    local scrollBar = CreateFrame("EventFrame", nil, container, "MinimalScrollBar")
    scrollBar:SetPoint("TOPRIGHT", container, "TOPRIGHT", 0, -4)
    scrollBar:SetPoint("BOTTOMRIGHT", container, "BOTTOMRIGHT", 0, 4)

    local scrollBox = CreateFrame("Frame", nil, container, "WowScrollBoxList")
    scrollBox:SetPoint("TOPLEFT", container, "TOPLEFT", 0, 0)
    scrollBox:SetPoint("BOTTOMRIGHT", scrollBar, "BOTTOMLEFT", -4, 0)
    list.scrollBox = scrollBox

    local view = CreateScrollBoxListLinearView()

    view:SetElementExtentCalculator(function(_, element)
        return element.kind == "header" and HEADER_HEIGHT or ROW_HEIGHT
    end)

    view:SetElementInitializer("Button", function(frame, element)
        EnsureRegions(frame)
        ResetRegions(frame)
        frame:SetHighlightTexture("Interface\\Buttons\\WHITE8X8")
        local highlight = frame:GetHighlightTexture()
        highlight:ClearAllPoints()
        highlight:SetVertexColor(0.75, 0.43, 0.18, 0.16)
        highlight:SetPoint("TOPLEFT", frame, "TOPLEFT", ROW_INSET, -3)
        highlight:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -ROW_INSET, 3)

        if element.kind == "header" then
            LayoutAsHeader(frame, element)
            frame:SetScript("OnClick", element.collapsible and function()
                if callbacks.onToggleHeader then
                    callbacks.onToggleHeader(element.token)
                end
            end or nil)
        else
            LayoutAsRow(frame, element, element.selectionID ~= nil
                and element.selectionID == list.selectedID)

            frame:SetScript("OnClick", element.selectionID and function()
                list:SetSelected(element.selectionID)
                if callbacks.onSelect then
                    callbacks.onSelect(element.selectionID)
                end
            end or nil)
        end
    end)

    ScrollUtil.InitScrollBoxListWithScrollBar(scrollBox, scrollBar, view)

    --- Replaces the contents of the list.
    function list:SetElements(elements)
        self.elements = elements
        self:Refresh()
    end

    --- Re-provides the current elements, keeping the scroll position.
    function list:Refresh()
        self.scrollBox:SetDataProvider(CreateDataProvider(self.elements), true)
    end

    --- Highlights one row and redraws.
    function list:SetSelected(id)
        if self.selectedID == id then
            return
        end

        self.selectedID = id
        self:Refresh()
    end

    --- @return any The selected row id.
    function list:GetSelected()
        return self.selectedID
    end

    ns.Debug:Log("UI", "pet list created")

    return list
end
