/**************************************************************************/
/*  compat_checker.c                                                      */
/**************************************************************************/
/*                         This file is part of:                          */
/*                             GODOT ENGINE                               */
/*                        https://godotengine.org                         */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
/*                                                                        */
/* Permission is hereby granted, free of charge, to any person obtaining  */
/* a copy of this software and associated documentation files (the        */
/* "Software"), to deal in the Software without restriction, including    */
/* without limitation the rights to use, copy, modify, merge, publish,    */
/* distribute, sublicense, and/or sell copies of the Software, and to     */
/* permit persons to whom the Software is furnished to do so, subject to  */
/* the following conditions:                                              */
/*                                                                        */
/* The above copyright notice and this permission notice shall be         */
/* included in all copies or substantial portions of the Software.        */
/*                                                                        */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
/**************************************************************************/

#include "compat_checker.h"

#include <stdbool.h>
#include <stdio.h>
#include <string.h>

GDExtensionInterfaceClassdbGetMethodBind classdb_get_method_bind = NULL;
GDExtensionInterfaceVariantGetPtrBuiltinMethod variant_get_ptr_builtin_method = NULL;
GDExtensionInterfaceVariantGetPtrUtilityFunction variant_get_ptr_utility_function = NULL;

GDExtensionPtrDestructor string_name_destructor = NULL;
GDExtensionInterfaceStringNameNewWithLatin1Chars string_name_new_with_latin1_chars = NULL;

typedef struct
{
	uint8_t data[8];
} StringName;

/**
 * Platform APIs are being registered only after extensions, making them unavailable during initialization (on any level).
 *
 * Due to that we run the tests in Mainloop `startup_callback` (available since Godot 4.5), called after the initialization.
 */
void startup_func() {
	bool success = (builtin_methods_compatibility_test() && class_methods_compatibility_test() && utility_functions_compatibility_test());
	if (success) {
		fprintf(stdout, "Outcome = SUCCESS\n");
	} else {
		fprintf(stderr, "Outcome = FAILURE\n");
	}
	fprintf(stdout, "COMPATIBILITY TEST FINISHED.\n");
}

GDExtensionMainLoopCallbacks callbacks = {
	(GDExtensionMainLoopStartupCallback)startup_func,
	NULL,
	NULL
};

void initialize_compatibility_test(void *p_userdata, GDExtensionInitializationLevel p_level) {}

void deinitialize_compatibility_test(void *p_userdata, GDExtensionInitializationLevel p_level) {}

GDExtensionBool builtin_methods_compatibility_test() {
	FILE *file = fopen("./builtin_methods.txt", "r");
	if (file == NULL) {
		fprintf(stderr, "Failed to open file `builtin_methods.txt` \n");
		return false;
	}

	bool ret = true;
	char line[512];

	while (fgets(line, sizeof line, file) != NULL) {
		int variant_type;
		char method_name[128];
		GDExtensionInt hash;
		if (sscanf(line, "%d %s %ld", &variant_type, method_name, &hash) != 3) {
			continue;
		}

		StringName method_stringname;
		string_name_new_with_latin1_chars(&method_stringname, method_name, false);
		GDExtensionPtrBuiltInMethod method_bind = variant_get_ptr_builtin_method(variant_type, &method_stringname, hash);

		if (method_bind == NULL) {
			fprintf(stderr, "Method bind not found: %d::%s (hash: %ld)\n", variant_type, method_name, hash);
			ret = false;
		}

		string_name_destructor(&method_stringname);
	}

	fclose(file);
	return ret;
}

GDExtensionBool utility_functions_compatibility_test() {
	FILE *file = fopen("./utility_functions.txt", "r");
	if (file == NULL) {
		fprintf(stderr, "Failed to open file `utility_functions.txt` \n");
		return false;
	}

	bool ret = true;
	char line[256];

	while (fgets(line, sizeof line, file) != NULL) {
		char method_name[128];
		GDExtensionInt hash;
		if (sscanf(line, "%s %ld", method_name, &hash) != 2) {
			continue;
		}

		StringName method_stringname;
		string_name_new_with_latin1_chars(&method_stringname, method_name, false);
		GDExtensionPtrUtilityFunction function_bind = variant_get_ptr_utility_function(&method_stringname, hash);

		if (function_bind == NULL) {
			fprintf(stderr, "Utility function not found: %s (hash: %ld)\n", method_name, hash);
			ret = false;
		}

		string_name_destructor(&method_stringname);
	}

	fclose(file);
	return ret;
}

