--!A cross-toolchain 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
-- @file        load.lua
--

-- imports
import("core.base.hashset")
import("core.project.config")

-- get triple
function _get_triple(arch)
    local triples =
    {
        ["armv5te"]     = "arm-linux-androideabi"   -- deprecated
    ,   ["armv7-a"]     = "arm-linux-androideabi"   -- deprecated
    ,   ["armeabi"]     = "arm-linux-androideabi"   -- removed in ndk r17
    ,   ["armeabi-v7a"] = "arm-linux-androideabi"
    ,   ["arm64-v8a"]   = "aarch64-linux-android"
    ,   ["riscv64"]     = "riscv64-linux-android"
    ,   i386            = "i686-linux-android"      -- deprecated
    ,   x86             = "i686-linux-android"
    ,   x86_64          = "x86_64-linux-android"
    ,   mips            = "mips-linux-android"      -- removed in ndk r17
    ,   mips64          = "mips64-linux-android"    -- removed in ndk r17
    }
    return triples[arch]
end

-- get target
function _get_target(arch, ndk_sdkver)
    local targets =
    {
        ["armv5te"]     = "armv5te-none-linux-androideabi"  -- deprecated
    ,   ["armeabi"]     = "armv5te-none-linux-androideabi"  -- removed in ndk r17
    ,   ["armv7-a"]     = "armv7-none-linux-androideabi"    -- deprecated
    ,   ["armeabi-v7a"] = "armv7-none-linux-androideabi"
    ,   ["arm64-v8a"]   = "aarch64-none-linux-android"
    ,   ["riscv64"]     = "riscv64-none-linux-android"
    ,   ["i386"]        = "i686-none-linux-android"         -- deprecated
    ,   ["x86"]         = "i686-none-linux-android"
    ,   ["x86_64"]      = "x86_64-none-linux-android"
    ,   ["mips"]        = "mipsel-none-linux-android"       -- removed in ndk r17
    ,   ["mips64"]      = "mips64el-none-linux-android"     -- removed in ndk r17
    }
    assert(targets[arch], "unknown arch(%s) for android!", arch)
    return targets[arch] .. (ndk_sdkver or "")
end

