class_name Canvas
extends Node2D

const CURSOR_SPEED_RATE := 6.0

var current_pixel := Vector2.ZERO
var sprite_changed_this_frame := false  ## For optimization purposes
var update_all_layers := false
var project_changed := false
var move_preview_location := Vector2i.ZERO
var layer_texture_array := Texture2DArray.new()
var layer_metadata_image := Image.new()
var layer_metadata_texture := ImageTexture.new()

@onready var currently_visible_frame := $CurrentlyVisibleFrame as SubViewport
@onready var current_frame_drawer := $CurrentlyVisibleFrame/CurrentFrameDrawer as Node2D
@onready var tile_mode := $TileMode as Node2D
@onready var pixel_grid := $PixelGrid as Node2D
@onready var grid := $Grid as Node2D
@onready var selection := $Selection as SelectionNode
@onready var onion_past := $OnionPast as Node2D
@onready var onion_future := $OnionFuture as Node2D
@onready var crop_rect := $CropRect as CropRect
@onready var indicators := $Indicators as Node2D
@onready var previews := $Previews as Node2D
@onready var previews_sprite := $PreviewsSprite as Sprite2D
@onready var mouse_guide_container := $MouseGuideContainer as Node2D
@onready var gizmos_3d := $Gizmos3D as Node2D
@onready var measurements := $Measurements as Node2D
@onready var reference_image_container := $ReferenceImages as Node2D


func _ready() -> void:
	material.set_shader_parameter("layers", layer_texture_array)
	material.set_shader_parameter("metadata", layer_metadata_texture)
	Global.project_switched.connect(
		func():
			project_changed = true
			queue_redraw()
	)
	onion_past.type = onion_past.PAST
	onion_past.blue_red_color = Global.onion_skinning_past_color
	onion_future.type = onion_future.FUTURE
	onion_future.blue_red_color = Global.onion_skinning_future_color
	await get_tree().process_frame
	camera_zoom()


func _draw() -> void:
	var position_tmp := position
	var scale_tmp := scale
	if Global.mirror_view:
		position_tmp.x = position_tmp.x + Global.current_project.size.x
		scale_tmp.x = -1
	# If we just use the first cel and it happens to be a GroupCel
	# nothing will get drawn
	var cel_to_draw := Global.current_project.find_first_drawable_cel()
	draw_set_transform(position_tmp, rotation, scale_tmp)
	# Placeholder so we can have a material here
	if is_instance_valid(cel_to_draw):
		draw_texture(cel_to_draw.image_texture, Vector2.ZERO)
	draw_layers(project_changed)
	project_changed = false
	if Global.onion_skinning:
		refresh_onion()
	currently_visible_frame.size = Global.current_project.size
	current_frame_drawer.queue_redraw()
	tile_mode.queue_redraw()
	draw_set_transform(position, rotation, scale)


func _input(event: InputEvent) -> void:
	# Move the cursor with the keyboard (numpad keys by default)
	var mouse_movement := Input.get_vector(
		&"move_mouse_left", &"move_mouse_right", &"move_mouse_up", &"move_mouse_down"
	)
	# Don't process anything below if the input isn't a mouse event, a tool activation shortcut,
	# or the numpad keys that move the cursor.
	# This decreases CPU/GPU usage slightly.
	if not event is InputEventMouseMotion:
		if (
			mouse_movement == Vector2.ZERO
			and not (
				event.is_action(&"activate_left_tool") or event.is_action(&"activate_right_tool")
			)
		):
			return
	# Get the viewport's mouse position instead of the local mouse position to use warp_mouse
	var tmp_position := get_viewport().get_mouse_position()
	if mouse_movement != Vector2.ZERO:
		tmp_position += mouse_movement * CURSOR_SPEED_RATE
		get_viewport().warp_mouse(tmp_position)
	var tmp_transform := get_canvas_transform().affine_inverse()
	current_pixel = tmp_transform.basis_xform(tmp_position) + tmp_transform.origin

	sprite_changed_this_frame = false
	Tools.handle_draw(Vector2i(current_pixel.floor()), event)

	if sprite_changed_this_frame:
		queue_redraw()
		update_selected_cels_textures()


func camera_zoom() -> void:
	for camera: CanvasCamera in get_tree().get_nodes_in_group("CanvasCameras"):
		camera.fit_to_frame(Global.current_project.size)

	Global.transparent_checker.update_rect()


