class_name JsonClassConverter

# Flag to control whether to save nested resources as separate .tres files 
static var save_temp_resources_tres: bool = false

## Checks if the provided class is valid (not null)
static func _check_cast_class(castClass: GDScript) -> bool:
	if typeof(castClass) == Variant.Type.TYPE_NIL:
		printerr("The provided class is null.")
		return false
	return true

## Checks if the directory for the given file path exists, creating it if necessary.
static func check_dir(file_path: String) -> void:
	if !DirAccess.dir_exists_absolute(file_path.get_base_dir()):
		DirAccess.make_dir_absolute(file_path.get_base_dir())

#region Json to Class

## Loads a JSON file and parses it into a Dictionary.
## Supports optional decryption using a security key.
static func json_file_to_dict(file_path: String, security_key: String = "") -> Dictionary:
	var file: FileAccess
	if FileAccess.file_exists(file_path):
		if security_key.length() == 0:
			file = FileAccess.open(file_path, FileAccess.READ)
		else:
			file = FileAccess.open_encrypted_with_pass(file_path, FileAccess.READ, security_key)
		if not file:
			printerr("Error opening file: ", file_path)
			return {}
		var parsed_results: Variant = JSON.parse_string(file.get_as_text())
		file.close()
		if parsed_results is Dictionary or parsed_results is Array:
			return parsed_results
	return {}

## Loads a JSON file and converts its contents into a Godot class instance.
## Uses the provided GDScript (castClass) as a template for the class.
static func json_file_to_class(castClass: GDScript, file_path: String, security_key: String = "") -> Object:
	if not _check_cast_class(castClass):
		printerr("The provided class is null.")
		return null
	var parsed_results = json_file_to_dict(file_path, security_key)
	if parsed_results == null:
		return castClass.new()
	return json_to_class(castClass, parsed_results)

## Converts a JSON string into a Godot class instance.
static func json_string_to_class(castClass: GDScript, json_string: String) -> Object:
	if not _check_cast_class(castClass):
		printerr("The provided class is null.")
		return null
	var json: JSON = JSON.new()
	var parse_result: Error = json.parse(json_string)
	if parse_result == Error.OK:
		return json_to_class(castClass, json.data)
	return castClass.new()

## Converts a JSON dictionary into a Godot class instance.
## This is the core deserialization function.
static func json_to_class(castClass: GDScript, json: Dictionary) -> Object:
	# Create an instance of the target class
	var _class: Object = castClass.new() as Object
	var properties: Array = _class.get_property_list()

	# Iterate through each key-value pair in the JSON dictionary
	for key: String in json.keys():
		var value: Variant = json[key]
		
		# Special handling for Vector types (stored as strings in JSON)
		if type_string(typeof(value)) == "String" and value.begins_with("Vector"):
			value = str_to_var(value)

		# Find the matching property in the target class
		for property: Dictionary in properties:
			# Skip the 'script' property (built-in)
			if property.name == "script":
				continue

			# Get the current value of the property in the class instance
			var property_value: Variant = _class.get(property.name)

			# If the property name matches the JSON key and is a script variable:
			if property.name == key and property.usage >= PROPERTY_USAGE_SCRIPT_VARIABLE:
				# Case 1: Property is an Object (not an array)
				if not property_value is Array and property.type == TYPE_OBJECT:
					var inner_class_path: String = ""
					if property_value:
						# If the property already holds an object, try to get its script path
						for inner_property: Dictionary in property_value.get_property_list():
							if inner_property.has("hint_string") and inner_property["hint_string"].contains(".gd"):
								inner_class_path = inner_property["hint_string"]
						# Recursively deserialize nested objects
						_class.set(property.name, json_to_class(load(inner_class_path), value))
					elif value:
						var script_type: GDScript = null
						# Determine the script type for the nested object
						if value is Dictionary and value.has("script_inheritance"):
							script_type = get_gdscript(value["script_inheritance"])
						else:
							script_type = get_gdscript(property. class_name )

						# If the value is a resource path, load the resource
						if value is String and value.is_absolute_path():
							_class.set(property.name, ResourceLoader.load(get_main_tres_path(value)))
						else:
							# Recursively deserialize nested objects
							_class.set(property.name, json_to_class(script_type, value))

				# Case 2: Property is an Array
				elif property_value is Array:
					if property.has("hint_string"):
						var class_hint: String = property["hint_string"]
						if class_hint.contains(":"):
							# Extract class name from hint string (e.g., "24/34:ClassName")
							class_hint = class_hint.split(":")[1]
						
						# Recursively convert the JSON array to a Godot array
						var arrayTemp: Array = convert_json_to_array(value, get_gdscript(class_hint))
						
						# Handle Vector arrays (convert string elements back to Vectors)
						if type_string(property_value.get_typed_builtin()).begins_with("Vector"):
							for obj_array: Variant in arrayTemp:
								_class.get(property.name).append(str_to_var(obj_array))
						else:
							_class.get(property.name).assign(arrayTemp)

				# Case 3: Property is a simple type (not an object or array)
				else:
					# Special handling for Color type (stored as a hex string)
					if property.type == TYPE_COLOR:
						value = Color(value)
					if property.type == TYPE_INT and property.hint == PROPERTY_HINT_ENUM:
						var enum_strs: Array = property.hint_string.split(",")
						var enum_value: int = 0
						for enum_str: String in enum_strs:
							if enum_str.contains(":"):
								var enum_keys: Array = enum_str.split(":")
								for i: int in enum_keys.size():
									if enum_keys[i].to_lower() == value.to_lower():
										enum_value = int(enum_keys[i + 1])
						_class.set(property.name, enum_value)
					else:
						_class.set(property.name, value)

	# Return the fully deserialized class instance
	return _class

