local A=require("\108\105\098\099\108\097\110\103-\108\117\097")local m=require("\108\102\115")local Y=require("\115\114\099\046\117\116\105\108\115")local x=Y.get_script_dir()assert(x,"\085\110\097\098\108\101 \116\111 \100\101\116\101\114\109\105\110\101 \115\099\114\105\112\116 \112\097\116\104\033")local a=Y.path_combine(x,"\046\046","\115\116\117\098\115")if not Y.file_exists(a)then assert(m.mkdir(a))end local s={require("\115\112\101\099\115\046\103\116\107\095\115\112\101\099"),require("\115\112\101\099\115\046\103\108\105\098\095\115\112\101\099");require("\115\112\101\099\115\046\103\111\098\106\101\099\116\095\115\112\101\099"),require("\115\112\101\099\115\046\103\105\111\095\115\112\101\099");require("\115\112\101\099\115\046\103\109\111\100\117\108\101\095\115\112\101\099");require("\115\112\101\099\115\046\103\105\114\101\112\111\115\105\116\111\114\121\095\115\112\101\099");require("\115\112\101\099\115\046\103\108\102\119\095\115\112\101\099");require("\115\112\101\099\115\046\111\112\101\110\103\108\095\115\112\101\099"),require("\115\112\101\099\115\046\115\100\108\050\095\115\112\101\099"),require("\115\112\101\099\115\046\115\100\108\050\095\116\116\102\095\115\112\101\099"),require("\115\112\101\099\115\046\115\100\108\050\095\105\109\097\103\101\095\115\112\101\099");nil}local function t(A)if A=="\067\104\097\114\095\083"then return"\099\104\097\114"elseif A=="\076\111\110\103\068\111\117\098\108\101"then return"\108\111\110\103 \100\111\117\098\108\101"elseif A=="\085\073\110\116"then return"\117\110\115\105\103\110\101\100 \105\110\116"elseif A=="\085\067\104\097\114"then return"\117\110\115\105\103\110\101\100 \099\104\097\114"elseif A=="\085\076\111\110\103"then return"\117\110\115\105\103\110\101\100 \108\111\110\103"elseif A=="\085\083\104\111\114\116"then return"\117\110\115\105\103\110\101\100 \115\104\111\114\116"else return A:lower()end end local function I(A,m)local Y=A local x=""while Y:name()=="\080\111\105\110\116\101\114"do local A=Y:is_const()and" \099\111\110\115\116"or""local a=Y:is_volatile()and" \118\111\108\097\116\105\108\101"or""local s=Y:is_restrict()and" \114\101\115\116\114\105\099\116"or""if not m then x=x..(A..(a.." \042"))else x=x..(" \042"..(A..(a..s)))end Y=Y:pointee()end if Y:name()=="\069\108\097\098\111\114\097\116\101\100"then local A=(Y:get_named_type()):name()=="\082\101\099\111\114\100"and"\115\116\114\117\099\116 "or""local m=Y:is_const()and"\099\111\110\115\116 "or""local a=Y:is_volatile()and"\118\111\108\097\116\105\108\101 "or""local s=Y:is_restrict()and"\114\101\115\116\114\105\099\116 "or""local t=Y:declaration()return m..(a..(s..(A..(t:name()..x)))),nil elseif Y:name()=="\067\111\110\115\116\097\110\116\065\114\114\097\121"then local A=Y:get_array_element_type()local m=Y:get_array_size()return I(A,true),m elseif Y:name()=="\073\110\099\111\109\112\108\101\116\101\065\114\114\097\121"then local A=Y:get_array_element_type()return I(A,true),0 elseif Y:name()=="\070\117\110\099\116\105\111\110\080\114\111\116\111"then local A=I(Y:result_type())return A,nil,Y:arguments()else local A=Y:is_const()and"\099\111\110\115\116 "or""local m=Y:is_volatile()and"\118\111\108\097\116\105\108\101 "or""local a=Y:is_restrict()and"\114\101\115\116\114\105\099\116 "or""return A..(m..(a..(t(Y:name())..x))),nil end end local Q local function Z(A,m,Y,x)assert(not(Y and x),"\102\109\116\095\097\114\103 \109\117\115\116 \111\110\108\121 \104\097\118\101 \110\097\109\101\095\111\110\108\121 \111\114 \105\103\110\111\114\101\095\109\105\115\115\105\110\103\095\110\097\109\101 \115\101\116\044 \110\111\116 \098\111\116\104")local a=A.type and A:name()or""if not x and(not a or#a==0)then a="\095"..tostring(m)end if Y then return a end local s,t,Z=I(A.type and A:type()or A)local W=""if t==0 then W="\091\093"elseif t then W=string.format("\091\037\115\093",t)end if#W~=0 then a=a..W end if not Z then return string.format("\037\115 \037\115",s,a)else return string.format("\037\115 \040\042\037\115\041\040\037\115\041",s,a,Q(Z,(A:type()):is_variadic(),false,true))end end Q=function(A,m,x,a)local s if#A==0 then s=x and""or"\118\111\105\100"else s=table.concat(Y.transform(A,function(A,m)return Z(m,A,x,a)end),"\044 ")end if m and not x then s=s.."\044 \046\046\046"end return s end local function W(A,m,x)local a={}local s=Y.split(m,"\095")table.insert(a,m.."\091\095\093\042\118\097\042\036")table.insert(a,m.."\091\095\093\042\118\097\108\105\115\116\036")for A=1,#s,1 do local m=""for Y,x in ipairs(s)do m=m..((Y~=1 and"\095"or"")..((Y==A and"\118"or"")..x))end table.insert(a,m)end local function t(A,Y,x,a)print(string.format("\070\111\117\110\100 \118\097\095\101\113\117\105\118 \102\111\114 \102\117\110\099 \037\115 \097\115 \102\117\110\099 \037\115\040\037\115\041 \119\105\116\104 \112\097\116\116\101\114\110 \037\115\044 \098\117\116 \101\114\114\058 \037\115\n",m,A,Q(Y),x,a))end local function Z(A)for A,m in ipairs(A)do if(I(m:type())):match("\118\097\095\108\105\115\116")then return true end end return false end for A,m in ipairs(A)do local Y=m:name()local s=m:arguments()for A,a in ipairs(a)do if Y:match(a)then if not Z(s)then t(Y,s,a,"\118\097\095\108\105\115\116 \109\105\115\115\105\110\103")elseif#s~=#x+1 then t(Y,s,a,string.format("\097\114\103\115 \099\111\117\110\116 \109\105\115\109\097\116\099\104\044 \101\120\112\101\099\116\101\100\058 \037\115\044 \102\111\117\110\100\058 \037\115",#x+1,#s))else return m end end end end return nil end local D="\095\095\098\117\105\108\116\105\110\095\117\110\114\101\097\099\104\097\098\108\101\040\041\059 "local function o(A,m,Y,x,a)if a then return string.format("\037\115 \040\042\037\115\040\037\115\041\041\040\037\115\041",m,A,x,Q(a,Y:is_variadic(),false))else return string.format("\037\115 \040\037\115\041\040\037\115\041",m,A,x)end end local function P(A,m,Y,x,a)local s=W(A,m,Y)if not s then return nil end local t=x~="\118\111\105\100"and not a local I=t and x.." \114\101\116\059 "or""local Z=s:arguments()return string.format("\123 \037\115\118\097\095\108\105\115\116 \118\097\097\114\103\115\059 \118\097\095\115\116\097\114\116\040\118\097\097\114\103\115\044 \037\115\041\059 \037\115\115\116\117\098\095\102\117\110\099\115\046\112\116\114\095\037\115\040\037\115\041\059 \118\097\095\101\110\100\040\118\097\097\114\103\115\041\059 \037\115\125",I,Y[#Y]:name(),t and"\114\101\116 \061 "or"",s:name(),Q(Y,true,true)..string.format("\044 \037\115\118\097\097\114\103\115",(Z[#Z]:type()):name()=="\080\111\105\110\116\101\114"and"\038"or""),s:is_no_return()and D or t and"\114\101\116\117\114\110 \114\101\116\059 "or"")end local function q(A,m,Y)local x=m:name()local a,s,t=I(m:result_type())local Z=m:arguments()local W=(m:type()):is_variadic()local q=Q(Z,W,false)local i=o(x,a,m:result_type(),q,t)local c if t then c=string.format("\040\042\112\116\114\095\037\115\041",x)else c="\042\112\116\114\095"..x end local j=o(c,a,m:result_type(),q,t)local r=true local B if W then B=P(A,x,Z,a,t)if not B then r=false end else local A=a=="\118\111\105\100"and(not t and"")or"\114\101\116\117\114\110 "local Y=Q(Z,false,true)B=string.format("\123 \037\115\115\116\117\098\095\102\117\110\099\115\046\112\116\114\095\037\115\040\037\115\041\059 \037\115\125",A,x,Y,m:is_no_return()and D or"")end return string.format("    \037\115\059",j),string.format("    \115\116\117\098\095\102\117\110\099\115\046\112\116\114\095\037\115 \061 \116\114\121\095\102\105\110\100\095\115\121\109\040\037\115\044 \"\037\115\"\041\059",x,Y,x),B and string.format("\037\115 \037\115",i,B)or nil,i.."\059",r end for x,s in ipairs(s)do print("\071\101\110\101\114\097\116\105\110\103 "..s.name)local t=Y.path_combine(a,s.name.."-\115\116\117\098")if not Y.file_exists(t)then assert(m.mkdir(t))end local I=Y.path_combine(t,s.name.."\095\115\116\117\098\046\099")local Q={}local Z=Y.path_combine(t,s.name.."\095\115\116\117\098\046\104")local W={}Y.tbl_extend(Q,{string.format("\035\105\110\099\108\117\100\101 \"\037\115\095\115\116\117\098\046\104\"",s.name),"\035\105\110\099\108\117\100\101 \060\115\116\100\098\111\111\108\046\104\062","\035\105\110\099\108\117\100\101 \060\115\116\100\105\111\046\104\062";"\035\105\110\099\108\117\100\101 \060\115\116\100\108\105\098\046\104\062";"","\035\100\101\102\105\110\101 \095\067\079\077\083\079\095\083\079\085\082\067\069";"\035\105\110\099\108\117\100\101 \060\108\105\098\099\047\100\108\111\112\101\110\047\100\108\102\099\110\046\104\062";""})assert(arg[1],"\080\097\115\115 \121\111\117\114 \099\108\097\110\103 \105\110\099\108\117\100\101 \100\105\114\101\099\116\111\114\121 \097\115 \097\114\103\117\109\101\110\116 \049\033")local D={"-\105\115\121\115\116\101\109",arg[1]}for A,m in ipairs(s.search_dirs or{})do table.insert(D,"-\105\115\121\115\116\101\109")table.insert(D,m)end Y.tbl_extend(D,{s.hfile})local o=A.create_index(false,true)local P=assert(o:parse_translation_unit(D))local function i(A)local m={}local function Y(A)for A,x in ipairs(A:children())do if x and(x:kind()=="\070\117\110\099\116\105\111\110\068\101\099\108"and not x:is_inlined())then local A=x:location()or""local Y=false if A==s.hfile then Y=true else for m,x in ipairs(s.filter)do if A:find(x)then Y=true break end end end if Y then table.insert(m,x)end end Y(x)end end Y(A)return m end local c=i(P:cursor())table.insert(Q,string.format("\115\116\097\116\105\099 \115\116\114\117\099\116 \037\115\070\117\110\099\115 \123",s.name))local j={}local r={}local B={}local b={}for A,m in ipairs(c)do local Y,x,a,t,I=q(c,m,s.so.name)local Q=s.explicit_function_bodies and s.explicit_function_bodies[m:name()]if Q then a=Q end if Y and(x and(a and t))then table.insert(j,Y)table.insert(r,x)table.insert(B,a)table.insert(b,t)elseif I then print("\115\107\105\112\112\105\110\103 \109\097\116\099\104\108\101\115\115 \118\097 \102\117\110\099\116\105\111\110 "..m:name())end end Y.tbl_extend(Q,j)Y.tbl_extend(Q,{"\125 \115\116\117\098\095\102\117\110\099\115\059","",string.format("\118\111\105\100 \042\037\115\059",s.so.name);"";string.format("\118\111\105\100 \105\110\105\116\105\097\108\105\122\101\095\037\115\040\118\111\105\100\041 \123",s.name);string.format("    \099\104\097\114 \042\099\097\110\100\105\100\097\116\101\115\095\037\115\091\093 \061 \123 \037\115 \125\059",s.so.name,table.concat(Y.transform(s.so.fnames,function(A,m)return string.format("\"\037\115\"",m)end),"\044 "));string.format("    \037\115 \061 \116\114\121\095\102\105\110\100\095\108\105\098\040\099\097\110\100\105\100\097\116\101\115\095\037\115\044 \037\115\041\059",s.so.name,s.so.name,#s.so.fnames),"";string.format("    \105\102 \040\033\037\115\041 \123",s.so.name),string.format("        \102\112\114\105\110\116\102\040\115\116\100\101\114\114\044 \"\085\110\097\098\108\101 \116\111 \108\111\099\097\116\101 \037\115\044 \101\120\105\116\105\110\103\033\"\041\059",s.so.name);"        \101\120\105\116\040\049\041\059";"    \125";""})Y.tbl_extend(Q,r)table.insert(Q,"\125")table.insert(Q,"")Y.tbl_extend(Q,B)Y.tbl_extend(Q,{"";string.format("\118\111\105\100 \099\108\111\115\101\095\037\115\040\118\111\105\100\041 \123",s.name),string.format("    \099\111\115\109\111\095\100\108\099\108\111\115\101\040\037\115\041\059",s.so.name);"\125"})table.insert(W,"\035\105\110\099\108\117\100\101 \"\046\046\047\115\116\117\098\046\104\"")Y.tbl_extend(W,Y.transform(s.header_includes,function(A,m)return string.format("\035\105\110\099\108\117\100\101 \"\037\115\"",m)end))Y.tbl_extend(W,{"",string.format("\118\111\105\100 \105\110\105\116\105\097\108\105\122\101\095\037\115\040\118\111\105\100\041\059",s.name),string.format("\118\111\105\100 \099\108\111\115\101\095\037\115\040\118\111\105\100\041\059",s.name);""})Y.tbl_extend(W,b)Y.file_write(I,table.concat(Q,"\n"))Y.file_write(Z,table.concat(W,"\n"))end
local clang = require("libclang-lua")
local lfs = require("lfs")
local utils = require("src.utils")

-- BEGIN!!

local script_path = utils.get_script_dir()
assert(script_path, "Unable to determine script path!")

local stubs_root = utils.path_combine(script_path, "..", "stubs")
if not utils.file_exists(stubs_root) then
    assert(lfs.mkdir(stubs_root))
end

local stubs = {
    require("specs.gtk_spec"),
    require("specs.glib_spec"),
    require("specs.gobject_spec"),
    require("specs.gio_spec"),
    require("specs.gmodule_spec"),
    require("specs.girepository_spec"),
    require("specs.glfw_spec"),
    require("specs.opengl_spec"),
    require("specs.sdl2_spec"),
    require("specs.sdl2_ttf_spec"),
    require("specs.sdl2_image_spec"),
    nil,
}

--- @param name string
--- @return string
local function cxtype_name(name)
    if name == "Char_S" then
        return "char"
    elseif name == "LongDouble" then
        return "long double"
    elseif name == "UInt" then
        return "unsigned int"
    elseif name == "UChar" then
        return "unsigned char"
    elseif name == "ULong" then
        return "unsigned long"
    elseif name == "UShort" then
        return "unsigned short"
    else
        return name:lower()
    end
end

--- @return string name, integer|nil len, table|nil arguments
local function get_full_type(type, flip)
    local _type = type

    local ptr_str = ""

    while _type:name() == "Pointer" do
        local const_str = _type:is_const() and " const" or ""
        local volatile_str = _type:is_volatile() and " volatile" or ""
        local restrict_str = _type:is_restrict() and " restrict" or ""

        if not flip then
            ptr_str = ptr_str .. const_str .. volatile_str .. " *"
        else
            ptr_str = ptr_str .. " *" .. const_str .. volatile_str .. restrict_str
        end

        _type = _type:pointee()
    end

    if _type:name() == "Elaborated" then
        local struct_str = _type:get_named_type():name() == "Record" and "struct " or ""
        local const_str = _type:is_const() and "const " or ""
        local volatile_str = _type:is_volatile() and "volatile " or ""
        local restrict_str = _type:is_restrict() and "restrict " or ""
        local e_type = _type:declaration()

        return const_str .. volatile_str .. restrict_str .. struct_str .. e_type:name() .. ptr_str, nil
    elseif _type:name() == "ConstantArray" then
        local array_type = _type:get_array_element_type()
        local len = _type:get_array_size()

        return get_full_type(array_type, true), len
    elseif _type:name() == "IncompleteArray" then
        local array_type = _type:get_array_element_type()

        return get_full_type(array_type, true), 0
    elseif _type:name() == "FunctionProto" then
        local result_type = get_full_type(_type:result_type())

        return result_type, nil, _type:arguments()
    else
        local const_str = _type:is_const() and "const " or ""
        local volatile_str = _type:is_volatile() and "volatile " or ""
        local restrict_str = _type:is_restrict() and "restrict " or ""

        return const_str .. volatile_str .. restrict_str .. cxtype_name(_type:name()) .. ptr_str, nil
    end
end

local fmt_args

local function fmt_arg(arg, idx, name_only, ignore_missing_name)
    assert(
        not (name_only and ignore_missing_name),
        "fmt_arg must only have name_only or ignore_missing_name set, not both"
    )

    local name = arg.type and arg:name() or ""

    if not ignore_missing_name and (not name or #name == 0) then
        name = "_" .. tostring(idx)
    end

    if name_only then
        return name
    end

    -- If when passing in fptr_args recursively, it's a list of types and not a
    -- list of cursors
    local type, len, fptr_args = get_full_type(arg.type and arg:type() or arg)
    local array_str = ""
    if len == 0 then
        array_str = "[]"
    elseif len then
        array_str = string.format("[%s]", len)
    end

    if #array_str ~= 0 then
        name = name .. array_str
    end

    if not fptr_args then
        return string.format("%s %s", type, name)
    else
        return string.format("%s (*%s)(%s)", type, name, fmt_args(fptr_args, arg:type():is_variadic(), false, true))
    end
end

fmt_args = function(args, is_var, name_only, ignore_missing_name)
    local ret
    if #args == 0 then
        ret = name_only and "" or "void"
    else
        ret = table.concat(
            utils.transform(args, function(k, v) return fmt_arg(v, k, name_only, ignore_missing_name) end),
            ", "
        )
    end

    if is_var and not name_only then
        ret = ret .. ", ..."
    end

    return ret
end

local function try_find_va_equivalent(funcs, fname, fargs)
    local patterns = {}
    local fsplit = utils.split(fname, "_")

    table.insert(patterns, fname .. "[_]*va*$")
    table.insert(patterns, fname .. "[_]*valist$")

    for i = 1, #fsplit do
        local prefix_vfname = ""

        for j, segment in ipairs(fsplit) do
            prefix_vfname = prefix_vfname .. (j ~= 1 and "_" or "") .. (j == i and "v" or "") .. segment
        end

        table.insert(patterns, prefix_vfname)
    end

    local function print_err(name, args, pattern, err)
        print(
            string.format(
                "Found va_equiv for func %s as func %s(%s) with pattern %s, but err: %s\n",
                fname,
                name,
                fmt_args(args),
                pattern,
                err
            )
        )
    end

    local function check_args(args)
        for _, arg in ipairs(args) do
            if get_full_type(arg:type()):match("va_list") then
                return true
            end
        end

        return false
    end

    for _, func in ipairs(funcs) do
        local name = func:name()
        local args = func:arguments()
        for _, _pattern in ipairs(patterns) do
            if name:match(_pattern) then
                if not check_args(args) then
                    print_err(name, args, _pattern, "va_list missing")
                elseif #args ~= #fargs + 1 then
                    print_err(
                        name,
                        args,
                        _pattern,
                        string.format("args count mismatch, expected: %s, found: %s", #fargs + 1, #args)
                    )
                else
                    return func
                end
            end
        end
    end

    return nil
end

local RAYO_COSMICO = "__builtin_unreachable(); "

--- @return string sig
local function gen_func_signature(name, ret_str, ret_type, args_str, fptr_args)
    -- TODO: What if the function pointer returns a function pointer
    if fptr_args then
        return string.format(
            "%s (*%s(%s))(%s)",
            ret_str,
            name,
            args_str,
            fmt_args(fptr_args, ret_type:is_variadic(), false)
        )
    else
        return string.format("%s (%s)(%s)", ret_str, name, args_str)
    end
end

local function gen_variadic_func(funcs, fname, fargs, ret_type_str, fptr_args)
    local va_equiv = try_find_va_equivalent(funcs, fname, fargs)

    if not va_equiv then
        return nil
    end

    local nonvoid = ret_type_str ~= "void" and not fptr_args
    local ret_var = nonvoid and (ret_type_str .. " ret; ") or ""

    local va_equiv_args = va_equiv:arguments()

    return string.format(
        "{ %sva_list vaargs; va_start(vaargs, %s); %sstub_funcs.ptr_%s(%s); va_end(vaargs); %s}",
        ret_var,
        fargs[#fargs]:name(),
        nonvoid and "ret = " or "",
        va_equiv:name(),
        fmt_args(fargs, true, true)
            .. string.format(", %svaargs", va_equiv_args[#va_equiv_args]:type():name() == "Pointer" and "&" or ""),
        va_equiv:is_no_return() and RAYO_COSMICO or nonvoid and "return ret; " or ""
    )
end

--- return string c struct def, string c load sym, string c function def, string header function def
local function gen_func(funcs, func, soname)
    local name = func:name()
    local ret_type_str, _, fptr_args = get_full_type(func:result_type())
    local args = func:arguments()
    local is_variadic = func:type():is_variadic()

    local args_str = fmt_args(args, is_variadic, false)

    local sig = gen_func_signature(name, ret_type_str, func:result_type(), args_str, fptr_args)

    local struct_def_sig_name
    if fptr_args then
        struct_def_sig_name = string.format("(*ptr_%s)", name)
    else
        struct_def_sig_name = "*ptr_" .. name
    end

    local struct_def_sig =
        gen_func_signature(struct_def_sig_name, ret_type_str, func:result_type(), args_str, fptr_args)

    local success = true
    local func_body
    if is_variadic then
        func_body = gen_variadic_func(funcs, name, args, ret_type_str, fptr_args)

        if not func_body then
            success = false
        end
    else
        local ret_kwd = (ret_type_str == "void" and not fptr_args and "") or "return "
        local args_inner_str = fmt_args(args, false, true)

        func_body = string.format(
            "{ %sstub_funcs.ptr_%s(%s); %s}",
            ret_kwd,
            name,
            args_inner_str,
            func:is_no_return() and RAYO_COSMICO or ""
        )
    end

    return string.format("    %s;", struct_def_sig),
        string.format('    stub_funcs.ptr_%s = try_find_sym(%s, "%s");', name, soname, name),
        func_body and string.format("%s %s", sig, func_body) or nil,
        sig .. ";",
        success
end

for _, stub in ipairs(stubs) do
    print("Generating " .. stub.name)
    local stub_dir = utils.path_combine(stubs_root, stub.name .. "-stub")
    if not utils.file_exists(stub_dir) then
        assert(lfs.mkdir(stub_dir))
    end

    local c_out_path = utils.path_combine(stub_dir, stub.name .. "_stub.c")
    local c_text = {}
    local h_out_path = utils.path_combine(stub_dir, stub.name .. "_stub.h")
    local h_text = {}

    utils.tbl_extend(c_text, {
        string.format('#include "%s_stub.h"', stub.name),
        "#include <stdbool.h>",
        "#include <stdio.h>",
        "#include <stdlib.h>",
        "",
        "#define _COMSO_SOURCE",
        "#include <libc/dlopen/dlfcn.h>",
        "",
    })

    assert(arg[1], "Pass your clang include directory as argument 1!")
    local parse_args = { "-isystem", arg[1] }
    for _, dir in ipairs(stub.search_dirs or {}) do
        table.insert(parse_args, "-isystem")
        table.insert(parse_args, dir)
    end
    utils.tbl_extend(parse_args, { stub.hfile })

    local idx = clang.create_index(false, true)
    local tu = assert(idx:parse_translation_unit(parse_args))

    local function find_functions(cursor)
        local ret = {}

        local function find(_cursor)
            for _, c in ipairs(_cursor:children()) do
                if c and c:kind() == "FunctionDecl" and not c:is_inlined() then
                    local loc = c:location() or ""
                    local matched_any = false
                    if loc == stub.hfile then
                        matched_any = true
                    else
                        for _, filter in ipairs(stub.filter) do
                            if loc:find(filter) then
                                matched_any = true
                                break
                            end
                        end
                    end

                    if matched_any then
                        table.insert(ret, c)
                    end
                end

                find(c)
            end
        end

        find(cursor)

        return ret
    end

    local funcs = find_functions(tu:cursor())

    table.insert(c_text, string.format("static struct %sFuncs {", stub.name))

    local c_struct_defs = {}
    local c_load_syms = {}
    local c_func_defs = {}
    local h_func_defs = {}

    for _, func in ipairs(funcs) do
        local c_struct_def, c_load_sym, c_func_def, h_func_def, var_failed = gen_func(funcs, func, stub.so.name)

        local explicit_body = stub.explicit_function_bodies and stub.explicit_function_bodies[func:name()]
        if explicit_body then
            c_func_def = explicit_body
        end

        if c_struct_def and c_load_sym and c_func_def and h_func_def then
            table.insert(c_struct_defs, c_struct_def)
            table.insert(c_load_syms, c_load_sym)
            table.insert(c_func_defs, c_func_def)
            table.insert(h_func_defs, h_func_def)
        elseif var_failed then
            print("skipping matchless va function " .. func:name())
        end
    end

    utils.tbl_extend(c_text, c_struct_defs)

    utils.tbl_extend(c_text, {
        "} stub_funcs;",
        "",
        string.format("void *%s;", stub.so.name),
        "",
        string.format("void initialize_%s(void) {", stub.name),
        string.format(
            "    char *candidates_%s[] = { %s };",
            stub.so.name,
            table.concat(utils.transform(stub.so.fnames, function(_, v) return string.format('"%s"', v) end), ", ")
        ),
        string.format("    %s = try_find_lib(candidates_%s, %s);", stub.so.name, stub.so.name, #stub.so.fnames),
        "",
        string.format("    if (!%s) {", stub.so.name),
        string.format('        fprintf(stderr, "Unable to locate %s, exiting!");', stub.so.name),
        "        exit(1);",
        "    }",
        "",
    })

    utils.tbl_extend(c_text, c_load_syms)
    table.insert(c_text, "}")
    table.insert(c_text, "")

    utils.tbl_extend(c_text, c_func_defs)

    utils.tbl_extend(c_text, {
        "",
        string.format("void close_%s(void) {", stub.name),
        string.format("    cosmo_dlclose(%s);", stub.so.name),
        "}",
    })

    table.insert(h_text, '#include "../stub.h"')
    utils.tbl_extend(
        h_text,
        utils.transform(stub.header_includes, function(_, v) return string.format('#include "%s"', v) end)
    )
    utils.tbl_extend(h_text, {
        "",
        string.format("void initialize_%s(void);", stub.name),
        string.format("void close_%s(void);", stub.name),
        "",
    })

    utils.tbl_extend(h_text, h_func_defs)

    utils.file_write(c_out_path, table.concat(c_text, "\n"))
    utils.file_write(h_out_path, table.concat(h_text, "\n"))
end
