# gdlint: ignore=max-public-methods
extends Node

signal project_saved
signal reference_image_imported

var preview_dialog_tscn := preload("res://src/UI/Dialogs/ImportPreviewDialog.tscn")
var preview_dialogs := []  ## Array of preview dialogs
var last_dialog_option := 0
var autosave_timer: Timer

# custom importer related dictionaries (received from extensions)
var custom_import_names := {}  ## Contains importer names as keys and ids as values
var custom_importer_scenes := {}  ## Contains ids keys and import option preloads as values


func _ready() -> void:
	autosave_timer = Timer.new()
	autosave_timer.one_shot = false
	autosave_timer.timeout.connect(_on_Autosave_timeout)
	add_child(autosave_timer)
	update_autosave()


func handle_loading_file(file: String) -> void:
	file = file.replace("\\", "/")
	var file_ext := file.get_extension().to_lower()
	if file_ext == "pxo":  # Pixelorama project file
		open_pxo_file(file)

	elif file_ext == "tres":  # Godot resource file
		return
	elif file_ext == "tscn":  # Godot scene file
		return

	elif file_ext == "gpl" or file_ext == "pal" or file_ext == "json":
		Palettes.import_palette_from_path(file, true)

	elif file_ext in ["pck", "zip"]:  # Godot resource pack file
		Global.control.get_node("Extensions").install_extension(file)

	elif file_ext == "shader" or file_ext == "gdshader":  # Godot shader file
		var shader := load(file)
		if not shader is Shader:
			return
		var file_name: String = file.get_file().get_basename()
		Global.control.find_child("ShaderEffect").change_shader(shader, file_name)

	else:  # Image files
		# Attempt to load as APNG.
		# Note that the APNG importer will *only* succeed for *animated* PNGs.
		# This is intentional as still images should still act normally.
		var apng_res := AImgIOAPNGImporter.load_from_file(file)
		if apng_res[0] == null:
			# No error - this is an APNG!
			if typeof(apng_res[1]) == TYPE_ARRAY:
				handle_loading_aimg(file, apng_res[1])
			return
		# Attempt to load as a regular image.
		var image := Image.load_from_file(file)
		if not is_instance_valid(image):  # Failed to import as image
			if handle_loading_video(file):
				return  # Succeeded in loading as video, so return early before the error appears
			var file_name: String = file.get_file()
			Global.popup_error(tr("Can't load file '%s'.") % [file_name])
			return
		handle_loading_image(file, image)


func add_import_option(import_name: StringName, import_scene: PackedScene) -> int:
	# Change format name if another one uses the same name
	var existing_format_names := (
		ImportPreviewDialog.ImageImportOptions.keys() + custom_import_names.keys()
	)
	for i in range(existing_format_names.size()):
		var test_name := import_name
		if i != 0:
			test_name = str(test_name, "_", i)
		if !existing_format_names.has(test_name):
			import_name = test_name
			break

	# Obtain a unique id
	var id := ImportPreviewDialog.ImageImportOptions.size()
	for i in custom_import_names.size():
		var format_id := id + i
		if !custom_import_names.values().has(i):
			id = format_id
	# Add to custom_file_formats
	custom_import_names.merge({import_name: id})
	custom_importer_scenes.merge({id: import_scene})
	return id


## Mostly used for downloading images from the Internet. Tries multiple file extensions
## in case the extension of the file is wrong, which is common for images on the Internet.
func load_image_from_buffer(buffer: PackedByteArray) -> Image:
	var image := Image.new()
	var err := image.load_png_from_buffer(buffer)
	if err != OK:
		err = image.load_jpg_from_buffer(buffer)
		if err != OK:
			err = image.load_webp_from_buffer(buffer)
			if err != OK:
				err = image.load_tga_from_buffer(buffer)
				if err != OK:
					image.load_bmp_from_buffer(buffer)
	return image


func handle_loading_image(file: String, image: Image) -> void:
	if Global.projects.size() <= 1 and Global.current_project.is_empty():
		open_image_as_new_tab(file, image)
		return
	var preview_dialog := preview_dialog_tscn.instantiate() as ImportPreviewDialog
	# add custom importers to preview dialog
	for import_name in custom_import_names.keys():
		var id = custom_import_names[import_name]
		var new_import_option = custom_importer_scenes[id].instantiate()
		preview_dialog.custom_importers[id] = new_import_option
	preview_dialogs.append(preview_dialog)
	preview_dialog.path = file
	preview_dialog.image = image
	Global.control.add_child(preview_dialog)
	preview_dialog.popup_centered()
	Global.dialog_open(true)


