class_name ShaderLoader
extends RefCounted

const VALUE_SLIDER_V2_TSCN := preload("res://src/UI/Nodes/Sliders/ValueSliderV2.tscn")
const BASIS_SLIDERS_TSCN := preload("res://src/UI/Nodes/Sliders/BasisSliders.tscn")
const GRADIENT_EDIT_TSCN := preload("res://src/UI/Nodes/GradientEdit.tscn")
const NOISE_GENERATOR := preload("res://src/UI/Nodes/NoiseGeneratorDialog.tscn")

static var dither_matrices: Array[DitherMatrix] = [
	DitherMatrix.new(preload("res://assets/dither-matrices/bayer2.png"), "Bayer 2x2"),
	DitherMatrix.new(preload("res://assets/dither-matrices/bayer4.png"), "Bayer 4x4"),
	DitherMatrix.new(preload("res://assets/dither-matrices/bayer8.png"), "Bayer 8x8"),
	DitherMatrix.new(preload("res://assets/dither-matrices/bayer16.png"), "Bayer 16x16"),
]


class DitherMatrix:
	var texture: Texture2D
	var name: String

	func _init(_texture: Texture2D, _name: String) -> void:
		texture = _texture
		name = _name


static func load_dither_matrix_from_file(file_path: String) -> void:
	var dither_image := Image.load_from_file(file_path)
	if is_instance_valid(dither_image):
		var dither_tex := ImageTexture.create_from_image(dither_image)
		var dither_matrix := DitherMatrix.new(dither_tex, file_path.get_file().get_basename())
		dither_matrices.append(dither_matrix)


