extends Node2D

signal score_changed(points: int)
signal grid_state_loaded(grid_size: Vector2i, tile_types: int)

## Match-3 Game State Machine
## WAITING: Idle state, accepting player input for tile selection
## SELECTING: First tile selected, waiting for second tile to complete swap
## SWAPPING: Animation in progress, input blocked during tile swap
## PROCESSING: Detecting matches, clearing tiles, dropping new ones, checking cascades
enum GameState { WAITING, SELECTING, SWAPPING, PROCESSING }

var GRID_SIZE := Vector2i(8, 8)
var TILE_TYPES := 5
const TILE_SCENE := preload("res://scenes/game/gameplays/tile.tscn")

# Safety constants
const MAX_GRID_SIZE := 15
const MAX_TILE_TYPES := 10
const MAX_CASCADE_ITERATIONS := 20
const MIN_GRID_SIZE := 3
const MIN_TILE_TYPES := 3

# Layout constants (replacing magic numbers)
const SCREEN_WIDTH_USAGE := 0.8  # Use 80% of screen width
const SCREEN_HEIGHT_USAGE := 0.7  # Use 70% of screen height
const GRID_LEFT_MARGIN := 50.0
const GRID_TOP_MARGIN := 50.0

# Timing constants
const CASCADE_WAIT_TIME := 0.1
const SWAP_ANIMATION_TIME := 0.3
const TILE_DROP_WAIT_TIME := 0.2

var grid: Array[Array] = []
var tile_size: float = 48.0
var grid_offset: Vector2 = Vector2.ZERO
var current_state: GameState = GameState.WAITING
var selected_tile: Node2D = null
var cursor_position: Vector2i = Vector2i(0, 0)
var keyboard_navigation_enabled: bool = false
var grid_initialized: bool = false
var instance_id: String = ""


func _ready() -> void:
	# Generate instance ID
	instance_id = "Match3_%d" % get_instance_id()

	if grid_initialized:
		DebugManager.log_warn(
			"[%s] Match3 _ready() called multiple times, skipping initialization" % instance_id,
			"Match3"
		)
		return

	DebugManager.log_debug("[%s] Match3 _ready() started" % instance_id, "Match3")
	grid_initialized = true

	# Calculate grid layout
	_calculate_grid_layout()

	# Try to load saved state, otherwise use default
	var loaded_saved_state = await load_saved_state()
	if not loaded_saved_state:
		DebugManager.log_info("No saved state found, using default grid initialization", "Match3")
		_initialize_grid()
	else:
		DebugManager.log_info("Successfully loaded saved grid state", "Match3")

	DebugManager.log_debug("Match3 _ready() completed, calling debug structure check", "Match3")

	# Notify UI that grid state is loaded
	grid_state_loaded.emit(GRID_SIZE, TILE_TYPES)

	# Debug: Check scene tree structure
	call_deferred("_debug_scene_structure")


func _calculate_grid_layout():
	var viewport_size = get_viewport().get_visible_rect().size
	var available_width = viewport_size.x * SCREEN_WIDTH_USAGE
	var available_height = viewport_size.y * SCREEN_HEIGHT_USAGE

	# Calculate tile size based on available space
	var max_tile_width = available_width / GRID_SIZE.x
	var max_tile_height = available_height / GRID_SIZE.y
	tile_size = min(max_tile_width, max_tile_height)

	# Align grid to left side with margins
	var total_grid_height = tile_size * GRID_SIZE.y
	grid_offset = Vector2(
		GRID_LEFT_MARGIN, (viewport_size.y - total_grid_height) / 2 + GRID_TOP_MARGIN
	)


func _initialize_grid():
	# Create gem pool for current tile types
	var gem_indices: Array[int] = []
	for i in range(TILE_TYPES):
		gem_indices.append(i)

	for y in range(GRID_SIZE.y):
		grid.append([])
		for x in range(GRID_SIZE.x):
			var tile = TILE_SCENE.instantiate()
			var tile_position = grid_offset + Vector2(x, y) * tile_size
			tile.position = tile_position
			tile.grid_position = Vector2i(x, y)
			add_child(tile)

			# Set gem types for this tile
			tile.set_active_gem_types(gem_indices)

			# Set tile type after adding to scene tree
			var new_type = randi() % TILE_TYPES
			tile.tile_type = new_type

			# Connect tile signals
			tile.tile_selected.connect(_on_tile_selected)
			DebugManager.log_debug(
				(
					"Created tile at grid(%d,%d) world_pos(%s) with type %d"
					% [x, y, tile_position, new_type]
				),
				"Match3"
			)
			grid[y].append(tile)


