@tool
## Resource or Project FILE for every PixelPenProject
class_name PixelPenProject
extends Resource

# Increase this number if @export property change
const COMPATIBILITY_NUMBER : int = 3

@export var compatibility_version : int
@export var project_name : String
@export var file_path : String
@export var last_export_file_path : String
@export var canvas_size : Vector2i

@export var palette : IndexedPalette

@export var pool_frames : Array[Frame]
@export var canvas_pool_frame_uid : Vector3i

@export var show_timeline : bool = false
@export var animation_timeline : Array[AnimationCell]
@export var animation_frame_index : int = -1
@export var animation_fps : float = 30
@export var animation_loop : bool = true
@export var onion_skinning : bool = false

## Used by internal for auto name layer
@export var layer_index_counter : int
@export var active_layer_uid : Vector3i
@export var use_sample : bool = false
@export var _sample_offset : Vector2i
@export var _cache_canvas_size : Vector2i
@export var _cache_pool_frames : Array[Frame]
@export var _cache_canvas_pool_frame_uid : Vector3i

@export var show_grid : bool = false
@export var show_tile : bool = false
@export var show_preview : bool = true
@export var show_symetric_vertical : bool = false
@export var show_symetric_horizontal : bool = false
@export var symetric_guid : Vector2
@export var background_color : BackgroundColor = BackgroundColor.TRANSPARENT

@export var uid_counter : int

var paint : PixelPenImage = PixelPenImage.new()
var brush_index : int = -1

var animation_is_play : bool = false
var skinning_frame_index : int = -1 # Record reference frame
var animation_prev_skinning_image : Array[Image] = []
var animation_next_skinning_image : Array[Image] = []

var active_frame : Frame:
	get:
		return get_pool_frame(canvas_pool_frame_uid)
var active_layer : IndexedColorImage:
	get:
		return get_index_image(active_layer_uid)
var is_saved : bool = false

var undo_redo : UndoRedoManager

var cache_copied_colormap : IndexedColorImage
var cache_thumbnail : Image
var multilayer_selected : Array[Vector3i] 

var _cache_undo_redo : UndoRedoManager

enum ProjectMode{
	BASE = 0,
	SAMPLE = 1
}

enum BackgroundColor{
	TRANSPARENT = 0,
	WHITE,
	GREY,
	BLACK
}


func initialized(p_size : Vector2i, p_name : String = "Untitled", p_file_path : String = "", one_layer : bool = true):
	compatibility_version = COMPATIBILITY_NUMBER
	layer_index_counter = 0
	project_name = p_name
	canvas_size = p_size
	animation_fps = PixelPen.state.userconfig.default_animation_fps
	symetric_guid = canvas_size * 0.5
	palette = IndexedPalette.new()
	palette.set_color_index_preset()
	sync_gui_palette()
	pool_frames = [Frame.create(get_uid())]
	canvas_pool_frame_uid = pool_frames[0].frame_uid
	var cell : AnimationCell = AnimationCell.create(get_uid())
	cell.frame = pool_frames[0]
	animation_timeline.push_back(cell)
	animation_frame_index = 0
	if one_layer:
		add_layer()
		active_layer_uid = active_frame.layers[0].layer_uid


func get_uid():
	uid_counter += 1
	var uid = Vector3i(uid_counter, randi(), Time.get_unix_time_from_system() as int)
	if uid == Vector3i.ZERO:
		return get_uid()
	return uid


