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

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

function lib:ResolveShow(value)
    if value == nil then return true end
    if type(value) == "function" then
        return value() and true or false
    end
    return value and true or false
end

local function resolveFlag(value, default)
    if value == nil then return default end
    if type(value) == "function" then
        return value() and true or false
    end
    return value and true or false
end

local ROSTER = {
    {
        key = "CritMatic", displayName = "CritMatic", slug = "critmatic",
        url = "https://www.curseforge.com/wow/addons/critmatic",
        allianceIcon = "critmatic-icon.tga", hordeIcon = "critmatic-icon.tga",
        factionIcons = false, released = true,
    },
    {
        key = "GearMatic", displayName = "GearMatic", slug = "gearmatic",
        url = "https://www.curseforge.com/wow/addons/gearmatic",
        allianceIcon = "gearmatic-icon.tga", hordeIcon = "gearmatic-icon.tga",
        factionIcons = false, released = true,
    },
    {
        key = "RepMatic", displayName = "RepMatic", slug = "repmatic",
        url = "https://www.curseforge.com/wow/addons/repmatic",
        allianceIcon = "repmatic-icon.tga", hordeIcon = "repmatic-icon.tga",
        factionIcons = false, released = true,
    },
    {
        key = "RareMatic", displayName = "RareMatic", slug = "rarematic",
        url = "https://www.curseforge.com/wow/addons/rarematic",
        allianceIcon = "rarematic-icon.tga", hordeIcon = "rarematic-icon.tga",
        factionIcons = false, released = false,
    },
    {
        key = "QuestMatic", displayName = "QuestMatic", slug = "questmatic",
        url = "https://www.curseforge.com/wow/addons/questmatic",
        allianceIcon = "questmatic-icon.tga", hordeIcon = "questmatic-icon.tga",
        factionIcons = false, released = false,
    },
    {
        key = "MountMatic", displayName = "MountMatic", slug = "mountmatic",
        url = "https://www.curseforge.com/wow/addons/mountmatic",
        allianceIcon = "mountmatic-icon.tga", hordeIcon = "mountmatic-icon.tga",
        factionIcons = false, released = false,
    },
}

local BRAND = {
    label = "Infinite Loop Alchemist",
    icon = "profile.tga",
}

function lib:GetRoster()
    return ROSTER
end

local STRINGS = {
    general = "General",
    alert = "Alert",
    sound = "Sound",
    social = "Social",
    changelog = "Change Log",
    profiles = "Profiles",
    alertGeneralTab = "General",
    alertMessageTab = "Messages",
    alertFontTab = "Font",
    positionAlert = "Position Alert",
    bounce = "Bounce Style",
    bounceDesc = "Animation style used for alert text.",
    bounceModern = "Bounce",
    bounceClassic = "Classic Bounce",
    preview = "Preview",
    support = "Support",
    discord = "Discord",
    coffee = "Buy Me a Coffee",
    moreAddons = "More Infinite Loop Alchemist Addons",
    reset = "Reset to Default",
    resetConfirm = "Reset these settings to default?",
    muteAll = "Mute All Sounds",
    party = "Party",
    raid = "Raid",
    guild = "Guild",
    battleground = "Battlegrounds",
    missingLSM = "LibSharedMedia-3.0 is not available.",
    missingChangeLog = "LibMatic-ChangeLog-1.0 is not available.",
    missingProfiles = "AceDBOptions-3.0 is not available.",
}

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

local function makeGetSet(bind, entry)
    if entry.get or entry.set then
        return entry.get, entry.set
    end
    if not bind or type(bind.table) ~= "function" then
        return nil, nil
    end
    local key = entry.key
    local get = function()
        local t = bind.table()
        return t and t[key]
    end
    local set = function(_, value)
        local t = bind.table()
        if t then t[key] = value end
    end
    return get, set
end

local function applyShow(node, show)
    if show == nil then return node end
    node.hidden = function()
        return not lib:ResolveShow(show)
    end
    return node
