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

-- imports
import("core.base.option")
import("core.project.rule")
import("core.project.config")
import("core.project.project")
import("async.runjobs")
import("private.utils.batchcmds")
import("rule_groups")

-- has scripts for the custom rule
function _has_scripts_for_rule(ruleinst, suffix)

    -- add batch jobs for xx_build_files
    local scriptname = "build_files" .. (suffix and ("_" .. suffix) or "")
    local script = ruleinst:script(scriptname)
    if script then
        return true
    end

    -- add batch jobs for xx_build_file
    scriptname = "build_file" .. (suffix and ("_" .. suffix) or "")
    script = ruleinst:script(scriptname)
    if script then
        return true
    end

    -- add batch jobs for xx_buildcmd_files
    scriptname = "buildcmd_files" .. (suffix and ("_" .. suffix) or "")
    script = ruleinst:script(scriptname)
    if script then
        return true
    end

    -- add batch jobs for xx_buildcmd_file
    scriptname = "buildcmd_file" .. (suffix and ("_" .. suffix) or "")
    script = ruleinst:script(scriptname)
    if script then
        return true
    end
end

-- has scripts for target
function _has_scripts_for_target(target, suffix)
    local scriptname = "build_files" .. (suffix and ("_" .. suffix) or "")
    local script = target:script(scriptname)
    if script then
        return true
    else
        scriptname = "build_file" .. (suffix and ("_" .. suffix) or "")
        script = target:script(scriptname)
        if script then
            return true
        end
    end
end

-- has scripts for group
function _has_scripts_for_group(group, suffix)
    for _, item in pairs(group) do
        if item.target and _has_scripts_for_target(item.target, suffix) then
            return true
        end
        if item.rule and _has_scripts_for_rule(item.rule, suffix) then
            return true
        end
    end
end

-- add batch jobs for the custom rule
function _add_batchjobs_for_rule(batchjobs, rootjob, target, sourcebatch, suffix)

    -- get rule
    local rulename = assert(sourcebatch.rulename, "unknown rule for sourcebatch!")
    local ruleinst = rule_groups.get_rule(target, rulename)

    -- add batch jobs for xx_build_files
    local scriptname = "build_files" .. (suffix and ("_" .. suffix) or "")
    local script = ruleinst:script(scriptname)
    if script then
        if ruleinst:extraconf(scriptname, "batch") then
            script(target, batchjobs, sourcebatch, {rootjob = rootjob, distcc = ruleinst:extraconf(scriptname, "distcc")})
        else
            batchjobs:addjob("rule/" .. rulename .. "/" .. scriptname, function (index, total, opt)
                script(target, sourcebatch, {progress = opt.progress})
            end, {rootjob = rootjob})
        end
    end

    -- add batch jobs for xx_build_file
    if not script then
        scriptname = "build_file" .. (suffix and ("_" .. suffix) or "")
        script = ruleinst:script(scriptname)
        if script then
            local sourcekind = sourcebatch.sourcekind
            for _, sourcefile in ipairs(sourcebatch.sourcefiles) do
                batchjobs:addjob(sourcefile, function (index, total, opt)
                    script(target, sourcefile, {sourcekind = sourcekind, progress = opt.progress})
                end, {rootjob = rootjob, distcc = ruleinst:extraconf(scriptname, "distcc")})
            end
        end
    end

    -- add batch jobs for xx_buildcmd_files
    if not script then
        scriptname = "buildcmd_files" .. (suffix and ("_" .. suffix) or "")
        script = ruleinst:script(scriptname)
        if script then
            batchjobs:addjob("rule/" .. rulename .. "/" .. scriptname, function (index, total, opt)
                local batchcmds_ = batchcmds.new({target = target})
                local distcc = ruleinst:extraconf(scriptname, "distcc")
                script(target, batchcmds_, sourcebatch, {progress = opt.progress, distcc = distcc})
                batchcmds_:runcmds({changed = target:is_rebuilt(), dryrun = option.get("dry-run")})
            end, {rootjob = rootjob})
        end
    end

    -- add batch jobs for xx_buildcmd_file
    if not script then
        scriptname = "buildcmd_file" .. (suffix and ("_" .. suffix) or "")
        script = ruleinst:script(scriptname)
        if script then
            local sourcekind = sourcebatch.sourcekind
            for _, sourcefile in ipairs(sourcebatch.sourcefiles) do
                batchjobs:addjob(sourcefile, function (index, total, opt)
                    local batchcmds_ = batchcmds.new({target = target})
                    script(target, batchcmds_, sourcefile, {sourcekind = sourcekind, progress = opt.progress})
                    batchcmds_:runcmds({changed = target:is_rebuilt(), dryrun = option.get("dry-run")})
                end, {rootjob = rootjob, distcc = ruleinst:extraconf(scriptname, "distcc")})
            end
        end
    end
