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

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

-- add a module or header unit into the mapper
--
-- e.g
-- /reference Foo=build/.gens/Foo/rules/modules/cache/Foo.ifc
-- /headerUnit:angle glm/mat4x4.hpp=Users\arthu\AppData\Local\.xmake\packages\g\glm\0.9.9+8\91454f3ee0be416cb9c7452970a2300f\include\glm\mat4x4.hpp.ifc
--
function _add_module_to_mapper(target, argument, namekey, path, objectfile, bmifile, deps)
    local modulemap = _get_modulemap_from_mapper(target)
    if modulemap[namekey] then
        return
    end
    local mapflag = {argument, path .. "=" .. bmifile}
    modulemap[namekey] = {flag = mapflag, objectfile = objectfile, deps = deps}
    common.localcache():set2(_mapper_cachekey(target), "modulemap", modulemap)
end

function _mapper_cachekey(target)
    local mode = config.mode()
    return target:name() .. "_modulemap_" .. (mode or "")
end

-- flush mapper file cache
function _flush_mapper(target)
    -- not using set2/get2 to flush only current target mapper
    common.localcache():save(_mapper_cachekey(target))
end

-- get modulemap from mapper
function _get_modulemap_from_mapper(target)
    return common.localcache():get2(_mapper_cachekey(target), "modulemap") or {}
end

-- do compile
function _compile(target, flags, sourcefile)
    local compinst = target:compiler("cxx")
    local msvc = target:toolchain("msvc")
    local compflags = compinst:compflags({sourcefile = sourcefile, target = target})
    os.vrunv(compinst:program(), winos.cmdargv(table.join(compflags or {}, flags)), {envs = msvc:runenvs()})
end

-- do compile for batchcmds
-- @note we need use batchcmds:compilev to translate paths in compflags for generator, e.g. -Ixx
function _batchcmds_compile(batchcmds, target, flags, sourcefile)
    local compinst = target:compiler("cxx")
    local compflags = compinst:compflags({sourcefile = sourcefile, target = target})
    batchcmds:compilev(table.join(compflags or {}, flags), {compiler = compinst, sourcekind = "cxx"})
end

-- add an objectfile to the linker flags
--
-- e.g
-- foo.obj
--
function _add_objectfile_to_link_arguments(target, objectfile)
    local cachekey = target:name() .. "dependency_objectfiles"
    local cache = common.localcache():get(cachekey) or {}
    if table.contains(cache, objectfile) then
        return
    end
    table.insert(cache, objectfile)
    common.localcache():set(cachekey, cache)
    common.localcache():save(cachekey)
end

-- build module file
function _build_modulefile(target, sourcefile, opt)
    local objectfile = opt.objectfile
    local dependfile = opt.dependfile
    local compinst = compiler.load("cxx", {target = target})
    local compflags = compinst:compflags({sourcefile = sourcefile, target = target})
    local dependinfo = option.get("rebuild") and {} or (depend.load(dependfile) or {})

    -- need build this object?
    local dryrun = option.get("dry-run")
    local depvalues = {compinst:program(), compflags}
    local lastmtime = os.isfile(objectfile) and os.mtime(dependfile) or 0
    if not dryrun and not depend.is_changed(dependinfo, {lastmtime = lastmtime, values = depvalues}) then
        return
    end

    -- init flags
    local flags = table.join("-TP", compflags, opt.flags or {})

    -- trace
    progress.show(opt.progress, "${color.build.object}compiling.module.$(mode) %s", opt.name)
    vprint(compinst:compcmd(sourcefile, objectfile, {compflags = flags, rawargs = true}))

    if not dryrun then

        -- do compile
        dependinfo.files = {}
        assert(compinst:compile(sourcefile, objectfile, {dependinfo = dependinfo, compflags = flags}))

        -- update files and values to the dependent file
        dependinfo.values = depvalues
        table.join2(dependinfo.files, sourcefile)
        depend.save(dependinfo, dependfile)
    end