func get_json() -> String:
	var pool_frames_data : Array = []
	for frame in pool_frames:
		pool_frames_data.push_back(frame.get_data())
	var _cache_pool_frames_data : Array = []
	for frame in _cache_pool_frames:
		_cache_pool_frames_data.push_back(frame.get_data())
	var arr_animation_timeline : Array = []
	for cell in animation_timeline:
		arr_animation_timeline.push_back(cell.get_data())
	var metadata : Dictionary = {
		"compatibility_version" : compatibility_version,
		"project_name" : project_name,
		"file_path" : file_path,
		"last_export_file_path" : last_export_file_path,
		"canvas_size" : var_to_str(canvas_size),
		"palette" : palette.get_data(),
		"pool_frames" : pool_frames_data,
		"canvas_pool_frame_uid" : var_to_str(canvas_pool_frame_uid),
		"show_timeline" : show_timeline,
		"animation_timeline" : arr_animation_timeline,
		"animation_frame_index" : animation_frame_index,
		"animation_fps" : animation_fps,
		"animation_loop" : animation_loop,
		"onion_skinning" : onion_skinning,
		"layer_index_counter" : layer_index_counter,
		"active_layer_uid" : var_to_str(active_layer_uid),
		"use_sample" : use_sample,
		"_sample_offset" : var_to_str(_sample_offset),
		"_cache_canvas_size" : var_to_str(_cache_canvas_size),
		"_cache_pool_frames" : _cache_pool_frames_data,
		"_cache_canvas_pool_frame_uid" : var_to_str(_cache_canvas_pool_frame_uid),
		"show_grid" : show_grid,
		"show_tile" : show_tile,
		"show_preview" : show_preview,
		"show_symetric_vertical" : show_symetric_vertical,
		"show_symetric_horizontal" : show_symetric_horizontal,
		"symetric_guid" : var_to_str(symetric_guid),
		"background_color" : var_to_str(background_color),
		"uid_counter" : uid_counter
	}
	var json := JSON.new()
	return json.stringify(metadata)


func from_json(json_string : String) -> Error:
	var json := JSON.new()
	var err := json.parse(json_string)
	if err != OK:
		return err

	var json_data : Dictionary = json.data as Dictionary
	if json_data.has("compatibility_version"):
		compatibility_version = json_data["compatibility_version"] as int
	else:
		return FAILED
	if json_data.has("project_name"):
		project_name = json_data["project_name"] as String
	else:
		return FAILED
	if json_data.has("file_path"):
		file_path = json_data["file_path"] as String
	else:
		return FAILED
	if json_data.has("last_export_file_path"):
		last_export_file_path = json_data["last_export_file_path"] as String
	else:
		return FAILED
	if json_data.has("canvas_size"):
		canvas_size = str_to_var(json_data["canvas_size"]) as Vector2i
	else:
		return FAILED
	if json_data.has("palette"):
		palette = IndexedPalette.new()
		if palette.from_data(json_data["palette"]) != OK:
			return FAILED
	else:
		return FAILED
	if json_data.has("pool_frames"):
		pool_frames.clear()
		var pool_frames_data : Array = json_data["pool_frames"] as Array
		for data in pool_frames_data:
			var frame := Frame.new()
			var frame_err := frame.from_data(data)
			if frame_err != OK:
				return FAILED
			pool_frames.push_back(frame)

	else:
		return FAILED
	if json_data.has("canvas_pool_frame_uid"):
		canvas_pool_frame_uid = str_to_var(json_data["canvas_pool_frame_uid"]) as Vector3i
	else:
		return FAILED
	if json_data.has("show_timeline"):
		show_timeline = json_data["show_timeline"] as bool
	else:
		return FAILED
	if json_data.has("animation_frame_index"):
		animation_frame_index = json_data["animation_frame_index"] as int
	else:
		return FAILED
	if json_data.has("animation_fps"):
		animation_fps = json_data["animation_fps"] as float
	else:
		return FAILED
	if json_data.has("animation_loop"):
		animation_loop = json_data["animation_loop"] as bool
	else:
		return FAILED
	if json_data.has("onion_skinning"):
		onion_skinning = json_data["onion_skinning"] as bool
	else:
		return FAILED
	if json_data.has("layer_index_counter"):
		layer_index_counter = json_data["layer_index_counter"] as int
	else:
		return FAILED
	if json_data.has("active_layer_uid"):
		active_layer_uid = str_to_var(json_data["active_layer_uid"]) as Vector3i
	else:
		return FAILED
	if json_data.has("use_sample"):
		use_sample = json_data["use_sample"] as bool
	else:
		return FAILED
	if json_data.has("_sample_offset"):
		_sample_offset = str_to_var(json_data["_sample_offset"]) as Vector2i
	else:
		return FAILED
	if json_data.has("_cache_canvas_size"):
		_cache_canvas_size = str_to_var(json_data["_cache_canvas_size"]) as Vector2i
	else:
		return FAILED
	if json_data.has("_cache_pool_frames"):
		_cache_pool_frames.clear()
		var pool_frames_data : Array = json_data["_cache_pool_frames"] as Array
		for data in pool_frames_data:
			var frame := Frame.new()
			var frame_err := frame.from_data(data)
			if frame_err != OK:
				return FAILED
			_cache_pool_frames.push_back(frame)
	else:
		return FAILED
	if json_data.has("animation_timeline"):
		animation_timeline.clear()
		var arr_animation_timeline : Array = json_data["animation_timeline"] as Array
		for data in arr_animation_timeline:
			var cell := AnimationCell.new()
			var cell_err := cell.from_data(data, self)
			if cell_err != OK:
				return FAILED
			animation_timeline.push_back(cell)
	else:
		return FAILED
	if json_data.has("_cache_canvas_pool_frame_uid"):
		_cache_canvas_pool_frame_uid = str_to_var(json_data["_cache_canvas_pool_frame_uid"]) as Vector3i
	else:
		return FAILED
	if json_data.has("show_grid"):
		show_grid = json_data["show_grid"] as bool
	else:
		return FAILED
	if json_data.has("show_tile"):
		show_tile = json_data["show_tile"] as bool
	else:
		return FAILED
	if json_data.has("show_preview"):
		show_preview = json_data["show_preview"] as bool
	else:
		return FAILED
	if json_data.has("show_symetric_vertical"):
		show_symetric_vertical = json_data["show_symetric_vertical"] as bool
	else:
		return FAILED
	if json_data.has("show_symetric_horizontal"):
		show_symetric_horizontal = json_data["show_symetric_horizontal"] as bool
	else:
		return FAILED
	if json_data.has("symetric_guid"):
		symetric_guid = str_to_var(json_data["symetric_guid"]) as Vector2
	else:
		return FAILED
	if json_data.has("background_color"):
		background_color = str_to_var(json_data["background_color"]) as BackgroundColor
	else:
		return FAILED
	if json_data.has("uid_counter"):
		uid_counter = json_data["uid_counter"] as int
	else:
		return FAILED
	return OK