## For loading the output of AImgIO as a project
func handle_loading_aimg(path: String, frames: Array) -> void:
	var project := Project.new([], path.get_file(), frames[0].content.get_size())
	project.layers.append(PixelLayer.new(project))
	Global.projects.append(project)

	# Determine FPS as 1, unless all frames agree.
	project.fps = 1
	var first_duration: float = frames[0].duration
	var frames_agree := true
	for v in frames:
		var aimg_frame: AImgIOFrame = v
		if aimg_frame.duration != first_duration:
			frames_agree = false
			break
	if frames_agree and (first_duration > 0.0):
		project.fps = 1.0 / first_duration
	# Convert AImgIO frames to Pixelorama frames
	for v in frames:
		var aimg_frame: AImgIOFrame = v
		var frame := Frame.new()
		if not frames_agree:
			frame.duration = aimg_frame.duration * project.fps
		var content := aimg_frame.content
		content.convert(Image.FORMAT_RGBA8)
		frame.cels.append(PixelCel.new(content, 1))
		project.frames.append(frame)

	set_new_imported_tab(project, path)


## Uses FFMPEG to attempt to load a video file as a new project. Works by splitting the video file
## to multiple png images for each of the video's frames,
## and then it imports these images as frames of a new project.
## TODO: Don't allow large files (how large?) to be imported, to avoid crashes due to lack of memory
## TODO: Find the video's fps and use that for the new project.
func handle_loading_video(file: String) -> bool:
	DirAccess.make_dir_absolute(Export.TEMP_PATH)
	var temp_path_real := ProjectSettings.globalize_path(Export.TEMP_PATH)
	var output_file_path := temp_path_real.path_join("%04d.png")
	# ffmpeg -y -i input_file %04d.png
	var ffmpeg_execute: PackedStringArray = ["-y", "-i", file, output_file_path]
	var success := OS.execute(Global.ffmpeg_path, ffmpeg_execute, [], true)
	if success < 0 or success > 1:  # FFMPEG is probably not installed correctly
		DirAccess.remove_absolute(Export.TEMP_PATH)
		return false
	var images_to_import: Array[Image] = []
	var project_size := Vector2i.ZERO
	var temp_dir := DirAccess.open(Export.TEMP_PATH)
	for temp_file in temp_dir.get_files():
		var temp_image := Image.load_from_file(Export.TEMP_PATH.path_join(temp_file))
		temp_dir.remove(temp_file)
		if not is_instance_valid(temp_image):
			continue
		images_to_import.append(temp_image)
		if temp_image.get_width() > project_size.x:
			project_size.x = temp_image.get_width()
		if temp_image.get_height() > project_size.y:
			project_size.y = temp_image.get_height()
	DirAccess.remove_absolute(Export.TEMP_PATH)
	if images_to_import.size() == 0 or project_size == Vector2i.ZERO:
		return false  # We didn't find any images, return
	# If we found images, create a new project out of them
	var new_project := Project.new([], file.get_basename().get_file(), project_size)
	new_project.layers.append(PixelLayer.new(new_project))
	for temp_image in images_to_import:
		open_image_as_new_frame(temp_image, 0, new_project, false)
	Global.projects.append(new_project)
	Global.tabs.current_tab = Global.tabs.get_tab_count() - 1
	Global.canvas.camera_zoom()
	return true