static func create_ui_for_shader_uniforms(
	shader: Shader,
	params: Dictionary,
	parent_node: Control,
	value_changed: Callable,
	file_selected: Callable
) -> void:
	var code := shader.code.split("\n")
	var uniforms: PackedStringArray = []
	var uniform_data: PackedStringArray = []
	var description: String = ""
	var description_began := false
	# A Dictionary of [String] and [Control], used to group together nodes
	# under the same group_uniform. Currently only used for CurveTextures.
	var group_nodes := {}
	var color_button_hbox: HBoxContainer = null  # Used for RGBA buttons, if they exist.
	for line in code:
		# Management of "end" tags
		if line.begins_with("// (end DESCRIPTION)"):
			description_began = false
		if description_began:
			description += "\n" + line.strip_edges()

		# Detection of uniforms
		if line.begins_with("uniform") or line.begins_with("group_uniforms"):
			uniforms.append(line)
		if line.begins_with("// uniform_data"):
			uniform_data.append(line)

		# Management of "begin" tags
		elif line.begins_with("// (begin DESCRIPTION)"):
			description_began = true
	# Validation of begin/end tags
	if description_began == true:  # Description started but never ended. treat it as an error
		print("Shader description started but never finished. Assuming empty description")
		description = ""
	if not description.is_empty():
		parent_node.tooltip_text = str(
			"Description:\n", description.replace("//", "").strip_edges()
		)

	var current_group := ""
	for uniform in uniforms:
		# Example uniform:
		# uniform float parameter_name : hint_range(0, 255) = 100.0;
		var uniform_split := uniform.split("=")
		var u_value := ""
		if uniform_split.size() > 1:
			u_value = uniform_split[1].replace(";", "").strip_edges()
		else:
			uniform_split[0] = uniform_split[0].replace(";", "").strip_edges()

		var u_left_side := uniform_split[0].split(":")
		var u_hint := ""
		if u_left_side.size() > 1:
			u_hint = u_left_side[1].strip_edges()
			u_hint = u_hint.replace(";", "")

		var u_init := u_left_side[0].split(" ")
		var uniform_string := u_init[0]
		if uniform_string == "group_uniforms":
			current_group = u_init[1]
			continue
		var u_type := u_init[1]
		var u_name := u_init[2]
		if u_name in ["PXO_time", "PXO_frame_index", "PXO_layer_index"]:
			continue
		# Find custom data of the uniform, if any exists
		# Right now it only checks if a uniform should have another type of node
		# Such as integers having OptionButtons
		# But in the future it could be expanded to include custom names or descriptions.
		var custom_data: PackedStringArray = []
		var type_override := ""
		for data in uniform_data:
			if u_name in data:
				var line_to_examine := data.split(" ")
				if line_to_examine[3] == "type::":
					var temp_splitter := data.split("::")
					if temp_splitter.size() > 1:
						type_override = temp_splitter[1].strip_edges()

				custom_data.append(data)
		var humanized_u_name := Keychain.humanize_snake_case(u_name) + ":"

		if u_type == "float" or u_type == "int":
			var hbox := HBoxContainer.new()
			var label := Label.new()
			label.text = humanized_u_name
			label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
			hbox.add_child(label)
			if type_override.begins_with("OptionButton"):
				var option_button := OptionButton.new()
				option_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
				option_button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
				option_button.item_selected.connect(value_changed.bind(u_name))
				var items := (
					type_override
					. replace("OptionButton ", "")
					. replace("[", "")
					. replace("]", "")
					. split("||")
				)
				for item in items:
					option_button.add_item(item)
				if u_value != "":
					option_button.select(int(u_value))
				if params.has(u_name):
					option_button.select(params[u_name])
				else:
					params[u_name] = option_button.selected
				hbox.add_child(option_button)
			else:
				var slider := ValueSlider.new()
				slider.allow_greater = true
				slider.allow_lesser = true
				slider.size_flags_horizontal = Control.SIZE_EXPAND_FILL
				var min_value := 0.0
				var max_value := 255.0
				var step := 1.0
				var value := 0.0
				var range_values_array: PackedStringArray
				if "hint_range" in u_hint:
					var range_values: String = u_hint.replace("hint_range(", "")
					range_values = range_values.replace(")", "").strip_edges()
					range_values_array = range_values.split(",")

				if u_type == "float":
					if range_values_array.size() >= 1:
						min_value = float(range_values_array[0])
					else:
						min_value = 0.01

					if range_values_array.size() >= 2:
						max_value = float(range_values_array[1])

					if range_values_array.size() >= 3:
						step = float(range_values_array[2])
					else:
						step = 0.01

					if u_value != "":
						value = float(u_value)
				else:
					if range_values_array.size() >= 1:
						min_value = int(range_values_array[0])

					if range_values_array.size() >= 2:
						max_value = int(range_values_array[1])

					if range_values_array.size() >= 3:
						step = int(range_values_array[2])

					if u_value != "":
						value = int(u_value)
				slider.min_value = min_value
				slider.max_value = max_value
				slider.step = step
				slider.value = value
				if params.has(u_name):
					slider.value = params[u_name]
				else:
					params[u_name] = slider.value
				slider.value_changed.connect(value_changed.bind(u_name))
				slider.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
				hbox.add_child(slider)
			parent_node.add_child(hbox)
		elif u_type == "vec2" or u_type == "ivec2" or u_type == "uvec2":
			var label := Label.new()
			label.text = humanized_u_name
			label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
			var vector2 := _vec2str_to_vector2(u_value)
			var slider := VALUE_SLIDER_V2_TSCN.instantiate() as ValueSliderV2
			slider.show_ratio = true
			slider.allow_greater = true
			if u_type != "uvec2":
				slider.allow_lesser = true
				if u_type != "ivec2":
					slider.step = 0.01
			slider.size_flags_horizontal = Control.SIZE_EXPAND_FILL
			slider.value = vector2
			if params.has(u_name):
				slider.value = params[u_name]
			else:
				params[u_name] = slider.value
			slider.value_changed.connect(value_changed.bind(u_name))
			var hbox := HBoxContainer.new()
			hbox.add_child(label)
			hbox.add_child(slider)
			parent_node.add_child(hbox)
		elif u_type == "vec4":
			if "source_color" in u_hint:
				var label := Label.new()
				label.text = humanized_u_name
				label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
				var color := _vec4str_to_color(u_value)
				var color_button := ColorPickerButton.new()
				color_button.custom_minimum_size = Vector2(20, 20)
				color_button.color = color
				if params.has(u_name):
					color_button.color = params[u_name]
				else:
					params[u_name] = color_button.color
				color_button.color_changed.connect(value_changed.bind(u_name))
				color_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
				color_button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
				var hbox := HBoxContainer.new()
				hbox.add_child(label)
				hbox.add_child(color_button)
				parent_node.add_child(hbox)
		elif u_type == "mat3":
			var label := Label.new()
			label.text = humanized_u_name
			label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
			var basis := _mat3str_to_basis(u_value)
			var sliders := BASIS_SLIDERS_TSCN.instantiate() as BasisSliders
			sliders.allow_greater = true
			sliders.allow_lesser = true
			sliders.size_flags_horizontal = Control.SIZE_EXPAND_FILL
			sliders.value = basis
			if params.has(u_name):
				sliders.value = params[u_name]
			else:
				params[u_name] = sliders.value
			sliders.value_changed.connect(value_changed.bind(u_name))
			var hbox := HBoxContainer.new()
			hbox.add_child(label)
			hbox.add_child(sliders)
			parent_node.add_child(hbox)
		elif u_type == "sampler2D":
			if u_name == "selection":
				continue
			if u_name == "palette_texture":
				var palette := Palettes.current_palette
				var palette_texture := ImageTexture.create_from_image(palette.convert_to_image())
				value_changed.call(palette_texture, u_name)
				Palettes.palette_selected.connect(
					func(_name): _shader_change_palette(value_changed, u_name)
				)
				palette.data_changed.connect(
					func(): _shader_update_palette_texture(palette, value_changed, u_name)
				)
				continue
			var create_label := true
			if not current_group.is_empty():
				if u_name.begins_with("curve_"):
					create_label = false
				if u_name.begins_with("gradient_"):
					var group_gradient_str := current_group + "_gradient"
					if group_nodes.has(group_gradient_str):
						create_label = false
			var hbox: HBoxContainer
			if create_label:
				hbox = HBoxContainer.new()
				var label := Label.new()
				label.text = humanized_u_name
				label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
				hbox.add_child(label)
				parent_node.add_child(hbox)
			if shader is VisualShader and u_name.begins_with("tex_frg_"):
				var node_id := int(u_name.replace("tex_frg_", ""))
				var shader_node := (shader as VisualShader).get_node(
					VisualShader.TYPE_FRAGMENT, node_id
				)
				if shader_node is VisualShaderNodeTexture:
					var texture := (shader_node as VisualShaderNodeTexture).texture
					params[u_name] = texture
					if texture is GradientTexture1D or texture is GradientTexture2D:
						_create_gradient_texture_ui(params, u_name, hbox, value_changed)
					elif texture is CurveTexture:
						_create_curve_texture_ui(params, u_name, hbox, value_changed)
					elif texture is NoiseTexture2D:
						_create_noise_texture_ui(params, u_name, hbox, value_changed, parent_node)
					else:  # Simple texture
						_create_simple_texture_ui(
							params, u_name, hbox, value_changed, parent_node, file_selected
						)
			elif u_name.begins_with("gradient_"):
				if current_group.is_empty():
					_create_gradient_texture_ui(params, u_name, hbox, value_changed)
				else:
					# If this curve uniform belongs in a group, group them into the same
					# CurveEdit node and use an OptionButton to switch between the different curves.
					var group_gradient_str := current_group + "_gradient"
					if group_nodes.has(group_gradient_str):
						var grad_edit := group_nodes[group_gradient_str] as GradientEditNode
						if "no_interpolation" in u_name:
							value_changed.call_deferred(
								grad_edit.get_gradient_texture_no_interpolation(), u_name
							)
							grad_edit.updated.connect(
								func(_gradient, _cc):
									value_changed.call(
										grad_edit.get_gradient_texture_no_interpolation(), u_name
									)
							)
						elif "offset" in u_name:
							value_changed.call_deferred(
								grad_edit.get_gradient_offsets_texture(), u_name
							)
							grad_edit.updated.connect(
								func(_gradient, _cc):
									value_changed.call(
										grad_edit.get_gradient_offsets_texture(), u_name
									)
							)
						else:
							value_changed.call_deferred(grad_edit.texture, u_name)
							grad_edit.updated.connect(
								func(_gradient, _cc): value_changed.call(grad_edit.texture, u_name)
							)
					else:
						var grad_edit := _create_gradient_texture_ui(
							params, u_name, hbox, value_changed
						)
						group_nodes[group_gradient_str] = grad_edit
			elif u_name.begins_with("dither_texture"):
				var option_button := OptionButton.new()
				option_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
				option_button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
				option_button.item_selected.connect(
					func(index: int):
						var dither_tex := dither_matrices[index].texture
						value_changed.call(dither_tex, u_name)
				)
				for matrix in dither_matrices:
					option_button.add_item(matrix.name)
				if params.has(u_name):
					var matrix_index := 0
					for i in dither_matrices.size():
						if dither_matrices[i].texture == params[u_name]:
							matrix_index = i
							break
					option_button.select(matrix_index)
				else:
					params[u_name] = dither_matrices[option_button.selected].texture
				hbox.add_child(option_button)
			elif u_name.begins_with("curve_"):
				if current_group.is_empty():
					_create_curve_texture_ui(params, u_name, hbox, value_changed)
				else:
					# If this curve uniform belongs in a group, group them into the same
					# CurveEdit node and use an OptionButton to switch between the different curves.
					var group_option_button_str := current_group + "_option_button"
					if group_nodes.has(group_option_button_str):
						# Add it to the current group CurveEdit and OptionButton.
						var option_button := group_nodes[group_option_button_str] as OptionButton
						if not params.has(u_name):
							var new_curve := Curve.new()
							# Set linear preset to the new curve
							CurveEdit.set_curve_preset(new_curve, 0)
							params[u_name] = CurveEdit.to_texture(new_curve)
						option_button.add_item(
							Keychain.humanize_snake_case(u_name.replace("curve_", ""))
						)
						option_button.set_item_metadata(option_button.item_count - 1, u_name)
					else:  # Create a the group's CurveEdit and OptionButton.
						var option_button := OptionButton.new()
						parent_node.add_child(option_button)
						var curve_edit := _create_curve_texture_ui(
							params, u_name, parent_node, value_changed
						)
						option_button.add_item(
							Keychain.humanize_snake_case(u_name.replace("curve_", ""))
						)
						option_button.set_item_metadata(option_button.item_count - 1, u_name)
						option_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
						option_button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
						option_button.item_selected.connect(
							# Disconnect all previous connections from the
							# curve_edit's value_changed signal. Then change the curve, and then
							# connect a new callable to the value_changed signal.
							func(index: int):
								var new_uniform_name = option_button.get_item_metadata(index)
								for connection in curve_edit.value_changed.get_connections():
									curve_edit.value_changed.disconnect(connection.callable)
								curve_edit.curve = params[new_uniform_name].curve
								curve_edit.value_changed.connect(
									func(curve: Curve):
										value_changed.call(
											CurveEdit.to_texture(curve), new_uniform_name
										)
								)
						)
						group_nodes[group_option_button_str] = option_button
			elif u_name.begins_with("noise_"):
				_create_noise_texture_ui(params, u_name, hbox, value_changed, parent_node)
			else:  # Simple texture
				_create_simple_texture_ui(
					params, u_name, hbox, value_changed, parent_node, file_selected
				)
		elif u_type == "bool":
			var button: BaseButton
			if u_name in ["red", "green", "blue", "alpha"]:
				button = Button.new()
				button.text = u_name[0].to_upper()
				button.toggle_mode = true
				if is_instance_valid(color_button_hbox):
					color_button_hbox.add_child(button)
				else:
					color_button_hbox = HBoxContainer.new()
					color_button_hbox.add_child(button)
					parent_node.add_child(color_button_hbox)
			else:
				button = CheckBox.new()
				var label := Label.new()
				label.text = humanized_u_name
				label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
				button.text = "On"
				var hbox := HBoxContainer.new()
				hbox.add_child(label)
				hbox.add_child(button)
				parent_node.add_child(hbox)
			if u_value == "true":
				button.button_pressed = true
			if params.has(u_name):
				button.button_pressed = params[u_name]
			else:
				params[u_name] = button.button_pressed
			button.toggled.connect(value_changed.bind(u_name))
			button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
			button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND


