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

-- imports
import("core.base.json")
import("core.base.semver")
import("core.project.config")
import("lib.detect.find_tool")
import(".support", {inherit = true})

-- get includedirs for stl headers
--
-- $ echo '#include <vector>' | gcc -x c++ -E - | grep '/vector"'
-- # 1 "/usr/include/c++/11/vector" 1 3
-- # 58 "/usr/include/c++/11/vector" 3
-- # 59 "/usr/include/c++/11/vector" 3
--
function _get_toolchain_includedirs_for_stlheaders(includedirs, gcc)
    local tmpfile = os.tmpfile() .. ".cc"
    io.writefile(tmpfile, "#include <vector>")
    local result = try {function () return os.iorunv(gcc, {"-E", "-x", "c++", tmpfile}) end}
    if result then
        for _, line in ipairs(result:split("\n", {plain = true})) do
            local line = line:trim()
            if line:startswith("#") and line:find("/vector\"", 1, true) then
                local includedir = line:match("\"(.+)/vector\"")
                if includedir and os.isdir(includedir) then
                    table.insert(includedirs, path.normalize(includedir))
                    break
                end
            end
        end
    end
    os.tryrm(tmpfile)
end

-- load module support for the current target
function load(target)
    local modulesflag = get_modulesflag(target)
    target:add("cxxflags", modulesflag, {force = true, expand = false})

    -- fix cxxabi issue
    -- @see https://github.com/xmake-io/xmake/issues/2716#issuecomment-1225057760
    -- https://github.com/xmake-io/xmake/issues/3855
    local cxx11abi = target:policy("build.c++.modules.gcc.cxx11abi") or
                     target:policy("build.c++.gcc.modules.cxx11abi")
    if cxx11abi == nil then
        -- enable cxx11abi on GCC >= 15 as it is required for C++23 module
        local gcc_version = get_gcc_version(target)
        if gcc_version and semver.compare(gcc_version, "15") >= 0 then
            cxx11abi = true
        end
    end
    if cxx11abi then
        target:add("cxxflags", "-D_GLIBCXX_USE_CXX11_ABI=1")
    else
        target:add("cxxflags", "-D_GLIBCXX_USE_CXX11_ABI=0")
    end
end

function has_two_phase_compilation_support(_)
    return false
end

-- flags that doesn't affect bmi generation
function strippeable_flags()
    -- speculative list as there is no resource that list flags that prevent reusability, this list will likely be improve over time
    local strippeable_flags = {
        "O",
        "W",
        "w",
        "Q",
        "fmodule-mapper",
        "fmodules-ts",
        "fmodules",
        "fPIC",
        "fsanitize",
        "embed-dir"
    }
    local splitted_strippeable_flags = {
        "I",
        "isystem",
        "cxx-isystem",
        "framework"
    }
    return strippeable_flags, splitted_strippeable_flags
end

-- provide toolchain include directories for stl headerunit when p1689 is not supported
function toolchain_includedirs(target)
    local includedirs = _g.includedirs
    if includedirs == nil then
        includedirs = {}
        local gcc, toolname = target:tool("cxx")
        assert(toolname == "gcc" or toolname == "gxx")
        _get_toolchain_includedirs_for_stlheaders(includedirs, gcc)
        local _, result = try {function () return os.iorunv(gcc, {"-E", "-Wp,-v", "-xc", os.nuldev()}) end}
        if result then
            for _, line in ipairs(result:split("\n", {plain = true})) do
                local line = line:trim()
                if os.isdir(line) then
                    table.insert(includedirs, path.normalize(line))
                elseif line:startswith("End") then
                    break
                end
            end
        end
        _g.includedirs = includedirs
    end
    return includedirs
end

function get_target_module_mapperpath(target)
    local path = path.join(modules_cachedir(target, {mkdir = true}), "..", "mapper.txt")
    if not os.isfile(path) then
        io.writefile(path, "")
    end
    return path
end

function _get_std_module_manifest_path(target)
    local compinst = target:compiler("cxx")
    local modules_json_path, _ = try {
        function()
            return os.iorunv(compinst:program(), {"-print-file-name=libstdc++.modules.json"}, {envs = compinst:runenvs()})
        end
    }
    if modules_json_path then
        modules_json_path = modules_json_path:trim()
        if os.isfile(modules_json_path) then
            return modules_json_path
        end
    end
    local ext = ".so"
    if target:is_plat("windows", "mingw") then
        ext = ".dll"
    elseif target:is_plat("macosx", "iphoneos", "appletvos") then
        ext = ".dylib"
    end
    local stdcpp_libfile, _ = try {
        function()
            return os.iorunv(compinst:program(), {"-print-file-name=libstdc++" .. ext}, {envs = compinst:runenvs()})
        end
    }
    if stdcpp_libfile then
        modules_json_path = path.join(path.directory(stdcpp_libfile), "libstdc++.modules.json")
        modules_json_path = modules_json_path:trim()
        if os.isfile(modules_json_path) then
            return modules_json_path
        end
    end
end

function get_stdmodules(target)
    local modules_json_path = _get_std_module_manifest_path(target)
    if modules_json_path then
        local modules_json = json.loadfile(modules_json_path)
        if modules_json and modules_json.modules and #modules_json.modules > 0 then
            local std_module_files = {}
            local modules_json_dir = path.directory(modules_json_path)
            for _, module_file in ipairs(modules_json.modules) do
                local module_file_path = module_file["source-path"]
                if not path.is_absolute(module_file_path) then
                    module_file_path = path.join(modules_json_dir, module_file_path)
                end
                table.insert(std_module_files, path.normalize(module_file_path))
            end
            return std_module_files
        end
    end
    wprint("std and std.compat modules not found! maybe try to add --sdk=<PATH/TO/LLVM> or install libc++")