func open_pxo_file(path: String, is_backup := false, replace_empty := true) -> void:
	var empty_project := Global.current_project.is_empty() and replace_empty
	var new_project: Project
	var zip_reader := ZIPReader.new()
	var err := zip_reader.open(path)
	if err == FAILED:
		# Most likely uses the old pxo format, load that
		new_project = open_v0_pxo_file(path, empty_project)
		if not is_instance_valid(new_project):
			return
	elif err != OK:
		Global.popup_error(tr("File failed to open. Error code %s (%s)") % [err, error_string(err)])
		return
	else:
		if empty_project:
			new_project = Global.current_project
			new_project.frames = []
			new_project.layers = []
			new_project.animation_tags.clear()
			new_project.name = path.get_file().get_basename()
		else:
			new_project = Project.new([], path.get_file().get_basename())
		var data_json := zip_reader.read_file("data.json").get_string_from_utf8()
		var test_json_conv := JSON.new()
		var error := test_json_conv.parse(data_json)
		if error != OK:
			print("Error, corrupt pxo file. Error code %s (%s)" % [error, error_string(error)])
			zip_reader.close()
			return
		var result = test_json_conv.get_data()
		if typeof(result) != TYPE_DICTIONARY:
			print("Error, json parsed result is: %s" % typeof(result))
			zip_reader.close()
			return

		new_project.deserialize(result, zip_reader)
		if result.has("brushes"):
			var brush_index := 0
			for brush in result.brushes:
				var b_width: int = brush.size_x
				var b_height: int = brush.size_y
				var image_data := zip_reader.read_file("image_data/brushes/brush_%s" % brush_index)
				var image := Image.create_from_data(
					b_width, b_height, false, Image.FORMAT_RGBA8, image_data
				)
				new_project.brushes.append(image)
				Brushes.add_project_brush(image)
				brush_index += 1
		if result.has("tile_mask") and result.has("has_mask"):
			if result.has_mask:
				var t_width = result.tile_mask.size_x
				var t_height = result.tile_mask.size_y
				var image_data := zip_reader.read_file("image_data/tile_map")
				var image := Image.create_from_data(
					t_width, t_height, false, Image.FORMAT_RGBA8, image_data
				)
				new_project.tiles.tile_mask = image
			else:
				new_project.tiles.reset_mask()
		zip_reader.close()
	new_project.export_directory_path = path.get_base_dir()

	if empty_project:
		new_project.change_project()
		Global.project_switched.emit()
		Global.cel_switched.emit()
	else:
		Global.projects.append(new_project)
		Global.tabs.current_tab = Global.tabs.get_tab_count() - 1
	Global.canvas.camera_zoom()

	if is_backup:
		new_project.backup_path = path
	else:
		# Loading a backup should not change window title and save path
		new_project.save_path = path
		get_window().title = new_project.name + " - Pixelorama " + Global.current_version
		# Set last opened project path and save
		Global.config_cache.set_value("data", "current_dir", path.get_base_dir())
		Global.config_cache.set_value("data", "last_project_path", path)
		Global.config_cache.save(Global.CONFIG_PATH)
		new_project.file_name = path.get_file().trim_suffix(".pxo")
		new_project.was_exported = false
		Global.top_menu_container.file_menu.set_item_text(
			Global.FileMenu.SAVE, tr("Save") + " %s" % path.get_file()
		)
		Global.top_menu_container.file_menu.set_item_text(Global.FileMenu.EXPORT, tr("Export"))

	save_project_to_recent_list(path)


func open_v0_pxo_file(path: String, empty_project: bool) -> Project:
	var file := FileAccess.open_compressed(path, FileAccess.READ, FileAccess.COMPRESSION_ZSTD)
	if FileAccess.get_open_error() == ERR_FILE_UNRECOGNIZED:
		# If the file is not compressed open it raw (pre-v0.7)
		file = FileAccess.open(path, FileAccess.READ)
	var err := FileAccess.get_open_error()
	if err != OK:
		Global.popup_error(tr("File failed to open. Error code %s (%s)") % [err, error_string(err)])
		return null

	var first_line := file.get_line()
	var test_json_conv := JSON.new()
	var error := test_json_conv.parse(first_line)
	if error != OK:
		print("Error, corrupt legacy pxo file. Error code %s (%s)" % [error, error_string(error)])
		file.close()
		return null

	var result = test_json_conv.get_data()
	if typeof(result) != TYPE_DICTIONARY:
		print("Error, json parsed result is: %s" % typeof(result))
		file.close()
		return null

	var new_project: Project
	if empty_project:
		new_project = Global.current_project
		new_project.frames = []
		new_project.layers = []
		new_project.animation_tags.clear()
		new_project.name = path.get_file().get_basename()
	else:
		new_project = Project.new([], path.get_file().get_basename())
	new_project.deserialize(result, null, file)
	if result.has("brushes"):
		for brush in result.brushes:
			var b_width = brush.size_x
			var b_height = brush.size_y
			var buffer := file.get_buffer(b_width * b_height * 4)
			var image := Image.create_from_data(
				b_width, b_height, false, Image.FORMAT_RGBA8, buffer
			)
			new_project.brushes.append(image)
			Brushes.add_project_brush(image)

	if result.has("tile_mask") and result.has("has_mask"):
		if result.has_mask:
			var t_width = result.tile_mask.size_x
			var t_height = result.tile_mask.size_y
			var buffer := file.get_buffer(t_width * t_height * 4)
			var image := Image.create_from_data(
				t_width, t_height, false, Image.FORMAT_RGBA8, buffer
			)
			new_project.tiles.tile_mask = image
		else:
			new_project.tiles.reset_mask()
	file.close()
	return new_project


