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

-- imports
import("core.base.bytes")
import("core.base.option")
import("core.base.json")
import("core.base.hashset")
import("core.tool.toolchain")
import("core.cache.memcache", {alias = "_memcache"})
import("core.cache.localcache", {alias = "_localcache"})
import("async.runjobs")
import("lib.detect.find_file")
import("core.project.project")
import("core.project.config")

function _support(target)
    return import_implementation_of(target, "support")
end

function import_implementation_of(target, name)
    local cachekey = tostring(target)
    local implementation = memcache():get2(name, cachekey)
    if implementation == nil then
        if target:has_tool("cxx", "clang", "clangxx", "clang_cl") then
            implementation = import("clang." .. name, {anonymous = true})
        elseif target:has_tool("cxx", "gcc", "gxx") then
            implementation = import("gcc." .. name, {anonymous = true})
        elseif target:has_tool("cxx", "cl") then
            implementation = import("msvc." .. name, {anonymous = true})
        else
            local _, toolname = target:tool("cxx")
            raise("compiler(%s): does not implementation c++ module!", toolname)
        end
        memcache():set2(name, cachekey, implementation)
    end
    return implementation
end

-- load module support for the current target
function load(target)
    -- At least std c++20 is required, and we should call `set_languages("c++20")` to set it
    local languages = target:get("languages")
    local cxxlang = false
    for _, lang in ipairs(languages) do
        if lang:find("cxx", 1, true) or lang:find("c++", 1, true) or lang:find("gnuxx", 1, true) or lang:find("gnu++", 1, true) then
            cxxlang = true
            break
        end
    end
    if not cxxlang then
        target:add("languages", "c++20")
    end
    -- load module support for the specific compiler
    _support(target).load(target)
end

function get_cpplibrary_name(target)
    -- libc++ come first because on windows, if we use libc++ clang will still use msvc crt so MD / MT / MDd / MTd can be set
    if target:has_runtime("c++_shared", "c++_static") then
        return "c++"
    elseif target:has_runtime("stdc++_shared", "stdc++_static", "gnustl_static", "gnustl_shared") then
        return "stdc++"
    elseif target:has_runtime("stlport_static", "stlport_shared") then
        return "stlport"
    elseif target:has_runtime("MD", "MT", "MDd", "MTd") then
        return "msstl"
    end
    -- if no specified runtime, fallback on native platform C++ library
    if target:is_plat("android") then
        local ndk = toolchain.load("ndk", { plat = target:plat(), arch = target:arch() })
        local ver = ndk:config("ndkver")
        if not ver or ver > 14 then
            return "c++"
        else
            return "stdc++"
        end
    elseif target:is_plat("macosx", "iphoneos", "watchos", "appletvos", "applexros", "bsd", "harmony") then
        return "c++"
    elseif target:is_plat("linux", "mingw", "cygwin", "msys", "haiku") then
        return "stdc++"
    elseif target:is_plat("windows") then
        return "msstl"
    end
end

function has_two_phase_compilation_support(target)
    return _support(target).has_two_phase_compilation_support(target)
end

-- strip flags not relevent for module reuse
function strip_flags(target, flags, opt)

    local strippeable_flags, splitted_strippeable_flags
    if not opt.requiresonly then
        strippeable_flags, splitted_strippeable_flags =  _support(target).strippeable_flags()
        if opt and opt.strip_defines then
            table.join2(splitted_strippeable_flags, {"D", "U"})
        end
    else
        strippeable_flags, splitted_strippeable_flags =  _support(target).require_flags()
    end

    local splitted_strippeable_flags_set = hashset.new()
    for _, flag in ipairs(splitted_strippeable_flags) do
        table.insert(strippeable_flags, flag)
        splitted_strippeable_flags_set:insert("/" .. flag)
        splitted_strippeable_flags_set:insert("--" .. flag)
        splitted_strippeable_flags_set:insert("-" .. flag)
    end

    local output = {}
    local strip_next_flag = false
    for _, flag in ipairs(flags) do
        local strip = false

        if strip_next_flag then
            strip = true
            strip_next_flag = false
        else
            for _, _flag in ipairs(strippeable_flags) do
                if (flag == "/" .. _flag) or (flag == "--" .. _flag) or (flag == "-" .. _flag) then
                    strip = true
                    strip_next_flag = splitted_strippeable_flags_set:has(flag)
                    break
                elseif flag:startswith("/" .. _flag) or flag:startswith("--" .. _flag) or flag:startswith("-" .. _flag) then
                    strip = true
                    break
                end
            end
        end

        if not strip then
            table.insert(output, flag)
        end
    end
    return output