end

function get_bmi_extension()
    return ".gcm"
end

function get_modulesflag(target)
    local modulesflag = _g.modulesflag
    if modulesflag == nil then
        local compinst = target:compiler("cxx")
        local gcc_version = get_gcc_version(target)
        -- GCC 12 and earlier version has a option '-fmodules' for Modula-2
        if gcc_version and semver.compare(gcc_version, "12") > 0 then
            if compinst:has_flags("-fmodules", "cxxflags", {flagskey = "gcc_modules"}) then
                modulesflag = "-fmodules"
            elseif compinst:has_flags("-fmodules-ts", "cxxflags", {flagskey = "gcc_modules_ts"}) then
                modulesflag = "-fmodules-ts"
            end
        elseif compinst:has_flags("-fmodules-ts", "cxxflags", {flagskey = "gcc_modules_ts"}) then
            modulesflag = "-fmodules-ts"
        end
        assert(modulesflag, "compiler(gcc): does not support c++ module!")
        _g.modulesflag = modulesflag or false
    end
    return modulesflag or nil
end

function get_moduleheaderflag(target)
    local moduleheaderflag = _g.moduleheaderflag
    if moduleheaderflag == nil then
        -- we need to suppress warnings/errors:
        -- external linkage definition of 'int main(int, char**)' in header module must be declared 'inline'
        local snippet = ""
        local compinst = target:compiler("cxx")
        if compinst:has_flags("-fmodule-header", "cxxflags", {snippet = snippet, flagskey = "gcc_module_header"}) then
            moduleheaderflag = "-fmodule-header="
        end
        _g.moduleheaderflag = moduleheaderflag or false
    end
    return moduleheaderflag or nil
end

function get_moduleonlyflag(target)
    local moduleonlyflag = _g.moduleonlyflag
    if moduleonlyflag == nil then
        local compinst = target:compiler("cxx")
        if compinst:has_flags("-fmodule-only", "cxxflags", {flagskey = "gcc_module_only"}) then
            moduleonlyflag = "-fmodule-only"
        end
        _g.moduleonlyflag = moduleonlyflag or false
    end
    return moduleonlyflag or nil
end

function get_modulemapperflag(target)
    local modulemapperflag = _g.modulemapperflag
    if modulemapperflag == nil then
        local compinst = target:compiler("cxx")
        if compinst:has_flags("-fmodule-mapper=" .. os.tmpfile(), "cxxflags", {flagskey = "gcc_module_mapper"}) then
            modulemapperflag = "-fmodule-mapper="
        end
        assert(modulemapperflag, "compiler(gcc): does not support c++ module!")
        _g.modulemapperflag = modulemapperflag or false
    end
    return modulemapperflag or nil
end

function get_depsflag(target)
    local depflag = _g.depflag
    if depflag == nil then
        local compinst = target:compiler("cxx")
        if compinst:has_flags("-fdeps-format=p1689r5", "cxxflags", {flagskey = "gcc_deps_format",
         on_check = function (ok, errors)
             if errors:find("-M") then
                ok = true
             end
             return ok, errors
        end}) then
            depflag = "-fdeps-format=p1689r5"
        end
        _g.depflag = depflag or false
    end
    return depflag or nil
end

function get_depsfileflag(target)
    local depfileflag = _g.depfileflag
    if depfileflag == nil then
        local compinst = target:compiler("cxx")
        if compinst:has_flags("-fdeps-file=" .. os.tmpfile(), "cxxflags", {flagskey = "gcc_deps_file",
         on_check = function (ok, errors)
             if errors:find("-M") then
                ok = true
             end
             return ok, errors
        end}) then
            depfileflag = "-fdeps-file="
        end
        _g.depfileflag = depfileflag or false
    end
    return depfileflag or nil
end

function get_depstargetflag(target)
    local depoutputflag = _g.depoutputflag
    if depoutputflag == nil then
        local compinst = target:compiler("cxx")
        if compinst:has_flags("-fdeps-target=" .. os.tmpfile() .. ".o", "cxxflags", {flagskey = "gcc_deps_output",
         on_check = function (ok, errors)
             if errors:find("-M") then
                ok = true
             end
             return ok, errors
        end}) then
            depoutputflag = "-fdeps-target="
        end
        _g.depoutputflag = depoutputflag or false
    end
    return depoutputflag or nil
end

function get_cppversionflag(target)
    local cppversionflag = _g.cppversionflag
    if cppversionflag == nil then
        local compinst = target:compiler("cxx")
        local flags = compinst:compflags({target = target})
        local idx = table.find_first_if(flags, function(_, v) return string.startswith(v, "-std=c++") end)
        cppversionflag = (idx and flags[idx]) or "-std=c++20"
        _g.cppversionflag = cppversionflag
    end
    return cppversionflag or nil
end

function get_gcc_version(target)
    local gcc_version = _g.gcc_version
    if not gcc_version then
        local program, toolname = target:tool("cxx")
        if program and (toolname:startswith("gcc") or toolname:startswith("gxx")) then
            local gcc = find_tool(toolname, {program = program, version = true,
                envs = os.getenvs(), cachekey = "modules_support_gcc_" .. toolname})
            if gcc then
                gcc_version = gcc.version
            end
        end
        gcc_version = gcc_version or false
        _g.gcc_version = gcc_version
    end
    return gcc_version or nil
end
