"""Camera subscriber frame utilities module.

This module provides frame processing utilities for the camera subscriber.
It is part of the subscriber refactoring - see subscriber.py for the main module.
"""

import time
from typing import Optional

import cv2
import numpy as np

from .image_processing_texts import Color


def convert_yuyv_to_bgr(frame_data: bytes, width: int, height: int) -> np.ndarray:
    """Convert YUYV frame data to BGR format.

    Args:
        frame_data: Raw YUYV bytes.
        width: Frame width in pixels.
        height: Frame height in pixels.

    Returns:
        Frame converted to BGR format (H, W, 3).
    """
    # Reshape raw bytes to YUYV format (H, W, 2)
    yuyv = np.frombuffer(frame_data, dtype=np.uint8).reshape((height, width, 2))
    # Convert to BGR
    return cv2.cvtColor(yuyv, cv2.COLOR_YUV2BGR_YUYV)


def scale_frame(frame: np.ndarray, display_width: int) -> np.ndarray:
    """Scale frame to display size maintaining aspect ratio.

    The frame is scaled to fit within display_width while maintaining
    the original aspect ratio.

    Args:
        frame: Input frame to scale.
        display_width: Target display width.

    Returns:
        Scaled frame.
    """
    height, width = frame.shape[:2]

    # Calculate new height to maintain aspect ratio
    if width != display_width:
        aspect_ratio = height / width
        new_width = display_width
        new_height = int(new_width * aspect_ratio)
        return cv2.resize(
            frame,
            (new_width, new_height),
        )
    return frame


def crop_frame(
    frame: np.ndarray,
    position: str,
    display_width: int,
    display_height: int,
) -> np.ndarray:
    """Crop a region from the specified position of the frame.

    Extracts a region from the frame based on the position parameter.
    If the frame is smaller than the target size, returns the original frame.

    Args:
        frame: Input frame to crop.
        position: Position to crop from - "center", "left", "right", "top", "bottom",
                 "top-left", "top-right", "bottom-left", "bottom-right".
        display_width: Target display width for the crop.
        display_height: Target display height for the crop.

    Returns:
        Cropped frame or original frame if too small.
    """
    height, width = frame.shape[:2]

    # If frame is smaller than target crop size, return original
    if width < display_width or height < display_height:
        return frame

    # Calculate crop coordinates based on position
    if position == "left":
        # Left-center: x=0, y=centered vertically
        x_start = 0
        y_start = (height - display_height) // 2
    elif position == "right":
        # Right-center: x=right edge, y=centered vertically
        x_start = width - display_width
        y_start = (height - display_height) // 2
    elif position == "top":
        # Center-top: x=centered horizontally, y=0
        x_start = (width - display_width) // 2
        y_start = 0
    elif position == "bottom":
        # Center-bottom: x=centered horizontally, y=bottom edge
        x_start = (width - display_width) // 2
        y_start = height - display_height
    elif position == "top-left":
        # Top-left corner
        x_start = 0
        y_start = 0
    elif position == "top-right":
        # Top-right corner
        x_start = width - display_width
        y_start = 0
    elif position == "bottom-left":
        # Bottom-left corner
        x_start = 0
        y_start = height - display_height
    elif position == "bottom-right":
        # Bottom-right corner
        x_start = width - display_width
        y_start = height - display_height
    else:  # center
        # Center of the frame
        x_start = (width - display_width) // 2
        y_start = (height - display_height) // 2

    # Crop and return the region
    return frame[y_start:y_start + display_height, x_start:x_start + display_width]


def create_no_signal_frame(
    display_width: int,
    display_height: int,
    connect_address: str,
) -> np.ndarray:
    """Create a blue 'no signal' frame with text.

    Args:
        display_width: Display width for the frame.
        display_height: Display height for the frame.
        connect_address: Address to display on the frame.

    Returns:
        BGR frame with blue background and "no signal" text.
    """
    # Create blue background (BGR format: blue=255, green=0, red=0)
    frame = np.zeros((display_height, display_width, 3), dtype=np.uint8)
    frame[:, :] = (255, 0, 0)  # Blue in BGR

    font = cv2.FONT_HERSHEY_SIMPLEX
    color = Color.YELLOW_BGR
    thickness = 2

    # Add "no signal" text in yellow (first line)
    text1 = "no signal"
    font_scale1 = 1.5
    (text1_width, text1_height), _ = cv2.getTextSize(text1, font, font_scale1, thickness)
    x1 = (display_width - text1_width) // 2
    y1 = (display_height // 2) - 10  # Slightly above center

    cv2.putText(frame, text1, (x1, y1), font, font_scale1, color, thickness, cv2.LINE_AA)

    # Add publisher address on second line
    text2 = connect_address
    font_scale2 = 0.7
    (text2_width, text2_height), _ = cv2.getTextSize(text2, font, font_scale2, thickness)
    x2 = (display_width - text2_width) // 2
    y2 = (display_height // 2) + text2_height + 15  # Below first line with spacing

    cv2.putText(frame, text2, (x2, y2), font, font_scale2, color, thickness, cv2.LINE_AA)

    return frame


class NoSignalManager:
    """Manages no-signal detection and frame generation.

    This class handles detection of signal loss and provides
    no-signal frames for display.
    """

    def __init__(
        self,
        display_width: int,
        display_height: int,
        connect_address: str,
        no_signal_timeout: float,
    ) -> None:
        """Initialize the no-signal manager.

        Args:
            display_width: Display width for no-signal frames.
            display_height: Display height for no-signal frames.
            connect_address: Address to display on no-signal frames.
            no_signal_timeout: Timeout in seconds before showing no-signal.
        """
        self.display_width = display_width
        self.display_height = display_height
        self.connect_address = connect_address
        self.no_signal_timeout = no_signal_timeout

        self._last_frame_time: Optional[float] = None
        self._no_signal_active = False
        self._no_signal_frame: Optional[np.ndarray] = None
        self._no_signal_start_time: Optional[float] = None
        self._subscriber_start_time: float = time.time()

    def update_frame_time(self) -> None:
        """Update the last frame time to current time."""
        self._last_frame_time = time.time()

    def check_no_signal(self) -> Optional[np.ndarray]:
        """Check if we should display 'no signal' frame.

        Returns:
            No signal frame if timeout exceeded, None otherwise.
        """
        # Determine the reference time for timeout calculation
        # Use last frame time if available, otherwise use subscriber start time
        if self._last_frame_time is not None:
            reference_time = self._last_frame_time
        else:
            reference_time = self._subscriber_start_time

        elapsed = time.time() - reference_time
        if elapsed >= self.no_signal_timeout:
            if not self._no_signal_active:
                self._no_signal_active = True
                self._no_signal_start_time = time.time()
                self._no_signal_frame = create_no_signal_frame(
                    self.display_width,
                    self.display_height,
                    self.connect_address,
                )
                if self._last_frame_time is not None:
                    print(f"\n[No signal detected - grace period {self.no_signal_timeout}s]")
                else:
                    print(f"\n[No signal from start - grace period {self.no_signal_timeout}s]")

            # Calculate how long signal has been lost
            signal_lost_duration = time.time() - self._no_signal_start_time
            hours = int(signal_lost_duration // 3600)
            minutes = int((signal_lost_duration % 3600) // 60)
            seconds = int(signal_lost_duration % 60)
            time_str = f"{hours:02d}:{minutes:02d}:{seconds:02d}"
            print(f"signal lost for {time_str}", end="\r")

            return self._no_signal_frame

        # Reset no signal state if we got a frame
        if self._no_signal_active:
            self._no_signal_active = False
            print("\n[Signal restored]")

        return None