end

-- load module support for the current target
function load(target)

    -- add modules flags
    local modulesflag = get_modulesflag(target)
    target:add("cxxflags", modulesflag)

    -- enable std modules if c++23 by defaults
    if target:data("c++.msvc.enable_std_import") == nil then
        local languages = target:get("languages")
        local isatleastcpp23 = false
        for _, language in ipairs(languages) do
            if language:startswith("c++") or language:startswith("cxx") then
                isatleastcpp23 = true
                local version = tonumber(language:match("%d+"))
                if (not version or version <= 20) and not language:match("latest") then
                    isatleastcpp23 = false
                    break
                end
            end
        end
        local stdmodulesdir
        local msvc = target:toolchain("msvc")
        if msvc then
            local vcvars = msvc:config("vcvars")
            if vcvars.VCInstallDir and vcvars.VCToolsVersion and semver.compare(vcvars.VCToolsVersion, "14.35") then
                stdmodulesdir = path.join(vcvars.VCInstallDir, "Tools", "MSVC", vcvars.VCToolsVersion, "modules")
            end
        end
        target:data_set("c++.msvc.enable_std_import", isatleastcpp23 and os.isdir(stdmodulesdir))
    end
end

-- provide toolchain include dir for stl headerunit when p1689 is not supported
function toolchain_includedirs(target)
    for _, toolchain_inst in ipairs(target:toolchains()) do
        if toolchain_inst:name() == "msvc" then
            local vcvars = toolchain_inst:config("vcvars")
            if vcvars.VCInstallDir and vcvars.VCToolsVersion then
                return { path.join(vcvars.VCInstallDir, "Tools", "MSVC", vcvars.VCToolsVersion, "include") }
            end
            break
        end
    end
    raise("msvc toolchain includedirs not found!")
end

-- generate dependency files
function generate_dependencies(target, sourcebatch, opt)
    local msvc = target:toolchain("msvc")
    local scandependenciesflag = get_scandependenciesflag(target)
    local ifcoutputflag = get_ifcoutputflag(target)
    local common_flags = {"-TP", scandependenciesflag}
    local changed = false
    for _, sourcefile in ipairs(sourcebatch.sourcefiles) do
        local dependfile = target:dependfile(sourcefile)
        depend.on_changed(function ()
            if opt.progress then
                progress.show(opt.progress, "${color.build.object}generating.module.deps %s", sourcefile)
            end
            local outputdir = common.get_outputdir(target, sourcefile)

            local jsonfile = path.join(outputdir, path.filename(sourcefile) .. ".module.json")
            if scandependenciesflag and not target:policy("build.c++.msvc.fallbackscanner") then
                local flags = {jsonfile, sourcefile, ifcoutputflag, outputdir, "-Fo" .. target:objectfile(sourcefile)}
                _compile(target, table.join(common_flags, flags), sourcefile)
            else
                common.fallback_generate_dependencies(target, jsonfile, sourcefile, function(file)
                    local compinst = target:compiler("cxx")
                    local compflags = compinst:compflags({sourcefile = file, target = target})
                    local ifile = path.translate(path.join(outputdir, path.filename(file) .. ".i"))
                    os.vrunv(compinst:program(), table.join(compflags,
                        {"/P", "-TP", file,  "/Fi" .. ifile}), {envs = msvc:runenvs()})
                    local content = io.readfile(ifile)
                    os.rm(ifile)
                    return content
                end)
            end
            changed = true

            local dependinfo = io.readfile(jsonfile)
            return { moduleinfo = dependinfo }
        end, {dependfile = dependfile, files = {sourcefile}})
    end
    return changed
end