end

-- extract defines from flags
function get_headerunit_key(target, sourcefile)
    local defines = table.wrap(target:get("defines"))
    local undefines = table.wrap(target:get("undefines"))
    local fileconfig = target:fileconfig(sourcefile)
    if fileconfig then
        table.join2(defines, fileconfig.defines or {})
        table.join2(undefines, fileconfig.undefines or {})
    end

    if #defines > 0 then
        defines = table.concat(defines, "-D")
    else
        defines = "<NO_DEFINES>"
    end
    if #undefines > 0 then
        undefines = table.concat(undefines, "-D")
    else
        undefines = "<NO_UNDEFINES>"
    end

    local key = hash.md5(bytes(defines .. undefines))
    return key
end

-- get bmi extension
function get_bmi_extension(target)
    return _support(target).get_bmi_extension()
end

-- get bmi path
-- @see https://github.com/xmake-io/xmake/issues/4063
function get_bmi_path(bmifile)
    bmifile = bmifile:gsub(":", "_PARTITION_")
    return path.normalize(bmifile)
end

-- has module extension? e.g. *.mpp, ...
function has_module_extension(sourcefile, opt)
    opt = opt or {}
    local modulexts = _g.modulexts
    if modulexts == nil then
        modulexts = hashset.of(".cppm", ".ccm", ".cxxm", ".c++m", ".mpp", ".mxx", ".ixx")
        _g.modulexts = modulexts
    end
    local extension = opt.extension or path.extension(sourcefile)
    return modulexts:has(extension:lower())
end

-- this target contains module files?
function contains_modules(target)
    -- we can not use `"c++.build.modules.builder"`, because it contains sourcekind/cxx.
    local target_with_modules = target:sourcebatches()["c++.build.modules"] and true or false
    if not target_with_modules then
        target_with_modules = target:policy("build.c++.modules")
    end
    if not target_with_modules then
        for _, dep in ipairs(target:orderdeps()) do
            local sourcebatches = dep:sourcebatches()
            if sourcebatches["c++.build.modules"] then
                target_with_modules = true
                break
            end
        end
    end
    return target_with_modules
end

-- mark that a module scan artifacts and bmifile are reused from an other target
function set_reused(target, from, sourcefile)
    memcache():set2(target:fullname() .. "/modules/" .. sourcefile, "reuse", from)
    if option.get("diagnosis") then
        print("<" .. target:fullname() .. ">", "reuse", sourcefile, "from", "<" .. from:fullname() .. ">")
    end
end

-- query if a module scan artifacts and bmifile are reused from an other target
function is_reused(target, sourcefile)
    local from = memcache():get2(target:fullname() .. "/modules/" .. sourcefile, "reuse")
    return from and true or false, from
end

-- query if a module is public
function is_public(target, sourcefile)
    local fileconfig = target:fileconfig(sourcefile)
    return fileconfig and fileconfig.public or false
end

-- query if a module from a target dep
function is_from_dep(target, sourcefile)
    local fileconfig = target:fileconfig(sourcefile)
    return fileconfig and fileconfig.from_dep or false
end

-- query if a module from a package
function is_from_package(target, sourcefile)
    local fileconfig = target:fileconfig(sourcefile)
    return fileconfig and fileconfig.from_package or false
end

-- query if we should only build bmi
function is_bmionly(target, sourcefile)
    local fileconfig = target:fileconfig(sourcefile)
    return fileconfig and fileconfig.bmionly or false
end

-- query if a module can be culled
function can_be_culled(target, sourcefile)
    local can_cull = target:policy("build.c++.modules.culling")
    local fileconfig = target:fileconfig(sourcefile)
    local _, stdmodules_set = get_stdmodules(target)
    local is_stdmodule = stdmodules_set and stdmodules_set:has(sourcefile) or false
    local public = target:kind() == "moduleonly" and not is_stdmodule
    if fileconfig then
        public = fileconfig.public
        if fileconfig.cull ~= nil then
            can_cull = can_cull and fileconfig.cull
        end
    end
    if can_cull then
        can_cull = is_stdmodule or (fileconfig and fileconfig.external)
    end
    return can_cull and not public
end

