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

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

-- load modules
local bit            = require("bit")
local os             = require("base/os")
local path           = require("base/path")
local utils          = require("base/utils")
local table          = require("base/table")
local baseoption     = require("base/option")
local deprecated     = require("base/deprecated")
local rule           = require("project/rule")
local option         = require("project/option")
local config         = require("project/config")
local requireinfo    = require("project/requireinfo")
local tool           = require("tool/tool")
local linker         = require("tool/linker")
local compiler       = require("tool/compiler")
local platform       = require("platform/platform")
local language       = require("language/language")
local sandbox        = require("sandbox/sandbox")
local sandbox_module = require("sandbox/modules/import/core/sandbox/module")

-- new a target instance
function _instance.new(name, info, project)
    local instance    = table.inherit(_instance)
    instance._NAME    = name
    instance._INFO    = info
    instance._PROJECT = project
    instance._CACHEID = 1
    return instance
end

-- load rule, move cache to target
function _instance:_load_rule(ruleinst, suffix)

    -- init cache
    local key = ruleinst:name() .. (suffix and ("_" .. suffix) or "")
    local cache = self._RULES_LOADED or {}

    -- do load
    if cache[key] == nil then
        local on_load = ruleinst:script("load" .. (suffix and ("_" .. suffix) or ""))
        if on_load then
            local ok, errors = sandbox.load(on_load, self)
            cache[key] = {ok, errors}
        else
            cache[key] = {true}
        end
    end

    -- save cache
    self._RULES_LOADED = cache

    -- return results
    local results = cache[key]
    if results then
        return results[1], results[2]
    end
end

-- load rules
function _instance:_load_rules(suffix)
    for _, r in pairs(self:orderules()) do
        local ok, errors = self:_load_rule(r, suffix)
        if not ok then
            return false, errors
        end
    end
    return true
end

-- load all
function _instance:_load_all()

    -- do before_load with target rules
    local ok, errors = self:_load_rules("before")
    if not ok then
        return false, errors
    end

    -- do load with target rules
    local ok, errors = self:_load_rules()
    if not ok then
        return false, errors
    end

    -- do load for target
    local on_load = self:script("load")
    if on_load then
        local ok, errors = sandbox.load(on_load, self)
        if not ok then
            return false, errors
        end
    end

    -- do after_load with target rules
    local ok, errors = self:_load_rules("after")
    if not ok then
        return false, errors
    end
    return true
end

-- do load target and rules
function _instance:_load()

    -- enter the environments of the target packages
    local oldenvs = {}
    for name, values in pairs(self:pkgenvs()) do
        oldenvs[name] = os.getenv(name)
        os.addenv(name, unpack(values))
    end

    -- load all
    local ok, errors = self:_load_all()
    if not ok then
        return false, errors
    end

    -- leave the environments of the target packages
    for name, values in pairs(oldenvs) do
        os.setenv(name, values)
    end
    return true
end

-- get the copied files
function _instance:_copiedfiles(filetype, outputdir, pathfilter)

    -- no copied files?
    local copiedfiles = self:get(filetype)
    if not copiedfiles then return end

    -- get the extra information
    local extrainfo = table.wrap(self:get("__extra_" .. filetype))

    -- get the source pathes and destinate pathes
    local srcfiles = {}
    local dstfiles = {}
    local fileinfos = {}
    for _, copiedfile in ipairs(table.wrap(copiedfiles)) do

        -- get the root directory
        local rootdir, count = copiedfile:gsub("|.*$", ""):gsub("%(.*%)$", "")
        if count == 0 then
            rootdir = nil
        end

        -- remove '(' and ')'
        local srcpathes = copiedfile:gsub("[%(%)]", "")
        if srcpathes then 

            -- get the source pathes
            srcpathes = os.match(srcpathes)
            if srcpathes and #srcpathes > 0 then

                -- add the source copied files
                table.join2(srcfiles, srcpathes)

                -- the copied directory exists?
                if outputdir then

                    -- get the file info
                    local fileinfo = extrainfo[copiedfile] or {}

                    -- get the prefix directory
                    local prefixdir = fileinfo.prefixdir

                    -- add the destinate copied files
                    for _, srcpath in ipairs(srcpathes) do

                        -- get the destinate directory
                        local dstdir = outputdir
                        if prefixdir then
                            dstdir = path.join(dstdir, prefixdir)
                        end

                        -- the destinate file
                        local dstfile = nil
                        if rootdir then
                            dstfile = path.absolute(path.relative(srcpath, rootdir), dstdir)
                        else
                            dstfile = path.join(dstdir, path.filename(srcpath))
                        end
                        assert(dstfile)

                        -- modify filename
                        if fileinfo.filename then
                            dstfile = path.join(path.directory(dstfile), fileinfo.filename)
                        end

                        -- filter the destinate file path
                        if pathfilter then
                            dstfile = pathfilter(dstfile, fileinfo)
                        end

                        -- add it
                        table.insert(dstfiles, dstfile)
                        table.insert(fileinfos, fileinfo)
                    end
                end
            end
        end
    end
    return srcfiles, dstfiles, fileinfos
end

