from __future__ import annotations
from dataclasses import dataclass
from math import sqrt, atan2, cos, sin, acos, pi
from typing import Optional, Tuple, List
from .point import Point
from .line import Line


@dataclass(frozen=True)
class Circle:
    """
    Circle with centre (h, k) and radius r.

    Standard equation:
        (x - h)^2 + (y - k)^2 = r^2
    """
    center: Point
    radius: float

    # ---------- membership ----------

    def contains(self, p: Point, *, tol: float = 1e-9) -> bool:
        return abs(self.center.distance_to(p) - self.radius) <= tol

    def is_inside(self, p: Point) -> bool:
        return self.center.distance_to(p) < self.radius

    def is_outside(self, p: Point) -> bool:
        return self.center.distance_to(p) > self.radius

    # ---------- equation ----------

    def equation(self) -> str:
        h, k = self.center.x, self.center.y
        return f"(x - {h})^2 + (y - {k})^2 = {self.radius**2}"

    def general_equation_coeffs(self) -> tuple[float, float, float]:
        """
        Returns (g, f, c) such that equation is:
            x^2 + y^2 + 2gx + 2fy + c = 0
        """
        h, k = self.center.x, self.center.y
        r = self.radius
        g = -h
        f = -k
        c = h*h + k*k - r*r
        return (g, f, c)

    # ---------- construction ----------

    @classmethod
    def from_diameter(cls, p1: Point, p2: Point) -> "Circle":
        center = p1.midpoint(p2)
        radius = p1.distance_to(p2) / 2.0
        return cls(center=center, radius=radius)

    @classmethod
    def from_three_points(cls, p1: Point, p2: Point, p3: Point) -> "Circle":
        x1, y1 = p1.x, p1.y
        x2, y2 = p2.x, p2.y
        x3, y3 = p3.x, p3.y

        D = 2 * (x1*(y2-y3) + x2*(y3-y1) + x3*(y1-y2))
        if abs(D) < 1e-12:
            raise ValueError("Points are collinear; circle is undefined.")

        ux = ((x1*x1+y1*y1)*(y2-y3)
            + (x2*x2+y2*y2)*(y3-y1)
            + (x3*x3+y3*y3)*(y1-y2)) / D

        uy = ((x1*x1+y1*y1)*(x3-x2)
            + (x2*x2+y2*y2)*(x1-x3)
            + (x3*x3+y3*y3)*(x2-x1)) / D

        center = Point(ux, uy)
        radius = center.distance_to(p1)
        return cls(center=center, radius=radius)

    # ---------- chord & arc ----------

    def chord_length(self, angle_rad: float) -> float:
        """
        Chord length subtending angle (in radians)
        at the center:
            L = 2R sin(θ/2)
        """
        return 2 * self.radius * sin(angle_rad / 2)

    def arc_length(self, angle_rad: float) -> float:
        """
        s = Rθ
        """
        return self.radius * angle_rad

    def sector_area(self, angle_rad: float) -> float:
        """
        area = 1/2 * R^2 * θ
        """
        return 0.5 * self.radius * self.radius * angle_rad

    def segment_area(self, angle_rad: float) -> float:
        """
        area of segment = area of sector - area of triangle
        """
        R = self.radius
        return 0.5 * R*R * (angle_rad - sin(angle_rad))

    # ---------- angle ----------

    def angle_at_center(self, p: Point, q: Point) -> float:
        """
        Computes ∠POQ where O is center
        """
        v1 = (p.x-self.center.x, p.y-self.center.y)
        v2 = (q.x-self.center.x, q.y-self.center.y)
        dot = v1[0]*v2[0] + v1[1]*v2[1]
        m1 = sqrt(v1[0]**2 + v1[1]**2)
        m2 = sqrt(v2[0]**2 + v2[1]**2)
        return acos(dot/(m1*m2))

    # ---------- intersection with line ----------

    def intersection_with_line(self, line: Line) -> List[Point]:
        """
        Returns list of intersection points with line:
          - 0 points (no intersection)
          - 1 point (tangent)
          - 2 points (secant)
        """
        h, k = self.center.x, self.center.y
        R = self.radius
        a, b, c = line.a, line.b, line.c

        d = abs(a*h + b*k + c) / sqrt(a*a + b*b)
        if d > R:
            return []  # no intersection
        if abs(d-R) < 1e-9:
            # tangent case — one solution
            return [self._projection(h, k, a, b, c)]

        # secant — two solutions
        pc = self._projection(h, k, a, b, c)
        offset = sqrt(R*R - d*d)

        dx = b / sqrt(a*a + b*b) * offset
        dy = -a / sqrt(a*a + b*b) * offset

        return [Point(pc.x + dx, pc.y + dy), Point(pc.x - dx, pc.y - dy)]

    def _projection(self, h, k, a, b, c) -> Point:
        t = (a*h + b*k + c) / (a*a + b*b)
        return Point(h - a*t, k - b*t)

    # ---------- intersection with another circle ----------

    def intersection_with_circle(self, other: "Circle") -> List[Point]:
        """
        Returns intersection points of two circles.
        """
        O1, O2 = self.center, other.center
        r1, r2 = self.radius, other.radius

        d = O1.distance_to(O2)
        if d > r1 + r2 or d < abs(r1 - r2):
            return []  # no intersection

        a = (r1*r1 - r2*r2 + d*d) / (2*d)
        h = sqrt(r1*r1 - a*a)

        x0 = O1.x + a*(O2.x-O1.x)/d
        y0 = O1.y + a*(O2.y-O1.y)/d

        rx = -(O2.y-O1.y)*(h/d)
        ry =  (O2.x-O1.x)*(h/d)

        p1 = Point(x0 + rx, y0 + ry)
        p2 = Point(x0 - rx, y0 - ry)

        if p1 == p2:
            return [p1]
        return [p1, p2]

    # ---------- tangents from point ----------

    def tangent_points_from(self, p: Point) -> List[Point]:
        """
        Returns the points where tangents from point p touch the circle
        """
        d = self.center.distance_to(p)
        if d < self.radius:
            return []  # inside — no tangents
        if abs(d - self.radius) < 1e-9:
            return [p]  # tangent degenerates

        a = self.radius*self.radius / d
        h = sqrt(self.radius*self.radius - a*a)

        M = Point(
            self.center.x + a*(p.x-self.center.x)/d,
            self.center.y + a*(p.y-self.center.y)/d,
        )

        rx = -(p.y-self.center.y)*(h/d)
        ry =  (p.x-self.center.x)*(h/d)

        return [Point(M.x+rx, M.y+ry), Point(M.x-rx, M.y-ry)]

    # ---------- miscellaneous ----------

    def random_point(self, angle_rad: float) -> Point:
        """Returns point on circumference at angle θ"""
        return Point(
            self.center.x + self.radius*cos(angle_rad),
            self.center.y + self.radius*sin(angle_rad),
        )

    def bounding_box(self) -> tuple[float, float, float, float]:
        """
        returns: xmin, xmax, ymin, ymax
        """
        cx, cy, r = self.center.x, self.center.y, self.radius
        return (cx-r, cx+r, cy-r, cy+r)

    def translate(self, dx: float, dy: float) -> "Circle":
        """Returns a translated circle"""
        return Circle(self.center.translate(dx, dy), self.radius)

    # ---------- representation ----------

    def __repr__(self):
        return f"Circle(center={self.center}, radius={self.radius:.4f})"
