"""
2D affine transformations using homogeneous coordinates.

Transforms are represented as 3x3 matrices for efficient composition:
    [[a, c, e],
     [b, d, f],
     [0, 0, 1]]

Where (a,b,c,d,e,f) match SVG transform parameters.
"""

from dataclasses import dataclass

import numpy as np
from numpy.typing import NDArray


@dataclass(frozen=True)
class Transform:
    """
    Immutable 2D affine transformation.

    Uses 3x3 homogeneous coordinate matrix for efficient composition.

    Attributes:
        matrix: 3x3 numpy array representing the transform
    """

    matrix: NDArray[np.float64]

    def __post_init__(self) -> None:
        """Validate matrix shape and bottom row."""
        if self.matrix.shape != (3, 3):
            raise ValueError(f"Transform matrix must be 3x3, got shape {self.matrix.shape}")

        # Verify bottom row is [0, 0, 1] for affine transform
        expected_bottom = np.array([0, 0, 1])
        if not np.allclose(self.matrix[2], expected_bottom):
            raise ValueError(f"Transform matrix bottom row must be [0, 0, 1], got {self.matrix[2]}")

    @classmethod
    def identity(cls) -> "Transform":
        """
        Create identity transform (no transformation).

        Returns:
            Identity transform

        Examples:
            >>> t = Transform.identity()
            >>> t.apply_to_point(5, 10)
            (5.0, 10.0)
        """
        return cls(matrix=np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]], dtype=np.float64))

    @classmethod
    def translate(cls, tx: float, ty: float) -> "Transform":
        """
        Create translation transform.

        Args:
            tx: Translation along X axis
            ty: Translation along Y axis

        Returns:
            Translation transform

        Examples:
            >>> t = Transform.translate(10, 20)
            >>> t.apply_to_point(5, 5)
            (15.0, 25.0)
        """
        return cls(matrix=np.array([[1, 0, tx], [0, 1, ty], [0, 0, 1]], dtype=np.float64))

    @classmethod
    def rotate(cls, angle: float, cx: float = 0, cy: float = 0) -> "Transform":
        """
        Create rotation transform around a point.

        Args:
            angle: Rotation angle in degrees (positive = counterclockwise)
            cx: X coordinate of rotation center (default: 0)
            cy: Y coordinate of rotation center (default: 0)

        Returns:
            Rotation transform

        Examples:
            >>> t = Transform.rotate(90)  # 90° counterclockwise
            >>> x, y = t.apply_to_point(1, 0)
            >>> abs(x) < 1e-10 and abs(y - 1) < 1e-10
            True
        """
        # Convert angle to radians
        angle_rad = np.deg2rad(angle)
        cos_a = np.cos(angle_rad)
        sin_a = np.sin(angle_rad)

        # Rotation matrix
        rotate_matrix = np.array(
            [[cos_a, -sin_a, 0], [sin_a, cos_a, 0], [0, 0, 1]], dtype=np.float64
        )

        # If rotating around a point, compose: translate to origin, rotate, translate back
        # Note: compose applies transforms right-to-left, so we build: translate_back.compose(rotate).compose(translate_to_origin)
        if cx != 0 or cy != 0:
            t1 = cls.translate(-cx, -cy)  # Translate center to origin
            t2 = cls(matrix=rotate_matrix)  # Rotate around origin
            t3 = cls.translate(cx, cy)  # Translate back
            return t3.compose(t2).compose(t1)  # Apply t1, then t2, then t3

        return cls(matrix=rotate_matrix)

    @classmethod
    def scale(cls, sx: float, sy: float | None = None) -> "Transform":
        """
        Create scaling transform.

        Args:
            sx: Scale factor along X axis
            sy: Scale factor along Y axis (defaults to sx for uniform scaling)

        Returns:
            Scaling transform

        Examples:
            >>> t = Transform.scale(2, 3)
            >>> t.apply_to_point(10, 10)
            (20.0, 30.0)
        """
        if sy is None:
            sy = sx

        return cls(matrix=np.array([[sx, 0, 0], [0, sy, 0], [0, 0, 1]], dtype=np.float64))

    @classmethod
    def skewX(cls, angle: float) -> "Transform":
        """
        Create skew transform along X axis.

        Args:
            angle: Skew angle in degrees

        Returns:
            SkewX transform

        Examples:
            >>> t = Transform.skewX(45)
            >>> t.apply_to_point(0, 10)
            (10.0, 10.0)
        """
        angle_rad = np.deg2rad(angle)
        tan_a = np.tan(angle_rad)

        return cls(matrix=np.array([[1, tan_a, 0], [0, 1, 0], [0, 0, 1]], dtype=np.float64))

    @classmethod
    def skewY(cls, angle: float) -> "Transform":
        """
        Create skew transform along Y axis.

        Args:
            angle: Skew angle in degrees

        Returns:
            SkewY transform

        Examples:
            >>> t = Transform.skewY(45)
            >>> t.apply_to_point(10, 0)
            (10.0, 10.0)
        """
        angle_rad = np.deg2rad(angle)
        tan_a = np.tan(angle_rad)

        return cls(matrix=np.array([[1, 0, 0], [tan_a, 1, 0], [0, 0, 1]], dtype=np.float64))

    @classmethod
    def from_matrix(cls, a: float, b: float, c: float, d: float, e: float, f: float) -> "Transform":
        """
        Create transform from SVG matrix parameters.

        SVG matrix(a,b,c,d,e,f) maps to:
            [[a, c, e],
             [b, d, f],
             [0, 0, 1]]

        Args:
            a: X scale / rotation component
            b: Y shear / rotation component
            c: X shear / rotation component
            d: Y scale / rotation component
            e: X translation
            f: Y translation

        Returns:
            Transform from matrix parameters

        Examples:
            >>> t = Transform.from_matrix(1, 0, 0, 1, 10, 20)  # Translation
            >>> t.apply_to_point(0, 0)
            (10.0, 20.0)
        """
        return cls(matrix=np.array([[a, c, e], [b, d, f], [0, 0, 1]], dtype=np.float64))

    def compose(self, other: "Transform") -> "Transform":
        """
        Compose this transform with another (matrix multiplication).

        The result applies `other` first, then `self`:
            result.apply(point) == self.apply(other.apply(point))

        Args:
            other: Transform to compose with

        Returns:
            Composed transform

        Examples:
            >>> t1 = Transform.translate(10, 0)
            >>> t2 = Transform.scale(2)
            >>> t = t1.compose(t2)
            >>> t.apply_to_point(5, 0)
            (20.0, 0.0)
        """
        # Matrix multiplication: self @ other (applies other first, then self)
        composed_matrix = self.matrix @ other.matrix
        return Transform(matrix=composed_matrix)

    def apply_to_point(self, x: float, y: float) -> tuple[float, float]:
        """
        Apply transform to a point.

        Args:
            x: X coordinate
            y: Y coordinate

        Returns:
            Transformed (x, y) coordinates

        Examples:
            >>> t = Transform.translate(10, 20)
            >>> t.apply_to_point(5, 5)
            (15.0, 25.0)
        """
        # Convert to homogeneous coordinates [x, y, 1]
        point = np.array([x, y, 1], dtype=np.float64)

        # Apply transformation
        transformed = self.matrix @ point

        # Return cartesian coordinates
        return (float(transformed[0]), float(transformed[1]))

    def get_scale(self) -> float:
        """
        Extract the average scale factor from this transform.

        For transforms with uniform scaling, this returns the scale factor.
        For non-uniform scaling, returns the average of X and Y scales.
        Useful for scaling stroke widths to match transformed coordinates.

        Returns:
            Average scale factor

        Examples:
            >>> t = Transform.scale(2, 2)
            >>> t.get_scale()
            2.0
            >>> t = Transform.scale(2, 4)
            >>> t.get_scale()
            3.0
        """
        # Extract scale factors from the transformation matrix
        # The scale factors are the lengths of the transformed basis vectors
        # For matrix [[a, c, e], [b, d, f], [0, 0, 1]]:
        # X-axis scale is sqrt(a^2 + b^2)
        # Y-axis scale is sqrt(c^2 + d^2)
        a, c = self.matrix[0, 0], self.matrix[0, 1]
        b, d = self.matrix[1, 0], self.matrix[1, 1]

        scale_x = np.sqrt(a * a + b * b)
        scale_y = np.sqrt(c * c + d * d)

        # Return average scale (for non-uniform scaling)
        return float((scale_x + scale_y) / 2.0)