-- generate header unit module bmi for batchjobs
function generate_headerunit_for_batchjob(target, name, flags, objectfile, index, total)
    -- don't generate same header unit bmi at the same time across targets
    if not common.memcache():get2(name, "generating") then
        local common_flags = {"-TP", "-c"}
        common.memcache():set2(name, "generating", true)
        progress.show((index * 100) / total, "${color.build.object}compiling.headerunit.$(mode) %s", name)
        _compile(target, table.join(common_flags, flags))
        _add_objectfile_to_link_arguments(target, objectfile)
    end
end

-- generate header unit module bmi for batchcmds
function generate_headerunit_for_batchcmds(target, name, flags, objectfile, batchcmds, opt)
    local common_flags = {"-TP", "-c"}
    batchcmds:show_progress(opt.progress, "${color.build.object}compiling.headerunit.$(mode) %s", name)
    _batchcmds_compile(batchcmds, target, table.join(common_flags, flags))
    _add_objectfile_to_link_arguments(target, objectfile)
end

-- generate target stl header unit modules for batchjobs
function generate_stl_headerunits_for_batchjobs(target, batchjobs, headerunits, opt)
    local stlcachedir = common.stlmodules_cachedir(target, {mkdir = true})

    -- get flags
    local exportheaderflag = get_exportheaderflag(target)
    local headerunitflag = get_headerunitflag(target)
    local headernameflag = get_headernameflag(target)
    local ifcoutputflag = get_ifcoutputflag(target)
    assert(headerunitflag and headernameflag and exportheaderflag, "compiler(msvc): does not support c++ header units!")

    -- flush job
    local flushjob = batchjobs:addjob(target:name() .. "_stl_headerunits_flush_mapper", function(index, total)
        _flush_mapper(target)
    end, {rootjob = opt.rootjob})

    -- build headerunits
    for _, headerunit in ipairs(headerunits) do
        local bmifile = path.join(stlcachedir, headerunit.name .. get_bmi_extension())
        local objectfile = bmifile .. ".obj"
        batchjobs:addjob(headerunit.name, function(index, total)
            depend.on_changed(function()
                local flags = {
                    exportheaderflag,
                    headernameflag .. ":angle",
                    headerunit.name,
                    ifcoutputflag,
                    headerunit.name:startswith("experimental/") and path.join(stlcachedir, "experimental") or stlcachedir,
                    "-Fo" .. objectfile
                }
                generate_headerunit_for_batchjob(target, headerunit.name, flags, objectfile, index, total)

            end, {dependfile = target:dependfile(bmifile), files = {headerunit.path}})
            _add_module_to_mapper(target, headerunitflag .. ":angle", headerunit.name, headerunit.name, objectfile, bmifile)
        end, {rootjob = flushjob})
    end
end

-- generate target stl header units for batchcmds
function generate_stl_headerunits_for_batchcmds(target, batchcmds, headerunits, opt)
    local stlcachedir = common.stlmodules_cachedir(target, {mkdir = true})
    local exportheaderflag = get_exportheaderflag(target)
    local headerunitflag = get_headerunitflag(target)
    local headernameflag = get_headernameflag(target)
    local ifcoutputflag = get_ifcoutputflag(target)
    assert(headerunitflag and headernameflag and exportheaderflag, "compiler(msvc): does not support c++ header units!")

    -- build headerunits
    local depmtime = 0
    for _, headerunit in ipairs(headerunits) do
        local bmifile = path.join(stlcachedir, headerunit.name .. get_bmi_extension())
        local objectfile = bmifile .. ".obj"
        local flags = {
            exportheaderflag,
            headernameflag .. ":angle",
            headerunit.name,
            ifcoutputflag,
            path(headerunit.name:startswith("experimental/") and path.join(stlcachedir, "experimental") or stlcachedir),
            path(objectfile, function (p) return "-Fo" .. p end)}
        generate_headerunit_for_batchcmds(target, headerunit.name, flags, objectfile, batchcmds, opt)
        batchcmds:add_depfiles(headerunit.path)
        _add_module_to_mapper(target, headerunitflag .. ":angle", headerunit.name, headerunit.name, objectfile, bmifile)
        depmtime = math.max(depmtime, os.mtime(bmifile))
    end
    batchcmds:set_depmtime(depmtime)
    _flush_mapper(target)