func sync_gui_palette(ok_save : bool = false):
	if palette.is_gui_valid():
		var sync : bool = palette.is_gui_order_equal_to_color_index_order()
		if not sync:
			var new_palette : PackedColorArray = [Color.TRANSPARENT]
			for idx in palette.grid_color_index:
				new_palette.push_back(palette.color_index[idx])
			for frame in pool_frames:
				for layer in frame.layers:
					PixelPenCPP.swap_palette(palette.color_index, new_palette, layer.colormap)
			if use_sample:
				for frame in _cache_pool_frames:
					for layer in frame.layers:
						PixelPenCPP.swap_palette(palette.color_index, new_palette, layer.colormap)
			palette.color_index = new_palette
			palette.grid_sync_to_palette()
			if ok_save:
				ResourceSaver.save(self, file_path)
			else:
				is_saved = false
	else:
		palette.grid_sync_to_palette()


func set_mode(mode : ProjectMode, mask : Image = null):
	if mode == ProjectMode.BASE and use_sample:
		var frame := get_pool_frame(_cache_canvas_pool_frame_uid, true)
		for layer_i in range(frame.layers.size()):
			var cache : IndexedColorImage = frame.layers[layer_i]
			var sample : IndexedColorImage = find_index_image(cache.layer_uid)
			if sample != null:
				cache.colormap.blit_rect(sample.colormap, Rect2i(Vector2i.ZERO, sample.colormap.get_size()), _sample_offset)
		use_sample = false

		# NOTE: break compat after typo `_cache_canvs_size`
		#canvas_size = _cache_canvs_size 
		canvas_size = _cache_pool_frames[0].layers[0].size

		pool_frames = _cache_pool_frames
		canvas_pool_frame_uid = _cache_canvas_pool_frame_uid
		undo_redo = _cache_undo_redo
	elif mode == ProjectMode.SAMPLE and mask != null and not use_sample:
		use_sample = true
		_cache_canvas_size = canvas_size
		_cache_pool_frames = pool_frames#get_frames_duplicate(false)
		_cache_canvas_pool_frame_uid = canvas_pool_frame_uid
		_cache_undo_redo = undo_redo
		var region : Rect2i = PixelPenCPP.get_mask_used_rect(mask)
		canvas_size = region.size
		_sample_offset = region.position
		var new_pool_frames : Array[Frame] = [active_frame.get_duplicate(false)]
		for layer_i in range(new_pool_frames[0].layers.size()):
			var new_layer : IndexedColorImage = new_pool_frames[0].layers[layer_i].get_duplicate(false)
			new_layer.size = canvas_size
			new_layer.colormap = new_pool_frames[0].layers[layer_i].colormap.get_region(region)
			new_pool_frames[0].layers[layer_i] = new_layer
		pool_frames = new_pool_frames
		undo_redo = UndoRedoManager.new()
	PixelPen.state.edit_mode_changed.emit(mode)