-- get the visibility, private: 1, interface: 2, public: 3 = 1 | 2
function _instance:_visibility(opt)
    local visibility = 1
    if opt then
        if opt.interface then
            visibility = 2
        elseif opt.public then
            visibility = 3
        end
    end
    return visibility
end

-- invalidate the previous cache key
function _instance:_invalidate()
    self._CACHEID = self._CACHEID + 1
end

-- get the target info
--
-- e.g. 
--
-- default: get private
--  - target:get("cflags")
--  - target:get("cflags", {private = true})
--
-- get private and interface
--  - target:get("cflags", {public = true})
--
-- get interface
--  - target:get("cflags", {interface = true})
--
function _instance:get(name, opt)

    -- get values
    local values = self._INFO:get(name)

    -- get thr required visibility
    local vs_private   = 1
    local vs_interface = 2
    local vs_public    = 3 -- all
    local vs_required  = self:_visibility(opt)

    -- get all values? (private and interface)
    if vs_required == vs_public then
        return values
    end

    -- get the extra configuration
    local extraconf = self:extraconf(name)
    if extraconf then
        -- filter values for public, private or interface if be not dictionary
        if not table.is_dictionary(values) then
            local results = {}
            for _, value in ipairs(table.wrap(values)) do
                local vs_conf = self:_visibility(extraconf[value])
                if bit.band(vs_required, vs_conf) ~= 0 then
                    table.insert(results, value)
                end
            end
            if #results > 0 then
                return table.unwrap(results)
            end
        else
            return values
        end
    else
        -- only get thr private values
        if bit.band(vs_required, vs_private) ~= 0 then
            return values
        end
    end
end

-- set the value to the target info
function _instance:set(name, ...)
    self._INFO:apival_set(name, ...)
    self:_invalidate()
end

-- add the value to the target info
function _instance:add(name, ...)
    self._INFO:apival_add(name, ...)
    self:_invalidate()
end

-- remove the value to the target info
function _instance:del(name, ...)
    self._INFO:apival_del(name, ...)
    self:_invalidate()
end

-- get the extra configuration
function _instance:extraconf(name, item, key)
    return self._INFO:extraconf(name, item, key)
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

-- get values
function _instance:values(name, sourcefile)

    -- get values from the source file first
    local values = {}
    if sourcefile then
        local fileconfig = self:fileconfig(sourcefile)
        if fileconfig then
            local filevalues = fileconfig.values
            if filevalues then
                -- we use '_' to simplify setting, for example:
                --
                -- add_files("xxx.mof", {values = {wdk_mof_header = "xxx.h"}}) 
                -- add_files("xxx.mof", {values = {["wdk.mof.header"] = "xxx.h"}}) 
                --
                table.join2(values, filevalues[name] or filevalues[name:gsub("%.", "_")])
            end
        end
    end

    -- get values from target
    table.join2(values, self:get("values." .. name))
    if #values > 0 then
        values = table.unwrap(values) 
    else
        values = nil
    end
    return values
end

-- set values
function _instance:values_set(name, ...)
    self:set("values." .. name, ...)
end

-- add values
function _instance:values_add(name, ...)
    self:add("values." .. name, ...)
end

-- get the target info
function _instance:info()
    return self._INFO:info()
end

-- get the type: option
function _instance:type()
    return "target"
end

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

-- get the cache key 
function _instance:cachekey()
    return string.format("%s_%d", tostring(self), self._CACHEID)
end

-- get the target version
function _instance:version()

    -- get version and build version
    local version = self:get("version")
    local version_build = nil
    if version then
        local version_extra = self:get("__extra_version")
        if version_extra then
            version_build = self._VERSION_BUILD
            if not version_build then
                version_build = table.wrap(version_extra[version]).build
                if type(version_build) == "string" then
                    version_build = os.date(version_build, os.time())
                    self._VERSION_BUILD = version_build
                end
            end
        end
    end

    -- ok?
    return version, version_build
end

-- get the base name of target file
function _instance:basename()
    local filename = self:get("filename")
    if filename then
        return path.basename(filename)
    end
    return self:get("basename") or self:name()
end

-- get the target linker
function _instance:linker()

    -- get it from cache first
    if self._LINKER then
        return self._LINKER
    end

    -- get the linker instance
    local instance, errors = linker.load(self:targetkind(), self:sourcekinds(), self)
    if not instance then
        os.raise(errors)
    end

    -- cache it
    self._LINKER = instance

    -- get it
    return instance
end

-- make linking command for this target 
function _instance:linkcmd(objectfiles)
    return self:linker():linkcmd(objectfiles or self:objectfiles(), self:targetfile(), {target = self})
end

-- make linking arguments for this target 
function _instance:linkargv(objectfiles)
    return self:linker():linkargv(objectfiles or self:objectfiles(), self:targetfile(), {target = self})
end

-- make link flags for the given target
function _instance:linkflags()
    return self:linker():linkflags({target = self})
end

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

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

-- get target ordered deps
function _instance:orderdeps()
    return self._ORDERDEPS
end

-- get target rules
function _instance:rules()
    return self._RULES
end

-- get target ordered rules
function _instance:orderules()
    return self._ORDERULES
end

-- get target rule from the given rule name
function _instance:rule(name)
    if self._RULES then
        return self._RULES[name]
    end
