--!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        verilator.lua
--

-- imports
import("utils.progress")
import("core.base.hashset")
import("core.project.depend")
import("private.action.build.object", { alias = "build_objectfiles" })
import("core.base.json")
import("lib.detect.find_tool")

-- parse sourcefiles from cmakefile
function _get_sourcefiles_from_cmake(target, cmakefile)
    local global_classes = {}
    local classefiles_slow = {}
    local classefiles_fast = {}
    local supportfiles_slow = {}
    local supportfiles_fast = {}
    local targetname = target:name()
    local verilator_root = assert(target:data("verilator.root"), "no verilator_root!")
    io.gsub(cmakefile, "set%((%S-) (.-)%)", function(key, values)
        if key == targetname .. "_GLOBAL" then
            -- get global class source files
            -- set(hello_GLOBAL "${VERILATOR_ROOT}/include/verilated.cpp" "${VERILATOR_ROOT}/include/verilated_threads.cpp")
            for classfile in values:gmatch("\"(.-)\"") do
                local classfile = classfile:gsub("%${VERILATOR_ROOT}", verilator_root)
                if os.isfile(classfile) then
                    table.insert(global_classes, classfile)
                end
            end
        elseif key == targetname .. "_CLASSES_SLOW" then
            for classfile in values:gmatch("\"(.-)\"") do
                table.insert(classefiles_slow, classfile)
            end
        elseif key == targetname .. "_CLASSES_FAST" then
            for classfile in values:gmatch("\"(.-)\"") do
                table.insert(classefiles_fast, classfile)
            end
        elseif key == targetname .. "_SUPPORT_SLOW" then
            for classfile in values:gmatch("\"(.-)\"") do
                table.insert(supportfiles_slow, classfile)
            end
        elseif key == targetname .. "_SUPPORT_FAST" then
            for classfile in values:gmatch("\"(.-)\"") do
                table.insert(supportfiles_fast, classfile)
            end
        end
    end)

    -- get compiled source files
    local sourcefiles = table.join(global_classes, classefiles_slow, classefiles_fast, supportfiles_slow,
        supportfiles_fast)
    return sourcefiles
end

-- parse sourcefiles from jsonfile
function _get_sourcefiles_from_json(jsonfile)
    local json_table = json.loadfile(jsonfile)
    local sources_table = assert(json_table["sources"], "No sources field found in json file: %s.", jsonfile)
    local global_classes = sources_table["global"] or {}
    local classefiles_slow = sources_table["classes_slow"] or {}
    local classefiles_fast = sources_table["classes_fast"] or {}
    local supportfiles_slow = sources_table["support_slow"] or {}
    local supportfiles_fast = sources_table["support_fast"] or {}

    -- get compiled source files
    local sourcefiles = table.join(global_classes, classefiles_slow, classefiles_fast, supportfiles_slow,
        supportfiles_fast)
    return sourcefiles
end

-- get languages
--
-- Select the Verilog language generation to support in the compiler.
-- This selects between v1364-1995, v1364-2001, v1364-2005, v1800-2005, v1800-2009, v1800-2012, v1800-2017, v1800-2023.
--
function _get_lanuage_flags(target)
    local language_v
    local languages = target:get("languages")
    if languages then
        for _, language in ipairs(languages) do
            if language:startswith("v") then
                language_v = language
                break
            end
        end
    end
    if language_v then
        local maps = {
            -- Verilog
            ["v1364-1995"] = "+1364-1995ext+v",
            ["v1364-2001"] = "+1364-2001ext+v",
            ["v1364-2005"] = "+1364-2005ext+v",
            -- SystemVerilog
            ["v1800-2005"] = "+1800-2005ext+v",
            ["v1800-2009"] = "+1800-2009ext+v",
            ["v1800-2012"] = "+1800-2012ext+v",
            ["v1800-2017"] = "+1800-2017ext+v",
            ["v1800-2023"] = "+1800-2023ext+v",
        }
        local flag = maps[language_v]
        if flag then
            return flag
        else
            assert("unknown language(%s) for verilator!", language_v)
        end
    end
