from __future__ import annotations
from dataclasses import dataclass
from typing import Optional
from math import isclose, sqrt

from .point import Point


@dataclass(frozen=True)
class Line:
    """
    Line represented in general form: a*x + b*y + c = 0

    - Vertical line: a = 1, b = 0, c = -x0
    - Non-vertical line: b = -1, so y = m*x + c0 => m*x - y + c0 = 0
    """
    a: float
    b: float
    c: float

    # -------- classmethods for creation --------

    @classmethod
    def from_points(cls, p1: Point, p2: Point) -> "Line":
        """Create a line passing through points p1 and p2."""
        if isclose(p1.x, p2.x):
            # vertical line x = constant
            return cls(1.0, 0.0, -p1.x)
        # slope form
        m = (p2.y - p1.y) / (p2.x - p1.x)
        c0 = p1.y - m * p1.x
        return cls(m, -1.0, c0)

    @classmethod
    def from_point_slope(cls, p: Point, m: Optional[float]) -> "Line":
        """Create a line passing through p with slope m. If m=None => vertical."""
        if m is None:
            # vertical: x = p.x
            return cls(1.0, 0.0, -p.x)
        c0 = p.y - m * p.x
        return cls(m, -1.0, c0)

    # -------- properties --------

    @property
    def slope(self) -> Optional[float]:
        """Return slope m, or None for vertical line."""
        if isclose(self.b, 0.0):
            return None
        # a*x + b*y + c = 0 => y = (-a/b)x - c/b
        return -self.a / self.b

    @property
    def y_intercept(self) -> Optional[float]:
        """Return y-intercept (0, c), or None if line is parallel to y-axis."""
        if isclose(self.b, 0.0):
            return None
        return -self.c / self.b

    @property
    def x_intercept(self) -> Optional[float]:
        """Return x-intercept (c, 0), or None if line is parallel to x-axis."""
        if isclose(self.a, 0.0):
            return None
        return -self.c / self.a

    # -------- operations --------

    def contains(self, p: Point, *, tol: float = 1e-9) -> bool:
        """Check if point lies on line."""
        return isclose(self.a * p.x + self.b * p.y + self.c, 0.0, abs_tol=tol)

    def intersection(self, other: "Line") -> Optional[Point]:
        """Return intersection point with another line, or None if parallel."""
        D = self.a * other.b - other.a * self.b
        if isclose(D, 0.0):
            return None  # parallel or coincident

        Dx = -self.c * other.b + other.c * self.b
        Dy = -self.a * other.c + other.a * self.c
        x = Dx / D
        y = Dy / D
        return Point(x, y)

    def distance_to_point(self, p: Point) -> float:
        """Perpendicular distance from point to this line."""
        num = abs(self.a * p.x + self.b * p.y + self.c)
        den = sqrt(self.a * self.a + self.b * self.b)
        return num / den

    def parallel_through(self, p: Point) -> "Line":
        """Line parallel to this one, passing through point p."""
        # same a, b; adjust c
        c_new = -(self.a * p.x + self.b * p.y)
        return Line(self.a, self.b, c_new)

    def perpendicular_through(self, p: Point) -> "Line":
        """Line perpendicular to this line, passing through point p."""
        # for ax + by + c=0, normal vector is (a, b).
        # perpendicular line has direction same as normal of this line,
        # so its coefficients swapped with sign.
        a_perp = self.b
        b_perp = -self.a
        c_perp = -(a_perp * p.x + b_perp * p.y)
        return Line(a_perp, b_perp, c_perp)
