import time
from statistics import median
from typing import TYPE_CHECKING, Any, Callable

from fraocme.grid.directions import Direction

from ..ui.colors import c
from .types import Position

if TYPE_CHECKING:
    from .core import Grid


def _calculate_viewport(
    grid_width: int,
    grid_height: int,
    max_cols: int | None,
    max_rows: int | None,
    center: Position | None = None,
) -> tuple[int, int, int, int]:
    """
    Calculate viewport bounds for displaying a region of the grid.

    Args:
        grid_width: Total grid width
        grid_height: Total grid height
        max_cols: Number of columns to display (None = all)
        max_rows: Number of rows to display (None = all)
        center: Optional position to center viewport on (x, y)

    Returns:
        Tuple (start_x, start_y, num_cols, num_rows) defining the viewport
    """
    # Use full dimensions if not constrained
    num_cols = min(grid_width, max_cols) if max_cols else grid_width
    num_rows = min(grid_height, max_rows) if max_rows else grid_height

    # No centering: show top-left corner
    if center is None:
        return (0, 0, num_cols, num_rows)

    # Center on position
    cx, cy = center

    # Calculate start position to center the viewport
    start_x = cx - num_cols // 2
    start_y = cy - num_rows // 2

    # Clamp to grid boundaries
    if start_x < 0:
        start_x = 0
    elif start_x + num_cols > grid_width:
        start_x = max(0, grid_width - num_cols)

    if start_y < 0:
        start_y = 0
    elif start_y + num_rows > grid_height:
        start_y = max(0, grid_height - num_rows)

    return (start_x, start_y, num_cols, num_rows)