end

function _get_makefile_type(verilator)
    local tool = assert(find_tool("verilator", { program = verilator, version = true }), "verilator not found!")
    local version = tool.version
    local support_json = version >= "5.036"
    local makefile_type = support_json and "json" or "cmake"
    return support_json, makefile_type
end

-- get the custom verilator flags
--
-- add_values("verilator.flags", "--x-assign", "fast")
-- add_values("verilator.flags", {"--x-assign", "fast"}, {"--x-initial", "fast"})
--
-- https://github.com/xmake-io/xmake/issues/7239
function _get_verilator_flags(target)
    local result = {}
    local flags = target:values("verilator.flags")
    if flags then
        for _, flag in ipairs(flags) do
            table.join2(result, flag)
        end
    end
    if #result > 0 then
        return result
    end
end

function config(target)
    local toolchain = assert(target:toolchain("verilator"), 'we need to set_toolchains("verilator") in target("%s")',
        target:name())
    local verilator = assert(toolchain:config("verilator"), "verilator not found!")
    local support_json, makefile_type = _get_makefile_type(verilator)
    local autogendir = path.join(target:autogendir(), "rules", "verilator")
    local tmpdir = os.tmpfile() .. ".dir"
    local makefile = path.join(tmpdir, "test." .. makefile_type)
    local sourcefile = path.join(tmpdir, "main.v")
    local argv = { "--cc", "--make", makefile_type, "--prefix", "test", "--Mdir", tmpdir, "main.v" }
    local flags = _get_verilator_flags(target)
    local switches_flags = hashset.of("sc", "coverage", "timing", "trace", "trace-vcd", "trace-fst", "trace-saif",
        "threads")
    if flags then
        for idx, flag in ipairs(flags) do
            if flag:startswith("--") and switches_flags:has(flag:sub(3)) then
                table.insert(argv, flag)
                if flag:startswith("--threads") then
                    table.insert(argv, flags[idx + 1])
                end
            end
        end
    end
    io.writefile(sourcefile, [[
module hello;
initial begin
$display("hello world!");
$finish ;
end
endmodule]])
    os.mkdir(tmpdir)
    -- we just pass relative sourcefile path to solve this issue on windows.
    -- @see https://github.com/verilator/verilator/issues/3873
    os.runv(verilator, argv, { curdir = tmpdir, envs = toolchain:runenvs() })

    -- parse some configurations from makefile
    local verilator_root
    local switches = {}
    local options_table = {}

    if support_json then
        local json_table = json.loadfile(makefile)
        local system_table = assert(json_table["system"], "No system field found in json file: %s.", makefile)
        verilator_root = system_table["verilator_root"]
        options_table = assert(json_table["options"], "No options field found in json file: %s.", makefile)
        switches.SC = options_table["system_c"] and "1" or "0"
        switches.COVERAGE = options_table["coverage"] and "1" or "0"
        switches.TIMING = options_table["use_timing"] and "1" or "0"
        -- The thread field is a integer in float format, we need to convert it to string.
        switches.THREADS = string.format("%d", options_table["threads"] or 1)
        switches.TRACE_VCD = options_table["trace_vcd"] and "1" or "0"
        switches.TRACE_FST = options_table["trace_fst"] and "1" or "0"
        switches.TRACE_SAIF = options_table["trace_saif"] and "1" or "0"
    else
        io.gsub(makefile, "set%((%S-) (.-)%)", function(key, values)
            if key == "VERILATOR_ROOT" then
                verilator_root = values:match("\"(.-)\" CACHE PATH")
                if not verilator_root then
                    verilator_root = values:match("(.-) CACHE PATH")
                end
            elseif key == "test_SC" then
                -- SystemC output mode?  0/1 (from --sc)
                switches.SC = values:trim()
            elseif key == "test_COVERAGE" then
                -- Coverage output mode?  0/1 (from --coverage)
                switches.COVERAGE = values:trim()
            elseif key == "test_TIMING" then
                -- Timing mode?  0/1 (from --timing)
                switches.TIMING = values:trim()
            elseif key == "test_THREADS" then
                -- Threaded output mode?  1/N threads (from --threads)
                switches.THREADS = values:trim()
            elseif key == "test_TRACE_VCD" then
                -- VCD Tracing output mode?  0/1 (from --trace)
                switches.TRACE_VCD = values:trim()
            elseif key == "test_TRACE_FST" then
                -- FST Tracing output mode? 0/1 (from --trace-fst)
                switches.TRACE_FST = values:trim()
            end
        end)
    end

    assert(verilator_root, "the verilator root directory not found!")
    target:data_set("verilator.root", verilator_root)

    -- add includedirs
    if not os.isfile(autogendir) then
        os.mkdir(autogendir)
    end
    target:add("includedirs", autogendir, { public = true })
    target:add("includedirs", path.join(verilator_root, "include"), { public = true })
    target:add("includedirs", path.join(verilator_root, "include", "vltstd"), { public = true })

    -- set languages
    local languages = target:get("languages")
    local cxxlang = false
    for _, lang in ipairs(languages) do
        if lang:find("xx", 1, true) or lang:find("++", 1, true) then
            cxxlang = true
            break
        end
    end
    if not cxxlang then
        target:add("languages", "c++20", { public = true })
    end

    -- add definitions for switches
    for k, v in table.orderpairs(switches) do
        target:add("defines", "VM_" .. k .. "=" .. v, { public = true })
    end
    if support_json then
        local cflags = options_table["cflags"] or {}
        for _, flag in ipairs(cflags) do
            if flag:startswith("-D") then
                target:add("defines", flag:sub(3), { public = true })
            end
        end
    end

    -- add syslinks
    if support_json then
        local ldflags = options_table["ldflags"] or {}
        for _, flag in ipairs(ldflags) do
            if flag:startswith("-l") then
                target:add("syslinks", flag:sub(3))
            end
        end
    else
        if target:is_plat("linux", "macosx") and switches.THREADS == "1" then
            target:add("syslinks", "pthread")
        end
        if target:is_plat("linux", "macosx") and switches.TRACE_FST == "1" then
            target:add("syslinks", "z")
        end
    end

    os.rm(tmpdir)
