--[[
    UI/MainWindow.lua

    The shell: a movable hunter field journal with six tabs.

        Collection    every named tameable rare and its field dossier
        My stable     your own pets, from the stored stable reading
        Sightings     tameable beasts met that the database does not contain

    The last two used to be slash commands. Information a collector wants
    should be on screen, not behind something they have to remember to type.

    The frame is built the first time it is opened, not at load time, so a
    player who never opens it pays nothing for it.

    It needs one global name -- UISpecialFrames, the mechanism that makes ESC
    close a window, takes a frame name string rather than a frame reference.
]]

local _, ns = ...

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

local FRAME_GLOBAL_NAME = "ShinyHunterMainWindow"

-- Taller since the detail pane gained the model view: the sources list of a
-- creature with eight spawn points has to fit underneath it.
local FRAME_WIDTH, FRAME_HEIGHT = 1160, 720

local LIST_WIDTH = 330
local PADDING = 12
-- Six sections must fit in the journal's authored tab rail without wrapping.
local TAB_WIDTH, TAB_HEIGHT = 142, 30
local MASTHEAD_HEIGHT = 46
local Theme = ns.UI.Theme

local frame
local browser
local rarePanel
local tabs = {}
local panels = {}
local activeTab = 1

--=============================================================================
-- Position
--=============================================================================

local function SavePosition(self)
    local point, _, relativePoint, x, y = self:GetPoint(1)
    ns.Config:SetWindowPosition(point, relativePoint, x, y)
end

local function RestorePosition(self)
    local position = ns.Config:GetWindowPosition()

    self:ClearAllPoints()
    self:SetPoint(position.point, UIParent, position.relativePoint, position.x, position.y)
end

local function CreateProgressLabel(parent)
    local label = parent:CreateFontString(nil, "ARTWORK", "GameFontNormal")
    label:SetPoint("TOPRIGHT", parent.Inset, "TOPRIGHT", -20, -10)

    local track = CreateFrame("StatusBar", nil, parent.Inset)
    track:SetSize(180, 6)
    track:SetPoint("TOPRIGHT", parent.Inset, "TOPRIGHT", -20, -30)
    track:SetStatusBarTexture("Interface\\Buttons\\WHITE8X8")
    track:SetStatusBarColor(Theme.color.gold[1], Theme.color.gold[2],
        Theme.color.gold[3], 0.95)

    local background = track:CreateTexture(nil, "BACKGROUND")
    background:SetAllPoints(track)
    background:SetColorTexture(0.16, 0.11, 0.07, 0.95)
    parent.ProgressBar = track

    return label
end

--=============================================================================
-- Tabs
--=============================================================================

--- The active tab is marked with a gold underline rather than by disabling the
--- button: a greyed-out button reads as "unavailable", not "selected", and no
--- tab-button atlas has been verified on this client.
local function CreateTab(parent, index, label)
    local button = CreateFrame("Button", nil, parent, "BackdropTemplate")

    button:SetSize(TAB_WIDTH, TAB_HEIGHT)
    button:SetNormalFontObject(GameFontNormal)
    button:SetText(label)
    button:SetScript("OnClick", function()
        MainWindow:SelectTab(index)
    end)

    button.TabMarkerGlow = button:CreateTexture(nil, "ARTWORK")
    button.TabMarkerGlow:SetSize(13, 13)
    button.TabMarkerGlow:SetPoint("LEFT", button, "LEFT", 14, 0)
    button.TabMarkerGlow:SetRotation(math.pi / 4)
    button.TabMarkerGlow:SetBlendMode("ADD")
    button.TabMarkerGlow:SetColorTexture(0.20, 0.95, 0.86, 0.22)

    button.TabMarker = button:CreateTexture(nil, "OVERLAY")
    button.TabMarker:SetSize(5, 5)
    button.TabMarker:SetPoint("CENTER", button.TabMarkerGlow, "CENTER")
    button.TabMarker:SetRotation(math.pi / 4)

    Theme:ApplyJournalTab(button, false)

    return button
end