func _has_match_at(pos: Vector2i) -> bool:
	"""Check if tile at position is part of a 3+ match horizontally or vertically"""
	# Bounds and null checks
	if not _is_valid_grid_position(pos):
		return false

	if pos.y >= grid.size() or pos.x >= grid[pos.y].size():
		DebugManager.log_error("Grid array bounds exceeded at (%d,%d)" % [pos.x, pos.y], "Match3")
		return false

	var tile = grid[pos.y][pos.x]
	if not tile or not is_instance_valid(tile):
		return false

	# Check if tile has required properties
	if not "tile_type" in tile:
		DebugManager.log_warn(
			"Tile at (%d,%d) missing tile_type property" % [pos.x, pos.y], "Match3"
		)
		return false

	var matches_horizontal = _get_match_line(pos, Vector2i(1, 0))
	if matches_horizontal.size() >= 3:
		return true

	var matches_vertical = _get_match_line(pos, Vector2i(0, 1))
	return matches_vertical.size() >= 3


func _check_for_matches() -> bool:
	"""Scan entire grid to detect if any matches exist (used for cascade detection)"""
	for y in range(GRID_SIZE.y):
		for x in range(GRID_SIZE.x):
			if _has_match_at(Vector2i(x, y)):
				return true
	return false


func _get_match_line(start: Vector2i, dir: Vector2i) -> Array:
	"""Find all consecutive matching tiles in a line from start position in given direction.

	Returns array of tile nodes that form a continuous match.
	Direction must be unit vector (1,0), (-1,0), (0,1), or (0,-1).
	Searches bidirectionally from start position.
	"""
	# Validate input parameters
	if not _is_valid_grid_position(start):
		DebugManager.log_error(
			"Invalid start position for match line: (%d,%d)" % [start.x, start.y], "Match3"
		)
		return []

	if abs(dir.x) + abs(dir.y) != 1 or (dir.x != 0 and dir.y != 0):
		DebugManager.log_error(
			"Invalid direction vector for match line: (%d,%d)" % [dir.x, dir.y], "Match3"
		)
		return []

	# Check grid bounds and tile validity
	if start.y >= grid.size() or start.x >= grid[start.y].size():
		return []

	var start_tile = grid[start.y][start.x]
	if not start_tile or not is_instance_valid(start_tile):
		return []

	if not "tile_type" in start_tile:
		return []

	var line = [start_tile]  # Initialize with start tile
	var type = start_tile.tile_type  # Type to match against

	# Check both directions from start position (bidirectional search)
	for offset in [1, -1]:  # offset 1 = forward direction, -1 = backward direction
		var current = start + dir * offset
		var steps = 0
		# Safety limit prevents infinite loops in case of logic errors
		while steps < GRID_SIZE.x + GRID_SIZE.y and _is_valid_grid_position(current):
			if current.y >= grid.size() or current.x >= grid[current.y].size():
				break

			var neighbor = grid[current.y][current.x]
			if not neighbor or not is_instance_valid(neighbor):
				break

			# Stop if tile type doesn't match (end of matching sequence)
			if not "tile_type" in neighbor or neighbor.tile_type != type:
				break

			line.append(neighbor)  # Add matching tile to sequence
			current += dir * offset  # Move to next position
			steps += 1  # Increment safety counter

	return line


func _clear_matches() -> void:
	"""Find and remove all match groups of 3+ tiles, calculating score and triggering effects.

	Uses flood-fill approach to group connected matches, prevents double-counting tiles.
	Handles tile removal, score calculation, and visual effects.
	"""
	# Check grid integrity
	if not _validate_grid_integrity():
		DebugManager.log_error("Grid integrity check failed in _clear_matches", "Match3")
		return

	var match_groups := []
	var processed_tiles := {}

	for y in range(GRID_SIZE.y):
		if y >= grid.size():
			continue

		for x in range(GRID_SIZE.x):
			if x >= grid[y].size():
				continue

			# Safe tile access with validation
			var tile = grid[y][x]
			if not tile or not is_instance_valid(tile):
				continue
			if processed_tiles.has(tile):
				continue

			var pos = Vector2i(x, y)
			var horiz = _get_match_line(pos, Vector2i(1, 0))
			var vert = _get_match_line(pos, Vector2i(0, 1))

			if horiz.size() >= 3:
				match_groups.append(horiz)
				for match_tile in horiz:
					if is_instance_valid(match_tile):
						processed_tiles[match_tile] = true

			if vert.size() >= 3:
				match_groups.append(vert)
				for match_tile in vert:
					if is_instance_valid(match_tile):
						processed_tiles[match_tile] = true

	# Calculate total score from all matches
	# Formula: 3 gems = 3 points, 4 gems = 6 points, 5 gems = 8 points, etc.
	# Pattern: n gems = n + (n-2) points for n >= 3, but 3 gems = exactly 3 points
	var total_score := 0
	for match_group in match_groups:
		var match_size = match_group.size()
		var match_score: int
		if match_size == 3:
			match_score = 3  # Exactly 3 points for 3 gems
		else:
			match_score = match_size + max(0, match_size - 2)  # n + (n-2) for n >= 4
		total_score += match_score
		print("Debug: Match of ", match_size, " gems = ", match_score, " points")

	# Remove duplicates from all matches combined
	var to_clear := []
	var unique_dict := {}
	for match_group in match_groups:
		for tile in match_group:
			unique_dict[tile] = true
	to_clear = unique_dict.keys()

	# Fixed: Only proceed if there are matches to clear
	if to_clear.size() == 0:
		return

	# Emit score before clearing tiles
	if total_score > 0:
		print("Debug: Total score for this turn: ", total_score)
		score_changed.emit(total_score)

	# Safe tile removal with validation
	for tile in to_clear:
		if not is_instance_valid(tile):
			continue

		# Validate tile has grid_position property
		if not "grid_position" in tile:
			DebugManager.log_warn("Tile missing grid_position during removal", "Match3")
			tile.queue_free()
			continue

		var tile_pos = tile.grid_position
		# Validate grid position before clearing reference
		if (
			_is_valid_grid_position(tile_pos)
			and tile_pos.y < grid.size()
			and tile_pos.x < grid[tile_pos.y].size()
		):
			grid[tile_pos.y][tile_pos.x] = null
		else:
			DebugManager.log_warn(
				"Invalid grid position during tile removal: (%d,%d)" % [tile_pos.x, tile_pos.y],
				"Match3"
			)

		tile.queue_free()

	# Wait one frame for nodes to be queued properly
	await get_tree().process_frame
	_drop_tiles()
	await get_tree().create_timer(TILE_DROP_WAIT_TIME).timeout
	_fill_empty_cells()


