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

-- imports
import("core.base.option")
import("core.tool.linker")
import("core.tool.compiler")
import("core.language.language")

-- get function name
--
-- sigsetjmp
-- sigsetjmp((void*)0, 0)
-- sigsetjmp{sigsetjmp((void*)0, 0);}
-- sigsetjmp{int a = 0; sigsetjmp((void*)a, a);}
--
function _funcname(funcinfo)

    -- parse function name
    local name = string.match(funcinfo, "(.+){.+}")
    if name == nil then
        local pos = funcinfo:find("%(")
        if pos then
            name = funcinfo:sub(1, pos - 1)
        else
            name = funcinfo
        end
    end

    -- ok
    return name:trim()
end

-- get function code
--
-- sigsetjmp
-- sigsetjmp((void*)0, 0)
-- sigsetjmp{sigsetjmp((void*)0, 0);}
-- sigsetjmp{int a = 0; sigsetjmp((void*)a, a);}
--
function _funccode(funcinfo)
    local code = string.match(funcinfo, ".+{(.+)}")
    if code == nil then
        local pos = funcinfo:find("%(")
        if pos then
            code = funcinfo
        else
            code = string.format("volatile void* p%s = (void*)&%s; if (p%s) {}", funcinfo, funcinfo, funcinfo)
        end
    end
    return code
end

-- make source code
function _sourcecode(snippets, opt)

    -- has main()?
    local has_main = false
    for _, snippet in pairs(snippets) do
        -- find int main(int argc, char** argv) {}
        if snippet:find("int%s+main%s*%(.-%)%s*{") then
            has_main = true
            break
        end
    end

    -- add includes
    local sourcecode = ""
    local includes = table.wrap(opt.includes)
    if not has_main and opt.tryrun and opt.output then
        table.insert(includes, "stdio.h")
    end
    for _, include in ipairs(includes) do
        sourcecode = format("%s\n#include <%s>", sourcecode, include)
    end
    sourcecode = sourcecode .. "\n"

    -- add types
    for _, typename in ipairs(opt.types) do
        sourcecode = format("%s\ntypedef %s __type_%s;", sourcecode, typename, typename:gsub("[^%a]", "_"))
    end
    sourcecode = sourcecode .. "\n"

    -- we just add all snippets if has main function
    -- @see https://github.com/xmake-io/xmake/issues/3527
    if has_main then
        for _, snippet in pairs(snippets) do
            sourcecode = sourcecode .. "\n" .. snippet
        end
        sourcecode = sourcecode .. "\n"
        return sourcecode
    end

    -- add snippets (build only)
    if not opt.tryrun then
        for _, snippet in pairs(snippets) do
            sourcecode = sourcecode .. "\n" .. snippet
        end
        sourcecode = sourcecode .. "\n"
    end

    -- enter main function
    sourcecode = sourcecode .. "int main(int argc, char** argv)\n{\n"

    -- add funcs
    for _, funcinfo in ipairs(opt.funcs) do
        sourcecode = format("%s\n    %s;", sourcecode, _funccode(funcinfo))
    end

    -- add snippets (tryrun)
    if opt.tryrun then
        for _, snippet in pairs(snippets) do
            sourcecode = sourcecode .. "\n" .. snippet
        end
        if opt.output then
            sourcecode = sourcecode .. "\nfflush(stdout);\n"
        end
        sourcecode = sourcecode .. "\n}\n" -- we need return exit code in snippet
    else
        -- leave main function
        sourcecode = sourcecode .. "\n    return 0;\n}\n"
    end
    return sourcecode
end

