tool
extends Control
class_name Taquin

var Piece = preload("res://src/Piece.tscn")

export var rows: int = 4
export var columns: int = 4
export var width: int = 512
export var height: int = 512
export var difficulty: int = 10

enum Direction { UP, DOWN, LEFT, RIGHT }

var interpiece := 4
var min_padding := 15
var padding := Vector2(min_padding, min_padding)

var pieces: Array = []
var rng := RandomNumberGenerator.new()

var current_animation_path := "AnimationPlayer/MockPiece:position"

var swipe := Vector2(0, 0)
var is_sliding := false
var minimum_slide_length := 5.0

var current_sliding_piece: Piece = null
var missing_piece: Piece = null

var current_origin := Vector2.ZERO
var current_goal := Vector2.ZERO
var current_axis := Vector2.ZERO
var current_touch_slide := Vector2.ZERO
var local_min_position := Vector2.ZERO
var local_max_position := Vector2.ZERO

func position_for_index(index: Vector2, size: int) -> Vector2:
	return padding + Vector2(index.x * (size + interpiece), index.y * (size + interpiece))

func compute_piece_size() -> int:
	var w_size: int = (width - (2 * min_padding) - ((columns - 1) * interpiece)) / columns
	var h_size: int = (height - (2 * min_padding) - ((rows - 1) * interpiece)) / rows
	return int(min(w_size, h_size))

func compute_padding(piece_size: int) -> Vector2:
	var p = Vector2(0, 0)
	p.x = width - columns * piece_size - (columns - 1) * interpiece
	p.y = height - rows * piece_size - (rows - 1) * interpiece
	p = p / Vector2(2, 2)
	return p

func _ready() -> void:
	$AnimationPlayer/MockPiece.visible = false
	$Particles2D.emitting = false

	$Background.rect_size.x = width
	$Background.rect_size.y = height
	
	rng.randomize()
	var piece_size: int = compute_piece_size()
	padding = compute_padding(piece_size)
	print("piece size: ", piece_size)
	print("padding: ", padding)

	for c in range(columns):
		var pieces_row: Array = []
		for r in range(rows):
			var piece = Piece.instance()

			piece.size = piece_size
			piece.position = position_for_index(Vector2(c, r), piece.size)
			piece.order = 1 + c + r * columns
			piece.taquin_index = Vector2(c, r)
			piece.piece_scale = Vector2((float(piece_size) / width), (float(piece_size) / height))
			piece.taquin_position = Vector2(float(piece.position.x) / width, float(piece.position.y) / height)

			if r == rows - 1 && c == columns - 1:
				piece.visible = false
				missing_piece = piece

			$Background.add_child(piece)
			pieces_row.append(piece)

		pieces.append(pieces_row)

	shuffle(difficulty)

func _process(delta):
	var game_state = get_node_or_null("/root/Main/GameState") as GameState
	if game_state != null:
		match game_state.current_state:
			# If we are in the winning animation, fast-forward to game over screen
			GameState.State.WINNING:
				game_state.transition_to(GameState.State.GAME_OVER)
				return
			GameState.State.GAME_OVER:
				return

func _input(event):
	if $AnimationPlayer.is_playing():
		# Disable input during animation
		return

	#
	# Handle keyboard input
	#
	if event.is_action_pressed("ui_up"):
		move_piece(Direction.UP)
	if event.is_action_pressed("ui_down"):
		move_piece(Direction.DOWN)
	if event.is_action_pressed("ui_left"):
		move_piece(Direction.LEFT)
	if event.is_action_pressed("ui_right"):
		move_piece(Direction.RIGHT)

	#
	# Handle touch input
	#
	if event is InputEventScreenDrag:
		swipe = event.relative
		# We check that the slide to have a minimum length
		# before detecting the direction to avoid jittering.
		if swipe.length() > minimum_slide_length and not is_sliding:
			print("sliding started")
			is_sliding = true
			current_touch_slide = Vector2.ZERO
			var angle = swipe.angle()
			var direction = direction_for_angle(angle)
			debug_print_direction(direction)
			current_sliding_piece = sliding_piece_for_direction(direction)
			if current_sliding_piece != null:
				current_axis = axis_for_direction(direction)
				var local_end_position = current_axis * (current_sliding_piece.size + interpiece)
				local_min_position = Vector2(min(0, local_end_position.x), min(0, local_end_position.y))
				local_max_position = Vector2(max(0, local_end_position.x), max(0, local_end_position.y))
				current_goal = current_sliding_piece.position + local_end_position
		current_touch_slide += swipe
		if current_sliding_piece != null:
			var delta = current_touch_slide.project(current_axis)
			delta.x = clamp(delta.x, local_min_position.x, local_max_position.x)
			delta.y = clamp(delta.y, local_min_position.y, local_max_position.y)
			current_sliding_piece.position = current_origin + delta

	if event is InputEventScreenTouch:
		if not event.pressed: # Touch released
			is_sliding = false
			if current_sliding_piece != null:
				var current_position = current_sliding_piece.position
				if current_position.distance_to(current_origin) > current_position.distance_to(current_goal):
					current_sliding_piece.position = current_goal
					commit_slide()
				else:
					reset_position(current_sliding_piece)
					reset_slide()

