local ADDON_NAME, ns = ...

local MainFrame = {}
ns.MainFrame = MainFrame

-- Adventure Guide proportions: ~24% nav / ~76% content.
local SIDEBAR = {
    { id = "blitz", label = "Battleground Blitz", icon = "achievement_bg_killxenemies_generalsroom" },
    { id = "terms", label = "Terminology", icon = "inv_misc_book_11" },
    { id = "buffs", label = "Buffs", icon = "spell_nature_bloodlust" },
    { id = "macros", label = "Macros", icon = "inv_misc_note_01" },
}

local SIDEBAR_W = 190
local SCROLLBAR_GUTTER = 28
local ROW_H = 32

-- Blizzard Encounter Journal "book" geometry (exact values from the EJ XML).
local BOOK_W, BOOK_H = 785, 425
local LEFT_LIST_W, LEFT_LIST_H = 338, 382     -- BossesScrollBox
local LEFT_LIST_X, LEFT_LIST_Y = 25, 1        -- BOTTOMLEFT
local RIGHT_PAGE_W, RIGHT_PAGE_H = 350, 383   -- overviewScroll
local RIGHT_PAGE_X, RIGHT_PAGE_Y = -5, 1      -- BOTTOMRIGHT
local ROW_W_BOOK, ROW_H_BOOK = 320, 55        -- EncounterBossButton size

-- Blizzard Adventure Guide boss-row art.
local EJ_TEX = "Interface\\EncounterJournal\\UI-EncounterJournalTextures"
local BOSS_UP = { 0.00195313, 0.63671875, 0.21386719, 0.26757813 }
local BOSS_DOWN = { 0.00195313, 0.63671875, 0.10253906, 0.15625000 }
local BOSS_HL = { 0.00195313, 0.63671875, 0.15820313, 0.21191406 }

local frame

local function StyleSidebarButton(btn, active)
    if active then
        btn.selected:Show()
        btn.label:SetFontObject("GameFontHighlight")
        if NORMAL_FONT_COLOR then
            btn.label:SetTextColor(NORMAL_FONT_COLOR:GetRGB())
        else
            btn.label:SetTextColor(1, 0.82, 0)
        end
    else
        btn.selected:Hide()
        btn.label:SetFontObject("GameFontNormal")
        if GRAY_FONT_COLOR then
            btn.label:SetTextColor(GRAY_FONT_COLOR.r + 0.15, GRAY_FONT_COLOR.g + 0.15, GRAY_FONT_COLOR.b + 0.15)
        else
            btn.label:SetTextColor(0.7, 0.7, 0.7)
        end
    end
end

function MainFrame:IsChapterNavCollapsed()
    return ns.db and ns.db.profile and ns.db.profile.collapseChapterNav == true
end

-- Hide-chapters is only offered on Map & Buffs (needs the extra width).
function MainFrame:CanCollapseChapterNav()
    return ns.GuideView and ns.GuideView.currentSlug and ns.GuideView.currentChapter == "map"
end

-- Prefer the saved collapse only while Map & Buffs is open; other chapters
-- always keep the left list so you are never stuck without navigation.
function MainFrame:ShouldCollapseChapterNav()
    return self:CanCollapseChapterNav() and self:IsChapterNavCollapsed()
end

-- Right-page width in book mode (wider when the left chapter list is hidden).
function MainFrame:BookPageWidth()
    if self:ShouldCollapseChapterNav() then
        return BOOK_W - LEFT_LIST_X - 18
    end
    return RIGHT_PAGE_W
end

function MainFrame:ContentWidth()
    local inset = self.inset
    local w = inset and inset.GetWidth and inset:GetWidth()
    if self.bookMode then
        return self:BookPageWidth() - 30
    end
    if w and w > 50 then
        return math.max(w - SCROLLBAR_GUTTER - 28, 160)
    end
    local fw = (frame and frame:GetWidth()) or 1020
    return math.max(fw - SCROLLBAR_GUTTER - 40, 160)
