#!/usr/bin/env python3

from argparse import ArgumentParser
import json


def needs_arg_ptr_stub(decl):
	res = (decl['ret']['tag'] == 'S')
	for arg in decl['args']:
		if arg['type']['tag'] == 'S':
			res = True
	return(res)


def bindgen(apiName, spec, **kwargs):
	guest_stubs_path = kwargs.get("guest_stubs")
	guest_include = kwargs.get("guest_include")
	wasm3_bindings_path = kwargs.get("wasm3_bindings")

	if guest_stubs_path == None:
		guest_stubs_path = 'bindgen_' + apiName + '_guest_stubs.c'
	if wasm3_bindings_path == None:
		wasm3_bindings_path = 'bindgen_' + apiName + '_wasm3_bindings.c'

	host_bindings = open(wasm3_bindings_path, 'w')
	guest_bindings = None

	specFile = open(spec, 'r')
	data = json.load(specFile)

	for decl in data:
		if needs_arg_ptr_stub(decl):
			guest_bindings = open(guest_stubs_path, 'w')
			if guest_include != None:
				s = '#include"' + guest_include + '"\n\n'
				print(s, file=guest_bindings)
			break

	for decl in data:

		name = decl['name']
		cname = decl.get('cname', name)

		if needs_arg_ptr_stub(decl):
			argPtrStubName = name + '_argptr_stub'
			# pointer arg stub declaration
			s = ''
			if decl['ret']['tag'] == 'S':
				s += 'void'
			else:
				s += decl['ret']['name']

			s += ' ORCA_IMPORT(' + argPtrStubName + ') ('

			if decl['ret']['tag'] == 'S':
				s += decl['ret']['name'] + '* __retArg'
				if len(decl['args']) > 0:
					s += ', '

			for i, arg in enumerate(decl['args']):
				s += arg['type']['name']
				if arg['type']['tag'] == 'S':
					s += '*'
				s += ' ' + arg['name']
				if i+1 < len(decl['args']):
					s += ', '
			s += ');\n\n'

			# forward function to pointer arg stub declaration
			s += decl['ret']['name'] + ' ' + name + '('
			for i, arg in enumerate(decl['args']):
				s += arg['type']['name'] + ' ' + arg['name']
				if i+1 < len(decl['args']):
					s += ', '
			s += ')\n'
			s += '{\n'
			s += '\t'
			if decl['ret']['tag'] == 'S':
				s += decl['ret']['name'] + ' __ret;\n\t'
			elif decl['ret']['tag'] != 'v':
				s += decl['ret']['name']
				s += ' __ret = '
			s += argPtrStubName + '('

			if decl['ret']['tag'] == 'S':
				s += '&__ret'
				if len(decl['args']) > 0:
					s += ', '

			for i, arg in enumerate(decl['args']):
				if arg['type']['tag'] == 'S':
					s += '&'

				s += arg['name']
				if i+1 < len(decl['args']):
					s += ', '
			s += ');\n'
			if decl['ret']['tag'] != 'v':
				s += '\treturn(__ret);\n'
			s += '}\n\n'

			print(s, file=guest_bindings)

		# host-side stub
		s = 'const void* ' + cname + '_stub(IM3Runtime runtime, IM3ImportContext _ctx, uint64_t* _sp, void* _mem)'

		gen_stub = decl.get('gen_stub', True)
		if gen_stub == False:
			s += ';\n\n'
		else:
			s += '\n{\n'


			# NOTE: check and cast arguments
			retTag = decl['ret']['tag']

			firstArgIndex = 0
			if retTag != 'v':
				firstArgIndex = 1

			for argIndex, arg in enumerate(decl['args']):

				argName = arg['name']
				typeName = arg['type']['name']
				typeCName = arg['type'].get('cname', typeName)
				argTag = arg['type']['tag']

				s += '\t'

				if argTag == 'i':
					s += typeCName + ' ' + argName + ' = ('+typeCName+')*(i32*)&_sp[' + str(firstArgIndex + argIndex) + '];\n'
				elif argTag == 'I':
					s += typeCName + ' ' + argName + ' = ('+typeCName+')*(i64*)&_sp[' + str(firstArgIndex + argIndex) + '];\n'
				elif argTag == 'f':
					s += typeCName + ' ' + argName + ' = ('+typeCName+')*(f32*)&_sp[' + str(firstArgIndex + argIndex) + '];\n'
				elif argTag == 'F':
					s += typeCName + ' ' + argName + ' = ('+typeCName+')*(f64*)&_sp[' + str(firstArgIndex + argIndex) + '];\n'
				elif argTag == 'p':
					s += typeCName + ' ' + argName + ' = ('+ typeCName +')((char*)_mem + *(u32*)&_sp[' + str(firstArgIndex + argIndex) + ']);\n'
				elif argTag == 'S':
					s += typeCName + ' ' + argName + ' = *('+ typeCName +'*)((char*)_mem + *(u32*)&_sp[' + str(firstArgIndex + argIndex) + ']);\n'
				else:
					print('unrecognized type ' + c + ' in procedure signature\n')
					break

			# check pointer arg length
			for arg in decl['args']:

				argName = arg['name']
				typeName = arg['type']['name']
				typeCName = arg['type'].get('cname', typeName)
				argTag = arg['type']['tag']
				argLen = arg.get('len')

				if argTag == 'p' and argLen != None:

					s += '\t{\n'
					s += '\t\tOC_ASSERT(((char*)'+ argName + ' >= (char*)_mem) && (((char*)'+ argName +' - (char*)_mem) < m3_GetMemorySize(runtime)), "parameter \''+argName+'\' is out of bounds");\n'
					s += '\t\tOC_ASSERT((char*)' + argName + ' + '

					proc = argLen.get('proc')
					if proc != None:
						s += proc + '(runtime, '
						lenProcArgs = argLen['args']
						for i, lenProcArg in enumerate(lenProcArgs):
							s += lenProcArg
							if i < len(lenProcArgs)-1:
								s += ', '
						s += ')'
					else:
						components =  argLen.get('components')
						countArg = argLen.get('count')

						if components != None:
							s += str(components)
							if countArg != None:
								s += '*'
						if countArg != None:
							s += countArg

					if typeCName.endswith('**') or (typeCName.startswith('void') == False and typeCName.startswith('const void') == False):
						s += '*sizeof('+typeCName[:-1]+')'

					s += ' <= ((char*)_mem + m3_GetMemorySize(runtime)), "parameter \''+argName+'\' overflows wasm memory");\n'
					s += '\t}\n'

			s += '\t'

			if retTag == 'i':
				s += '*((i32*)&_sp[0]) = (i32)'
			elif retTag == 'I':
				s += '*((i64*)&_sp[0]) = (i64)'
			elif retTag == 'f':
				s += '*((f32*)&_sp[0]) = (f32)'
			elif retTag == 'F':
				s += '*((f64*)&_sp[0]) = (f64)'
			elif retTag == 'S':
				retTypeName = decl['ret']['name']
				retTypeCName = decl['ret'].get('cname', retTypeName)
				s += '*(' + retTypeCName + '*)((char*)_mem + *(i32*)&_sp[0]) = '
			elif retTag == 'p':
				print("Warning: " + name + ": pointer return type not supported yet")

			s += cname + '('

			for i, arg in enumerate(decl['args']):
				s += arg['name']

				if i+1 < len(decl['args']):
					s += ', '

			s += ');\n\treturn(0);\n}\n\n'

		print(s, file=host_bindings)

	# link function
	s = 'int bindgen_link_' + apiName + '_api(IM3Module module)\n{\n'
	s += '	M3Result res;\n'
	s += '	int ret = 0;\n'

	for decl in data:
		name = decl['name']
		cname = decl.get('cname', name)

		if needs_arg_ptr_stub(decl):
			name = name + '_argptr_stub'

		m3Sig = ''
		if decl['ret']['tag'] == 'S':
			m3Sig += 'v'
		else:
			m3Sig += decl['ret']['tag']

		m3Sig += '('
		if decl['ret']['tag'] == 'S':
			m3Sig += 'i'
		for arg in decl['args']:
			tag = arg['type']['tag']
			if tag == 'p' or tag == 'S':
				tag = 'i'
			m3Sig += tag
		m3Sig += ')'


		s += '	res = m3_LinkRawFunction(module, "*", "' + name + '", "' + m3Sig + '", ' + cname + '_stub);\n'
		s += '	if(res != m3Err_none && res != m3Err_functionLookupFailed)\n'
		s += '	{\n'
		s += '		oc_log_error("Couldn\'t link function ' + name + ' (%s)\\n", res);\n'
		s += '		ret = -1;\n'
		s += '	}\n\n'

	s += '\treturn(ret);\n}\n'

	print(s, file=host_bindings)


if __name__ == "__main__":
	parser = ArgumentParser(prog='bindgen.py')
	parser.add_argument('api')
	parser.add_argument('spec')
	parser.add_argument('-g', '--guest-stubs')
	parser.add_argument('--guest-include')
	parser.add_argument('--wasm3-bindings')

	args = parser.parse_args()

	apiName = args.api
	spec = args.spec
	guest_stubs_path = args.guest_stubs
	if guest_stubs_path == None:
		guest_stubs_path = 'bindgen_' + apiName + '_guest_stubs.c'

	wasm3_bindings_path = args.wasm3_bindings
	if wasm3_bindings_path == None:
		wasm3_bindings_path = 'bindgen_' + apiName + '_wasm3_bindings.c'

	bindgen(apiName, spec,
		guest_stubs=guest_stubs_path,
		guest_include=args.guest_include,
		wasm3_bindings=wasm3_bindings_path,
	)