## Helper function to find a GDScript by its class name.
static func get_gdscript(hint_class: String) -> GDScript:
	for className: Dictionary in ProjectSettings.get_global_class_list():
		if className. class == hint_class:
			return load(className.path)
	return null
	
## Helper function to recursively convert JSON arrays to Godot arrays.
static func convert_json_to_array(json_array: Array, cast_class: GDScript = null) -> Array:
	var godot_array: Array = []
	for element: Variant in json_array:
		if typeof(element) == TYPE_DICTIONARY:
			# If json element has a script_inheritance, get the script (for inheritance or for untyped array/dictionary)
			if "script_inheritance" in element:
				cast_class = get_gdscript(element["script_inheritance"])
			godot_array.append(json_to_class(cast_class, element))
		elif typeof(element) == TYPE_ARRAY:
			godot_array.append(convert_json_to_array(element))
		else:
			godot_array.append(element)
	return godot_array

#endregion

#region Class to Json
## Stores a JSON dictionary to a file, optionally with encryption.
static func store_json_file(file_path: String, data: Dictionary, security_key: String = "") -> bool:
	check_dir(file_path)
	var file: FileAccess
	if security_key.length() == 0:
		file = FileAccess.open(file_path, FileAccess.WRITE)
	else:
		file = FileAccess.open_encrypted_with_pass(file_path, FileAccess.WRITE, security_key)
	if not file:
		printerr("Error writing to a file")
		return false
	var json_string: String = JSON.stringify(data, "\t")
	file.store_string(json_string)
	file.close()
	return true

## Converts a Godot class instance into a JSON string.
static func class_to_json_string(_class: Object, save_temp_res: bool = false) -> String:
	return JSON.stringify(class_to_json(_class, save_temp_res))