static func _vec2str_to_vector2(vec2: String) -> Vector2:
	vec2 = vec2.replace("uvec2", "vec2")
	vec2 = vec2.replace("ivec2", "vec2")
	vec2 = vec2.replace("vec2(", "")
	vec2 = vec2.replace(")", "")
	var vec_values := vec2.split(",")
	if vec_values.size() == 0:
		return Vector2.ZERO
	var y := float(vec_values[0])
	if vec_values.size() == 2:
		y = float(vec_values[1])
	var vector2 := Vector2(float(vec_values[0]), y)
	return vector2


static func _vec3str_to_vector3(vec3: String) -> Vector3:
	vec3 = vec3.replace("uvec3", "vec3")
	vec3 = vec3.replace("ivec3", "vec3")
	vec3 = vec3.replace("vec3(", "")
	vec3 = vec3.replace(")", "")
	var vec_values := vec3.split(",")
	if vec_values.size() == 0:
		return Vector3.ZERO
	var y := float(vec_values[0])
	var z := float(vec_values[0])
	if vec_values.size() >= 2:
		y = float(vec_values[1])
	if vec_values.size() == 3:
		z = float(vec_values[2])
	var vector3 := Vector3(float(vec_values[0]), y, z)
	return vector3


static func _vec4str_to_color(vec4: String) -> Color:
	vec4 = vec4.replace("vec4(", "")
	vec4 = vec4.replace(")", "")
	var rgba_values := vec4.split(",")
	var red := float(rgba_values[0])

	var green := float(rgba_values[0])
	if rgba_values.size() >= 2:
		green = float(rgba_values[1])

	var blue := float(rgba_values[0])
	if rgba_values.size() >= 3:
		blue = float(rgba_values[2])

	var alpha := float(rgba_values[0])
	if rgba_values.size() == 4:
		alpha = float(rgba_values[3])
	var color := Color(red, green, blue, alpha)
	return color


