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

-- define module
local builder = builder or {}

-- load modules
local io       = require("base/io")
local path     = require("base/path")
local utils    = require("base/utils")
local table    = require("base/table")
local string   = require("base/string")
local option   = require("base/option")
local tool     = require("tool/tool")
local config   = require("project/config")
local sandbox  = require("sandbox/sandbox")
local language = require("language/language")
local platform = require("platform/platform")

-- get the tool of builder
function builder:_tool()
    return self._TOOL
end

-- get the name flags
function builder:_nameflags()
    return self._NAMEFLAGS
end

-- get the target kind
function builder:_targetkind()
    return self._TARGETKIND
end

-- map gcc flag to the given builder flag
function builder:_mapflag(flag, flagkind, mapflags)

    -- attempt to map it directly
    local flag_mapped = mapflags[flag]
    if flag_mapped then
        return flag_mapped
    end

    -- find and replace it using pattern
    for k, v in pairs(mapflags) do
        local flag_mapped, count = flag:gsub("^" .. k .. "$", function (w) return v end)
        if flag_mapped and count ~= 0 then
            return utils.ifelse(#flag_mapped ~= 0, flag_mapped, nil) 
        end
    end

    -- has this flag?
    if self:has_flags(flag, flagkind) then
        return flag
    end
end

-- map gcc flags to the given builder flags
function builder:_mapflags(flags, flagkind)

    -- wrap flags first
    flags = table.wrap(flags)

    -- done
    local results = {}
    local mapflags = self:get("mapflags")
    if mapflags then

        -- map flags
        for _, flag in pairs(flags) do
            local flag_mapped = self:_mapflag(flag, flagkind, mapflags)
            if flag_mapped then
                table.insert(results, flag_mapped)
            end
        end

    else

        -- has flags?
        for _, flag in pairs(flags) do
            if self:has_flags(flag, flagkind) then
                table.insert(results, flag)
            end
        end

    end

    -- ok?
    return results
end

-- get the flag kinds
function builder:_flagkinds()
    return self._FLAGKINDS
end

-- inherit links from target deps
function builder:_inherit_links_from_targetdeps(results, target, flagname)

    -- for all target deps
    local orderdeps = target:orderdeps()
    local total = #orderdeps
    local deplinks = {}
    for idx, _ in ipairs(orderdeps) do

        -- reverse deps order for links
        local dep = orderdeps[total + 1 - idx]

        -- the dependent target is static or shared library? inherit it's links
        local depkind      = dep:targetkind()
        local targetkind   = target:targetkind()
        local depinherit   = target:extraconf("deps", dep:name(), "inherit")
        if (depkind == "static" or depkind == "shared" or depkind == "object") and (depinherit == nil or depinherit) then
            if targetkind == "binary" or targetkind == "shared" then
                if flagname == "links" or flagname == "syslinks" then

                    -- add dependent link
                    if depkind ~= "object" and flagname == "links" then
                        table.insert(results, dep:basename())
                    end

                    -- inherit links from the depdent target
                    self:_add_values_from_target(deplinks, dep, flagname)

                elseif flagname == "linkdirs" then

                    -- add dependent linkdirs
                    if depkind ~= "object" then
                        table.insert(results, path.directory(dep:targetfile()))
                    end

                    -- inherit linkdirs from the depdent target
                    self:_add_values_from_target(results, dep, flagname)

                elseif flagname == "rpathdirs" then

                    -- add dependent rpathdirs 
                    if depkind ~= "object" then
                        local rpathdir = "@loader_path"
                        local subdir = path.relative(path.directory(dep:targetfile()), path.directory(target:targetfile()))
                        if subdir and subdir ~= '.' then
                            rpathdir = path.join(rpathdir, subdir)
                        end
                        table.insert(results, rpathdir)
                    end
                end

            -- TODO deprecated
            elseif flagname == "includedirs" then

                -- add dependent headerdir
                if dep:get("headers") and os.isdir(dep:headerdir()) then
                    table.insert(results, dep:headerdir())
                end

                -- add dependent configheader directory
                local configheader = dep:configheader()
                if configheader and os.isfile(configheader) then
                    table.insert(results, path.directory(configheader))
                end
            end
        end
    end

    -- @note we need ensure add option and package links after all dependent targets
    if #deplinks > 0 then
        table.join2(results, deplinks)
    end
end

-- inherit flags (only for public/interface) from target deps
--
-- e.g. 
-- add_cflags("", {public = true})
-- add_cflags("", {interface = true})
--
function builder:_inherit_flags_from_targetdeps(flags, target)
    local orderdeps = target:orderdeps()
    local total = #orderdeps
    for idx, _ in ipairs(orderdeps) do
        local dep = orderdeps[total + 1 - idx]
        local depinherit = target:extraconf("deps", dep:name(), "inherit")
        if depinherit == nil or depinherit then
            for _, flagkind in ipairs(self:_flagkinds()) do
                self:_add_flags_from_flagkind(flags, dep, flagkind, {interface = true})
            end
        end
    end
end

-- inherit values (only for public/interface) from target deps
--
-- e.g. 
-- add_defines("", {public = true})
-- add_defines("", {interface = true})
--
function builder:_inherit_values_from_targetdeps(values, target, name)
    local orderdeps = target:orderdeps()
    local total = #orderdeps
    for idx, _ in ipairs(orderdeps) do
        local dep = orderdeps[total + 1 - idx]
        local depinherit = target:extraconf("deps", dep:name(), "inherit")
        if depinherit == nil or depinherit then
            table.join2(values, dep:get(name, {interface = true}))
        end
    end
end

-- add values from target
function builder:_add_values_from_target(values, target, name)
    table.join2(values, target:get(name))
    if target:type() == "target" then
        self:_add_values_from_targetopts(values, target, name)
        self:_add_values_from_targetpkgs(values, target, name)
    end
end

-- add values from target options
function builder:_add_values_from_targetopts(values, target, name)
    for _, opt in ipairs(target:orderopts()) do
        table.join2(values, table.wrap(opt:get(name)))
    end
end

-- add values from target packages
function builder:_add_values_from_targetpkgs(values, target, name)
    for _, pkg in ipairs(target:orderpkgs()) do
        -- uses them instead of the builtin configs if exists extra package config
        -- e.g. `add_packages("xxx", {links = "xxx"})`
        local configinfo = target:pkgconfig(pkg:name())
        if configinfo and configinfo[name] then
            table.join2(values, configinfo[name])
        else
            -- uses the builtin package configs
            table.join2(values, pkg:get(name))
        end
    end
end

-- add flags from the flagkind 
function builder:_add_flags_from_flagkind(flags, target, flagkind, opt)
    local targetflags = target:get(flagkind, opt)
    local extraconf   = target:extraconf(flagkind)
    if extraconf then
        for _, flag in ipairs(table.wrap(targetflags)) do
            -- force to add flags?
            local flagconf = extraconf[flag]
            if flagconf and flagconf.force then
                table.join2(flags, flag)
            else
                table.join2(flags, self:_mapflags(flag, flagkind))
            end
        end
    else
        table.join2(flags, self:_mapflags(targetflags, flagkind))
    end
end

-- add flags from the configure 
function builder:_add_flags_from_config(flags)
    for _, flagkind in ipairs(self:_flagkinds()) do
        table.join2(flags, config.get(flagkind))
    end
end

-- add flags from the option 
function builder:_add_flags_from_option(flags, opt)
    for _, flagkind in ipairs(self:_flagkinds()) do
        self:_add_flags_from_flagkind(flags, opt, flagkind)
    end
end

-- add flags from the package 
function builder:_add_flags_from_package(flags, pkg)
    for _, flagkind in ipairs(self:_flagkinds()) do
        table.join2(flags, self:_mapflags(pkg:get(flagkind), flagkind))
    end
end

-- add flags from the target 
function builder:_add_flags_from_target(flags, target)

    -- no target?
    if not target then
        return
    end
 
    -- init cache
    self._TARGETFLAGS = self._TARGETFLAGS or {}
    local cache = self._TARGETFLAGS

    -- get flags from cache first
    local key = target:cachekey()
    local targetflags = cache[key]
    if not targetflags then
    
        -- add flags from language
        targetflags = {}
        self:_add_flags_from_language(targetflags, target)

        -- add flags for the target 
        if target:type() == "target" then

            -- add flags from options
            for _, opt in ipairs(target:orderopts()) do
                self:_add_flags_from_option(targetflags, opt)
            end

            -- add flags from packages
            for _, pkg in ipairs(target:orderpkgs()) do
                self:_add_flags_from_package(targetflags, pkg)
            end

            -- inherit flags (public/interface) from all dependent targets
            self:_inherit_flags_from_targetdeps(targetflags, target)
        end

        -- add the target flags 
        for _, flagkind in ipairs(self:_flagkinds()) do
            self:_add_flags_from_flagkind(targetflags, target, flagkind)
        end

        -- cache it
        cache[key] = targetflags
    end

    -- add flags
    table.join2(flags, targetflags)
end

-- add flags from the argument option 
function builder:_add_flags_from_argument(flags, target, args)

    -- add flags from the flag kinds (cxflags, ..)
    for _, flagkind in ipairs(self:_flagkinds()) do

        -- add auto mapping flags
        table.join2(flags, self:_mapflags(args[flagkind], flagkind))

        -- add original flags
        local original_flags = (args.force or {})[flagkind]
        if original_flags then
            table.join2(flags, original_flags)
        end
    end

    -- add flags (named) from the language 
    if target then
        local key = target:type()
        self:_add_flags_from_language(flags, target, {[key] = function (name) return args[name] end})
    else
        self:_add_flags_from_language(flags, nil, {target = function (name) return args[name] end})
    end
end

-- add flags from the language 
function builder:_add_flags_from_language(flags, target, getters)

    -- init getters
    --
    -- e.g.
    --
    -- target.linkdirs => flags = getters("target")("linkdirs")
    --
    local getters = getters or
    {
        config      =   function (name)
                            local values = config.get(name)
                            if values and name:endswith("dirs") then
                                values = path.splitenv(values)
                            end
                            return values
                        end
    ,   platform    =   platform.get
    ,   target      =   function (name) 

                            -- only for target
                            local results = {}
                            if target:type() == "target" then

                                -- link? add includes and links of all dependent targets first
                                if name == "links" or name == "syslinks" or name == "linkdirs" or name == "rpathdirs" or name == "includedirs" then
                                    self:_inherit_links_from_targetdeps(results, target, name)
                                end

                                -- inherit flagvalues (public or interface) of all dependent targets
                                self:_inherit_values_from_targetdeps(results, target, name)

                                -- get flagvalues of target with given flagname
                                table.join2(results, target:get(name))
                            end
                            return results
                        end
    ,   option      =   function (name)

                            -- is target? get flagvalues of the attached options and packages
                            local results = {}
                            if target:type() == "target" then
                                self:_add_values_from_targetopts(results, target, name)
                                self:_add_values_from_targetpkgs(results, target, name)

                            -- is option? get flagvalues of option with given flagname
                            elseif target:type() == "option" then
                                table.join2(results, target:get(name))
                            end
                            return results
                        end
    }

    -- get name flags for builder
    for _, flaginfo in ipairs(self:_nameflags()) do

        -- get flag info
        local flagscope     = flaginfo[1]
        local flagname      = flaginfo[2]
        local checkstate    = flaginfo[3]

        -- get getter
        local getter = getters[flagscope]
        if getter then

            -- get api name of tool 
            --
            -- ignore "nf_" and "_if_ok"
            --
            -- e.g.
            --
            -- defines => define
            -- defines_if_ok => define
            -- ...
            --
            local apiname = flagname:gsub("^nf_", ""):gsub("_if_ok$", "")
            if apiname:endswith("s") then
                apiname = apiname:sub(1, #apiname - 1)
            end

            -- map name flag to real flag
            local mapper = self:_tool()["nf_" .. apiname]
            if mapper then
                
                -- add the flags 
                for _, flagvalue in ipairs(table.wrap(getter(flagname))) do

                    -- map and check flag
                    local flag = mapper(self:_tool(), flagvalue, target, self:_targetkind())
                    if flag and flag ~= "" and (not checkstate or self:has_flags(flag)) then
                        table.join2(flags, flag)
                    end
                end
            end
        end
    end
end

-- preprocess flags
function builder:_preprocess_flags(flags)

    -- remove repeat
    flags = table.unique(flags)

    -- split flag group, e.g. "-I /xxx" => {"-I", "/xxx"}
    local results = {}
    for _, flag in ipairs(flags) do
        flag = flag:trim()
        if #flag > 0 then
            if flag:find(" ", 1, true) then
                table.join2(results, os.argv(flag))
            else
                table.insert(results, flag)
            end
        end
    end

    -- get it
    return results 
end

-- get tool name
function builder:name()
    return self:_tool():name()
end

-- get tool kind
function builder:kind()
    return self:_tool():kind()
end

-- get tool program
function builder:program()
    return self:_tool():program()
end

-- get properties of the tool
function builder:get(name)
    return self:_tool():get(name)
end

-- has flags?
function builder:has_flags(flags, flagkind)
    return self:_tool():has_flags(flags, flagkind)
end

-- get the format of the given target kind 
function builder:format(targetkind)

    -- get formats
    local formats = self:get("formats")
    if formats then
        return formats[targetkind]
    end
end

-- return module
return builder
