class_name GdFunctionDoubler
extends RefCounted

const DEFAULT_TYPED_RETURN_VALUES := {
	TYPE_NIL: "null",
	TYPE_BOOL: "false",
	TYPE_INT: "0",
	TYPE_FLOAT: "0.0",
	TYPE_STRING: "\"\"",
	TYPE_STRING_NAME: "&\"\"",
	TYPE_VECTOR2: "Vector2.ZERO",
	TYPE_VECTOR2I: "Vector2i.ZERO",
	TYPE_RECT2: "Rect2()",
	TYPE_RECT2I: "Rect2i()",
	TYPE_VECTOR3: "Vector3.ZERO",
	TYPE_VECTOR3I: "Vector3i.ZERO",
	TYPE_VECTOR4: "Vector4.ZERO",
	TYPE_VECTOR4I: "Vector4i.ZERO",
	TYPE_TRANSFORM2D: "Transform2D()",
	TYPE_PLANE: "Plane()",
	TYPE_QUATERNION: "Quaternion()",
	TYPE_AABB: "AABB()",
	TYPE_BASIS: "Basis()",
	TYPE_TRANSFORM3D: "Transform3D()",
	TYPE_PROJECTION: "Projection()",
	TYPE_COLOR: "Color()",
	TYPE_NODE_PATH: "NodePath()",
	TYPE_RID: "RID()",
	TYPE_OBJECT: "null",
	TYPE_CALLABLE: "Callable()",
	TYPE_SIGNAL: "Signal()",
	TYPE_DICTIONARY: "Dictionary()",
	TYPE_ARRAY: "Array()",
	TYPE_PACKED_BYTE_ARRAY: "PackedByteArray()",
	TYPE_PACKED_INT32_ARRAY: "PackedInt32Array()",
	TYPE_PACKED_INT64_ARRAY: "PackedInt64Array()",
	TYPE_PACKED_FLOAT32_ARRAY: "PackedFloat32Array()",
	TYPE_PACKED_FLOAT64_ARRAY: "PackedFloat64Array()",
	TYPE_PACKED_STRING_ARRAY: "PackedStringArray()",
	TYPE_PACKED_VECTOR2_ARRAY: "PackedVector2Array()",
	TYPE_PACKED_VECTOR3_ARRAY: "PackedVector3Array()",
	TYPE_PACKED_COLOR_ARRAY: "PackedColorArray()",
	GdObjects.TYPE_VARIANT: "null",
	GdObjects.TYPE_ENUM: "0"
}

# @GlobalScript enums
# needs to manually map because of https://github.com/godotengine/godot/issues/73835
const DEFAULT_ENUM_RETURN_VALUES = {
	"Side" : "SIDE_LEFT",
	"Corner" : "CORNER_TOP_LEFT",
	"Orientation" : "HORIZONTAL",
	"ClockDirection" : "CLOCKWISE",
	"HorizontalAlignment" : "HORIZONTAL_ALIGNMENT_LEFT",
	"VerticalAlignment" : "VERTICAL_ALIGNMENT_TOP",
	"InlineAlignment" : "INLINE_ALIGNMENT_TOP_TO",
	"EulerOrder" : "EULER_ORDER_XYZ",
	"Key" : "KEY_NONE",
	"KeyModifierMask" : "KEY_CODE_MASK",
	"MouseButton" : "MOUSE_BUTTON_NONE",
	"MouseButtonMask" : "MOUSE_BUTTON_MASK_LEFT",
	"JoyButton" : "JOY_BUTTON_INVALID",
	"JoyAxis" : "JOY_AXIS_INVALID",
	"MIDIMessage" : "MIDI_MESSAGE_NONE",
	"Error" : "OK",
	"PropertyHint" : "PROPERTY_HINT_NONE",
	"Variant.Type" : "TYPE_NIL",
}

var _push_errors :String


# Determine the enum default by reflection
static func get_enum_default(value :String) -> Variant:
	var script := GDScript.new()
	script.source_code = """
	extends Resource

	static func get_enum_default() -> Variant:
		return %s.values()[0]

	""".dedent() % value
	script.reload()
	script.source_code
	return script.new().call("get_enum_default")


