--!A cross-platform build utility based on Lua
--
-- Licensed under the Apache License, Version 2.0 (the "License");
-- you may not use this file except in compliance with the License.
-- You may obtain a copy of the License at
--
--     http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
-- Copyright (C) 2015-present, Xmake Open Source Community.
--
-- @author      ruki
-- @file        menuconf.lua
--

-- imports
import("core.base.option")
import("core.base.global")
import("core.ui.log")
import("core.ui.rect")
import("core.ui.view")
import("core.ui.label")
import("core.ui.event")
import("core.ui.action")
import("core.ui.menuconf")
import("core.ui.mconfdialog")
import("core.ui.application")

-- the app application
local app = application()

-- init app
function app:init()

    -- init name
    application.init(self, "app.config")

    -- init background
    self:background_set("blue")

    -- insert menu config dialog
    self:insert(self:mconfdialog())

    -- load configs
    self:load()
end

-- get menu config dialog
function app:mconfdialog()
    if not self._MCONFDIALOG then
        local mconfdialog = mconfdialog:new("app.config.mconfdialog", rect {1, 1, self:width() - 1, self:height() - 1}, "menu config")
        mconfdialog:action_set(action.ac_on_exit, function (v)
            self:quit()
            os.exit()
        end)
        mconfdialog:action_set(action.ac_on_save, function (v)
            self:save()
            self:quit()
        end)
        self._MCONFDIALOG = mconfdialog
    end
    return self._MCONFDIALOG
end

-- on resize
function app:on_resize()
    self:mconfdialog():bounds_set(rect{1, 1, self:width() - 1, self:height() - 1})
    application.on_resize(self)
end

-- filter option
function app:_filter_option(name)
    local options =
    {
        file        = true
    ,   root        = true
    ,   yes         = true
    ,   quiet       = true
    ,   confirm     = true
    ,   project     = true
    ,   verbose     = true
    ,   diagnosis   = true
    ,   version     = true
    ,   help        = true
    ,   clean       = true
    ,   menu        = true
    ,   check       = true
    }
    return not options[name]
end

-- get or make menu by category
function app:_menu_by_category(root, configs, menus, category)

    -- is root?
    if category == "." or category == "" then
        return
    end

    -- attempt to get menu first
    local menu = menus[category]
    if not menu then

        -- get config path
        local parentdir = path.directory(category)
        local config_path = path.join(root, parentdir == "." and "" or parentdir)

        -- make a new menu
        menu = menuconf.menu {name = category, path = config_path, description = path.basename(category), configs = {}}
        menus[category] = menu

        -- insert to the parent or root configs
        local parent = self:_menu_by_category(root, configs, menus, parentdir)
        table.insert(parent and parent.configs or configs, menu)
    end
    return menu
end

-- make configs by category
function app:_make_configs_by_category(root, options_by_category, cache, get_option_info)

    -- make configs category
    --
    -- root category: "."
    -- category path: "a", "a/b", "a/b/c" ...
    --
    local menus = {}
    local configs = {}
    for category, options in pairs(options_by_category) do

        -- get or make menu by category
        local menu = self:_menu_by_category(root, configs, menus, category)

        -- get sub-configs
        local subconfigs = menu and menu.configs or configs

        -- insert options to sub-configs
        for _, opt in ipairs(options) do

            -- get option info
            local info = get_option_info(opt)

            -- new value?
            local newvalue = true

            -- load value
            local value = nil
            if cache then
                value = global.get(info.name)
                if value ~= nil and info.kind == "choice" and info.values then
                    for idx, val in ipairs(info.values) do
                        if value == val then
                            value = idx
                            break
                        end
                    end
                end
                if value ~= nil then
                    newvalue = false
                end
            end

            -- find the menu index in subconfigs
            local menu_index = #subconfigs + 1
            for idx, subconfig in ipairs(subconfigs) do
                if subconfig.kind == "menu" then
                    menu_index = idx
                    break
                end
            end

            -- get config path
            local config_path = path.join(root, category == "." and "" or category)

            -- insert config before all sub-menus
            if info.kind == "string" then
                table.insert(subconfigs, menu_index, menuconf.string {name = info.name, value = value, new = newvalue, default = info.default, path = config_path, description = info.description, sourceinfo = info.sourceinfo})
            elseif info.kind == "boolean" then
                table.insert(subconfigs, menu_index, menuconf.boolean {name = info.name, value = value, new = newvalue, default = info.default, path = config_path, description = info.description, sourceinfo = info.sourceinfo})
            elseif info.kind == "choice" then
                table.insert(subconfigs, menu_index, menuconf.choice {name = info.name, value = value, new = newvalue, default = info.default, path = config_path, values = info.values, description = info.description, sourceinfo = info.sourceinfo})
            end
        end
    end

    -- done
    return configs
end

-- get global configs
function app:_global_configs(cache)

    -- get configs from the cache first
    local configs = self._GLOBAL_CONFIGS
    if configs then
        return configs
    end

    -- get config menu
    local menu = option.taskmenu("global")

    -- merge options by category
    local category = "."
    local options = menu and menu.options or {}
    local options_by_category = {}
    for _, opt in pairs(options) do
        local name = opt[2] or opt[1]
        if name and self:_filter_option(name) then
            options_by_category[category] = options_by_category[category] or {}
            table.insert(options_by_category[category], opt)
        elseif opt.category then
            category = opt.category
        end
    end

    -- make configs by category
    self._GLOBAL_CONFIGS = self:_make_configs_by_category("Global Configuration", options_by_category, cache, function (opt)

        -- get default
        local default = opt[4]

        -- get kind
        local kind = (opt[3] == "k" or type(default) == "boolean") and "boolean" or "string"

        -- choice option?
        local values = opt.values
        if values then
            if type(values) == "function" then
                values = values()
            end
            values = table.wrap(values)
            for idx, value in ipairs(values) do
                if default == value then
                    default = idx
                    break
                end
            end
        end
        if values then
            kind = "choice"
        end

        -- get description
        local description = {}
        for i = 5, 64 do
            local desc = opt[i]
            if type(desc) == "function" then
                desc = desc()
            end
            if type(desc) == "string" then
                table.insert(description, desc)
            elseif type(desc) == "table" then
                table.join2(description, desc)
            else
                break
            end
        end

        -- make option info
        return {name = opt[2] or opt[1], kind = kind, default = default, values = values, description = description}
    end)
    return self._GLOBAL_CONFIGS
end

-- save the given configs
function app:_save_configs(configs)
    local options = option.options()
    for _, conf in pairs(configs) do
        if conf.kind == "menu" then
            self:_save_configs(conf.configs)
        elseif not conf.new and (conf.kind == "boolean" or conf.kind == "string") then
            options[conf.name] = conf.value
        elseif not conf.new and (conf.kind == "choice") then
            options[conf.name] = conf.values[conf.value]
        end
    end
end

-- load configs from options
function app:load()

    -- clean the global configuration
    if option.get("clean") then
        global.clear()
    end

    -- clear configs first
    self._GLOBAL_CONFIGS = nil

    -- load configs
    local configs = {}
    table.insert(configs, menuconf.menu {description = "Global Configuration", configs = self:_global_configs(cache)})
    self:mconfdialog():load(configs)

    -- the previous config is only for loading menuconf, so clear config now
    if option.get("clean") then
        global.clear()
    end
end

-- save configs to options
function app:save()
    self:_save_configs(self:_global_configs())
end

-- main entry
function main(...)
    app:run(...)
end
