local MAJOR, MINOR = "LibMatic-ChangeLog-1.0", 6
assert(LibStub, MAJOR .. " requires LibStub")
local lib = LibStub:NewLibrary(MAJOR, MINOR)
if not lib then return end

lib.frame = lib.frame or nil
lib.frames = lib.frames or {}

local DEFAULT_STYLE = {
    fontSettings = {
        font = "MoK",
        fontSize = 15,
        fontColor = { 0, 0, 0 },
        fontOutline = "OUTLINEMONOCHROME",
    },
    borderAndBackgroundSettings = {
        backgroundTexture = "Blizzard Parchment 2",
        borderTexture = "Blizzard Achievement Wood",
        borderSize = 15,
    },
}

local function fetchLib(name)
    return LibStub and LibStub(name, true)
end

local function mergeStyle(s)
    s = s or {}
    local fs = s.fontSettings or {}
    local bb = s.borderAndBackgroundSettings or {}
    local d = DEFAULT_STYLE
    return {
        fontSettings = {
            font = fs.font or d.fontSettings.font,
            fontSize = fs.fontSize or d.fontSettings.fontSize,
            fontColor = fs.fontColor or d.fontSettings.fontColor,
            fontOutline = fs.fontOutline or d.fontSettings.fontOutline,
        },
        borderAndBackgroundSettings = {
            backgroundTexture = bb.backgroundTexture or d.borderAndBackgroundSettings.backgroundTexture,
            borderTexture = bb.borderTexture or d.borderAndBackgroundSettings.borderTexture,
            borderSize = bb.borderSize or d.borderAndBackgroundSettings.borderSize,
        },
    }
end

