--!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        info.lua
--

-- imports
import("core.base.task")
import("core.base.option")
import("core.base.json")
import("core.base.hashset")
import("core.project.project")
import("core.package.package", {alias = "core_package"})
import("devel.git")
import("utils.archive")
import("private.action.require.impl.utils.filter")
import("private.action.require.impl.utils.url_filename")
import("private.action.require.impl.package")
import("private.action.require.impl.repository")
import("private.action.require.impl.environment")
import("private.action.require.impl.utils.get_requires")

-- from xmake/system/remote?
function _from(instance)
    local fetchinfo = instance:fetch()
    if fetchinfo then
        if instance:is_thirdparty() then
            return ", ${green}3rd${clear}"
        elseif instance:is_system() then
            return ", ${green}system${clear}"
        else
            return ""
        end
    elseif #instance:urls() > 0 then
        local repo = instance:repo()
        local reponame = repo and repo:name() or "unknown"
        return instance:is_supported() and format(", ${yellow}remote${clear}(in %s)", reponame) or format(", ${yellow}remote${clear}(${red}unsupported${clear} in %s)", reponame)
    elseif instance:is_system() then
        return ", ${red}missing${clear}"
    else
        return ""
    end
end

-- get package info
function _info(instance)
    local info = instance:version_str() and instance:version_str() or "no version"
    info = info .. _from(instance)
    info = info .. (instance:is_optional() and ", ${yellow}optional${clear}" or "")
    return info
end

-- collect package info as table for json output
function _collect_package_info(instance)
    local info = {}
    local requireinfo = instance:requireinfo() or {}
    info.require = requireinfo.originstr
    info.description = instance:get("description")
    info.version = instance:version_str()
    info.license = instance:get("license")

    -- urls
    local urls = instance:urls()
    if urls and #urls > 0 then
        info.urls = {}
        json.mark_as_array(info.urls)
        local schemes = instance:schemes_orderlist()
        if schemes then
            for _, scheme in ipairs(schemes) do
                if not scheme:is_precompiled() then
                    local surls = scheme:urls()
                    if surls and #surls > 0 then
                        local scheme_name = scheme:is_default() and "default" or scheme:name()
                        local scheme_info = {name = scheme_name, urls = {}}
                        json.mark_as_array(scheme_info.urls)
                        for _, url in ipairs(surls) do
                            local url_entry = {url = filter.handle(url, instance)}
                            if git.asgiturl(url) then
                                local url_alias = scheme:url_alias(url)
                                url_entry.revision = scheme:revision(url_alias) or scheme:tag() or scheme:version_str()
                            else
                                url_entry.sourcehash = scheme:sourcehash(scheme:url_alias(url))
                            end
                            table.insert(scheme_info.urls, url_entry)
                        end
                        table.insert(info.urls, scheme_info)
                    end
                end
            end
        end
    end

    -- repository
    local repo = instance:repo()
    if repo then
        info.repo = {name = repo:name(), url = repo:url(), branch = repo:branch()}
    end

    -- deps
    local deps = instance:orderdeps()
    if deps and #deps > 0 then
        info.deps = {}
        json.mark_as_array(info.deps)
        for _, dep in ipairs(deps) do
            local dep_requireinfo = dep:requireinfo() or {}
            table.insert(info.deps, dep_requireinfo.originstr)
        end
    end

    info.cachedir = instance:cachedir()
    info.installdir = instance:installdir()
    info.searchdirs = table.wrap(core_package.searchdirs())
    json.mark_as_array(info.searchdirs)

    -- fetch info
    local fetchinfo = instance:fetch()
    if fetchinfo then
        info.fetchinfo = {}
        for name, finfo in pairs(fetchinfo) do
            info.fetchinfo[name] = table.wrap(table.unwrap(finfo))
        end
    end

    -- platforms
    local platforms = {}
    local on_install = instance:get("install")
    if type(on_install) == "table" then
        for plat, _ in pairs(on_install) do
            table.insert(platforms, plat)
        end
    else
        table.insert(platforms, "all")
    end
    json.mark_as_array(platforms)
    info.platforms = platforms

    -- requires
    info.requires = {plat = instance:plat(), arch = instance:arch()}
    local configs_required = instance:configs()
    if configs_required then
        info.requires.configs = configs_required
    end

    -- user configs
    local configs_defined = instance:get("configs")
    if configs_defined then
        local configs = {}
        local builtin_configs = {}
        for _, conf in ipairs(configs_defined) do
            local configs_extra = instance:extraconf("configs", conf)
            if configs_extra then
                local entry = {name = conf}
                if configs_extra.description then
                    entry.description = configs_extra.description
                end
                if configs_extra.default ~= nil then
                    entry.default = configs_extra.default
                end
                if configs_extra.type then
                    entry.type = configs_extra.type
                end
                if configs_extra.values then
                    entry.values = configs_extra.values
                end
                if configs_extra.readonly then
                    entry.readonly = true
                end
                if configs_extra.builtin then
                    table.insert(builtin_configs, entry)
                else
                    table.insert(configs, entry)
                end
            end
        end
        if #configs > 0 then
            json.mark_as_array(configs)
            info.configs = configs
        end
        if #builtin_configs > 0 then
            json.mark_as_array(builtin_configs)
            info.builtin_configs = builtin_configs
        end
    end

    -- components
    local components = instance:get("components")
    if components then
        info.components = {}
        json.mark_as_array(info.components)
        for _, comp in ipairs(components) do
            local comp_info = {name = comp}
            local plaindeps = instance:extraconf("components", comp, "deps")
            if plaindeps then
                comp_info.deps = table.wrap(plaindeps)
                json.mark_as_array(comp_info.deps)
            end
            table.insert(info.components, comp_info)
        end
    end

    -- references
    local references = instance:references()
    if references then
        info.references = {}
        json.mark_as_array(info.references)
        for projectdir, refdate in pairs(references) do
            table.insert(info.references, {dir = projectdir, date = refdate, exists = os.isdir(projectdir)})
        end
    end
    return info
