"""
Transition effects for FMUS-VID.

This module provides various transition effects for videos, including fades,
dissolves, wipes, and other common transitions.
"""

import numpy as np
from typing import List, Dict, Tuple, Optional, Union, Any, Callable
import cv2
from pathlib import Path
import logging

logger = logging.getLogger(__name__)

class Transition:
    """Base class for video transitions."""

    def __init__(self, duration: float = 1.0):
        """
        Initialize transition.

        Args:
            duration: Transition duration in seconds
        """
        self.duration = duration

    def apply(self, frame1: np.ndarray, frame2: np.ndarray,
             progress: float) -> np.ndarray:
        """
        Apply transition between two frames.

        Args:
            frame1: First frame (previous frame)
            frame2: Second frame (next frame)
            progress: Transition progress from 0.0 to 1.0

        Returns:
            Blended frame
        """
        raise NotImplementedError("Subclasses must implement apply()")

class FadeTransition(Transition):
    """Fade transition (fade out/in)."""

    def __init__(self, duration: float = 1.0, fade_to_black: bool = True):
        """
        Initialize fade transition.

        Args:
            duration: Transition duration in seconds
            fade_to_black: If True, fade through black; if False, direct crossfade
        """
        super().__init__(duration)
        self.fade_to_black = fade_to_black

    def apply(self, frame1: np.ndarray, frame2: np.ndarray,
             progress: float) -> np.ndarray:
        """Apply fade transition."""
        if self.fade_to_black:
            # Fade through black (fade out, then fade in)
            if progress < 0.5:
                # Fade out first frame to black
                p = 1.0 - (progress * 2)
                return (frame1 * p).astype(np.uint8)
            else:
                # Fade in second frame from black
                p = (progress - 0.5) * 2
                return (frame2 * p).astype(np.uint8)
        else:
            # Direct crossfade
            return cv2.addWeighted(frame1, 1.0 - progress, frame2, progress, 0)

class DissolveTransition(Transition):
    """Dissolve transition (crossfade with optional effects)."""

    def __init__(self, duration: float = 1.0, mode: str = 'simple'):
        """
        Initialize dissolve transition.

        Args:
            duration: Transition duration in seconds
            mode: Dissolve mode ('simple', 'zoom', 'blur')
        """
        super().__init__(duration)
        self.mode = mode

    def apply(self, frame1: np.ndarray, frame2: np.ndarray,
             progress: float) -> np.ndarray:
        """Apply dissolve transition."""
        h, w = frame1.shape[:2]

        if self.mode == 'simple':
            # Simple crossfade
            return cv2.addWeighted(frame1, 1.0 - progress, frame2, progress, 0)

        elif self.mode == 'zoom':
            # Zoom dissolve (first frame zooms out, second zooms in)
            scale1 = 1.0 + (progress * 0.2)  # Scale up to 120%
            scale2 = 0.8 + (progress * 0.2)  # Scale from 80% to 100%

            # Calculate new dimensions
            new_w1, new_h1 = int(w * scale1), int(h * scale1)
            new_w2, new_h2 = int(w * scale2), int(h * scale2)

            # Resize frames
            resized1 = cv2.resize(frame1, (new_w1, new_h1), interpolation=cv2.INTER_LINEAR)
            resized2 = cv2.resize(frame2, (new_w2, new_h2), interpolation=cv2.INTER_LINEAR)

            # Calculate crop offsets (center crop)
            x1, y1 = (new_w1 - w) // 2, (new_h1 - h) // 2
            x2, y2 = (new_w2 - w) // 2, (new_h2 - h) // 2

            # Crop to original size
            cropped1 = resized1[y1:y1+h, x1:x1+w]
            cropped2 = resized2[y2:y2+h, x2:x2+w]

            # Ensure both crops have the exact same size
            if cropped1.shape != cropped2.shape:
                # Resize to match dimensions
                if cropped1.shape[:2] != (h, w):
                    cropped1 = cv2.resize(cropped1, (w, h))
                if cropped2.shape[:2] != (h, w):
                    cropped2 = cv2.resize(cropped2, (w, h))

            # Crossfade the cropped frames
            return cv2.addWeighted(cropped1, 1.0 - progress, cropped2, progress, 0)

        elif self.mode == 'blur':
            # Blur dissolve (first frame gets increasingly blurry)
            blur_amount = int(progress * 30) * 2 + 1  # Odd number needed
            blurred1 = cv2.GaussianBlur(frame1, (blur_amount, blur_amount), 0)

            # Crossfade between blurred first frame and second frame
            return cv2.addWeighted(blurred1, 1.0 - progress, frame2, progress, 0)

        else:
            logger.warning(f"Unknown dissolve mode: {self.mode}, falling back to simple")
            return cv2.addWeighted(frame1, 1.0 - progress, frame2, progress, 0)