end

function build_cppfiles(target, jobgraph, sourcebatch, opt)
    local toolchain = assert(target:toolchain("verilator"), 'we need to set_toolchains("verilator") in target("%s")',
        target:name())
    local verilator = assert(toolchain:config("verilator"), "verilator not found!")
    local support_json, makefile_type = _get_makefile_type(verilator)
    local autogendir = path.join(target:autogendir(), "rules", "verilator")
    local targetname = target:name()
    local makefile = path.join(autogendir, targetname .. "." .. makefile_type)

    -- build verilog files
    depend.on_changed(function()
        local argv = { "--cc", "--make", makefile_type, "--prefix", targetname, "--Mdir", autogendir }
        local flags = _get_verilator_flags(target)
        if flags then
            table.join2(argv, flags)
        end
        local language_flags = _get_lanuage_flags(target)
        if language_flags then
            table.join2(argv, language_flags)
        end
        local sourcefiles = sourcebatch.sourcefiles
        for _, sourcefile in ipairs(sourcefiles) do
            local sourcefile = sourcefile
            progress.show(opt.progress or 0, "${color.build.object}compiling.verilog %s", sourcefile)
            -- we need to use slashes to fix it on windows
            -- @see https://github.com/verilator/verilator/issues/3873
            if is_host("windows") then
                sourcefile = sourcefile:gsub("\\", "/")
            end
            table.insert(argv, sourcefile)
        end

        -- generate c++ sourcefiles
        os.vrunv(verilator, argv, { envs = toolchain:runenvs() })
    end, {
        dependfile = makefile .. ".d",
        files = sourcebatch.sourcefiles,
        changed = target:is_rebuilt(),
        lastmtime = os.mtime(makefile)
    })

    -- get compiled source files
    local sourcefiles
    if support_json then
        sourcefiles = _get_sourcefiles_from_json(makefile)
    else
        sourcefiles = _get_sourcefiles_from_cmake(target, makefile)
    end

    -- do build
    local sourcebatch_cpp = {
        rulename = "c++.build",
        sourcekind = "cxx",
        sourcefiles = sourcefiles,
        objectfiles = {},
        dependfiles = {}
    }
    for _, sourcefile in ipairs(sourcefiles) do
        local objectfile = target:objectfile(sourcefile)
        local dependfile = target:dependfile(objectfile)
        table.insert(target:objectfiles(), objectfile)
        table.insert(sourcebatch_cpp.objectfiles, objectfile)
        table.insert(sourcebatch_cpp.dependfiles, dependfile)
    end
    build_objectfiles(target, jobgraph, sourcebatch_cpp, opt)
