"""Geometric primitives for document layout.

Provides basic geometric types used in document positioning and layout.
All coordinates are in EMU (English Metric Units) unless otherwise specified.
"""

from __future__ import annotations

import math
from dataclasses import dataclass
from typing import Iterator, Sequence


@dataclass(frozen=True, slots=True)
class Point:
    """A 2D point in document coordinates (EMU)."""

    x: float
    y: float

    def __iter__(self) -> Iterator[float]:
        yield self.x
        yield self.y

    def __add__(self, other: Point) -> Point:
        return Point(self.x + other.x, self.y + other.y)

    def __sub__(self, other: Point) -> Point:
        return Point(self.x - other.x, self.y - other.y)

    def __mul__(self, scalar: float) -> Point:
        return Point(self.x * scalar, self.y * scalar)

    def __truediv__(self, scalar: float) -> Point:
        return Point(self.x / scalar, self.y / scalar)

    def distance_to(self, other: Point) -> float:
        """Calculate Euclidean distance to another point."""
        dx = other.x - self.x
        dy = other.y - self.y
        return math.sqrt(dx * dx + dy * dy)

    def midpoint(self, other: Point) -> Point:
        """Calculate midpoint between this point and another."""
        return Point((self.x + other.x) / 2, (self.y + other.y) / 2)

    def rotate(self, angle_deg: float, center: Point | None = None) -> Point:
        """Rotate point around a center (default: origin)."""
        cx, cy = (center.x, center.y) if center else (0, 0)
        angle_rad = math.radians(angle_deg)
        cos_a = math.cos(angle_rad)
        sin_a = math.sin(angle_rad)

        dx = self.x - cx
        dy = self.y - cy

        return Point(
            cx + dx * cos_a - dy * sin_a,
            cy + dx * sin_a + dy * cos_a,
        )

    @classmethod
    def origin(cls) -> Point:
        """Return the origin point (0, 0)."""
        return cls(0, 0)


@dataclass(frozen=True, slots=True)
class Size:
    """A 2D size (width, height) in EMU."""

    width: float
    height: float

    def __iter__(self) -> Iterator[float]:
        yield self.width
        yield self.height

    @property
    def area(self) -> float:
        """Calculate area."""
        return self.width * self.height

    @property
    def aspect_ratio(self) -> float:
        """Calculate aspect ratio (width / height)."""
        if self.height == 0:
            return float("inf")
        return self.width / self.height

    def scale(self, factor: float) -> Size:
        """Scale both dimensions by a factor."""
        return Size(self.width * factor, self.height * factor)

    def fit_within(self, max_size: Size) -> Size:
        """Scale to fit within max_size while preserving aspect ratio."""
        if self.width == 0 or self.height == 0:
            return self

        scale_x = max_size.width / self.width
        scale_y = max_size.height / self.height
        scale = min(scale_x, scale_y)

        return Size(self.width * scale, self.height * scale)


@dataclass(frozen=True, slots=True)
class Rect:
    """An axis-aligned rectangle."""

    x: float
    y: float
    width: float
    height: float

    @classmethod
    def from_points(cls, p1: Point, p2: Point) -> Rect:
        """Create rectangle from two corner points."""
        x = min(p1.x, p2.x)
        y = min(p1.y, p2.y)
        width = abs(p2.x - p1.x)
        height = abs(p2.y - p1.y)
        return cls(x, y, width, height)

    @classmethod
    def from_center(cls, center: Point, size: Size) -> Rect:
        """Create rectangle from center point and size."""
        return cls(
            center.x - size.width / 2,
            center.y - size.height / 2,
            size.width,
            size.height,
        )

    @property
    def left(self) -> float:
        return self.x

    @property
    def top(self) -> float:
        return self.y

    @property
    def right(self) -> float:
        return self.x + self.width

    @property
    def bottom(self) -> float:
        return self.y + self.height

    @property
    def center(self) -> Point:
        return Point(self.x + self.width / 2, self.y + self.height / 2)

    @property
    def size(self) -> Size:
        return Size(self.width, self.height)

    @property
    def top_left(self) -> Point:
        return Point(self.x, self.y)

    @property
    def top_right(self) -> Point:
        return Point(self.right, self.y)

    @property
    def bottom_left(self) -> Point:
        return Point(self.x, self.bottom)

    @property
    def bottom_right(self) -> Point:
        return Point(self.right, self.bottom)

    @property
    def area(self) -> float:
        return self.width * self.height

    def contains_point(self, point: Point) -> bool:
        """Check if a point is inside this rectangle."""
        return self.left <= point.x <= self.right and self.top <= point.y <= self.bottom

    def contains_rect(self, other: Rect) -> bool:
        """Check if another rectangle is fully inside this one."""
        return (
            self.left <= other.left
            and self.right >= other.right
            and self.top <= other.top
            and self.bottom >= other.bottom
        )

    def intersects(self, other: Rect) -> bool:
        """Check if this rectangle intersects with another."""
        return not (
            self.right < other.left
            or self.left > other.right
            or self.bottom < other.top
            or self.top > other.bottom
        )

    def intersection(self, other: Rect) -> Rect | None:
        """Return the intersection rectangle, or None if no intersection."""
        if not self.intersects(other):
            return None

        x = max(self.left, other.left)
        y = max(self.top, other.top)
        right = min(self.right, other.right)
        bottom = min(self.bottom, other.bottom)

        return Rect(x, y, right - x, bottom - y)

    def union(self, other: Rect) -> Rect:
        """Return the smallest rectangle containing both rectangles."""
        x = min(self.left, other.left)
        y = min(self.top, other.top)
        right = max(self.right, other.right)
        bottom = max(self.bottom, other.bottom)

        return Rect(x, y, right - x, bottom - y)

    def inset(self, dx: float, dy: float | None = None) -> Rect:
        """Return a rectangle inset by dx, dy (or dx for both if dy is None)."""
        if dy is None:
            dy = dx
        return Rect(
            self.x + dx,
            self.y + dy,
            self.width - 2 * dx,
            self.height - 2 * dy,
        )

    def expand(self, dx: float, dy: float | None = None) -> Rect:
        """Return a rectangle expanded by dx, dy."""
        if dy is None:
            dy = dx
        return self.inset(-dx, -dy)

    def translate(self, dx: float, dy: float) -> Rect:
        """Return a rectangle translated by (dx, dy)."""
        return Rect(self.x + dx, self.y + dy, self.width, self.height)