end

local function resolveIcon(entry, iconRoot, factionIcons)
    local useFaction = factionIcons
    if entry.factionIcons ~= nil then
        useFaction = entry.factionIcons
    end
    local file = entry.allianceIcon
    if useFaction and entry.hordeIcon then
        local faction = UnitFactionGroup and UnitFactionGroup("player")
        if faction == "Horde" then
            file = entry.hordeIcon
        end
    end
    return (iconRoot or "") .. (file or "")
end

local function buildNavBlock(opts, locale)
    local nav = opts.nav or {}
    local iconRoot = nav.iconRoot or ""
    local roster = nav.roster or ROSTER
    local factionIcons = resolveFlag(nav.factionIcons, false)
    local current = opts.currentAddon
    local navArgs = {}
    local order = 1

    navArgs.brand = {
        type = "description",
        order = order,
        width = "full",
        name = "|cffffffff" .. BRAND.label .. "|r",
        image = iconRoot .. BRAND.icon,
        imageWidth = 30,
        imageHeight = 30,
        fontSize = "large",
    }
    order = order + 1

    for _, entry in ipairs(roster) do
        if entry.key ~= current and entry.released then
            navArgs[entry.key .. "Icon"] = {
                type = "description",
                order = order,
                width = "full",
                name = "|cffffffff" .. entry.displayName .. "|r",
                image = resolveIcon(entry, iconRoot, factionIcons),
                imageWidth = 24,
                imageHeight = 24,
                fontSize = "medium",
            }
            order = order + 1
            navArgs[entry.key .. "Link"] = {
                type = "input",
                order = order,
                width = "full",
                name = "",
                get = function() return entry.url end,
                set = function() end,
            }
            order = order + 1
        end
    end

    if nav.extraLinks then
        for index, link in ipairs(nav.extraLinks) do
            navArgs["extra" .. index .. "Icon"] = {
                type = "description",
                order = order,
                width = "full",
                name = "|cffffffff" .. (link.label or "") .. "|r",
                image = link.icon and (iconRoot .. link.icon) or nil,
                imageWidth = link.icon and 24 or nil,
                imageHeight = link.icon and 24 or nil,
                fontSize = "medium",
            }
            order = order + 1
            navArgs["extra" .. index .. "Link"] = {
                type = "input",
                order = order,
                width = "full",
                name = "",
                get = function() return link.url or "" end,
                set = function() end,
            }
            order = order + 1
        end
    end

    return {
        type = "group",
        inline = true,
        order = nav.order or 99,
        name = str(locale, "moreAddons"),
        args = navArgs,
    }
end

local function buildSupportGroup(support, locale)
    local sArgs = {}
    local order = 1
    if support.discordURL then
        sArgs.discordLabel = {
            type = "description",
            order = order,
            width = "full",
            name = "|cffffffff" .. str(locale, "discord") .. "|r",
            fontSize = "medium",
        }
        order = order + 1
        sArgs.discordLink = {
            type = "input",
            order = order,
            width = "full",
            name = "",
            get = function() return support.discordURL end,
            set = function() end,
        }
        order = order + 1
    end
    if support.coffeeURL then
        sArgs.coffeeLabel = {
            type = "description",
            order = order,
            width = "full",
            name = "|cffffffff" .. str(locale, "coffee") .. "|r",
            image = support.coffeeIcon,
            imageWidth = support.coffeeIcon and 24 or nil,
            imageHeight = support.coffeeIcon and 24 or nil,
            fontSize = "medium",
        }
        order = order + 1
        sArgs.coffeeLink = {
            type = "input",
            order = order,
            width = "full",
            name = "",
            get = function() return support.coffeeURL end,
            set = function() end,
        }
        order = order + 1
    end
    return {
        type = "group",
        inline = true,
        order = 90,
        name = str(locale, "support"),
        args = sArgs,
    }
end