def print_grid(
    grid: "Grid | list[list[Any]]",
    separator: str = "",
    highlight: set[Position] | None = None,
    max_rows: int | None = 25,
    max_cols: int | None = 80,
    show_coords: bool = True,
    center: Position | None = None,
) -> None:
    """
    Print a 2D grid with optional position highlighting and size limits.

    Args:
        grid: 2D grid to print
        separator: String to place between cells (default: no separator)
        highlight: Optional set of positions to highlight in cyan
        max_rows: Maximum rows to display (None = all). Default: 25
        max_cols: Maximum cols to display (None = all). Default: 80
        show_coords: Show row/column coordinates
        center: Optional position to center viewport on (x, y)

    Example:
        grid = Grid.from_chars("abc\\ndef\\nghi")
        print_grid(grid, separator=' ', highlight={(1, 1)}, show_coords=True)
        # 10x10 window centered on (5,5)
        print_grid(grid, center=(5, 5), max_cols=10, max_rows=10)
    """
    # Get dimensions
    if hasattr(grid, "height"):
        height, width = grid.height, grid.width
    else:
        height, width = len(grid), len(grid[0]) if grid else 0

    # Calculate viewport
    start_x, start_y, num_cols, num_rows = _calculate_viewport(
        width, height, max_cols, max_rows, center
    )

    truncated_rows = height > num_rows
    truncated_cols = width > num_cols

    # Determine grid type and element type for display
    (
        type(grid).__name__
        if not hasattr(grid, "_grid_type_str")
        else grid._grid_type_str
    )
    # Try to infer element type
    element_type = None
    try:
        sample = None
        if hasattr(grid, "at"):
            for y in range(height):
                for x in range(width):
                    sample = grid.at(x, y)
                    if sample is not None:
                        break
                if sample is not None:
                    break
        elif isinstance(grid, list) and grid and grid[0]:
            sample = grid[0][0]
        if sample is not None:
            element_type = type(sample).__name__
    except Exception:
        pass
    type_str = "Grid"
    if element_type:
        type_str += f"[{element_type}]"
    type_str += f"({width}x{height})"
    info = type_str
    if center:
        info += f", Center: {center}"
    if truncated_rows or truncated_cols:
        info += f" (viewport: {num_cols}x{num_rows})"
    print(c.dim(info))

    # Print header if coords enabled
    if show_coords and num_cols > 0:
        max_col = start_x + num_cols - 1
        digits_spacer = "   "  # Default for digits
        if max_col >= 10:
            upper_parts = [""]
            for x in range(num_cols):
                col_num = start_x + x
                if col_num % 10 == 0 and col_num > 0:
                    upper_parts.append(str(col_num // 10))
                else:
                    upper_parts.append(" ")
            print(c.dim(separator.join(upper_parts)))

        col_header = digits_spacer + separator.join(
            str((start_x + x + 1) % 10) for x in range(num_cols)
        )
        if truncated_cols:
            col_header += f" {c.dim('...')}"
        print(c.bold(col_header))

    # Print rows
    for row_idx in range(num_rows):
        y = start_y + row_idx
        line_parts = []

        if show_coords:
            line_parts.append(c.bold(f"{y + 1:2} "))

        # Cells
        for col_idx in range(num_cols):
            x = start_x + col_idx
            cell = grid[y][x] if isinstance(grid, list) else grid.at(x, y)
            cell_str = str(cell)
            if highlight and (x, y) in highlight:
                cell_str = c.cyan(cell_str)
            line_parts.append(cell_str)

        # Truncation indicator
        if truncated_cols:
            line_parts.append(c.dim("..."))

        print(separator.join(line_parts))

    # Footer if truncated
    if truncated_rows or truncated_cols:
        if center:
            footer = c.dim(
                f"Showing {num_cols}x{num_rows} centered on {center} "
                f"(grid: {width}x{height})"
            )
        else:
            footer = c.dim(
                f"... ({height - num_rows} more rows, {height}x{width} total)"
            )
        print(footer)


def print_grid_heatmap(
    grid: "Grid",
    value_fn: Callable[[Any], float] | None = None,
    separator: str = " ",
    max_rows: int | None = 25,
    max_cols: int | None = 80,
    show_coords: bool = True,
    center: Position | None = None,
) -> None:
    """
    Print grid with color-coded values (heat map visualization).

    Automatically colors cells based on min/max/median values using c.stat().

    Args:
        grid: Grid to visualize
        value_fn: Optional function to extract numeric value from cell
                  (default: identity)
        separator: String between cells
        max_rows: Maximum rows to display (None = all)
        max_cols: Maximum cols to display (None = all)
        show_coords: Show row/column coordinates
        center: Optional position to center viewport on (x, y)

    Example:
        grid = Grid.from_ints("123\\n456\\n789")
        print_grid_heatmap(grid)  # Colors: 1=red(min), 5=yellow(median), 9=green(max)
    """
    values = []
    for y in range(grid.height):
        for x in range(grid.width):
            cell = grid.at(x, y)
            val = value_fn(cell) if value_fn else cell
            if isinstance(val, (int, float)):
                values.append(val)

    if not values:
        print(c.dim("(empty or non-numeric grid)"))
        return

    min_val, max_val, median_val = min(values), max(values), median(values)

    # Calculate viewport
    start_x, start_y, num_cols, num_rows = _calculate_viewport(
        grid.width, grid.height, max_cols, max_rows, center
    )

    truncated_rows = grid.height > num_rows
    truncated_cols = grid.width > num_cols

    # Print header
    if show_coords:
        header = "   " + separator.join(
            str((start_x + x) % 10) for x in range(num_cols)
        )
        if truncated_cols:
            header += f" {c.dim('...')}"
        print(c.dim(header))

    # Print legend
    legend = (
        f"Legend: {c.green('max')}={max_val} "
        f"{c.yellow('median')}={median_val} {c.red('min')}={min_val}"
    )
    print(c.dim(legend))

    # Print rows
    for row_idx in range(num_rows):
        y = start_y + row_idx
        line_parts = []

        if show_coords:
            line_parts.append(c.dim(f"{y:2} "))

        for col_idx in range(num_cols):
            x = start_x + col_idx
            cell = grid.at(x, y)
            val = value_fn(cell) if value_fn else cell

            # Color based on value
            if isinstance(val, (int, float)):
                cell_str = c.stat(val, min_val, max_val, median_val)
            else:
                cell_str = c.dim(str(cell))

            line_parts.append(cell_str)

        if truncated_cols:
            line_parts.append(c.dim("..."))

        print(separator.join(line_parts))

    if truncated_rows or truncated_cols:
        if center:
            footer = c.dim(
                f"Showing {num_cols}x{num_rows} centered on {center} "
                f"(grid: {grid.width}x{grid.height})"
            )
        else:
            footer = c.dim(
                f"... ({grid.height - num_rows} more rows, "
                f"{grid.height}x{grid.width} total)"
            )
        print(footer)


def print_grid_path(
    grid: "Grid",
    path: list[Position],
    separator: str = " ",
    max_rows: int | None = 25,
    max_cols: int | None = 80,
    show_coords: bool = True,
    center: Position | None = None,
) -> None:
    """
    Print grid with a path highlighted (shows direction arrows).

    Args:
        grid: Grid to print
        path: List of positions forming a path
        separator: String between cells
        max_rows: Maximum rows to display
        max_cols: Maximum cols to display
        show_coords: Show coordinates
        center: Optional position to center viewport on (x, y)

    Example:
        from fraocme.grid import bfs
        path_result = bfs(grid, start=(0,0), end=(5,5), is_walkable=lambda p,v: v!='#')
        if path_result:
            print_grid_path(grid, path_result.positions)
    """
    path_set = set(path)
    path_indices = {pos: i for i, pos in enumerate(path)}

    # Calculate viewport
    start_x, start_y, num_cols, num_rows = _calculate_viewport(
        grid.width, grid.height, max_cols, max_rows, center
    )

    truncated_rows = grid.height > num_rows
    truncated_cols = grid.width > num_cols

    # Header
    if show_coords:
        header = "   " + separator.join(
            str((start_x + x) % 10) for x in range(num_cols)
        )
        if truncated_cols:
            header += f" {c.dim('...')}"
        print(c.dim(header))

    # Print rows
    for row_idx in range(num_rows):
        y = start_y + row_idx
        line_parts = []

        if show_coords:
            line_parts.append(c.dim(f"{y:2} "))

        for col_idx in range(num_cols):
            x = start_x + col_idx
            pos = (x, y)
            cell = grid.at(x, y)

            if pos in path_set:
                idx = path_indices[pos]

                # Determine arrow direction
                if idx == 0:
                    arrow = "S"  # Start
                elif idx == len(path) - 1:
                    arrow = "E"  # End
                else:
                    prev_pos = path[idx - 1]
                    next_pos = path[idx + 1]
                    dx = next_pos[0] - prev_pos[0]
                    dy = next_pos[1] - prev_pos[1]

                    # Arrow mapping
                    if dx > 0:
                        arrow = "→"
                    elif dx < 0:
                        arrow = "←"
                    elif dy > 0:
                        arrow = "↓"
                    elif dy < 0:
                        arrow = "↑"
                    else:
                        arrow = "•"

                cell_str = c.cyan(arrow)
            else:
                cell_str = c.dim(str(cell))

            line_parts.append(cell_str)

        if truncated_cols:
            line_parts.append(c.dim("..."))

        print(separator.join(line_parts))

    if truncated_rows or truncated_cols:
        if center:
            footer = c.dim(
                f"Showing {num_cols}x{num_rows} centered on {center} "
                f"(grid: {grid.width}x{grid.height})"
            )
        else:
            footer = c.dim(
                f"... ({grid.height - num_rows} more rows, "
                f"{grid.height}x{grid.width} total)"
            )
        print(footer)

    print(c.dim(f"Path length: {len(path)} steps"))


def print_grid_diff(
    grid1: "Grid",
    grid2: "Grid",
    separator: str = " ",
    max_rows: int | None = 25,
    max_cols: int | None = 80,
    show_coords: bool = True,
    center: Position | None = None,
) -> None:
    """
    Print difference between two grids (highlights changes).

    Args:
        grid1: First grid (baseline)
        grid2: Second grid (comparison)
        separator: String between cells
        max_rows: Maximum rows to display
        max_cols: Maximum cols to display
        show_coords: Show coordinates
        center: Optional position to center viewport on (x, y)

    Example:
        original = Grid.from_chars("abc\\ndef\\nghi")
        modified = original.set(1, 1, "X")
        print_grid_diff(original, modified)  # Shows X highlighted
    """
    if grid1.dimensions != grid2.dimensions:
        print(c.error(f"Dimension mismatch: {grid1.dimensions} vs {grid2.dimensions}"))
        return

    # Calculate viewport
    start_x, start_y, num_cols, num_rows = _calculate_viewport(
        grid1.width, grid1.height, max_cols, max_rows, center
    )

    truncated_rows = grid1.height > num_rows
    truncated_cols = grid1.width > num_cols

    changes = 0

    # Header
    if show_coords:
        header = "   " + separator.join(
            str((start_x + x) % 10) for x in range(num_cols)
        )
        if truncated_cols:
            header += f" {c.dim('...')}"
        print(c.dim(header))

    # Print rows
    for row_idx in range(num_rows):
        y = start_y + row_idx
        line_parts = []

        if show_coords:
            line_parts.append(c.dim(f"{y:2} "))

        for col_idx in range(num_cols):
            x = start_x + col_idx
            cell1 = grid1.at(x, y)
            cell2 = grid2.at(x, y)

            if cell1 != cell2:
                cell_str = c.success(str(cell2))  # Green for changes
                changes += 1
            else:
                cell_str = c.dim(str(cell2))

            line_parts.append(cell_str)

        if truncated_cols:
            line_parts.append(c.dim("..."))

        print(separator.join(line_parts))

    if truncated_rows or truncated_cols:
        if center:
            footer = c.dim(
                f"Showing {num_cols}x{num_rows} centered on {center} "
                f"(grid: {grid1.width}x{grid1.height})"
            )
        else:
            footer = c.dim(
                f"... ({grid1.height - num_rows} more rows, "
                f"{grid1.height}x{grid1.width} total)"
            )
        print(footer)

    print(c.dim(f"Changes: {changes} cells"))


def print_grid_neighbors(
    grid: "Grid",
    pos: Position,
    ring: int = 1,
    include_diagonals: bool = True,
    separator: str = " ",
    max_rows: int | None = 25,
    max_cols: int | None = 80,
    show_coords: bool = True,
) -> None:
    """
    Print grid with neighbors of a position highlighted.
    Viewport is automatically centered on pos.

    Args:
        grid: Grid to print
        pos: Center position (viewport will be centered here)
        ring: Neighbor ring distance (1=immediate, 2=next layer, etc.)
        include_diagonals: Include diagonal neighbors
        separator: String between cells
        max_rows: Maximum rows to display
        max_cols: Maximum cols to display
        show_coords: Show coordinates

    Example:
        grid = Grid.from_chars("abc\\ndef\\nghi")
        print_grid_neighbors(grid, (1, 1), ring=1, include_diagonals=True)
    """
    neighbors = grid.get_neighbors(pos, ring=ring, include_diagonals=include_diagonals)
    neighbor_set = set(neighbors)

    # Calculate viewport centered on pos
    start_x, start_y, num_cols, num_rows = _calculate_viewport(
        grid.width, grid.height, max_cols, max_rows, center=pos
    )

    truncated_rows = grid.height > num_rows
    truncated_cols = grid.width > num_cols

    # Header
    if show_coords:
        header = "   " + separator.join(
            str((start_x + x) % 10) for x in range(num_cols)
        )
        if truncated_cols:
            header += f" {c.dim('...')}"
        print(c.dim(header))

    diagonal_text = "with diagonals" if include_diagonals else "cardinal only"
    info = f"Center: {pos}, Ring: {ring} ({diagonal_text}), Neighbors: {len(neighbors)}"
    print(c.dim(info))

    # Print rows
    for row_idx in range(num_rows):
        y = start_y + row_idx
        line_parts = []

        if show_coords:
            line_parts.append(c.dim(f"{y:2} "))

        for col_idx in range(num_cols):
            x = start_x + col_idx
            cell_pos = (x, y)
            cell = grid.at(x, y)

            if cell_pos == pos:
                cell_str = c.success(str(cell))  # Green for center
            elif cell_pos in neighbor_set:
                cell_str = c.cyan(str(cell))  # Cyan for neighbors
            else:
                cell_str = c.dim(str(cell))

            line_parts.append(cell_str)

        if truncated_cols:
            line_parts.append(c.dim("..."))

        print(separator.join(line_parts))

    if truncated_rows or truncated_cols:
        footer = c.dim(
            f"Showing {num_cols}x{num_rows} centered on {pos} "
            f"(grid: {grid.width}x{grid.height})"
        )
        print(footer)


def print_grid_animated(
    grid: "Grid",
    positions: list[Position],
    delay: float = 0.1,
    separator: str = " ",
    max_rows: int | None = 50,
    max_cols: int | None = 100,
    show_coords: bool = True,
    trail_length: int = 0,
    show_step_count: bool = True,
    max_iterations: int = 200,
    erase_after: bool = False,
) -> None:
    """
    Animate movement through grid positions (clears console each frame).
    Viewport automatically centers on current position.

    Perfect for visualizing guard patrols, pathfinding, or any sequential movement.

    Args:
        grid: Grid to display
        positions: List of positions to animate through
        delay: Seconds between frames (default: 0.1)
        separator: String between cells
        max_rows: Maximum rows to display (default: 50 for animations)
        max_cols: Maximum cols to display (default: 100 for animations)
        show_coords: Show row/column coordinates
        trail_length: Number of previous positions to show as trail (0=none)
        show_step_count: Show current step number
        max_iterations: Maximum frames to display (default: 200). If positions > max,
                        frames are skipped to stay within limit
        erase_after: Erase animation from console after completion (default: False)

    Example:
        # Day 1 guard patrol animation
        grid = Grid.from_chars(raw_input)
        path = simulate_guard_patrol(grid)
        print_grid_animated(grid, path, delay=0.05, trail_length=10)
        # Multiple animations: use erase_after=True to prevent stacking
        print_grid_animated(
                            grid, path2, delay=0.05,
                            erase_after=True)
    """
    if not positions:
        print(c.dim("(no positions to animate)"))
        return

    # Calculate frame skipping if needed
    total_positions = len(positions)
    if total_positions > max_iterations:
        step_size = total_positions / max_iterations
        frame_indices = [int(i * step_size) for i in range(max_iterations)]
        # Always include the last position
        if frame_indices[-1] != total_positions - 1:
            frame_indices[-1] = total_positions - 1
        print(
            c.dim(
                f"Showing {max_iterations}/{total_positions} positions "
                f"(skipping frames)"
            )
        )
    else:
        frame_indices = list(range(total_positions))

    prev_total_lines = None
    for frame_num, step in enumerate(frame_indices):
        current_pos = positions[step]

        # Calculate viewport centered on current position
        start_x, start_y, num_cols, num_rows = _calculate_viewport(
            grid.width, grid.height, max_cols, max_rows, center=current_pos
        )

        # Calculate total lines for cursor repositioning for THIS frame
        total_lines = num_rows
        if show_coords:
            max_col = start_x + num_cols - 1
            total_lines += 1  # Ones header line (always)
            if max_col >= 10:
                total_lines += 1  # Upper digits line
        if show_step_count:
            total_lines += 1  # Step counter line

        # Move cursor up to overwrite previous frame (except first frame)
        if frame_num > 0 and prev_total_lines is not None:
            print(f"\033[{prev_total_lines}A", end="")

        prev_total_lines = total_lines

        # Calculate trail positions
        trail_positions = set()
        if trail_length > 0 and step > 0:
            trail_start = max(0, step - trail_length)
            trail_positions = set(positions[trail_start:step])

        # Print step counter first (above everything)
        if show_step_count:
            print(c.dim(f"Step {step + 1}/{total_positions} - Position: {current_pos}"))

        # Print column headers
        if show_coords:
            # Build upper digits line (tens, hundreds, etc.)
            max_col = start_x + num_cols - 1
            if max_col >= 10:
                upper_parts = ["  "]  # Row number padding
                for x in range(num_cols):
                    col_num = start_x + x
                    if col_num % 10 == 0 and col_num > 0:
                        # Show the upper digits at multiples of 10
                        upper_parts.append(str(col_num // 10))
                    else:
                        upper_parts.append(" ")
                print(c.dim(separator.join(upper_parts)))

            # Ones place header (always shown)
            ones_header = "   " + separator.join(
                str((start_x + x) % 10) for x in range(num_cols)
            )
            print(c.dim(ones_header))

        # Print rows
        for row_idx in range(num_rows):
            y = start_y + row_idx
            line_parts = []

            if show_coords:
                line_parts.append(c.dim(f"{y:2} "))

            for col_idx in range(num_cols):
                x = start_x + col_idx
                pos = (x, y)
                cell = grid.at(x, y)

                if pos == current_pos:
                    # Current position - bright cyan/bold @ symbol
                    cell_str = c.cyan(c.bold("@"))
                elif pos in trail_positions:
                    # Trail - bright yellow + symbol
                    cell_str = c.yellow("+")
                else:
                    # Normal cell
                    cell_str = c.dim(str(cell))

                line_parts.append(cell_str)

            print(separator.join(line_parts))

        # Sleep before next frame
        time.sleep(delay)

    if erase_after:
        # Use the last frame's total_lines, plus one for completion message
        lines_to_clear = prev_total_lines + 1 if prev_total_lines is not None else 0
        print(f"\033[{lines_to_clear}A\033[J", end="")
    else:
        print(c.success(f"\n Animation complete! ({total_positions} steps)"))


def print_grid_animated_with_direction(
    grid: "Grid",
    positions: list[Position],
    directions: list[Direction] | None = None,
    delay: float = 0.1,
    separator: str = " ",
    max_rows: int | None = 50,
    max_cols: int | None = 100,
    show_coords: bool = True,
    trail_length: int = 0,
    show_step_count: bool = True,
    max_iterations: int = 200,
    center: Position | None = None,
    erase_after: bool = False,
) -> None:
    """
    Animate movement with directional arrows (for guard patrol simulations).
    Viewport can be centered on current position.

    Args:
        grid: Grid to display
        positions: List of positions to animate through
        directions: Optional list of Direction objects (same length as positions)
        delay: Seconds between frames
        separator: String between cells
        max_rows: Maximum rows to display
        max_cols: Maximum cols to display
        show_coords: Show coordinates
        trail_length: Previous positions to show as trail
        show_step_count: Show current step
        max_iterations: Maximum frames to display (default: 200). If positions > max,
                        frames are skipped to stay within limit
        center: Optional position to center viewport on (x, y)
        erase_after: Erase animation after completion (default: False)

    Example:
        # With direction tracking
        path = [(0,0), (1,0), (2,0)]
        dirs = [EAST, EAST, EAST]
        print_grid_animated_with_direction(grid, path, dirs, delay=0.05)
        # Multiple animations: use erase_after=True to prevent stacking
        print_grid_animated_with_direction(
                                        grid, path2, dirs2, delay=0.05,
                                        erase_after=True
                                        )
    """
    if not positions:
        print(c.dim("(no positions to animate)"))
        return

    # Direction to arrow mapping
    direction_arrows = {
        "north": "^",
        "south": "v",
        "east": ">",
        "west": "<",
        "northeast": "/",
        "southeast": "\\",
        "southwest": "\\",
        "northwest": "/",
    }

    total_positions = len(positions)
    if total_positions > max_iterations:
        # Calculate step size to reduce frames to max_iterations
        step_size = total_positions / max_iterations
        frame_indices = [int(i * step_size) for i in range(max_iterations)]
        # Always include the last position
        if frame_indices[-1] != total_positions - 1:
            frame_indices[-1] = total_positions - 1
        print(
            c.dim(
                f"Showing {max_iterations}/{total_positions} positions "
                f"(skipping frames)"
            )
        )
    else:
        frame_indices = list(range(total_positions))

    prev_total_lines = None
    for frame_num, step in enumerate(frame_indices):
        current_pos = positions[step]

        # Calculate viewport centered on current position
        start_x, start_y, num_cols, num_rows = _calculate_viewport(
            grid.width, grid.height, max_cols, max_rows, center=center or current_pos
        )

        # Calculate total lines for cursor repositioning for THIS frame
        total_lines = num_rows
        if show_coords:
            max_col = start_x + num_cols - 1
            total_lines += 1  # Ones header line (always)
            if max_col >= 10:
                total_lines += 1  # Upper digits line
        if show_step_count:
            total_lines += 1  # Step counter line

        # Move cursor up to overwrite previous frame (except first frame)
        if frame_num > 0 and prev_total_lines is not None:
            print(f"\033[{prev_total_lines}A", end="")

        prev_total_lines = total_lines

        # Get current direction
        current_dir = None
        if directions and step < len(directions):
            current_dir = directions[step]

        # Calculate trail
        trail_positions = set()
        if trail_length > 0 and step > 0:
            trail_start = max(0, step - trail_length)
            trail_positions = set(positions[trail_start:step])

        # Print step counter first (above everything)
        if show_step_count:
            dir_text = ""
            if current_dir:
                dir_name = current_dir.name
                dir_text = f" - Facing: {dir_name}"
            print(
                c.dim(
                    f"Step {step + 1}/{total_positions} - "
                    f"Position: {current_pos}{dir_text}"
                )
            )

        # Print column headers
        if show_coords:
            # Build upper digits line (tens, hundreds, etc.)
            max_col = start_x + num_cols - 1
            if max_col >= 10:
                upper_parts = ["  "]  # Row number padding
                for x in range(num_cols):
                    col_num = start_x + x
                    if col_num % 10 == 0 and col_num > 0:
                        # Show the upper digits at multiples of 10
                        upper_parts.append(str(col_num // 10))
                    else:
                        upper_parts.append(" ")
                print(c.dim(separator.join(upper_parts)))

            # Ones place header (always shown)
            ones_header = "   " + separator.join(
                str((start_x + x) % 10) for x in range(num_cols)
            )
            print(c.dim(ones_header))

        # Print rows
        for row_idx in range(num_rows):
            y = start_y + row_idx
            line_parts = []

            if show_coords:
                line_parts.append(c.dim(f"{y:2} "))

            for col_idx in range(num_cols):
                x = start_x + col_idx
                pos = (x, y)
                cell = grid.at(x, y)

                if pos == current_pos:
                    # Show arrow for current direction
                    if current_dir:
                        dir_name = current_dir.name
                        arrow = direction_arrows.get(dir_name, "@")
                        cell_str = c.cyan(c.bold(arrow))
                    else:
                        cell_str = c.cyan(c.bold("@"))
                elif pos in trail_positions:
                    # Trail - bright yellow
                    cell_str = c.yellow("+")
                else:
                    # Normal cell
                    cell_str = c.dim(str(cell))

                line_parts.append(cell_str)

            print(separator.join(line_parts))

        time.sleep(delay)

    if erase_after:
        # Use the last frame's total_lines, plus one for completion message
        lines_to_clear = prev_total_lines + 1 if prev_total_lines is not None else 0
        print(f"\033[{lines_to_clear}A\033[J", end="")
    else:
        print(c.success(f"\n Animation complete! ({total_positions} steps)"))