-- check the given c/c++ snippets?
--
-- @param snippets  the snippets
-- @param opt       the argument options
--                  e.g.
--                  { verbose = false, target = [target|option], sourcekind = "[cc|cxx|mm|mxx]"
--                  , types = {"wchar_t", "char*"}, includes = "stdio.h", funcs = {"sigsetjmp", "sigsetjmp((void*)0, 0)"}
--                  , configs = {defines = "xx", cxflags = ""}
--                  , tryrun = true, output = true}
--
-- funcs:
--      sigsetjmp
--      sigsetjmp((void*)0, 0)
--      sigsetjmp{sigsetjmp((void*)0, 0);}
--      sigsetjmp{int a = 0; sigsetjmp((void*)a, a);}
--
-- @return          true or false
--
-- @code
-- local ok, output_or_errors = check_cxsnippets("void test() {}")
-- local ok, output_or_errors = check_cxsnippets({"void test(){}", "#define TEST 1"}, {types = "wchar_t", includes = "stdio.h"})
-- local ok, output_or_errors = check_cxsnippets({snippet_name = "void test(){}", "#define TEST 1"}, {types = "wchar_t", includes = "stdio.h"})
-- local ok, output_or_errors = check_cxsnippets([[
--  int test() {
--      return (sizeof(int) == 4)? 0 : -1;
--  }
--  int main(int argc, char** argv) {
--      return test();
--  }]], {tryrun = true})
-- @endcode
--
-- check C/C++ code snippets for compilation
--
-- @param snippets   the code snippets table
-- @param opt        the options, e.g. {target = target, sourcekind = "cc", includes = {}, configs = {}}
-- @return           true and output on success, or false
--
function main(snippets, opt)

    -- init options
    opt = opt or {}

    -- init snippets
    snippets = snippets or {}

    -- get configs
    local configs = opt.configs or opt.config

    -- get links
    local links = {}
    local target = opt.target
    if configs and configs.links then
        table.join2(links, configs.links)
    end
    if target and target:type() ~= "package" then
        table.join2(links, target:get("links"))
    end
    if configs and configs.syslinks then
        table.join2(links, configs.syslinks)
    end
    if target and target:type() ~= "package" then
        table.join2(links, opt.target:get("syslinks"))
    end

    -- get types
    local types = table.wrap(opt.types)

    -- get includes
    local includes = table.wrap(opt.includes)

    -- get funcs
    local funcs = {}
    for _, funcinfo in ipairs(opt.funcs) do
        table.insert(funcs, _funcname(funcinfo))
    end

    -- make source code
    local sourcecode = _sourcecode(snippets, opt)

    -- get c/c++ extension
    local extension = ".c"
    local sourcekind = opt.sourcekind
    if sourcekind then
        extension = table.wrap(language.sourcekinds()[sourcekind])[1] or ".c"
    end

    -- make the source file
    -- @note we use fixed temporary filenames in order to better cache the compilation results for build_cache.
    local tmpfile = os.tmpfile(sourcecode)
    local sourcefile = tmpfile .. extension
    local objectfile = os.tmpfile() .. ".o"
    local binaryfile = objectfile:gsub("%.o$", ".b")
    if not os.isfile(sourcefile) then
        io.writefile(sourcefile, sourcecode)
    end

    -- @note cannot cache result, all conditions will be changed
    -- attempt to compile it
    local errors = nil
    local ok, output = try
    {
        function ()
            if option.get("diagnosis") then
                cprint("${dim}> %s", compiler.compcmd(sourcefile, objectfile, opt))
            end
            opt = table.clone(opt)
            opt.build_warnings = false
            compiler.compile(sourcefile, objectfile, opt)
            if #links > 0 or opt.tryrun or opt.binary_match then
                if option.get("diagnosis") then
                    cprint("${dim}> %s", linker.linkcmd("binary", {"cc", "cxx"}, objectfile, binaryfile, opt))
                end
                linker.link("binary", {"cc", "cxx"}, objectfile, binaryfile, opt)
            end
            if opt.tryrun then
                -- @note we use the *v variants so the binary path is not split on
                -- whitespace by os.argv. mostly hits on Windows where TEMP lives
                -- under the user profile and may contain spaces.
                if opt.output then
                    local output = os.iorunv(binaryfile)
                    if output then
                        output = output:trim()
                    end
                    return true, output
                else
                    os.vrunv(binaryfile)
                end
            end
            local binary_match = opt.binary_match
            if binary_match then
                local content = io.readfile(binaryfile, {encoding = "binary"})
                local match = type(binary_match) == "function" and binary_match(content) or content:match(binary_match)
                if match ~= nil then
                    return true, match
                end
            end
            return true
        end,
        catch { function (errs) errors = errs end }
    }

    -- remove some files
    os.tryrm(objectfile)
    os.tryrm(binaryfile)

    -- trace
    if opt.verbose or option.get("verbose") or option.get("diagnosis") then
        local kind = "c"
        if sourcekind == "cxx" then
            kind = "c++"
        elseif sourcekind == "mxx" then
            kind = "objc++"
        elseif sourcekind == "cc" then
            kind = "c"
        elseif sourcekind == "mm" then
            kind = "objc"
        end
        if #includes > 0 then
            cprint("${dim}> checking for %s includes(%s)", kind, table.concat(includes, ", "))
        end
        if #types > 0 then
            cprint("${dim}> checking for %s types(%s)", kind, table.concat(types, ", "))
        end
        if #funcs > 0 then
            cprint("${dim}> checking for %s funcs(%s)", kind, table.concat(funcs, ", "))
        end
        if #links > 0 then
            cprint("${dim}> checking for %s links(%s)", kind, table.concat(links, ", "))
        end
        for idx_or_name, snippet in pairs(snippets) do
            local name = idx_or_name
            if type(name) == "number" then
                name = snippet:sub(1, 16)
            end
            cprint("${dim}> checking for %s snippet(%s)", kind, name)
        end
    end
    if errors and option.get("diagnosis") and #tostring(errors) > 0 then
        cprint("${color.warning}checkinfo:${clear dim} %s", errors)
    end
    return ok, ok and output or errors
end

