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

-- imports
import("core.base.json")
import("core.base.option")
import("core.base.bytes")
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})

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

    local headertype = (headerunit.method == "include-angle") and "system" or "user"
    local includedir = headertype == "user" and "-I" .. path.directory(headerunit.sourcefile)
    return table.join(includedir and {includedir} or {},
                      {"-x", "c++-" .. headertype .. "-header",
                      module_mapperflag .. headerunit_mapper,
                     module_headerflag .. headertype})
end

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

    opt = opt or {}
    local sourcefile = opt.headerunit and path.filename(module.sourcefile) or module.sourcefile
    local outputfile = opt.headerunit and module.bmifile 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
        cmd = "\n" .. compinst:compcmd(sourcefile, outputfile, {target = target, compflags = flags, rawargs = true})
    end
    local mapper_str
    if option.get("diagnosis") then
        if opt.headerunit then
            mapper_str = format("\n${dim color.warning}mapper file for %s (%s) --------\n%s\n--------", sourcefile, module.name, io.readfile(opt.mapper_file):trim())
        elseif module.name  then
            mapper_str = format("\n${dim color.warning}mapper file for %s (%s) --------\n%s\n--------", module.name, sourcefile, io.readfile(opt.mapper_file):trim())
        else
            mapper_str = format("\n${dim color.warning}mapper file for %s --------\n%s\n--------", sourcefile, io.readfile(opt.mapper_file):trim())
        end
    end
    show_progress(target, module, table.join(opt, {cmd = cmd, suffix = mapper_str}))

    -- 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)
    local sourcefile = opt.headerunit and path.filename(module.sourcefile) or module.sourcefile
    local outputfile = opt.headerunit and module.bmifile 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 {}, {"-o", outputfile, sourcefile})

    -- trace
    local cmd
    if option.get("verbose") then
        cmd = "\n" .. compinst:compcmd(sourcefile, outputfile, {target = target, compflags = flags, rawargs = true})
    end
    local mapper_str
    if option.get("diagnosis") then
        if opt.headerunit then
            mapper_str = format("\n${dim color.warning}mapper file for %s (%s) --------\n%s\n--------", sourcefile, module.name, io.readfile(opt.mapper_file):trim())
        elseif module.name  then
            mapper_str = format("\n${dim color.warning}mapper file for %s (%s) --------\n%s\n--------", module.name, sourcefile, io.readfile(opt.mapper_file):trim())
        else
            mapper_str = format("\n${dim color.warning}mapper file for %s --------\n%s\n--------", sourcefile, io.readfile(opt.mapper_file):trim())
        end
    end
    show_progress(target, module, table.join(opt, {cmd = cmd, suffix = mapper_str, batchcmds = batchcmds}))

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

function _module_map_cachekey(target)
    local mode = config.mode()
    return target:fullname() .. "module_mapper" .. (mode or "")
end

-- generate a module mapper file for build a headerunit
function _generate_headerunit_modulemapper_file(target, headerunit)
    local mapper_path = _get_modulemapper_file(target, headerunit)
    local mapper_file = io.open(mapper_path, "wb")
    mapper_file:write("root " .. path.directory(headerunit.sourcefile) .. "\n")
    mapper_file:write(mapper_file, path.unix(headerunit.sourcefile) .. " " .. path.unix(path.absolute(headerunit.bmifile)) .. "\n")
    mapper_file:write("\n")
    mapper_file:close()
    return mapper_path
end

function _get_maplines(target, module)

    local maplines = {}
    if module.name then
        table.insert(maplines, module.name .. " " .. path.absolute(module.bmifile))
    end
    for dep_name, dep_module in table.orderpairs(module.deps) do
        local key = dep_name
        if dep_module.headerunit then
            key = dep_name .. dep_module.key
        end
        local dep_module_mapped = mapper.get(target, key)
        assert(dep_module_mapped, "module dependency %s required for %s not found", dep_name, module.name or module.sourcefile)
        local mapline
        local name = dep_name
        if dep_module_mapped.headerunit then
            name = dep_module_mapped.method == "include-angle" and dep_module_mapped.sourcefile or path.join("./", path.directory(module.sourcefile), dep_name)
        end
        mapline = path.unix(name) .. " " .. path.unix(path.absolute(dep_module_mapped.bmifile))
        table.insert(maplines, mapline)

        -- append deps
        if dep_module.deps then
            local deps = _get_maplines(target, {name = dep_module.name, deps = dep_module.deps, sourcefile = dep_module.sourcefile})
            table.join2(maplines, deps)
        end
    end

    -- remove duplicates
    return table.unique(maplines)