end

-- generate target user header units for batchcmds
function generate_user_headerunits_for_batchjobs(target, batchjobs, headerunits, opt)
    -- get flags
    local exportheaderflag = get_exportheaderflag(target)
    local headerunitflag = get_headerunitflag(target)
    local headernameflag = get_headernameflag(target)
    local ifcoutputflag = get_ifcoutputflag(target)
    assert(headerunitflag and headernameflag and exportheaderflag, "compiler(msvc): does not support c++ header units!")

    -- flush job
    local flushjob = batchjobs:addjob(target:name() .. "_user_headerunits_flush_mapper", function(index, total)
        _flush_mapper(target)
    end, {rootjob = opt.rootjob})

    -- build headerunits
    for _, headerunit in ipairs(headerunits) do
        local file = path.relative(headerunit.path, target:scriptdir())
        local objectfile = target:objectfile(file)
        local outputdir = common.get_outputdir(target, headerunit)
        local bmifilename = path.basename(objectfile) .. get_bmi_extension()
        local bmifile = path.join(outputdir, bmifilename)
        batchjobs:addjob(headerunit.name, function (index, total)
            depend.on_changed(function()
                local objectdir = path.directory(objectfile)
                if not os.isdir(objectdir) then
                    os.mkdir(objectdir)
                end
                if not os.isdir(outputdir) then
                    os.mkdir(outputdir)
                end

                -- generate headerunit
                local flags = {
                    exportheaderflag,
                    headernameflag .. headerunit.type,
                    headerunit.path,
                    ifcoutputflag,
                    outputdir,
                    "/Fo" .. objectfile
                }
                generate_headerunit_for_batchjob(target, headerunit.unique and path.filename(headerunit.name) or headerunit.name, flags, objectfile, index, total)

            end, {dependfile = target:dependfile(bmifile), files = {headerunit.path}})
            _add_module_to_mapper(target, headerunitflag .. headerunit.type, headerunit.name, headerunit.type == ":quote" and headerunit.path or headerunit.name, objectfile,  bmifile)
        end, {rootjob = flushjob})
    end
end

-- generate target user header units for batchcmds
function generate_user_headerunits_for_batchcmds(target, batchcmds, headerunits, opt)
    local exportheaderflag = get_exportheaderflag(target)
    local headerunitflag = get_headerunitflag(target)
    local headernameflag = get_headernameflag(target)
    local ifcoutputflag = get_ifcoutputflag(target)
    assert(headerunitflag and headernameflag and exportheaderflag, "compiler(msvc): does not support c++ header units!")

    -- build headerunits
    local depmtime = 0
    for _, headerunit in ipairs(headerunits) do
        local file = path.relative(headerunit.path, target:scriptdir())
        local objectfile = target:objectfile(file)
        local outputdir = common.get_outputdir(target, headerunit)
        batchcmds:mkdir(outputdir)

        local bmifilename = path.basename(objectfile) .. get_bmi_extension()
        local bmifile = path.join(outputdir, bmifilename)
        batchcmds:mkdir(path.directory(objectfile))

        local flags = {
            exportheaderflag,
            headernameflag .. headerunit.type,
            headerunit.path,
            ifcoutputflag,
            outputdir,
            "/Fo" .. objectfile
        }
        generate_headerunit_for_batchcmds(target, headerunit.unique and path.filename(headerunit.name) or headerunit.name, flags, objectfile, batchcmds, opt)
        batchcmds:add_depfiles(headerunit.path)

        _add_module_to_mapper(target, headerunitflag .. headerunit.type, headerunit.name, headerunit.type == ":quote" and headerunit.path or headerunit.name, objectfile,  bmifile)

        depmtime = math.max(depmtime, os.mtime(bmifile))
    end
    batchcmds:set_depmtime(depmtime)
    _flush_mapper(target)