func resize_canvas(new_size : Vector2i, anchor : PixelPenEnum.ResizeAnchor):
	canvas_size = new_size
	for frame in pool_frames:
		for layer in frame.layers:
			layer.resize(new_size, anchor)
	cache_copied_colormap = null
	symetric_guid = canvas_size * 0.5
	show_symetric_vertical = false
	show_symetric_horizontal = false


func break_history():
	undo_redo.break_history()
	if not (undo_redo.history_breaked as Signal).is_connected(clean_invisible_color):
		undo_redo.history_breaked.connect(clean_invisible_color)


func unbreak_history():
	undo_redo.unbreak_history()


func undo():
	undo_redo.undo()


func redo():
	undo_redo.redo()


func create_undo_layer_and_palette(name : String, callable : Callable):
	undo_redo.create_action(name)
	undo_redo.add_undo_property(active_frame, "layers", active_frame.get_layer_duplicate(false))
	undo_redo.add_undo_property(palette, "color_index", palette.color_index.duplicate())
	undo_redo.add_undo_method(callable)


func create_redo_layer_and_palette(callable : Callable):
	undo_redo.add_do_property(active_frame, "layers", active_frame.get_layer_duplicate(false))
	undo_redo.add_do_property(palette, "color_index", palette.color_index.duplicate())
	undo_redo.add_do_method(callable)
	undo_redo.commit_action()


func create_undo_layers(name : String, callable : Callable):
	undo_redo.create_action(name)
	undo_redo.add_undo_property(active_frame, "layers", active_frame.get_layer_duplicate(false))
	undo_redo.add_undo_method(callable)


func create_redo_layers(callable : Callable):
	undo_redo.add_do_property(active_frame, "layers", active_frame.get_layer_duplicate(false))
	undo_redo.add_do_method(callable)
	undo_redo.commit_action()


func create_undo_layer(name : String, layer_uid : Vector3i , callable : Callable = Callable(), create_action : bool = true):
	var image : IndexedColorImage = find_index_image(layer_uid)
	assert(image != null, "Error: IndexedColorImage != null -> fail")
	if create_action:
		undo_redo.create_action(name)
	undo_redo.add_undo_property(image, "colormap", image.colormap.duplicate())
	undo_redo.add_undo_method(callable)


func create_redo_layer(layer_uid : Vector3i, callable : Callable = Callable(), commit_action : bool = true):
	var image : IndexedColorImage = find_index_image(layer_uid)
	assert(image != null, "Error: IndexedColorImage != null -> fail")
	undo_redo.add_do_property(image, "colormap", image.colormap.duplicate())
	undo_redo.add_do_method(callable)
	if commit_action:
		undo_redo.commit_action()


func create_undo_palette(name : String, callable : Callable):
	undo_redo.create_action(name)
	undo_redo.add_undo_property(palette, "color_index", palette.color_index.duplicate())
	undo_redo.add_undo_method(callable)


func create_redo_palette(callable : Callable):
	undo_redo.add_do_property(palette, "color_index", palette.color_index.duplicate())
	undo_redo.add_do_method(callable)
	undo_redo.commit_action()


func create_undo_palette_gui(name : String, callable : Callable):
	undo_redo.create_action(name)
	undo_redo.add_undo_property(palette, "grid_color_index", palette.grid_color_index.duplicate())
	undo_redo.add_undo_method(callable)


func create_redo_palette_gui(callable : Callable):
	undo_redo.add_do_property(palette, "grid_color_index", palette.grid_color_index.duplicate())
	undo_redo.add_do_method(callable)
	undo_redo.commit_action()


func create_undo_palette_all(name : String, callable : Callable):
	undo_redo.create_action(name)
	undo_redo.add_undo_property(palette, "color_index", palette.color_index.duplicate())
	undo_redo.add_undo_property(palette, "grid_color_index", palette.grid_color_index.duplicate())
	undo_redo.add_undo_method(callable)


