--!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      ruki
-- @file        rule.lua
--

-- define module
local rule = rule or {}

-- load modules
local os             = require("base/os")
local path           = require("base/path")
local utils          = require("base/utils")
local table          = require("base/table")
local global         = require("base/global")
local interpreter    = require("base/interpreter")
local config         = require("project/config")
local sandbox        = require("sandbox/sandbox")
local sandbox_os     = require("sandbox/modules/os")
local sandbox_module = require("sandbox/modules/import/core/sandbox/module")

-- the directories of rule
function rule._directories()

    -- the directories
    return  {   path.join(global.directory(), "rules")
            ,   path.join(os.programdir(), "rules")
            }
end

-- the interpreter
function rule._interpreter()

    -- the interpreter has been initialized? return it directly
    if rule._INTERPRETER then
        return rule._INTERPRETER
    end

    -- init interpreter
    local interp = interpreter.new()
    assert(interp)

    -- define apis
    interp:api_define(rule.apis())

    -- set filter
    interp:filter():register("rule", function (variable)

        -- check
        assert(variable)

        -- attempt to get it directly from the configure
        local result = config.get(variable)
        if not result or type(result) ~= "string" then

            -- init maps
            local maps =
            {
                host        = os.host()
            ,   tmpdir      = function () return os.tmpdir() end
            ,   curdir      = function () return os.curdir() end
            ,   scriptdir   = function () return sandbox_os.scriptdir() end
            ,   globaldir   = global.directory()
            ,   configdir   = config.directory()
            ,   projectdir  = os.projectdir()
            ,   programdir  = os.programdir()
            }

            -- map it
            result = maps[variable]
            if type(result) == "function" then
                result = result()
            end
        end

        -- ok?
        return result
    end)

    -- save interpreter
    rule._INTERPRETER = interp

    -- ok?
    return interp
end

-- load rule
function rule._load(filepath)

    -- get interpreter
    local interp = rule._interpreter()
    assert(interp)

    -- load script
    local ok, errors = interp:load(filepath)
    if not ok then
        return nil, errors
    end

    -- load rules
    local results, errors = interp:make("rule", true, true)
    if not results then
        return nil, errors
    end

    -- ok
    return results
end

-- load deps
--
-- e.g.
--
-- a.deps = b
-- b.deps = c
--
-- orderdeps: c -> b -> a
--
function rule._load_deps(self, rules, deps, orderdeps)

    -- get dep rules
    for _, dep in ipairs(table.wrap(self:get("deps"))) do
        local deprule = rules[dep]
        if deprule then
            rule._load_deps(deprule, rules, deps, orderdeps)
            if not deps[dep] then
                deps[dep] = deprule
                table.insert(orderdeps, deprule)
            end
        end
    end
end

-- get rule apis
function rule.apis()

    return
    {
        values =
        {
            -- rule.set_xxx
            "rule.set_extensions"
        ,   "rule.set_sourcekinds"
        ,   "rule.set_kind"
            -- rule.add_xxx
        ,   "rule.add_deps"
        ,   "rule.add_imports"
        }
    ,   script =
        {
            -- rule.on_xxx
            "rule.on_run"
        ,   "rule.on_load"
        ,   "rule.on_config"
        ,   "rule.on_link"
        ,   "rule.on_build"
        ,   "rule.on_build_file"
        ,   "rule.on_build_files"
        ,   "rule.on_clean"
        ,   "rule.on_package"
        ,   "rule.on_install"
        ,   "rule.on_uninstall"
        ,   "rule.on_linkcmd"
        ,   "rule.on_buildcmd"
        ,   "rule.on_buildcmd_file"
        ,   "rule.on_buildcmd_files"
            -- rule.before_xxx
        ,   "rule.before_run"
        ,   "rule.before_load"
        ,   "rule.before_link"
        ,   "rule.before_build"
        ,   "rule.before_build_file"
        ,   "rule.before_build_files"
        ,   "rule.before_clean"
        ,   "rule.before_package"
        ,   "rule.before_install"
        ,   "rule.before_uninstall"
        ,   "rule.before_linkcmd"
        ,   "rule.before_buildcmd"
        ,   "rule.before_buildcmd_file"
        ,   "rule.before_buildcmd_files"
            -- rule.after_xxx
        ,   "rule.after_run"
        ,   "rule.after_load"
        ,   "rule.after_link"
        ,   "rule.after_build"
        ,   "rule.after_build_file"
        ,   "rule.after_build_files"
        ,   "rule.after_clean"
        ,   "rule.after_package"
        ,   "rule.after_install"
        ,   "rule.after_uninstall"
        ,   "rule.after_linkcmd"
        ,   "rule.after_buildcmd"
        ,   "rule.after_buildcmd_file"
        ,   "rule.after_buildcmd_files"
        }
    }
