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

-- define module
local addon = addon or {}

-- load modules
local os     = require("base/os")
local io     = require("base/io")
local path   = require("base/path")
local table  = require("base/table")
local utils  = require("base/utils")
local global = require("base/global")

-- the payload directories of an addon
--
-- an addon can provide any subset of them, e.g. only `plugins`
--
-- @note only `plugins` is activated for now, the others are reserved
--
function addon._payloaddirs()
    return {"plugins", "rules", "toolchains", "platforms", "modules", "templates", "themes", "includes"}
end

-- the manifest file of an addon, e.g. <sourcedir>/addon.lua
--
-- an addon describes itself in this file, so its name and layout never depend on
-- the package name of the repository which distributes it
--
function addon._manifestfile(sourcedir)
    return path.join(sourcedir, "addon.lua")
end

-- the interpreter of the addon manifest
function addon._interpreter()
    local interp = addon._INTERPRETER
    if interp == nil then
        -- we need to load it lazily, the interpreter also depends on this module
        local interpreter = require("base/interpreter")
        interp = interpreter.new()
        interp:api_define(addon.apis())
        addon._INTERPRETER = interp
    end
    return interp
end

-- the registry file of the installed addons, e.g. ~/.xmake/addons/addons.conf
--
-- we save all installed addons to this file when installing/removing them,
-- so we do not need to scan the whole addons directory on startup
--
function addon._registryfile()
    return path.join(addon.installdir(), "addons.conf")
end

-- save the given registry to the registry file
function addon._save(registry)
    addon._REGISTRY = registry
    addon._ADDONS = nil
    local registryfile = addon._registryfile()
    -- we need not create an empty registry file if no addons are installed
    if table.empty(registry) and not os.isfile(registryfile) then
        return
    end
    local ok, errors = io.save(registryfile, registry)
    if not ok then
        utils.warning(errors)
    end
end

-- get the payload directory of the given addon
--
-- @param name  the addon name, e.g. "esp32"
-- @param kind  the payload kind, e.g. "rules", "modules"
-- @return      the directory, e.g. ~/.xmake/addons/esp32/v1.0.0/rules
--
function addon._payloaddir(name, kind)
    local dirname = addon.dirname(name)
    local addoninfo = addon.addons()[dirname]
    if addoninfo and table.contains(addoninfo.payloads or {}, kind) then
        return path.join(addon.installdir(), dirname, addoninfo.version, kind)
    end
end

-- get the plugin task names of the given addon directory
--
-- the plugins are not namespaced, we need them to check the conflicts
--
function addon._plugins_of(addondir)
    local plugins = {}
    for _, filepath in ipairs(os.files(path.join(addondir, "plugins", "*", "xmake.lua"))) do
        local content = io.readfile(filepath)
        if content then
            for taskname in content:gmatch("task%s*%(%s*\"(.-)\"") do
                table.insert(plugins, taskname)
            end
        end
    end
    return plugins
end

-- get the template ids of the given addon directory, e.g. {"c/console"}
--
-- the templates are not namespaced, we need them to check the conflicts
--
function addon._templates_of(addondir)
    local templates = {}
    local templatesdir = path.join(addondir, "templates")
    for _, langdir in ipairs(os.dirs(path.join(templatesdir, "*"))) do
        local lang = path.filename(langdir)
        local accepted = {}
        for _, filepath in ipairs(os.files(path.join(langdir, "**", "xmake.lua"))) do
            local dir = path.directory(filepath)
            local relpath = path.relative(dir, langdir)
            if relpath and relpath ~= "." then
                local nested = false
                for _, root in ipairs(accepted) do
                    if dir:startswith(root .. path.sep()) then
                        nested = true
                        break
                    end
                end
                if not nested then
                    table.insert(accepted, dir)
                    table.insert(templates, lang .. "/" .. (relpath:gsub("[/\\]", ".")))
                end
            end
        end
    end
    return templates