func save_pxo_file(
	path: String, autosave: bool, include_blended := false, project := Global.current_project
) -> bool:
	if not autosave:
		project.name = path.get_file().trim_suffix(".pxo")
	var serialized_data := project.serialize()
	if not serialized_data:
		Global.popup_error(tr("File failed to save. Converting project data to dictionary failed."))
		return false
	var to_save := JSON.stringify(serialized_data)
	if not to_save:
		Global.popup_error(tr("File failed to save. Converting dictionary to JSON failed."))
		return false

	# Check if a file with the same name exists. If it does, rename the new file temporarily.
	# Needed in case of a crash, so that the old file won't be replaced with an empty one.
	var temp_path := path
	if FileAccess.file_exists(path):
		temp_path = path + "1"

	var zip_packer := ZIPPacker.new()
	var err := zip_packer.open(temp_path)
	if err != OK:
		if temp_path.is_valid_filename():
			return false
		Global.popup_error(tr("File failed to save. Error code %s (%s)") % [err, error_string(err)])
		if zip_packer:  # this would be null if we attempt to save filenames such as "//\\||.pxo"
			zip_packer.close()
		return false
	zip_packer.start_file("data.json")
	zip_packer.write_file(to_save.to_utf8_buffer())
	zip_packer.close_file()

	if not autosave:
		project.save_path = path

	var frame_index := 1
	for frame in project.frames:
		if not autosave and include_blended:
			var blended := Image.create(project.size.x, project.size.y, false, Image.FORMAT_RGBA8)
			DrawingAlgos.blend_layers(blended, frame, Vector2i.ZERO, project)
			zip_packer.start_file("image_data/final_images/%s" % frame_index)
			zip_packer.write_file(blended.get_data())
			zip_packer.close_file()
		var cel_index := 1
		for cel in frame.cels:
			var cel_image := cel.get_image()
			if is_instance_valid(cel_image) and cel is PixelCel:
				zip_packer.start_file("image_data/frames/%s/layer_%s" % [frame_index, cel_index])
				zip_packer.write_file(cel_image.get_data())
				zip_packer.close_file()
			cel_index += 1
		frame_index += 1
	var brush_index := 0
	for brush in project.brushes:
		zip_packer.start_file("image_data/brushes/brush_%s" % brush_index)
		zip_packer.write_file(brush.get_data())
		zip_packer.close_file()
		brush_index += 1
	if project.tiles.has_mask:
		zip_packer.start_file("image_data/tile_map")
		zip_packer.write_file(project.tiles.tile_mask.get_data())
		zip_packer.close_file()
	zip_packer.close()

	if temp_path != path:
		# Rename the new file to its proper name and remove the old file, if it exists.
		DirAccess.rename_absolute(temp_path, path)

	if OS.has_feature("web") and not autosave:
		var file := FileAccess.open(path, FileAccess.READ)
		if FileAccess.get_open_error() == OK:
			var file_data := file.get_buffer(file.get_length())
			JavaScriptBridge.download_buffer(file_data, path.get_file())
		file.close()
		# Remove the .pxo file from memory, as we don't need it anymore
		DirAccess.remove_absolute(path)

	if autosave:
		Global.notification_label("File autosaved")
	else:
		# First remove backup then set current save path
		if project.has_changed:
			project.has_changed = false
		Global.current_project.remove_backup_file()
		Global.notification_label("File saved")
		get_window().title = project.name + " - Pixelorama " + Global.current_version

		# Set last opened project path and save
		Global.config_cache.set_value("data", "current_dir", path.get_base_dir())
		Global.config_cache.set_value("data", "last_project_path", path)
		Global.config_cache.save(Global.CONFIG_PATH)
		if !project.was_exported:
			project.file_name = path.get_file().trim_suffix(".pxo")
			project.export_directory_path = path.get_base_dir()
		Global.top_menu_container.file_menu.set_item_text(
			Global.FileMenu.SAVE, tr("Save") + " %s" % path.get_file()
		)
		project_saved.emit()
		SteamManager.set_achievement("ACH_SAVE")

	save_project_to_recent_list(path)
	return true