func _drop_tiles():
	var moved = true
	while moved:
		moved = false
		for x in range(GRID_SIZE.x):
			# Fixed: Start from GRID_SIZE.y - 1 to avoid out of bounds
			for y in range(GRID_SIZE.y - 1, -1, -1):
				var tile = grid[y][x]
				# Fixed: Check bounds before accessing y + 1
				if tile and y + 1 < GRID_SIZE.y and not grid[y + 1][x]:
					grid[y + 1][x] = tile
					grid[y][x] = null
					tile.grid_position = Vector2i(x, y + 1)
					# You can animate position here using Tween for smooth drop:
					# tween.interpolate_property(tile, "position", tile.position, grid_offset + Vector2(x, y + 1) * tile_size, 0.2)
					tile.position = grid_offset + Vector2(x, y + 1) * tile_size
					moved = true


func _fill_empty_cells():
	# Safety check for grid integrity
	if not _validate_grid_integrity():
		DebugManager.log_error("Grid integrity check failed in _fill_empty_cells", "Match3")
		return

	# Create gem pool for current tile types
	var gem_indices: Array[int] = []
	for i in range(TILE_TYPES):
		gem_indices.append(i)

	var tiles_created = 0
	for y in range(GRID_SIZE.y):
		if y >= grid.size():
			DebugManager.log_error("Grid row %d does not exist" % y, "Match3")
			continue

		for x in range(GRID_SIZE.x):
			if x >= grid[y].size():
				DebugManager.log_error("Grid column %d does not exist in row %d" % [x, y], "Match3")
				continue

			if not grid[y][x]:
				var tile = TILE_SCENE.instantiate()
				if not tile:
					DebugManager.log_error(
						"Failed to instantiate tile at (%d,%d)" % [x, y], "Match3"
					)
					continue

				tile.grid_position = Vector2i(x, y)
				tile.position = grid_offset + Vector2(x, y) * tile_size
				add_child(tile)

				# Set gem types for this tile with error handling
				if tile.has_method("set_active_gem_types"):
					tile.set_active_gem_types(gem_indices)
				else:
					DebugManager.log_warn("Tile missing set_active_gem_types method", "Match3")

				# Set random tile type with bounds checking
				if TILE_TYPES > 0:
					tile.tile_type = randi() % TILE_TYPES
				else:
					DebugManager.log_error("TILE_TYPES is 0, cannot set tile type", "Match3")
					tile.queue_free()
					continue

				grid[y][x] = tile
				# Connect tile signals for new tiles with error handling
				if tile.has_signal("tile_selected"):
					tile.tile_selected.connect(_on_tile_selected)
				else:
					DebugManager.log_warn("Tile missing tile_selected signal", "Match3")

				tiles_created += 1

	DebugManager.log_debug("Created %d new tiles" % tiles_created, "Match3")

	# Enhanced cascade protection with better limits
	await get_tree().create_timer(CASCADE_WAIT_TIME).timeout
	var iteration = 0
	while _check_for_matches() and iteration < MAX_CASCADE_ITERATIONS:
		_clear_matches()
		await get_tree().create_timer(CASCADE_WAIT_TIME).timeout
		iteration += 1

	if iteration >= MAX_CASCADE_ITERATIONS:
		DebugManager.log_warn(
			(
				"Maximum cascade iterations reached (%d), stopping to prevent infinite loop"
				% MAX_CASCADE_ITERATIONS
			),
			"Match3"
		)

	# Save grid state after cascades complete
	save_current_state()