local function CreateTabArea(inset)
    local masthead = CreateFrame("Frame", nil, inset)
    masthead:SetPoint("TOPLEFT", inset, "TOPLEFT", PADDING, -5)
    masthead:SetPoint("TOPRIGHT", inset, "TOPRIGHT", -PADDING, -5)
    masthead:SetHeight(MASTHEAD_HEIGHT)

    local title = masthead:CreateFontString(nil, "ARTWORK", "GameFontNormalHuge")
    title:SetPoint("LEFT", masthead, "LEFT", 4, 0)
    title:SetText(ns.L["JOURNAL_TITLE"])
    title:SetTextColor(Theme.color.gold[1], Theme.color.gold[2], Theme.color.gold[3])

    local rule = Theme:CreateRule(masthead)
    rule:SetHeight(1)
    rule:SetPoint("BOTTOMLEFT", masthead, "BOTTOMLEFT", 0, 0)
    rule:SetPoint("BOTTOMRIGHT", masthead, "BOTTOMRIGHT", 0, 0)

    local tabRow = CreateFrame("Frame", nil, inset)
    tabRow:SetPoint("TOPLEFT", masthead, "BOTTOMLEFT", 0, -8)
    tabRow:SetPoint("TOPRIGHT", masthead, "BOTTOMRIGHT", 0, -8)
    tabRow:SetHeight(TAB_HEIGHT)

    local contentArea = CreateFrame("Frame", nil, inset)
    contentArea:SetPoint("TOPLEFT", tabRow, "BOTTOMLEFT", 0, -12)
    contentArea:SetPoint("BOTTOMRIGHT", inset, "BOTTOMRIGHT", -PADDING, PADDING)

    local labels = {
        ns.L["TAB_COLLECTION"],
        ns.L["TAB_STABLE"],
        ns.L["TAB_SIGHTINGS"],
        ns.L["TAB_TERRITORIES"],
        ns.L["TAB_GUIDE"],
        ns.L["TAB_OPTIONS"],
    }

    for index, label in ipairs(labels) do
        local button = CreateTab(tabRow, index, label)

        if index == 1 then
            button:SetPoint("LEFT", tabRow, "LEFT", 0, 0)
        else
            button:SetPoint("LEFT", tabs[index - 1], "RIGHT", 4, 0)
        end

        tabs[index] = button
    end

    return contentArea
end

--=============================================================================
-- Panels
--=============================================================================

--- The collection tab: list on the left, detail on the right.
local function CreateCollectionPanel(parent)
    local panel = CreateFrame("Frame", nil, parent)
    panel:SetAllPoints(parent)

    local listShell = Theme:CreatePanel(panel, Theme.color.ink, Theme.color.line)
    listShell:SetPoint("TOPLEFT", panel, "TOPLEFT", 0, 0)
    listShell:SetPoint("BOTTOMLEFT", panel, "BOTTOMLEFT", 0, 0)
    listShell:SetWidth(LIST_WIDTH)

    local listContainer = CreateFrame("Frame", nil, listShell)
    listContainer:SetPoint("TOPLEFT", listShell, "TOPLEFT", 10, -10)
    listContainer:SetPoint("BOTTOMRIGHT", listShell, "BOTTOMRIGHT", -8, 10)

    local divider = panel:CreateTexture(nil, "ARTWORK")
    divider:SetColorTexture(1, 1, 1, 0.08)
    divider:SetWidth(1)
    divider:SetPoint("TOPLEFT", listShell, "TOPRIGHT", PADDING, 0)
    divider:SetPoint("BOTTOMLEFT", listShell, "BOTTOMRIGHT", PADDING, 0)

    local detailShell = Theme:CreatePanel(panel, Theme.color.panel, Theme.color.line)
    detailShell:SetPoint("TOPLEFT", divider, "TOPRIGHT", PADDING, 0)
    detailShell:SetPoint("BOTTOMRIGHT", panel, "BOTTOMRIGHT", 0, 0)

    local detailContainer = CreateFrame("Frame", nil, detailShell)
    detailContainer:SetPoint("TOPLEFT", detailShell, "TOPLEFT", 16, -14)
    detailContainer:SetPoint("BOTTOMRIGHT", detailShell, "BOTTOMRIGHT", -16, 14)

    browser = ns.UI.Browser:Create(listContainer, detailContainer)

    return panel
end