end

-- check the conflicts of the plugins and templates, they are not namespaced
--
-- @param dirname   the addon directory name
-- @param addoninfo the addon information, @see addon.register
--
-- @return          the errors if there are some conflicts
--
function addon._check_conflicts(dirname, addoninfo)
    local kindnames = {plugins = "plugin", templates = "template", globalmodules = "global module"}
    for _, kind in ipairs({"plugins", "templates", "globalmodules"}) do
        for _, name in ipairs(addoninfo[kind] or {}) do
            for otherdirname, otheraddoninfo in pairs(addon.addons()) do
                if otherdirname ~= dirname and table.contains(otheraddoninfo[kind] or {}, name) then
                    return string.format("%s(%s) conflicts, it has been provided by the addon(%s)!\nplease remove one of them, e.g. xmake addon --remove %s",
                        kindnames[kind], name, otherdirname, otherdirname)
                end
            end
        end
    end

    -- the global modules can also conflict with the builtin and the user modules
    --
    -- @note the `core.*` modules are in the core directory of the sandbox,
    -- they are not in `<programdir>/modules`,
    -- @see core/sandbox/modules/import/core/sandbox/module.lua
    local moduledirs = {path.join(os.programdir(), "modules"),
                        path.join(os.programdir(), "core", "sandbox", "modules", "import"),
                        path.join(global.directory(), "modules")}
    for _, name in ipairs(addoninfo.globalmodules or {}) do
        local modulepath = (name:gsub("%.", "/")) .. ".lua"
        for _, moduledir in ipairs(moduledirs) do
            if os.isfile(path.join(moduledir, modulepath)) then
                return string.format("global module(%s) conflicts, it has been provided by %s!\nplease rename it in the addon manifest.",
                    name, moduledir:startswith(os.programdir()) and "xmake" or moduledir)
            end
        end
    end
end

-- get the addons which depend on the given addon
function addon._parents(name)
    local dirname = addon.dirname(name)
    local parents
    for otherdirname, entry in table.orderpairs(addon._registry()) do
        if otherdirname ~= dirname then
            for _, addoninfo in pairs(entry.versions or {}) do
                if table.contains(addoninfo.deps or {}, dirname) then
                    parents = parents or {}
                    table.insert(parents, otherdirname)
                    break
                end
            end
        end
    end
    return parents
end

-- unregister the given addon or only one of its versions
function addon.unregister(name, version)
    local dirname = addon.dirname(name)
    local registry = addon._registry()
    local entry = registry[dirname]
    if entry == nil then
        return
    end
    if version then
        entry.versions[version] = nil
        if entry.active == version then
            -- we need to select the other one deterministically
            entry.active = addon.versions(name)[1]
        end
        if table.empty(entry.versions) then
            registry[dirname] = nil
        end
    else
        registry[dirname] = nil
    end
    addon._save(registry)
end

-- get the apis of the addon manifest
function addon.apis()
    return {
        values = {
            -- addon.set_xxx
            "addon.set_description"
        ,   "addon.set_homepage"
        ,   "addon.set_license"
        ,   "addon.set_sourcedir"
            -- addon.add_xxx
        ,   "addon.add_deps"
        ,   "addon.add_globalmodules"
        }
    }
end

