"""Animated player sprite with 4-directional movement.

This module provides an animated sprite class for the player character with support
for idle and walking animations in all four cardinal directions. The sprite loads
animation frames from a sprite sheet and handles frame progression based on movement
state.

Key features:
- Idle and walk animations for all four directions (up, down, left, right)
- Optional animations - specify only the directions you need
- Automatic texture flipping for left-facing animations (mirrors right direction)
- Configurable animation speeds and frame counts per direction
- Delta time-based animation for smooth playback
- Simple API for movement control

The sprite sheet format expects animations laid out in rows:
- Each row contains frames for one animation type and direction
- Frames are ordered left-to-right in the sprite sheet
- Tile size determines the dimensions of each frame
- Left-facing animations can be auto-generated by flipping right-facing frames

Unlike AnimatedNPC, AnimatedPlayer does not support special animations like appear,
disappear, or interact. This keeps the player sprite simple and focused on core
movement functionality.

Example usage:
    # Create player with all 4 directions
    player = AnimatedPlayer(
        "assets/characters/player.png",
        idle_up_frames=4, idle_up_row=0,
        idle_down_frames=4, idle_down_row=1,
        idle_right_frames=4, idle_right_row=2,
        walk_up_frames=6, walk_up_row=4,
        walk_down_frames=6, walk_down_row=5,
        walk_right_frames=6, walk_right_row=6,
        scale=2.0,
        center_x=512,
        center_y=384
    )

    # Update animation each frame
    player.update_animation(delta_time, is_moving)

    # Change direction
    player.set_direction("down")
"""

import logging
from typing import TYPE_CHECKING

import arcade
from PIL import Image

if TYPE_CHECKING:
    from pathlib import Path
from PIL.Image import Transpose

logger = logging.getLogger(__name__)