end

function buildcmd_vfiles(target, batchcmds, sourcebatch, opt)
    local toolchain = assert(target:toolchain("verilator"), 'we need to set_toolchains("verilator") in target("%s")',
        target:name())
    local verilator = assert(toolchain:config("verilator"), "verilator not found!")
    local _, makefile_type = _get_makefile_type(verilator)
    local autogendir = path.join(target:autogendir(), "rules", "verilator")
    local targetname = target:name()
    local makefile = path.join(autogendir, targetname .. "." .. makefile_type)
    local dependfile = makefile .. ".d"

    local argv = { "--cc", "--make", makefile_type, "--prefix", targetname, "--Mdir", path(autogendir) }
    local flags = _get_verilator_flags(target)
    if flags then
        table.join2(argv, flags)
    end
    local language_flags = _get_lanuage_flags(target)
    if language_flags then
        table.join2(argv, language_flags)
    end
    local sourcefiles = sourcebatch.sourcefiles
    for _, sourcefile in ipairs(sourcefiles) do
        batchcmds:show_progress(opt.progress, "${color.build.object}compiling.verilog %s", sourcefile)
        table.insert(argv, path(sourcefile, function(v)
            -- we need to use slashes to fix it on windows
            -- @see https://github.com/verilator/verilator/issues/3873
            if is_host("windows") then
                v = v:gsub("\\", "/")
            end
            return v
        end))
    end

    -- generate c++ sourcefiles
    batchcmds:vrunv(verilator, argv, { envs = toolchain:runenvs() })
    batchcmds:add_depfiles(sourcefiles)
    batchcmds:set_depmtime(os.mtime(makefile))
    batchcmds:set_depcache(dependfile)
end

function buildcmd_cppfiles(target, batchcmds, sourcebatch, opt)
    local toolchain = assert(target:toolchain("verilator"), 'we need set_toolchains("verilator") in target("%s")',
        target:name())
    local verilator = assert(toolchain:config("verilator"), "verilator not found!")
    local support_json, makefile_type = _get_makefile_type(verilator)
    local autogendir = path.join(target:autogendir(), "rules", "verilator")
    local targetname = target:name()
    local makefile = path.join(autogendir, targetname .. "." .. makefile_type)
    local dependfile = path.join(autogendir, targetname .. ".build.d")

    -- get compiled source files
    local sourcefiles
    if support_json then
        sourcefiles = _get_sourcefiles_from_json(makefile)
    else
        sourcefiles = _get_sourcefiles_from_cmake(target, makefile)
    end

    -- do build
    for _, sourcefile in ipairs(sourcefiles) do
        local objectfile = target:objectfile(sourcefile)
        batchcmds:show_progress(opt.progress, "${color.build.object}compiling.$(mode) %s", path.filename(sourcefile))
        batchcmds:compile(sourcefile, objectfile)
        table.insert(target:objectfiles(), objectfile)
    end
    batchcmds:add_depfiles(sourcefiles)
    batchcmds:set_depmtime(os.mtime(dependfile))
    batchcmds:set_depcache(dependfile)
end