-- get the manifest of the given addon directory
--
-- @param sourcedir the addon source or install directory, which contains `addon.lua`
--
-- @return          the manifest, e.g. {name = "esp32-devel", description = "...", sourcedir = "src", deps = {"serial-tools"}}
--                  it will be nil if this addon does not describe itself
--
function addon.manifest(sourcedir)

    -- we may resolve a lot of `@self` references, so we need to cache them
    local manifests = addon._MANIFESTS
    if manifests == nil then
        manifests = {}
        addon._MANIFESTS = manifests
    end
    local cachekey = path.absolute(sourcedir)
    local cacheinfo = manifests[cachekey]
    if cacheinfo ~= nil then
        return cacheinfo or nil
    end

    local manifestfile = addon._manifestfile(sourcedir)
    if not os.isfile(manifestfile) then
        manifests[cachekey] = false
        return
    end
    local interp = addon._interpreter()
    local ok, errors = interp:load(manifestfile)
    if not ok then
        return nil, errors
    end
    local results, errors = interp:make("addon", true, true)
    if not results then
        return nil, errors
    end
    local manifest
    for name, addoninfo in pairs(results) do
        if manifest then
            return nil, string.format("%s: only one addon() scope is allowed!", manifestfile)
        end
        manifest = {name = name,
                    description = addoninfo:get("description"),
                    homepage = addoninfo:get("homepage"),
                    license = addoninfo:get("license"),
                    sourcedir = addoninfo:get("sourcedir"),
                    deps = table.wrap(addoninfo:get("deps")),
                    globalmodules = table.wrap(addoninfo:get("globalmodules"))}
    end
    if not manifest then
        return nil, string.format("%s: no addon() scope found!", manifestfile)
    end
    manifests[cachekey] = manifest
    return manifest
end

-- get a working directory which has no project
--
-- we need it to run the sub-processes of the addons, e.g. `xrepo install --addon`,
-- otherwise they would load the project of the current directory again
--
-- @note we cannot use `os.tmpdir()` directly, it is shared by all the commands,
-- e.g. a stray `xmake.lua` in it would break the isolation
--
-- @note we can share it between the processes, we only use it as the working directory
-- and never write anything into it, @see private/action/addon/impl/xrepo.lua
--
function addon.workdir()
    local workdir = path.join(os.tmpdir(), "addons", "working")
    if not os.isdir(workdir) then
        -- it may be created by the other processes at the same time, we can ignore it
        os.mkdir(workdir)
    end
    return workdir
end

-- the install directory of addons, e.g. ~/.xmake/addons
function addon.installdir()
    return path.join(global.directory(), "addons")
end

-- get the directory name of the given addon name, e.g. "myns::foo" -> "myns_foo"
function addon.dirname(name)
    return (name:lower():gsub("::", "_"))
end

-- is the given reference an addon reference?
--
-- e.g. "@addon/esp32/flash", "@self/flash", "@addon.esp32.sdkconfig", "@self.sdkconfig"
--
function addon.is_reference(reference, sep)
    return reference:startswith("@addon" .. sep) or reference:startswith("@self" .. sep)
end

-- get the addon which owns the given script directory
--
-- it's used to resolve the `@self` references inside an addon,
-- so that the addon code never needs to know its own installed name
--
-- @param scriptdir the script directory, e.g. ~/.xmake/addons/esp32/v1.0.0/rules/flash
--                  it will be the directory of the caller script by default
-- @return          the addon name and its root directory, e.g. esp32, ~/.xmake/addons/esp32/v1.0.0
--
function addon.owner(scriptdir)
    if not scriptdir then
        -- we can get it from the sandbox of the caller script, e.g. the rule script of an addon
        local sandbox = require("sandbox/sandbox")
        local instance = sandbox.instance()
        scriptdir = instance and instance:rootdir()
    end
    if not scriptdir then
        return
    end
    scriptdir = path.absolute(scriptdir)

    -- the installed addons, e.g. ~/.xmake/addons/<name>/<version>/...
    local installdir = path.absolute(addon.installdir())
    if scriptdir:startswith(installdir .. path.sep()) then
        local parts = path.split(path.relative(scriptdir, installdir))
        if #parts >= 2 then
            local addondir = path.join(installdir, parts[1], parts[2])
            -- the registry keeps the raw addon name, the directory name is only
            -- its normalized form, e.g. "myns::foo" -> "myns_foo"
            local addoninfo = addon.addons()[parts[1]]
            return addoninfo and addoninfo.name or parts[1], addondir
        end
        return
    end

    -- the addon source directory, we can also run the addon code in place when developing it
    local dir = scriptdir
    while dir and #dir > 0 do
        -- the addon describes itself? we get its name from the manifest
        local manifest = addon.manifest(dir)
        if manifest then
            return manifest.name, dir
        end
        -- otherwise we can only guess it from the payload directories
        for _, payloaddir in ipairs(addon._payloaddirs()) do
            if os.isdir(path.join(dir, payloaddir)) then
                return path.filename(dir), dir
            end
        end
        local parentdir = path.directory(dir)
        if not parentdir or parentdir == dir then
            break
        end
        dir = parentdir
    end