local function buildBounceNode(bounce, locale)
    local values = bounce.values or {
        mop = bounce.labelModern or str(locale, "bounceModern"),
        legacy = bounce.labelClassic or str(locale, "bounceClassic"),
    }
    return {
        name = bounce.name or str(locale, "bounce"),
        desc = bounce.desc or str(locale, "bounceDesc"),
        type = "select",
        width = "full",
        order = 1,
        values = values,
        get = function()
            if bounce.get then return bounce.get() end
        end,
        set = function(info, value)
            if bounce.set then bounce.set(info, value) end
        end,
    }
end

function lib:BuildGeneralPage(opts)
    opts = opts or {}
    if not lib:ResolveShow(opts.show) then return nil end
    assert(type(opts.currentAddon) == "string" and opts.currentAddon ~= "",
        MAJOR .. ":BuildGeneralPage requires opts.currentAddon")
    local locale = opts.locale
    local args = {}

    if opts.toggles then
        for index, t in ipairs(opts.toggles) do
            local get, set = makeGetSet(opts.bind, t)
            local node = {
                name = t.name,
                desc = t.desc,
                type = "toggle",
                order = t.order or index * 10,
                width = t.width,
                confirm = t.confirm,
                confirmText = t.confirmText,
                disabled = t.disabled,
                get = t.get or get,
                set = t.set or set,
            }
            applyShow(node, t.show)
            assert(args[t.key] == nil,
                MAJOR .. ":BuildGeneralPage toggle key collision: " .. tostring(t.key))
            args[t.key] = node
        end
    end

    if opts.extras then
        for k, v in pairs(opts.extras) do
            assert(args[k] == nil,
                MAJOR .. ":BuildGeneralPage extras key collision: " .. tostring(k))
            args[k] = v
        end
    end

    if opts.support then
        assert(args.supportGroup == nil,
            MAJOR .. ":BuildGeneralPage extras key collision: supportGroup")
        args.supportGroup = buildSupportGroup(opts.support, locale)
    end

    assert(args.byGroup == nil,
        MAJOR .. ":BuildGeneralPage extras key collision: byGroup")
    args.byGroup = buildNavBlock(opts, locale)

    return {
        name = opts.name or str(locale, "general"),
        type = "group",
        order = opts.order or 1,
        disabled = opts.disabled,
        args = args,
    }
end