func debug_print_direction(direction: int):
	match direction:
		Direction.UP:
			print("Direction ⬆️ UP")
		Direction.DOWN:
			print("Direction ⬇️ DOWN")
		Direction.LEFT:
			print("Direction ⬅️ LEFT")
		Direction.RIGHT:
			print("Direction ➡️ RIGHT")
		_:
			assert(false)

func axis_for_direction(direction: int) -> Vector2:
	match direction:
		Direction.UP:
			return Vector2.UP
		Direction.DOWN:
			return Vector2.DOWN
		Direction.LEFT:
			return Vector2.LEFT
		Direction.RIGHT:
			return Vector2.RIGHT
		_:
			assert(false)
			return Vector2.ZERO

func direction_for_angle(angle: float) -> int:
	if angle < PI / 4 and angle >= - PI / 4:
		return Direction.RIGHT
	if angle >= PI / 4 and angle < PI - PI / 4:
		return Direction.DOWN
	if angle >= - PI + PI / 4 and angle < - PI / 4:
		return Direction.UP
	if angle >= PI - PI / 4 or angle < -PI + PI / 4:
		return Direction.LEFT
	assert(false)
	return Direction.DOWN
	
func sliding_piece_for_direction(direction) -> Piece:
	var destination: Vector2 = missing_piece.taquin_index
	match direction:
		Direction.UP:
			destination.y += 1
		Direction.DOWN:
			destination.y -= 1
		Direction.LEFT:
			destination.x += 1
		Direction.RIGHT:
			destination.x -= 1
	
	if (destination.x < 0 || destination.x >= columns
	|| destination.y < 0 || destination.y >= rows):
		print("\/!\\ Impossible move")
		return null

	var piece = pieces[destination.x][destination.y]
	current_origin = piece.position
	
	return piece

func move_piece(direction) -> void:
	current_sliding_piece = sliding_piece_for_direction(direction)
	if current_sliding_piece == null:
		reset_slide()
		return

	var moving_piece_animation: Animation = $AnimationPlayer.get_animation("MovingPiece")

	assert(moving_piece_animation != null)
	assert(moving_piece_animation.get_track_count() > 0)

	var moving_piece_track_index: int = moving_piece_animation.find_track(current_animation_path)

	assert(moving_piece_track_index != -1)

	var new_animation_path: String = str($AnimationPlayer.get_parent().get_path_to(current_sliding_piece), ":position")
	moving_piece_animation.track_set_path(moving_piece_track_index, new_animation_path)
	current_animation_path = new_animation_path
	
	moving_piece_animation.track_set_key_value(moving_piece_track_index, 0, current_sliding_piece.position)
	moving_piece_animation.track_set_key_value(moving_piece_track_index, 1, missing_piece.position)
	$AnimationPlayer.play("MovingPiece")

	commit_slide()
	
	update()
	check_solved()

func commit_slide():
	assert(current_sliding_piece != null)
	assert(current_origin != Vector2.ZERO)
	
	var x_delta = missing_piece.taquin_index.x - current_sliding_piece.taquin_index.x
	var y_delta = missing_piece.taquin_index.y - current_sliding_piece.taquin_index.y
	assert(abs(x_delta) + abs(y_delta) == 1)
	
	swap_pieces(missing_piece, current_sliding_piece)
	reset_position(missing_piece)

	ensure_validity()
	reset_slide()
	check_solved()

func reset_slide():
	current_sliding_piece = null
	current_origin = Vector2.ZERO

func reset_position(p: Piece):
	p.position = position_for_index(p.taquin_index, p.size)

func swap_pieces(a: Piece, b: Piece) -> void:
	var a_index := a.taquin_index
	a.taquin_index = b.taquin_index
	pieces[b.taquin_index.x][b.taquin_index.y] = a
	b.taquin_index = a_index
	pieces[a_index.x][a_index.y] = b

func shuffle(count: int) -> void:
	while count > 0:
		count -= 1
		var direction = rng.randi_range(Direction.UP, Direction.RIGHT)
		move_piece(direction)

func check_solved() -> bool:
	for c in range(columns):
		for r in range(rows):
			if pieces[c][r].order != 1 + c + r * columns:
				return false

	var game_state = get_node_or_null("/root/Main/GameState") as GameState
	if game_state != null:
		game_state.transition_to(GameState.State.WINNING)

	return true

func ensure_validity() -> void:
	for c in range(columns):
		for r in range(rows):
			var piece = pieces[c][r]
			assert(piece.taquin_index.x == c)
			assert(piece.taquin_index.y == r)

func _on_GameState_state_changed(previous, current):
	match current:
		GameState.State.WINNING:
			$Particles2D.emitting = true
			$Timer.start(-1)
		GameState.State.GAME_OVER:
			$Particles2D.emitting = false
			$Timer.stop()

func _on_Timer_timeout():
	var game_state = get_node("/root/Main/GameState") as GameState
	if game_state != null:
		game_state.transition_to(GameState.State.GAME_OVER)