end

-- resolve the given addon reference to its payload directory
--
-- the addon resources are referenced with the addon name from the outside,
-- and with `@self` from the addon code itself, e.g.
--
-- add_rules("@addon/esp32/flash"), import("@addon.esp32.sdkconfig")   -- from a project
-- add_rules("@self/flash"),        import("@self.sdkconfig")          -- from the addon itself
--
-- @param reference the reference, e.g. "@addon/esp32/flash", "@self.sdkconfig"
-- @param sep       the separator, e.g. "/", "."
-- @param kind      the payload kind, e.g. "rules", "modules"
-- @param opt       the options, e.g. {scriptdir = "..."}, it's used to resolve `@self`
--
-- @return          the reference information and errors,
--                  e.g. {dir = "~/.xmake/addons/esp32/v1.0.0/rules", name = "flash", addon = "esp32"}
--
function addon.resolve_reference(reference, sep, kind, opt)
    opt = opt or {}

    -- resolve the `@self` reference from the addon which owns the current script
    if reference:startswith("@self" .. sep) then
        local name = reference:sub(#("@self" .. sep) + 1)
        if name == "" then
            return nil, string.format("invalid addon reference(%s)!", reference)
        end
        local addonname, addondir = addon.owner(opt.scriptdir)
        if not addondir then
            return nil, string.format("%s: cannot resolve `@self`, it can only be used inside an addon!", reference)
        end
        return {dir = path.join(addondir, kind), name = name, addon = addonname}
    end

    -- resolve the `@addon` reference, the addon name is always required
    local prefix = "@addon" .. sep
    if not reference:startswith(prefix) then
        return
    end
    local pos = reference:find(sep, #prefix + 1, true)
    local addonname = pos and reference:sub(#prefix + 1, pos - 1)
    local name = pos and reference:sub(pos + 1)
    if not addonname or addonname == "" or not name or name == "" then
        return nil, string.format("invalid addon reference(%s), it should be `@addon%s<addon>%s<name>`", reference, sep, sep)
    end
    local payloaddir = addon._payloaddir(addonname, kind)
    if not payloaddir then
        return nil, string.format("%s not found!\nplease install the addon which provides it first: xmake addon --install %s", reference, addonname)
    end
    return {dir = payloaddir, name = name, addon = addonname}
end

-- get the registry of the installed addons
--
-- an addon can be installed with several versions at the same time, e.g. the projects
-- may lock the different versions of it, so we save all of them
--
-- @return  the registry, e.g. {["esp32"] = {active = "1.0.3", versions = {["1.0.3"] = {...}}}}
--
function addon._registry(opt)
    local registry = addon._REGISTRY
    if opt and opt.force then
        registry = nil
    end
    if registry == nil then
        registry = {}
        local registryfile = addon._registryfile()
        if os.isfile(registryfile) then
            registry = io.load(registryfile) or {}
        end
        -- migrate the old registry, it only saved one version for each addon
        for dirname, addoninfo in pairs(registry) do
            if addoninfo.versions == nil then
                registry[dirname] = {active = addoninfo.version,
                                     versions = {[addoninfo.version] = addoninfo}}
            end
        end
        addon._REGISTRY = registry
        addon._ADDONS = nil
    end
    return registry
end

-- pin the active version of the given addon for this process
--
-- @note a project locks the versions of its addons, so we need to activate them
-- when we load it, @see core/project/addons.lua
--
function addon.pin(name, version)
    local pinned = addon._PINNED
    if pinned == nil then
        pinned = {}
        addon._PINNED = pinned
    end
    pinned[addon.dirname(name)] = version
    addon._ADDONS = nil
end

-- get all the installed versions of the given addon, e.g. {"1.0.2", "1.0.3"}
function addon.versions(name)
    local addoninfo = addon._registry()[addon.dirname(name)]
    return table.orderkeys(addoninfo and addoninfo.versions or {})
end

-- get all installed addons, only the active version of each addon
--
-- @param opt   the options, e.g. {force = true}, we need it to reload the registry
--              if the addons have been installed by another process
--
-- @return      the addons table, e.g. {["hello-world"] = {version = "latest", payloads = {"plugins"}}}
--
function addon.addons(opt)
    opt = opt or {}
    if opt.force then
        addon._registry({force = true})
    end

    -- get the really installed versions instead of the pinned ones? e.g. locking them
    -- @note we do not cache it, the cache is the pinned view of this project
    if opt.unpinned then
        local addons = {}
        for dirname, addoninfo in pairs(addon._registry()) do
            local versioninfo = addoninfo.versions and addoninfo.versions[addoninfo.active]
            if versioninfo then
                addons[dirname] = versioninfo
            end
        end
        return addons
    end

    local addons = addon._ADDONS
    if addons == nil then
        addons = {}
        local pinned = addon._PINNED or {}
        for dirname, addoninfo in pairs(addon._registry()) do
            -- the project may lock another version of it, @see addon.pin
            local version = pinned[dirname] or addoninfo.active
            local versioninfo = addoninfo.versions and addoninfo.versions[version]
            if versioninfo then
                addons[dirname] = versioninfo
            end
        end
        addon._ADDONS = addons
    end
    return addons
end

-- get the install directory of the given addon, e.g. ~/.xmake/addons/<name>/<version>
function addon.addondir(name, version)
    local dirname = addon.dirname(name)
    if version == nil then
        local addoninfo = addon.addons()[dirname]
        if addoninfo == nil then
            return nil
        end
        version = addoninfo.version
    end
    return path.join(addon.installdir(), dirname, version)
end

-- get the modules which the installed addons export as the global modules
--
-- they are declared in the addon manifest, e.g. add_globalmodules("core.tools.esptool"),
-- so that they can be imported with their plain names by the internal calls,
-- e.g. import("core.tools.esptool"), find_tool("esptool")
--
-- @return      the modules table, e.g. {["core.tools.esptool"] = "~/.xmake/addons/esp32/v1.0.0/modules"}
--
function addon.globalmodules()
    local globalmodules = addon._GLOBALMODULES
    if globalmodules == nil then
        globalmodules = {}
        for dirname, addoninfo in pairs(addon.addons()) do
            for _, name in ipairs(addoninfo.globalmodules or {}) do
                globalmodules[name] = path.join(addon.installdir(), dirname, addoninfo.version, "modules")
            end
        end
        addon._GLOBALMODULES = globalmodules
    end
    return globalmodules
end

-- find the include files of the given addon reference, e.g. includes("@addon/esp32/board")
--
-- @param interp    the interpreter which is loading the file, @see interpreter:includes_resolver_add
-- @param reference the reference, e.g. "@addon/esp32/board", "@self/board"
--
-- @return          the files, or nil and errors
--
function addon.find_includes(interp, reference)
    if not addon.is_reference(reference, "/") then
        return
    end
    local referenceinfo, errors = addon.resolve_reference(reference, "/", "includes", {scriptdir = interp:scriptdir()})
    if not referenceinfo then
        return nil, errors
    end
    local name = referenceinfo.name
    local files
    if name:endswith(".lua") then
        files = os.files(path.join(referenceinfo.dir, name))
    else
        files = os.files(path.join(referenceinfo.dir, name, "xmake.lua"))
    end

    -- the addon is installed, but it does not provide this file, we cannot ignore it
    if not files or #files == 0 then
        os.raise("includes(%s) not found!", reference)
    end
    return files
end

-- get the payload directories of the given kind from all installed addons
--
-- @param kind  the payload kind, e.g. "plugins", "rules"
-- @return      the directories, e.g. {"~/.xmake/addons/hello-world/latest/plugins"}
--
-- @note we do not check if these directories exist, the callers will just ignore the invalid ones
--
function addon.payloads(kind)
    local payloads = {}
    for _, payloadinfo in ipairs(addon.payloadinfos(kind)) do
        table.insert(payloads, payloadinfo.dir)
    end
    return payloads
end

-- get the payload information of the given kind from all installed addons
--
-- @param kind  the payload kind, e.g. "plugins", "rules"
-- @return      the payload infos, e.g. {{name = "hello-world", version = "latest", dir = "~/.xmake/addons/hello-world/latest/plugins"}}
--
function addon.payloadinfos(kind)
    local payloadinfos = {}
    for name, addoninfo in table.orderpairs(addon.addons()) do
        if table.contains(addoninfo.payloads or {}, kind) then
            table.insert(payloadinfos, {
                name = name,
                version = addoninfo.version,
                dir = path.join(addon.installdir(), name, addoninfo.version, kind)})
        end
    end
    return payloadinfos
end

-- get the payload root directory of the given addon source directory
--
-- an addon repository has its own files, e.g. tests, ci scripts and documents,
-- so its payloads can be placed in the `src` subdirectory, and we only install them
--
-- e.g.
--   esp32-devel/src/{plugins,rules,toolchains,templates}   -- with the `src` layout
--   hello-world/{plugins}                                  -- without it, for the simple addons
--
-- @param sourcedir   the addon source directory
-- @return            the payload root directory, it will be nil if no payload is found
--
function addon.payloadroot(sourcedir)

    -- the addon can set its payload root directory explicitly, e.g. set_sourcedir("src")
    local manifest = addon.manifest(sourcedir)
    if manifest and manifest.sourcedir then
        local payloadroot = path.join(sourcedir, manifest.sourcedir)
        if #addon.payloads_of(payloadroot) > 0 then
            return payloadroot
        end
        return
    end

    local srcdir = path.join(sourcedir, "src")
    if #addon.payloads_of(srcdir) > 0 then
        return srcdir
    end
    if #addon.payloads_of(sourcedir) > 0 then
        return sourcedir
    end
end

-- get the payload directories of the given addon directory, e.g. {"plugins", "rules"}
function addon.payloads_of(addondir)
    local payloads = {}
    for _, payloaddir in ipairs(addon._payloaddirs()) do
        if os.isdir(path.join(addondir, payloaddir)) then
            table.insert(payloads, payloaddir)
        end
    end
    return payloads
end

-- get the default on_install script of addon packages
--
-- we only install the payload directories of this addon, e.g. plugins, rules, toolchains, ...
--
function addon.installscript()
    return function (package)
        local sourcedir = os.curdir()

        -- the addon name is its identity, e.g. the install directory, the registry key
        -- and the `@addon/<name>/xxx` references, so the package must be distributed with the same name
        local manifest, errors = addon.manifest(sourcedir)
        if errors then
            os.raise(errors)
        end
        if manifest and addon.dirname(manifest.name) ~= addon.dirname(package:name()) then
            os.raise("addon(%s) does not match the package name(%s) in the repository!\nplease fix the package recipe or the addon manifest.",
                manifest.name, package:name())
        end

        local payloadroot = addon.payloadroot(sourcedir)
        if not payloadroot then
            os.raise("addon(%s): no payload directory found, e.g. plugins!", package:name())
        end
        for _, payloaddir in ipairs(addon.payloads_of(payloadroot)) do
            os.cp(path.join(payloadroot, payloaddir), package:installdir())
        end

        -- we need not install the manifest, the package manifest(manifest.txt) and the addons
        -- registry already have all the information, we just pass it to the registration
        if manifest then
            package:data_set("addon.manifest", manifest)
        end
    end
end

-- register the given installed addon
--
-- @param name      the addon name
-- @param version   the addon version, e.g. "1.0.1", "latest"
-- @param opt       the options, e.g. {description = "...", deps = {"foo"}}
--
-- @note the addon manifest(addon.lua) is only read when installing, everything which
-- is needed later is recorded here, so we never parse it again
--
-- @return          true or false and errors
--
function addon.register(name, version, opt)
    opt = opt or {}
    local dirname = addon.dirname(name)
    local addondir = path.join(addon.installdir(), dirname, version)
    local addoninfo = {version = version,
                       -- we need to keep the raw name, the directory name is only its
                       -- normalized form, e.g. "myns::foo" -> "myns_foo"
                       name = name ~= dirname and name or nil,
                       description = opt.description,
                       deps = opt.deps,
                       -- where it comes from, e.g. {url = ..., commit = ..., branch = ...}
                       repo = opt.repo,
                       -- the deps which the addon itself declares in its manifest, they are
                       -- recorded whenever this addon has one, so that the repositories can
                       -- check that the manifest and the package recipe are kept in sync
                       manifest_deps = opt.manifest_deps,
                       globalmodules = opt.globalmodules,
                       payloads = addon.payloads_of(addondir),
                       plugins = addon._plugins_of(addondir),
                       templates = addon._templates_of(addondir)}

    -- we need to check the conflicts of the plugins and templates first,
    -- they are not namespaced and we do not know which one will be used
    local errors = addon._check_conflicts(dirname, addoninfo)
    if errors then
        return false, errors
    end

    -- we can install several versions of an addon at the same time,
    -- and the version which we install now is always the active one
    local registry = addon._registry()
    local entry = registry[dirname]
    if entry == nil or entry.versions == nil then
        entry = {versions = {}}
        registry[dirname] = entry
    end
    entry.versions[version] = addoninfo
    entry.active = version
    addon._save(registry)
    return true
end

-- remove the given installed addon
--
-- @param name  the addon name
-- @return      true or false and errors
--
function addon.remove(name, opt)
    opt = opt or {}
    local dirname = addon.dirname(name)
    local installdir = path.join(addon.installdir(), dirname)
    if not os.isdir(installdir) then
        return false, string.format("addon(%s) not found!", name)
    end

    -- we cannot remove it if the other addons depend on it
    if not opt.force then
        local parents = addon._parents(name)
        if parents then
            return false, string.format("addon(%s) cannot be removed, it's depended on by the addon(%s)!\nplease remove them first, or pass --force to remove it anyway",
                name, table.concat(parents, ", "))
        end
    end
    -- we need to remove the symlinks first, we cannot remove them recursively,
    -- otherwise the linked files would be removed too
    --
    -- e.g. the user may link the install directory to the addon source directory when developing it
    for _, versiondir in ipairs(os.dirs(path.join(installdir, "*"))) do
        if os.islink(versiondir) then
            os.rmfile(versiondir)
        end
    end
    if os.islink(installdir) then
        return os.rmfile(installdir)
    end

    local ok, errors = os.rm(installdir)
    if not ok then
        return false, errors
    end
    addon.unregister(name)
    return true
end

-- reload the addons registry and the caches which are built from it
--
-- @note we need it if the addons have been installed by another process,
-- e.g. the addons which a project declares, @see core/project/project.lua
--
function addon.reload()
    addon._REGISTRY = nil
    addon._ADDONS = nil
    addon._MANIFESTS = nil
    addon._GLOBALMODULES = nil
end

-- return module
return addon