local function CreateSimplePanel(parent, builder)
    local panel = CreateFrame("Frame", nil, parent)
    panel:SetAllPoints(parent)

    local shell = Theme:CreatePanel(panel, Theme.color.ink, Theme.color.line)
    shell:SetPoint("TOPLEFT", panel, "TOPLEFT", 0, 0)
    shell:SetPoint("BOTTOMRIGHT", panel, "BOTTOMRIGHT", 0, 0)

    local content = CreateFrame("Frame", nil, shell)
    content:SetPoint("TOPLEFT", shell, "TOPLEFT", 10, -10)
    content:SetPoint("BOTTOMRIGHT", shell, "BOTTOMRIGHT", -10, 10)

    panel.handle = builder(content)

    return panel
end

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

local function Build()
    local window = CreateFrame("Frame", FRAME_GLOBAL_NAME, UIParent)

    window:SetSize(FRAME_WIDTH, FRAME_HEIGHT)
    window:SetFrameStrata("MEDIUM")
    window:SetToplevel(true)
    window:SetClampedToScreen(true)
    window:Hide()

    local artwork = window:CreateTexture(nil, "BACKGROUND")
    artwork:SetAllPoints(window)
    artwork:SetTexture(Theme.bestiaryShellTexture)
    window.Artwork = artwork

    local inset = CreateFrame("Frame", nil, window)
    inset:SetPoint("TOPLEFT", window, "TOPLEFT", 154, -76)
    inset:SetPoint("BOTTOMRIGHT", window, "BOTTOMRIGHT", -26, 25)
    window.Inset = inset

    -- The leather cartouche is the journal's maker's mark.  Morpheus is the
    -- client-shipped display face used for Warcraft chapter titles; centring it
    -- on the whole shell (not on the asymmetric content inset) makes the name
    -- sit in the authored recess instead of drifting towards the close button.
    local shellTitle = window:CreateFontString(nil, "OVERLAY")
    shellTitle:SetFont("Fonts\\MORPHEUS.TTF", 23, "OUTLINE")
    shellTitle:SetPoint("TOP", window, "TOP", 0, -48)
    shellTitle:SetWidth(470)
    shellTitle:SetJustifyH("CENTER")
    shellTitle:SetText(ns.L["ADDON_TITLE"])
    shellTitle:SetTextColor(0.95, 0.76, 0.31)
    shellTitle:SetShadowColor(0.06, 0.025, 0.01, 0.95)
    shellTitle:SetShadowOffset(2, -2)
    window.ShellTitle = shellTitle

    local close = CreateFrame("Button", nil, window)
    close:SetSize(30, 30)
    close:SetPoint("TOPRIGHT", window, "TOPRIGHT", -17, -16)
    close:SetNormalFontObject(GameFontNormalHuge)
    close:SetText("×")
    local closeFont = close:GetFontString()
    if closeFont then
        closeFont:SetTextColor(0.82, 0.62, 0.24)
    end
    local closeHighlight = close:CreateTexture(nil, "HIGHLIGHT")
    closeHighlight:SetAllPoints(close)
    closeHighlight:SetColorTexture(0.20, 0.90, 0.80, 0.10)
    close:SetScript("OnClick", function()
        window:Hide()
    end)
    window.CloseButton = close

    window:SetMovable(true)
    window:EnableMouse(true)
    window:RegisterForDrag("LeftButton")
    window:SetScript("OnDragStart", window.StartMoving)
    window:SetScript("OnDragStop", function(self)
        self:StopMovingOrSizing()
        SavePosition(self)
    end)

    window.ProgressLabel = CreateProgressLabel(window)

    local contentArea = CreateTabArea(window.Inset)

    panels[1] = CreateSimplePanel(contentArea, function(parent)
        rarePanel = ns.UI.RareCatalogPanel:Create(parent)
        return rarePanel
    end)
    panels[2] = CreateSimplePanel(contentArea, function(parent)
        return ns.UI.StablePanel:Create(parent)
    end)
    panels[3] = CreateSimplePanel(contentArea, function(parent)
        return ns.UI.SightingsPanel:Create(parent)
    end)
    panels[4] = CreateSimplePanel(contentArea, function(parent)
        return ns.UI.RegionProgressPanel:Create(parent)
    end)
    panels[5] = CreateSimplePanel(contentArea, function(parent)
        return ns.UI.TutorialPanel:Create(parent)
    end)
    panels[6] = CreateSimplePanel(contentArea, function(parent)
        return ns.UI.OptionsPanel:Create(parent)
    end)

    RestorePosition(window)

    -- Lets ESC close the window, the same way Blizzard panels behave.
    table.insert(UISpecialFrames, FRAME_GLOBAL_NAME)

    ns.Debug:Log("UI", "main window created")

    return window