class AnimatedPlayer(arcade.Sprite):
    """Player sprite with walking animations for all four cardinal directions.

    Extends arcade.Sprite with support for idle and walk animations loaded from a sprite
    sheet. The sprite automatically selects the appropriate animation based on movement
    state and facing direction. All animations are optional - specify only the directions
    you need.

    Unlike AnimatedNPC, this class does not include special animations (appear, disappear,
    interact) to keep the player character implementation simple and focused on movement.

    Attributes:
        tile_size: Size of each frame in the sprite sheet (pixels).
        columns: Number of columns in the sprite sheet.
        current_direction: Current facing direction ("up", "down", "left", or "right").
        current_frame: Current frame index in the active animation.
        animation_speed: Time between animation frames in seconds.
        animation_timer: Accumulator for frame timing.
        animation_textures: Dictionary mapping animation names to texture lists.

    Animation texture keys:
        - "idle_up", "idle_down", "idle_left", "idle_right": Idle animations for each direction
        - "walk_up", "walk_down", "walk_left", "walk_right": Walking animations for each direction
    """

    def __init__(
        self,
        sprite_sheet_path: Path | str,
        *,
        tile_size: int = 64,
        columns: int = 12,
        scale: float = 1.0,
        center_x: float = 0,
        center_y: float = 0,
        # Parameters for 4-directional animations
        # New parameters for 4-directional animations
        idle_up_frames: int | None = None,
        idle_up_row: int | None = None,
        idle_down_frames: int | None = None,
        idle_down_row: int | None = None,
        idle_left_frames: int | None = None,
        idle_left_row: int | None = None,
        idle_right_frames: int | None = None,
        idle_right_row: int | None = None,
        walk_up_frames: int | None = None,
        walk_up_row: int | None = None,
        walk_down_frames: int | None = None,
        walk_down_row: int | None = None,
        walk_left_frames: int | None = None,
        walk_left_row: int | None = None,
        walk_right_frames: int | None = None,
        walk_right_row: int | None = None,
    ) -> None:
        """Initialize the animated player sprite.

                        Loads animation textures from a sprite sheet and sets up animation state. All
                        animations are optional - specify only the directions you need. At least one
                        animation should be specified, otherwise a placeholder texture is created.

                        The sprite sheet should be organized with animations in horizontal rows. Each row
                        contains sequential frames for one animation type and direction.

        Args:
                            sprite_sheet_path: Path to the sprite sheet PNG file.
                            tile_size: Size of each frame in pixels (width and height). Default is 64x64.
                            columns: Total number of columns in the sprite sheet. Default is 12.
                            scale: Sprite scale multiplier for rendering. Default is1.0.
                            center_x: Initial X position in world coordinates.
                            center_y: Initial Y position in world coordinates.

                            Direction-specific parameters (recommended):
                            idle_up_frames: Number of frames in up idle animation.
                            idle_up_row: Row index for up idle animation.
                            idle_down_frames: Number of frames in down idle animation.
                            idle_down_row: Row index for down idle animation.
                            idle_left_frames: Number of frames in left idle animation.
                            idle_left_row: Row index for left idle animation.
                            idle_right_frames: Number of frames in right idle animation.
                            idle_right_row: Row index for right idle animation.
                            walk_up_frames: Number of frames in up walk animation.
                            walk_up_row: Row index for up walk animation.
                            walk_down_frames: Number of frames in down walk animation.
                            walk_down_row: Row index for down walk animation.
                            walk_left_frames: Number of frames in left walk animation.
                            walk_left_row: Row index for left walk animation.
                            walk_right_frames: Number of frames in right walk animation.
                            walk_right_row: Row index for right walk animation.

        Example:
                    # 4-directional player with all animations
                    player = AnimatedPlayer(
                        "assets/player.png",
                        idle_up_frames=4, idle_up_row=0,
                        idle_down_frames=4, idle_down_row=1,
                        idle_right_frames=4, idle_right_row=2,
                        idle_left_frames=4, idle_left_row=3,
                        walk_up_frames=6, walk_up_row=4,
                        walk_down_frames=6, walk_down_row=5,
                        walk_left_frames=6, walk_left_row=6,
                        walk_right_frames=6, walk_right_row=7,
                        scale=2.0
                    )
        """
        super().__init__(scale=scale)

        self.tile_size = tile_size
        self.columns = columns
        self.center_x = center_x
        self.center_y = center_y

        # Store animation config per direction (None means don't load that animation)
        self.idle_up_frames = idle_up_frames
        self.idle_up_row = idle_up_row
        self.idle_down_frames = idle_down_frames
        self.idle_down_row = idle_down_row
        self.idle_left_frames = idle_left_frames
        self.idle_left_row = idle_left_row
        self.idle_right_frames = idle_right_frames
        self.idle_right_row = idle_right_row

        self.walk_up_frames = walk_up_frames
        self.walk_up_row = walk_up_row
        self.walk_down_frames = walk_down_frames
        self.walk_down_row = walk_down_row
        self.walk_left_frames = walk_left_frames
        self.walk_left_row = walk_left_row
        self.walk_right_frames = walk_right_frames
        self.walk_right_row = walk_right_row

        # Animation state
        self.current_direction = "down"  # up, down, left, or right
        self.current_frame = 0
        self.animation_speed = 0.15  # Time between frames in seconds
        self.animation_timer = 0.0

        # Load all animation textures
        self.animation_textures: dict[str, list[arcade.Texture]] = {
            "idle_up": [],
            "idle_down": [],
            "idle_left": [],
            "idle_right": [],
            "walk_up": [],
            "walk_down": [],
            "walk_left": [],
            "walk_right": [],
        }

        self._load_textures(sprite_sheet_path)

        # Set initial texture - try down first (common default), then other directions
        for direction in ["down", "right", "up", "left"]:
            if self.animation_textures[f"idle_{direction}"]:
                self.texture = self.animation_textures[f"idle_{direction}"][0]
                self.current_direction = direction
                break
            if self.animation_textures[f"walk_{direction}"]:
                self.texture = self.animation_textures[f"walk_{direction}"][0]
                self.current_direction = direction
                break
        else:
            # No animations defined, create a placeholder
            logger.warning("No animations loaded for player sprite")
            placeholder = Image.new("RGBA", (self.tile_size, self.tile_size), (0, 255, 0, 255))
            self.texture = arcade.Texture(name="player_placeholder", image=placeholder)

    def _load_textures(self, sprite_sheet_path: Path | str) -> None:
        """Load animation textures from the sprite sheet (internal implementation).

        Reads the sprite sheet image and extracts individual frames for idle and walk
        animations in all four directions based on the frame counts and row indices
        specified during initialization. If left animations aren't specified but right
        animations are, left textures are auto-generated by flipping.

        Args:
            sprite_sheet_path: Path to the sprite sheet PNG file.

        Side effects:
            Populates self.animation_textures with loaded arcade.Texture objects.
            Logs info about sprite sheet dimensions and loaded animations.
            Logs debug message with total frame counts.
        """
        # Open the sprite sheet
        sprite_sheet = Image.open(str(sprite_sheet_path))
        sheet_width = sprite_sheet.width
        sheet_height = sprite_sheet.height

        # Build list of animations to load for logging
        animations_to_load = []
        animation_configs = [
            ("idle_up", self.idle_up_frames, self.idle_up_row),
            ("idle_down", self.idle_down_frames, self.idle_down_row),
            ("idle_left", self.idle_left_frames, self.idle_left_row),
            ("idle_right", self.idle_right_frames, self.idle_right_row),
            ("walk_up", self.walk_up_frames, self.walk_up_row),
            ("walk_down", self.walk_down_frames, self.walk_down_row),
            ("walk_left", self.walk_left_frames, self.walk_left_row),
            ("walk_right", self.walk_right_frames, self.walk_right_row),
        ]

        for anim_name, frames, row in animation_configs:
            if frames is not None and row is not None:
                animations_to_load.append(f"{anim_name} ({frames} frames, row {row})")

        logger.info(
            "Loading player sprite sheet: %dx%d pixels, tile_size=%d, animations: %s",
            sheet_width,
            sheet_height,
            self.tile_size,
            ", ".join(animations_to_load) if animations_to_load else "none",
        )

        # Helper function to load frames from a row
        def load_animation_frames(anim_name: str, frame_count: int, row_index: int, *, flip: bool = False) -> None:
            """Load animation frames from sprite sheet row."""
            for frame_num in range(frame_count):
                left = frame_num * self.tile_size
                top = row_index * self.tile_size
                right = left + self.tile_size
                bottom = top + self.tile_size

                frame_image = sprite_sheet.crop((left, top, right, bottom))

                if flip:
                    frame_image = frame_image.transpose(Transpose.FLIP_LEFT_RIGHT)

                texture = arcade.Texture(
                    name=f"player_{anim_name}_{frame_num}",
                    image=frame_image,
                )
                self.animation_textures[anim_name].append(texture)

        # Load all specified animations
        for anim_name, frames, row in animation_configs:
            if frames is not None and row is not None:
                load_animation_frames(anim_name, frames, row)

        # Auto-generate left animations from right if not explicitly provided
        if not self.animation_textures["idle_left"] and self.animation_textures["idle_right"]:
            logger.debug("Auto-generating idle_left by flipping idle_right")
            if self.idle_right_frames is not None and self.idle_right_row is not None:
                load_animation_frames("idle_left", self.idle_right_frames, self.idle_right_row, flip=True)

        if not self.animation_textures["walk_left"] and self.animation_textures["walk_right"]:
            logger.debug("Auto-generating walk_left by flipping walk_right")
            if self.walk_right_frames is not None and self.walk_right_row is not None:
                load_animation_frames("walk_left", self.walk_right_frames, self.walk_right_row, flip=True)

        # Calculate totals for logging
        total_count = sum(len(textures) for textures in self.animation_textures.values())
        idle_count = sum(
            len(self.animation_textures[f"idle_{direction}"]) for direction in ["up", "down", "left", "right"]
        )
        walk_count = sum(
            len(self.animation_textures[f"walk_{direction}"]) for direction in ["up", "down", "left", "right"]
        )

        logger.debug(
            "Loaded %d idle frames and %d walk frames (total: %d)",
            idle_count,
            walk_count,
            total_count,
        )

    def update_animation(self, delta_time: float = 1 / 60, *args: object, **_kwargs: object) -> None:
        """Update animation state and advance frames.

        Called each frame to update the sprite's texture based on movement state. Automatically
        selects between idle and walk animations based on the moving parameter and current
        facing direction. If the requested animation doesn't exist, the texture remains unchanged.

        The animation advances through frames at a fixed rate (animation_speed), cycling back
        to the first frame when reaching the end of the animation.

        Args:
            delta_time: Time elapsed since last update in seconds. Default is 1/60.
            *args: First positional argument should be moving (bool), indicating whether the
                  character is currently moving. Defaults to False if not provided.
            **_kwargs: Additional keyword arguments (unused, for compatibility).

        Side effects:
            - Updates self.texture to current animation frame (if animation exists)
            - Advances self.current_frame based on animation_timer
            - Increments self.animation_timer by delta_time
            - Resets frame if it exceeds animation length
        """
        # Extract moving parameter from args (default to False if not provided)
        moving = bool(args[0]) if args else False

        # Determine animation state
        state_prefix = "walk" if moving else "idle"
        animation_key = f"{state_prefix}_{self.current_direction}"

        # If this animation doesn't exist, skip update
        if not self.animation_textures[animation_key]:
            return

        # Reset frame if it exceeds the number of frames in current animation
        if self.current_frame >= len(self.animation_textures[animation_key]):
            self.current_frame = 0

        # Animate through frames
        self.animation_timer += delta_time

        if self.animation_timer >= self.animation_speed:
            self.animation_timer = 0.0
            self.current_frame = (self.current_frame + 1) % len(self.animation_textures[animation_key])

        # Update texture
        self.texture = self.animation_textures[animation_key][self.current_frame]

    def set_direction(self, direction: str) -> None:
        """Set the current facing direction.

        Changes the sprite's facing direction, which affects which animation textures
        are used for idle and walk animations. Resets the animation frame and timer
        to ensure smooth transition.

        Args:
            direction: New facing direction, must be "up", "down", "left", or "right".
                      Other values are ignored.

        Side effects:
            - Updates self.current_direction if direction is valid and different
            - Resets self.current_frame to 0
            - Resets self.animation_timer to 0.0
            - Logs debug message on direction change
        """
        if direction in ["up", "down", "left", "right"] and direction != self.current_direction:
            logger.debug("Direction changed from %s to %s", self.current_direction, direction)
            self.current_direction = direction
            self.current_frame = 0
            self.animation_timer = 0.0
