--!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-2020, TBOOX Open Source Group.
--
-- @author      ruki
-- @file        platform.lua
--

-- define module
local platform      = platform or {}
local _instance     = _instance or {}

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

-- new an instance
function _instance.new(name, info, rootdir)
    local instance    = table.inherit(_instance)
    instance._NAME    = name
    instance._INFO    = info
    instance._ROOTDIR = rootdir
    return instance
end

-- get platform name
function _instance:name()
    return self._NAME
end

-- set the value to the platform configuration
function _instance:set(name, ...)
    self._INFO:apival_set(name, ...)
end

-- add the value to the platform configuration
function _instance:add(name, ...)
    self._INFO:apival_add(name, ...)
end

-- get the platform configuration
function _instance:get(name)

    -- attempt to get the static configure value
    local value = self._INFO:get(name)
    if value ~= nil then
        return value
    end

    -- lazy loading platform if get other configuration
    if not self._LOADED and not self:_is_builtin_conf(name) then
        local on_load = self._INFO:get("load")
        if on_load then
            local ok, errors = sandbox.load(on_load, self)
            if not ok then
                os.raise(errors)
            end
        end
        self._LOADED = true
    end

    -- get other platform info
    return self._INFO:get(name)
end

-- get the platform os
function _instance:os()
    return self._INFO:get("os")
end

-- get the platform menu
function _instance:menu()
    -- @note do not use self:get("menu") to avoid loading platform early
    return self._INFO:get("menu")
end

-- get the platform hosts
function _instance:hosts()
    return self._INFO:get("hosts")
end

-- get the platform archs
function _instance:archs()
    return self._INFO:get("archs")
end

-- get the platform script
function _instance:script(name)
    return self._INFO:get(name)
end

-- get user private data
function _instance:data(name)
    return self._DATA and self._DATA[name] or nil
end

-- set user private data
function _instance:data_set(name, data)
    self._DATA = self._DATA or {}
    self._DATA[name] = data
end

-- add user private data
function _instance:data_add(name, data)
    self._DATA = self._DATA or {}
    self._DATA[name] = table.unwrap(table.join(self._DATA[name] or {}, data))
end

-- is builtin configuration?
function _instance:_is_builtin_conf(name)

    local builtin_configs = self._BUILTIN_CONFIGS
    if not builtin_configs then
        builtin_configs = {}
        for apiname, _ in pairs(platform._interpreter():apis("platform")) do
            local pos = apiname:find('_', 1, true)
            if pos then
                builtin_configs[apiname:sub(pos + 1)] = true
            end
        end
        self._BUILTIN_CONFIGS = builtin_configs
    end
    return builtin_configs[name]
end

-- the interpreter
function platform._interpreter()

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

    -- init interpreter
    local interp = interpreter.new()
    assert(interp)
 
    -- define apis
    interp:api_define(platform._apis())

    -- save interpreter
    platform._INTERPRETER = interp

    -- ok?
    return interp
end

-- get platform apis
function platform._apis()
    return 
    {
        values =
        {
            -- platform.set_xxx
            "platform.set_os"
        ,   "platform.set_hosts"
        ,   "platform.set_archs"
        ,   "platform.set_installdir"
        }
    ,   script =
        {
            -- platform.on_xxx
            "platform.on_load"
        ,   "platform.on_config_check"
        ,   "platform.on_global_check"
        ,   "platform.on_environment_enter"
        ,   "platform.on_environment_leave"
        }
    ,   dictionary =
        {
            -- platform.set_xxx
            "platform.set_menu"
        ,   "platform.set_formats"
        }
    }
end

-- get platform directories
function platform.directories()

    -- init directories
    local dirs = platform._DIRS or  {   path.join(global.directory(), "platforms")
                                    ,   path.join(os.programdir(), "platforms")
                                    }
                                
    -- save directories to cache
    platform._DIRS = dirs
    return dirs
end

-- add platform directories
function platform.add_directories(...)

    -- add directories
    local dirs = platform.directories()
    for _, dir in ipairs({...}) do
        table.insert(dirs, 1, dir)
    end

    -- remove unique directories
    platform._DIRS = table.unique(dirs)