class WipeTransition(Transition):
    """Wipe transition (one frame wipes over the other)."""

    def __init__(self, duration: float = 1.0, direction: str = 'left-to-right'):
        """
        Initialize wipe transition.

        Args:
            duration: Transition duration in seconds
            direction: Wipe direction ('left-to-right', 'right-to-left',
                      'top-to-bottom', 'bottom-to-top')
        """
        super().__init__(duration)
        self.direction = direction

    def apply(self, frame1: np.ndarray, frame2: np.ndarray,
             progress: float) -> np.ndarray:
        """Apply wipe transition."""
        h, w = frame1.shape[:2]
        result = frame1.copy()

        if self.direction == 'left-to-right':
            # Calculate wipe position
            x = int(w * progress)
            result[:, :x] = frame2[:, :x]

        elif self.direction == 'right-to-left':
            # Calculate wipe position
            x = int(w * (1.0 - progress))
            result[:, x:] = frame2[:, x:]

        elif self.direction == 'top-to-bottom':
            # Calculate wipe position
            y = int(h * progress)
            result[:y, :] = frame2[:y, :]

        elif self.direction == 'bottom-to-top':
            # Calculate wipe position
            y = int(h * (1.0 - progress))
            result[y:, :] = frame2[y:, :]

        else:
            logger.warning(f"Unknown wipe direction: {self.direction}, falling back to left-to-right")
            x = int(w * progress)
            result[:, :x] = frame2[:, :x]

        return result

class SlideTransition(Transition):
    """Slide transition (frames slide in/out)."""

    def __init__(self, duration: float = 1.0, direction: str = 'left-to-right'):
        """
        Initialize slide transition.

        Args:
            duration: Transition duration in seconds
            direction: Slide direction ('left-to-right', 'right-to-left',
                      'top-to-bottom', 'bottom-to-top')
        """
        super().__init__(duration)
        self.direction = direction

    def apply(self, frame1: np.ndarray, frame2: np.ndarray,
             progress: float) -> np.ndarray:
        """Apply slide transition."""
        h, w = frame1.shape[:2]
        result = np.zeros_like(frame1)

        if self.direction == 'left-to-right':
            # First frame slides out to the right
            x1 = int(w * progress)
            # Second frame slides in from the left
            x2 = int(-w * (1.0 - progress))

            # Copy portions of frames that are visible
            if x1 < w:
                result[:, x1:] = frame1[:, :(w-x1)]
            if x2 < 0:
                result[:, :w+x2] = frame2[:, -x2:]
            else:
                result = frame2

        elif self.direction == 'right-to-left':
            # First frame slides out to the left
            x1 = int(-w * progress)
            # Second frame slides in from the right
            x2 = int(w * (1.0 - progress))

            # Copy portions of frames that are visible
            if x1 > -w:
                result[:, :w+x1] = frame1[:, -x1:]
            if x2 < w:
                result[:, x2:] = frame2[:, :(w-x2)]
            else:
                result = frame2

        elif self.direction == 'top-to-bottom':
            # First frame slides out to the bottom
            y1 = int(h * progress)
            # Second frame slides in from the top
            y2 = int(-h * (1.0 - progress))

            # Copy portions of frames that are visible
            if y1 < h:
                result[y1:, :] = frame1[:(h-y1), :]
            if y2 < 0:
                result[:h+y2, :] = frame2[-y2:, :]
            else:
                result = frame2

        elif self.direction == 'bottom-to-top':
            # First frame slides out to the top
            y1 = int(-h * progress)
            # Second frame slides in from the bottom
            y2 = int(h * (1.0 - progress))

            # Copy portions of frames that are visible
            if y1 > -h:
                result[:h+y1, :] = frame1[-y1:, :]
            if y2 < h:
                result[y2:, :] = frame2[:(h-y2), :]
            else:
                result = frame2

        else:
            logger.warning(f"Unknown slide direction: {self.direction}, falling back to left-to-right")
            return self.apply(frame1, frame2, progress)

        return result