end

function MainFrame:EnterBookMode()
    if self.bookMode then return end
    self.bookMode = true
    self:LayoutContent()
end

function MainFrame:ExitBookMode()
    if not self.bookMode then return end
    self.bookMode = false
    self:LayoutContent()
end

function MainFrame:ToggleChapterNav()
    if not (ns.db and ns.db.profile) then return end
    if not self:CanCollapseChapterNav() then return end
    ns.db.profile.collapseChapterNav = not ns.db.profile.collapseChapterNav
    self:LayoutContent()
    -- Re-render so the map / guide content picks up the new width.
    if ns.GuideView and ns.GuideView.currentSlug then
        ns.GuideView:RenderChapter(ns.GuideView.currentChapter or "overview", true)
    end
end

function MainFrame:UpdateChapterNavButton()
    local btn = self.chapterNavBtn
    if not btn then return end
    if not self.bookMode or not self:CanCollapseChapterNav() then
        btn:Hide()
        return
    end
    local collapsed = self:IsChapterNavCollapsed()
    btn:SetText(collapsed and "Chapters" or "<<")
    btn:SetWidth(collapsed and 72 or 36)
    btn:ClearAllPoints()
    -- Sit just above the content page so it stays usable in both layouts.
    btn:SetPoint("BOTTOMLEFT", self.scroll, "TOPLEFT", 0, 2)
    btn:Show()
end

function MainFrame:ScrollToTop()
    if self.scroll then self.scroll:SetVerticalScroll(0) end
end

-- Keep the MinimalScrollBar's thumb size / visibility in sync with the content.
function MainFrame:UpdateScrollBar()
    local sf, bar = self.scroll, self.scrollBar
    if not (sf and bar) then return end
    local viewH = sf:GetHeight() or 1
    local range = sf:GetVerticalScrollRange() or 0
    local contentH = viewH + range
    if contentH > 0 then
        if bar.SetVisibleExtentPercentage then
            bar:SetVisibleExtentPercentage(math.min(viewH / contentH, 1))
        end
        if bar.SetPanExtentPercentage then
            bar:SetPanExtentPercentage(math.min(46 / contentH, 1))
        end
    end
    self:SyncScrollBar()
    if bar.Update then bar:Update() end
end

-- Push the current scroll position onto the bar (guarded against feedback).
function MainFrame:SyncScrollBar()
    local sf, bar = self.scroll, self.scrollBar
    if not (sf and bar) or self._barSync then return end
    local range = sf:GetVerticalScrollRange() or 0
    local pct = (range > 0) and ((sf:GetVerticalScroll() or 0) / range) or 0
    if bar.SetScrollPercentage then
        self._barSync = true
        bar:SetScrollPercentage(pct)
        self._barSync = false
    end
end

function MainFrame:GetScroll()
    if not self.scroll then return 0 end
    return self.scroll:GetVerticalScroll() or 0
end

function MainFrame:SetScroll(y)
    if not self.scroll then return end
    self.scroll:SetVerticalScroll(math.max(0, y or 0))
end

function MainFrame:ClearContent()
    if self.builder then
        self.builder:Reset(self:ContentWidth())
        self.builder:SetTheme(false)
    end
    if ns.HomeView then ns.HomeView:Hide() end
    if ns.GuideView then ns.GuideView:HideList() end
    if ns.HubViews then ns.HubViews:HideMacroRows() end
    if ns.Search then ns.Search:HideRows() end
    if ns.MapView then ns.MapView:HideCanvas() end
end

function MainFrame:ClearSidebarActive()
    for _, btn in ipairs(self.sidebarButtons or {}) do
        StyleSidebarButton(btn, false)
    end
end

function MainFrame:ShowHome()
    self.currentSection = "home"
    ns.db.profile.lastSection = "home"
    if ns.GuideView then
        ns.GuideView.currentSlug = nil
        ns.GuideView.currentSection = nil
    end
    self:ShowSectionSidebar()
    self:ClearSidebarActive()
    self:SetBreadcrumbTrail({ { text = "Home" } })
    ns.HomeView:Show()
