--!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
-- @file        xmake.lua
--

-- define rule: gencodes
rule("cuda.gencodes")

    -- add cuda `-gencode` flags to target
    --
    -- the gpu arch format syntax
    -- - compute_xx                   --> `-gencode arch=compute_xx,code=compute_xx`
    -- - sm_xx                        --> `-gencode arch=compute_xx,code=sm_xx`
    -- - sm_xx,sm_yy                  --> `-gencode arch=compute_xx,code=[sm_xx,sm_yy]`
    -- - compute_xx,sm_yy             --> `-gencode arch=compute_xx,code=sm_yy`
    -- - compute_xx,sm_yy,sm_zz       --> `-gencode arch=compute_xx,code=[sm_yy,sm_zz]`
    -- - native                       --> match the fastest cuda device on current host,
    --                                    eg. for a Tesla P100, `-gencode arch=compute_60,code=sm_60` will be added,
    --                                    if no available device is found, no `-gencode` flags will be added
    --                                    @seealso xmake/modules/lib/detect/find_cudadevices
    --
    on_config(function (target)

        -- imports
        import("core.platform.platform")
        import("lib.detect.find_cudadevices")
        import("core.base.hashset")

        -- sm_20 and compute_20 is supported until CUDA 8
        -- sm_30 and compute_30 is supported until CUDA 10
        -- sm_37 and compute_37 is supported until CUDA 11
        -- sm_72 and compute_72 is supported until CUDA 12
        local known_v_archs = hashset.of(20, 30, 32, 35, 37, 50, 52, 53, 60, 61, 62, 70, 72, 75, 80, 86, 87, 89, 90, 100, 103, 110, 120, 121)
        local known_r_archs = hashset.of(20, 30, 32, 35, 37, 50, 52, 53, 60, 61, 62, 70, 72, 75, 80, 86, 87, 89, 90, 100, 103, 110, 120, 121)

        local function nf_cugencode(archs)
            if type(archs) ~= "string" then
                return nil
            end
            archs = archs:trim():lower()
            if archs == "native" then
                local cuda_envs
                for _, toolchain_inst in ipairs(target:toolchains()) do
                    if toolchain_inst:name() == "cuda" then
                        cuda_envs = toolchain_inst:runenvs()
                        break
                    end
                end
                local device = find_cudadevices({skip_compute_mode_prohibited = true, order_by_flops = true, envs = cuda_envs, plat = target:plat(), arch = target:arch()})[1]
                if device then
                    return nf_cugencode("sm_" .. device.major .. device.minor)
                end
                return nil
            end

            local v_arch = nil
            local r_archs = {}

            -- full legal value list could be found in nvcc docs
            -- https://docs.nvidia.com/cuda/cuda-compiler-driver-nvcc/index.html#gpu-name-gpuname-arch
            -- examples: sm_75, compute_75, sm_90a, compute_100, sm_100f, compute_100a, etc.
            -- For robustness, xmake support a unoffical format: sm75, compute75, etc. New version still support it.
            -- examples: sm75, compute75, sm90a, compute100, sm100f, compute100a, etc.
            local function parse_arch(value, prefix, know_list)
                if not value:startswith(prefix) then
                    return nil
                end
                local arch_str = value:sub(#prefix + 1)
                if arch_str:startswith("_") then
                    arch_str = arch_str:sub(2)
                end

                -- a legal arch_str should be like: 75, 90a, 100f, etc.
                local arch_ver, suffix = arch_str:match("^(%d+)([af]?)$")
                local arch = tonumber(arch_ver)
                if arch == nil then
                    raise("unknown architecture: " .. value)
                end
                if suffix == 'a' and arch < 90 then
                    raise("unknown architecture: " .. prefix .. "_" .. arch .. suffix)
                end
                if suffix == 'f' and arch < 100 then
                    raise("unknown architecture: " .. prefix .. "_" .. arch .. suffix)
                end
                if not know_list:has(arch) then
                    if arch <= table.maxn(know_list:data()) then
                        raise("unknown architecture: " .. prefix .. "_" .. arch)
                    else
                        utils.warning("unknown architecture: " .. prefix .. "_" .. arch)
                    end
                end
                if suffix and #suffix > 0 then
                    return arch .. suffix
                end
                return arch
            end

            for _, v in ipairs(archs:split(',')) do
                local arch = v:trim()
                local temp_r_arch = parse_arch(arch, "sm", known_r_archs)
                if temp_r_arch then
                    table.insert(r_archs, temp_r_arch)
                end

                local temp_v_arch = parse_arch(arch, "compute", known_v_archs)
                if temp_v_arch then
                    if v_arch ~= nil then
                        raise("more than one virtual architecture is defined in one gpu gencode option: compute_" .. v_arch .. " and compute_" .. temp_v_arch)
                    end
                    v_arch = temp_v_arch
                end
                if not (temp_r_arch or temp_v_arch) then
                    raise("unknown architecture: " .. arch)
                end
            end

            if v_arch == nil and #r_archs == 0 then
                return nil
            end

            if #r_archs == 0 then
                return {
                    clang = "--cuda-gpu-arch=sm_" .. v_arch,
                    nvcc = "-gencode arch=compute_" .. v_arch .. ",code=compute_" .. v_arch
                }
            end

            if v_arch then
                table.insert(r_archs, v_arch)
            else
                v_arch = r_archs[1]
                local v_arch_ver = type(v_arch) == "string" and tonumber(v_arch:match("^(%d+)")) or v_arch
                for i = 2, #r_archs do
                    local r_arch = r_archs[i]
                    local r_arch_ver = type(r_arch) == "string" and tonumber(r_arch:match("^(%d+)")) or r_arch
                    if r_arch_ver < v_arch_ver then
                        v_arch = r_arch
                        v_arch_ver = r_arch_ver
                    elseif r_arch_ver == v_arch_ver and type(r_arch) == "number" and type(v_arch) == "string" then
                        v_arch = r_arch
                    end
                end
            end
            r_archs = table.unique(r_archs)

            local clang_flags = {}
            for _, r_arch in ipairs(r_archs) do
                table.insert(clang_flags, "--cuda-gpu-arch=sm_" .. r_arch)
            end

            local nvcc_flags = nil
            if #r_archs == 1 then
                nvcc_flags = "-gencode arch=compute_" .. v_arch .. ",code=sm_" .. r_archs[1]
            else
                nvcc_flags = "-gencode arch=compute_" .. v_arch .. ",code=[sm_" .. table.concat(r_archs, ",sm_") .. "]"
            end

            return { clang = clang_flags, nvcc = nvcc_flags }
        end

        local cugencodes = table.wrap(target:get("cugencodes"))
        for _, opt in ipairs(target:orderopts()) do
            table.join2(cugencodes, opt:get("cugencodes"))
        end
        for _, v in ipairs(cugencodes) do
            local flag = nf_cugencode(v)
            if flag then
                if target:has_tool("cu", "nvcc") then
                    target:add("cuflags", flag.nvcc)
                else
                    target:add("cuflags", flag.clang)
                end
                target:add("culdflags", flag.nvcc)
            end
        end
    end)
rule_end()