func regenerate_grid():
	# Validate grid size before regeneration
	if (
		GRID_SIZE.x < MIN_GRID_SIZE
		or GRID_SIZE.y < MIN_GRID_SIZE
		or GRID_SIZE.x > MAX_GRID_SIZE
		or GRID_SIZE.y > MAX_GRID_SIZE
	):
		DebugManager.log_error(
			"Invalid grid size for regeneration: %dx%d" % [GRID_SIZE.x, GRID_SIZE.y], "Match3"
		)
		return

	if TILE_TYPES < 3 or TILE_TYPES > MAX_TILE_TYPES:
		DebugManager.log_error(
			"Invalid tile types count for regeneration: %d" % TILE_TYPES, "Match3"
		)
		return

	# Use time-based seed to ensure different patterns each time
	var new_seed = Time.get_ticks_msec()
	seed(new_seed)
	DebugManager.log_debug("Regenerating grid with seed: " + str(new_seed), "Match3")

	# Safe tile cleanup with improved error handling
	var children_to_remove = []
	var removed_count = 0

	for child in get_children():
		if not is_instance_valid(child):
			continue

		# More robust tile detection
		if child.has_method("get_script") and child.get_script():
			var script_path = child.get_script().resource_path
			if script_path == "res://scenes/game/gameplays/tile.gd":
				children_to_remove.append(child)
				removed_count += 1
		elif "grid_position" in child:  # Fallback detection
			children_to_remove.append(child)
			removed_count += 1

	DebugManager.log_debug("Found %d tile children to remove" % removed_count, "Match3")

	# First clear grid array references to prevent access to nodes being freed
	for y in range(grid.size()):
		if y < grid.size() and grid[y] and grid[y] is Array:
			for x in range(grid[y].size()):
				if x < grid[y].size():
					grid[y][x] = null

	# Clear the grid array safely
	grid.clear()

	# Remove all found tile children using queue_free for safe cleanup
	for child in children_to_remove:
		if is_instance_valid(child):
			child.queue_free()

	# Wait for nodes to be properly freed from the scene tree
	await get_tree().process_frame
	await get_tree().process_frame  # Extra frame for complex hierarchies

	# Recalculate grid layout before regenerating tiles
	_calculate_grid_layout()

	# Regenerate the grid with safety checks
	_initialize_grid()


func set_tile_types(new_count: int):
	# Input validation
	if new_count < 3:
		DebugManager.log_error("Tile types count too low: %d (minimum 3)" % new_count, "Match3")
		return

	if new_count > MAX_TILE_TYPES:
		DebugManager.log_error(
			"Tile types count too high: %d (maximum %d)" % [new_count, MAX_TILE_TYPES], "Match3"
		)
		return

	if new_count == TILE_TYPES:
		DebugManager.log_debug("Tile types count unchanged, skipping regeneration", "Match3")
		return

	DebugManager.log_debug("Changing tile types from %d to %d" % [TILE_TYPES, new_count], "Match3")
	TILE_TYPES = new_count

	# Regenerate grid with new tile types (gem pool is updated in regenerate_grid)
	await regenerate_grid()


func set_grid_size(new_size: Vector2i):
	# Comprehensive input validation
	if new_size.x < MIN_GRID_SIZE or new_size.y < MIN_GRID_SIZE:
		DebugManager.log_error(
			(
				"Grid size too small: %dx%d (minimum %dx%d)"
				% [new_size.x, new_size.y, MIN_GRID_SIZE, MIN_GRID_SIZE]
			),
			"Match3"
		)
		return

	if new_size.x > MAX_GRID_SIZE or new_size.y > MAX_GRID_SIZE:
		DebugManager.log_error(
			(
				"Grid size too large: %dx%d (maximum %dx%d)"
				% [new_size.x, new_size.y, MAX_GRID_SIZE, MAX_GRID_SIZE]
			),
			"Match3"
		)
		return

	if new_size == GRID_SIZE:
		DebugManager.log_debug("Grid size unchanged, skipping regeneration", "Match3")
		return

	DebugManager.log_debug("Changing grid size from %s to %s" % [GRID_SIZE, new_size], "Match3")
	GRID_SIZE = new_size

	# Regenerate grid with new size
	await regenerate_grid()


func reset_all_visual_states() -> void:
	# Debug function to reset all tile visual states
	DebugManager.log_debug("Resetting all tile visual states", "Match3")
	for y in range(GRID_SIZE.y):
		for x in range(GRID_SIZE.x):
			if grid[y][x] and grid[y][x].has_method("force_reset_visual_state"):
				grid[y][x].force_reset_visual_state()

	# Reset game state
	selected_tile = null
	current_state = GameState.WAITING
	keyboard_navigation_enabled = false


func _debug_scene_structure() -> void:
	DebugManager.log_debug("=== Scene Structure Debug ===", "Match3")
	DebugManager.log_debug("Match3 node children count: %d" % get_child_count(), "Match3")
	DebugManager.log_debug("Match3 global position: %s" % global_position, "Match3")
	DebugManager.log_debug("Match3 scale: %s" % scale, "Match3")

	# Check if grid is properly initialized
	if not grid or grid.size() == 0:
		DebugManager.log_error("Grid not initialized when debug structure called", "Match3")
		return

	# Check tiles
	var tile_count = 0
	for y in range(GRID_SIZE.y):
		for x in range(GRID_SIZE.x):
			if y < grid.size() and x < grid[y].size() and grid[y][x]:
				tile_count += 1
	DebugManager.log_debug(
		"Created %d tiles out of %d expected" % [tile_count, GRID_SIZE.x * GRID_SIZE.y], "Match3"
	)

	# Check first tile in detail
	if grid.size() > 0 and grid[0].size() > 0 and grid[0][0]:
		var first_tile = grid[0][0]
		DebugManager.log_debug(
			"First tile global position: %s" % first_tile.global_position, "Match3"
		)
		DebugManager.log_debug("First tile local position: %s" % first_tile.position, "Match3")

	# Check parent chain
	var current_node = self
	var depth = 0
	while current_node and depth < 10:
		DebugManager.log_debug(
			"Parent level %d: %s (type: %s)" % [depth, current_node.name, current_node.get_class()],
			"Match3"
		)
		current_node = current_node.get_parent()
		depth += 1