func open_image_as_new_tab(path: String, image: Image) -> void:
	var project := Project.new([], path.get_file(), image.get_size())
	project.layers.append(PixelLayer.new(project))
	Global.projects.append(project)

	var frame := Frame.new()
	image.convert(Image.FORMAT_RGBA8)
	frame.cels.append(PixelCel.new(image, 1))

	project.frames.append(frame)
	set_new_imported_tab(project, path)


func open_image_as_spritesheet_tab_smart(
	path: String, image: Image, sliced_rects: Array[Rect2i], frame_size: Vector2i
) -> void:
	if sliced_rects.size() == 0:  # Image is empty sprite (manually set data to be consistent)
		frame_size = image.get_size()
		sliced_rects.append(Rect2i(Vector2i.ZERO, frame_size))
	var project := Project.new([], path.get_file(), frame_size)
	project.layers.append(PixelLayer.new(project))
	Global.projects.append(project)
	for rect in sliced_rects:
		var offset: Vector2 = (0.5 * (frame_size - rect.size)).floor()
		var frame := Frame.new()
		var cropped_image := Image.create(frame_size.x, frame_size.y, false, Image.FORMAT_RGBA8)
		image.convert(Image.FORMAT_RGBA8)
		cropped_image.blit_rect(image, rect, offset)
		frame.cels.append(PixelCel.new(cropped_image, 1))
		project.frames.append(frame)
	set_new_imported_tab(project, path)


func open_image_as_spritesheet_tab(path: String, image: Image, horiz: int, vert: int) -> void:
	horiz = mini(horiz, image.get_size().x)
	vert = mini(vert, image.get_size().y)
	var frame_width := image.get_size().x / horiz
	var frame_height := image.get_size().y / vert
	var project := Project.new([], path.get_file(), Vector2(frame_width, frame_height))
	project.layers.append(PixelLayer.new(project))
	Global.projects.append(project)
	for yy in range(vert):
		for xx in range(horiz):
			var frame := Frame.new()
			var cropped_image := image.get_region(
				Rect2i(frame_width * xx, frame_height * yy, frame_width, frame_height)
			)
			project.size = cropped_image.get_size()
			cropped_image.convert(Image.FORMAT_RGBA8)
			frame.cels.append(PixelCel.new(cropped_image, 1))
			project.frames.append(frame)
	set_new_imported_tab(project, path)