class PixelateTransition(Transition):
    """Pixelate transition (image pixelates and forms new image)."""

    def __init__(self, duration: float = 1.0, max_pixel_size: int = 50):
        """
        Initialize pixelate transition.

        Args:
            duration: Transition duration in seconds
            max_pixel_size: Maximum pixel size during pixelation
        """
        super().__init__(duration)
        self.max_pixel_size = max_pixel_size

    def apply(self, frame1: np.ndarray, frame2: np.ndarray,
             progress: float) -> np.ndarray:
        """Apply pixelate transition."""
        h, w = frame1.shape[:2]

        if progress < 0.5:
            # First half: pixelate first frame
            p = progress * 2  # Scale to 0-1
            pixel_size = int(p * self.max_pixel_size) + 1

            # Skip if pixel_size is 1 (no pixelation)
            if pixel_size > 1:
                # Downscale and upscale to create pixelation
                small_h, small_w = h // pixel_size, w // pixel_size
                pixelated = cv2.resize(frame1, (small_w, small_h), interpolation=cv2.INTER_LINEAR)
                pixelated = cv2.resize(pixelated, (w, h), interpolation=cv2.INTER_NEAREST)
                return pixelated
            return frame1

        else:
            # Second half: unpixelate second frame
            p = (1.0 - progress) * 2  # Scale to 1-0
            pixel_size = int(p * self.max_pixel_size) + 1

            # Skip if pixel_size is 1 (no pixelation)
            if pixel_size > 1:
                # Downscale and upscale to create pixelation
                small_h, small_w = h // pixel_size, w // pixel_size
                pixelated = cv2.resize(frame2, (small_w, small_h), interpolation=cv2.INTER_LINEAR)
                pixelated = cv2.resize(pixelated, (w, h), interpolation=cv2.INTER_NEAREST)
                return pixelated
            return frame2

class ZoomTransition(Transition):
    """Zoom transition (zoom in on first frame, zoom out on second)."""

    def __init__(self, duration: float = 1.0, zoom_factor: float = 2.0):
        """
        Initialize zoom transition.

        Args:
            duration: Transition duration in seconds
            zoom_factor: Maximum zoom factor
        """
        super().__init__(duration)
        self.zoom_factor = zoom_factor

    def apply(self, frame1: np.ndarray, frame2: np.ndarray,
             progress: float) -> np.ndarray:
        """Apply zoom transition."""
        h, w = frame1.shape[:2]

        if progress < 0.5:
            # First half: zoom in on first frame
            p = progress * 2  # Scale to 0-1
            scale = 1.0 + (p * (self.zoom_factor - 1.0))

            # Calculate new dimensions
            new_w, new_h = int(w * scale), int(h * scale)

            # Resize frame
            resized = cv2.resize(frame1, (new_w, new_h), interpolation=cv2.INTER_LINEAR)

            # Calculate crop offsets (center crop)
            x, y = (new_w - w) // 2, (new_h - h) // 2

            # Crop to original size
            result = resized[y:y+h, x:x+w]
            return result

        else:
            # Second half: zoom out on second frame
            p = (progress - 0.5) * 2  # Scale to 0-1
            scale = self.zoom_factor - (p * (self.zoom_factor - 1.0))

            # Calculate new dimensions
            new_w, new_h = int(w * scale), int(h * scale)

            # Create canvas
            result = np.zeros_like(frame2)

            # Resize frame
            resized = cv2.resize(frame2, (new_w, new_h), interpolation=cv2.INTER_LINEAR)

            # Calculate paste offsets (center)
            x, y = (w - new_w) // 2, (h - new_h) // 2

            # Paste resized frame onto canvas
            # Ensure we don't go out of bounds
            y_end = min(y + new_h, h)
            x_end = min(x + new_w, w)
            y_start = max(0, y)
            x_start = max(0, x)

            # Adjust resized frame to fit the slice
            resized_h = y_end - y_start
            resized_w = x_end - x_start

            # Calculate source offsets to crop the resized frame
            src_y = max(0, -y)
            src_x = max(0, -x)

            result[y_start:y_end, x_start:x_end] = resized[src_y:src_y+resized_h, src_x:src_x+resized_w]
            return result