func _input(event: InputEvent) -> void:
	# Debug key to reset all visual states
	if event.is_action_pressed("action_east") and DebugManager.is_debug_enabled():
		reset_all_visual_states()
		return

	if current_state == GameState.SWAPPING or current_state == GameState.PROCESSING:
		return

	# Handle action_east (B button/ESC) to deselect selected tile
	if event.is_action_pressed("action_east") and current_state == GameState.SELECTING:
		DebugManager.log_debug("action_east pressed - deselecting current tile", "Match3")
		_deselect_tile()
		return

	# Handle keyboard/gamepad navigation
	if event.is_action_pressed("move_up"):
		_move_cursor(Vector2i.UP)
		keyboard_navigation_enabled = true
	elif event.is_action_pressed("move_down"):
		_move_cursor(Vector2i.DOWN)
		keyboard_navigation_enabled = true
	elif event.is_action_pressed("move_left"):
		_move_cursor(Vector2i.LEFT)
		keyboard_navigation_enabled = true
	elif event.is_action_pressed("move_right"):
		_move_cursor(Vector2i.RIGHT)
		keyboard_navigation_enabled = true
	elif event.is_action_pressed("action_south"):
		if keyboard_navigation_enabled:
			_select_tile_at_cursor()


func _move_cursor(direction: Vector2i) -> void:
	# Input validation for direction vector
	if abs(direction.x) > 1 or abs(direction.y) > 1:
		DebugManager.log_error("Invalid cursor direction vector: " + str(direction), "Match3")
		return

	if direction.x != 0 and direction.y != 0:
		DebugManager.log_error(
			"Diagonal cursor movement not supported: " + str(direction), "Match3"
		)
		return

	# Validate grid integrity before cursor operations
	if not _validate_grid_integrity():
		DebugManager.log_error("Grid integrity check failed in cursor movement", "Match3")
		return

	var old_pos = cursor_position
	var new_pos = cursor_position + direction

	# Bounds checking
	new_pos.x = clamp(new_pos.x, 0, GRID_SIZE.x - 1)
	new_pos.y = clamp(new_pos.y, 0, GRID_SIZE.y - 1)

	if new_pos != cursor_position:
		# Safe access to old tile
		var old_tile = _safe_grid_access(old_pos)
		if old_tile and "is_selected" in old_tile and "is_highlighted" in old_tile:
			if not old_tile.is_selected:
				old_tile.is_highlighted = false

		DebugManager.log_debug(
			"Cursor moved from (%d,%d) to (%d,%d)" % [old_pos.x, old_pos.y, new_pos.x, new_pos.y],
			"Match3"
		)
		cursor_position = new_pos

		# Safe access to new tile
		var new_tile = _safe_grid_access(cursor_position)
		if new_tile and "is_selected" in new_tile and "is_highlighted" in new_tile:
			if not new_tile.is_selected:
				new_tile.is_highlighted = true


func _select_tile_at_cursor() -> void:
	# Validate cursor position and grid integrity
	if not _is_valid_grid_position(cursor_position):
		DebugManager.log_warn(
			(
				"Invalid cursor position for selection: (%d,%d)"
				% [cursor_position.x, cursor_position.y]
			),
			"Match3"
		)
		return

	var tile = _safe_grid_access(cursor_position)
	if tile:
		DebugManager.log_debug(
			"Keyboard selection at cursor (%d,%d)" % [cursor_position.x, cursor_position.y],
			"Match3"
		)
		_on_tile_selected(tile)
	else:
		DebugManager.log_warn(
			"No valid tile at cursor position (%d,%d)" % [cursor_position.x, cursor_position.y],
			"Match3"
		)