end

function MainFrame:GoHome()
    self:ShowHome()
end

function MainFrame:GoBlitz()
    self:SelectSection("blitz")
end

function MainFrame:SetSubnav()
    self:LayoutContent()
end

function MainFrame:SetSubnavActive()
end

-- Clickable breadcrumb trail. segments = { { text = "Home", onClick = fn }, ... }
-- The final segment is rendered as the current (non-clickable) location.
function MainFrame:SetBreadcrumbTrail(segments)
    -- At the top level (Home) the Adventure Guide shows just the red "Home"
    -- breadcrumb node in the top row; the "Field Manual" section title renders in
    -- the band beneath it (HomeView), matching Blizzard's category pages.
    if #segments <= 1 then
        if ns.AdventureGuide and ns.AdventureGuide.ShowHomeNav
            and ns.AdventureGuide:ShowHomeNav() then
            if self.crumbHost then self.crumbHost:Hide() end
            return
        end
        if ns.AdventureGuide and ns.AdventureGuide.nav then ns.AdventureGuide.nav:Hide() end
        if self.crumbHost then self.crumbHost:Hide() end
        return
    end

    -- Prefer the native Adventure Guide nav bar (red Home breadcrumb).
    if ns.AdventureGuide and ns.AdventureGuide.SetNavTrail
        and ns.AdventureGuide:IsActive() then
        if ns.AdventureGuide:SetNavTrail(segments) then
            if self.crumbHost then self.crumbHost:Hide() end
            return
        end
    end
    if not self.crumbHost then return end
    self.crumbHost:Show()
    self.crumbPool = self.crumbPool or {}
    self.crumbSepPool = self.crumbSepPool or {}
    for _, b in ipairs(self.crumbPool) do b:Hide() end
    for _, s in ipairs(self.crumbSepPool) do s:Hide() end

    local x = 0
    for i, seg in ipairs(segments) do
        if i > 1 then
            local sep = self.crumbSepPool[i - 1]
            if not sep then
                sep = self.crumbHost:CreateFontString(nil, "OVERLAY", "GameFontDisableSmall")
                self.crumbSepPool[i - 1] = sep
            end
            sep:ClearAllPoints()
            sep:SetPoint("LEFT", self.crumbHost, "LEFT", x, 0)
            sep:SetText(">")
            sep:Show()
            x = x + (sep:GetStringWidth() or 8) + 7
        end
        local btn = self.crumbPool[i]
        if not btn then
            btn = CreateFrame("Button", nil, self.crumbHost)
            btn:SetHeight(16)
            btn.text = btn:CreateFontString(nil, "OVERLAY", "GameFontDisableSmall")
            btn.text:SetPoint("LEFT", 0, 0)
            self.crumbPool[i] = btn
        end
        btn.text:SetText(seg.text or "")
        local isLast = (i == #segments)
        if seg.onClick and not isLast then
            btn.text:SetFontObject("GameFontNormalSmall")
            btn:SetScript("OnClick", seg.onClick)
            btn:EnableMouse(true)
        else
            btn.text:SetFontObject("GameFontDisableSmall")
            btn:SetScript("OnClick", nil)
            btn:EnableMouse(false)
        end
        local w = (btn.text:GetStringWidth() or 10) + 2
        btn:ClearAllPoints()
        btn:SetPoint("LEFT", self.crumbHost, "LEFT", x, 0)
        btn:SetWidth(w)
        btn:Show()
        x = x + w + 7
    end
end

-- Legacy single-string breadcrumb.
function MainFrame:SetBreadcrumb(text)
    self:SetBreadcrumbTrail({ { text = text or "" } })
end

function MainFrame:ShowSectionSidebar()
    -- The left navigation bar has been removed; this now just leaves book mode.
    self:ExitBookMode()
    if self.sidebar then self.sidebar:Hide() end
    if self.chapterRows then
        for _, r in ipairs(self.chapterRows) do r:Hide() end
    end
end

-- Swap the left panel to a guide's chapter list, styled as Blizzard's parchment
-- encounter (boss) rows: row art, portrait icon, dark title.
function MainFrame:ShowChapterSidebar(chapters, onSelect, activeId)
    for _, b in ipairs(self.sidebarButtons or {}) do b:Hide() end
    self.chapterRows = self.chapterRows or {}
    for _, r in ipairs(self.chapterRows) do r:Hide() end

    local y = 0
    for i, ch in ipairs(chapters) do
        local row = self.chapterRows[i]
        if not row then
            row = CreateFrame("Button", nil, self.chapterChild)
            -- Boss-button art (Up/Down/Highlight) exactly as the dungeon list.
            row:SetNormalTexture(EJ_TEX)
            row:SetPushedTexture(EJ_TEX)
            row:SetHighlightTexture(EJ_TEX)
            row:GetPushedTexture():SetTexCoord(unpack(BOSS_DOWN))
            row:GetHighlightTexture():SetTexCoord(unpack(BOSS_HL))

            -- Chapter icon fills the boss-row portrait socket (Blizzard's creature
            -- slot is 128x64; we use a circular mask sized to that black pocket).
            -- OVERLAY so it draws above the boss-button art.
            row.icon = row:CreateTexture(nil, "OVERLAY")
            row.icon:SetSize(52, 52)
            row.icon:SetPoint("LEFT", 8, 0)
            row.iconMask = row:CreateMaskTexture()
            row.iconMask:SetTexture(
                "Interface\\CharacterFrame\\TempPortraitAlphaMask",
                "CLAMPTOBLACKADDITIVE",
                "CLAMPTOBLACKADDITIVE"
            )
            row.iconMask:SetAllPoints(row.icon)
            row.icon:AddMaskTexture(row.iconMask)

            row.label = row:CreateFontString(nil, "OVERLAY", "GameFontNormalMed3")
            row.label:SetPoint("LEFT", row, "LEFT", 100, -2)
            row.label:SetPoint("RIGHT", row, "RIGHT", -12, -2)
            row.label:SetJustifyH("LEFT")
            row.label:SetJustifyV("MIDDLE")
            self.chapterRows[i] = row
        end
        row:SetSize(ROW_W_BOOK, ROW_H_BOOK)
        row:ClearAllPoints()
        row:SetPoint("TOPLEFT", 0, y)
        -- Keep icon sized to the black socket even if the row was created earlier.
        -- Slight inset from the boss-row left edge so the circle reads centered
        -- in the portrait pocket (was LEFT 6,1 -> art sat high-left).
        row.icon:SetSize(52, 52)
        row.icon:ClearAllPoints()
        row.icon:SetPoint("LEFT", 8, 0)
        if not row.iconMask then
            row.iconMask = row:CreateMaskTexture()
            row.iconMask:SetTexture(
                "Interface\\CharacterFrame\\TempPortraitAlphaMask",
                "CLAMPTOBLACKADDITIVE",
                "CLAMPTOBLACKADDITIVE"
            )
            row.icon:AddMaskTexture(row.iconMask)
        end
        row.iconMask:SetAllPoints(row.icon)
        local path, isAsset = ns.UI.ResolveIcon(ch.icon)
        row.icon:SetTexture(path)
        if isAsset then
            row.icon:SetTexCoord(0, 1, 0, 1)
        else
            -- Blizzard icon crop so art fills the circle (no empty border ring).
            row.icon:SetTexCoord(0.07, 0.93, 0.07, 0.93)
        end
        row.label:SetText(ch.title)
        local id = ch.id
        row:SetScript("OnClick", function() onSelect(id) end)

        local active = (ch.id == activeId)
        row:GetNormalTexture():SetTexCoord(unpack(active and BOSS_DOWN or BOSS_UP))
        -- Selection is the boss-row art (background), not bright gold text.
        if active then
            row.label:SetTextColor(0.92, 0.88, 0.80)
        else
            row.label:SetTextColor(0.70, 0.62, 0.52)
        end
        row:Show()
        y = y - (ROW_H_BOOK + 1)
    end
    self.chapterChild:SetHeight(math.max(-y + 2, 10))
    if self.chapterScroll then self.chapterScroll:SetVerticalScroll(0) end
end

function MainFrame:LayoutContent()
    if not (frame and self.inset) then return end
    local inset = self.inset
    local W = (inset.GetWidth and inset:GetWidth()) or 785
    local H = (inset.GetHeight and inset:GetHeight()) or 420

    if self.bookMode then
        -- Two-page parchment book, matching Blizzard's encounter view exactly.
        -- Optional: collapse the left chapter list so the map/content can use
        -- nearly the full book width (persisted in profile.collapseChapterNav).
        self.book:Show()
        self.divider:Hide()
        self.crumbHost:Hide()
        self.sidebar:Hide()

        local collapsed = self:ShouldCollapseChapterNav()
        local pageW = self:BookPageWidth()
        self.sidebarW = collapsed and 0 or ROW_W_BOOK

        self.chapterScroll:ClearAllPoints()
        self.chapterScroll:SetPoint("BOTTOMLEFT", self.book, "BOTTOMLEFT", LEFT_LIST_X, LEFT_LIST_Y)
        self.chapterScroll:SetSize(LEFT_LIST_W, LEFT_LIST_H)
        self.chapterChild:SetWidth(LEFT_LIST_W)
        self.chapterScroll:SetShown(not collapsed)

        self.scroll:ClearAllPoints()
        if collapsed then
            self.scroll:SetPoint("BOTTOMLEFT", self.book, "BOTTOMLEFT", LEFT_LIST_X, RIGHT_PAGE_Y)
            self.scroll:SetSize(pageW, RIGHT_PAGE_H)
        else
            self.scroll:SetPoint("BOTTOMRIGHT", self.book, "BOTTOMRIGHT", RIGHT_PAGE_X, RIGHT_PAGE_Y)
            self.scroll:SetSize(RIGHT_PAGE_W, RIGHT_PAGE_H)
        end
        self.content:SetWidth(self:ContentWidth() + 2)
        self.scrollBar:ClearAllPoints()
        self.scrollBar:SetPoint("TOPRIGHT", self.scroll, "TOPRIGHT", -4, -2)
        self.scrollBar:SetPoint("BOTTOMRIGHT", self.scroll, "BOTTOMRIGHT", -4, 2)
        self:UpdateScrollBar()
        self:UpdateChapterNavButton()
        return
    end

    -- Full-width content (no left navigation bar); navigation is via the home
    -- tile grid and the breadcrumb nav bar.
    self.book:Hide()
    self.divider:Hide()
    self.chapterScroll:Hide()
    if self.sidebar then self.sidebar:Hide() end
    self.crumbHost:Hide()
    self.sidebarW = 0
    if self.chapterNavBtn then self.chapterNavBtn:Hide() end

    self.scroll:ClearAllPoints()
    self.scroll:SetPoint("TOPLEFT", inset, "TOPLEFT", 16, -6)
    self.scroll:SetPoint("BOTTOMRIGHT", inset, "BOTTOMRIGHT", -SCROLLBAR_GUTTER, 8)
    self.content:SetWidth(self:ContentWidth() + 2)
    self.scrollBar:ClearAllPoints()
    self.scrollBar:SetPoint("TOPLEFT", self.scroll, "TOPRIGHT", 6, 0)
    self.scrollBar:SetPoint("BOTTOMLEFT", self.scroll, "BOTTOMRIGHT", 6, 0)
    self:UpdateScrollBar()
end

-- Suggested Content-style header seam (home only). Lives on AdventureGuide.
function MainFrame:ShowHomeHeaderSeam(anchor, gap)
    if ns.AdventureGuide and ns.AdventureGuide.ShowHeaderSeam then
        ns.AdventureGuide:ShowHeaderSeam(anchor, gap)
    end
end

function MainFrame:HideHomeHeaderSeam()
    if ns.AdventureGuide and ns.AdventureGuide.HideHeaderSeam then
        ns.AdventureGuide:HideHeaderSeam()
    end
end

function MainFrame:RenderCurrent()
    local q = self.searchBox and self.searchBox:GetText() or ""
    q = q:gsub("^%s+", ""):gsub("%s+$", "")
    if q ~= "" then
        -- In the battleground browser, live-filter the tiles instead of the
        -- global result list.
        if self.currentSection == "blitz" and ns.GuideView and not ns.GuideView.currentSlug then
            ns.GuideView:ShowList(q)
            return
        end
        ns.Search:Show(q)
        return
    end
    local sec = self.currentSection or "home"
    if sec == "home" then
        self:ShowHome()
    elseif sec == "blitz" then
        if ns.GuideView.currentSlug then
            ns.GuideView:RenderChapter(ns.GuideView.currentChapter or "overview", true)
        else
            ns.GuideView:ShowList()
        end
    elseif sec == "terms" then
        ns.HubViews:RenderTerms()
    elseif sec == "buffs" then
        ns.HubViews:RenderBuffs()
    elseif sec == "macros" then
        ns.HubViews:RenderMacros()
    end
end

function MainFrame:SelectSection(id)
    if id == "home" then
        self:ShowHome()
        return
    end
    self.currentSection = id
    ns.db.profile.lastSection = id
    self:ShowSectionSidebar()
    for _, btn in ipairs(self.sidebarButtons or {}) do
        StyleSidebarButton(btn, btn._id == id)
    end

    local labels = {
        blitz = "Battleground Blitz", terms = "Terminology",
        buffs = "Buffs", macros = "Macros",
    }
    self:SetBreadcrumbTrail({
        { text = "Home", onClick = function() self:GoHome() end },
        { text = labels[id] or id },
    })

    if id == "blitz" then
        ns.GuideView.currentSlug = nil
        ns.GuideView.currentSection = nil
        ns.GuideView:ShowList()
    elseif id == "terms" then
        ns.HubViews:RenderTerms()
    elseif id == "buffs" then
        ns.HubViews:RenderBuffs()
    elseif id == "macros" then
        ns.HubViews:RenderMacros()
    end
end

-- Build the manual panel (sidebar + content) into a host frame that lives
-- inside the Blizzard Adventure Guide's inset. The header search box is owned
-- by the Adventure Guide integration and passed in here.
function MainFrame:BuildInto(host, searchBox)
    if self.built then return end
    self.built = true

    frame = host
    self.frame = host
    self.inset = host
    self.searchBox = searchBox

    if searchBox then
        searchBox:HookScript("OnTextChanged", function()
            self:RenderCurrent()
        end)
        searchBox:HookScript("OnEscapePressed", function(s)
            s:SetText("")
            s:ClearFocus()
            self:RenderCurrent()
        end)
    end

    self.sidebar = CreateFrame("Frame", nil, host)
    self.sidebarButtons = {}
    local sy = -2
    for i, item in ipairs(SIDEBAR) do
        local btn = CreateFrame("Button", nil, self.sidebar)
        btn:SetPoint("TOPLEFT", 2, sy)
        btn:SetSize(SIDEBAR_W - 8, ROW_H)
        btn:SetHighlightTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight", "ADD")

        btn.selected = btn:CreateTexture(nil, "BACKGROUND")
        btn.selected:SetAllPoints(btn)
        btn.selected:SetTexture("Interface\\FriendsFrame\\UI-FriendsFrame-HighlightBar")
        btn.selected:SetBlendMode("ADD")
        btn.selected:SetAlpha(0.55)
        btn.selected:Hide()

        btn.icon = btn:CreateTexture(nil, "ARTWORK")
        btn.icon:SetSize(20, 20)
        btn.icon:SetPoint("LEFT", 6, 0)
        btn.icon:SetTexture(ns.UI.ResolveIcon(item.icon))
        btn.icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)

        btn.label = btn:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        btn.label:SetPoint("LEFT", btn.icon, "RIGHT", 8, 0)
        btn.label:SetPoint("RIGHT", btn, "RIGHT", -6, 0)
        btn.label:SetJustifyH("LEFT")
        btn.label:SetText(item.label)

        btn._id = item.id
        btn:SetScript("OnClick", function() self:SelectSection(item.id) end)
        StyleSidebarButton(btn, false)
        self.sidebarButtons[i] = btn
        sy = sy - (ROW_H + 2)
    end

    -- Two-page parchment book, using Blizzard's exact Encounter Journal geometry:
    -- a 785x425 frame pinned to the bottom-right of the inset, with the book art
    -- drawn from the correct sub-region of UI-EJ-JournalBG.
    self.book = CreateFrame("Frame", nil, host)
    self.book:SetSize(BOOK_W, BOOK_H)
    self.book:SetPoint("BOTTOMRIGHT", host, "BOTTOMRIGHT", 1, 0)
    self.book:EnableMouse(true)
    self.book:Hide()

    self.bookBG = self.book:CreateTexture(nil, "BACKGROUND")
    self.bookBG:SetTexture("Interface\\EncounterJournal\\UI-EJ-JournalBG")
    self.bookBG:SetTexCoord(0, 0.766601562, 0, 0.830078125)
    self.bookBG:SetAllPoints(self.book)

    -- Collapse / expand the left chapter list (more room for the interactive map).
    self.chapterNavBtn = CreateFrame("Button", nil, self.book, "UIPanelButtonTemplate")
    self.chapterNavBtn:SetSize(36, 22)
    self.chapterNavBtn:SetFrameLevel((self.book:GetFrameLevel() or 1) + 20)
    self.chapterNavBtn:SetScript("OnClick", function() self:ToggleChapterNav() end)
    self.chapterNavBtn:SetScript("OnEnter", function(btn)
        GameTooltip:SetOwner(btn, "ANCHOR_RIGHT")
        if self:IsChapterNavCollapsed() then
            GameTooltip:SetText("Show chapters", 1, 0.82, 0)
            GameTooltip:AddLine("Restore the left chapter list.", 1, 1, 1, true)
        else
            GameTooltip:SetText("Hide chapters", 1, 0.82, 0)
            GameTooltip:AddLine("Widen the guide page for the map.", 1, 1, 1, true)
        end
        GameTooltip:Show()
    end)
    self.chapterNavBtn:SetScript("OnLeave", function() GameTooltip:Hide() end)
    self.chapterNavBtn:Hide()

    self.divider = host:CreateTexture(nil, "ARTWORK")
    self.divider:SetColorTexture(0.25, 0.25, 0.25, 0.6)
    self.divider:SetWidth(1)

    self.crumbHost = CreateFrame("Frame", nil, host)
    self.crumbHost:SetHeight(16)

    self.sidebarW = SIDEBAR_W

    -- Clipped, mouse-wheel scrollable container for the guide chapter list
    -- (the left page can hold more chapters than fit at once). Parented to the
    -- book so its rows always draw above the parchment art.
    self.chapterScroll = CreateFrame("ScrollFrame", nil, self.book)
    self.chapterScroll:SetClipsChildren(true)
    self.chapterScroll:EnableMouseWheel(true)
    self.chapterChild = CreateFrame("Frame", nil, self.chapterScroll)
    self.chapterChild:SetSize(SIDEBAR_W, 10)
    self.chapterScroll:SetScrollChild(self.chapterChild)
    self.chapterScroll:SetScript("OnMouseWheel", function(sf, delta)
        local max = math.max((self.chapterChild:GetHeight() or 0) - (sf:GetHeight() or 0), 0)
        local cur = sf:GetVerticalScroll() or 0
        local new = math.min(math.max(cur - delta * 40, 0), max)
        sf:SetVerticalScroll(new)
    end)
    self.chapterScroll:Hide()

    -- Plain scroll frame driven by Blizzard's modern MinimalScrollBar, matching
    -- the Adventure Guide (thin bar that auto-hides when content fits) instead of
    -- the old chunky UIPanelScrollFrameTemplate arrows.
    self.scroll = CreateFrame("ScrollFrame", "RatedTrackerFieldManualScroll", host)
    self.scroll:EnableMouseWheel(true)
    self.content = CreateFrame("Frame", nil, self.scroll)
    self.content:SetSize(400, 10)
    self.content:EnableMouse(true)
    if self.content.SetHyperlinksEnabled then
        self.content:SetHyperlinksEnabled(true)
    end
    self.scroll:SetScrollChild(self.content)
    self.builder = ns.UI.NewBuilder(self.content)

    -- Dragging empty space in our content/book panels should move the Adventure
    -- Guide window, matching Blizzard's own tabs. These frames enable mouse (for
    -- tiles, links, wheel) so they otherwise swallow the drag.
    if ns.AdventureGuide and ns.AdventureGuide.MakeDraggable then
        ns.AdventureGuide:MakeDraggable(self.content)
        ns.AdventureGuide:MakeDraggable(self.book)
        ns.AdventureGuide:MakeDraggable(self.chapterChild)
        ns.AdventureGuide:MakeDraggable(self.crumbHost)
    end

    self.scrollBar = CreateFrame("EventFrame", nil, host, "MinimalScrollBar")
    if self.scrollBar.SetHideIfUnscrollable then
        self.scrollBar:SetHideIfUnscrollable(true)
    end
    self.scroll:SetScript("OnMouseWheel", function(sf, delta)
        local range = sf:GetVerticalScrollRange() or 0
        local cur = sf:GetVerticalScroll() or 0
        sf:SetVerticalScroll(math.min(math.max(cur - delta * 46, 0), range))
    end)
    self.scroll:SetScript("OnScrollRangeChanged", function() self:UpdateScrollBar() end)
    self.scroll:SetScript("OnVerticalScroll", function() self:SyncScrollBar() end)
    if self.scrollBar.RegisterCallback then
        local ev = (self.scrollBar.Event and self.scrollBar.Event.OnScroll) or "OnScroll"
        self.scrollBar:RegisterCallback(ev, function(_, pct)
            if self._barSync then return end
            local range = self.scroll:GetVerticalScrollRange() or 0
            self._barSync = true
            self.scroll:SetVerticalScroll((pct or 0) * range)
            self._barSync = false
        end, self)
    end

    self.currentSection = (ns.db and ns.db.profile and ns.db.profile.lastSection) or "home"

    self:LayoutContent()
end

function MainFrame:IsBuilt()
    return self.built == true
end

-- Called by the Adventure Guide integration when our tab becomes active.
function MainFrame:Enter()
    if self.searchBox then self.searchBox:SetText("") end
    -- Always land on the home tile grid.
    self:ShowHome()
end

function MainFrame:Show()
    ns:OpenAdventureGuide()
end

function MainFrame:Hide()
    if ns.AdventureGuide and ns.AdventureGuide:IsActive()
        and EncounterJournal and EncounterJournal:IsShown() then
        HideUIPanel(EncounterJournal)
    end
end

function MainFrame:Toggle()
    if ns.AdventureGuide and ns.AdventureGuide:IsActive()
        and EncounterJournal and EncounterJournal:IsShown() then
        HideUIPanel(EncounterJournal)
    else
        ns:OpenAdventureGuide()
    end
end
