from __future__ import annotations
from dataclasses import dataclass
from math import cos, sin, sqrt
from .point import Point
from .line import Line


@dataclass(frozen=True)
class Ellipse:
    """
    Standard ellipse centred at origin:

        x^2 / a^2 + y^2 / b^2 = 1,   with a >= b > 0

    Major axis along x-axis, minor along y-axis.
    """
    a: float
    b: float

    # ---------- basic geometry ----------

    @property
    def center(self) -> Point:
        return Point(0.0, 0.0)

    @property
    def major_axis_length(self) -> float:
        return 2.0 * self.a

    @property
    def minor_axis_length(self) -> float:
        return 2.0 * self.b

    @property
    def vertices(self) -> tuple[Point, Point]:
        """Endpoints of major axis: (±a, 0)."""
        return (Point(self.a, 0.0), Point(-self.a, 0.0))

    @property
    def co_vertices(self) -> tuple[Point, Point]:
        """Endpoints of minor axis: (0, ±b)."""
        return (Point(0.0, self.b), Point(0.0, -self.b))

    # ---------- eccentricity, foci, directrices, latus rectum ----------

    @property
    def eccentricity(self) -> float:
        """
        e = sqrt(1 - b^2 / a^2),     (a >= b)
        """
        return sqrt(1.0 - (self.b * self.b) / (self.a * self.a))

    @property
    def focus_points(self) -> tuple[Point, Point]:
        """
        Foci of ellipse: (±ae, 0)
        """
        e = self.eccentricity
        return (Point(self.a * e, 0.0), Point(-self.a * e, 0.0))

    @property
    def directrices(self) -> tuple[Line, Line]:
        """
        Directrices: x = ± a/e
        """
        e = self.eccentricity
        d = self.a / e
        # x = d  => 1*x + 0*y - d = 0
        return (Line(1.0, 0.0, -d), Line(1.0, 0.0, d))

    @property
    def latus_rectum_length(self) -> float:
        """
        Length of latus rectum: 2b^2 / a
        """
        return 2.0 * self.b * self.b / self.a

    @property
    def latus_rectum_endpoints(self) -> tuple[Point, Point, Point, Point]:
        """
        Endpoints of the two latus recta:

           Through (ae, 0):   (ae, ±b^2 / a)
           Through (-ae, 0):  (-ae, ±b^2 / a)
        """
        e = self.eccentricity
        ae = self.a * e
        y0 = self.b * self.b / self.a
        return (
            Point(ae, y0),
            Point(ae, -y0),
            Point(-ae, y0),
            Point(-ae, -y0),
        )

    # ---------- parametric form & membership ----------

    def point_from_angle(self, theta: float) -> Point:
        """
        Parametric form (θ in radians):

            x = a cos θ
            y = b sin θ
        """
        return Point(self.a * cos(theta), self.b * sin(theta))

    def contains(self, p: Point, *, tol: float = 1e-9) -> bool:
        """Check if a point lies on the ellipse."""
        val = (p.x * p.x) / (self.a * self.a) + (p.y * p.y) / (self.b * self.b)
        return abs(val - 1.0) <= tol

    # ---------- slopes, tangents & normals ----------

    def tangent_slope_at(self, p: Point) -> float:
        """
        Slope of tangent at point (x, y):

            x^2/a^2 + y^2/b^2 = 1
         => 2x/a^2 + 2y/b^2 * dy/dx = 0
         => dy/dx = - (b^2 x) / (a^2 y)
        """
        return -(self.b * self.b * p.x) / (self.a * self.a * p.y)

    def normal_slope_at(self, p: Point) -> float:
        """Slope of normal at point (x, y)."""
        m_tan = self.tangent_slope_at(p)
        return -1.0 / m_tan

    def tangent_line_at(self, p: Point) -> Line:
        """
        Tangent line at point p on the ellipse.

        Special cases:
            at (±a, 0)  ->  x = ±a
            at (0, ±b)  ->  y = ±b
        """
        x, y = p.x, p.y

        # handle vertices explicitly
        if abs(y) < 1e-12 and abs(abs(x) - self.a) < 1e-12:
            # x = ±a
            return Line(1.0, 0.0, -x)
        if abs(x) < 1e-12 and abs(abs(y) - self.b) < 1e-12:
            # y = ±b
            return Line(0.0, 1.0, -y)

        m = self.tangent_slope_at(p)
        if abs(m) > 1e15:
            return Line(1.0, 0.0, -x)
        c = p.y - m * p.x
        return Line(m, -1.0, c)

    def normal_line_at(self, p: Point) -> Line:
        """
        Normal line at point p on the ellipse.
        """
        x, y = p.x, p.y

        # if tangent is vertical => normal horizontal
        if abs(y) < 1e-12 and abs(abs(x) - self.a) < 1e-12:
            # tangent x = ±a, normal: y = 0 through (±a, 0)
            return Line(0.0, 1.0, 0.0)

        m = self.normal_slope_at(p)
        if abs(m) > 1e15:
            return Line(1.0, 0.0, -x)
        c = p.y - m * p.x
        return Line(m, -1.0, c)

    # ---------- equation helpers ----------

    def equation(self) -> str:
        """Human-readable standard equation string."""
        return f"(x^2)/({self.a**2}) + (y^2)/({self.b**2}) = 1"

    def general_equation_coeffs(self) -> tuple[float, float, float]:
        """
        Returns (A, B, C) such that:

            A x^2 + B y^2 = C

        From x^2/a^2 + y^2/b^2 = 1:

            b^2 x^2 + a^2 y^2 = a^2 b^2
        """
        A = self.b * self.b
        B = self.a * self.a
        C = self.a * self.a * self.b * self.b
        return (A, B, C)

    def __repr__(self) -> str:
        return (
            f"Ellipse(a={self.a}, b={self.b}, "
            f"e={self.eccentricity:.4f})"
        )
