--!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, Arthapz
-- @file        clang/builder.lua
--

-- imports
import("core.base.json")
import("core.base.bytes")
import("core.base.option")
import("core.base.semver")
import("utils.progress")
import("private.action.build.object", {alias = "objectbuilder"})
import("core.tool.compiler")
import("core.project.config")
import("core.project.depend")
import("support")
import(".mapper")
import(".builder", {inherit = true})

function _get_bmifile(target, module)
    local has_reduced_bmi = support.get_modulesreducedbmiflag(target)
    local has_two_phases = target:policy("build.c++.modules.two_phases")
    -- disabled with two phases currently, LLVM currently have a bug which prevent to emit reduced bmi when using two phase compilation
    -- will be enabled after the fix
    local add_reduced_flag = not has_two_phases and has_reduced_bmi
    local bmifile = module.bmifile

    if has_two_phases and add_reduced_flag then
        bmifile = path.join(path.directory(module.bmifile), "reduced." .. path.filename(module.bmifile))
    end

    return bmifile, add_reduced_flag
end

function _update_bmihash(target, module)
    local localcache = support.localcache()

    local bmifile = _get_bmifile(target, module)
    local bmihash = hash.xxhash128(bytes(io.readfile(bmifile)))
    local old_bmihash = localcache:get2(bmifile, "hash")

    if not old_bmihash or bmihash ~= old_bmihash then
        localcache:set2(bmifile, "hash", bmihash)
        support.memcache():set2(bmifile, "updated", true)
    end
end

function _make_modulebuildflags(target, module, opt)
    assert(not module.headerunit)

    local modules_reduced_bmi_flag = support.get_modulesreducedbmiflag(target)
    local has_two_phases = target:policy("build.c++.modules.two_phases")
    local flags
    if opt.bmi then
        local module_outputflag = support.get_moduleoutputflag(target)

        flags = {"-x", "c++-module"}

        if not opt.objectfile then
            table.insert(flags, "--precompile")
            if target:has_tool("cxx", "clang_cl") then
                table.join2(flags, "/clang:-o", "/clang:" .. module.bmifile)
            end
        end
        local std = (module.name == "std" or module.name == "std.compat")
        if std then
            table.join2(flags, {"-Wno-include-angled-in-module-purview", "-Wno-reserved-module-identifier", "-Wno-deprecated-declarations"})
        end

        local bmifile, add_reduced_flag = _get_bmifile(target, module)
        if add_reduced_flag then
            table.insert(flags, modules_reduced_bmi_flag)
        end

        if not has_two_phases or add_reduced_flag  then
            table.insert(flags, module_outputflag .. bmifile)
        end
    else
        flags = {}
        if not has_two_phases or not module.bmifile then
            flags = {"-x", "c++"}
        end
        local std = (module.name == "std" or module.name == "std.compat")
        if std then
            table.join2(flags, {"-Wno-include-angled-in-module-purview", "-Wno-reserved-module-identifier", "-Wno-deprecated-declarations"})
        end
    end
    return flags
end

function _compile_one_step(target, module, opt)
    -- get flags
    local module_outputflag = support.get_moduleoutputflag(target)
    if module_outputflag then
        local flags = _make_modulebuildflags(target, module, opt)
        if opt and opt.batchcmds then
            _batchcmds_compile(opt.batchcmds, target, flags, module, opt)
        else
            _compile(target, flags, module, opt)
        end
    else
        _compile_bmi_step(target, module, opt)
        _compile_objectfile_step(target, module, opt)
    end
end

function _compile_bmi_step(target, module, opt)
    local flags = _make_modulebuildflags(target, module, opt)
    if opt and opt.batchcmds then
        _batchcmds_compile(opt.batchcmds, target, flags, module, opt)
    else
        _compile(target, flags, module, opt)
    end
end

function _compile_objectfile_step(target, module, opt)
    local flags = _make_modulebuildflags(target, module, opt)
    if opt and opt.batchcmds then
        _batchcmds_compile(opt.batchcmds, target, flags, module, opt)
    else
        _compile(target, flags, module, opt)
    end
end

-- get flags for building a headerunit
function _make_headerunitflags(target, headerunit)
    local module_headerflag = support.get_moduleheaderflag(target)
    assert(module_headerflag, "compiler(clang): does not support c++ header units!")

    local local_directory = (headerunit.type == ":quote") and {"-I" .. path.directory(headerunit.path)} or {}
    local headertype = (headerunit.method == "include-quote") and "system" or "user"
    local flags = table.join(local_directory, {"-xc++-header", "-Wno-everything", module_headerflag .. headertype})
    return flags
end