func _on_tile_selected(tile: Node2D) -> void:
	"""Handle tile selection with state machine logic for match-3 gameplay.

	State transitions:
	WAITING -> SELECTING: First tile selected
	SELECTING -> WAITING: Same tile clicked (deselect)
	SELECTING -> SWAPPING: Different tile clicked (attempt swap)
	"""
	# Block tile selection during busy states
	if current_state == GameState.SWAPPING or current_state == GameState.PROCESSING:
		DebugManager.log_debug(
			"Tile selection ignored - game busy (state: %s)" % [GameState.keys()[current_state]],
			"Match3"
		)
		return

	DebugManager.log_debug(
		(
			"Tile selected at (%d,%d), gem type: %d"
			% [tile.grid_position.x, tile.grid_position.y, tile.tile_type]
		),
		"Match3"
	)

	if current_state == GameState.WAITING:
		# First tile selection
		_select_tile(tile)
	elif current_state == GameState.SELECTING:
		if tile == selected_tile:
			# Deselect current tile
			DebugManager.log_debug("Same tile clicked - deselecting", "Match3")
			_deselect_tile()
		else:
			# Attempt to swap with selected tile
			DebugManager.log_debug(
				(
					"Attempting swap between (%d,%d) and (%d,%d)"
					% [
						selected_tile.grid_position.x,
						selected_tile.grid_position.y,
						tile.grid_position.x,
						tile.grid_position.y
					]
				),
				"Match3"
			)
			_attempt_swap(selected_tile, tile)


func _select_tile(tile: Node2D) -> void:
	selected_tile = tile
	tile.is_selected = true
	current_state = GameState.SELECTING  # State transition: WAITING -> SELECTING
	DebugManager.log_debug(
		"Selected tile at (%d, %d)" % [tile.grid_position.x, tile.grid_position.y], "Match3"
	)


func _deselect_tile() -> void:
	if selected_tile and is_instance_valid(selected_tile):
		# Safe access to tile properties
		if "grid_position" in selected_tile:
			DebugManager.log_debug(
				(
					"Deselecting tile at (%d,%d)"
					% [selected_tile.grid_position.x, selected_tile.grid_position.y]
				),
				"Match3"
			)
		else:
			DebugManager.log_debug("Deselecting tile (no grid position available)", "Match3")

		# Safe property access for selection state
		if "is_selected" in selected_tile:
			selected_tile.is_selected = false

		# Handle cursor highlighting for keyboard navigation
		if keyboard_navigation_enabled:
			# Clear highlighting on selected tile
			if "is_highlighted" in selected_tile:
				selected_tile.is_highlighted = false

			# Re-highlight cursor position if it's different from the deselected tile
			var cursor_tile = _safe_grid_access(cursor_position)
			if cursor_tile and "is_highlighted" in cursor_tile:
				cursor_tile.is_highlighted = true
				DebugManager.log_debug(
					(
						"Restored cursor highlighting at (%d,%d)"
						% [cursor_position.x, cursor_position.y]
					),
					"Match3"
				)
		else:
			# For mouse navigation, just clear highlighting
			if "is_highlighted" in selected_tile:
				selected_tile.is_highlighted = false

		selected_tile = null
	current_state = GameState.WAITING


func _are_adjacent(tile1: Node2D, tile2: Node2D) -> bool:
	if not tile1 or not tile2:
		return false

	var pos1 = tile1.grid_position
	var pos2 = tile2.grid_position
	var diff = abs(pos1.x - pos2.x) + abs(pos1.y - pos2.y)
	return diff == 1


func _attempt_swap(tile1: Node2D, tile2: Node2D) -> void:
	if not _are_adjacent(tile1, tile2):
		DebugManager.log_debug("Tiles are not adjacent, selecting new tile instead", "Match3")
		_deselect_tile()
		_select_tile(tile2)
		return

	DebugManager.log_debug(
		(
			"Starting swap animation: (%d,%d)[type:%d] <-> (%d,%d)[type:%d]"
			% [
				tile1.grid_position.x,
				tile1.grid_position.y,
				tile1.tile_type,
				tile2.grid_position.x,
				tile2.grid_position.y,
				tile2.tile_type
			]
		),
		"Match3"
	)

	current_state = GameState.SWAPPING  # State transition: SELECTING -> SWAPPING
	await _swap_tiles(tile1, tile2)  # Animate tile swap

	# Check if swap creates matches
	if _has_match_at(tile1.grid_position) or _has_match_at(tile2.grid_position):
		DebugManager.log_info(
			(
				"Valid swap created matches at (%d,%d) or (%d,%d)"
				% [
					tile1.grid_position.x,
					tile1.grid_position.y,
					tile2.grid_position.x,
					tile2.grid_position.y
				]
			),
			"Match3"
		)
		_deselect_tile()
		current_state = GameState.PROCESSING
		_clear_matches()
		await get_tree().create_timer(0.3).timeout
		current_state = GameState.WAITING
	else:
		# Invalid swap - revert
		DebugManager.log_debug("No matches created, reverting swap", "Match3")
		await _swap_tiles(tile1, tile2)  # Swap back
		DebugManager.log_debug("Swap reverted successfully", "Match3")
		_deselect_tile()
		current_state = GameState.WAITING


