local ui = require('openmw.ui')
local util = require('openmw.util')
local async = require('openmw.async')
local core = require('openmw.core')
local storage = require('openmw.storage')
local I = require('openmw.interfaces')

local common = require('scripts.omw.settings.common')

local renderers = {}
local function registerRenderer(name, renderFunction)
    renderers[name] = renderFunction
end

local interfaceL10n = core.l10n('Interface')

local pages = {}
local groups = {}
local pageOptions = {}

local interval = { template = I.MWUI.templates.interval }
local growingIntreval = {
    template = I.MWUI.templates.interval,
    external = {
        grow = 1,
    },
}
local spacer =  {
    props = {
        size = util.vector2(0, 10),
    },
}
local bigSpacer =  {
    props = {
        size = util.vector2(0, 50),
    },
}
local stretchingLine = {
    template = I.MWUI.templates.horizontalLine,
    external = {
        stretch = 1,
    },
}
local spacedLines = function(count)
    local content = {}
    table.insert(content, spacer)
    table.insert(content, stretchingLine)
    for i = 2, count do
        table.insert(content, interval)
        table.insert(content, stretchingLine)
    end
    table.insert(content, spacer)
    return {
        type = ui.TYPE.Flex,
        external = {
            stretch = 1,
        },
        content = ui.content(content),
    }
end

local function interlaceSeparator(layouts, separator)
    local result = {}
    result[1] = layouts[1]
    for i = 2, #layouts do
        table.insert(result, separator)
        table.insert(result, layouts[i])
    end
    return result
end

local function setSettingValue(global, groupKey, settingKey, value)
    if global then
        core.sendGlobalEvent(common.setGlobalEvent, {
            groupKey = groupKey,
            settingKey = settingKey,
            value = value,
        })
    else
        storage.playerSection(groupKey):set(settingKey, value)
    end
end

local function renderSetting(group, setting, value, global)
    local renderFunction = renderers[setting.renderer]
    if not renderFunction then
        error(('Setting %s of %s has unknown renderer %s'):format(setting.key, group.key, setting.renderer))
    end
    local set = function(value)
        setSettingValue(global, group.key, setting.key, value)
    end
    local l10n = core.l10n(group.l10n)
    local titleLayout = {
        type = ui.TYPE.Flex,
        content = ui.content {
            {
                template = I.MWUI.templates.textHeader,
                props = {
                    text = l10n(setting.name),
                },
            },
        },
    }
    if setting.description then
        titleLayout.content:add(interval)
        titleLayout.content:add {
            template = I.MWUI.templates.textParagraph,
            props = {
                text = l10n(setting.description),
                size = util.vector2(300, 0),
            },
        }
    end
    local argument = common.getArgumentSection(global, group.key):get(setting.key)
    return {
        name = setting.key,
        type = ui.TYPE.Flex,
        props = {
            horizontal = true,
            arrange = ui.ALIGNMENT.Center,
        },
        external = {
            stretch = 1,
        },
        content = ui.content {
            titleLayout,
            growingIntreval,
            renderFunction(value, set, argument),
        },
    }
end

local groupLayoutName = function(key, global)
    return ('%s%s'):format(global and 'global_' or 'player_', key)
end

local function renderGroup(group, global)
    local l10n = core.l10n(group.l10n)

    local valueSection = common.getSection(global, group.key)
    local settingLayouts = {}
    local sortedSettings = {}
    for _, setting in pairs(group.settings) do
        sortedSettings[setting.order] = setting
    end
    for _, setting in ipairs(sortedSettings) do
        table.insert(settingLayouts, renderSetting(group, setting, valueSection:get(setting.key), global))
    end
    local settingsContent = ui.content(interlaceSeparator(settingLayouts, spacedLines(1)))

    local resetButtonLayout = {
        template = I.MWUI.templates.box,
        events = {
            mouseClick = async:callback(function()
                for _, setting in pairs(group.settings) do
                    setSettingValue(global, group.key, setting.key, setting.default)
                end
            end),
        },
        content = ui.content {
            {
                template = I.MWUI.templates.padding,
                content = ui.content {
                    {
                        template = I.MWUI.templates.textNormal,
                        props = {
                            text = interfaceL10n('Reset')
                        },
                    },
                },
            },
        },
    }

    local titleLayout = {
        type = ui.TYPE.Flex,
        external = {
            stretch = 1,
        },
        content = ui.content {
            {
                template = I.MWUI.templates.textHeader,
                props = {
                    text = l10n(group.name),
                    textSize = 20,
                },
            }
        },
    }
    if group.description then
        titleLayout.content:add(interval)
        titleLayout.content:add {
            template = I.MWUI.templates.textParagraph,
            props = {
                text = l10n(group.description),
                size = util.vector2(300, 0),
            },
        }
    end

    return {
        name = groupLayoutName(group.key, global),
        type = ui.TYPE.Flex,
        external = {
            stretch = 1,
        },
        content = ui.content {
            {
                type = ui.TYPE.Flex,
                props = {
                    horizontal = true,
                    arrange = ui.ALIGNMENT.Center,
                },
                external = {
                    stretch = 1,
                },
                content = ui.content {
                    titleLayout,
                    growingIntreval,
                    resetButtonLayout,
                },
            },
            spacedLines(2),
            {
                name = 'settings',
                type = ui.TYPE.Flex,
                content = settingsContent,
                external = {
                    stretch = 1,
                },
            },
        },
    }
end

local function pageGroupComparator(a, b)
    return a.order < b.order or (
        a.order == b.order and a.key < b.key
    )
end