function lib:BuildAlertPage(opts)
    opts = opts or {}
    if not lib:ResolveShow(opts.show) then return nil end
    assert(type(opts.notificationTypes) == "table" and #opts.notificationTypes > 0,
        MAJOR .. ":BuildAlertPage requires opts.notificationTypes")
    local locale = opts.locale
    local tabs = {}
    local tabOrder = 1

    local function buildPositionNode(position)
        return {
            name = position.name or str(locale, "positionAlert"),
            type = "execute",
            width = position.width or "double",
            order = position.order or -1,
            func = function()
                if position.close then position.close() end
                local positioner = fetchLib("LibMatic-AlertPositioner-1.0")
                if positioner then
                    positioner:Open({
                        get = position.get,
                        set = position.set,
                        default = position.default,
                        reopen = position.reopen,
                        font = position.font,
                        color = position.color,
                    })
                end
            end,
        }
    end
    local positionTab = (opts.position and opts.position.tab) or "general"

    if opts.globals or (opts.position and positionTab == "general") then
        local generalArgs = {}
        if opts.globals then
            for k, v in pairs(opts.globals) do
                generalArgs[k] = v
            end
        end
        if opts.position and positionTab == "general" then
            assert(generalArgs.positionAlert == nil,
                MAJOR .. ":BuildAlertPage globals key collision: positionAlert")
            generalArgs.positionAlert = buildPositionNode(opts.position)
        end
        tabs.generalTab = {
            name = str(locale, "alertGeneralTab"),
            type = "group",
            order = tabOrder,
            args = generalArgs,
        }
        tabOrder = tabOrder + 1
    end

    local messageArgs = {}
    if opts.bounce then
        messageArgs.bounceStyle = buildBounceNode(opts.bounce, locale)
    end
    for index, nt in ipairs(opts.notificationTypes) do
        local get, set = makeGetSet(opts.bind, nt)
        if not get then
            get = function() return nt.defaultFormat end
        end
        local node = {
            name = nt.title,
            desc = nt.desc,
            type = "input",
            width = "full",
            order = 10 + index * 10,
            get = get,
            set = set,
        }
        applyShow(node, nt.show)
        assert(messageArgs[nt.key] == nil,
            MAJOR .. ":BuildAlertPage notificationType key collision: " .. tostring(nt.key))
        messageArgs[nt.key] = node
        if nt.preview then
            messageArgs[nt.key .. "Preview"] = {
                name = str(locale, "preview"),
                type = "execute",
                width = "normal",
                order = 11 + index * 10,
                func = nt.preview,
            }
        end
    end
    if opts.messageExtras then
        for k, v in pairs(opts.messageExtras) do
            assert(messageArgs[k] == nil,
                MAJOR .. ":BuildAlertPage messageExtras key collision: " .. tostring(k))
            messageArgs[k] = v
        end
    end
    tabs.messageTab = {
        name = str(locale, "alertMessageTab"),
        type = "group",
        order = tabOrder,
        args = messageArgs,
    }
    tabOrder = tabOrder + 1

    if opts.font or (opts.position and positionTab == "font") then
        local fontArgs = {}
        if opts.font then
            for k, v in pairs(opts.font) do
                fontArgs[k] = v
            end
        end
        if opts.position and positionTab == "font" then
            assert(fontArgs.positionAlert == nil,
                MAJOR .. ":BuildAlertPage font key collision: positionAlert")
            fontArgs.positionAlert = buildPositionNode(opts.position)
        end
        tabs.fontTab = {
            name = str(locale, "alertFontTab"),
            type = "group",
            order = tabOrder,
            args = fontArgs,
        }
    end

    return {
        name = opts.name or str(locale, "alert"),
        type = "group",
        childGroups = "tab",
        order = opts.order or 2,
        disabled = opts.disabled,
        args = tabs,
    }
end

function lib:BuildSoundPage(opts)
    opts = opts or {}
    if not lib:ResolveShow(opts.show) then return nil end
    assert(type(opts.slots) == "table",
        MAJOR .. ":BuildSoundPage requires opts.slots")
    local locale = opts.locale
    local LSM = fetchLib("LibSharedMedia-3.0")
    if not LSM then
        return {
            name = opts.name or str(locale, "sound"),
            type = "group",
            order = opts.order or 3,
            args = {
                missing = { type = "description", order = 1, name = str(locale, "missingLSM") },
            },
        }
    end
    local args = {}
    for index, slot in ipairs(opts.slots) do
        local get, set = makeGetSet(opts.bind, slot)
        local node = {
            name = slot.label,
            desc = slot.desc,
            type = "select",
            dialogControl = "LSM30_Sound",
            values = LSM:HashTable("sound"),
            width = "full",
            order = index * 10,
            get = get,
            set = set,
        }
        applyShow(node, slot.show)
        assert(args[slot.key] == nil,
            MAJOR .. ":BuildSoundPage slot key collision: " .. tostring(slot.key))
        args[slot.key] = node
    end
    if opts.muteToggle then
        local mt = opts.muteToggle
        local get, set = makeGetSet(opts.bind, mt)
        assert(args[mt.key] == nil,
            MAJOR .. ":BuildSoundPage slot key collision: " .. tostring(mt.key))
        args[mt.key] = {
            name = mt.name or str(locale, "muteAll"),
            desc = mt.desc,
            type = "toggle",
            width = "full",
            order = 500,
            get = mt.get or get,
            set = mt.set or set,
        }
    end
    if opts.reset ~= false and opts.bind
        and type(opts.bind.table) == "function"
        and type(opts.bind.defaults) == "function" then
        args.resetSounds = {
            name = str(locale, "reset"),
            type = "execute",
            width = "full",
            order = 600,
            confirm = true,
            confirmText = str(locale, "resetConfirm"),
            func = function()
                local target = opts.bind.table()
                local defaults = opts.bind.defaults()
                if target and defaults then
                    for k in pairs(target) do target[k] = nil end
                    for k, v in pairs(defaults) do target[k] = v end
                end
            end,
        }
    end
    return {
        name = opts.name or str(locale, "sound"),
        type = "group",
        order = opts.order or 3,
        disabled = opts.disabled,
        args = args,
    }