## Converts a Godot class instance into a JSON dictionary.
## This is the core serialization function.
static func class_to_json(_class: Object, save_temp_res: bool = false, inheritance: bool = false) -> Dictionary:
	var dictionary: Dictionary = {}
	save_temp_resources_tres = save_temp_res
	# Store the script name for reference during deserialization if inheritance exists
	if inheritance:
		dictionary["script_inheritance"] = _class.get_script().get_global_name()
	var properties: Array = _class.get_property_list()

	# Iterate through each property of the class
	for property: Dictionary in properties:
		var property_name: String = property["name"]
		# Skip the built-in 'script' property
		if property_name == "script":
			continue
		var property_value: Variant = _class.get(property_name)

		# Only serialize properties that are exported or marked for storage
		if not property_name.is_empty() and property.usage >= PROPERTY_USAGE_SCRIPT_VARIABLE and property.usage & PROPERTY_USAGE_STORAGE > 0:
			if property_value is Array:
				# Recursively convert arrays to JSON
				dictionary[property_name] = convert_array_to_json(property_value)
			elif property_value is Dictionary:
				# Recursively convert dictionaries to JSON
				dictionary[property_name] = convert_dictionary_to_json(property_value)
			# If the property is a Resource:
			elif property["type"] == TYPE_OBJECT and property_value != null and property_value.get_property_list():
				if property_value is Resource and ResourceLoader.exists(property_value.resource_path):
					var main_src: String = get_main_tres_path(property_value.resource_path)
					if main_src.get_extension() != "tres":
						# Store the resource path if it's not a .tres file
						dictionary[property.name] = property_value.resource_path
					elif save_temp_resources_tres:
						# Save the resource as a separate .tres file
						var tempfile = "user://temp_resource/"
						check_dir(tempfile)
						var nodePath: String = get_node_tres_path(property_value.resource_path)
						if not nodePath.is_empty():
							tempfile += nodePath
							tempfile += ".tres"
						else:
							tempfile += property_value.resource_path.get_file()
						dictionary[property.name] = tempfile
						ResourceSaver.save(property_value, tempfile)
					else:
						# Recursively serialize the nested resource
						dictionary[property.name] = class_to_json(property_value, save_temp_resources_tres)
				else:
					dictionary[property.name] = class_to_json(property_value, save_temp_resources_tres, property. class_name != property_value.get_script().get_global_name())
			# Special handling for Vector types (store as strings)
			elif type_string(typeof(property_value)).begins_with("Vector"):
				dictionary[property_name] = var_to_str(property_value)
			elif property["type"] == TYPE_COLOR:
				# Store Color as a hex string
				dictionary[property_name] = property_value.to_html()
			else:
				# Store other basic types directly
				if property.type == TYPE_INT and property.hint == PROPERTY_HINT_ENUM:
					var enum_value: String = property.hint_string.split(",")[property_value]
					if enum_value.contains(":"):
						dictionary[property.name] = enum_value.split(":")[0]
					else:
						dictionary[property.name] = enum_value
				else:
					dictionary[property.name] = property_value
	return dictionary

## Extracts the main path from a resource path (removes node path if present).
static func get_main_tres_path(path: String) -> String:
	var path_parts: PackedStringArray = path.split("::", true, 1)
	if path_parts.size() > 0:
		return path_parts[0]
	else:
		return path

## Extracts the node path from a resource path.
static func get_node_tres_path(path: String) -> String:
	var path_parts: PackedStringArray = path.split("::", true, 1)
	if path_parts.size() > 1:
		return path_parts[1]
	else:
		return ""


## Helper function to recursively convert Godot arrays to JSON arrays.
static func convert_array_to_json(array: Array) -> Array:
	var json_array: Array = []
	for element: Variant in array:
		if element is Object:
			json_array.append(class_to_json(element, save_temp_resources_tres,!array.is_typed()))
		elif element is Array:
			json_array.append(convert_array_to_json(element))
		elif element is Dictionary:
			json_array.append(convert_dictionary_to_json(element))
		elif type_string(typeof(element)).begins_with("Vector"):
			json_array.append(var_to_str(element))
		else:
			json_array.append(element)
	return json_array

## Helper function to recursively convert Godot dictionaries to JSON dictionaries.
static func convert_dictionary_to_json(dictionary: Dictionary) -> Dictionary:
	var json_dictionary: Dictionary = {}
	for key: Variant in dictionary.keys():
		var value: Variant = dictionary[key]
		if value is Object:
			json_dictionary[key] = class_to_json(value, save_temp_resources_tres)
		elif value is Array:
			json_dictionary[key] = convert_array_to_json(value)
		elif value is Dictionary:
			json_dictionary[key] = convert_dictionary_to_json(value)
		else:
			json_dictionary[key] = value
	return json_dictionary
#endregion