end

-- is phony target?
function _instance:isphony()
    
    -- get target kind
    local targetkind = self:targetkind()

    -- is phony?
    return not targetkind or targetkind == "phony"
end

-- get the enabled option
function _instance:opt(name)
    return self:opts()[name]
end

-- get the enabled options
function _instance:opts()

    -- attempt to get it from cache first
    if self._OPTS_ENABLED then
        return self._OPTS_ENABLED
    end

    -- load options if be enabled 
    self._OPTS_ENABLED = {}
    for _, opt in ipairs(self:orderopts()) do
        self._OPTS_ENABLED[opt:name()] = opt
    end

    -- get it 
    return self._OPTS_ENABLED
end

-- get the enabled ordered options 
function _instance:orderopts()

    -- attempt to get it from cache first
    if self._ORDEROPTS_ENABLED then
        return self._ORDEROPTS_ENABLED
    end

    -- load options if be enabled 
    self._ORDEROPTS_ENABLED = {}
    for _, name in ipairs(table.wrap(self:get("options"))) do
        local opt = nil
        if config.get(name) then opt = option.load(name) end
        if opt then
            table.insert(self._ORDEROPTS_ENABLED, opt)
        end
    end

    -- load options from packages if no require info, be compatible with the option package in (*.pkg)
    for _, name in ipairs(table.wrap(self:get("packages"))) do
        if not requireinfo.load(name) then
            local opt = nil
            if config.get(name) then opt = option.load(name) end
            if opt then
                table.insert(self._ORDEROPTS_ENABLED, opt)
            end
        end
    end

    -- get it 
    return self._ORDEROPTS_ENABLED
end

-- get the enabled package
function _instance:pkg(name)
    return self:pkgs()[name]
end

-- get the enabled packages
function _instance:pkgs()

    -- attempt to get it from cache first
    if self._PKGS_ENABLED then
        return self._PKGS_ENABLED
    end

    -- load packages if be enabled 
    self._PKGS_ENABLED = {}
    for _, pkg in ipairs(self:orderpkgs()) do
        self._PKGS_ENABLED[pkg:name()] = pkg
    end

    -- get it 
    return self._PKGS_ENABLED
end

-- get the required packages 
function _instance:orderpkgs()
    if not self._ORDERPKGS_ENABLED then
        local packages = {}
        for _, pkg in ipairs(self._PACKAGES) do
            if pkg:enabled() then
                table.insert(packages, pkg)
            end
        end
        self._ORDERPKGS_ENABLED = packages
    end
    return self._ORDERPKGS_ENABLED
end

-- get the environments of packages
function _instance:pkgenvs()
    local pkgenvs = self._PKGENVS 
    if not pkgenvs then
        pkgenvs = {}
        self._PKGENVS = pkgenvs
        for _, pkgname in ipairs(table.wrap(self:get("packages"))) do
            local pkg = self:pkg(pkgname)
            if pkg then
                local envs = pkg:get("envs")
                if envs then
                    for name, values in pairs(envs) do
                        pkgenvs[name] = pkgenvs[name] or {}
                        table.join2(pkgenvs[name], values)
                    end
                end
            end
        end
    end
    return pkgenvs
end

-- get the config info of the given package 
function _instance:pkgconfig(pkgname)
    local extra_packages = self:get("__extra_packages")
    if extra_packages then
        return extra_packages[pkgname]
    end
end

-- get the object files directory
function _instance:objectdir(opt)

    -- the object directory
    local objectdir = self:get("objectdir")
    if not objectdir then
        objectdir = path.join(config.buildir(), ".objs")
    end
    objectdir = path.join(objectdir, self:name())

    -- get root directory of target
    if opt and opt.root then
        return objectdir
    end

    -- append plat sub-directory
    local plat = config.get("plat")
    if plat then
        objectdir = path.join(objectdir, plat)
    end

    -- append arch sub-directory
    local arch = config.get("arch")
    if arch then
        objectdir = path.join(objectdir, arch)
    end

    -- append mode sub-directory
    local mode = config.get("mode")
    if mode then
        objectdir = path.join(objectdir, mode)
    end
    return objectdir
end

-- get the dependent files directory
function _instance:dependir(opt)

    -- init the dependent directory
    local dependir = self:get("dependir")
    if not dependir then
        dependir = path.join(config.buildir(), ".deps")
    end
    dependir = path.join(dependir, self:name())

    -- get root directory of target
    if opt and opt.root then
        return dependir
    end

    -- append plat sub-directory
    local plat = config.get("plat")
    if plat then
        dependir = path.join(dependir, plat)
    end

    -- append arch sub-directory
    local arch = config.get("arch")
    if arch then
        dependir = path.join(dependir, arch)
    end

    -- append mode sub-directory
    local mode = config.get("mode")
    if mode then
        dependir = path.join(dependir, mode)
    end
    return dependir
end