GDExtensionBool class_methods_compatibility_test() {
	FILE *file = fopen("./class_methods.txt", "r");
	if (file == NULL) {
		fprintf(stderr, "Failed to open file `class_methods.txt` \n");
		return false;
	}

	char current_class_name[128] = "";
	bool ret = true;
	char line[512];
	bool has_class_string = false;
	StringName p_classname;

	while (fgets(line, sizeof(line), file) != NULL) {
		GDExtensionInt hash;
		StringName p_methodname;
		char class_name[128];
		char method_name[128];

		if (sscanf(line, "%s %s %ld", class_name, method_name, &hash) != 3) {
			continue;
		}

		if (strcmp(current_class_name, class_name) != 0) {
			if (has_class_string) {
				string_name_destructor(&p_classname);
			}
			strcpy(current_class_name, class_name);

			string_name_new_with_latin1_chars(&p_classname, current_class_name, false);

			has_class_string = true;
		}

		string_name_new_with_latin1_chars(&p_methodname, method_name, false);
		GDExtensionMethodBindPtr method_bind = classdb_get_method_bind(&p_classname, &p_methodname, hash);

		if (method_bind == NULL) {
			fprintf(stderr, "Method bind not found: %s.%s (hash: %ld)\n", class_name, method_name, hash);
			ret = false;
		}

		string_name_destructor(&p_methodname);
	}

	if (has_class_string) {
		string_name_destructor(&p_classname);
	}

	fclose(file);
	return ret;
}

GDExtensionBool __attribute__((visibility("default"))) compatibility_test_init(GDExtensionInterfaceGetProcAddress p_get_proc_address, GDExtensionClassLibraryPtr p_library, GDExtensionInitialization *r_initialization) {
	classdb_get_method_bind = (GDExtensionInterfaceClassdbGetMethodBind)p_get_proc_address("classdb_get_method_bind");
	if (classdb_get_method_bind == NULL) {
		fprintf(stderr, "Failed to load interface method `classdb_get_method_bind`\n");
		return false;
	}

	variant_get_ptr_builtin_method = (GDExtensionInterfaceVariantGetPtrBuiltinMethod)p_get_proc_address("variant_get_ptr_builtin_method");
	if (variant_get_ptr_builtin_method == NULL) {
		fprintf(stderr, "Failed to load interface method `variant_get_ptr_builtin_method`\n");
		return false;
	}

	variant_get_ptr_utility_function = (GDExtensionInterfaceVariantGetPtrUtilityFunction)p_get_proc_address("variant_get_ptr_utility_function");
	if (variant_get_ptr_utility_function == NULL) {
		fprintf(stderr, "Failed to load interface method `variant_get_ptr_utility_function`\n");
		return false;
	}

	GDExtensionInterfaceVariantGetPtrDestructor variant_get_ptr_destructor = (GDExtensionInterfaceVariantGetPtrDestructor)p_get_proc_address("variant_get_ptr_destructor");
	if (variant_get_ptr_destructor == NULL) {
		fprintf(stderr, "Failed to load interface method `variant_get_ptr_destructor`\n");
		return false;
	}
	string_name_destructor = variant_get_ptr_destructor(GDEXTENSION_VARIANT_TYPE_STRING_NAME);

	string_name_new_with_latin1_chars = (GDExtensionInterfaceStringNameNewWithLatin1Chars)p_get_proc_address("string_name_new_with_latin1_chars");
	if (classdb_get_method_bind == NULL) {
		fprintf(stderr, "Failed to load interface method `string_name_new_with_latin1_chars`\n");
		return false;
	}

	GDExtensionInterfaceRegisterMainLoopCallbacks register_main_loop_callbacks = (GDExtensionInterfaceRegisterMainLoopCallbacks)p_get_proc_address("register_main_loop_callbacks");
	if (register_main_loop_callbacks == NULL) {
		fprintf(stderr, "Failed to load interface method `register_main_loop_callbacks`\n");
		return false;
	}

	register_main_loop_callbacks(p_library, &callbacks);

	r_initialization->initialize = initialize_compatibility_test;
	r_initialization->deinitialize = deinitialize_compatibility_test;
	r_initialization->userdata = NULL;
	r_initialization->minimum_initialization_level = GDEXTENSION_INITIALIZATION_EDITOR;

	return true;
}
