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

-- imports
import("core.base.json")
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("private.tools.vstool")
import("support")
import(".mapper")
import(".builder", {inherit = true})

-- get flags for building a module
function _make_modulebuildflags(target, module, opt)
    opt = opt or {}
    local ifcoutputflag = support.get_ifcoutputflag(target)
    local ifconlyflag = support.get_ifconlyflag(target)
    local interfaceflag = support.get_interfaceflag(target)
    local internalpartitionflag = support.get_internalpartitionflag(target)

    local bmionly = opt.bmi and not opt.objectfile

    local flags
    if module.name then -- named module
        flags = table.join("-TP", module.interface and interfaceflag or internalpartitionflag, bmionly and ifconlyflag or {}, ifcoutputflag, path(module.bmifile))
    else
        flags = {"-TP"}
    end
    return flags
end

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

function _compile_bmi_step(target, module, opt)
    local ifconlyflag = support.get_ifconlyflag(target)
    if not ifconlyflag then
        _compile_one_step(target, module, opt)
    else
        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
end

function _compile_objectfile_step(target, module, opt)
    local flags = _make_modulebuildflags(target, module)
    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, headertype)
    -- get flags
    local exportheaderflag = support.get_exportheaderflag(target)
    local headernameflag = support.get_headernameflag(target)
    local ifcoutputflag = support.get_ifcoutputflag(target)
    local ifconlyflag = support.get_ifconlyflag(target)
    assert(headernameflag and exportheaderflag, "compiler(msvc): does not support c++ header units!")

    local flags = {"-TP",
                   exportheaderflag,
                   ifcoutputflag,
                   headerunit.bmifile,
                   ifconlyflag or {},
                   headernameflag .. headertype} -- keep it at last flag
    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
    local outputfile = opt.headerunit and nil or module.objectfile
    local dryrun = option.get("dry-run")
    local compinst = target:compiler("cxx")
    local compflags = compinst:compflags({sourcefile = sourcefile, target = target, sourcekind = "cxx"})
    flags = table.join(compflags or {}, flags or {})

    -- trace
    local cmd
    if option.get("verbose") then
        if not outputfile then
            cmd = "\n" .. os.args(table.join(compinst:program(), flags, sourcefile))
        else
            cmd = "\n" .. compinst:compcmd(sourcefile, outputfile, {target = target, compflags = flags, sourcekind = "cxx", rawargs = true})
        end
    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 or target:objectfile(sourcefile), {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.headerunit and nil or module.objectfile
    local compinst = target:compiler("cxx")
    local compflags = compinst:compflags({sourcefile = sourcefile, target = target, sourcekind = "cxx"})
    flags = table.join("/c", compflags or {}, flags or {}, outputfile and "-Fo" .. outputfile or {}, sourcefile or {})

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

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

-- get module requires flags
-- 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 _get_requiresflags(target, module)

    local referenceflag = support.get_referenceflag(target)
    local headerunitflag = support.get_headerunitflag(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
        local deps_flags = {}
        for required, dep in pairs(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 <%s>", required, name, target:fullname())

            -- aliased headerunit
            local mapflag
            if dep_module.headerunit then
                local type = dep_module.method == "include-angle" and ":angle" or ":quote"
                mapflag = {headerunitflag .. type}
                table.insert(deps_flags, {headerunitflag, dep_module.sourcefile .. "=" .. dep_module.bmifile})
            else
                mapflag = {referenceflag}
            end
            table.insert(mapflag, dep_module.name .. "=" .. dep_module.bmifile)
            table.insert(deps_flags, mapflag)

            -- append deps
            if dep_module.deps then
                local deps = _get_requiresflags(target, dep_module)
                table.join2(deps_flags, deps)
            end
        end

        -- remove duplicates
        requiresflags = {}
        local contains = {}
        table.sort(deps_flags, function(a, b) return a[2] > b[2] end)
        for _, map in ipairs(deps_flags) do
            local name = map[2]:split("=")[1]
            if name and not contains[name] then
                table.insert(requiresflags, map)
                contains[name] = true
            end
        end
        support.memcache():set2(cachekey, "requiresflags", requiresflags)
        support.memcache():set2(cachekey, "oldrequires", requires)
    end
    return requiresflags
end

function _append_requires_flags(target, module)
    local cxxflags = _get_requiresflags(target, module)
    if #cxxflags > 0 then
        if target:policy("build.c++.modules.hide_dependencies") then
            local _cxxflags = {}
            for _, flag in ipairs(cxxflags) do
                table.insert(_cxxflags, flag[1] .. ' "' .. path.unix(flag[2]) .. '"')
            end
            local requires_flagsfile = target:autogenfile(module.sourcefile .. ".requiresflags.txt")
            io.writefile(requires_flagsfile, 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")

    -- generate and append module mapper file
    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)
            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
    end
end

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

    -- generate and append module mapper file
    local build = should_build(target, module)
    local bmi = opt and opt.bmi
    local objectfile = opt and opt.objectfile

    if build then
        local objectdir = path.directory(module.objectfile)
        batchcmds:mkdir(objectdir)
        if module.bmifile then
            local bmidir = path.directory(module.bmifile)
            batchcmds:mkdir(bmidir)
        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
    end
    batchcmds:add_depfiles(module.sourcefile)
    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
        local headertype = (headerunit.method == "include-angle") and ":angle" or ":quote"
        _compile(target, _make_headerunitflags(target, headerunit, headertype), {sourcefile = (headertype == ":angle") and headerunit.name or headerunit.sourcefile}, 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
        local headertype = (headerunit.method == "include-angle") and ":angle" or ":quote"
        _batchcmds_compile(batchcmds, target, _make_headerunitflags(target, headerunit, headertype), {(headertype == ":angle") and headerunit.name or headerunit.sourcefile}, table.join(opt, {headerunit = true}))
        batchcmds:add_depfiles(headerunit.sourcefile)
    end
    batchcmds:add_depvalues(depvalues)
end
