"""Animated NPC sprite with 4-directional movement and special animations.

This module provides an animated sprite class for NPCs with support for idle, walk,
appear, disappear, and interact animations. The sprite loads animation frames from
a sprite sheet and provides methods to control animation playback.

Key features:
- Idle and walk animations for all four directions (up, down, left, right)
- Optional animations - specify only the directions you need
- Appear animation for materialization effects (invisible to visible)
- Disappear animation for dematerialization effects (visible to invisible)
- Interact animation for NPC reactions in all four directions (e.g., waving, nodding)
- Automatic texture flipping for left-facing animations
- Configurable animation speeds and frame counts per direction
- Integration with game scripting system via animation events

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

Example usage:
    # Create NPC with 4-directional animations
    npc = AnimatedNPC(
        "assets/characters/npc.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,
        appear_frames=9, appear_row=8,
        interact_up_frames=3, interact_up_row=10,
        scale=2.0
    )

    # Only 4-directional mode supported
    npc = AnimatedNPC(
        "assets/characters/npc.png",
        idle_up_frames=4, idle_up_row=0,
        idle_down_frames=4, idle_down_row=1,
        idle_left_frames=4, idle_left_row=2,
        idle_right_frames=4, idle_right_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,
        appear_frames=9, appear_row=6,
        scale=2.0
    )

    # Update animation each frame
    npc.update_animation(delta_time, moving=is_moving)

    # Trigger special animations
    npc.start_appear_animation()
    npc.start_interact_animation()
"""

import logging
from typing import TYPE_CHECKING

import arcade
from PIL import Image

from pedre.sprites.animated_player import AnimatedPlayer

if TYPE_CHECKING:
    from pathlib import Path


logger = logging.getLogger(__name__)