static func _mat3str_to_basis(mat3: String) -> Basis:
	mat3 = mat3.replace("mat3(", "")
	mat3 = mat3.replace("))", ")")
	mat3 = mat3.replace("), ", ")")
	var vec3_values := mat3.split("vec3", false)
	var vec3_x := _vec3str_to_vector3(vec3_values[0])

	var vec3_y := _vec3str_to_vector3(vec3_values[0])
	if vec3_values.size() >= 2:
		vec3_y = _vec3str_to_vector3(vec3_values[1])

	var vec3_z := _vec3str_to_vector3(vec3_values[0])
	if vec3_values.size() == 3:
		vec3_z = _vec3str_to_vector3(vec3_values[2])
	var basis := Basis(vec3_x, vec3_y, vec3_z)
	return basis


static func _create_simple_texture_ui(
	params: Dictionary,
	u_name: String,
	hbox: BoxContainer,
	value_changed: Callable,
	parent_node: Control,
	file_selected: Callable
) -> void:
	var file_dialog := FileDialog.new()
	file_dialog.always_on_top = true
	file_dialog.file_mode = FileDialog.FILE_MODE_OPEN_FILE
	file_dialog.access = FileDialog.ACCESS_FILESYSTEM
	file_dialog.size = Vector2(384, 281)
	file_dialog.file_selected.connect(file_selected.bind(u_name))
	file_dialog.use_native_dialog = Global.use_native_file_dialogs
	var button := Button.new()
	button.text = "Load texture"
	button.pressed.connect(file_dialog.popup_centered)
	button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
	button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
	var mod_button := Button.new()
	mod_button.text = "Modify"
	mod_button.pressed.connect(
		func():
			_modify_texture_resource(
				_get_loaded_texture(params, u_name),
				u_name,
				_shader_update_texture.bind(value_changed, u_name)
			)
	)
	mod_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
	mod_button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
	hbox.add_child(button)
	hbox.add_child(mod_button)
	parent_node.add_child(file_dialog)