function _get_mapper_str(target, module, opt)
    local mapper_str
    if target:policy("build.c++.modules.hide_dependencies") and option.get("diagnosis") then
        if not opt.headerunit then
            local requires_flagsfile = target:autogenfile(module.sourcefile .. ".requiresflags.txt")
            if os.isfile(requires_flagsfile) then
                if module.name then
                    mapper_str = format("\n${dim color.warning}mapper file for %s (%s) --------\n%s\n--------", module.name, module.sourcefile, io.readfile(requires_flagsfile):trim())
                else
                    mapper_str = format("\n${dim color.warning}mapper file for %s --------\n%s\n--------", module.sourcefile, io.readfile(requires_flagsfile):trim())
                end
            end
        end
    end
    return mapper_str
end

-- do compile
function _compile(target, flags, module, opt)

    opt = opt or {}
    local sourcefile = module.sourcefile
    if not opt.bmi and opt.objectfile and module.bmifile then
        sourcefile = module.bmifile
    end
    local outputfile = ((opt.bmi and not opt.objectfile) or opt.headerunit) and module.bmifile or module.objectfile
    local dryrun = option.get("dry-run")
    local compinst = target:compiler("cxx")
    local compflags = compinst:compflags({sourcefile = module.sourcefile, target = target, sourcekind = "cxx"})
    flags = table.join(compflags or {}, flags or {})
    -- trace
    local cmd
    if option.get("verbose") then
        cmd = "\n" .. compinst:compcmd(sourcefile, outputfile, {target = target, compflags = flags, sourcekind = "cxx", rawargs = true})
    end
    show_progress(target, module, table.join(opt, {cmd = cmd, suffix = _get_mapper_str(target, module, opt)}))

    -- do compile
    if not dryrun then
        assert(compinst:compile(sourcefile, outputfile, {target = target, compflags = flags}))
    end
end

-- do compile for batchcmds
-- @note we need to use batchcmds:compilev to translate paths in compflags for generator, e.g. -Ixx
function _batchcmds_compile(batchcmds, target, flags, module, opt)
    opt = opt or {}
    local sourcefile = module.sourcefile
    local outputfile = (opt.bmi and not opt.objectfile) and module.bmifile or module.objectfile
    local compinst = target:compiler("cxx")
    local compflags = compinst:compflags({sourcefile = module.sourcefile, target = target, sourcekind = "cxx"})
    flags = table.join("-c", compflags or {}, flags or {}, {"-o", outputfile, sourcefile})

    -- trace
    local cmd
    if option.get("verbose") then
        cmd = "\n" .. compinst:compcmd(sourcefile, outputfile, {target = target, compflags = flags, sourcekind = "cxx", rawargs = true})
    end
    show_progress(target, module, table.join(opt, {cmd = cmd, batchcmds = batchcmds, suffix = _get_mapper_str(target, module, opt)}))

    -- do compile
    batchcmds:compilev(flags, {compiler = compinst, sourcekind = "cxx", verbose = false})
end

-- get module requires flags
-- e.g
-- -fmodule-file=build/.gens/Foo/rules/modules/cache/foo.pcm
-- -fmodule-file=build/.gens/Foo/rules/modules/cache/iostream.pcm
-- -fmodule-file=build/.gens/Foo/rules/modules/cache/bar.hpp.pcm
-- on LLVM >= 16
-- -fmodule-file=foo=build/.gens/Foo/rules/modules/cache/foo.pcm
-- -fmodule-file=build/.gens/Foo/rules/modules/cache/iostream.pcm
-- -fmodule-file=build/.gens/Foo/rules/modules/cache/bar.hpp.pcm
--
function _get_requiresflags(target, module)

    local modulefileflag = support.get_modulefileflag(target)
    local name = module.name or module.sourcefile
    local cachekey = target:fullname() .. name

    local requires, requires_changed = is_dependencies_changed(target, module)
    local requiresflags = support.memcache():get2(cachekey, "requiresflags")
    if not requiresflags or requires_changed then
        requiresflags = {}
        for required, dep in table.orderpairs(module.deps) do
            local required = required
            if dep.headerunit then
                required = required .. dep.key
            end
            local dep_module = mapper.get(target, required)
            assert(dep_module, "module dependency %s required for %s not found", required, name)

            local dep_bmifile, _ = dep.headerunit and dep_module.bmifile or _get_bmifile(target, dep_module)
            local mapflag = dep_module.headerunit and modulefileflag .. dep_bmifile or format("%s%s=%s", modulefileflag, required, dep_bmifile)
            table.insert(requiresflags, mapflag)

            -- append deps
            if dep_module.deps then
                local deps = _get_requiresflags(target, dep_module)
                table.join2(requiresflags, deps)
            end
        end
        requiresflags = table.unique(requiresflags)
        support.memcache():set2(cachekey, "requiresflags", requiresflags)
        support.memcache():set2(cachekey, "oldrequires", requires)
    end
    return requiresflags
end