local function versionParts(v)
    local parts = {}
    local clean = tostring(v or ""):gsub("^[vV]", ""):gsub("%-.*$", "")
    for n in clean:gmatch("%d+") do
        parts[#parts + 1] = tonumber(n)
    end
    return parts
end

local function isNewer(a, b)
    local pa, pb = versionParts(a), versionParts(b)
    local count = math.max(#pa, #pb)
    for i = 1, count do
        local x, y = pa[i] or 0, pb[i] or 0
        if x ~= y then
            return x > y
        end
    end
    return false
end

local function renderText(spec)
    local lines = { "" }
    local maxEntries = spec.maxEntries or 10
    for i, entry in ipairs(spec.entries or {}) do
        if i > maxEntries then break end
        local header = "v" .. tostring(entry.version or "?")
        if entry.date then
            header = header .. "   " .. tostring(entry.date)
        end
        lines[#lines + 1] = header
        lines[#lines + 1] = ""
        for _, section in ipairs(entry.sections or {}) do
            if section.label then
                lines[#lines + 1] = tostring(section.label)
                lines[#lines + 1] = ""
            end
            if section.body then
                lines[#lines + 1] = tostring(section.body)
                lines[#lines + 1] = ""
            end
        end
    end
    if spec.footer then
        lines[#lines + 1] = tostring(spec.footer)
        lines[#lines + 1] = ""
    end
    return table.concat(lines, "\n")
end

local function stripCodes(text)
    return (text:gsub("|c%x%x%x%x%x%x%x%x", ""):gsub("|r", ""))
end

local function showFallback(title, text)
    text = stripCodes(text)
    if StaticPopupDialogs and StaticPopup_Show then
        local key = "LIBMATIC_CHANGELOG"
        local suffix = tostring(title or ""):gsub("%W", "")
        if suffix ~= "" then
            key = key .. "_" .. suffix
        end
        StaticPopupDialogs[key] = {
            text = "%s\n\n%s",
            button1 = OKAY or "OK",
            timeout = 0,
            whileDead = true,
            hideOnEscape = true,
            preferredIndex = 3,
        }
        local safeTitle = tostring(title or "Change Log"):gsub("%%", "%%%%")
        local safeText = text:gsub("%%", "%%%%")
        StaticPopup_Show(key, safeTitle, safeText)
    elseif DEFAULT_CHAT_FRAME then
        DEFAULT_CHAT_FRAME:AddMessage((title or "Change Log") .. "\n" .. text)
    end
end

function lib:Show(spec)
    spec = spec or {}
    local AceGUI = fetchLib("AceGUI-3.0")
    local LSM = fetchLib("LibSharedMedia-3.0")
    local text = renderText(spec)

    if not AceGUI then
        showFallback(spec.title, text)
        return
    end

    local style = mergeStyle(spec.style)
    local frame = AceGUI:Create("Frame")
    lib.frames[frame] = true
    lib.frame = frame

    local cascade = 0
    for _ in pairs(lib.frames) do
        cascade = cascade + 1
    end
    cascade = cascade - 1
    if cascade > 0 and frame.frame and frame.frame.ClearAllPoints then
        local offset = cascade * 28
        frame.frame:ClearAllPoints()
        frame.frame:SetPoint("CENTER", UIParent, "CENTER", offset, -offset)
    end

    frame:SetCallback("OnClose", function(widget)
        lib.frames[widget] = nil
        if lib.frame == widget then
            lib.frame = next(lib.frames) or nil
        end
        AceGUI:Release(widget)
    end)
    frame:SetTitle(spec.title or "Change Log")
    if spec.statusText then
        frame:SetStatusText(spec.statusText)
    end
    frame:SetLayout("Fill")
    frame:SetWidth(spec.width or 600)
    frame:SetHeight(spec.height or 500)

    if LSM and frame.frame and frame.frame.SetBackdrop then
        frame.frame:SetBackdrop({
            bgFile = LSM:Fetch("background", style.borderAndBackgroundSettings.backgroundTexture),
            edgeFile = LSM:Fetch("border", style.borderAndBackgroundSettings.borderTexture),
            edgeSize = style.borderAndBackgroundSettings.borderSize,
        })
    end

    local container = AceGUI:Create("SimpleGroup")
    container:SetFullWidth(true)
    container:SetFullHeight(true)
    container:SetLayout("Fill")
    frame:AddChild(container)

    local scroll = AceGUI:Create("ScrollFrame")
    scroll:SetLayout("List")
    container:AddChild(scroll)

    local label = AceGUI:Create("Label")
    label:SetText(text)
    label:SetFullWidth(true)
    -- lib.fontOverride: a host addon running in a language its decorative
    -- fonts cannot draw (missing glyphs) forces the client's own font here.
    local fontPath = lib.fontOverride or (LSM and LSM:Fetch("font", style.fontSettings.font))
    if fontPath then
        label:SetFont(fontPath, style.fontSettings.fontSize, style.fontSettings.fontOutline)
    end
    local color = style.fontSettings.fontColor
    label:SetColor(color[1], color[2], color[3])
    scroll:AddChild(label)
end

function lib:ShowIfNew(spec, lastSeenVersion)
    spec = spec or {}
    local wasNewer = isNewer(spec.version, lastSeenVersion)
    if wasNewer and not spec.freshInstall then
        lib:Show(spec)
    end
    return wasNewer
end

function lib:Close()
    local AceGUI = fetchLib("AceGUI-3.0")
    local open = {}
    for f in pairs(lib.frames) do
        open[#open + 1] = f
    end
    for _, f in ipairs(open) do
        lib.frames[f] = nil
        if AceGUI then
            AceGUI:Release(f)
        end
    end
    lib.frame = nil
end

local STRINGS = {
    group = "Change Log",
    fontTab = "Font",
    font = "Font",
    fontDesc = "Font used for the change log text.",
    fontSize = "Font Size",
    fontColor = "Font Color",
    fontColorDesc = "Color of the change log text.",
    fontOutline = "Font Outline",
    outlineNone = "None",
    outline = "Outline",
    outlineMono = "Monochrome Outline",
    outlineThick = "Thick Outline",
    outlineThickMono = "Thick Monochrome Outline",
    reset = "Reset to Default",
    resetDesc = "Restore the default change log appearance.",
    resetConfirm = "Reset change log appearance to default?",
    bgTab = "Border and Background",
    border = "Border",
    borderDesc = "Border texture of the change log window.",
    borderSize = "Border Size",
    borderSizeDesc = "Thickness of the change log window border.",
    background = "Background",
    backgroundDesc = "Background texture of the change log window.",
}

local BORDER_ALLOWLIST = {
    "Blizzard Achievement Wood",
    "Blizzard Tooltip",
    "Blizzard Dialog",
    "Blizzard Dialog Gold",
    "None",
}

local function str(locale, key)
    if locale and locale[key] then
        return locale[key]
    end
    return STRINGS[key]
end

function lib:DefaultStyle(overrides)
    local d = DEFAULT_STYLE
    local ov = overrides or {}
    local ovfs = ov.fontSettings or {}
    local ovbb = ov.borderAndBackgroundSettings or {}
    local function pick(v, fallback)
        if v ~= nil then return v end
        return fallback
    end
    return {
        fontSettings = {
            font = pick(ovfs.font, d.fontSettings.font),
            fontSize = pick(ovfs.fontSize, d.fontSettings.fontSize),
            fontColor = pick(ovfs.fontColor, { d.fontSettings.fontColor[1], d.fontSettings.fontColor[2], d.fontSettings.fontColor[3] }),
            fontOutline = pick(ovfs.fontOutline, d.fontSettings.fontOutline),
        },
        borderAndBackgroundSettings = {
            backgroundTexture = pick(ovbb.backgroundTexture, d.borderAndBackgroundSettings.backgroundTexture),
            borderTexture = pick(ovbb.borderTexture, d.borderAndBackgroundSettings.borderTexture),
            borderSize = pick(ovbb.borderSize, d.borderAndBackgroundSettings.borderSize),
        },
    }
end

function lib:BuildOptionsTable(opts)
    opts = opts or {}
    local profileSource = opts.profile
    assert(profileSource, "LibMatic-ChangeLog-1.0: BuildOptionsTable requires opts.profile")
    local base = (opts.defaults and opts.defaults.fontSettings) and opts.defaults or DEFAULT_STYLE
    local locale = opts.locale
    local LSM = fetchLib("LibSharedMedia-3.0")

    local function P()
        local p = profileSource
        if type(p) == "function" then p = p() end
        p = p or {}
        p.fontSettings = p.fontSettings or {}
        local fs = p.fontSettings
        if fs.font == nil then fs.font = base.fontSettings.font end
        if fs.fontSize == nil then fs.fontSize = base.fontSettings.fontSize end
        if fs.fontColor == nil then
            fs.fontColor = { base.fontSettings.fontColor[1], base.fontSettings.fontColor[2], base.fontSettings.fontColor[3] }
        end
        if fs.fontOutline == nil then fs.fontOutline = base.fontSettings.fontOutline end
        p.borderAndBackgroundSettings = p.borderAndBackgroundSettings or {}
        local bb = p.borderAndBackgroundSettings
        if bb.backgroundTexture == nil then bb.backgroundTexture = base.borderAndBackgroundSettings.backgroundTexture end
        if bb.borderTexture == nil then bb.borderTexture = base.borderAndBackgroundSettings.borderTexture end
        if bb.borderSize == nil then bb.borderSize = base.borderAndBackgroundSettings.borderSize end
        return p
    end

    local borderValues = {}
    local backgroundValues = {}
    if LSM then
        local allBorders = LSM:HashTable("border")
        for _, name in ipairs(BORDER_ALLOWLIST) do
            if allBorders[name] then
                borderValues[name] = allBorders[name]
            end
        end
        for name, path in pairs(LSM:HashTable("background")) do
            if name ~= "None" then
                backgroundValues[name] = path
            end
        end
    end

    local function resetStyle()
        local p = P()
        p.fontSettings = {
            font = base.fontSettings.font,
            fontSize = base.fontSettings.fontSize,
            fontColor = { base.fontSettings.fontColor[1], base.fontSettings.fontColor[2], base.fontSettings.fontColor[3] },
            fontOutline = base.fontSettings.fontOutline,
        }
        p.borderAndBackgroundSettings = {
            backgroundTexture = base.borderAndBackgroundSettings.backgroundTexture,
            borderTexture = base.borderAndBackgroundSettings.borderTexture,
            borderSize = base.borderAndBackgroundSettings.borderSize,
        }
    end

    return {
        name = opts.name or str(locale, "group"),
        type = "group",
        childGroups = "tab",
        order = opts.order or 5,
        args = {
            fontTab = {
                name = str(locale, "fontTab"),
                type = "group",
                order = 1,
                args = {
                    font = {
                        name = str(locale, "font"),
                        desc = str(locale, "fontDesc"),
                        type = "select",
                        dialogControl = "LSM30_Font",
                        values = (LSM and LSM:HashTable("font")) or {},
                        width = "full",
                        order = 1,
                        get = function() return P().fontSettings.font end,
                        set = function(_, val) P().fontSettings.font = val end,
                    },
                    fontSize = {
                        name = str(locale, "fontSize"),
                        type = "range",
                        min = 8, max = 32, step = 1,
                        width = "full",
                        order = 2,
                        get = function() return P().fontSettings.fontSize end,
                        set = function(_, val) P().fontSettings.fontSize = val end,
                    },
                    fontColor = {
                        name = str(locale, "fontColor"),
                        desc = str(locale, "fontColorDesc"),
                        type = "color",
                        hasAlpha = false,
                        width = "full",
                        order = 3,
                        get = function()
                            local c = P().fontSettings.fontColor
                            return c[1], c[2], c[3]
                        end,
                        set = function(_, r, g, b)
                            P().fontSettings.fontColor = { r, g, b }
                        end,
                    },
                    fontOutline = {
                        name = str(locale, "fontOutline"),
                        type = "select",
                        width = "full",
                        order = 4,
                        values = {
                            [""] = str(locale, "outlineNone"),
                            ["OUTLINE"] = str(locale, "outline"),
                            ["OUTLINEMONOCHROME"] = str(locale, "outlineMono"),
                            ["THICKOUTLINE"] = str(locale, "outlineThick"),
                            ["THICKOUTLINEMONOCHROME"] = str(locale, "outlineThickMono"),
                        },
                        get = function() return P().fontSettings.fontOutline end,
                        set = function(_, val) P().fontSettings.fontOutline = val end,
                    },
                    reset = {
                        name = str(locale, "reset"),
                        desc = str(locale, "resetDesc"),
                        type = "execute",
                        width = "full",
                        order = 5,
                        confirm = true,
                        confirmText = str(locale, "resetConfirm"),
                        func = resetStyle,
                    },
                },
            },
            bgTab = {
                name = str(locale, "bgTab"),
                type = "group",
                order = 2,
                args = {
                    border = {
                        name = str(locale, "border"),
                        desc = str(locale, "borderDesc"),
                        type = "select",
                        dialogControl = "LSM30_Border",
                        values = borderValues,
                        width = "full",
                        order = 1,
                        get = function() return P().borderAndBackgroundSettings.borderTexture end,
                        set = function(_, val) P().borderAndBackgroundSettings.borderTexture = val end,
                    },
                    borderSize = {
                        name = str(locale, "borderSize"),
                        desc = str(locale, "borderSizeDesc"),
                        type = "range",
                        min = 1, max = 35, step = 1,
                        width = "full",
                        order = 2,
                        get = function() return P().borderAndBackgroundSettings.borderSize end,
                        set = function(_, val) P().borderAndBackgroundSettings.borderSize = val end,
                    },
                    background = {
                        name = str(locale, "background"),
                        desc = str(locale, "backgroundDesc"),
                        type = "select",
                        dialogControl = "LSM30_Background",
                        values = backgroundValues,
                        width = "full",
                        order = 3,
                        get = function() return P().borderAndBackgroundSettings.backgroundTexture end,
                        set = function(_, val) P().borderAndBackgroundSettings.backgroundTexture = val end,
                    },
                    reset = {
                        name = str(locale, "reset"),
                        desc = str(locale, "resetDesc"),
                        type = "execute",
                        width = "full",
                        order = 4,
                        confirm = true,
                        confirmText = str(locale, "resetConfirm"),
                        func = resetStyle,
                    },
                },
            },
        },
    }
end