func create_redo_palette_all(callable : Callable):
	undo_redo.add_do_property(palette, "color_index", palette.color_index.duplicate())
	undo_redo.add_do_property(palette, "grid_color_index", palette.grid_color_index.duplicate())
	undo_redo.add_do_method(callable)
	undo_redo.commit_action()


func create_undo_property(name : String, object : Object, property : String, value : Variant, callable : Callable, create : bool = false):
	if create: 
		undo_redo.create_action(name)
	undo_redo.add_undo_property(object, property, value)
	undo_redo.add_undo_method(callable)


func create_redo_property(object : Object, property : String, value : Variant, callable : Callable, commit : bool = false):
	undo_redo.add_do_property(object, property, value)
	undo_redo.add_do_method(callable)
	if commit:
		undo_redo.commit_action()

## TODO: prevent linked layer to get duplicated twice
func get_frames_duplicate(new_uid : bool = true) -> Array[Frame]:
	var new_frames : Array[Frame] = pool_frames.duplicate()
	for i in range(new_frames.size()):
		new_frames[i] = new_frames[i].get_duplicate(new_uid)
	return new_frames


func active_layer_is_valid():
	return get_image_index(active_layer_uid) != -1


func add_layer(label : String = "", above_layer_uid : Vector3i = Vector3i.ZERO) -> Vector3i:
	var new_index_image = IndexedColorImage.new()
	if label == "":
		layer_index_counter += 1
		new_index_image.label = str("Layer " , layer_index_counter)
	else:
		new_index_image.label = label
	new_index_image.layer_uid = get_uid()
	new_index_image.size = canvas_size
	var index = get_image_index(above_layer_uid)
	if index == -1 or above_layer_uid == Vector3i.ZERO:
		active_frame.layers.insert(active_frame.layers.size(), new_index_image)
	else:
		active_frame.layers.insert(index + 1, new_index_image)
	return new_index_image.layer_uid


func duplicate_layer(layer_uid : Vector3i):
	var new_index_image : IndexedColorImage = get_index_image(layer_uid)
	if new_index_image != null:
		new_index_image = new_index_image.get_duplicate()
	else:
		return
	layer_index_counter += 1
	new_index_image.label = str(new_index_image.label , " Duplicate", layer_index_counter)
	var index = get_image_index(layer_uid)
	if index == -1:
		active_frame.layers.insert(active_frame.layers.size(), new_index_image)
	else:
		active_frame.layers.insert(index + 1, new_index_image)


func paste_copied_layer(above_layer_uid : Vector3i = Vector3i.ZERO):
	var new_index_image : IndexedColorImage = cache_copied_colormap
	if new_index_image != null:
		new_index_image = new_index_image.get_duplicate()
	else:
		return
	layer_index_counter += 1
	new_index_image.label = str(new_index_image.label , " Copy", layer_index_counter)
	var index = get_image_index(above_layer_uid)
	if index == -1:
		active_frame.layers.insert(active_frame.layers.size(), new_index_image)
	else:
		active_frame.layers.insert(index + 1, new_index_image)
	active_layer_uid = new_index_image.layer_uid


func delete_layer(layer_uid : Vector3i):
	var index = get_image_index(layer_uid)
	if index != -1:
		active_frame.layers.remove_at(index)
	if active_frame.layers.size() == 0:
		layer_index_counter = 0


func sort_palette() -> PackedInt32Array:
	var new_palette : PackedColorArray = palette.get_sorted_palette()
	var new_grid_palette : PackedInt32Array = []
	for i in range(1, new_palette.size()):
		var exist_index : int = palette.color_index.find(new_palette[i], 1)
		while exist_index != -1 and new_grid_palette.has(exist_index):
			if exist_index + 1 >= palette.color_index.size():
				break
			exist_index = palette.color_index.find(new_palette[i], exist_index + 1)
		if new_grid_palette.has(exist_index):
			continue
		elif exist_index != -1:
			new_grid_palette.push_back(exist_index)
	if new_grid_palette.size() == palette.color_index.size() - 1:
		return new_grid_palette
	return []