-- get the autogen files directory
function _instance:autogendir(opt)

    -- the autogen directory
    local autogendir = path.join(config.buildir(), ".gens", self:name())

    -- get root directory of target
    if opt and opt.root then
        return autogendir
    end

    -- append plat sub-directory
    local plat = config.get("plat")
    if plat then
        autogendir = path.join(autogendir, plat)
    end

    -- append arch sub-directory
    local arch = config.get("arch")
    if arch then
        autogendir = path.join(autogendir, arch)
    end

    -- append mode sub-directory
    local mode = config.get("mode")
    if mode then
        autogendir = path.join(autogendir, mode)
    end
    return autogendir
end

-- get the target kind
function _instance:targetkind()
    return self:get("kind") or "phony"
end

-- get the target directory
function _instance:targetdir()

    -- the target directory
    local targetdir = self:get("targetdir")
    if not targetdir then

        -- get build directory
        targetdir = config.buildir()

        -- append plat sub-directory
        local plat = config.get("plat")
        if plat then
            targetdir = path.join(targetdir, plat)
        end

        -- append arch sub-directory
        local arch = config.get("arch")
        if arch then
            targetdir = path.join(targetdir, arch)
        end

        -- append mode sub-directory
        local mode = config.get("mode")
        if mode then
            targetdir = path.join(targetdir, mode)
        end
    end

    -- ok?
    return targetdir
end

-- get the target file 
function _instance:targetfile()

    -- the target directory
    local targetdir = self:targetdir()

    -- get target kind
    local targetkind = self:targetkind()

    -- only compile objects? no target file
    if targetkind == "object" then
        return 
    end

    -- make the target file name and attempt to use the format of linker first
    local filename = self:get("filename") or target.filename(self:basename(), targetkind, self:linker():format(targetkind))
    assert(filename)

    -- make the target file path
    return path.join(targetdir, filename)
end

-- get the symbol file
function _instance:symbolfile()

    -- the target directory
    local targetdir = self:targetdir() or config.buildir()
    assert(targetdir and type(targetdir) == "string")

    -- the symbol file name
    local filename = target.filename(self:basename(), "symbol")
    assert(filename)

    -- make the symbol file path
    return path.join(targetdir, filename)
end

-- get the script directory of xmake.lua
function _instance:scriptdir()
    return self:get("__scriptdir")
end

-- TODO get header directory (deprecated)
function _instance:headerdir()
    return self:get("headerdir") or config.buildir()
end

-- get configuration output directory
function _instance:configdir()
    return self:get("configdir") or config.buildir()
end

-- get run directory
function _instance:rundir()
    return baseoption.get("workdir") or self:get("rundir") or path.directory(self:targetfile())
end

-- get install directory
function _instance:installdir()

    -- get it from the cache
    local installdir = self._INSTALLDIR
    if not installdir then

        -- get it from target
        installdir = baseoption.get("installdir")
        if not installdir then

            -- DESTDIR: be compatible with https://www.gnu.org/prep/standards/html_node/DESTDIR.html
            installdir = self:get("installdir") or os.getenv("INSTALLDIR") or os.getenv("PREFIX") or os.getenv("DESTDIR") or platform.get("installdir")
            if installdir then
                installdir = installdir:trim()
            end
        end
        self._INSTALLDIR = installdir or false
    end

    -- ok
    return installdir or nil
end

-- get rules of the source file 
function _instance:filerules(sourcefile)

    -- add rules from file config
    local rules = {}
    local fileconfig = self:fileconfig(sourcefile)
    if fileconfig then
        local filerules = fileconfig.rules or fileconfig.rule 
        if filerules then
            for _, rulename in ipairs(table.wrap(filerules)) do
                local r = self._PROJECT.rule(rulename) or rule.rule(rulename)
                if r then
                    table.insert(rules, r)
                end
            end
        end
    end

    -- load all rules for this target with sourcekinds and extensions
    local key2rules = self._KEY2RULES
    if not key2rules then
        key2rules = {}
        for _, r in pairs(table.wrap(self:rules())) do
            for _, sourcekind in ipairs(table.wrap(r:get("sourcekinds"))) do
                key2rules[sourcekind] = key2rules[sourcekind] or {}
                table.insert(key2rules[sourcekind], r)
            end
            for _, extension in ipairs(table.wrap(r:get("extensions"))) do
                extension = extension:lower()
                key2rules[extension] = key2rules[extension] or {}
                table.insert(key2rules[extension], r)
            end
        end
        self._KEY2RULES = key2rules
    end

    -- get target rules from the given sourcekind or extension 
    for _, r in ipairs(table.wrap(key2rules[self:sourcekind_of(sourcefile)] or key2rules[path.extension(sourcefile):lower()])) do
        table.insert(rules, r)
    end
    return rules 
end

-- get the config info of the given source file
function _instance:fileconfig(sourcefile)

    -- get files config
    local filesconfig = self._FILESCONFIG
    if not filesconfig then
        filesconfig = {}
        for filepath, fileconfig in pairs(table.wrap(self:get("__extra_files"))) do

            -- match source files
            local results = os.match(filepath)
            if #results == 0 then
                local sourceinfo = (self:get("__sourceinfo_files") or {})[filepath] or {}
                utils.warning("cannot match %s(%s).add_files(\"%s\") at %s:%d", self:type(), self:name(), filepath, sourceinfo.file or "", sourceinfo.line or -1)
            end

            -- process source files
            for _, file in ipairs(results) do

                -- convert to the relative path
                if path.is_absolute(file) then
                    file = path.relative(file, os.projectdir())
                end

                -- save it
                filesconfig[file] = fileconfig
            end
        end
        self._FILESCONFIG = filesconfig
    end

    -- get file config
    return filesconfig[sourcefile]