end

local function EnsureFrame()
    if not frame then
        local ok, result = xpcall(Build, function(err)
            local trace = debugstack and debugstack(2, 8, 8) or ""
            return tostring(err) .. (trace ~= "" and "\n" .. trace or "")
        end)
        if not ok then
            ns.Debug:ReportError("opening hunter journal", result)
            return nil
        end

        frame = result
        MainWindow:SelectTab(activeTab)
    end

    return frame
end

--=============================================================================
-- Public API
--=============================================================================

--- @return table|nil The browser, once the window has been built.
function MainWindow:GetBrowser()
    return browser
end

--- Shows one tab and refreshes it.
function MainWindow:SelectTab(index)
    activeTab = index

    if not frame then
        return
    end

    for position, panel in ipairs(panels) do
        panel:SetShown(position == index)
        Theme:ApplyJournalTab(tabs[position], position == index)
    end

    -- Panels are refreshed on show rather than on every change: a tab nobody
    -- is looking at does not need redrawing.
    local panel = panels[index]
    if panel and panel.handle and panel.handle.Refresh then
        panel.handle:Refresh()
    end
end

--- Refreshes the header counter from the current collection state.
function MainWindow:RefreshProgress()
    if not frame then
        return
    end

    local collected, total = ns.Collection.RareState:GetProgress()
    frame.ProgressLabel:SetText(ns.L["PROGRESS_FORMAT"]:format(collected, total))
    frame.ProgressBar:SetMinMaxValues(0, math.max(total, 1))
    frame.ProgressBar:SetValue(collected)
end

function MainWindow:OpenRegion(regionID)
    local window = EnsureFrame()
    if not window then return end
    self:SelectTab(1)
    if rarePanel then rarePanel:SetRegion(regionID) end
    window:Show()
end

function MainWindow:OpenRare(npcID)
    local window = EnsureFrame()
    if not window then return false end
    self:SelectTab(1)
    local focused = rarePanel and rarePanel:FocusRare(npcID)
    window:Show()
    return focused == true
end

function MainWindow:Show()
    local window = EnsureFrame()
    if not window then
        return
    end
    self:RefreshProgress()
    self:SelectTab(activeTab)
    window:Show()
end

function MainWindow:Hide()
    if frame then
        frame:Hide()
    end
end

function MainWindow:IsShown()
    return frame ~= nil and frame:IsShown()
end

--- Rebuilds only the journal shell after a live locale change. Every panel
--- stores rendered text, so rebuilding is more reliable than trying to find
--- every label through private widget internals.
function MainWindow:Rebuild()
    if not frame then return end
    frame:Hide()
    frame, browser, rarePanel = nil, nil, nil
    wipe(tabs)
    wipe(panels)
    self:Show()
end

function MainWindow:Toggle()
    if self:IsShown() then
        self:Hide()
    else
        self:Show()
    end
end

function MainWindow:OnEnable()
    ns.Events:Subscribe("COLLECTION_CHANGED", self, function()
        MainWindow:RefreshProgress()

        -- The stable tab shows collection-derived state, so keep it current
        -- when it happens to be the visible one.
        if activeTab ~= 1 then
            MainWindow:SelectTab(activeTab)
        end
    end)

    ns.Events:Subscribe("RARE_COLLECTION_CHANGED", self, function()
        MainWindow:RefreshProgress()
        if activeTab == 4 then MainWindow:SelectTab(activeTab) end
    end)

    ns.Events:Subscribe("DATABASE_RESET", self, function()
        MainWindow:RefreshProgress()
        MainWindow:SelectTab(activeTab)
    end)

    ns.Events:Subscribe("LOCALE_CHANGED", self, function()
        if frame then C_Timer.After(0, function() MainWindow:Rebuild() end) end
    end)
end