static func _create_gradient_texture_ui(
	params: Dictionary, u_name: String, hbox: BoxContainer, value_changed: Callable
) -> GradientEditNode:
	var gradient_edit := GRADIENT_EDIT_TSCN.instantiate() as GradientEditNode
	gradient_edit.size_flags_horizontal = Control.SIZE_EXPAND_FILL
	if params.has(u_name):
		var texture = params[u_name]
		if texture is GradientTexture2D:
			gradient_edit.set_gradient_texture(texture)
		elif texture is GradientTexture1D:
			gradient_edit.set_gradient_texture_1d(texture)
	else:
		params[u_name] = gradient_edit.texture
	# This needs to be call_deferred because GradientTexture2D gets updated next frame.
	# Without this, the texture is purple.
	value_changed.call_deferred(gradient_edit.texture, u_name)
	gradient_edit.updated.connect(
		func(_gradient, _cc): value_changed.call(gradient_edit.texture, u_name)
	)
	hbox.add_child(gradient_edit)
	return gradient_edit


static func _create_curve_texture_ui(
	params: Dictionary, u_name: String, parent: Control, value_changed: Callable
) -> CurveEdit:
	var curve_edit := CurveEdit.new()
	curve_edit.size_flags_horizontal = Control.SIZE_EXPAND_FILL
	if params.has(u_name) and params[u_name] is CurveTexture:
		curve_edit.curve = params[u_name].curve
	else:
		curve_edit.set_default_curve()
		params[u_name] = CurveEdit.to_texture(curve_edit.curve)
	curve_edit.value_changed.connect(
		func(curve: Curve): value_changed.call(CurveEdit.to_texture(curve), u_name)
	)
	parent.add_child(curve_edit)
	return curve_edit