function _append_requires_flags(target, module)
    local cxxflags = {}
    local requiresflags = _get_requiresflags(target, module)
    local has_two_phases = target:policy("build.c++.modules.two_phases")
    local hide_dependencies = target:policy("build.c++.modules.hide_dependencies")
    if #requiresflags> 0 then
        for _, flag in ipairs(requiresflags) do
            -- we need to wrap flag to support flag with space
            if type(flag) == "string" and flag:find(" ", 1, true) and not hide_dependencies then
                table.insert(cxxflags, {flag})
            else
                if hide_dependencies then
                    table.insert(cxxflags, '"' .. path.unix(flag) .. '"')
                else
                    table.insert(cxxflags, flag)
                end
            end
        end
        if hide_dependencies then
            local requires_flagsfile = target:autogenfile(module.sourcefile .. ".requiresflags.txt")
            io.writefile(requires_flagsfile, path.unix(table.concat(cxxflags, "\n")))
            target:fileconfig_add(module.sourcefile, {force = {cxxflags = {{"@" .. requires_flagsfile}}}})
        else
            target:fileconfig_add(module.sourcefile, {force = {cxxflags = cxxflags}})
        end
    end
end

function append_requires_flags(target, built_modules)
    -- append requires flags
    for _, sourcefile in ipairs(built_modules) do
        local module = mapper.get(target, sourcefile)
        if module.deps then
            _append_requires_flags(target, module)
        end
    end
end

-- build module file for batchjobs / jobgraph
function make_module_job(target, module, opt)

    local dryrun = option.get("dry-run")
    local enable_hash_comparison = target:policy("build.c++.modules.non_cascading_changes")

    local build, because_of_dependencies = should_build(target, module)
    local bmi = opt and opt.bmi
    local objectfile = opt and opt.objectfile

    if build and enable_hash_comparison and because_of_dependencies then
        build = false
        for dep_name, dep_module in table.orderpairs(module.deps) do
            local mapped_dep = mapper.get(target, dep_module.headerunit and dep_name .. dep_module.key or dep_name)
            if support.memcache():get2(_get_bmifile(target, dep_module), "updated") then
                build = true
                break
            end
        end
    end

    if build then
        if not dryrun then
            local objectdir = path.directory(module.objectfile)
            if not os.isdir(objectdir) then
                os.mkdir(objectdir)
            end
            if module.bmifile then
                local bmidir = path.directory(module.bmifile)
                if not os.isdir(bmidir) then
                    os.mkdir(bmidir)
                end
            end
        end

        if bmi and objectfile then
            _compile_one_step(target, module, opt)
        elseif bmi then
            _compile_bmi_step(target, module, opt)
        else
            if support.has_module_extension(module.sourcefile) or module.name then
                _compile_objectfile_step(target, module, opt)
            else
                os.tryrm(module.objectfile) -- force rebuild for .cpp files
            end
        end

        if enable_hash_comparison and bmi then
            _update_bmihash(target, module)
        end
    end
end

-- build module file for batchcmds
function make_module_buildcmds(target, batchcmds, module, opt)

    local build = should_build(target, module)
    local bmi = opt and opt.bmi
    local objectfile = opt and opt.objectfile

    if build then
        if not dryrun then
            local objectdir = path.directory(module.objectfile)
            batchcmds:mkdir(objectdir)
            if module.bmifile then
                local bmidir = path.directory(module.bmifile)
                batchcmds:mkdir(bmidir)
            end
        end

        if bmi and objectfile then
            _compile_one_step(target, module, table.join(opt, {batchcmds = batchcmds}))
        elseif bmi then
            _compile_bmi_step(target, module, table.join(opt, {batchcmds = batchcmds}))
        else
            if support.has_module_extension(module.sourcefile) or module.name then
                _compile_objectfile_step(target, module, table.join(opt, {batchcmds = batchcmds}))
            else
                batchcmds:rm(module.objectfile) -- force rebuild for .cpp files
            end
        end
        batchcmds:add_depfiles(module.sourcefile)
    end
    return os.mtime(module.objectfile)
end

-- build headerunit file for batchjobs / jobgraph
function make_headerunit_job(target, headerunit, opt)
    local build = should_build(target, headerunit)
    if build then
        _compile(target, _make_headerunitflags(target, headerunit), headerunit, table.join(opt, {headerunit = true}))
    end
end

-- build headerunit file for batchcmds
function make_headerunit_buildcmds(target, batchcmds, headerunit, opt)
    local compinst = compiler.load("cxx", {target = target})
    local compflags = compinst:compflags({sourcefile = headerunit.sourcefile, target = target, sourcekind = "cxx"})
    local depvalues = {compinst:program(), compflags}

    local build = should_build(target, headerunit)
    if build then
        _batchcmds_compile(batchcmds, target, table.join(_make_headerunitflags(target, headerunit)), headerunit, table.join(opt, {headerunit = true}))
        batchcmds:add_depfiles(headerunit.sourcefile)
    end
    batchcmds:add_depvalues(depvalues)
end

function get_requires(target, module)
    local _requires
    local flags = _get_requiresflags(target, module)
    for _, flag in ipairs(flags) do
        _requires = _requires or {}
        table.insert(_requires, flag:split("=")[3])
    end
    return _requires
end