func open_image_as_spritesheet_layer_smart(
	_path: String,
	image: Image,
	file_name: String,
	sliced_rects: Array[Rect2i],
	start_frame: int,
	frame_size: Vector2i
) -> void:
	# Resize canvas to if "frame_size.x" or "frame_size.y" is too large
	var project := Global.current_project
	var project_width := maxi(frame_size.x, project.size.x)
	var project_height := maxi(frame_size.y, project.size.y)
	if project.size < Vector2i(project_width, project_height):
		DrawingAlgos.resize_canvas(project_width, project_height, 0, 0)

	# Initialize undo mechanism
	project.undos += 1
	project.undo_redo.create_action("Add Spritesheet Layer")

	# Create new frames (if needed)
	var new_frames_size := maxi(project.frames.size(), start_frame + sliced_rects.size())
	var frames := []
	var frame_indices := PackedInt32Array([])
	if new_frames_size > project.frames.size():
		var required_frames := new_frames_size - project.frames.size()
		frame_indices = range(
			project.current_frame + 1, project.current_frame + required_frames + 1
		)
		for i in required_frames:
			var new_frame := Frame.new()
			for l in range(project.layers.size()):  # Create as many cels as there are layers
				new_frame.cels.append(project.layers[l].new_empty_cel())
				if project.layers[l].new_cels_linked:
					var prev_cel := project.frames[project.current_frame].cels[l]
					if prev_cel.link_set == null:
						prev_cel.link_set = {}
						project.undo_redo.add_do_method(
							project.layers[l].link_cel.bind(prev_cel, prev_cel.link_set)
						)
						project.undo_redo.add_undo_method(
							project.layers[l].link_cel.bind(prev_cel, null)
						)
					new_frame.cels[l].set_content(prev_cel.get_content(), prev_cel.image_texture)
					new_frame.cels[l].link_set = prev_cel.link_set
			frames.append(new_frame)

	# Create new layer for spritesheet
	var layer := PixelLayer.new(project, file_name)
	var cels: Array[PixelCel] = []
	for f in new_frames_size:
		if f >= start_frame and f < (start_frame + sliced_rects.size()):
			# Slice spritesheet
			var offset: Vector2 = (0.5 * (frame_size - sliced_rects[f - start_frame].size)).floor()
			image.convert(Image.FORMAT_RGBA8)
			var cropped_image := Image.create(
				project_width, project_height, false, Image.FORMAT_RGBA8
			)
			cropped_image.blit_rect(image, sliced_rects[f - start_frame], offset)
			cels.append(PixelCel.new(cropped_image))
		else:
			cels.append(layer.new_empty_cel())

	project.undo_redo.add_do_method(project.add_frames.bind(frames, frame_indices))
	project.undo_redo.add_do_method(
		project.add_layers.bind([layer], [project.layers.size()], [cels])
	)
	project.undo_redo.add_do_method(
		project.change_cel.bind(new_frames_size - 1, project.layers.size())
	)
	project.undo_redo.add_do_method(Global.undo_or_redo.bind(false))

	project.undo_redo.add_undo_method(project.remove_layers.bind([project.layers.size()]))
	project.undo_redo.add_undo_method(project.remove_frames.bind(frame_indices))
	project.undo_redo.add_undo_method(
		project.change_cel.bind(project.current_frame, project.current_layer)
	)
	project.undo_redo.add_undo_method(Global.undo_or_redo.bind(true))
	project.undo_redo.commit_action()


func open_image_as_spritesheet_layer(
	_path: String, image: Image, file_name: String, horizontal: int, vertical: int, start_frame: int
) -> void:
	# Data needed to slice images
	horizontal = mini(horizontal, image.get_size().x)
	vertical = mini(vertical, image.get_size().y)
	var frame_width := image.get_size().x / horizontal
	var frame_height := image.get_size().y / vertical

	# Resize canvas to if "frame_width" or "frame_height" is too large
	var project := Global.current_project
	var project_width := maxi(frame_width, project.size.x)
	var project_height := maxi(frame_height, project.size.y)
	if project.size < Vector2i(project_width, project_height):
		DrawingAlgos.resize_canvas(project_width, project_height, 0, 0)

	# Initialize undo mechanism
	project.undos += 1
	project.undo_redo.create_action("Add Spritesheet Layer")

	# Create new frames (if needed)
	var new_frames_size := maxi(project.frames.size(), start_frame + (vertical * horizontal))
	var frames := []
	var frame_indices := PackedInt32Array([])
	if new_frames_size > project.frames.size():
		var required_frames := new_frames_size - project.frames.size()
		frame_indices = range(
			project.current_frame + 1, project.current_frame + required_frames + 1
		)
		for i in required_frames:
			var new_frame := Frame.new()
			for l in range(project.layers.size()):  # Create as many cels as there are layers
				new_frame.cels.append(project.layers[l].new_empty_cel())
				if project.layers[l].new_cels_linked:
					var prev_cel := project.frames[project.current_frame].cels[l]
					if prev_cel.link_set == null:
						prev_cel.link_set = {}
						project.undo_redo.add_do_method(
							project.layers[l].link_cel.bind(prev_cel, prev_cel.link_set)
						)
						project.undo_redo.add_undo_method(
							project.layers[l].link_cel.bind(prev_cel, null)
						)
					new_frame.cels[l].set_content(prev_cel.get_content(), prev_cel.image_texture)
					new_frame.cels[l].link_set = prev_cel.link_set
			frames.append(new_frame)

	# Create new layer for spritesheet
	var layer := PixelLayer.new(project, file_name)
	var cels := []
	for f in new_frames_size:
		if f >= start_frame and f < (start_frame + (vertical * horizontal)):
			# Slice spritesheet
			var xx := (f - start_frame) % horizontal
			var yy := (f - start_frame) / horizontal
			image.convert(Image.FORMAT_RGBA8)
			var cropped_image := Image.create(
				project_width, project_height, false, Image.FORMAT_RGBA8
			)
			cropped_image.blit_rect(
				image,
				Rect2i(frame_width * xx, frame_height * yy, frame_width, frame_height),
				Vector2i.ZERO
			)
			cels.append(PixelCel.new(cropped_image))
		else:
			cels.append(layer.new_empty_cel())

	project.undo_redo.add_do_method(project.add_frames.bind(frames, frame_indices))
	project.undo_redo.add_do_method(
		project.add_layers.bind([layer], [project.layers.size()], [cels])
	)
	project.undo_redo.add_do_method(
		project.change_cel.bind(new_frames_size - 1, project.layers.size())
	)
	project.undo_redo.add_do_method(Global.undo_or_redo.bind(false))

	project.undo_redo.add_undo_method(project.remove_layers.bind([project.layers.size()]))
	project.undo_redo.add_undo_method(project.remove_frames.bind(frame_indices))
	project.undo_redo.add_undo_method(
		project.change_cel.bind(project.current_frame, project.current_layer)
	)
	project.undo_redo.add_undo_method(Global.undo_or_redo.bind(true))
	project.undo_redo.commit_action()