func delete_unused_color_palette():
	var new_palette : PackedColorArray = []
	
	for frame in pool_frames:
		for layer in frame.layers:
			for x in range(layer.colormap.get_width()):
				for y in range(layer.colormap.get_height()):
					var idx : int = layer.colormap.get_pixel(x, y).r8
					var color : Color = palette.color_index[idx]
					if color not in new_palette:
						new_palette.push_back(color)
	
	var i = 0
	while i < palette.color_index.size() and i < palette.INDEX_COLOR_SIZE:
		if palette.color_index[i] not in new_palette and palette.color_index[i] != Color.TRANSPARENT:
			palette.color_index[i] = Color.TRANSPARENT
		i += 1


func delete_color(palette_index : int):
	var gui_index := palette.palette_to_gui_index(palette_index)
	if gui_index != -1:
		palette.color_index[palette_index] = Color.TRANSPARENT
		palette.grid_color_index.remove_at(gui_index)
		palette.grid_color_index.push_back(palette_index)


func get_image_index(layer_uid : Vector3i) -> int:
	var active_layer_index : int  = -1
	for i in range(active_frame.layers.size()):
		if active_frame.layers[i].layer_uid == layer_uid:
			active_layer_index = i
			break
	return active_layer_index


func get_index_image(layer_uid : Vector3i) -> IndexedColorImage:
	var active_layer_index : int  = -1
	for i in range(active_frame.layers.size()):
		if active_frame.layers[i].layer_uid == layer_uid:
			active_layer_index = i
			break
	if active_layer_index == -1:
		return null
	return active_frame.layers[active_layer_index]


func find_index_image(layer_uid : Vector3i, cache : bool = false) -> IndexedColorImage:
	var frames : Array[Frame] 
	if cache:
		frames = _cache_pool_frames
	else:
		frames = pool_frames
	for frame in frames:
		for layer in frame.layers:
			if layer.layer_uid == layer_uid:
				return layer
	return null


func get_layer_image(layer_uid : Vector3i) -> Image:
	var image = Image.create(canvas_size.x, canvas_size.y, false, Image.FORMAT_RGBAF)
	var color_map : Image = get_index_image(layer_uid).colormap
	for y in range(canvas_size.y):
		for x in range(canvas_size.x):
			image.set_pixel(x, y, palette.color_index[color_map.get_pixel(x, y).r8])
	
	return image


func get_image(frame : Frame = null) -> Image:
	var src_frame : Frame = active_frame if frame == null else frame
	var canvas_image = Image.create(canvas_size.x, canvas_size.y, false, Image.FORMAT_RGBAF)
	for index in range(src_frame.layers.size()):
		if not src_frame.layers[index].visible:
			continue
		var image = PixelPenCPP.get_image(palette.color_index, src_frame.layers[index].colormap, false)
		var rect  = image.get_used_rect()
		canvas_image.blend_rect(image, rect, rect.position)
	if frame == null:
		cache_thumbnail = canvas_image
	return canvas_image


func get_region_project_image(mask : Image = null) -> Image:
	var canvas_image = Image.create(canvas_size.x, canvas_size.y, false, Image.FORMAT_RGBAF)
	for index in range(active_frame.layers.size()):
		if not active_frame.layers[index].visible:
			continue
		var image = PixelPenCPP.get_image(palette.color_index, active_frame.layers[index].colormap, false)
		var rect  = image.get_used_rect()
		canvas_image.blend_rect(image, rect, rect.position)
	
	if mask == null:
		return canvas_image
	else:
		var rect : Rect2i = PixelPenCPP.get_mask_used_rect(mask)
		return canvas_image.get_region(rect)


func get_region_project_colormap(mask : Image = null) -> Image:
	var canvas_image = Image.create(canvas_size.x, canvas_size.y, false, Image.FORMAT_R8)
	for index in range(active_frame.layers.size()):
		if not active_frame.layers[index].visible:
			continue
		if mask == null:
			PixelPenCPP.fill_color(active_frame.layers[index].colormap, canvas_image, Color8(255, 0, 0), null)
		else:
			PixelPenCPP.fill_color(active_frame.layers[index].colormap, canvas_image, Color8(255, 0, 0), mask)
	if mask == null:
		return canvas_image
	else:
		var rect : Rect2i = PixelPenCPP.get_mask_used_rect(canvas_image)
		return canvas_image.get_region(rect)