func update_texture(layer_i: int, frame_i := -1, project := Global.current_project) -> void:
	if frame_i == -1:
		frame_i = project.current_frame

	if frame_i < project.frames.size() and layer_i < project.layers.size():
		var current_cel := project.frames[frame_i].cels[layer_i]
		current_cel.update_texture()
		# Needed so that changes happening to the non-selected layer(s) are also visible
		# e.g. when undoing/redoing, when applying image effects to the entire frame, etc
		if frame_i != project.current_frame:
			# Don't update if the cel is on a different frame (can happen with undo/redo)
			return
		var layer := project.layers[layer_i].get_blender_ancestor()
		var cel_image: Image
		if layer is GroupLayer:
			cel_image = layer.blend_children(
				project.frames[project.current_frame], Vector2i.ZERO, Global.display_layer_effects
			)
		else:
			if Global.display_layer_effects:
				cel_image = layer.display_effects(current_cel)
			else:
				cel_image = current_cel.get_image()
		if (
			cel_image.get_size()
			== Vector2i(layer_texture_array.get_width(), layer_texture_array.get_height())
		):
			layer_texture_array.update_layer(cel_image, project.ordered_layers[layer.index])


func update_selected_cels_textures(project := Global.current_project) -> void:
	for cel_index in project.selected_cels:
		var frame_index: int = cel_index[0]
		var layer_index: int = cel_index[1]
		if frame_index < project.frames.size() and layer_index < project.layers.size():
			var current_cel := project.frames[frame_index].cels[layer_index]
			current_cel.update_texture()


func draw_layers(force_recreate := false) -> void:
	var project := Global.current_project
	var recreate_texture_array := (
		layer_texture_array.get_layers() != project.layers.size()
		or layer_texture_array.get_width() != project.size.x
		or layer_texture_array.get_height() != project.size.y
		or force_recreate
	)
	if recreate_texture_array:
		var textures: Array[Image] = []
		textures.resize(project.layers.size())
		# Nx4 texture, where N is the number of layers and the first row are the blend modes,
		# the second are the opacities, the third are the origins and the fourth are the
		# clipping mask booleans.
		layer_metadata_image = Image.create(project.layers.size(), 4, false, Image.FORMAT_RGF)
		# Draw current frame layers
		for i in project.layers.size():
			var layer := project.layers[i]
			var ordered_index := project.ordered_layers[layer.index]
			var cel_image := Image.new()
			_update_texture_array_layer(project, layer, cel_image, false)
			textures[ordered_index] = cel_image
			# Store the origin
			if [project.current_frame, i] in project.selected_cels:
				var origin := Vector2(move_preview_location).abs() / Vector2(cel_image.get_size())
				layer_metadata_image.set_pixel(
					ordered_index, 2, Color(origin.x, origin.y, 0.0, 0.0)
				)
			else:
				layer_metadata_image.set_pixel(ordered_index, 2, Color())

		layer_texture_array.create_from_images(textures)
		layer_metadata_texture.set_image(layer_metadata_image)
	else:  # Update the TextureArray
		if layer_texture_array.get_layers() > 0:
			for i in project.layers.size():
				if not update_all_layers:
					var test_array := [project.current_frame, i]
					if not test_array in project.selected_cels:
						continue
				var layer := project.layers[i]
				var ordered_index := project.ordered_layers[layer.index]
				var cel_image := Image.new()
				_update_texture_array_layer(project, layer, cel_image, true)
				var parent_layer := layer.get_blender_ancestor()
				if layer != parent_layer:
					# True when the layer has parents. In that case, update its top-most parent.
					_update_texture_array_layer(project, parent_layer, Image.new(), true)
				# Update the origin
				var origin := Vector2(move_preview_location).abs() / Vector2(cel_image.get_size())
				layer_metadata_image.set_pixel(
					ordered_index, 2, Color(origin.x, origin.y, 0.0, 0.0)
				)
			layer_metadata_texture.update(layer_metadata_image)

	material.set_shader_parameter("origin_x_positive", move_preview_location.x > 0)
	material.set_shader_parameter("origin_y_positive", move_preview_location.y > 0)
	update_all_layers = false


func _update_texture_array_layer(
	project: Project, layer: BaseLayer, cel_image: Image, update_layer: bool
) -> void:
	var ordered_index := project.ordered_layers[layer.index]
	var cel := project.frames[project.current_frame].cels[layer.index]
	var include := true
	if layer is GroupLayer and layer.blend_mode != BaseLayer.BlendModes.PASS_THROUGH:
		cel_image.copy_from(
			layer.blend_children(
				project.frames[project.current_frame],
				move_preview_location,
				Global.display_layer_effects
			)
		)
	else:
		if Global.display_layer_effects:
			cel_image.copy_from(layer.display_effects(cel))
		else:
			cel_image.copy_from(cel.get_image())
	if layer.is_blended_by_ancestor():
		include = false
	if update_layer:
		layer_texture_array.update_layer(cel_image, ordered_index)
	DrawingAlgos.set_layer_metadata_image(layer, cel, layer_metadata_image, ordered_index, include)


func refresh_onion() -> void:
	onion_past.queue_redraw()
	onion_future.queue_redraw()