static func default_return_value(func_descriptor :GdFunctionDescriptor) -> String:
	var return_type :Variant = func_descriptor.return_type()
	if return_type == GdObjects.TYPE_ENUM:
		var enum_class := func_descriptor._return_class
		var enum_path := enum_class.split(".")
		if enum_path.size() >= 2:
			var keys := ClassDB.class_get_enum_constants(enum_path[0], enum_path[1])
			if not keys.is_empty():
				return "%s.%s" % [enum_path[0], keys[0]]
			var enum_value := get_enum_default(enum_class)
			if enum_value != null:
				return str(enum_value)
		# we need fallback for @GlobalScript enums,
		return DEFAULT_ENUM_RETURN_VALUES.get(func_descriptor._return_class, "0")
	return DEFAULT_TYPED_RETURN_VALUES.get(return_type, "invalid")


func _init(push_errors :bool = false):
	_push_errors = "true" if push_errors else "false"
	for type_key in TYPE_MAX:
		if not DEFAULT_TYPED_RETURN_VALUES.has(type_key):
			push_error("missing default definitions! Expexting %d bud is %d" % [DEFAULT_TYPED_RETURN_VALUES.size(), TYPE_MAX])
			prints("missing default definition for type", type_key)
			assert(DEFAULT_TYPED_RETURN_VALUES.has(type_key), "Missing Type default definition!")


@warning_ignore("unused_parameter")
func get_template(return_type :Variant, is_vararg :bool) -> String:
	push_error("Must be implemented!")
	return ""

func double(func_descriptor :GdFunctionDescriptor) -> PackedStringArray:
	var func_signature := func_descriptor.typeless()
	var is_static := func_descriptor.is_static()
	var is_vararg := func_descriptor.is_vararg()
	var is_coroutine := func_descriptor.is_coroutine()
	var func_name := func_descriptor.name()
	var args := func_descriptor.args()
	var varargs := func_descriptor.varargs()
	var return_value := GdFunctionDoubler.default_return_value(func_descriptor)
	var arg_names := extract_arg_names(args)
	var vararg_names := extract_arg_names(varargs)

	# save original constructor arguments
	if func_name == "_init":
		var constructor_args := ",".join(GdFunctionDoubler.extract_constructor_args(args))
		var constructor := "func _init(%s) -> void:\n	super(%s)\n	pass\n" % [constructor_args, ", ".join(arg_names)]
		return constructor.split("\n")

	var double_src := ""
	double_src += '@warning_ignore("untyped_declaration")\n' if Engine.get_version_info().hex >= 0x40200 else '\n'
	if func_descriptor.is_engine():
		double_src += '@warning_ignore("native_method_override")\n'
	if func_descriptor.return_type() == GdObjects.TYPE_ENUM:
		double_src += '@warning_ignore("int_as_enum_without_match")\n'
		double_src += '@warning_ignore("int_as_enum_without_cast")\n'
	double_src += '@warning_ignore("shadowed_variable")\n'
	double_src += func_signature
	# fix to  unix format, this is need when the template is edited under windows than the template is stored with \r\n
	var func_template := get_template(func_descriptor.return_type(), is_vararg).replace("\r\n", "\n")
	double_src += func_template\
		.replace("$(arguments)", ", ".join(arg_names))\
		.replace("$(varargs)", ", ".join(vararg_names))\
		.replace("$(await)", GdFunctionDoubler.await_is_coroutine(is_coroutine)) \
		.replace("$(func_name)", func_name )\
		.replace("${default_return_value}", return_value)\
		.replace("$(push_errors)", _push_errors)

	if is_static:
		double_src = double_src.replace("$(instance)", "__instance().")
	else:
		double_src = double_src.replace("$(instance)", "")
	return double_src.split("\n")


func extract_arg_names(argument_signatures :Array[GdFunctionArgument]) -> PackedStringArray:
	var arg_names := PackedStringArray()
	for arg in argument_signatures:
		arg_names.append(arg._name)
	return arg_names


static func extract_constructor_args(args :Array) -> PackedStringArray:
	var constructor_args := PackedStringArray()
	for arg in args:
		var a := arg as GdFunctionArgument
		var arg_name := a._name
		var default_value = get_default(a)
		if default_value == "null":
			constructor_args.append(arg_name + ":Variant=" + default_value)
		else:
			constructor_args.append(arg_name + ":=" + default_value)
	return constructor_args


static func get_default(arg :GdFunctionArgument) -> String:
	if arg.has_default():
		return arg.value_as_string()
	else:
		return DEFAULT_TYPED_RETURN_VALUES.get(arg.type(), "null")


static func await_is_coroutine(is_coroutine :bool) -> String:
	return "await " if is_coroutine else ""
