"""
Camera system for viewport scrolling.

Provides abstraction for camera motion in different modes:
- Vertical: traditional upward scroll
- Horizontal: left/right movement
- Omni: combination of both
- Floating: sinusoidal/bobbing motion

The camera defines a visible viewport into a larger Canvas.
"""

import math
from collections.abc import Callable
from dataclasses import dataclass, field
from enum import Enum, auto


class CameraMode(Enum):
    FEED = auto()  # Single item view (static or rapid cycling)
    SCROLL = auto()  # Smooth vertical scrolling (movie credits style)
    HORIZONTAL = auto()
    OMNI = auto()
    FLOATING = auto()
    BOUNCE = auto()
    RADIAL = auto()  # Polar coordinates (r, theta) for radial scanning


@dataclass
class CameraViewport:
    """Represents the visible viewport."""

    x: int
    y: int
    width: int
    height: int


@dataclass
class Camera:
    """Camera for viewport scrolling.

    The camera defines a visible viewport into a Canvas.
    It can be smaller than the canvas to allow scrolling,
    and supports zoom to scale the view.

    Attributes:
        x: Current horizontal offset (positive = scroll left)
        y: Current vertical offset (positive = scroll up)
        mode: Current camera mode
        speed: Base scroll speed
        zoom: Zoom factor (1.0 = 100%, 2.0 = 200% zoom out)
        canvas_width: Width of the canvas being viewed
        canvas_height: Height of the canvas being viewed
        custom_update: Optional custom update function
    """

    x: int = 0
    y: int = 0
    mode: CameraMode = CameraMode.FEED
    speed: float = 1.0
    zoom: float = 1.0
    canvas_width: int = 200  # Larger than viewport for scrolling
    canvas_height: int = 200
    custom_update: Callable[["Camera", float], None] | None = None
    _x_float: float = field(default=0.0, repr=False)
    _y_float: float = field(default=0.0, repr=False)
    _time: float = field(default=0.0, repr=False)

    @property
    def w(self) -> int:
        """Shorthand for viewport_width."""
        return self.viewport_width

    def set_speed(self, speed: float) -> None:
        """Set the camera scroll speed dynamically.

        This allows camera speed to be modulated during runtime
        via PipelineParams or directly.

        Args:
            speed: New speed value (0.0 = stopped, >0 = movement)
        """
        self.speed = max(0.0, speed)

    @property
    def h(self) -> int:
        """Shorthand for viewport_height."""
        return self.viewport_height

    @property
    def viewport_width(self) -> int:
        """Get the visible viewport width.

        This is the canvas width divided by zoom.
        """
        return max(1, int(self.canvas_width / self.zoom))

    @property
    def viewport_height(self) -> int:
        """Get the visible viewport height.

        This is the canvas height divided by zoom.
        """
        return max(1, int(self.canvas_height / self.zoom))

    def get_viewport(self, viewport_height: int | None = None) -> CameraViewport:
        """Get the current viewport bounds.

        Args:
            viewport_height: Optional viewport height to use instead of camera's viewport_height

        Returns:
            CameraViewport with position and size (clamped to canvas bounds)
        """
        vw = self.viewport_width
        vh = viewport_height if viewport_height is not None else self.viewport_height

        clamped_x = max(0, min(self.x, self.canvas_width - vw))
        clamped_y = max(0, min(self.y, self.canvas_height - vh))

        return CameraViewport(
            x=clamped_x,
            y=clamped_y,
            width=vw,
            height=vh,
        )

        return CameraViewport(
            x=clamped_x,
            y=clamped_y,
            width=vw,
            height=vh,
        )

    def set_zoom(self, zoom: float) -> None:
        """Set the zoom factor.

        Args:
            zoom: Zoom factor (1.0 = 100%, 2.0 = zoomed out 2x, 0.5 = zoomed in 2x)
        """
        self.zoom = max(0.1, min(10.0, zoom))

    def update(self, dt: float) -> None:
        """Update camera position based on mode.

        Args:
            dt: Delta time in seconds
        """
        self._time += dt

        if self.custom_update:
            self.custom_update(self, dt)
            return

        if self.mode == CameraMode.FEED:
            self._update_feed(dt)
        elif self.mode == CameraMode.SCROLL:
            self._update_scroll(dt)
        elif self.mode == CameraMode.HORIZONTAL:
            self._update_horizontal(dt)
        elif self.mode == CameraMode.OMNI:
            self._update_omni(dt)
        elif self.mode == CameraMode.FLOATING:
            self._update_floating(dt)
        elif self.mode == CameraMode.BOUNCE:
            self._update_bounce(dt)
        elif self.mode == CameraMode.RADIAL:
            self._update_radial(dt)

        # Bounce mode handles its own bounds checking
        if self.mode != CameraMode.BOUNCE:
            self._clamp_to_bounds()

    def _clamp_to_bounds(self) -> None:
        """Clamp camera position to stay within canvas bounds.

        Only clamps if the viewport is smaller than the canvas.
        If viewport equals canvas (no scrolling needed), allows any position
        for backwards compatibility with original behavior.
        """
        vw = self.viewport_width
        vh = self.viewport_height

        # Only clamp if there's room to scroll
        if vw < self.canvas_width:
            self.x = max(0, min(self.x, self.canvas_width - vw))
        if vh < self.canvas_height:
            self.y = max(0, min(self.y, self.canvas_height - vh))

    def _update_feed(self, dt: float) -> None:
        """Feed mode: rapid scrolling (1 row per frame at speed=1.0)."""
        self.y += int(self.speed * dt * 60)

    def _update_scroll(self, dt: float) -> None:
        """Scroll mode: smooth vertical scrolling with float accumulation."""
        self._y_float += self.speed * dt * 60
        self.y = int(self._y_float)

    def _update_horizontal(self, dt: float) -> None:
        self.x += int(self.speed * dt * 60)

    def _update_omni(self, dt: float) -> None:
        speed = self.speed * dt * 60
        self.y += int(speed)
        self.x += int(speed * 0.5)

    def _update_floating(self, dt: float) -> None:
        base = self.speed * 30
        self.y = int(math.sin(self._time * 2) * base)
        self.x = int(math.cos(self._time * 1.5) * base * 0.5)

    def _update_bounce(self, dt: float) -> None:
        """Bouncing DVD-style camera that bounces off canvas edges."""
        vw = self.viewport_width
        vh = self.viewport_height

        # Initialize direction if not set
        if not hasattr(self, "_bounce_dx"):
            self._bounce_dx = 1
            self._bounce_dy = 1

        # Calculate max positions
        max_x = max(0, self.canvas_width - vw)
        max_y = max(0, self.canvas_height - vh)

        # Move
        move_speed = self.speed * dt * 60

        # Bounce off edges - reverse direction when hitting bounds
        self.x += int(move_speed * self._bounce_dx)
        self.y += int(move_speed * self._bounce_dy)

        # Bounce horizontally
        if self.x <= 0:
            self.x = 0
            self._bounce_dx = 1
        elif self.x >= max_x:
            self.x = max_x
            self._bounce_dx = -1

        # Bounce vertically
        if self.y <= 0:
            self.y = 0
            self._bounce_dy = 1
        elif self.y >= max_y:
            self.y = max_y
            self._bounce_dy = -1

    def _update_radial(self, dt: float) -> None:
        """Radial camera mode: polar coordinate scrolling (r, theta).

        The camera rotates around the center of the canvas while optionally
        moving outward/inward along rays. This enables:
        - Radar sweep animations
        - Pendulum view oscillation
        - Spiral scanning motion

        Uses polar coordinates internally:
        - _r_float: radial distance from center (accumulates smoothly)
        - _theta_float: angle in radians (accumulates smoothly)
        - Updates x, y based on conversion from polar to Cartesian
        """
        # Initialize radial state if needed
        if not hasattr(self, "_r_float"):
            self._r_float = 0.0
            self._theta_float = 0.0

        # Update angular position (rotation around center)
        # Speed controls rotation rate
        theta_speed = self.speed * dt * 1.0  # radians per second
        self._theta_float += theta_speed

        # Update radial position (inward/outward from center)
        # Can be modulated by external sensor
        if hasattr(self, "_radial_input"):
            r_input = self._radial_input
        else:
            # Default: slow outward drift
            r_input = 0.0

        r_speed = self.speed * dt * 20.0  # pixels per second
        self._r_float += r_input + r_speed * 0.01

        # Clamp radial position to canvas bounds
        max_r = min(self.canvas_width, self.canvas_height) / 2
        self._r_float = max(0.0, min(self._r_float, max_r))

        # Convert polar to Cartesian, centered at canvas center
        center_x = self.canvas_width / 2
        center_y = self.canvas_height / 2

        self.x = int(center_x + self._r_float * math.cos(self._theta_float))
        self.y = int(center_y + self._r_float * math.sin(self._theta_float))

        # Clamp to canvas bounds
        self._clamp_to_bounds()

    def set_radial_input(self, value: float) -> None:
        """Set radial input for sensor-driven radius modulation.

        Args:
            value: Sensor value (0-1) that modulates radial distance
        """
        self._radial_input = value * 10.0  # Scale to reasonable pixel range

    def set_radial_angle(self, angle: float) -> None:
        """Set radial angle directly (for OSC integration).

        Args:
            angle: Angle in radians (0 to 2π)
        """
        self._theta_float = angle

    def reset(self) -> None:
        """Reset camera position and state."""
        self.x = 0
        self.y = 0
        self._time = 0.0
        self.zoom = 1.0
        # Reset bounce direction state
        if hasattr(self, "_bounce_dx"):
            self._bounce_dx = 1
            self._bounce_dy = 1
        # Reset radial state
        if hasattr(self, "_r_float"):
            self._r_float = 0.0
            self._theta_float = 0.0

    def set_canvas_size(self, width: int, height: int) -> None:
        """Set the canvas size and clamp position if needed.

        Args:
            width: New canvas width
            height: New canvas height
        """
        self.canvas_width = width
        self.canvas_height = height
        self._clamp_to_bounds()

    def apply(
        self, buffer: list[str], viewport_width: int, viewport_height: int | None = None
    ) -> list[str]:
        """Apply camera viewport to a text buffer.

        Slices the buffer based on camera position (x, y) and viewport dimensions.
        Handles ANSI escape codes correctly for colored/styled text.

        Args:
            buffer: List of strings representing lines of text
            viewport_width: Width of the visible viewport in characters
            viewport_height: Height of the visible viewport (overrides camera's viewport_height if provided)

        Returns:
            Sliced buffer containing only the visible lines and columns
        """
        from sideline.effects.legacy import vis_offset, vis_trunc

        if not buffer:
            return buffer

        # Get current viewport bounds (clamped to canvas size)
        viewport = self.get_viewport(viewport_height)

        # Use provided viewport_height if given, otherwise use camera's viewport
        vh = viewport_height if viewport_height is not None else viewport.height

        # Vertical slice: extract lines that fit in viewport height
        start_y = viewport.y
        end_y = min(viewport.y + vh, len(buffer))

        if start_y >= len(buffer):
            # Scrolled past end of buffer, return empty viewport
            return [""] * vh

        vertical_slice = buffer[start_y:end_y]

        # Horizontal slice: apply horizontal offset and truncate to width
        horizontal_slice = []
        for line in vertical_slice:
            # Apply horizontal offset (skip first x characters, handling ANSI)
            offset_line = vis_offset(line, viewport.x)
            # Truncate to viewport width (handling ANSI)
            truncated_line = vis_trunc(offset_line, viewport_width)

            # Pad line to full viewport width to prevent ghosting when panning
            # Skip padding for empty lines to preserve intentional blank lines
            import re

            visible_len = len(re.sub(r"\x1b\[[0-9;]*m", "", truncated_line))
            if visible_len < viewport_width and visible_len > 0:
                truncated_line += " " * (viewport_width - visible_len)

            horizontal_slice.append(truncated_line)

        # Pad with empty lines if needed to fill viewport height
        while len(horizontal_slice) < vh:
            horizontal_slice.append("")

        return horizontal_slice

    @classmethod
    def feed(cls, speed: float = 1.0) -> "Camera":
        """Create a feed camera (rapid single-item scrolling, 1 row/frame at speed=1.0)."""
        return cls(mode=CameraMode.FEED, speed=speed, canvas_height=200)

    @classmethod
    def scroll(cls, speed: float = 0.5) -> "Camera":
        """Create a smooth scrolling camera (movie credits style).

        Uses float accumulation for sub-integer speeds.
        Sets canvas_width=0 so it matches viewport_width for proper text wrapping.
        """
        return cls(
            mode=CameraMode.SCROLL, speed=speed, canvas_width=0, canvas_height=200
        )

    @classmethod
    def vertical(cls, speed: float = 1.0) -> "Camera":
        """Deprecated: Use feed() or scroll() instead."""
        return cls(mode=CameraMode.FEED, speed=speed, canvas_height=200)

    @classmethod
    def horizontal(cls, speed: float = 1.0) -> "Camera":
        """Create a horizontal scrolling camera."""
        return cls(mode=CameraMode.HORIZONTAL, speed=speed, canvas_width=200)

    @classmethod
    def omni(cls, speed: float = 1.0) -> "Camera":
        """Create an omnidirectional scrolling camera."""
        return cls(
            mode=CameraMode.OMNI, speed=speed, canvas_width=200, canvas_height=200
        )

    @classmethod
    def floating(cls, speed: float = 1.0) -> "Camera":
        """Create a floating/bobbing camera."""
        return cls(
            mode=CameraMode.FLOATING, speed=speed, canvas_width=200, canvas_height=200
        )

    @classmethod
    def bounce(cls, speed: float = 1.0) -> "Camera":
        """Create a bouncing DVD-style camera that bounces off canvas edges."""
        return cls(
            mode=CameraMode.BOUNCE, speed=speed, canvas_width=200, canvas_height=200
        )

    @classmethod
    def radial(cls, speed: float = 1.0) -> "Camera":
        """Create a radial camera (polar coordinate scanning).

        The camera rotates around the center of the canvas with smooth angular motion.
        Enables radar sweep, pendulum view, and spiral scanning animations.

        Args:
            speed: Rotation speed (higher = faster rotation)

        Returns:
            Camera configured for radial polar coordinate scanning
        """
        cam = cls(
            mode=CameraMode.RADIAL, speed=speed, canvas_width=200, canvas_height=200
        )
        # Initialize radial state
        cam._r_float = 0.0
        cam._theta_float = 0.0
        return cam

    @classmethod
    def custom(cls, update_fn: Callable[["Camera", float], None]) -> "Camera":
        """Create a camera with custom update function."""
        return cls(custom_update=update_fn)