end

-- set the config info to the given source file
function _instance:fileconfig_set(sourcefile, info)

    -- get files config
    local filesconfig = self._FILESCONFIG or {}

    -- set config info
    filesconfig[sourcefile] = info
    
    -- update files config
    self._FILESCONFIG = filesconfig
end

-- get the source files 
function _instance:sourcefiles()

    -- cached? return it directly
    if self._SOURCEFILES then
        return self._SOURCEFILES, false
    end

    -- get files
    local files = self:get("files")

    -- no files?
    if not files then
        return {}, false
    end

    -- match files
    local i = 1
    local count = 0
    local cache = true
    local sourcefiles = {}
    for _, file in ipairs(table.wrap(files)) do

        -- mark as deleted files?
        local deleted = false
        if file:startswith("__del_") then
            file = file:sub(7)
            deleted = true
        end

        -- match source files
        local results = os.match(file)
        if #results == 0 then
            local sourceinfo = (self:get("__sourceinfo_files") or {})[file] or {}
            utils.warning("cannot match %s(%s).%s_files(\"%s\") at %s:%d", self:type(), self:name(), utils.ifelse(deleted, "del", "add"), file, sourceinfo.file or "", sourceinfo.line or -1)
        end

        -- process source files
        for _, sourcefile in ipairs(results) do

            -- convert to the relative path
            if path.is_absolute(sourcefile) then
                sourcefile = path.relative(sourcefile, os.projectdir())
            end

            -- add or delete it
            if deleted then
                sourcefiles[sourcefile] = nil
            else
                sourcefiles[sourcefile] = true
            end
        end
    end

    -- make last source files
    local sourcefiles_last = {}
    for sourcefile, _ in pairs(sourcefiles) do
        table.insert(sourcefiles_last, sourcefile)
    end

    -- cache it
    if cache then
        self._SOURCEFILES = sourcefiles_last
    end

    -- ok? modified?
    return sourcefiles_last, not cache
end

-- get object file from source file
function _instance:objectfile(sourcefile, sourcekind)

    -- get relative directory in the autogen directory
    local relativedir = nil
    local origindir  = path.directory(path.absolute(sourcefile))
    local autogendir = path.absolute(self:autogendir())
    if origindir:startswith(autogendir) then
        relativedir = path.join("gens", path.relative(origindir, autogendir))
    end

    -- get relative directory in the source directory
    if not relativedir then
        relativedir = path.directory(sourcefile)
    end

    -- translate path
    --
    -- e.g. 
    --
    -- src/xxx.c
    --      project/xmake.lua
    --          build/.objs
    --
    -- objectfile: project/build/.objs/xxxx/../../xxx.c will be out of range for objectdir
    --
    -- we need replace '..' to '__' in this case
    --
    if path.is_absolute(relativedir) and os.host() == "windows" then
        relativedir = relativedir:gsub(":[\\/]*", '\\') -- replace C:\xxx\ => C\xxx\
    end
    relativedir = relativedir:gsub("%.%.", "__")

    -- make object file
    -- full file name(not base) to avoid name-clash of object file
    return path.join(self:objectdir(), relativedir, target.filename(path.filename(sourcefile), "object"))
end

-- get the object files
function _instance:objectfiles()

    -- get source batches
    local sourcebatches, modified = self:sourcebatches()

    -- cached? return it directly
    if self._OBJECTFILES and not modified then
        return self._OBJECTFILES
    end

    -- get object files from source batches
    local objectfiles = {}
    local batchcount = 0
    for _, sourcebatch in pairs(self:sourcebatches()) do
        table.join2(objectfiles, sourcebatch.objectfiles)
        batchcount = batchcount + 1
    end

    -- some object files may be repeat and appear link errors if multi-batches exists, so we need remove all repeat object files
    -- e.g. add_files("src/*.c", {rules = {"rule1", "rule2"}})
    local remove_repeat = batchcount > 1

    -- get object files from all dependent targets (object kind)
    if self:orderdeps() then
        for _, dep in ipairs(self:orderdeps()) do
            if dep:targetkind() == "object" then
                table.join2(objectfiles, dep:objectfiles())
                remove_repeat = true
            end
        end
    end

    -- remove repeat object files
    if remove_repeat then
        objectfiles = table.unique(objectfiles)
    end

    -- cache it
    self._OBJECTFILES = objectfiles

    -- ok?
    return objectfiles
end

-- TODO get the header files, get("headers") (deprecated)
function _instance:headers(outputdir)
    return self:headerfiles(outputdir, true)
end