func _swap_tiles(tile1: Node2D, tile2: Node2D) -> void:
	if not tile1 or not tile2:
		DebugManager.log_error("Cannot swap tiles - one or both tiles are null", "Match3")
		return

	# Update grid positions
	var pos1 = tile1.grid_position
	var pos2 = tile2.grid_position

	DebugManager.log_debug(
		(
			"Swapping tile positions: (%d,%d) -> (%d,%d), (%d,%d) -> (%d,%d)"
			% [pos1.x, pos1.y, pos2.x, pos2.y, pos2.x, pos2.y, pos1.x, pos1.y]
		),
		"Match3"
	)

	tile1.grid_position = pos2
	tile2.grid_position = pos1

	# Update grid array
	grid[pos1.y][pos1.x] = tile2
	grid[pos2.y][pos2.x] = tile1

	# Animate position swap
	var target_pos1 = grid_offset + Vector2(pos2.x, pos2.y) * tile_size
	var target_pos2 = grid_offset + Vector2(pos1.x, pos1.y) * tile_size

	DebugManager.log_trace("Animating tile movement over 0.3 seconds", "Match3")

	var tween = create_tween()
	tween.set_parallel(true)
	tween.tween_property(tile1, "position", target_pos1, SWAP_ANIMATION_TIME)
	tween.tween_property(tile2, "position", target_pos2, SWAP_ANIMATION_TIME)

	await tween.finished
	DebugManager.log_trace("Tile swap animation completed", "Match3")


func serialize_grid_state() -> Array:
	# Convert the current grid to a serializable 2D array
	DebugManager.log_info(
		(
			"Starting serialization: grid.size()=%d, GRID_SIZE=(%d,%d)"
			% [grid.size(), GRID_SIZE.x, GRID_SIZE.y]
		),
		"Match3"
	)

	if grid.size() == 0:
		DebugManager.log_error("Grid array is empty during serialization!", "Match3")
		return []

	var serialized_grid = []
	var valid_tiles = 0
	var null_tiles = 0

	for y in range(GRID_SIZE.y):
		var row = []
		for x in range(GRID_SIZE.x):
			if y < grid.size() and x < grid[y].size() and grid[y][x]:
				row.append(grid[y][x].tile_type)
				valid_tiles += 1
				# Only log first few for brevity
				if valid_tiles <= 5:
					DebugManager.log_debug(
						"Serializing tile (%d,%d): type %d" % [x, y, grid[y][x].tile_type], "Match3"
					)
			else:
				row.append(-1)  # Invalid/empty tile
				null_tiles += 1
				# Only log first few nulls for brevity
				if null_tiles <= 5:
					DebugManager.log_debug(
						"Serializing tile (%d,%d): NULL/empty (-1)" % [x, y], "Match3"
					)
		serialized_grid.append(row)

	DebugManager.log_info(
		(
			"Serialized grid state: %dx%d grid, %d valid tiles, %d null tiles"
			% [GRID_SIZE.x, GRID_SIZE.y, valid_tiles, null_tiles]
		),
		"Match3"
	)
	return serialized_grid


func get_active_gem_types() -> Array:
	# Get active gem types from the first available tile
	if grid.size() > 0 and grid[0].size() > 0 and grid[0][0]:
		return grid[0][0].active_gem_types.duplicate()
	else:
		# Fallback to default
		var default_types = []
		for i in range(TILE_TYPES):
			default_types.append(i)
		return default_types


func save_current_state():
	# Save complete game state
	var grid_layout = serialize_grid_state()
	var active_gems = get_active_gem_types()

	DebugManager.log_info(
		(
			"Saving match3 state: size(%d,%d), %d tile types, %d active gems"
			% [GRID_SIZE.x, GRID_SIZE.y, TILE_TYPES, active_gems.size()]
		),
		"Match3"
	)

	SaveManager.save_grid_state(GRID_SIZE, TILE_TYPES, active_gems, grid_layout)


func load_saved_state() -> bool:
	# Check if there's a saved grid state
	if not SaveManager.has_saved_grid():
		DebugManager.log_info("No saved grid state found, using default generation", "Match3")
		return false

	var saved_state = SaveManager.get_saved_grid_state()

	# Restore grid settings
	var saved_size = Vector2i(saved_state.grid_size.x, saved_state.grid_size.y)
	TILE_TYPES = saved_state.tile_types_count
	var saved_gems: Array[int] = []
	for gem in saved_state.active_gem_types:
		saved_gems.append(int(gem))
	var saved_layout = saved_state.grid_layout

	DebugManager.log_info(
		(
			"[%s] Loading saved grid state: size(%d,%d), %d tile types, layout_size=%d"
			% [instance_id, saved_size.x, saved_size.y, TILE_TYPES, saved_layout.size()]
		),
		"Match3"
	)

	# Debug: Print first few rows of loaded layout
	for y in range(min(3, saved_layout.size())):
		var row_str = ""
		for x in range(min(8, saved_layout[y].size())):
			row_str += str(saved_layout[y][x]) + " "
		DebugManager.log_debug("Loading row %d: %s" % [y, row_str], "Match3")

	# Validate saved data
	if saved_layout.size() != saved_size.y:
		DebugManager.log_error(
			(
				"Saved grid layout height mismatch: expected %d, got %d"
				% [saved_size.y, saved_layout.size()]
			),
			"Match3"
		)
		return false

	if saved_layout.size() > 0 and saved_layout[0].size() != saved_size.x:
		DebugManager.log_error(
			(
				"Saved grid layout width mismatch: expected %d, got %d"
				% [saved_size.x, saved_layout[0].size()]
			),
			"Match3"
		)
		return false

	# Apply the saved settings
	var old_size = GRID_SIZE
	GRID_SIZE = saved_size

	# Recalculate layout if size changed
	if old_size != saved_size:
		DebugManager.log_info(
			"Grid size changed from %s to %s, recalculating layout" % [old_size, saved_size],
			"Match3"
		)
		_calculate_grid_layout()

	await _restore_grid_from_layout(saved_layout, saved_gems)

	DebugManager.log_info("Successfully loaded saved grid state", "Match3")
	return true