func open_image_at_cel(image: Image, layer_index := 0, frame_index := 0) -> void:
	var project := Global.current_project
	var project_width := maxi(image.get_width(), project.size.x)
	var project_height := maxi(image.get_height(), project.size.y)
	if project.size < Vector2i(project_width, project_height):
		DrawingAlgos.resize_canvas(project_width, project_height, 0, 0)
	project.undos += 1
	project.undo_redo.create_action("Replaced Cel")

	var cel := project.frames[frame_index].cels[layer_index]
	if not cel is PixelCel:
		return
	image.convert(Image.FORMAT_RGBA8)
	var cel_image := Image.create(project_width, project_height, false, Image.FORMAT_RGBA8)
	cel_image.blit_rect(image, Rect2i(Vector2i.ZERO, image.get_size()), Vector2i.ZERO)
	Global.undo_redo_compress_images(
		{cel.image: cel_image.data}, {cel.image: cel.image.data}, project
	)

	project.undo_redo.add_do_property(project, "selected_cels", [])
	project.undo_redo.add_do_method(project.change_cel.bind(frame_index, layer_index))
	project.undo_redo.add_do_method(Global.undo_or_redo.bind(false))

	project.undo_redo.add_undo_property(project, "selected_cels", [])
	project.undo_redo.add_undo_method(
		project.change_cel.bind(project.current_frame, project.current_layer)
	)
	project.undo_redo.add_undo_method(Global.undo_or_redo.bind(true))
	project.undo_redo.commit_action()


func open_image_as_new_frame(
	image: Image, layer_index := 0, project := Global.current_project, undo := true
) -> void:
	var project_width := maxi(image.get_width(), project.size.x)
	var project_height := maxi(image.get_height(), project.size.y)
	if project.size < Vector2i(project_width, project_height):
		DrawingAlgos.resize_canvas(project_width, project_height, 0, 0)

	var frame := Frame.new()
	for i in project.layers.size():
		if i == layer_index:
			image.convert(Image.FORMAT_RGBA8)
			var cel_image := Image.create(project_width, project_height, false, Image.FORMAT_RGBA8)
			cel_image.blit_rect(image, Rect2i(Vector2i.ZERO, image.get_size()), Vector2i.ZERO)
			frame.cels.append(PixelCel.new(cel_image, 1))
		else:
			frame.cels.append(project.layers[i].new_empty_cel())
	if not undo:
		project.frames.append(frame)
		return
	project.undos += 1
	project.undo_redo.create_action("Add Frame")
	project.undo_redo.add_do_method(Global.undo_or_redo.bind(false))
	project.undo_redo.add_do_method(project.add_frames.bind([frame], [project.frames.size()]))
	project.undo_redo.add_do_method(project.change_cel.bind(project.frames.size(), layer_index))

	project.undo_redo.add_undo_method(Global.undo_or_redo.bind(true))
	project.undo_redo.add_undo_method(project.remove_frames.bind([project.frames.size()]))
	project.undo_redo.add_undo_method(
		project.change_cel.bind(project.current_frame, project.current_layer)
	)
	project.undo_redo.commit_action()


