---A cross-platform terminal ui library 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-2020, TBOOX Open Source Group.
--
-- @author      ruki
-- @file        menuconf.lua
--

-- load modules
local log       = require("ltui/base/log")
local view      = require("ltui/view")
local rect      = require("ltui/rect")
local panel     = require("ltui/panel")
local event     = require("ltui/event")
local action    = require("ltui/action")
local curses    = require("ltui/curses")
local button    = require("ltui/button")
local object    = require("ltui/object")

-- define module
local menuconf = menuconf or panel()

-- init menuconf
function menuconf:init(name, bounds)

    -- init panel
    panel.init(self, name, bounds)

    -- init configs
    self._CONFIGS = {}

    -- init items
    self._ITEMS = {}

    -- init start index
    self._STARTINDEX = 1
end

-- load configs
function menuconf:load(configs)

    -- clear the views first
    self:clear()

    -- reset start index
    self._STARTINDEX = 1

    -- detach the previous config and view
    local configs_prev = self._CONFIGS._PREV
    if configs_prev then
        for _, config in ipairs(configs_prev) do
            config._view = nil
        end
    end

    -- save configs
    self._CONFIGS = configs

    -- load items
    local items = {}
    for idx, config in ipairs(configs) do
        table.insert(items, self:_load_item(config, idx))
    end
    self._ITEMS = items

    -- insert top-n items
    local startindex = self._STARTINDEX
    for idx = startindex, startindex + self:height() - 1 do
        local item = items[idx]
        if item then
            self:insert(item)
        else
            break
        end
    end

    -- select the first item
    self:select(self:first())

    -- on loaded
    self:action_on(action.ac_on_load)

    -- invalidate
    self:invalidate()
end

-- is scrollable?
function menuconf:scrollable()
    return #self:_items() > self:height()
end

-- scroll
function menuconf:scroll(count)
    if self:scrollable() then
        local items = self:_items()
        local totalcount = #items
        local startindex = self._STARTINDEX + count
        if startindex > totalcount then
            return
        elseif startindex < 1 then
            startindex = 1
        end
        self._STARTINDEX = startindex
        self:clear()
        for idx = startindex, startindex + self:height() - 1 do
            local item = items[idx]
            if item then
                item:bounds():move2(0, idx - startindex)
                self:insert(item)
            else
                break
            end
        end
        if count > 0 then
            self:select(self:first())
        else
            self:select(self:last())
        end
        self:invalidate()
    end
end

-- on resize
function menuconf:on_resize()
    local items = self:_items()
    local totalcount = #items
    local startindex = self._STARTINDEX
    for idx = 1, totalcount do
        local item = items[idx]
        if item then
            if idx >= startindex and idx < startindex + self:height() then
                if not self:view(item:name()) then
                    item:bounds():move2(0, idx - startindex)
                    self:insert(item)
                end
            else
                if self:view(item:name()) then
                    self:remove(item)
                end
            end
        end
    end
    panel.on_resize(self)
end

-- on event
function menuconf:on_event(e)
    local back = false
    if e.type == event.ev_keyboard then
        if e.key_name == "Down" then
            if self:current() == self:last() then
                self:scroll(self:height())
            else
                self:select_next()
            end
            self:_notify_scrolled()
            return true
        elseif e.key_name == "Up" then
            if self:current() == self:first() then
                self:scroll(-self:height())
            else
                self:select_prev()
            end
            self:_notify_scrolled()
            return true
        elseif e.key_name == "PageDown" or e.key_name == "PageUp" then
            local direction = e.key_name == "PageDown" and 1 or -1
            self:scroll(self:height() * direction)
            self:_notify_scrolled()
            return true
        elseif e.key_name == "Enter" or e.key_name == " " then
            self:_do_select()
            return true
        elseif e.key_name:lower() == "y" then
            self:_do_include(true)
            return true
        elseif e.key_name:lower() == "n" then
            self:_do_include(false)
            return true
        elseif e.key_name == "Esc" then
            back = true
        end
    elseif e.type == event.ev_command then
        if e.command == "cm_enter" then
            self:_do_select()
            return true
        elseif e.command == "cm_back" then
            back = true
        end
    end

    -- back?
    if back then
        -- load the previous menu configs
        local configs_prev = self._CONFIGS._PREV
        if configs_prev then
            self._CONFIGS._PREV = configs_prev._PREV
            self:load(configs_prev)
            return true
        end
    end
end

-- load a config item
function menuconf:_load_item(config, index)

    -- init a config item view
    local item = button:new("menuconf.config." .. index,
                    rect:new(0, index - 1, self:width(), 1),
                    tostring(config),
                    function (v, e)
                        self:_do_select()
                    end)

    -- attach this index and config
    item:extra_set("index", index)
    item:extra_set("config", config)

    -- attach this view
    config._view = item
    return item