-- load module infos
function load_moduleinfo(target, sourcefile)
    local reused, from = is_reused(target, sourcefile)
    local dependfile = reused and from:dependfile(sourcefile) or target:dependfile(sourcefile)

    local is_file = os.isfile(dependfile)
    if is_file then
        local data = io.load(dependfile)
        if data then
            local moduleinfo = json.decode(data.moduleinfo)
            moduleinfo.sourcefile = sourcefile
            return moduleinfo
        end
    end

    local targets = memcache():get("targets")
    local target_fullname = reused and from:fullname() or target:fullname()
    if not (targets and targets[target_fullname]) then
        return nil, string.format("module scanner did not run for target(%s), maybe a custom `on_prepare()` script overrides the modules one", target_fullname)
    end
    if not is_file then
        return nil, string.format("target(%s): dependfile for '%s' not found at '%s'", target_fullname, sourcefile, dependfile)
    end
    return nil, string.format("target(%s): failed to load moduleinfo for '%s'", target_fullname, sourcefile)
end

function find_quote_header_file(sourcefile, file)
    local p = path.join(path.directory(path.absolute(sourcefile, project.directory())), file)
    assert(os.isfile(p), "\"%s\" not found", p)
    return p
end

function find_angle_header_file(target, file)
    local headerpaths = _support(target).toolchain_includedirs(target)
    for _, dep in ipairs(target:orderdeps()) do
        local includedirs = table.join(dep:get("sysincludedirs") or {}, dep:get("includedirs") or {})
        table.join2(headerpaths, includedirs)
    end
    for _, pkg in ipairs(target:orderpkgs()) do
        local includedirs = table.join(pkg:get("sysincludedirs") or {}, pkg:get("includedirs") or {})
        table.join2(headerpaths, includedirs)
    end
    table.join2(headerpaths, target:get("includedirs"))
    local p = find_file(file, headerpaths)
    assert(p, "<%s> not found!", file)
    return p
end

-- get stdmodules
function get_stdmodules(target)
    if not target:policy("build.c++.modules.std") then
        return
    end
    local cpplib = get_cpplibrary_name(target)
    if not cpplib then
        return
    end
    local stdmodules = memcache():get2(cpplib, "c++.modules.stdmodules")
    local stdmodules_set = memcache():get2(cpplib, "c++.modules.stdmodules_set")
    if not stdmodules or not stdmodules_set then
        stdmodules = _support(target).get_stdmodules(target)
        stdmodules_set = hashset.from(stdmodules or {})
        memcache():set2(cpplib, "c++.modules.stdmodules", stdmodules)
        memcache():set2(cpplib, "c++.modules.stdmodules_set", stdmodules_set)
    end
    return stdmodules, stdmodules_set
end

-- get memcache
function memcache()
    return _memcache.cache("cxxmodules")
end

-- get localcache
function localcache()
    return _localcache.cache("cxxmodules")
end

-- get modules cache directory
function modules_cachedir(target, opt)
    assert(opt and (opt.interface ~= nil or opt.headerunit or opt.scan))
    local moduletype
    if opt.headerunit then
        moduletype = "headerunits"
    elseif opt.interface then
        moduletype = "interfaces"
    elseif opt.scan then
        moduletype = "scans"
    else
        moduletype = "implementation"
    end
    local cachedir = path.join(target:autogendir(), "rules", "bmi", "cache", moduletype)
    if opt.mkdir and not os.isdir(cachedir) then
        os.mkdir(cachedir)
    end
    return cachedir
end

function get_modulehash(sourcefile)
    return hash.strhash64(sourcefile)
end

function get_metafile(target, module)
    -- metafile are only for named modules
    local outputdir = get_outputdir(target, module.sourcefile, {interface = module.interface or false})
    return path.join(outputdir, path.filename(module.sourcefile) .. ".meta-info")
end

function get_outputdir(target, sourcefile, opt)
    local cachedir = modules_cachedir(target, opt)
    local modulehash = opt.key or get_modulehash(sourcefile)
    local outputdir = path.join(cachedir, modulehash)
    if not os.exists(outputdir) then
        os.mkdir(outputdir)
    end
    return outputdir
end

function add_installfiles_for_modules(target, modules)
    local sourcebatch = target:sourcebatches()["c++.build.modules.install"]
    if sourcebatch and sourcebatch.sourcefiles then
        for _, sourcefile in ipairs(sourcebatch.sourcefiles) do
            local fileconfig = target:fileconfig(sourcefile)
            local install = fileconfig and fileconfig.public or false
            if install then
                local modulehash = get_modulehash(sourcefile)
                local prefixdir = path.join("modules", modulehash)
                target:add("installfiles", sourcefile, {prefixdir = prefixdir})
                local metafile = get_metafile(target, modules[sourcefile])
                if os.exists(metafile) then
                    target:add("installfiles", metafile, {prefixdir = prefixdir})
                end
            end
        end
    end
end