end

-- new a rule instance
function rule.new(name, info)
    local instance = table.inherit(rule)
    instance._NAME = name
    instance._INFO = info
    return instance
end

-- get the rule info
function rule:get(name)
    return self._INFO:get(name)
end

-- get the extra configuration
function rule:extraconf(name, item, key)
    return self._INFO:extraconf(name, item, key)
end

-- get the rule name
function rule:name()
    return self._NAME
end

-- get the rule kind
--
-- current supported kind:
--  - target: default, only for each target
--  - project: global rule, for whole project
--
function rule:kind()
    return self:get("kind") or "target"
end

-- get the given dependent rule
function rule:dep(name)
    local deps = self:deps()
    if deps then
        return deps[name]
    end
end

-- get rule deps
function rule:deps()
    return self._DEPS
end

-- get rule order deps
function rule:orderdeps()
    return self._ORDERDEPS
end

-- get xxx_script
function rule:script(name, generic)

    -- get script
    local script = self:get(name)
    local result = nil
    if type(script) == "function" then
        result = script
    elseif type(script) == "table" then

        -- get plat and arch
        local plat = config.get("plat") or ""
        local arch = config.get("arch") or ""

        -- match pattern
        --
        -- `@linux`
        -- `@linux|x86_64`
        -- `@macosx,linux`
        -- `android@macosx,linux`
        -- `android|armeabi-v7a@macosx,linux`
        -- `android|armeabi-v7a@macosx,linux|x86_64`
        -- `android|armeabi-v7a@linux|x86_64`
        --
        for _pattern, _script in pairs(script) do
            local hosts = {}
            local hosts_spec = false
            _pattern = _pattern:gsub("@(.+)", function (v)
                for _, host in ipairs(v:split(',')) do
                    hosts[host] = true
                    hosts_spec = true
                end
                return ""
            end)
            if not _pattern:startswith("__") and (not hosts_spec or hosts[os.subhost() .. '|' .. os.subarch()] or hosts[os.subhost()])
            and (_pattern:trim() == "" or (plat .. '|' .. arch):find('^' .. _pattern .. '$') or plat:find('^' .. _pattern .. '$')) then
                result = _script
                break
            end
        end

        -- get generic script
        result = result or script["__generic__"] or generic
    end

    -- only generic script
    result = result or generic

    -- imports some modules first
    if result and result ~= generic then
        local scope = getfenv(result)
        if scope then
            for _, modulename in ipairs(table.wrap(self:get("imports"))) do
                scope[sandbox_module.name(modulename)] = sandbox_module.import(modulename, {anonymous = true})
            end
        end
    end

    -- ok
    return result
end

-- get the given global rule
function rule.rule(name)
    return rule.rules()[name]
end

-- get global rules
function rule.rules()

    -- return it directly if exists
    if rule._RULES then
        return rule._RULES
    end

    -- load rules
    local rules = {}
    local dirs = rule._directories()
    for _, dir in ipairs(dirs) do

        -- get files
        local files = os.match(path.join(dir, "**/xmake.lua"))
        if files then
            for _, filepath in ipairs(files) do

                -- load rule
                local results, errors = rule._load(filepath)

                -- save rule
                if results then
                    table.join2(rules, results)
                else
                    os.raise(errors)
                end
            end
        end
    end

    -- make rule instances
    local instances = {}
    for rulename, ruleinfo in pairs(rules) do
        local instance = rule.new(rulename, ruleinfo)
        instances[rulename] = instance
    end

    -- load rule deps
    for _, instance in pairs(instances)  do
        instance._DEPS      = instance._DEPS or {}
        instance._ORDERDEPS = instance._ORDERDEPS or {}
        rule._load_deps(instance, instances, instance._DEPS, instance._ORDERDEPS)
    end

    -- save it
    rule._RULES = instances

    -- ok?
    return rule._RULES
end

-- return module
return rule
