--!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.project.config")
import("core.project.project")
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")
import("private.detect.find_platform")

-- 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(not option.get("clean"))
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 =
    {
        target      = true
    ,   file        = true
    ,   root        = true
    ,   yes         = true
    ,   quiet       = true
    ,   confirm     = true
    ,   project     = true
    ,   verbose     = true
    ,   diagnosis   = true
    ,   require     = true
    ,   version     = true
    ,   help        = true
    ,   clean       = true
    ,   menu        = true
    ,   import      = true
    ,   export      = true
    ,   check       = true
    }
    return not options[name] and not project.option(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 = {}
    local categories = table.orderkeys(options_by_category)
    for _, category in ipairs(categories) do

        -- get or make menu by category
        local options  = options_by_category[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 = config.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 basic configs
function app:_basic_configs(cache)

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

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

    -- merge options by category
    local category = "."
    local options = menu and menu.options or {}
    local options_by_category = {}
    local keys = table.orderkeys(options)
    for _, key in ipairs(keys) do
        local opt = options[key]
        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._BASIC_CONFIGS = self:_make_configs_by_category("Basic Configuration", options_by_category, cache, function (opt)

        -- get option
        local name    = opt[2] or opt[1]
        local default = opt[4]
        local kind    = (opt[3] == "k" or type(default) == "boolean") and "boolean" or "string"

        -- get default values
        if name == "plat" then
            default = find_platform()
        elseif name == "arch" then
            _, default = find_platform()
        elseif name == "mode" then
            default = project.get("defaultmode")
            if not default then
                default = "release"
            end
        end

        -- choice option?
        local values = opt.values
        if values then
            if type(values) == "function" then
                values = values(false, {menuconf = true})
            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 = name, kind = kind, default = default, values = values, description = description}
    end)
    return self._BASIC_CONFIGS
end

-- get project configs
function app:_project_configs(cache)

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

    -- merge options by category
    local options = project.options()
    local options_by_category = {}
    local keys = table.orderkeys(options)
    for _, key in ipairs(keys) do
        local opt = options[key]
        if opt:showmenu() ~= false then
            local category = "."
            if opt:get("category") then category = table.unwrap(opt:get("category")) end
            options_by_category[category] = options_by_category[category] or {}
            table.insert(options_by_category[category], opt)
        end
    end

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

        -- the default value
        local default = "auto"
        if opt:get("default") ~= nil then
            default = opt:get("default")
        end

        -- get kind
        local kind = (type(default) == "string") and "string" or "boolean"

        -- get description
        local description = opt:description()

        -- get source info
        local sourceinfo = (opt:get("__sourceinfo_description") or {})[type(description) == "table" and description[1] or description]

        -- choice option?
        local values = opt:get("values")
        if values then
            kind = "choice"
            values = table.wrap(values)
            for idx, value in ipairs(values) do
                if default == value then
                    default = idx
                    break
                end
            end
        end
        return {name = opt:name(), kind = kind, default = default, values = values, description = description, sourceinfo = sourceinfo}
    end)
    return self._PROJECT_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

-- Have configs been changed?
function app:_configs_changed()
    return self._CONFIGS_CHANGED
end

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

    -- merge configuration from the given import file or cache
    local loaded = false
    local importfile = option.get("import")
    if importfile and os.isfile(importfile) then
        loaded = config.load(importfile)
    elseif cache then
        loaded = config.load()
    end

    -- clear configs first
    self._BASIC_CONFIGS = nil
    self._PROJECT_CONFIGS = nil

    -- load configs
    local configs = {}
    table.insert(configs, menuconf.menu {description = "Basic Configuration", configs = self:_basic_configs(cache)})
    table.insert(configs, menuconf.menu {description = "Project Configuration", configs = self:_project_configs(cache)})
    self:mconfdialog():load(configs)

    -- the previous config is only for loading menuconf, so clear config now
    if loaded then
        config.clear()
    end
end

-- save configs to options
function app:save()
    self:_save_configs(self:_basic_configs())
    self:_save_configs(self:_project_configs())
    self._CONFIGS_CHANGED = true
end

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