func import_file(path : String) -> Vector3i:
	var image : Image = Image.load_from_file(path)
	image.convert(Image.FORMAT_RGBA8)
	var image_size : Vector2i = image.get_size()
	var layer_uid = add_layer(path.get_file().get_basename(), active_layer_uid)
	var index_image : IndexedColorImage = get_index_image(layer_uid)
	palette.color_index = PixelPenCPP.import_image(index_image.colormap, image, palette.color_index)
	return layer_uid


func import_image(image : Image, path : String) -> Vector3i:
	image.convert(Image.FORMAT_RGBA8)
	var image_size : Vector2i = image.get_size()
	var layer_uid : Vector3i = add_layer(path.get_file().get_basename(), active_layer_uid)
	var index_image : IndexedColorImage = get_index_image(layer_uid)
	palette.color_index = PixelPenCPP.import_image(index_image.colormap, image, palette.color_index)
	return layer_uid


func export_png_image(path : String) -> Error:
	var image : Image = get_image()
	image.convert(Image.FORMAT_RGBA8)
	last_export_file_path = path
	return image.save_png(path)


func export_jpg_image(path : String) -> Error:
	var image : Image = get_image()
	last_export_file_path = path
	return image.save_jpg(path, 1.0)


func export_webp_image(path : String) -> Error:
	var image : Image = get_image()
	image.convert(Image.FORMAT_RGBA8)
	last_export_file_path = path
	return image.save_webp(path)


func export_animation_gif(path : String):
	# initialize exporter object with width and height of gif canvas
	var exporter = PixelPen.state.GIFExporter.new(canvas_size.x, canvas_size.y)
	# write image using median cut quantization method and with one second animation delay
	for cell in animation_timeline:
		var img : Image = get_image(cell.frame)
		img.convert(Image.FORMAT_RGBA8)
		exporter.add_frame(img, 1.0 / animation_fps, PixelPen.state.MedianCutQuantization)

	# when you have exported all frames of animation you, then you can save data into file
	# open new file with write privlige
	var file: FileAccess = FileAccess.open(path, FileAccess.WRITE)
	if file.get_open_error() == OK:
		# save data stream into file
		file.store_buffer(exporter.export_file_data())
		# close the file
		file.close()


func export_animation_frame(folder_path : String):
	for cell_i in range(animation_timeline.size()):
		var cell : AnimationCell = animation_timeline[cell_i]
		var img : Image = get_image(cell.frame)
		img.convert(Image.FORMAT_RGBA8)
		
		var file_path : String = str(folder_path, "/" , project_name ,"." , cell_i + 1, ".png")
		img.save_png(file_path)


func get_pool_index(frame_uid : Vector3i) ->int:
	for i in range(pool_frames.size()):
		if pool_frames[i].frame_uid == frame_uid:
			return i
	return -1


func get_pool_frame(frame_uid : Vector3i, cache : bool = false) -> Frame:
	var frames : Array[Frame]
	if cache:
		frames = _cache_pool_frames
	else:
		frames = pool_frames
	for frame in frames:
		if frame.frame_uid == frame_uid:
			return frame
	return null


func get_animation_draft_pool_index() -> Array[int]:
	var used : Array[int] = []
	for cell in animation_timeline:
		used.push_back(get_pool_index(cell.frame.frame_uid))
	var unused : Array[int] = []
	for frame_i in range(pool_frames.size()):
		if not used.has(frame_i):
			unused.push_back(frame_i)
	return unused


func update_onion_skin_images():
	if animation_frame_index == -1:
		animation_prev_skinning_image.clear()
		animation_next_skinning_image.clear()
		return

	if skinning_frame_index == animation_frame_index: # Do nothing active frame not change
		return

	animation_prev_skinning_image.clear()
	animation_next_skinning_image.clear()

	skinning_frame_index = animation_frame_index
	var last_uid : Vector3i = animation_timeline[animation_frame_index].frame.frame_uid # to ignore linked frame
	var prev_idx : int = animation_frame_index
	for i in PixelPen.state.userconfig.onion_skin_total:
		prev_idx -= 1
		while prev_idx > 0 and animation_timeline[prev_idx].frame.frame_uid == last_uid:
			prev_idx -= 1
		if prev_idx >= 0 and animation_timeline[prev_idx].frame.frame_uid != animation_timeline[animation_frame_index].frame.frame_uid:
			animation_prev_skinning_image.push_back(get_image(animation_timeline[prev_idx].frame))
			last_uid = animation_timeline[prev_idx].frame.frame_uid

	last_uid = animation_timeline[animation_frame_index].frame.frame_uid
	var next_idx : int = animation_frame_index
	var timeline_total = animation_timeline.size()
	for i in PixelPen.state.userconfig.onion_skin_total:
		next_idx += 1
		while next_idx < timeline_total and animation_timeline[next_idx].frame.frame_uid == last_uid:
			next_idx += 1
		if next_idx < timeline_total and animation_timeline[next_idx].frame.frame_uid != animation_timeline[animation_frame_index].frame.frame_uid:
			animation_next_skinning_image.push_back(get_image(animation_timeline[next_idx].frame))
			last_uid = animation_timeline[next_idx].frame.frame_uid