-- load ndk toolchain
--
-- some extra configuration for target
-- e.g.
--   set_values("ndk.arm_mode", "arm") -- or thumb
--
function main(toolchain)

    -- get cross
    local cross = toolchain:cross() or ""

    -- get gcc toolchain bin directory
    local gcc_toolchain_bin = nil
    local gcc_toolchain = toolchain:config("gcc_toolchain")
    if gcc_toolchain then
        gcc_toolchain_bin = path.join(gcc_toolchain, "bin")
    end

    -- get ndk and version
    local ndk = toolchain:config("ndk")
    local ndkver = toolchain:config("ndkver")
    local ndk_sdkver = toolchain:config("ndk_sdkver")

    -- set toolset
    local use_gcc = toolchain:config("gcc")
    if use_gcc then
        toolchain:set("toolset", "cc", cross .. "gcc")
        toolchain:set("toolset", "cxx", cross .. "g++")
        toolchain:set("toolset", "cpp", cross .. "gcc -E")
        toolchain:set("toolset", "as", cross .. "gcc")
        toolchain:set("toolset", "ld", cross .. "g++", cross .. "gcc")
        toolchain:set("toolset", "sh", cross .. "g++", cross .. "gcc")
    else
        toolchain:set("toolset", "cc", "clang", cross .. "gcc")
        toolchain:set("toolset", "cxx", "clang++", cross .. "g++")
        toolchain:set("toolset", "cpp", "clang -E", cross .. "gcc -E")
        toolchain:set("toolset", "as", "clang", cross .. "gcc")
        toolchain:set("toolset", "ld", "clang++", "clang", cross .. "g++", cross .. "gcc")
        toolchain:set("toolset", "sh", "clang++", "clang", cross .. "g++", cross .. "gcc")
    end
    toolchain:set("toolset", "ar", gcc_toolchain_bin and path.join(gcc_toolchain_bin, cross .. "ar") or (cross .. "ar"), "llvm-ar")
    toolchain:set("toolset", "ranlib", gcc_toolchain_bin and path.join(gcc_toolchain_bin, cross .. "ranlib") or (cross .. "ranlib"))
    toolchain:set("toolset", "strip", gcc_toolchain_bin and path.join(gcc_toolchain_bin, cross .. "strip") or (cross .. "strip"), "llvm-strip")
    -- gnustl and stlport have been removed in ndk r18 (deprecated in ndk r17)
    -- https://github.com/android/ndk/wiki/Changelog-r18
    local old_runtimes = {"gnustl_static", "gnustl_shared", "stlport_static", "stlport_shared"}
    if ndkver and ndkver < 18 then
        toolchain:add("runtimes", table.unpack(old_runtimes))
    end

    -- init flags
    local arm32 = false
    local arch = toolchain:arch()
    toolchain:add("ldflags", "-llog")
    toolchain:add("shflags", "-llog")
    if arch and (arch == "armeabi" or arch == "armeabi-v7a" or arch == "armv5te" or arch == "armv7-a") then -- armv5te/armv7-a are deprecated
        arm32 = true
    end

    -- use llvm directory? e.g. android-ndk/toolchains/llvm/prebuilt/darwin-x86_64/bin
    local isllvm = false
    local bindir = toolchain:bindir()
    if bindir and bindir:find("llvm", 1, true) then
        isllvm = true
    end

    -- init architecture
    if isllvm then

        -- add ndk target
        local ndk_target = _get_target(arch, ndk_sdkver)
        toolchain:add("cxflags", "--target=" .. ndk_target)
        toolchain:add("asflags", "--target=" .. ndk_target)
        toolchain:add("ldflags", "--target=" .. ndk_target)
        toolchain:add("shflags", "--target=" .. ndk_target)

        -- add gcc toolchain
        local gcc_toolchain = toolchain:config("gcc_toolchain")
        if gcc_toolchain then
            toolchain:add("cxflags", "--gcc-toolchain=" .. gcc_toolchain)
            toolchain:add("asflags", "--gcc-toolchain=" .. gcc_toolchain)
            toolchain:add("ldflags", "--gcc-toolchain=" .. gcc_toolchain)
            toolchain:add("shflags", "--gcc-toolchain=" .. gcc_toolchain)
        end
    else
        local march = arch
        if arch == "arm64-v8a" then
            march = "armv8-a"
        else
            march = "armv5te"
        end
        -- old version ndk
        toolchain:add("cxflags", "-march=" .. march)
        toolchain:add("asflags", "-march=" .. march)
        toolchain:add("ldflags", "-march=" .. march)
        toolchain:add("shflags", "-march=" .. march)
    end

    -- init cxflags for the target kind: binary
    toolchain:add("binary.cxflags", "-fPIE", "-pie")

    -- add flags for the sdk directory of ndk
    if ndk and ndk_sdkver then

        -- the sysroot archs
        local sysroot_archs =
        {
            ["armv5te"]     = "arch-arm"    -- deprecated
        ,   ["armv7-a"]     = "arch-arm"    -- deprecated
        ,   ["armeabi"]     = "arch-arm"    -- removed in ndk r17
        ,   ["armeabi-v7a"] = "arch-arm"
        ,   ["arm64-v8a"]   = "arch-arm64"
        ,   ["riscv64"]     = "arch-riscv64"
        ,   i386            = "arch-x86"    -- deprecated
        ,   x86             = "arch-x86"
        ,   x86_64          = "arch-x86_64"
        ,   mips            = "arch-mips"   -- removed in ndk r17
        ,   mips64          = "arch-mips64" -- removed in ndk r17
        }
        local sysroot_arch = sysroot_archs[arch]

        -- add sysroot flags
        local sysroot_cflags_added = false
        local ndk_sysroot = toolchain:config("ndk_sysroot")
        if ndk_sysroot and os.isdir(ndk_sysroot) then
            local triple = _get_triple(arch)
            if ndkver and tonumber(ndkver) < 22 then
                toolchain:add("cxflags", "-D__ANDROID_API__=" .. ndk_sdkver)
                toolchain:add("asflags", "-D__ANDROID_API__=" .. ndk_sdkver)
            end
            local flag_sysroot = "--sysroot=" .. os.args(ndk_sysroot)
            local flag_isystem = "-isystem " .. os.args(path.join(ndk_sysroot, "usr", "include", triple))
            toolchain:add("cflags",   flag_sysroot)
            toolchain:add("cxxflags", flag_sysroot)
            toolchain:add("asflags",  flag_sysroot)
            toolchain:add("cflags",   flag_isystem)
            toolchain:add("cxxflags", flag_isystem)
            toolchain:add("asflags",  flag_isystem)
            sysroot_cflags_added = true
        end
        local ndk_sdkdir = path.translate(format("%s/platforms/android-%d", ndk, ndk_sdkver))
        if os.isdir(ndk_sdkdir) then
            if sysroot_arch then
                local flag_sysroot = "--sysroot=" .. os.args(path.join(ndk_sdkdir, sysroot_arch))
                if not sysroot_cflags_added then
                    toolchain:add("cflags",   flag_sysroot)
                    toolchain:add("cxxflags", flag_sysroot)
                    toolchain:add("asflags",  flag_sysroot)
                end
                -- we need to add sysroot flags for low-version ndk
                -- @see https://github.com/xmake-io/xmake/issues/6621
                toolchain:add("ldflags",  flag_sysroot)
                toolchain:add("shflags",  flag_sysroot)
            end
        end

        -- add "-fPIE -pie" to ldflags
        toolchain:add("ldflags", "-fPIE")
        toolchain:add("ldflags", "-pie")

        -- get llvm c++ stl sdk directory
        local cxxstl_sdkdir_llvmstl = path.translate(format("%s/sources/cxx-stl/llvm-libc++", ndk))

        -- get gnu c++ stl sdk directory
        local cxxstl_sdkdir_gnustl = nil
        if toolchain:config("ndk_toolchains_ver") then
            cxxstl_sdkdir_gnustl = path.translate(format("%s/sources/cxx-stl/gnu-libstdc++/%s", ndk, toolchain:config("ndk_toolchains_ver")))
        end

        -- get stlport c++ sdk directory
        local cxxstl_sdkdir_stlport = path.translate(format("%s/sources/cxx-stl/stlport", ndk))

        -- get c++ stl sdk directory
        local cxxstl_sdkdir = nil
        local ndk_cxxstl = config.get("runtimes") or config.get("ndk_cxxstl")
        if ndk_cxxstl then
            if (ndkver and ndkver >= 18) and table.contains(old_runtimes, ndk_cxxstl)  then
                utils.warning("%s is was removed in ndk v%s", ndk_cxxstl, ndk_sdkver)
            end

            if ndk_cxxstl:find(",", 1, true) then
                local runtimes_supported = hashset.from(toolchain:get("runtimes"))
                for _, item in ipairs(ndk_cxxstl:split(",")) do
                    if runtimes_supported:has(item) then
                        ndk_cxxstl = item
                        break
                    end
                end
            end
            -- we uses c++_static/c++_shared instead of llvmstl_static/llvmstl_shared
            if ndk_cxxstl:startswith("c++") or ndk_cxxstl:startswith("llvmstl") then
                cxxstl_sdkdir = cxxstl_sdkdir_llvmstl
            elseif ndk_cxxstl:startswith("gnustl") then
                cxxstl_sdkdir = cxxstl_sdkdir_gnustl
            elseif ndk_cxxstl:startswith("stlport") then
                cxxstl_sdkdir = cxxstl_sdkdir_stlport
            end
        else
            if isllvm then
                ndk_cxxstl = "c++_static"
                cxxstl_sdkdir = cxxstl_sdkdir_llvmstl
            end
            if (cxxstl_sdkdir == nil or not os.isdir(cxxstl_sdkdir)) and cxxstl_sdkdir_gnustl then -- <= ndk r16
                ndk_cxxstl = "gnustl_static"
                cxxstl_sdkdir = cxxstl_sdkdir_gnustl
            end
        end

        -- only for c++ stl
        if config.get("ndk_stdcxx") then
            if cxxstl_sdkdir and os.isdir(cxxstl_sdkdir) then

                -- the toolchains archs
                local toolchains_archs =
                {
                    ["armv5te"]     = "armeabi"         -- deprecated
                ,   ["armv7-a"]     = "armeabi-v7a"     -- deprecated
                ,   ["armeabi"]     = "armeabi"         -- removed in ndk r17
                ,   ["armeabi-v7a"] = "armeabi-v7a"
                ,   ["arm64-v8a"]   = "arm64-v8a"
                ,   ["riscv64"]     = "riscv64"
                ,   i386            = "x86"             -- deprecated
                ,   x86             = "x86"
                ,   x86_64          = "x86_64"
                ,   mips            = "mips"            -- removed in ndk r17
                ,   mips64          = "mips64"          -- removed in ndk r17
                }
                local toolchains_arch = toolchains_archs[arch]

                -- add c++ stl include and link directories
                if toolchains_arch then
                    toolchain:add("linkdirs", format("%s/libs/%s", cxxstl_sdkdir, toolchains_arch))
                end
                if ndk_cxxstl:startswith("c++") or ndk_cxxstl:startswith("llvmstl") then
                    toolchain:add("cxxflags", "-nostdinc++")
                    toolchain:add("sysincludedirs", format("%s/include", cxxstl_sdkdir))
                    if toolchains_arch then
                        toolchain:add("sysincludedirs", format("%s/libs/%s/include", cxxstl_sdkdir, toolchains_arch))
                    end
                    local abi_path = path.join(ndk, "sources", "cxx-stl", "llvm-libc++abi")
                    local before_r13 = path.join(abi_path, "libcxxabi")
                    local after_r13 = path.join(abi_path, "include")
                    if os.isdir(before_r13) then
                        toolchain:add("sysincludedirs", before_r13)
                    elseif os.isdir(after_r13) then
                        toolchain:add("sysincludedirs", after_r13)
                    end
                elseif ndk_cxxstl:startswith("gnustl") then
                    toolchain:add("cxxflags", "-nostdinc++")
                    toolchain:add("sysincludedirs", format("%s/include", cxxstl_sdkdir))
                    if toolchains_arch then
                        toolchain:add("sysincludedirs", format("%s/libs/%s/include", cxxstl_sdkdir, toolchains_arch))
                    end
                elseif ndk_cxxstl:startswith("stlport") then
                    toolchain:add("cxxflags", "-nostdinc++")
                    toolchain:add("sysincludedirs", format("%s/stlport", cxxstl_sdkdir))
                end

                -- add c++ stl links
                if ndk_cxxstl == "c++_static" or ndk_cxxstl == "llvmstl_static" then
                    toolchain:add("syslinks", "c++_static", "c++abi")
                    if arm32 then
                        toolchain:add("syslinks", "unwind", "atomic")
                    end
                elseif ndk_cxxstl == "c++_shared" or ndk_cxxstl == "llvmstl_shared" then
                    toolchain:add("syslinks", "c++_shared", "c++abi")
                    if arm32 then
                        toolchain:add("syslinks", "unwind", "atomic")
                    end
                elseif ndk_cxxstl == "gnustl_static" then
                    toolchain:add("syslinks", "gnustl_static")
                elseif ndk_cxxstl == "gnustl_shared" then
                    toolchain:add("syslinks", "gnustl_shared")
                elseif ndk_cxxstl == "stlport_static" then
                    toolchain:add("syslinks", "stlport_static")
                elseif ndk_cxxstl == "stlport_shared" then
                    toolchain:add("syslinks", "stlport_shared")
                end

                -- fix 'ld: error: cannot find -lc++' for clang++.exe on r20/windows
                -- @see https://github.com/xmake-io/xmake/issues/684
                if ndkver and ndkver >= 20 and (ndk_cxxstl:startswith("c++") or ndk_cxxstl:startswith("llvmstl")) then
                    toolchain:add("ldflags", "-nostdlib++")
                    toolchain:add("shflags", "-nostdlib++")
                end
            elseif ndkver and ndkver >= 26 then
                -- The NDK's libc++ now comes directly from our LLVM toolchain above 26b
                -- https://github.com/xmake-io/xmake/issues/4614
                if ndk_cxxstl == "c++_static" then
                    toolchain:add("ldflags", "-static-libstdc++", "-lc++abi")
                    toolchain:add("shflags", "-static-libstdc++", "-lc++abi")
                    if arm32 then
                        toolchain:add("syslinks", "unwind", "atomic")
                    end
                elseif ndk_cxxstl == "c++_shared" then
                    if arm32 then
                        toolchain:add("syslinks", "unwind", "atomic")
                    end
                end
            end
        end
    end

    -- init flags for target
    local target_on_xxflags = function (target)
        if arm32 then
            -- @see https://github.com/xmake-io/xmake/issues/927
            if target:values("ndk.arm_mode") == "arm" then
                return "-marm"
            else
                return "-mthumb"
            end
        end
    end
    toolchain:add("target.on_cxflags", target_on_xxflags)
    toolchain:add("target.on_asflags", target_on_xxflags)
    toolchain:add("target.on_ldflags", target_on_xxflags)
    toolchain:add("target.on_shflags", target_on_xxflags)

    local rcshflags = table.copy(toolchain:get("shflags"))
    local rcldflags = table.copy(toolchain:get("ldflags"))
    for _, link in ipairs(toolchain:get("syslinks")) do
        table.insert(rcshflags, "-l" .. link)
        table.insert(rcldflags, "-l" .. link)
    end
    -- fix [[note: clang++: error: no such file or directory: '"-llog']] on windows
    -- @note 'link-args=...' should not be 'link-args="..."'
    toolchain:set("rcshflags", {"-C", "link-args=" .. (table.concat(rcshflags, " "):gsub("%-march=.-%s", ""))}, {expand = false})
    toolchain:set("rcldflags", {"-C", "link-args=" .. (table.concat(rcldflags, " "):gsub("%-march=.-%s", ""))}, {expand = false})
    local sh = toolchain:tool("sh") -- @note we cannot use `config.get("sh")`, because we need to check sh first
    if sh then
        toolchain:add("rcshflags", "-C linker=" .. sh)
    end
    local ld = toolchain:tool("ld")
    if ld then
        toolchain:add("rcldflags", "-C linker=" .. ld)
    end
end