local function generateSearchHints(page)
    local hints = {}
    local l10n = core.l10n(page.l10n)
    table.insert(hints, l10n(page.name))
    if page.description then
        table.insert(hints, l10n(page.description))
    end
    local pageGroups = groups[page.key]
    for _, pageGroup in pairs(pageGroups) do
        local group = common.getSection(pageGroup.global, common.groupSectionKey):get(pageGroup.key)
        local l10n = core.l10n(group.l10n)
        table.insert(hints, l10n(group.name))
        if group.description then
            table.insert(hints, l10n(group.description))
        end
        for _, setting in pairs(group.settings) do
            table.insert(hints, l10n(setting.name))
            if setting.description then
                table.insert(hints, l10n(setting.description))
            end
        end
    end
    return table.concat(hints, ' ')
end

local function renderPage(page)
    local l10n = core.l10n(page.l10n)
    local sortedGroups = {}
    for i, v in ipairs(groups[page.key]) do sortedGroups[i] = v end
    table.sort(sortedGroups, pageGroupComparator)
    local groupLayouts = {}
    for _, pageGroup in ipairs(sortedGroups) do
        local group = common.getSection(pageGroup.global, common.groupSectionKey):get(pageGroup.key)
        table.insert(groupLayouts, renderGroup(group, pageGroup.global))
    end
    local groupsLayout = {
        name = 'groups',
        type = ui.TYPE.Flex,
        external = {
            stretch = 1,
        },
        content = ui.content(interlaceSeparator(groupLayouts, bigSpacer)),
    }
    local titleLayout = {
        type = ui.TYPE.Flex,
        external = {
            stretch = 1,
        },
        content = ui.content {
            {
                template = I.MWUI.templates.textHeader,
                props = {
                    text = l10n(page.name),
                    textSize = 22,
                },
            },
            spacedLines(3),
        },
    }
    if page.description then
        titleLayout.content:add {
            template = I.MWUI.templates.textParagraph,
            props = {
                text = l10n(page.description),
                size = util.vector2(300, 0),
            },
        }
    end
    local layout = {
        name = page.key,
        type = ui.TYPE.Flex,
        props = {
            position = util.vector2(10, 10),
        },
        content = ui.content {
            titleLayout,
            bigSpacer,
            groupsLayout,
            bigSpacer,
        },
    }
    return {
        name = l10n(page.name),
        element = ui.create(layout),
        searchHints = generateSearchHints(page),
    }
end

local function onSettingChanged(global)
    return async:callback(function(groupKey, settingKey)
        local group = common.getSection(global, common.groupSectionKey):get(groupKey)
        if not group or not pageOptions[group.page] then return end

        local value = common.getSection(global, group.key):get(settingKey)

        local element = pageOptions[group.page].element
        local groupsLayout = element.layout.content.groups
        local groupLayout = groupsLayout.content[groupLayoutName(group.key, global)]
        local settingsContent = groupLayout.content.settings.content
        settingsContent[settingKey] = renderSetting(group, group.settings[settingKey], value, global)
        element:update()
    end)
end
local function onGroupRegistered(global, key)
    local group = common.getSection(global, common.groupSectionKey):get(key)
    groups[group.page] = groups[group.page] or {}
    local pageGroup =  {
        key = group.key,
        global = global,
        order = group.order,
    }
    table.insert(groups[group.page], pageGroup)
    common.getSection(global, group.key):subscribe(onSettingChanged(global))
    common.getArgumentSection(global, group.key):subscribe(async:callback(function(_, settingKey)
        local groupKey = group.key
        local group = common.getSection(global, common.groupSectionKey):get(groupKey)
        if not group or not pageOptions[group.page] then return end

        local value = common.getSection(global, group.key):get(settingKey)

        local element = pageOptions[group.page].element
        local groupsLayout = element.layout.content.groups
        local groupLayout = groupsLayout.content[groupLayoutName(group.key, global)]
        local settingsContent = groupLayout.content.settings.content
        settingsContent[settingKey] = renderSetting(group, group.settings[settingKey], value, global)
        element:update()
    end))

    if not pages[group.page] then return end
    local options = renderPage(pages[group.page])
    if pageOptions[group.page] then
        pageOptions[group.page].element:destroy()
    else
        pageOptions[group.page] = {}
    end
    for k, v in pairs(options) do
        pageOptions[group.page][k] = v
    end
end
local globalGroups = storage.globalSection(common.groupSectionKey)
for groupKey in pairs(globalGroups:asTable()) do
    onGroupRegistered(true, groupKey)
end
globalGroups:subscribe(async:callback(function(_, key)
    if key then onGroupRegistered(true, key) end
end))
storage.playerSection(common.groupSectionKey):subscribe(async:callback(function(_, key)
    if key then onGroupRegistered(false, key) end
end))

local function registerPage(options)
    if type(options) ~= 'table' then
        error('Page options must be a table')
    end
    if type(options.key) ~= 'string' then
        error('Page must have a key')
    end
    if type(options.l10n) ~= 'string' then
        error('Page must have a localization context')
    end
    if type(options.name) ~= 'string' then
        error('Page must have a name')
    end
    if options.description ~= nil and type(options.description) ~= 'string' then
        error('Page description key must be a string')
    end
    local page = {
        key = options.key,
        l10n = options.l10n,
        name = options.name,
        description = options.description,
    }
    pages[page.key] = page
    groups[page.key] = groups[page.key] or {}
    pageOptions[page.key] = renderPage(page)
    ui.registerSettingsPage(pageOptions[page.key])
end

return {
    registerPage = registerPage,
    registerRenderer = registerRenderer,
}