-- get the header files
--
-- default: get("headers") + get("headerfiles")
-- only_deprecated: get("headers")
--
function _instance:headerfiles(outputdir, only_deprecated)

    -- get header files?
    local headers = self:get("headers") -- TODO deprecated
    if not only_deprecated then
       headers = table.join(headers or {}, self:get("headerfiles")) 
    end
    if not headers then return end

    -- get the installed header directory
    local headerdir = outputdir 
    if not headerdir then
        if only_deprecated then
            headerdir = self:headerdir()
        elseif self:installdir() then
            headerdir = path.join(self:installdir(), "include")
        end
    end

    -- get the extra information
    local extrainfo = table.wrap(self:get("__extra_headerfiles"))

    -- get the source pathes and destinate pathes
    local srcheaders = {}
    local dstheaders = {}
    for _, header in ipairs(table.wrap(headers)) do

        -- get the root directory
        local rootdir, count = header:gsub("|.*$", ""):gsub("%(.*%)$", "")
        if count == 0 then
            rootdir = nil
        end

        -- remove '(' and ')'
        local srcpathes = header:gsub("[%(%)]", "")
        if srcpathes then 

            -- get the source pathes
            srcpathes = os.match(srcpathes)
            if srcpathes then

                -- add the source headers
                table.join2(srcheaders, srcpathes)

                -- get the destinate directories if the install directory exists
                if headerdir then

                    -- get the prefix directory
                    local prefixdir = (extrainfo[header] or {}).prefixdir

                    -- add the destinate headers
                    for _, srcpath in ipairs(srcpathes) do

                        -- get the destinate directory
                        local dstdir = headerdir
                        if prefixdir then
                            dstdir = path.join(dstdir, prefixdir)
                        end

                        -- the destinate header
                        local dstheader = nil
                        if rootdir then
                            dstheader = path.absolute(path.relative(srcpath, rootdir), dstdir)
                        else
                            dstheader = path.join(dstdir, path.filename(srcpath))
                        end
                        assert(dstheader)

                        -- add it
                        table.insert(dstheaders, dstheader)
                    end
                end
            end
        end
    end

    -- ok?
    return srcheaders, dstheaders
end

-- get the configuration files
function _instance:configfiles(outputdir)
    return self:_copiedfiles("configfiles", outputdir or self:configdir(), function (dstpath, fileinfo)
            if dstpath:endswith(".in") then
                dstpath = dstpath:sub(1, -4)
            end
            return dstpath
        end)
end

-- get the install files
function _instance:installfiles(outputdir)
    return self:_copiedfiles("installfiles", outputdir or self:installdir())
end

-- get depend file from object file
function _instance:dependfile(objectfile)

    -- get the dependent original file and directory, @note relative to the root directory
    local originfile = path.absolute(objectfile and objectfile or self:targetfile())
    local origindir  = path.directory(originfile)

    -- get relative directory in the object directory
    local relativedir = nil
    local objectdir = path.absolute(self:objectdir())
    if origindir:startswith(objectdir) then
        relativedir = path.relative(origindir, objectdir)
    end

    -- get relative directory in the target directory
    if not relativedir then
        local targetdir = path.absolute(self:targetdir())
        if origindir:startswith(targetdir) then
            relativedir = path.relative(origindir, targetdir)
        end
    end

    -- get relative directory in the autogen directory
    if not relativedir then
        local autogendir = path.absolute(self:autogendir())
        if origindir:startswith(autogendir) then
            relativedir = path.join("gens", path.relative(origindir, autogendir))
        end
    end

    -- get relative directory in the build directory
    if not relativedir then
        local buildir = path.absolute(config.buildir())
        if origindir:startswith(buildir) then
            relativedir = path.join("build", path.relative(origindir, buildir))
        end
    end

    -- get relative directory in the project directory
    if not relativedir then
        local projectdir = os.projectdir()
        if origindir:startswith(projectdir) then
            relativedir = path.relative(origindir, projectdir)
        end
    end

    -- get the relative directory from the origin file
    if not relativedir then
        relativedir = origindir
    end
    if path.is_absolute(relativedir) and os.host() == "windows" then
        relativedir = relativedir:gsub(":[\\/]*", '\\') -- replace C:\xxx\ => C\xxx\
    end

    -- originfile: project/build/.objs/xxxx/../../xxx.c will be out of range for objectdir
    --
    -- we need replace '..' to '__' in this case
    --
    relativedir = relativedir:gsub("%.%.", "__")

    -- make dependent file
    -- full file name(not base) to avoid name-clash of original file
    return path.join(self:dependir(), relativedir, path.basename(originfile) .. ".d")
end

-- get the dependent include files
function _instance:dependfiles()

    -- get source batches
    local sourcebatches, modified = self:sourcebatches()

    -- cached? return it directly
    if self._DEPENDFILES and not modified then
        return self._DEPENDFILES
    end

    -- get dependent files from source batches
    local dependfiles = {}
    for _, sourcebatch in pairs(self:sourcebatches()) do
        table.join2(dependfiles, sourcebatch.dependfiles)
    end

    -- cache it
    self._DEPENDFILES = dependfiles

    -- ok?
    return dependfiles
end

-- get the sourcekind for the given source file
function _instance:sourcekind_of(sourcefile)

    -- get the sourcekind of this source file
    local sourcekind = language.sourcekind_of(sourcefile)
    local fileconfig = self:fileconfig(sourcefile)
    if fileconfig and fileconfig.sourcekind then
        -- we can override the sourcekind, e.g. add_files("*.c", {sourcekind = "cxx"})
        sourcekind = fileconfig.sourcekind
    end
    return sourcekind
