--!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, TBOOX Open Source Group.
--
-- @author      OpportunityLiu
-- @file        complete.lua
--

-- imports
import("core.base.option")
import("core.base.task")
import("core.base.cli")
import("core.base.json")

local raw_words = {}
local raw_config
local position = 0
local no_json = false
local use_spaces = true
local no_key = false
local reenter = false

function _print_candidate(candidate)
    if candidate.value and #candidate.value ~= 0 then
        printf(candidate.value)
        if use_spaces and candidate.is_complete then
            print(" ")
        else
            print("")
        end
    end
end

function _print_candidates(candidates)
    if to_json then 
        print(json.encode(candidates))
    else
        for _, v in ipairs(candidates) do
            _print_candidate(v)
        end
    end 
end

function _find_candidates(candidates, find)

    if type(candidates) ~= 'table' then
        return {}
    end

    local has_candidate = false
    local results = table.new(#candidates, 0)

    -- find candidate starts with find str
    for _, v in ipairs(candidates) do
        if tostring(v):startswith(find) then
            table.insert(results, v)
            has_candidate = true
        end
    end

    -- stop searching if found any
    if has_candidate then
        return results
    end

    -- find candidate contains find str
    for _, v in ipairs(candidates) do
        if tostring(v):find(find, 1, true) then
            table.insert(results, v)
        end
    end

    return results
end

function _complete_task(tasks, name)
    local found_candidates = {}
    for i, v in ipairs(_find_candidates((table.keys(tasks)), name)) do
        table.insert(found_candidates, { value = v, is_complete = true, description = tasks[v].description })
    end
    _print_candidates(found_candidates)
    return #found_candidates > 0 
end

-- complete values of kv
function _complete_option_kv_v(options, current, completing, name, value)

    -- find completion option
    local opt
    for _, v in ipairs(options) do
        if v[3] == "kv" and (v[1] == name or v[2] == name) then
            opt = v
            break
        end
    end
    if not opt then
        return false
    end

    -- show candidates of values
    local values = opt.values
    if type(values) == "function" then
        values = values(value, current)
    end
    if values == nil and type(opt[4]) == "boolean" then
        values = { "yes", "no" }
        -- ignore existing input
        value = ""
    end

    -- match values starts with value first
    local found_candidates = {}
    for _, v in ipairs(_find_candidates(values, value)) do
        if no_key then
            table.insert(found_candidates, { value = v, is_complete = true })
        else
            table.insert(found_candidates, { value = format("--%s=%s", name, v), is_complete = true })
        end
    end
    _print_candidates(found_candidates)

    -- whether any candidates has been found, finish complete since we don't have more info
    return true
end

-- complete keys of kv
function _complete_option_kv_k(options, current, completing, name)

    local opcandi = table.new(0, 10)
    for _, opt in ipairs(options) do
        if opt[2] and current[opt[2]] == nil and (opt[3] == "kv" or opt[3] == "k") then
            opcandi[opt[2]] = opt
        end
    end
    local found_candidates = {}
    for _, k in ipairs(_find_candidates((table.keys(opcandi)), name)) do
        local opt = opcandi[k]
        local name = format((opt[3] == "k") and "--%s" or "--%s=", opt[2])
        table.insert(found_candidates, { value = name, description = opt[5], is_complete = (opt[3] == "k") })
    end
    _print_candidates(found_candidates)

    return true
end

function _complete_option_kv(options, current, completing)

    local name, value
    if completing == "-" or completing == "--" then
        name = ""
    elseif completing:startswith("--") then
        local parg = cli.parsev({completing})[1]
        if parg.type == "option" then
            name, value = parg.key, parg.value
        elseif parg.type == "flag" then
            name = parg.key
        end
    elseif completing:startswith("-") then
        -- search full names only
        return true
    end

    if value then
        -- complete values
        return _complete_option_kv_v(options, current, completing, name, value)
    else
        -- complete keys
        return _complete_option_kv_k(options, current, completing, name)
    end
end

-- complete options v and vs
function _complete_option_v(options, current, completing)
    -- find completion option
    local opt
    local optvs
    for _, v in ipairs(options) do
        if v[3] == "v" and current[v[2] or v[1]] == nil then
            opt = v
        end
        if v[3] == "vs" and not optvs then
            optvs = v
        end
    end
    local found_candidates = {}
    if opt then
        -- show candidates of values
        local values = opt.values
        if type(values) == "function" then
            values = values(completing, current)
        end
        for _, v in ipairs(values) do
            if tostring(v):startswith(completing) then
                table.insert(found_candidates, { value = v, is_complete = true })
            end
        end
    end

    if optvs and #found_candidates == 0 then
        -- show candidates of values
        local values = optvs.values
        if type(values) == "function" then
            values = values(completing)
        end
        for _, v in ipairs(values) do
            if tostring(v):startswith(completing) then
                table.insert(found_candidates, { value = v, is_complete = true })
            end
        end
    end
    _print_candidates(found_candidates)
    return #found_candidates > 0
end

function _complete_option(options, segs, completing)
    local current_options = try
    {
        function()
            return option.raw_parse(segs, options, { populate_defaults = false, allow_unknown = true })
        end
    }
    -- current options is invalid
    if not current_options then return false end

    -- current context is wrong
    if not reenter and (current_options.file or current_options.project) then
        local args = {"lua", "private.utils.complete", tostring(position), raw_config .. "-reenter", table.unpack(raw_words) }
        if current_options.file then
            table.insert(args, 2, "--file=" .. current_options.file)
        end
        if current_options.project then
            table.insert(args, 2, "--project=" .. current_options.project)
        end
        os.execv("xmake", args)
        return true
    end

    if completing:startswith("-") then
        return _complete_option_kv(options, current_options, completing)
    else
        return _complete_option_v(options, current_options, completing)
    end
end

function _complete(argv, completing)

    local tasks = table.new(10, 0)
    local shortnames = table.new(0, 10)
    for _, v in ipairs(task.names()) do
        local menu = option.taskmenu(v)
        tasks[v] = menu
        if menu.shortname then
            shortnames[menu.shortname] = v
        end
    end

    if #argv == 0 then
        if _complete_task(tasks, completing) then return end
    end

    local task_name = "build"
    if argv[1] and not argv[1]:startswith("-") then
        task_name = table.remove(argv, 1)
    end

    if shortnames[task_name] then task_name = shortnames[task_name] end

    local options
    if tasks[task_name] then
        options = tasks[task_name].options
    else
        options = tasks["build"].options
    end

    _complete_option(options, argv, completing)
end

function main(pos, config, ...)

    raw_words = {...}

    raw_config = (config or ""):trim()
    if raw_config:find("nospace", 1, true) then
        use_spaces = false
    end
    if raw_config:find("reenter", 1, true) then
        reenter = true
    end
    if raw_config:find("nokey", 1, true) then
        no_key = true
    end
    if raw_config:find("debug", 1, true) then
        debug = true
    end
    if raw_config:find("json", 1, true) then
        to_json = true
    end

    local word = table.concat(raw_words, " ") or ""
    position = tonumber(pos) or 0

    local has_space = word:endswith(" ") or position > #word
    word = word:trim()

    local argv = os.argv(word)

    if argv[1] then

        -- normailize word to remove "xmake"
        if is_host("windows") and argv[1]:lower() == "xmake.exe" then
            argv[1] = "xmake"
        end
        if argv[1] == "xmake" then
            table.remove(argv, 1)
        end
    end

    if has_space then
        _complete(argv, "")
    else
        local completing = table.remove(argv)
        _complete(argv, completing or "")
    end
end