end

local DEFAULT_CHANNELS = { "party", "raid", "guild", "battleground" }

function lib:BuildSocialPage(opts)
    opts = opts or {}
    if not lib:ResolveShow(opts.show) then return nil end
    local locale = opts.locale
    local args = {}
    local channels = opts.channels
    if not channels then
        channels = {}
        for _, key in ipairs(DEFAULT_CHANNELS) do
            channels[#channels + 1] = { key = key }
        end
    end
    for index, ch in ipairs(channels) do
        local get, set = makeGetSet(opts.bind, ch)
        local node = {
            name = ch.name or str(locale, ch.key) or ch.key,
            desc = ch.desc,
            type = "toggle",
            width = "full",
            order = ch.order or index * 10,
            get = ch.get or get,
            set = ch.set or set,
        }
        applyShow(node, ch.show)
        assert(args[ch.key] == nil,
            MAJOR .. ":BuildSocialPage channel key collision: " .. tostring(ch.key))
        args[ch.key] = node
    end
    if opts.formats then
        for index, fmt in ipairs(opts.formats) do
            local get, set = makeGetSet(opts.bind, fmt)
            if not get then
                get = function() return fmt.defaultFormat end
            end
            local node = {
                name = fmt.title,
                desc = fmt.desc,
                type = "input",
                width = "full",
                order = 100 + index * 10,
                get = get,
                set = set,
            }
            applyShow(node, fmt.show)
            assert(args[fmt.key] == nil,
                MAJOR .. ":BuildSocialPage format key collision: " .. tostring(fmt.key))
            args[fmt.key] = node
        end
    end
    return {
        name = opts.name or str(locale, "social"),
        type = "group",
        order = opts.order or 4,
        disabled = opts.disabled,
        args = args,
    }
end

function lib:BuildChangeLogPage(opts)
    opts = opts or {}
    if not lib:ResolveShow(opts.show) then return nil end
    local locale = opts.locale
    local cl = fetchLib("LibMatic-ChangeLog-1.0")
    if not cl then
        return {
            name = opts.name or str(locale, "changelog"),
            type = "group",
            order = opts.order or 5,
            args = {
                missing = { type = "description", order = 1, name = str(locale, "missingChangeLog") },
            },
        }
    end
    local passthrough = {}
    for k, v in pairs(opts) do
        passthrough[k] = v
    end
    passthrough.order = opts.order or 5
    passthrough.name = opts.name or str(locale, "changelog")
    return cl:BuildOptionsTable(passthrough)
end

function lib:BuildProfilesPage(opts)
    opts = opts or {}
    if not lib:ResolveShow(opts.show) then return nil end
    local locale = opts.locale
    local ado = fetchLib("AceDBOptions-3.0")
    if not ado or not opts.db then
        return {
            name = opts.name or str(locale, "profiles"),
            type = "group",
            order = opts.order or 90,
            args = {
                missing = { type = "description", order = 1, name = str(locale, "missingProfiles") },
            },
        }
    end
    local group = ado:GetOptionsTable(opts.db)
    group.order = opts.order or 90
    if opts.name then
        group.name = opts.name
    end
    if opts.disabled ~= nil then
        group.disabled = opts.disabled
    end
    return group
end