class RotateTransition(Transition):
    """Rotate transition (3D flip effect)."""

    def __init__(self, duration: float = 1.0, direction: str = 'horizontal'):
        """
        Initialize rotate transition.

        Args:
            duration: Transition duration in seconds
            direction: Rotation direction ('horizontal' or 'vertical')
        """
        super().__init__(duration)
        self.direction = direction

    def apply(self, frame1: np.ndarray, frame2: np.ndarray,
             progress: float) -> np.ndarray:
        """Apply rotate/flip transition."""
        h, w = frame1.shape[:2]

        if self.direction == 'horizontal':
            # Horizontal flip (like a page turning)
            if progress < 0.5:
                # First half: shrink first frame horizontally
                p = progress * 2  # Scale to 0-1
                new_w = int(w * (1.0 - p))

                # Skip if new width is too small
                if new_w < 1:
                    return frame2

                # Resize frame
                resized = cv2.resize(frame1, (new_w, h), interpolation=cv2.INTER_LINEAR)

                # Create canvas
                result = np.zeros_like(frame1)

                # Calculate paste offset (center)
                x = (w - new_w) // 2

                # Paste resized frame
                result[:, x:x+new_w] = resized
                return result

            else:
                # Second half: grow second frame horizontally
                p = (progress - 0.5) * 2  # Scale to 0-1
                new_w = int(w * p)

                # Skip if new width is too small
                if new_w < 1:
                    return frame1

                # Resize frame
                resized = cv2.resize(frame2, (new_w, h), interpolation=cv2.INTER_LINEAR)

                # Create canvas
                result = np.zeros_like(frame2)

                # Calculate paste offset (center)
                x = (w - new_w) // 2

                # Paste resized frame
                result[:, x:x+new_w] = resized
                return result

        elif self.direction == 'vertical':
            # Vertical flip
            if progress < 0.5:
                # First half: shrink first frame vertically
                p = progress * 2  # Scale to 0-1
                new_h = int(h * (1.0 - p))

                # Skip if new height is too small
                if new_h < 1:
                    return frame2

                # Resize frame
                resized = cv2.resize(frame1, (w, new_h), interpolation=cv2.INTER_LINEAR)

                # Create canvas
                result = np.zeros_like(frame1)

                # Calculate paste offset (center)
                y = (h - new_h) // 2

                # Paste resized frame
                result[y:y+new_h, :] = resized
                return result

            else:
                # Second half: grow second frame vertically
                p = (progress - 0.5) * 2  # Scale to 0-1
                new_h = int(h * p)

                # Skip if new height is too small
                if new_h < 1:
                    return frame1

                # Resize frame
                resized = cv2.resize(frame2, (w, new_h), interpolation=cv2.INTER_LINEAR)

                # Create canvas
                result = np.zeros_like(frame2)

                # Calculate paste offset (center)
                y = (h - new_h) // 2

                # Paste resized frame
                result[y:y+new_h, :] = resized
                return result

        else:
            logger.warning(f"Unknown rotation direction: {self.direction}, falling back to horizontal")
            return self.apply(frame1, frame2, progress)

def get_transition(name: str, **kwargs) -> Transition:
    """
    Factory function to create a transition by name.

    Args:
        name: Transition name
        **kwargs: Parameters for the transition

    Returns:
        Transition object
    """
    transitions = {
        'fade': FadeTransition,
        'dissolve': DissolveTransition,
        'wipe': WipeTransition,
        'slide': SlideTransition,
        'pixelate': PixelateTransition,
        'zoom': ZoomTransition,
        'rotate': RotateTransition
    }

    if name not in transitions:
        logger.warning(f"Unknown transition: {name}, falling back to dissolve")
        name = 'dissolve'

    return transitions[name](**kwargs)