static func _create_noise_texture_ui(
	params: Dictionary,
	u_name: String,
	hbox: BoxContainer,
	value_changed: Callable,
	parent_node: Control
) -> void:
	var noise_generator_dialog := NOISE_GENERATOR.instantiate() as AcceptDialog
	var noise_generator := noise_generator_dialog.get_child(0) as NoiseGenerator
	if params.has(u_name) and params[u_name] is NoiseTexture2D:
		noise_generator.noise_texture = params[u_name]
	else:
		params[u_name] = noise_generator.noise_texture
	noise_generator.value_changed.connect(
		func(noise_texture: NoiseTexture2D): value_changed.call(noise_texture, u_name)
	)
	parent_node.add_child(noise_generator_dialog)
	var button := Button.new()
	button.text = "Generate noise"
	button.pressed.connect(noise_generator_dialog.popup_centered)
	button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
	button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
	hbox.add_child(button)


static func _shader_change_palette(value_changed: Callable, parameter_name: String) -> void:
	var palette := Palettes.current_palette
	_shader_update_palette_texture(palette, value_changed, parameter_name)
	#if not palette.data_changed.is_connected(_shader_update_palette_texture):
	palette.data_changed.connect(
		func(): _shader_update_palette_texture(palette, value_changed, parameter_name)
	)


static func _shader_update_palette_texture(
	palette: Palette, value_changed: Callable, parameter_name: String
) -> void:
	value_changed.call(ImageTexture.create_from_image(palette.convert_to_image()), parameter_name)


static func _get_loaded_texture(params: Dictionary, parameter_name: String) -> Image:
	if parameter_name in params:
		if params[parameter_name] is ImageTexture:
			return params[parameter_name].get_image()
	var image := Image.create_empty(64, 64, false, Image.FORMAT_RGBA8)
	return image


static func _shader_update_texture(
	resource_proj: ResourceProject, value_changed: Callable, parameter_name: String
) -> void:
	var warnings := ""
	if resource_proj.frames.size() > 1:
		warnings += "This resource is intended to have 1 frame only. Extra frames will be ignored."
	if resource_proj.layers.size() > 1:
		warnings += "\nThis resource is intended to have 1 layer only. layers will be blended."

	var updated_image := Image.create_empty(
		resource_proj.size.x, resource_proj.size.y, false, Image.FORMAT_RGBA8
	)
	var frame := resource_proj.frames[0]
	DrawingAlgos.blend_layers(updated_image, frame, Vector2i.ZERO, resource_proj)
	value_changed.call(ImageTexture.create_from_image(updated_image), parameter_name)
	if not warnings.is_empty():
		Global.popup_error(warnings)


static func _modify_texture_resource(
	image: Image, resource_name: StringName, update_callable: Callable
) -> void:
	var resource_proj := ResourceProject.new([], resource_name, image.get_size())
	resource_proj.layers.append(PixelLayer.new(resource_proj))
	resource_proj.frames.append(resource_proj.new_empty_frame())
	resource_proj.frames[0].cels[0].set_content(image)
	resource_proj.resource_updated.connect(update_callable)
	Global.projects.append(resource_proj)
	Global.tabs.current_tab = Global.tabs.get_tab_count() - 1
	Global.canvas.camera_zoom()