end

-- add batch jobs for target
function _add_batchjobs_for_target(batchjobs, rootjob, target, sourcebatch, suffix)

    -- we just build sourcebatch with on_build_files scripts
    --
    -- for example, c++.build and c++.build.modules.builder rules have same sourcefiles,
    -- but we just build it for c++.build
    --
    -- @see https://github.com/xmake-io/xmake/issues/3171
    --
    local rulename = sourcebatch.rulename
    if rulename then
        local ruleinst = rule_groups.get_rule(target, rulename)
        if not ruleinst:script("build_file") and
            not ruleinst:script("build_files") then
            return
        end
    end

    -- add batch jobs
    local scriptname = "build_files" .. (suffix and ("_" .. suffix) or "")
    local script = target:script(scriptname)
    if script then
        if target:extraconf(scriptname, "batch") then
            script(target, batchjobs, sourcebatch, {rootjob = rootjob, distcc = target:extraconf(scriptname, "distcc")})
        else
            batchjobs:addjob(target:name() .. "/" .. scriptname, function (index, total, opt)
                script(target, sourcebatch, {progress = opt.progress})
            end, {rootjob = rootjob})
        end
        return true
    else
        scriptname = "build_file" .. (suffix and ("_" .. suffix) or "")
        script = target:script(scriptname)
        if script then
            local sourcekind = sourcebatch.sourcekind
            for _, sourcefile in ipairs(sourcebatch.sourcefiles) do
                batchjobs:addjob(sourcefile, function (index, total, opt)
                    script(target, sourcefile, {sourcekind = sourcekind, progress = opt.progress})
                end, {rootjob = rootjob, distcc = target:extraconf(scriptname, "distcc")})
            end
            return true
        end
    end
end

-- add batch jobs for group
function _add_batchjobs_for_group(batchjobs, rootjob, target, group, suffix)
    for _, item in pairs(group) do
        local sourcebatch = item.sourcebatch
        if item.target then
            _add_batchjobs_for_target(batchjobs, rootjob, target, sourcebatch, suffix)
        end
        -- override on_xxx script in target? we need to ignore rule scripts
        if item.rule and (suffix or not _has_scripts_for_target(target, suffix)) then
            _add_batchjobs_for_rule(batchjobs, rootjob, target, sourcebatch, suffix)
        end
    end
end

-- add batch jobs for building source files
function add_batchjobs_for_sourcefiles(batchjobs, rootjob, target, sourcebatches)

    -- build sourcebatch groups first
    local groups = rule_groups.build_sourcebatch_groups(target, sourcebatches)

    -- add batch jobs for build_after
    local groups_root
    local groups_leaf = rootjob
    for idx, group in ipairs(groups) do
        if _has_scripts_for_group(group, "after") then
            batchjobs:group_enter(target:name() .. "/after_build_files" .. idx)
            _add_batchjobs_for_group(batchjobs, groups_leaf, target, group, "after")
            groups_leaf = batchjobs:group_leave() or groups_leaf
            groups_root = groups_root or groups_leaf
        end
    end

    -- add batch jobs for build
    for idx, group in ipairs(groups) do
        if _has_scripts_for_group(group) then
            batchjobs:group_enter(target:name() .. "/build_files" .. idx)
            _add_batchjobs_for_group(batchjobs, groups_leaf, target, group)
            groups_leaf = batchjobs:group_leave() or groups_leaf
            groups_root = groups_root or groups_leaf
        end
    end

    -- add batch jobs for build_before
    for idx, group in ipairs(groups) do
        if _has_scripts_for_group(group, "before") then
            batchjobs:group_enter(target:name() .. "/before_build_files" .. idx)
            _add_batchjobs_for_group(batchjobs, groups_leaf, target, group, "before")
            groups_leaf = batchjobs:group_leave() or groups_leaf
            groups_root = groups_root or groups_leaf
        end
    end
    return groups_leaf, groups_root or groups_leaf
end

-- add batch jobs for building object files
function add_batchjobs_for_object(batchjobs, rootjob, target)
    return add_batchjobs_for_sourcefiles(batchjobs, rootjob, target, target:sourcebatches())
end

-- add batch jobs for building object target
function main(batchjobs, rootjob, target)

    -- add a fake link job
    local job_link = batchjobs:addjob(target:name() .. "/fakelink", function (index, total, opt)
    end, {rootjob = rootjob})

    -- we only need to return and depend the link job for each target,
    -- so we can compile the source files for each target in parallel
    --
    -- unless call set_policy("build.across_targets_in_parallel", false) to disable to build across targets in parallel.
    --
    local job_objects = add_batchjobs_for_object(batchjobs, job_link, target)
    return target:policy("build.across_targets_in_parallel") == false and job_objects or job_link, job_objects
end