@dataclass(frozen=True, slots=True)
class Margin:
    """Document margins (top, right, bottom, left) in EMU."""

    top: float
    right: float
    bottom: float
    left: float

    @classmethod
    def uniform(cls, value: float) -> Margin:
        """Create uniform margins."""
        return cls(value, value, value, value)

    @classmethod
    def symmetric(cls, vertical: float, horizontal: float) -> Margin:
        """Create symmetric margins (top/bottom, left/right)."""
        return cls(vertical, horizontal, vertical, horizontal)

    @property
    def horizontal(self) -> float:
        """Total horizontal margin (left + right)."""
        return self.left + self.right

    @property
    def vertical(self) -> float:
        """Total vertical margin (top + bottom)."""
        return self.top + self.bottom

    def apply_to_rect(self, rect: Rect) -> Rect:
        """Return the inner rectangle after applying margins."""
        return Rect(
            rect.x + self.left,
            rect.y + self.top,
            rect.width - self.horizontal,
            rect.height - self.vertical,
        )


@dataclass(frozen=True, slots=True)
class Matrix2D:
    """2D affine transformation matrix.

    Represents a 3x3 matrix for 2D transformations:
    | a  c  e |
    | b  d  f |
    | 0  0  1 |

    Where (a, b, c, d) form the linear transformation and (e, f) is translation.
    Compatible with OOXML ST_Matrix and SVG transform matrices.
    """

    a: float = 1.0  # scale x / cos(rotation)
    b: float = 0.0  # skew y / sin(rotation)
    c: float = 0.0  # skew x / -sin(rotation)
    d: float = 1.0  # scale y / cos(rotation)
    e: float = 0.0  # translate x
    f: float = 0.0  # translate y

    @classmethod
    def identity(cls) -> Matrix2D:
        """Return the identity matrix."""
        return cls()

    @classmethod
    def translate(cls, tx: float, ty: float) -> Matrix2D:
        """Create a translation matrix."""
        return cls(1, 0, 0, 1, tx, ty)

    @classmethod
    def scale(cls, sx: float, sy: float | None = None) -> Matrix2D:
        """Create a scaling matrix. If sy is None, uniform scaling is used."""
        if sy is None:
            sy = sx
        return cls(sx, 0, 0, sy, 0, 0)

    @classmethod
    def rotate(cls, angle_deg: float, cx: float = 0, cy: float = 0) -> Matrix2D:
        """Create a rotation matrix around point (cx, cy)."""
        angle_rad = math.radians(angle_deg)
        cos_a = math.cos(angle_rad)
        sin_a = math.sin(angle_rad)

        # Rotation around origin
        if cx == 0 and cy == 0:
            return cls(cos_a, sin_a, -sin_a, cos_a, 0, 0)

        # Rotation around (cx, cy): translate -> rotate -> translate back
        return cls(
            cos_a,
            sin_a,
            -sin_a,
            cos_a,
            cx - cos_a * cx + sin_a * cy,
            cy - sin_a * cx - cos_a * cy,
        )

    @classmethod
    def skew_x(cls, angle_deg: float) -> Matrix2D:
        """Create a skew matrix along the X axis."""
        tan_a = math.tan(math.radians(angle_deg))
        return cls(1, 0, tan_a, 1, 0, 0)

    @classmethod
    def skew_y(cls, angle_deg: float) -> Matrix2D:
        """Create a skew matrix along the Y axis."""
        tan_a = math.tan(math.radians(angle_deg))
        return cls(1, tan_a, 0, 1, 0, 0)

    @classmethod
    def from_rect_to_rect(cls, src: Rect, dst: Rect) -> Matrix2D:
        """Create a matrix that maps src rectangle to dst rectangle."""
        if src.width == 0 or src.height == 0:
            return cls.identity()

        sx = dst.width / src.width
        sy = dst.height / src.height
        tx = dst.x - src.x * sx
        ty = dst.y - src.y * sy

        return cls(sx, 0, 0, sy, tx, ty)

    def __matmul__(self, other: Matrix2D) -> Matrix2D:
        """Matrix multiplication: self @ other (apply other first, then self)."""
        return Matrix2D(
            a=self.a * other.a + self.c * other.b,
            b=self.b * other.a + self.d * other.b,
            c=self.a * other.c + self.c * other.d,
            d=self.b * other.c + self.d * other.d,
            e=self.a * other.e + self.c * other.f + self.e,
            f=self.b * other.e + self.d * other.f + self.f,
        )

    def transform_point(self, point: Point) -> Point:
        """Apply this transformation to a point."""
        return Point(
            self.a * point.x + self.c * point.y + self.e,
            self.b * point.x + self.d * point.y + self.f,
        )

    def transform_points(self, points: Sequence[Point]) -> list[Point]:
        """Apply this transformation to multiple points."""
        return [self.transform_point(p) for p in points]

    def transform_size(self, size: Size) -> Size:
        """Apply the linear part of this transformation to a size (no translation)."""
        # Transform vectors (1, 0) and (0, 1) to get new basis
        new_width = math.sqrt(self.a * self.a + self.b * self.b) * size.width
        new_height = math.sqrt(self.c * self.c + self.d * self.d) * size.height
        return Size(new_width, new_height)

    @property
    def determinant(self) -> float:
        """Calculate the determinant of the linear part."""
        return self.a * self.d - self.b * self.c

    @property
    def is_invertible(self) -> bool:
        """Check if this matrix can be inverted."""
        return abs(self.determinant) > 1e-10

    def inverse(self) -> Matrix2D | None:
        """Return the inverse matrix, or None if not invertible."""
        det = self.determinant
        if abs(det) < 1e-10:
            return None

        inv_det = 1.0 / det
        return Matrix2D(
            a=self.d * inv_det,
            b=-self.b * inv_det,
            c=-self.c * inv_det,
            d=self.a * inv_det,
            e=(self.c * self.f - self.d * self.e) * inv_det,
            f=(self.b * self.e - self.a * self.f) * inv_det,
        )

    def then(self, other: Matrix2D) -> Matrix2D:
        """Chain transformations: apply self first, then other."""
        return other @ self

    def decompose(self) -> dict[str, float]:
        """Decompose matrix into translate, rotate, scale, skew components.

        Returns dict with keys: translate_x, translate_y, rotate, scale_x, scale_y, skew_x
        """
        # Extract translation
        translate_x = self.e
        translate_y = self.f

        # Extract scale
        scale_x = math.sqrt(self.a * self.a + self.b * self.b)
        scale_y = math.sqrt(self.c * self.c + self.d * self.d)

        # Check for negative scale (reflection)
        if self.determinant < 0:
            scale_x = -scale_x

        # Extract rotation (from normalized first column)
        rotate = math.degrees(math.atan2(self.b, self.a)) if scale_x != 0 else 0

        # Extract skew
        skew_x = 0.0
        if scale_y != 0:
            # Skew is the angle between rotated y-axis and actual y-axis
            skew_x = math.degrees(
                math.atan2(self.a * self.c + self.b * self.d, scale_x * scale_y)
            )

        return {
            "translate_x": translate_x,
            "translate_y": translate_y,
            "rotate": rotate,
            "scale_x": scale_x,
            "scale_y": scale_y,
            "skew_x": skew_x,
        }

    def to_ooxml_string(self) -> str:
        """Convert to OOXML matrix string format: 'a b c d e f'."""
        return f"{self.a} {self.b} {self.c} {self.d} {self.e} {self.f}"

    @classmethod
    def from_ooxml_string(cls, s: str) -> Matrix2D:
        """Parse from OOXML matrix string format: 'a b c d e f'."""
        parts = s.split()
        if len(parts) != 6:
            raise ValueError(f"Invalid matrix string: {s}")
        return cls(*map(float, parts))


def bounding_box(points: Sequence[Point]) -> Rect:
    """Calculate the bounding box of a sequence of points."""
    if not points:
        return Rect(0, 0, 0, 0)

    xs = [p.x for p in points]
    ys = [p.y for p in points]

    min_x, max_x = min(xs), max(xs)
    min_y, max_y = min(ys), max(ys)

    return Rect(min_x, min_y, max_x - min_x, max_y - min_y)


__all__ = [
    "Point",
    "Size",
    "Rect",
    "Margin",
    "Matrix2D",
    "bounding_box",
]