end

-- build module files for batchjobs
function build_modules_for_batchjobs(target, batchjobs, objectfiles, modules, opt)

    -- get flags
    local ifcoutputflag = get_ifcoutputflag(target)
    local interfaceflag = get_interfaceflag(target)
    local referenceflag = get_referenceflag(target)
    local internalpartitionflag = get_internalpartitionflag(target)

    -- flush job
    local flushjob = batchjobs:addjob(target:name() .. "_modules", function(index, total)
        _flush_mapper(target)
    end, {rootjob = opt.rootjob})

    if target:data("c++.msvc.enable_std_import") then
        for objectfile, module in pairs(get_stdmodules(target)) do
            table.insert(objectfiles, objectfile)
            modules[objectfile] = module
            modules[objectfile].external = true
        end
    end

    local modulesjobs = {}
    for _, objectfile in ipairs(objectfiles) do
        local module = modules[objectfile]
        if module then
            local cppfile = module.cppfile
            local name, provide
            if module.provides then
                -- assume there that provides is only one, until we encounter the case
                local length = 0
                for k, v in pairs(module.provides) do
                    length = length + 1
                    name = k
                    provide = v
                    cppfile = provide.sourcefile
                    if length > 1 then
                        raise("multiple provides are not supported now!")
                    end
                    break
                end
            end
            local moduleinfo = table.copy(provide) or {}
            local flags = {"-TP"}
            local dependfile = target:dependfile(objectfile)

            if provide then
                table.join2(flags, {ifcoutputflag, path(provide.bmi), provide.interface and interfaceflag or internalpartitionflag})
                dependfile = target:dependfile(provide.bmi)

                local fileconfig = target:fileconfig(cppfile)
                if fileconfig and fileconfig.install then
                    batchjobs:addjob(name .. "_metafile", function(index, total)
                        local outputdir = common.get_outputdir(target, cppfile)
                        local metafilepath = path.join(outputdir, path.filename(cppfile) .. ".meta-info")
                        depend.on_changed(function()
                            progress.show((index * 100) / total, "${color.build.object}generating.module.metadata %s", name)
                            local metadata = common.generate_meta_module_info(target, name, cppfile, module.requires)
                            json.savefile(metafilepath, metadata)

                        end, {dependfile = target:dependfile(metafilepath), files = {cppfile}})

                    end, {rootjob = flushjob})
                end
            end

            table.join2(moduleinfo, {
                name = name or cppfile,
                deps = table.keys(module.requires or {}),
                sourcefile = cppfile,
                job = batchjobs:newjob(name or cppfile, function(index, total)
                    -- append module mapper flags first
                    -- @note we add it at the end to ensure that the full modulemap are already stored in the mapper
                    local requiresflags
                    if module.requires then
                        requiresflags = get_requiresflags(target, module.requires, {expand = true})
                    end
                    local _flags = table.join(flags, requiresflags or {})

                    if provide or common.has_module_extension(cppfile) then
                        if not common.memcache():get2(name or cppfile, "compiling") then
                            if name and module.external then
                                common.memcache():set2(name or cppfile, "compiling", true)
                            end
                            _build_modulefile(target, cppfile, {
                                objectfile = objectfile,
                                dependfile = dependfile,
                                name = name or module.cppfile,
                                flags = _flags,
                                progress = (index * 100) / total})
                        end
                        _add_objectfile_to_link_arguments(target, objectfile)
                    elseif requiresflags then
                        requiresflags = get_requiresflags(target, module.requires)
                        target:fileconfig_add(cppfile, {force = {cxxflags = table.join(flags, requiresflags)}})
                    end

                    if provide then
                        _add_module_to_mapper(target, referenceflag, name, name, objectfile, provide.bmi, requiresflags)
                    end
                end)})
            modulesjobs[name or cppfile] = moduleinfo
        end
    end

    -- build batchjobs for modules
    common.build_batchjobs_for_modules(modulesjobs, batchjobs, flushjob)
