#!/usr/bin/lua
-- BindingGenerator.lua (c) hecks 2021
-- This script is a part of IrrlichtMT, released under the same license.

-- By default we assume you're running this from /scripts/
-- and that you have the necessary headers there (gitignored for your convenience)
local sourceTreePath = os.getenv( "IRRMTREPO" ) or "..";
-- Otherwise run this from wherever you want and set the above env variable.
local glHeaderPath = os.getenv( "GLHEADERPATH" ) or ".";
-- GL headers will be looked for in the current directory or GLHEADERPATH.
-- At the moment we require:
-- 		"glcorearb.h"
-- 		"gl2ext.h"
-- Files other than glcorearb.h are only parsed for vendor specific defines
-- and aliases. Otherwise we only use what exists in glcorearb.h, further
-- restricted to procedures that are either core or ARB.


-- Emulate a portion of the libraries that this was written against.
getmetatable( "" ).__index = string;
getmetatable( "" ).__len = string.len;
getmetatable( "" ).__call = string.format;
function string:Split( pat )
	local r = {};
	local pos = 1;
	local from, to;
	while pos and pos <= #self do
		from, to = self:find( pat, pos );
		if not from then
			break;
		end
		r[#r+1] = self:sub( pos, from - 1 );
		pos = to + 1;
	end
	r[#r+1] = self:sub( pos, #self );
	return r;
end
function string:TrimBothEnds()
	return self:gsub("^%s+",""):gsub("%s+$","");
end
local List;
List = function( t )
	return setmetatable( t or {}, {
		__index = {
			Add = function( t, v )
				t[#t+1] = v;
			end;
			AddFormat = function( t, str, ... )
				t:Add( str( ... ) );
			end;
			Where = function( t, f )
				local r = {};
				for i=1, #t do
					if f(t[i]) then r[#r+1] = t[i]; end
				end
				return List( r );
			end;
			Select = function( t, f )
				local r = {};
				for i=1, #t do
					r[#r+1] = f( t[i] );
				end
				return List( r );
			end;
			Join = function( t, n )
				local r = {};
				for i=1, #t do
					r[i] = t[i];
				end
				for i=1, #n do
					r[#r+1] = n[i];
				end
				return List( r );
			end;
			Concat = table.concat;
		};
	} );
end


------------ Header parsing ------------

-- GL and GLES alike
local driverVendors = {
	"NV", "AMD", "INTEL", "OVR", "QCOM", "IMG", "ANGLE", "APPLE", "MESA"
}
local vendorSuffixes = {
	"ARB", "EXT", "KHR", "OES",
	unpack( driverVendors )
};
local vendorSuffixPattern = {};
local constSuffixPattern = {};
for i=1, #vendorSuffixes do
	vendorSuffixPattern[i] = vendorSuffixes[i] .. "$";
	constSuffixPattern[i] = ("_%s$")( vendorSuffixes[i] );
end
local constBanned = {};
for i=1, #driverVendors do
	constBanned[driverVendors[i]] = true;
end
-- Strip the uppercase extension vendor suffix from a name.
local function StripVendorSuffix( str, const )
	local patterns = const and constSuffixPattern or vendorSuffixPattern;
	local n;
	for i=1, #patterns do
		str, n = str:gsub( patterns[i], "" );
		if n > 0 then
			return str, vendorSuffixes[i];
		end
	end
	return str;
end

-- Normalize the type of an argument or return, also stripping any suffix
-- and normalizing all whitespace regions to single spaces.
local function NormalizeType( str )
	local chunks = str:Split( "%s+" );
	for j=1, #chunks do
		chunks[j] = StripVendorSuffix( chunks[j] );
	end
	local T = table.concat(chunks, " " );
	return T:TrimBothEnds();
end

-- Normalize an argument, returning the normalized type and the name separately,
-- always sticking the * of a pointer to the type rather than the name.
-- We need this to generate a normalized arg list and function signature (below)
local function NormalizeArgument( str )
	local chunks = str:Split( "%s+" );
	for j=1, #chunks do
		chunks[j] = StripVendorSuffix( chunks[j] );
	end
	local last = chunks[#chunks];
	local name = last:match( "[%w_]+$" );
	chunks[#chunks] = #name ~= #last and last:sub( 1, #last-#name) or nil;
	local T = table.concat(chunks, " " ):TrimBothEnds();
	return T, name
end

-- Normalize an argument list so that two matching prototypes
-- will produce the same table if fed to this function.
local function NormalizeArgList( str )
	local args = str:Split( ",%s*" );
	local r = {};
	for i=1, #args do
		local T, name = NormalizeArgument( args[i] );
		r[i] = { T, name };
	end
	return r;
end

-- Normalize a function signature into a unique string for keying
-- in such a way that if two different GL procedures may be assigned
-- to the same function pointer, this will produce an identical string for both.
-- This makes it possible to detect function aliases that may work as a fallback.
-- You still have to check for the appropriate extension.
local function NormalizeFunctionSignature( T, str )
	local args = str:Split( ",%s*" );
	local r = {};
	for i=1, #args do
		r[i] = NormalizeArgument( args[i] );
	end
	return ("%s(%s)")( T, table.concat( r, ", " ) );
end

-- Mangle the PFN name so that we don't collide with a
-- typedef from any of the GL headers.
local pfnFormat = "PFNGL%sPROC_MT";

--( T, name, args )
local typedefFormat = "\ttypedef %s (APIENTRYP %s) (%s);"
-- Generate a PFN...GL style typedef for a procedure
--
local function GetProcedureTypedef( proc )
	local args = {};
	for i=1, #proc.args do
		args[i] = ("%s %s")( unpack( proc.args[i] ) )
	end
	return typedefFormat( proc.retType, pfnFormat( proc.name:upper() ), table.concat( args, ", " ) );
end

local procedures = List();
local nameset = {};
local definitions = List();
local consts = List();

--[[
	Structured procedure representation:

	ProcSpec = {
		string name;		-- Normalized name as it appears in the GL spec
		string? vendor;		-- Uppercase vendor string (ARB, EXT, AMD, NV etc)
		string signature;
		string retType;
		args = { { type, name } };
	};
]]
-- Parse a whole header, extracting the data.
local function ParseHeader( path, into, apiRegex, defs, consts, nameSet, noNewNames )
	defs:AddFormat( "\t// %s", path );
	local f = assert( io.open( path, "r" ), "Could not open " .. path );
	for line in f:lines() do
		-- Do not parse PFN typedefs; they're easily reconstructible.
		local T, rawName, args = line:match( apiRegex );
		if T then
			T = NormalizeType( T );
			-- Strip the 'gl' namespace prefix.
			local procName = rawName:sub(3,-1);
			local name, vendor = StripVendorSuffix( procName );
			if not (noNewNames and nameSet[name]) then
				nameSet[name] = true;
				into:Add{
					name = name;
					vendor = vendor;
					-- pfnType = pfnFormat( procName:upper() );
					signature = NormalizeFunctionSignature( T, args );
					retType = T;
					args = NormalizeArgList( args );
				};
			end
		elseif ( line:find( "#" ) and not line:find( "#include" ) ) then
			local rawName, value = line:match( "#define%s+GL_([_%w]+)%s+0x(%w+)" );
			if rawName and value then
				local name, vendor = StripVendorSuffix( rawName, true );
				if not constBanned[vendor] then
					consts:Add{ name = name, vendor = vendor, value = "0x"..value };
				end
			end
			::skip::
		elseif( line:find( "typedef" ) and not line:find( "%(" ) ) then
			-- Passthrough non-PFN typedefs
			defs:Add( "\t" .. line );
		end
	end
	defs:Add "";
	f:close();
end

------------ Parse the headers ------------

-- ES/gl2.h is a subset of glcorearb.h and does not need parsing.

local funcRegex = "GLAPI%s+(.+)APIENTRY%s+(%w+)%s*%((.*)%)";
local funcRegexES = "GL_APICALL%s+(.+)GL_APIENTRY%s+(%w+)%s*%((.*)%)";
ParseHeader( glHeaderPath .. "/glcorearb.h", procedures, funcRegex, definitions, consts, nameset );
ParseHeader( glHeaderPath .. "/gl2ext.h", procedures, funcRegexES, List(), consts, nameset, true );
-- Typedefs are redirected to a dummy list here on purpose.
-- The only unique typedef from gl2ext is this:
definitions:Add "\ttypedef void *GLeglClientBufferEXT;";

------------ Sort out constants ------------

local cppConsts = List();
do
	local constBuckets = {};
	for i=1, #consts do
		local vendor = consts[i].vendor or "core";
		constBuckets[consts[i].name] = constBuckets[consts[i].name] or {};
		constBuckets[consts[i].name][vendor] = consts[i].value;
	end
	local names = {};
	for i=1, #consts do
		local k = consts[i].name;
		local b = constBuckets[k];
		if k == "WAIT_FAILED" or k == "DIFFERENCE" then
			-- This is why using #define as const is evil.
			k = "_" .. k;
		end
		if b and not names[k] then
			names[k] = true;
			-- I have empirically tested that constants in GL with the same name do not differ,
			-- at least for these suffixes.
			local v = b.core or b.KHR or b.ARB or b.OES or b.EXT;
			if v then
				local T = v:find( "ull" ) and "GLuint64" or "GLenum";
				cppConsts:AddFormat( "\tstatic constexpr const %s %s = %s;", T, k, v );
			end
		end
	end
end


------------ Sort out procedures ------------

local procTable = {};

local coreProcedures = procedures:Where( function(x) return not x.vendor; end );
local arbProcedures = procedures:Where( function(x) return x.vendor == "ARB"; end );

-- Only consider core and ARB functions.
local nameList = coreProcedures:Join( arbProcedures ):Select(
	function(p)
		return p.name;
	end );

local nameSet = {};
local uniqueNames = List();

for s, k in ipairs( nameList ) do
	if not nameSet[k] then
		nameSet[k] = true;
		uniqueNames:Add( k );
	end
end

for i=1, #procedures do
	local p = procedures[i];
	procTable[p.name] = procTable[p.name] or {};
	local key = p.vendor or "core";
	procTable[p.name][key] = p;
end

local priorityList = List{ "core", "ARB", "OES", "KHR" };

local typedefs = List();
local pointers = List();
local loader = List();

for s, str in ipairs( uniqueNames ) do
	pointers:Add( ("\t%s %s = NULL;")( pfnFormat( str:upper() ), str ) );
	local typeDefGenerated = false;
	for i=1, #priorityList do
		local k = priorityList[i];
		local proc = procTable[str][k]
		if proc then
			if not typeDefGenerated then
				typedefs:Add( GetProcedureTypedef( proc ) );
				typeDefGenerated = true;
			end
			local vendor = k == "core" and "" or k;
			loader:AddFormat(
				'\tif (!%s) %s = (%s)cmgr->getProcAddress("%s");\n',
				str, str, pfnFormat( proc.name:upper() ), ("gl%s%s")(str,vendor)
			);
		end
	end
end


------------ Write files ------------

-- Write loader header
local f = io.open( sourceTreePath .. "/include/mt_opengl.h", "wb" );
f:write[[
// This code was generated by scripts/BindingGenerator.lua
// Do not modify it, modify and run the generator instead.

#pragma once

#include <string>
#include <unordered_set>
#include "IrrCompileConfig.h"
#include "irrTypes.h"
#include "IContextManager.h"
#include <KHR/khrplatform.h>

#ifndef APIENTRY
	#define APIENTRY
#endif
#ifndef APIENTRYP
	#define APIENTRYP APIENTRY *
#endif
#ifndef GLAPI
	#define GLAPI extern
#endif

]];

f:write[[
class OpenGLProcedures {
private:
]];
f:write( definitions:Concat( "\n" ) );
f:write( "\n" );
f:write[[
	// The script will miss this particular typedef thinking it's a PFN,
	// so we have to paste it in manually. It's the only such type in OpenGL.
	typedef void (APIENTRY *GLDEBUGPROC)
		(GLenum source,GLenum type,GLuint id,GLenum severity,GLsizei length,const GLchar *message,const void *userParam);

]]
f:write( typedefs:Concat( "\n" ) );
f:write( "\n\n" );
f:write [[
	std::unordered_set<std::string> extensions;
public:
	// Call this once after creating the context.
	void LoadAllProcedures(irr::video::IContextManager *cmgr);
	// Check if an extension is supported.
	inline bool IsExtensionPresent(const std::string &ext)
	{
		return extensions.find(ext) != extensions.end();
	}

]];
f:write( pointers:Concat( "\n" ) );
f:write( "\n\n" );
f:write( cppConsts:Concat( "\n" ) );
f:write( "\n\n" );
f:write[[
	static constexpr const GLenum ZERO = 0;
	static constexpr const GLenum ONE = 1;
	static constexpr const GLenum NONE = 0;
]];
f:write( "};\n" );
f:write( "\n//Global GL procedures object.\n" );
f:write( "IRRLICHT_API extern OpenGLProcedures GL;\n" );
f:close();

-- Write loader implementation
f = io.open( sourceTreePath .. "/source/Irrlicht/mt_opengl_loader.cpp", "wb" );
f:write[[
#include "mt_opengl.h"
#include <string>
#include <sstream>

OpenGLProcedures GL = OpenGLProcedures();

void OpenGLProcedures::LoadAllProcedures(irr::video::IContextManager *cmgr)
{

]];
f:write( loader:Concat() );
f:write[[

	// get the extension string, chop it up
	std::string ext_string = std::string((char*)GetString(EXTENSIONS));
	std::stringstream ext_ss(ext_string);
	std::string tmp;
	while (std::getline(ext_ss, tmp, ' '))
		extensions.emplace(tmp);

}
]];
f:close();