end

-- get the kinds of sourcefiles
--
-- e.g. cc cxx mm mxx as ...
--
function _instance:sourcekinds()

    -- cached? return it directly
    if self._SOURCEKINDS then
        return self._SOURCEKINDS
    end

    -- make source kinds
    local sourcekinds = {}
    for _, sourcefile in pairs(self:sourcefiles()) do

        -- get source kind
        local sourcekind = self:sourcekind_of(sourcefile)
        if sourcekind then
            table.insert(sourcekinds, sourcekind)
        end
    end

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

    -- cache it
    self._SOURCEKINDS = sourcekinds

    -- ok?
    return sourcekinds 
end

-- get source count
function _instance:sourcecount()
    return #self:sourcefiles()
end

-- get source batches
function _instance:sourcebatches()

    -- get source files
    local sourcefiles, modified = self:sourcefiles()

    -- cached? return it directly
    if self._SOURCEBATCHES and not modified then
        return self._SOURCEBATCHES, false
    end

    -- make source batches for each source kinds
    local sourcebatches = {}
    for _, sourcefile in ipairs(sourcefiles) do

        -- get file rules
        local filerules = self:filerules(sourcefile)
        if #filerules == 0 then
            os.raise("unknown source file: %s", sourcefile)
        end

        -- add source batch for the file rules
        for _, filerule in ipairs(filerules) do

            -- get rule name
            local rulename = filerule:name()

            -- make this batch
            local sourcebatch = sourcebatches[rulename] or {sourcefiles = {}}
            sourcebatches[rulename] = sourcebatch

            -- save the rule name
            sourcebatch.rulename = rulename

            -- add source file to this batch
            table.insert(sourcebatch.sourcefiles, sourcefile)

            -- attempt to get source kind from the builtin languages
            local sourcekind = self:sourcekind_of(sourcefile)
            if sourcekind then

                -- save source kind
                sourcebatch.sourcekind = sourcekind

                -- insert object files to source batches
                sourcebatch.objectfiles = sourcebatch.objectfiles or {}
                sourcebatch.dependfiles = sourcebatch.dependfiles or {}
                local objectfile = self:objectfile(sourcefile, sourcekind)
                table.insert(sourcebatch.objectfiles, objectfile)
                table.insert(sourcebatch.dependfiles, self:dependfile(objectfile))
            end
        end
    end

    -- cache it
    self._SOURCEBATCHES = sourcebatches

    -- ok?
    return sourcebatches, modified
end

-- get xxx_script
function _instance: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.host() .. '|' .. os.arch()] or hosts[os.host()])  
            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
    return result
end

-- TODO get the config header version (deprecated)
function _instance:configversion()

    -- get the config version and build version
    local version = nil
    local buildversion = nil
    local configheader = self:get("config_header")
    local configheader_extra = self:get("__extra_config_header")
    if type(configheader_extra) == "table" then
        version      = table.wrap(configheader_extra[configheader]).version
        buildversion = self._CONFIGHEADER_BUILDVERSION
        if not buildversion then
            buildversion = table.wrap(configheader_extra[configheader]).buildversion
            if buildversion then
                buildversion = os.date(buildversion, os.time())
            end
            self._CONFIGHEADER_BUILDVERSION = buildversion
        end
    end

    -- ok?
    return version, buildversion
end

-- get the config header prefix
function _instance:configprefix()

    -- get the config prefix
    local configprefix = nil
    local configheader = self:get("config_header")
    local configheader_extra = self:get("__extra_config_header")
    if type(configheader_extra) == "table" then
        configprefix = table.wrap(configheader_extra[configheader]).prefix
    end
    if not configprefix then
        configprefix = self:get("config_h_prefix") or (self:name():upper() .. "_CONFIG")
    end

    -- ok?
    return configprefix
end

-- get the config header files (deprecated)
function _instance:configheader(outputdir)

    -- get config header
    local configheader = self:get("config_header") or self:get("config_h")
    if not configheader then
        return 
    end

    -- mark as deprecated
    if self:get("config_h") then
        deprecated.add("set_config_header(\"%s\", {prefix = \"...\"})", "set_config_h(\"%s\")", path.relative(self:get("config_h"), os.projectdir()))
    end

    -- get the root directory
    local rootdir, count = configheader:gsub("|.*$", ""):gsub("%(.*%)$", "")
    if count == 0 then
        rootdir = nil
    end

    -- remove '(' and ')'
    configheader = configheader:gsub("[%(%)]", "")

    -- get the output header
    local outputheader = nil
    if outputdir then
        if rootdir then
            outputheader = path.absolute(path.relative(configheader, rootdir), outputdir)
        else
            outputheader = path.join(outputdir, path.filename(configheader))
        end
    end

    -- ok
    return configheader, outputheader
end

-- get the precompiled header file (xxx.[h|hpp|inl])
--
-- @param langkind  c/cxx
--
function _instance:pcheaderfile(langkind)
    return self:get("p" .. langkind .. "header")
end