end

-- build module files for batchcmds
function build_modules_for_batchcmds(target, batchcmds, objectfiles, modules, opt)

    -- get flags
    local ifcoutputflag = get_ifcoutputflag(target)
    local interfaceflag = get_interfaceflag(target)
    local referenceflag = get_referenceflag(target)
    local internalpartitionflag = get_internalpartitionflag(target)

    if target:data("c++.msvc.enable_std_import") then
        for objectfile, module in pairs(get_stdmodules(target)) do
            table.insert(objectfiles, objectfile)
            modules[objectfile] = module
        end
    end

    -- build modules
    local depmtime = 0
    for _, objectfile in ipairs(objectfiles) do
        local module = modules[objectfile]
        if module then
            local cppfile = module.cppfile
            local name, provide
            if module.provides then
                local length = 0
                for k, v in pairs(module.provides) do
                    length = length + 1
                    name = k
                    provide = v
                    cppfile = provide.sourcefile
                    if length > 1 then
                        raise("multiple provides are not supported now!")
                    end
                    break
                end
            end
            -- append required modulemap flags to module
            local requiresflags
            if module.requires then
                requiresflags = get_requiresflags(target, module.requires, {expand = true})
            end

            local flags = table.join({"-TP", "-c", path(cppfile), path(objectfile, function (p) return "-Fo" .. p end)})
            if provide or common.has_module_extension(cppfile) then
                if provide then
                    table.join2(flags, {ifcoutputflag, path(provide.bmi), provide.interface and interfaceflag or internalpartitionflag})
                end

                batchcmds:show_progress(opt.progress, "${color.build.object}compiling.module.$(mode) %s", name or cppfile)
                batchcmds:mkdir(path.directory(objectfile))
                _batchcmds_compile(batchcmds, target, table.join(flags, requiresflags or {}), cppfile)
                batchcmds:add_depfiles(cppfile)
                _add_objectfile_to_link_arguments(target, path.translate(objectfile))
                if provide then
                    _add_module_to_mapper(target, referenceflag, name, name, objectfile, provide.bmi, requiresflags)
                end
                depmtime = math.max(depmtime, os.mtime(provide and provide.bmi or objectfile))
            elseif requiresflags then
                requiresflags = get_requiresflags(target, module.requires)
                target:fileconfig_add(cppfile, {force = {cxxflags = table.join(flags, requiresflags)}})
            end
        end
    end

    batchcmds:set_depmtime(depmtime)
    _flush_mapper(target)
end

function get_stdmodules(target)
    local modules = {}

    -- build c++23 standard modules if needed
    if target:data("c++.msvc.enable_std_import") then
        local msvc = target:toolchain("msvc")
        if msvc then
            local vcvars = msvc:config("vcvars")
            if vcvars.VCInstallDir and vcvars.VCToolsVersion then
                local stdmodulesdir = path.join(vcvars.VCInstallDir, "Tools", "MSVC", vcvars.VCToolsVersion, "modules")
                assert(stdmodulesdir, "Can't enable C++23 std modules, directory missing !")

                local stlcachedir = common.stlmodules_cachedir(target)
                local modulefile = path.join(stdmodulesdir, "std.ixx")
                local bmifile = path.join(stlcachedir, "std.ixx" .. get_bmi_extension())
                local objfile = bmifile .. ".obj"
                modules[objfile] = {provides = {std = {interface = true, sourcefile = modulefile, bmi = bmifile}}}
                stlcachedir = common.stlmodules_cachedir(target)
                modulefile = path.join(stdmodulesdir, "std.compat.ixx")
                bmifile = path.join(stlcachedir, "std.compat.ixx" .. get_bmi_extension())
                objfile = bmifile .. ".obj"
                modules[objfile] = {provides = {["std.compat"] = {interface = true, sourcefile = modulefile, bmi = bmifile}}, requires = {std = {unique = false, method = "by-name"}}}
            end
        end
    end
    return modules