end

-- print the given package info
function _print_package_info(instance)

    -- show package name
    local requireinfo = instance:requireinfo() or {}
    cprint("    ${color.dump.string_quote}require${clear}(%s): ", requireinfo.originstr)

    -- show description
    local description = instance:get("description")
    if description then
        cprint("      -> ${color.dump.string_quote}description${clear}: %s", description)
    end

    -- show version
    local version = instance:version_str()
    if version then
        cprint("      -> ${color.dump.string_quote}version${clear}: %s", version)
    end

    -- show license
    local license = instance:get("license")
    if license then
        cprint("      -> ${color.dump.string_quote}license${clear}: %s", license)
    end

    -- show urls
    local urls = instance:urls()
    if urls and #urls > 0 then
        cprint("      -> ${color.dump.string_quote}urls${clear}:")
        local schemes = instance:schemes_orderlist()
        if schemes then
            for _, scheme in ipairs(schemes) do
                if not scheme:is_precompiled() then
                    local urls = scheme:urls()
                    if urls and #urls > 0 then
                        local scheme_name = scheme:is_default() and "default" or scheme:name()
                        cprint("         -> ${magenta}%s${clear}:", scheme_name)
                        for _, url in ipairs(urls) do
                            print("            -> %s", filter.handle(url, instance))
                            if git.asgiturl(url) then
                                local url_alias = scheme:url_alias(url)
                                cprint("               -> ${yellow}%s", scheme:revision(url_alias) or scheme:tag() or scheme:version_str())
                            else
                                local sourcehash = scheme:sourcehash(scheme:url_alias(url))
                                if sourcehash then
                                    cprint("               -> ${yellow}%s", sourcehash)
                                end
                            end
                        end
                    end
                end
            end
        end
    end

    -- show repository
    local repo = instance:repo()
    if repo then
        cprint("      -> ${color.dump.string_quote}repo${clear}: %s %s %s", repo:name(), repo:url(), repo:branch() or "")
    end

    -- show deps
    local deps = instance:orderdeps()
    if deps and #deps > 0 then
        cprint("      -> ${color.dump.string_quote}deps${clear}:")
        for _, dep in ipairs(deps) do
            requireinfo = dep:requireinfo() or {}
            cprint("         -> %s", requireinfo.originstr)
        end
    end

    -- show cache directory
    cprint("      -> ${color.dump.string_quote}cachedir${clear}: %s", instance:cachedir())

    -- show install directory
    cprint("      -> ${color.dump.string_quote}installdir${clear}: %s", instance:installdir())

    -- show search directories and search names
    cprint("      -> ${color.dump.string_quote}searchdirs${clear}: %s", table.concat(table.wrap(core_package.searchdirs()), path.envsep()))
    local searchnames = hashset.new()
    for _, url in ipairs(urls) do
        local url = filter.handle(url, instance)
        if git.checkurl(url) then
            searchnames:insert(instance:name() .. archive.extension(url) .. " ${dim}(git)${clear}")
            searchnames:insert(path.basename(url_filename(url)) .. " ${dim}(git)${clear}")
        else
            local extension = archive.extension(url)
            if extension then
                searchnames:insert(instance:name() .. "-" .. instance:version_str() .. extension)
            end
            searchnames:insert(url_filename(url))

            -- match github name mangling https://github.com/xmake-io/xmake/issues/1343
            local github_name = url_filename.github_filename(url)
            if github_name then
                searchnames:insert(github_name)
            end
        end
    end
    cprint("      -> ${color.dump.string_quote}searchnames${clear}:")
    for _, searchname in searchnames:keys() do
        cprint("         -> %s", searchname)
    end

    -- show fetch info
    cprint("      -> ${color.dump.string_quote}fetchinfo${clear}: %s", _info(instance))
    local fetchinfo = instance:fetch()
    if fetchinfo then
        for name, info in pairs(fetchinfo) do
            local info = table.unwrap(info)
            if type(info) ~= "table" then
                info = tostring(info)
            end
            cprint("          -> ${color.dump.string_quote}%s${clear}: %s", name, table.concat(table.wrap(info), " "))
        end
    end

    -- show supported platforms
    local platforms = {}
    local on_install = instance:get("install")
    if type(on_install) == "table" then
        for plat, _ in pairs(on_install) do
            table.insert(platforms, plat)
        end
    else
        table.insert(platforms, "all")
    end
    cprint("      -> ${color.dump.string_quote}platforms${clear}: %s", table.concat(platforms, ", "))

    -- show requires
    cprint("      -> ${color.dump.string_quote}requires${clear}:")
    cprint("         -> ${cyan}plat${clear}: %s", instance:plat())
    cprint("         -> ${cyan}arch${clear}: %s", instance:arch())
    local configs_required = instance:configs()
    if configs_required then
        cprint("         -> ${cyan}configs${clear}:")
        for name, value in pairs(configs_required) do
            cprint("            -> %s: %s", name, value)
        end
    end

    -- show user configs
    local configs_defined = instance:get("configs")
    if configs_defined then
        cprint("      -> ${color.dump.string_quote}configs${clear}:")
        for _, conf in ipairs(configs_defined) do
            local configs_extra = instance:extraconf("configs", conf)
            if configs_extra and not configs_extra.builtin then
                cprintf("         -> ${cyan}%s${clear}: ", conf)
                if configs_extra.description then
                    printf(configs_extra.description)
                end
                if configs_extra.default ~= nil then
                    printf(" (default: %s)", configs_extra.default)
                elseif configs_extra.type ~= nil and configs_extra.type ~= "string" then
                    printf(" (type: %s)", configs_extra.type)
                end
                if configs_extra.readonly then
                    printf(" (readonly)")
                end
                print("")
                if configs_extra.values then
                    cprint("            -> values: %s", string.serialize(configs_extra.values, true))
                end
            end
        end
    end

    -- show builtin configs
    local configs_defined = instance:get("configs")
    if configs_defined then
        cprint("      -> ${color.dump.string_quote}configs (builtin)${clear}:")
        for _, conf in ipairs(configs_defined) do
            local configs_extra = instance:extraconf("configs", conf)
            if configs_extra and configs_extra.builtin then
                cprintf("         -> ${cyan}%s${clear}: ", conf)
                if configs_extra.description then
                    printf(configs_extra.description)
                end
                if configs_extra.default ~= nil then
                    printf(" (default: %s)", configs_extra.default)
                elseif configs_extra.type ~= nil and configs_extra.type ~= "string" then
                    printf(" (type: %s)", configs_extra.type)
                end
                print("")
                if configs_extra.values then
                    cprint("            -> values: %s", string.serialize(configs_extra.values, true))
                end
            end
        end
    end

    -- show components
    local components = instance:get("components")
    if components then
        cprint("      -> ${color.dump.string_quote}components${clear}: ")
        for _, comp in ipairs(components) do
            cprintf("         -> ${cyan}%s${clear}: ", comp)
            local plaindeps = instance:extraconf("components", comp, "deps")
            if plaindeps then
                print("%s", table.concat(table.wrap(plaindeps), ", "))
            else
                print("")
            end
        end
    end

    -- show references
    local references = instance:references()
    if references then
        cprint("      -> ${color.dump.string_quote}references${clear}:")
        for projectdir, refdate in pairs(references) do
            cprint("         -> %s: %s%s", refdate, projectdir, os.isdir(projectdir) and "" or " ${red}(not found)${clear}")
        end
    end

    -- end
    print("")
end

-- show the given package info
function main(requires_raw)

    -- get requires and extra config
    local requires_extra = nil
    local requires, requires_extra = get_requires(requires_raw)
    if not requires or #requires == 0 then
        return
    end

    -- enter environment
    environment.enter()

    -- pull all repositories first if not exists
    if not repository.pulled() then
        task.run("repo", {update = true})
    end

    -- list all packages
    local instances = package.load_packages(requires, {requires_extra = requires_extra})
    local format = option.get("format")
    if format == "json" then
        local results = {}
        json.mark_as_array(results)
        for _, instance in ipairs(instances) do
            table.insert(results, _collect_package_info(instance))
        end
        print(json.encode(results, {pretty = true, orderkeys = true}))
    else
        print("The package info of project:")
        for _, instance in ipairs(instances) do
            _print_package_info(instance)
        end
    end

    -- leave environment
    environment.leave()
end