-- get the output of precompiled header file (xxx.h.pch)
--
-- @param langkind  c/cxx
--
function _instance:pcoutputfile(langkind)

    -- init cache
    self._PCOUTPUTFILES = self._PCOUTPUTFILES or {}

    -- get it from the cache first
    local pcoutputfile = self._PCOUTPUTFILES[langkind]
    if pcoutputfile then
        return pcoutputfile
    end
        
    -- get the precompiled header file in the object directory
    local pcheaderfile = self:pcheaderfile(langkind)
    if pcheaderfile then

        -- load tool instance
        local toolinstance = tool.load(language.langkinds()[langkind])

        -- make precompiled output file 
        --
        -- @note gcc has not -include-pch option to set the pch file path
        --
        pcoutputfile = self:objectfile(pcheaderfile)
        pcoutputfile = path.join(path.directory(pcoutputfile), path.basename(pcoutputfile) .. (toolinstance and toolinstance:name() == "gcc" and ".gch" or ".pch"))

        -- save to cache
        self._PCOUTPUTFILES[langkind] = pcoutputfile
        return pcoutputfile
    end
end

-- get target apis
function target.apis()

    return 
    {
        values =
        {
            -- target.set_xxx
            "target.set_kind"
        ,   "target.set_strip"
        ,   "target.set_rules"
        ,   "target.set_version"
        ,   "target.set_enabled"
        ,   "target.set_default"
        ,   "target.set_options"
        ,   "target.set_symbols"
        ,   "target.set_filename"
        ,   "target.set_basename"
        ,   "target.set_warnings"
        ,   "target.set_optimize"
        ,   "target.set_languages"
            -- target.add_xxx
        ,   "target.add_deps"
        ,   "target.add_rules"
        ,   "target.add_options"
        ,   "target.add_packages"
        ,   "target.add_imports"
        ,   "target.add_languages"
        ,   "target.add_vectorexts"
        }
    ,   keyvalues =
        {
            -- target.set_xxx
            "target.set_values"
        ,   "target.set_configvar"
        ,   "target.set_runenv"
        ,   "target.set_toolchain"
            -- target.add_xxx
        ,   "target.add_values"
        ,   "target.add_runenvs"
        }
    ,   pathes =
        {
            -- target.set_xxx
            "target.set_targetdir"
        ,   "target.set_objectdir"
        ,   "target.set_dependir"
        ,   "target.set_configdir"
        ,   "target.set_installdir"
        ,   "target.set_rundir"
            -- target.add_xxx
        ,   "target.add_files"
        ,   "target.add_cleanfiles"
        ,   "target.add_configfiles"
        ,   "target.add_installfiles"
            -- target.del_xxx
        ,   "target.del_files"
        }
    ,   dictionary =
        {
            -- target.set_xxx
            "target.set_tools" -- TODO: deprecated
        ,   "target.add_tools" -- TODO: deprecated
        }
    ,   script =
        {
            -- target.on_xxx
            "target.on_run"
        ,   "target.on_load"
        ,   "target.on_link"
        ,   "target.on_build"
        ,   "target.on_build_file"
        ,   "target.on_build_files"
        ,   "target.on_clean"
        ,   "target.on_package"
        ,   "target.on_install"
        ,   "target.on_uninstall"
            -- target.before_xxx
        ,   "target.before_run"
        ,   "target.before_link"
        ,   "target.before_build"
        ,   "target.before_build_file"
        ,   "target.before_build_files"
        ,   "target.before_clean"
        ,   "target.before_package"
        ,   "target.before_install"
        ,   "target.before_uninstall"
            -- target.after_xxx
        ,   "target.after_run"
        ,   "target.after_link"
        ,   "target.after_build"
        ,   "target.after_build_file"
        ,   "target.after_build_files"
        ,   "target.after_clean"
        ,   "target.after_package"
        ,   "target.after_install"
        ,   "target.after_uninstall"
        }
    }
end

-- get the filename from the given target name and kind
function target.filename(targetname, targetkind, targetformat)

    -- check
    assert(targetname and targetkind)

    -- make filename by format
    local format = targetformat or platform.format(targetkind) 
    return format and (format:gsub("%$%(name%)", targetname)) or targetname
end

-- get the link name of the target file
function target.linkname(filename)
    local linkname, count = filename:gsub(target.filename("__pattern__", "static"):gsub("%.", "%%."):gsub("__pattern__", "(.+)") .. "$", "%1")
    if count == 0 then
        linkname, count = filename:gsub(target.filename("__pattern__", "shared"):gsub("%.", "%%."):gsub("__pattern__", "(.+)") .. "$", "%1")
    end
    if count == 0 and config.is_plat("mingw") then
        -- for the mingw platform, it is compatible with the libxxx.a and xxx.lib
        local formats = {static = "lib$(name).a", shared = "lib$(name).so"}
        linkname, count = filename:gsub(target.filename("__pattern__", "static", formats["static"]):gsub("%.", "%%."):gsub("__pattern__", "(.+)") .. "$", "%1")
        if count == 0 then
            linkname, count = filename:gsub(target.filename("__pattern__", "shared", formats["shared"]):gsub("%.", "%%."):gsub("__pattern__", "(.+)") .. "$", "%1")
        end
    end
    return count > 0 and linkname or nil
end

-- new a target instance
function target.new(...)
    return _instance.new(...)
end

-- return module
return target