end

function get_bmi_extension()
    return ".ifc"
end

function get_modulesflag(target)
    local modulesflag = _g.modulesflag
    if modulesflag == nil then
        local compinst = target:compiler("cxx")
        if compinst:has_flags("-experimental:module", "cxxflags", {flagskey = "cl_experimental_module"}) then
            modulesflag = "-experimental:module"
        end
        assert(modulesflag, "compiler(msvc): does not support c++ module!")
        _g.modulesflag = modulesflag or false
    end
    return modulesflag or nil
end

function get_ifcoutputflag(target)
    local ifcoutputflag = _g.ifcoutputflag
    if ifcoutputflag == nil then
        local compinst = target:compiler("cxx")
        if compinst:has_flags({"-ifcOutput", os.tmpfile()}, "cxxflags", {flagskey = "cl_ifc_output"})  then
            ifcoutputflag = "-ifcOutput"
        end
        assert(ifcoutputflag, "compiler(msvc): does not support c++ module flag(/ifcOutput)!")
        _g.ifcoutputflag = ifcoutputflag or false
    end
    return ifcoutputflag or nil
end

function get_ifcsearchdirflag(target)
    local ifcsearchdirflag = _g.ifcsearchdirflag
    if ifcsearchdirflag == nil then
        local compinst = target:compiler("cxx")
        if compinst:has_flags({"-ifcSearchDir", os.tmpdir()}, "cxxflags", {flagskey = "cl_ifc_search_dir"})  then
            ifcsearchdirflag = "-ifcSearchDir"
        end
        assert(ifcsearchdirflag, "compiler(msvc): does not support c++ module flag(/ifcSearchDir)!")
        _g.ifcsearchdirflag = ifcsearchdirflag or false
    end
    return ifcsearchdirflag or nil
end

function get_interfaceflag(target)
    local interfaceflag = _g.interfaceflag
    if interfaceflag == nil then
        local compinst = target:compiler("cxx")
        if compinst:has_flags("-interface", "cxxflags", {flagskey = "cl_interface"}) then
            interfaceflag = "-interface"
        end
        assert(interfaceflag, "compiler(msvc): does not support c++ module flag(/interface)!")
        _g.interfaceflag = interfaceflag or false
    end
    return interfaceflag
end

function get_referenceflag(target)
    local referenceflag = _g.referenceflag
    if referenceflag == nil then
        local compinst = target:compiler("cxx")
        if compinst:has_flags({"-reference", "Foo=" .. os.tmpfile()}, "cxxflags", {flagskey = "cl_reference"}) then
            referenceflag = "-reference"
        end
        assert(referenceflag, "compiler(msvc): does not support c++ module flag(/reference)!")
        _g.referenceflag = referenceflag or false
    end
    return referenceflag or nil
end

function get_headernameflag(target)
    local headernameflag = _g.headernameflag
    if headernameflag == nil then
        local compinst = target:compiler("cxx")
        if compinst:has_flags({"-std:c++latest", "-exportHeader", "-headerName:quote"}, "cxxflags", {flagskey = "cl_header_name_quote"}) and
        compinst:has_flags({"-std:c++latest", "-exportHeader", "-headerName:angle"}, "cxxflags", {flagskey = "cl_header_name_angle"}) then
            headernameflag = "-headerName"
        end
        _g.headernameflag = headernameflag or false
    end
    return headernameflag or nil
end