end

function _get_modulemapper_file(target, module)
    return path.join(os.tmpdir(), hash.md5(bytes(target:fullname() .. "/" .. module.sourcefile)), path.filename(module.sourcefile) .. ".mapper.txt")
end

-- generate a module mapper file for build a module
-- e.g
-- /usr/include/c++/11/iostream build/.gens/stl_headerunit/linux/x86_64/release/stlmodules/cache/iostream.gcm
-- hello build/.gens/stl_headerunit/linux/x86_64/release/rules/modules/cache/hello.gcm
--
function _generate_modulemapper_file(target, module)
    local maplines = _get_maplines(target, module)
    local mapper_path = _get_modulemapper_file(target, module)
    if os.isfile(mapper_path) then
        os.rm(mapper_path)
    end
    local mapper_content = {}
    table.insert(mapper_content, "root " .. path.unix(os.projectdir()))
    for _, mapline in ipairs(maplines) do
        table.insert(mapper_content, mapline)
    end
    mapper_content = table.concat(mapper_content, "\n") .. "\n"
    if not os.isfile(mapper_path) or io.readfile(mapper_path, {encoding = "binary"}) ~= mapper_content then
        io.writefile(mapper_path, mapper_content, {encoding = "binary"})
    end
    return mapper_path
end

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

    local module_mapperflag = support.get_modulemapperflag(target)
    local module_onlyflag = support.get_moduleonlyflag(target)
    local module_flag = support.get_modulesflag(target)
    local dryrun = option.get("dry-run")

    -- generate and append module mapper file
    local module_mapper
    if module.deps then
        module_mapper = _get_modulemapper_file(target, module)
        target:fileconfig_add(module.sourcefile, {force = {cxxflags = {module_mapperflag .. module_mapper}}})
    end

    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 module.deps then
            _generate_modulemapper_file(target, module)
        end

        local flags = {"-x", "c++"}
        if support.has_module_extension(module.sourcefile) or module.name then
            if bmi then
                if objectfile then
                    table.insert(flags, module_flag)
                else
                    table.insert(flags, module_flag)
                    table.insert(flags, module_onlyflag)
                end
            end
            _compile(target, flags, module, table.join(opt or {}, {mapper_file = module_mapper}))
        else
            os.tryrm(module.objectfile) -- force rebuild for .cpp files
        end
    end
end

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

    local module_mapperflag = support.get_modulemapperflag(target)
    local module_onlyflag = support.get_moduleonlyflag(target)
    local module_flag = support.get_modulesflag(target)

    local module_mapper
    if module.deps then
        module_mapper = _get_modulemapper_file(target, module)
        target:fileconfig_add(module.sourcefile, {force = {cxxflags = {module_mapperflag .. module_mapper}}})
    end

    -- 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 module.deps then
            _generate_modulemapper_file(target, module)
        end

        local flags = {"-x", "c++"}
        if support.has_module_extension(module.sourcefile) or module.name then
            if bmi then
                if objectfile then
                    table.insert(flags, module_flag)
                else
                    table.insert(flags, module_flag)
                    table.insert(flags, module_onlyflag)
                end
            end
            _batchcmds_compile(batchcmds, target, flags, module, table.join(opt, {batchcmds = batchcmds, mapper_file = module_mapper}))
        else
            batchcmds:rm(module.objectfile) -- force rebuild for .cpp files
        end
    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
        local headerunit_mapper = _generate_headerunit_modulemapper_file(target, headerunit)
        _compile(target,
                 _make_headerunitflags(target, headerunit_mapper, headerunit),
                 headerunit, table.join(opt, {mapper_file = headerunit_mapper, 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 headerunit_mapper = _generate_headerunit_modulemapper_file(target, headerunit)
        _batchcmds_compile(batchcmds, target,
                     _make_headerunitflags(target, headerunit_mapper, headerunit),
                     headerunit, table.join(opt, {mapper_file = headerunit_mapper, headerunit = true}))
        batchcmds:add_depfiles(headerunit.sourcefile)
    end
    batchcmds:add_depvalues(depvalues)
end