end

-- load the given platform 
function platform.load(plat)

    -- get platform name
    plat = plat or config.get("plat") or os.host()
    if not plat then
        return nil, string.format("unknown platform!")
    end

    -- get cache key
    local cachekey = plat .. "_" .. (config.get("arch") or os.arch())

    -- get it directly from cache dirst
    platform._PLATFORMS = platform._PLATFORMS or {}
    if platform._PLATFORMS[cachekey] then
        return platform._PLATFORMS[cachekey]
    end

    -- find the platform script path
    local scriptpath = nil
    for _, dir in ipairs(platform.directories()) do

        -- find this directory
        scriptpath = path.join(dir, plat, "xmake.lua")
        if os.isfile(scriptpath) then
            break
        end
    end

    -- unknown platform? switch to cross compilation platform
    local cross = false
    if not scriptpath or not os.isfile(scriptpath) then
        scriptpath = path.join(os.programdir(), "platforms", "cross", "xmake.lua")
        cross = true
    end

    -- not exists?
    if not scriptpath or not os.isfile(scriptpath) then
        return nil, string.format("the platform %s not found!", plat)
    end

    -- get interpreter
    local interp = platform._interpreter()

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

    -- load platform
    local results, errors = interp:make("platform", true, false)
    if not results and os.isfile(scriptpath) then
        return nil, errors
    end

    -- get result
    local result = cross and results["cross"] or results[plat]

    -- check the platform name
    if not result then
        return nil, string.format("the platform %s not found!", plat)
    end

    -- new an instance
    local instance, errors = _instance.new(plat, result, interp:rootdir())
    if not instance then
        return nil, errors
    end

    -- save instance to the cache
    platform._PLATFORMS[cachekey] = instance
    return instance
end

-- get the given platform configuration
function platform.get(name, plat)
    local instance, errors = platform.load(plat)
    if instance then
        return instance:get(name)
    else
        os.raise(errors)
    end
end

-- get the platform tool from the kind
--
-- e.g. cc, cxx, mm, mxx, as, ar, ld, sh, ..
--
function platform.tool(toolkind, plat)

    -- attempt to get program from config first
    local program = config.get(toolkind)
    local toolname = config.get("__toolname_" .. toolkind)
    if program == nil then 

        -- get the current platform 
        local instance, errors = platform.load(plat)
        if not instance then
            os.raise(errors)
        end
        
        -- check it first
        local on_check = instance:script("config_check")
        if on_check then
            on_check(instance, toolkind)
        end

        -- get it again
        program = config.get(toolkind)
        toolname = config.get("__toolname_" .. toolkind)
    end

    -- contain toolname? parse it, e.g. 'gcc@xxxx.exe'
    if program and type(program) == "string" then
        local pos = program:find('@', 1, true)
        if pos then
            toolname = program:sub(1, pos - 1)
            program = program:sub(pos + 1)
        end
    end
    return program, toolname
end

-- get the all platforms
function platform.plats()
    
    -- return it directly if exists
    if platform._PLATS then
        return platform._PLATS 
    end

    -- get all platforms
    local plats = {}
    local dirs  = platform.directories()
    for _, dir in ipairs(dirs) do

        -- get the platform list 
        local platpathes = os.match(path.join(dir, "*"), true)
        if platpathes then
            for _, platpath in ipairs(platpathes) do
                if os.isfile(path.join(platpath, "xmake.lua")) then
                    table.insert(plats, path.basename(platpath))
                end
            end
        end
    end

    -- save them
    platform._PLATS = plats

    -- ok
    return plats
end

-- get the platform os
function platform.os(plat)
    return platform.get("os", plat)
end

-- get the platform archs
function platform.archs(plat)
    return platform.get("archs", plat)
end

-- get the format of the given target kind for platform
function platform.format(targetkind)

    -- get formats
    local formats = platform.get("formats")
    if formats then
        return formats[targetkind]
    end
end


-- return module
return platform