function get_headerunitflag(target)
    local headerunitflag = _g.headerunitflag
    if headerunitflag == nil then
        local compinst = target:compiler("cxx")
        local ifcfile = os.tmpfile()
        if compinst:has_flags({"-std:c++latest", "-headerUnit:quote", "foo.h=" .. ifcfile}, "cxxflags", {flagskey = "cl_header_unit_quote"}) and
        compinst:has_flags({"-std:c++latest", "-headerUnit:angle", "foo.h=" .. ifcfile}, "cxxflags", {flagskey = "cl_header_unit_angle"}) then
            headerunitflag = "-headerUnit"
        end
        _g.headerunitflag = headerunitflag or false
    end
    return headerunitflag or nil
end

function get_exportheaderflag(target)
    local exportheaderflag = _g.exportheaderflag
    if exportheaderflag == nil then
        if get_headernameflag(target) then
            exportheaderflag = "-exportHeader"
        end
        _g.exportheaderflag = exportheaderflag or false
    end
    return exportheaderflag or nil
end

function get_scandependenciesflag(target)
    local scandependenciesflag = _g.scandependenciesflag
    if scandependenciesflag == nil then
        local compinst = target:compiler("cxx")
        local scan_dependencies_jsonfile = os.tmpfile() .. ".json"
        if compinst:has_flags("-scanDependencies " .. scan_dependencies_jsonfile, "cxflags", {flagskey = "cl_scan_dependencies",
            on_check = function (ok, errors)
                if os.isfile(scan_dependencies_jsonfile) then
                    ok = true
                end
                if ok and not os.isfile(scan_dependencies_jsonfile) then
                    ok = false
                end
                return ok, errors
            end}) then
            scandependenciesflag = "-scanDependencies"
        end
        _g.scandependenciesflag = scandependenciesflag or false
    end
    return scandependenciesflag or nil
end

-- get requireflags from module mapper
function get_requiresflags(target, requires, opt)
    opt = opt or {}
    local flags = {}
    local modulemap = _get_modulemap_from_mapper(target)
    -- add deps required module flags
    local already_mapped_modules = {}
    for name, _ in table.orderpairs(requires) do
        -- if already in flags, continue
        if already_mapped_modules[name] then
            goto continue
        end

        for _, dep in ipairs(target:orderdeps()) do
            local modulemap_ = _get_modulemap_from_mapper(dep)
            if modulemap_[name] then
                table.join2(flags, modulemap_[name].flag)
                table.join2(flags, modulemap_[name].deps or {})
                already_mapped_modules[name] = true
                goto continue
            end
        end

        -- append target required module mapper flags
        if modulemap[name] then
            table.join2(flags, modulemap[name].flag)
            table.join2(flags, modulemap[name].deps or {})
            goto continue
        end

        ::continue::
    end
    local requireflags = {}
    local contains = {}
    for i = 1, #flags, 2 do
        local value = flags[i + 1]
        if not contains[value] then
            local key = flags[i]
            if opt.expand then
                table.insert(requireflags, key)
                table.insert(requireflags, value)
            else
                table.insert(requireflags, {key, value})
            end
            contains[value] = true
        end
    end
    if #requireflags > 0 then
        return requireflags
    end
end

function get_cppversionflag(target)
    local cppversionflag = _g.cppversionflag
    if cppversionflag == nil then
        local compinst = target:compiler("cxx")
        local flags = compinst:compflags({target = target})
        cppversionflag = table.find_if(flags, function(v) string.startswith(v, "/std:c++") end) or "/std:c++latest"
    end
    return cppversionflag or nil
end

function get_internalpartitionflag(target)
    local internalpartitionflag = _g.internalpartitionflag
    if internalpartitionflag == nil then
        local compinst = target:compiler("cxx")
        if compinst:has_flags("-internalPartition", "cxxflags", {flagskey = "cl_internal_partition"}) then
            internalpartitionflag = "-internalPartition"
        end
        _g.internalpartitionflag = internalpartitionflag or false
    end
    return internalpartitionflag or nil
end