func open_image_as_new_layer(image: Image, file_name: String, frame_index := 0) -> void:
	var project := Global.current_project
	var project_width := maxi(image.get_width(), project.size.x)
	var project_height := maxi(image.get_height(), project.size.y)
	if project.size < Vector2i(project_width, project_height):
		DrawingAlgos.resize_canvas(project_width, project_height, 0, 0)
	var layer := PixelLayer.new(project, file_name)
	var cels := []

	Global.current_project.undos += 1
	Global.current_project.undo_redo.create_action("Add Layer")
	for i in project.frames.size():
		if i == frame_index:
			image.convert(Image.FORMAT_RGBA8)
			var cel_image := Image.create(project_width, project_height, false, Image.FORMAT_RGBA8)
			cel_image.blit_rect(image, Rect2i(Vector2i.ZERO, image.get_size()), Vector2i.ZERO)
			cels.append(PixelCel.new(cel_image, 1))
		else:
			cels.append(layer.new_empty_cel())

	project.undo_redo.add_do_method(
		project.add_layers.bind([layer], [project.layers.size()], [cels])
	)
	project.undo_redo.add_do_method(project.change_cel.bind(frame_index, project.layers.size()))

	project.undo_redo.add_undo_method(project.remove_layers.bind([project.layers.size()]))
	project.undo_redo.add_undo_method(
		project.change_cel.bind(project.current_frame, project.current_layer)
	)

	project.undo_redo.add_undo_method(Global.undo_or_redo.bind(true))
	project.undo_redo.add_do_method(Global.undo_or_redo.bind(false))
	project.undo_redo.commit_action()


func import_reference_image_from_path(path: String) -> void:
	var project := Global.current_project
	var ri := ReferenceImage.new()
	ri.project = project
	ri.deserialize({"image_path": path})
	Global.canvas.reference_image_container.add_child(ri)
	reference_image_imported.emit()


## Useful for Web
func import_reference_image_from_image(image: Image) -> void:
	var project := Global.current_project
	var ri := ReferenceImage.new()
	ri.project = project
	ri.create_from_image(image)
	Global.canvas.reference_image_container.add_child(ri)
	reference_image_imported.emit()


func set_new_imported_tab(project: Project, path: String) -> void:
	var prev_project_empty := Global.current_project.is_empty()
	var prev_project_pos := Global.current_project_index

	get_window().title = (
		path.get_file() + " (" + tr("imported") + ") - Pixelorama " + Global.current_version
	)
	if project.has_changed:
		get_window().title = get_window().title + "(*)"
	var file_name := path.get_basename().get_file()
	project.export_directory_path = path.get_base_dir()
	project.file_name = file_name
	project.was_exported = true
	if path.get_extension().to_lower() == "png":
		project.export_overwrite = true

	Global.tabs.current_tab = Global.tabs.get_tab_count() - 1
	Global.canvas.camera_zoom()

	if prev_project_empty:
		Global.tabs.delete_tab(prev_project_pos)


func update_autosave() -> void:
	if not is_instance_valid(autosave_timer):
		return
	autosave_timer.stop()
	# Interval parameter is in minutes, wait_time is seconds
	autosave_timer.wait_time = Global.autosave_interval * 60
	if Global.enable_autosave:
		autosave_timer.start()


func _on_Autosave_timeout() -> void:
	for i in Global.projects.size():
		var project := Global.projects[i]
		if project.backup_path.is_empty():
			project.backup_path = (
				"user://backups/backup-" + str(Time.get_unix_time_from_system()) + "-%s" % i
			)
		save_pxo_file(project.backup_path, true, false, project)


## Load the backup files
func reload_backup_file() -> void:
	var dir := DirAccess.open("user://backups")
	var i := 0
	for file in dir.get_files():
		open_pxo_file("user://backups".path_join(file), true, i == 0)
		i += 1
	Global.notification_label("Backup reloaded")


func save_project_to_recent_list(path: String) -> void:
	var top_menu_container := Global.top_menu_container
	if path.get_file().substr(0, 7) == "backup-" or path == "":
		return

	if top_menu_container.recent_projects.has(path):
		top_menu_container.recent_projects.erase(path)

	if top_menu_container.recent_projects.size() >= 5:
		top_menu_container.recent_projects.pop_front()
	top_menu_container.recent_projects.push_back(path)

	Global.config_cache.set_value("data", "recent_projects", top_menu_container.recent_projects)

	top_menu_container.recent_projects_submenu.clear()
	top_menu_container.update_recent_projects_submenu()