class AnimatedNPC(AnimatedPlayer):
    """NPC sprite with walking animations for all 4 directions and special animations.

    Extends AnimatedPlayer with support for special animation types: appear, disappear,
        and interact. Inherits all idle and walk animation functionality (including 4-directional
        support) from the base class.

        Special animations (appear, disappear, interact) take precedence over normal
        idle/walk animations and complete before returning to normal state.

        Only 4-directional mode supported - all idle and walk animations must be specified
        for each direction explicitly.

        Additional attributes (beyond AnimatedPlayer):
            is_appearing: Whether appear animation is currently playing.
            appear_complete: Whether appear animation has finished.
            is_disappearing: Whether disappear animation is currently playing.
            disappear_complete: Whether disappear animation has finished.
            is_interacting: Whether interact animation is currently playing.
            interact_complete: Whether interact animation has finished.

        Additional animation texture keys (beyond AnimatedPlayer):
            - "appear": Materialization animation (direction-agnostic)
            - "disappear": Dematerialization animation (direction-agnostic)
            - "interact_up", "interact_down", "interact_left", "interact_right": Interaction
              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,
        # 4-directional parameters (inherited from AnimatedPlayer)
        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,
        # NPC-specific special animations
        appear_frames: int | None = None,
        appear_row: int | None = None,
        interact_up_frames: int | None = None,
        interact_up_row: int | None = None,
        interact_down_frames: int | None = None,
        interact_down_row: int | None = None,
        interact_left_frames: int | None = None,
        interact_left_row: int | None = None,
        interact_right_frames: int | None = None,
        interact_right_row: int | None = None,
    ) -> None:
        """Initialize the animated NPC sprite.

        Extends AnimatedPlayer with additional special animations (appear, disappear, interact).
        Inherits all idle and walk animation functionality (including 4-directional support)
        from the parent class.

        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 is 1.0.
            center_x: Initial X position in world coordinates.
            center_y: Initial Y position in world coordinates.

            4-directional idle/walk parameters (same as AnimatedPlayer):
            idle_up_frames: Number of frames for up idle animation.
            idle_up_row: Row index for up idle animation.
            idle_down_frames: Number of frames for down idle animation.
            idle_down_row: Row index for down idle animation.
            idle_left_frames: Number of frames for left idle animation.
            idle_left_row: Row index for left idle animation.
            idle_right_frames: Number of frames for right idle animation.
            idle_right_row: Row index for right idle animation.
            walk_up_frames: Number of frames for up walk animation.
            walk_up_row: Row index for up walk animation.
            walk_down_frames: Number of frames for down walk animation.
            walk_down_row: Row index for down walk animation.
            walk_left_frames: Number of frames for left walk animation.
            walk_left_row: Row index for left walk animation.
            walk_right_frames: Number of frames for right walk animation.
            walk_right_row: Row index for right walk animation.

            NPC-specific special animations:
            appear_frames: Number of frames in appear/disappear animations.
            appear_row: Row index for appear/disappear frames.
            interact_up_frames: Number of frames for up interact animation.
            interact_up_row: Row index for up interact animation.
            interact_down_frames: Number of frames for down interact animation.
            interact_down_row: Row index for down interact animation.
            interact_left_frames: Number of frames for left interact animation.
            interact_left_row: Row index for left interact animation.
            interact_right_frames: Number of frames for right interact animation.
            interact_right_row: Row index for right interact animation.

        Example:
            # 4-directional NPC with all animations
            npc = AnimatedNPC(
                "assets/npc.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,
                interact_down_frames=3, interact_down_row=10,
                appear_frames=9, appear_row=8,
                scale=2.0
            )
        """
        # Initialize parent class with base animation params (including 4-directional support)
        super().__init__(
            sprite_sheet_path,
            tile_size=tile_size,
            columns=columns,
            scale=scale,
            center_x=center_x,
            center_y=center_y,
            idle_up_frames=idle_up_frames,
            idle_up_row=idle_up_row,
            idle_down_frames=idle_down_frames,
            idle_down_row=idle_down_row,
            idle_left_frames=idle_left_frames,
            idle_left_row=idle_left_row,
            idle_right_frames=idle_right_frames,
            idle_right_row=idle_right_row,
            walk_up_frames=walk_up_frames,
            walk_up_row=walk_up_row,
            walk_down_frames=walk_down_frames,
            walk_down_row=walk_down_row,
            walk_left_frames=walk_left_frames,
            walk_left_row=walk_left_row,
            walk_right_frames=walk_right_frames,
            walk_right_row=walk_right_row,
        )

        # Store NPC-specific animation config
        self.appear_frames = appear_frames
        self.appear_row = appear_row

        self.interact_up_frames = interact_up_frames
        self.interact_up_row = interact_up_row
        self.interact_down_frames = interact_down_frames
        self.interact_down_row = interact_down_row
        self.interact_left_frames = interact_left_frames
        self.interact_left_row = interact_left_row
        self.interact_right_frames = interact_right_frames
        self.interact_right_row = interact_right_row

        # Appear animation state
        self.is_appearing = False
        self.appear_complete = False

        # Disappear animation state
        self.is_disappearing = False
        self.disappear_complete = False

        # Interact animation state
        self.is_interacting = False
        self.interact_complete = False

        # Add NPC-specific animation texture keys to parent's dict
        self.animation_textures["appear"] = []
        self.animation_textures["disappear"] = []
        self.animation_textures["interact_up"] = []
        self.animation_textures["interact_down"] = []
        self.animation_textures["interact_left"] = []
        self.animation_textures["interact_right"] = []

        # Load NPC-specific animations
        self._load_npc_textures(sprite_sheet_path)

        # Update initial texture if appear animation is available and no idle/walk was set
        has_any_base_animation = any(
            self.animation_textures[f"{anim}_{direction}"]
            for anim in ["idle", "walk"]
            for direction in ["up", "down", "left", "right"]
        )
        if not has_any_base_animation and self.animation_textures["appear"]:
            self.texture = self.animation_textures["appear"][0]

    def _load_npc_textures(self, sprite_sheet_path: Path | str) -> None:
        """Load NPC-specific animation textures (appear, disappear, interact in 4 directions).

        Loads only the special animations unique to NPCs. Base animations (idle, walk)
        are loaded by the parent AnimatedPlayer class.

        For appear/disappear animations, frames are loaded in reverse order for the appear
        animation (creating a fade-in effect) and forward order for the disappear animation
        (creating a fade-out effect).

        Interact animations can be specified per direction. If only right is specified,
        left is auto-generated by flipping.

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

        Side effects:
            Populates NPC-specific entries in self.animation_textures.
            Logs debug information about loaded NPC animations.
        """
        # Open the sprite sheet
        sprite_sheet = Image.open(str(sprite_sheet_path))

        # Build list of animations to load for logging
        animations_to_load = []
        if self.appear_frames is not None and self.appear_row is not None:
            animations_to_load.append(f"appear ({self.appear_frames} frames, row {self.appear_row})")

        interact_configs = [
            ("interact_up", self.interact_up_frames, self.interact_up_row),
            ("interact_down", self.interact_down_frames, self.interact_down_row),
            ("interact_left", self.interact_left_frames, self.interact_left_row),
            ("interact_right", self.interact_right_frames, self.interact_right_row),
        ]

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

        if animations_to_load:
            logger.info(
                "Loading NPC-specific animations: %s",
                ", ".join(animations_to_load),
            )

        # Load appear/disappear animations if defined
        if self.appear_frames is not None and self.appear_row is not None:
            # Load appear animation - reverse order to go from invisible to visible
            for frame_num in range(self.appear_frames - 1, -1, -1):
                left = frame_num * self.tile_size
                top = self.appear_row * self.tile_size
                right = left + self.tile_size
                bottom = top + self.tile_size

                # Appear animation (no flipping needed)
                frame_image = sprite_sheet.crop((left, top, right, bottom))
                texture = arcade.Texture(
                    name=f"npc_appear_{self.appear_frames - 1 - frame_num}",
                    image=frame_image,
                )
                self.animation_textures["appear"].append(texture)

            # Load disappear animation - forward order to go from visible to invisible
            for frame_num in range(self.appear_frames):
                left = frame_num * self.tile_size
                top = self.appear_row * self.tile_size
                right = left + self.tile_size
                bottom = top + self.tile_size

                # Disappear animation (no flipping needed)
                frame_image = sprite_sheet.crop((left, top, right, bottom))
                texture = arcade.Texture(
                    name=f"npc_disappear_{frame_num}",
                    image=frame_image,
                )
                self.animation_textures["disappear"].append(texture)

        # Helper function to load interact frames from a row
        def load_interact_frames(anim_name: str, frame_count: int, row_index: int, *, flip: bool = False) -> None:
            """Load interact 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(Image.FLIP_LEFT_RIGHT)  # type: ignore[attr-defined]

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

        # Load all specified interact animations
        for anim_name, frames, row in interact_configs:
            if frames is not None and row is not None:
                load_interact_frames(anim_name, frames, row)

        # Auto-generate left interact from right if not explicitly provided
        if not self.animation_textures["interact_left"] and self.animation_textures["interact_right"]:
            logger.debug("Auto-generating interact_left by flipping interact_right")
            if self.interact_right_frames is not None and self.interact_right_row is not None:
                load_interact_frames("interact_left", self.interact_right_frames, self.interact_right_row, flip=True)

        # Calculate totals for logging
        appear_count = self.appear_frames or 0
        interact_count = sum(
            len(self.animation_textures[f"interact_{direction}"]) for direction in ["up", "down", "left", "right"]
        )

        if appear_count or interact_count:
            logger.debug(
                "Loaded %d appear/disappear frames, %d interact frames",
                appear_count * 2,
                interact_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 the current animation state.
        Handles special NPC animations (interact, disappear, appear) with priority, falling back
        to normal idle/walk animations from the parent class.

        Animation priority (highest to lowest):
        1. Interact animation - plays once, then returns to idle
        2. Disappear animation - plays once, then hides sprite
        3. Appear animation - plays once, then transitions to idle
        4. Normal animations - idle or walk based on moving parameter (handled by parent)

        Special animations set completion flags when finished and automatically transition
        to appropriate states. For example, disappear sets sprite.visible = False when complete.

        Args:
            delta_time: Time elapsed since last update in seconds. Default is 1/60.
            *args: Positional arguments (unused, for compatibility with base class).
            **kwargs: Keyword arguments. Should include 'moving' (bool) to indicate if the
                     character is currently moving.

        Side effects:
            - Updates self.texture to current animation frame
            - Advances self.current_frame based on animation_timer
            - Sets completion flags (interact_complete, disappear_complete, appear_complete)
            - May set sprite.visible = False after disappear animation
            - Resets animation state when transitioning between animations
        """
        # Handle interact animation
        if self.is_interacting:
            self.animation_timer += delta_time

            if self.animation_timer >= self.animation_speed:
                self.animation_timer = 0.0
                self.current_frame += 1

                animation_key = f"interact_{self.current_direction}"
                # Check if interact animation is complete
                if self.current_frame >= len(self.animation_textures[animation_key]):
                    self.is_interacting = False
                    self.interact_complete = True
                    self.current_frame = 0
                    logger.debug("Interact animation complete")
                    # Return to idle after interaction
                    idle_key = f"idle_{self.current_direction}"
                    if self.animation_textures[idle_key]:
                        self.texture = self.animation_textures[idle_key][0]
                else:
                    self.texture = self.animation_textures[animation_key][self.current_frame]
            return

        # Handle disappear animation
        if self.is_disappearing:
            self.animation_timer += delta_time

            if self.animation_timer >= self.animation_speed:
                self.animation_timer = 0.0
                self.current_frame += 1

                # Check if disappear animation is complete
                if self.current_frame >= len(self.animation_textures["disappear"]):
                    self.is_disappearing = False
                    self.disappear_complete = True
                    self.visible = False
                    self.current_frame = 0
                    logger.debug("Disappear animation complete, NPC now invisible")
                else:
                    self.texture = self.animation_textures["disappear"][self.current_frame]
            return

        # Handle appear animation
        if self.is_appearing:
            self.animation_timer += delta_time

            if self.animation_timer >= self.animation_speed:
                self.animation_timer = 0.0
                self.current_frame += 1

                # Check if appear animation is complete
                if self.current_frame >= len(self.animation_textures["appear"]):
                    self.is_appearing = False
                    self.appear_complete = True
                    self.current_frame = 0
                    logger.debug("Appear animation complete")
                else:
                    self.texture = self.animation_textures["appear"][self.current_frame]
            return

        # No special animation active - delegate to parent for normal idle/walk animations
        super().update_animation(delta_time, *args, **kwargs)

    def start_appear_animation(self) -> None:
        """Start the appear animation (invisible to visible).

        Triggers the appear animation which transitions the sprite from invisible to visible.
        This is typically used when revealing NPCs through scripts (RevealNPCsAction).

        The animation plays once and sets appear_complete = True when finished. The sprite
        then transitions to idle animation automatically.

        Side effects:
            - Sets is_appearing = True
            - Resets appear_complete to False
            - Resets current_frame and animation_timer to 0
            - Sets texture to first appear frame
            - Logs debug message
        """
        if not self.animation_textures.get("appear"):
            logger.debug("No appear animation, showing immediately")
            self.visible = True
            self.appear_complete = True
            return

        self.is_appearing = True
        self.appear_complete = False
        self.current_frame = 0
        self.animation_timer = 0.0
        self.texture = self.animation_textures["appear"][0]
        logger.debug("Starting appear animation")

    def start_disappear_animation(self) -> None:
        """Start the disappear animation (visible to invisible).

        Triggers the disappear animation which transitions the sprite from visible to invisible.
        This is typically used in scripts (StartDisappearAnimationAction) when NPCs leave
        the scene dramatically.

        The animation plays once, sets disappear_complete = True, and sets sprite.visible = False
        when finished. The NPC manager typically emits an NPCDisappearCompleteEvent afterward.

        Side effects:
            - Sets is_disappearing = True
            - Resets disappear_complete to False
            - Resets current_frame and animation_timer to 0
            - Sets texture to first disappear frame
            - Sets sprite.visible = False when animation completes (via update_animation)
            - Logs debug message
        """
        if not self.animation_textures.get("disappear"):
            logger.debug("No disappear animation, hiding immediately")
            self.visible = False
            self.disappear_complete = True
            return

        self.is_disappearing = True
        self.disappear_complete = False
        self.current_frame = 0
        self.animation_timer = 0.0
        self.texture = self.animation_textures["disappear"][0]
        logger.debug("Starting disappear animation")

    def start_interact_animation(self) -> None:
        """Start the interact animation (plays once, then returns to idle).

        Triggers the interact animation which shows the NPC reacting to interaction (e.g.,
        waving, nodding, emoting). The animation plays once and automatically returns to
        idle when complete.

        If no interact animation is loaded for the current direction, logs a warning and
        does nothing. This allows NPCs without interact animations to gracefully handle
        interaction attempts.

        Side effects:
            - Sets is_interacting = True (only if animation textures exist)
            - Resets interact_complete to False
            - Resets current_frame and animation_timer to 0
            - Sets texture to first interact frame for current direction
            - Logs info message on success, warning on failure
        """
        animation_key = f"interact_{self.current_direction}"

        logger.debug(
            "Attempting to start interact animation: direction=%s, has_textures=%s, texture_count=%d",
            self.current_direction,
            animation_key in self.animation_textures,
            len(self.animation_textures.get(animation_key, [])),
        )

        if not self.animation_textures.get(animation_key):
            logger.warning(
                "No interact animation loaded for direction %s",
                self.current_direction,
            )
            return

        self.is_interacting = True
        self.interact_complete = False
        self.current_frame = 0
        self.animation_timer = 0.0
        self.texture = self.animation_textures[animation_key][0]
        logger.info(
            "Started interact animation (direction: %s, frames: %d)",
            self.current_direction,
            len(self.animation_textures[animation_key]),
        )