func resolve_missing_visible_frame() -> bool:
	var active_index : int = -1
	if active_frame != null:
		active_index = get_pool_index(active_frame.frame_uid)
	if active_index != -1:
		var frame := pool_frames[active_index]
		for cell_i in range(animation_timeline.size()):
			if frame.frame_uid == animation_timeline[cell_i].frame.frame_uid:
				animation_frame_index = cell_i
				canvas_pool_frame_uid = frame.frame_uid
				return true
		animation_frame_index = -1
		canvas_pool_frame_uid = frame.frame_uid
		return true
	elif animation_frame_index == -1:
		var unused : Array[int] = get_animation_draft_pool_index()
		if unused.size() > 0:
			canvas_pool_frame_uid = pool_frames[unused[0]].frame_uid
			return true
	
	if animation_timeline.size() > 0:
		animation_frame_index = 0
		canvas_pool_frame_uid = animation_timeline[0].frame.frame_uid
		return true
	elif pool_frames.size() > 0:
		animation_frame_index = -1
		canvas_pool_frame_uid = pool_frames[0].frame_uid
		return true
	else:
		pool_frames = [Frame.create(get_uid())]
		canvas_pool_frame_uid = pool_frames[0].frame_uid
		add_layer()
		active_layer_uid = active_frame.layers[0].layer_uid
	return true


func clean_invisible_color():
	for frame in pool_frames:
		for layer in frame.layers:
			PixelPenCPP.clean_invisible_color(layer.colormap, palette.color_index)
	if use_sample:
		for frame in _cache_pool_frames:
			for layer in frame.layers:
				PixelPenCPP.clean_invisible_color(layer.colormap, palette.color_index)
	for i in palette.color_index.size():
		if palette.color_index[i].a == 0:
			palette.color_index[i] = Color.TRANSPARENT


func reset_brush_to_default():
	var Tool := load("res://addons/net.yarvis.pixel_pen/editor/editor_canvas/tool.gd")
	PixelPen.state.userconfig.brush.clear()
	for i in range(0, 16):
		var start : Vector2 = Vector2(0.5, 0.5)
		var end : Vector2 = Vector2(i, i) + Vector2(0.5, 0.5)
		var rect : Rect2 = Rect2(start, end - start)
		var image : Image = Image.create(rect.size.x + 1, rect.size.y + 1, false, Image.FORMAT_R8)
		var center_mass : Vector2 = Tool.get_midpoint_ellipse(start, end, Color8(255, 0, 0), image)
		if center_mass != Vector2(-1, -1):
			var image_f = PixelPenCPP.get_image_flood(center_mass, image, Vector2i.ZERO, true)
			if image_f != null:
				PixelPenCPP.fill_color(image_f, image, Color8(255, 0, 0), null)
		image = image.get_region(PixelPenCPP.get_mask_used_rect(image))
		PixelPen.state.userconfig.brush.push_back(image)
		PixelPen.state.userconfig.save()


func reset_stamp_to_default():
	PixelPen.state.userconfig.stamp.clear()
	PixelPen.state.userconfig.save()


func crop_canvas(mask : Image) -> void:
	if use_sample:
		return
	var rect : Rect2i = PixelPenCPP.get_mask_used_rect(mask)
	for frame in pool_frames:
		for layer in frame.layers:
			var colormap : Image = layer.colormap.duplicate()
			layer.size = rect.size
			layer.colormap = colormap.get_region(rect)
	_cache_pool_frames = []
	undo_redo.clear_history()
	canvas_size = rect.size