func _restore_grid_from_layout(grid_layout: Array, active_gems: Array[int]) -> void:
	DebugManager.log_info(
		(
			"[%s] Starting grid restoration: layout_size=%d, active_gems=%s"
			% [instance_id, grid_layout.size(), active_gems]
		),
		"Match3"
	)

	# Clear ALL existing tile children, not just ones in grid array
	# This ensures no duplicate layers are created
	var all_tile_children = []
	for child in get_children():
		if child.has_method("get_script") and child.get_script():
			var script_path = child.get_script().resource_path
			if script_path == "res://scenes/game/gameplays/tile.gd":
				all_tile_children.append(child)

	DebugManager.log_debug(
		"Found %d existing tile children to remove" % all_tile_children.size(), "Match3"
	)

	# Remove all found tile children
	for child in all_tile_children:
		child.queue_free()

	# Clear existing grid array
	for y in range(grid.size()):
		if grid[y] and grid[y] is Array:
			for x in range(grid[y].size()):
				grid[y][x] = null

	# Clear grid array
	grid.clear()

	# Wait for nodes to be freed
	await get_tree().process_frame

	# Restore grid from saved layout
	for y in range(GRID_SIZE.y):
		grid.append([])
		for x in range(GRID_SIZE.x):
			var tile = TILE_SCENE.instantiate()
			var tile_position = grid_offset + Vector2(x, y) * tile_size
			tile.position = tile_position
			tile.grid_position = Vector2i(x, y)
			add_child(tile)

			# Set gem types for this tile
			tile.set_active_gem_types(active_gems)

			# Set the saved tile type
			var saved_tile_type = grid_layout[y][x]
			DebugManager.log_debug(
				(
					"Setting tile (%d,%d): saved_type=%d, TILE_TYPES=%d"
					% [x, y, saved_tile_type, TILE_TYPES]
				),
				"Match3"
			)

			if saved_tile_type >= 0 and saved_tile_type < TILE_TYPES:
				tile.tile_type = saved_tile_type
				DebugManager.log_debug(
					"✓ Restored tile (%d,%d) with saved type %d" % [x, y, saved_tile_type], "Match3"
				)
			else:
				# Fallback for invalid tile types
				tile.tile_type = randi() % TILE_TYPES
				DebugManager.log_error(
					(
						"✗ Invalid saved tile type %d at (%d,%d), using random %d"
						% [saved_tile_type, x, y, tile.tile_type]
					),
					"Match3"
				)

			# Connect tile signals
			tile.tile_selected.connect(_on_tile_selected)
			grid[y].append(tile)

	DebugManager.log_info(
		"Completed grid restoration: %d tiles restored" % [GRID_SIZE.x * GRID_SIZE.y], "Match3"
	)


# Safety and validation helper functions
func _is_valid_grid_position(pos: Vector2i) -> bool:
	return pos.x >= 0 and pos.y >= 0 and pos.x < GRID_SIZE.x and pos.y < GRID_SIZE.y


func _validate_grid_integrity() -> bool:
	# Check if grid array structure is valid
	if not grid is Array:
		DebugManager.log_error("Grid is not an array", "Match3")
		return false

	if grid.size() != GRID_SIZE.y:
		DebugManager.log_error(
			"Grid height mismatch: %d vs %d" % [grid.size(), GRID_SIZE.y], "Match3"
		)
		return false

	for y in range(grid.size()):
		if not grid[y] is Array:
			DebugManager.log_error("Grid row %d is not an array" % y, "Match3")
			return false

		if grid[y].size() != GRID_SIZE.x:
			DebugManager.log_error(
				"Grid row %d width mismatch: %d vs %d" % [y, grid[y].size(), GRID_SIZE.x], "Match3"
			)
			return false

	return true


func _safe_grid_access(pos: Vector2i) -> Node2D:
	# Safe grid access with comprehensive bounds checking
	if not _is_valid_grid_position(pos):
		return null

	if pos.y >= grid.size() or pos.x >= grid[pos.y].size():
		DebugManager.log_warn("Grid bounds exceeded: (%d,%d)" % [pos.x, pos.y], "Match3")
		return null

	var tile = grid[pos.y][pos.x]
	if not tile or not is_instance_valid(tile):
		return null

	return tile


func _safe_tile_access(tile: Node2D, property: String):
	# Safe property access on tiles
	if not tile or not is_instance_valid(tile):
		return null

	if not property in tile:
		DebugManager.log_warn("Tile missing property: %s" % property, "Match3")
		return null

	return tile.get(property)