end

-- notify scrolled
function menuconf:_notify_scrolled()
    local totalcount = #self:_items()
    local startindex = self:current():extra("index")
    self:action_on(action.ac_on_scrolled, startindex / totalcount)
end

-- get all items
function menuconf:_items()
    return self._ITEMS
end

-- do select the current config
function menuconf:_do_select()

    -- get the current item
    local item = self:current()

    -- get the current config
    local config = item:extra("config")

    -- clear new state
    config.new = false

    -- do action: on selected
    if self:action_on(action.ac_on_selected, config) then
        return
    end

    -- select the boolean config
    if config.kind == "boolean" then
        config.value = not config.value
    -- show sub-menu configs
    elseif config.kind == "menu" and config.configs and #config.configs > 0 then
        local configs_prev = self._CONFIGS
        self:load(config.configs)
        self._CONFIGS._PREV = configs_prev
    end
end

-- do include
function menuconf:_do_include(enabled)

    -- get the current item
    local item = self:current()

    -- get the current config
    local config = item:extra("config")

    -- clear new state
    config.new = false

    -- select the boolean config
    if config.kind == "boolean" then
        config.value = enabled
    end
end

-- init config object
--
-- kind
--  - {kind = "number/boolean/string/choice/menu"}
--
-- description
--  - {description = "config item description"}
--  - {description = {"config item description", "line2", "line3", "more description ..."}}
--
-- boolean config
--  - {name = "...", kind = "boolean", value = true, default = true, description = "boolean config item", new = true/false}
--
-- number config
--  - {name = "...", kind = "number", value = 10, default = 0, description = "number config item", new = true/false}
--
-- string config
--  - {name = "...", kind = "string", value = "xmake", default = "", description = "string config item", new = true/false}
--
-- choice config (value is index)
--  - {name = "...", kind = "choice", value = 1, default = 1, description = "choice config item", values = {2, 2, 3, 4, 5}}
--
-- menu config
--  - {name = "...", kind = "menu", description = "menu config item", configs = {...}}
--
local config = config or object{new = true,
                                __index = function (tbl, key)
                                    if key == "value" then
                                        local val = rawget(tbl, "_value")
                                        if val == nil then
                                            val = rawget(tbl, "default")
                                        end
                                        return val
                                    end
                                    return rawget(tbl, key)
                                end,
                                __newindex = function (tbl, key, val)
                                    if key == "value" then
                                        key = "_value"
                                    end
                                    rawset(tbl, key, val)
                                    if key == "_value" then
                                        local v = rawget(tbl, "_view") -- update the config item text in view
                                        if v then
                                            v:text_set(tostring(tbl))
                                        end
                                    end
                                end}

-- the prompt info
function config:prompt()

    -- get text (first line in description)
    local text = self.description or ""
    if type(text) == "table" then
        text = text[1] or ""
    end
    return text
end

-- to string
function config:__tostring()

    -- get text (first line in description)
    local text = self:prompt()

    -- get value
    local value = self.value

    -- update text
    if self.kind == "boolean" or (not self.kind and type(value) == "boolean") then -- boolean config?
        text = (value and "[*] " or "[ ] ") .. text
    elseif self.kind == "number" or (not self.kind and type(value) == "number") then -- number config?
        text = "    " .. text .. " (" .. tostring(value or 0) .. ")"
    elseif self.kind == "string" or (not self.kind and type(value) == "string") then -- string config?
        text = "    " .. text .. " (" .. tostring(value or "") .. ")"
    elseif self.kind == "choice" then -- choice config?
        if self.values and #self.values > 0 then
            text = "    " .. text .. " (" .. tostring(self.values[value or 1]) .. ")" .. "  --->"
        else
            text = "    " .. text .. " ()  ----"
        end
    elseif self.kind == "menu" then -- menu config?
        text = "    " .. text .. (self.configs and #self.configs > 0 and "  --->" or "  ----")
    end

    -- new config?
    if self.new and self.kind ~= "choice" and self.kind ~= "menu" then
        text = text .. " (NEW)"
    end

    -- ok
    return text
end

-- save config objects
menuconf.config  = menuconf.config or config
menuconf.menu    = menuconf.menu or config { kind = "menu", configs = {} }
menuconf.number  = menuconf.number or config { kind = "number", default = 0 }
menuconf.string  = menuconf.string or config { kind = "string", default = "" }
menuconf.choice  = menuconf.choice or config { kind = "choice", default = 1, values = {} }
menuconf.boolean = menuconf.boolean or config { kind = "boolean", default = false }

-- return module
return menuconf
