from __future__ import annotations
import math
from typing import Tuple, Optional

class Line:
	def __init__(self, slope: float, y_at_xzero: float):
		"""Pour une ligne horizontale passer 0 en slope, et pour une verticale passer math.inf et le y_at_xzero sera ignoré"""
		self.slope = slope
		self.y_at_xzero = y_at_xzero
		self.x_if_vertical = 0

	def is_horizontal(self) -> bool:
		return self.slope == 0

	def is_vertical(self) -> bool:
		return self.slope == math.inf

	@classmethod
	def get_line_from_2_pos(cls, p1: Tuple[float, float], p2: Tuple[float, float]) -> Optional[Line]:
		# mêmes 2 points => impossible d'extraire une ligne
		if p1 == p2:
			return
		# même x => ligne verticale
		if p1[0] == p2[0]:
			line = Line(math.inf, math.nan)
			line.x_if_vertical = p1[0]
			return line
		# même y => ligne horizontale
		if p1[1] == p2[1]:
			return Line(0, p1[1])
		# cas général
		point_with_smallest_x = p1 if p1[0] < p2[0] else p2
		point_with_largest_x = p1 if point_with_smallest_x == p2 else p2
		slope = (point_with_largest_x[1] - point_with_smallest_x[1]) / (point_with_largest_x[0] - point_with_smallest_x[0])
		y_at_xzero = p1[1] - slope * p1[0]
		return Line(slope, y_at_xzero)

	def get_intersection_pos(self, other: Line) -> Optional[Tuple[float, float]]:
		"""Retourne None si parallèles et pas d'intersection, 0 si les deux lignes sont identiques, ou la position sinon"""
		# lignes parallèles
		if self.slope == other.slope:
			# identiques => infinité d'intersections
			if self.y_at_xzero == other.y_at_xzero:
				return 0
			# non identiques => aucune intersection
			else:
				return None
		# l'une des deux est verticale
		if self.slope == math.inf or other.slope == math.inf:
			ligne_verticale = self if self.slope == math.inf else other
			ligne_normale = self if ligne_verticale == other else other
			intersection_x = ligne_verticale.x_if_vertical
			intersection_y = ligne_normale.slope * intersection_x + ligne_normale.y_at_xzero
		# cas général
		else:
			intersection_x = (other.y_at_xzero - self.y_at_xzero) / (self.slope - other.slope)
			intersection_y = (self.slope * other.y_at_xzero - other.slope * self.y_at_xzero) / (self.slope - other.slope)
		return intersection_x, intersection_y


class Vector2:
	"""Don't change x and y manually!"""

	def __init__(self, x: float, y: float):
		self.x = x
		self.y = y
		self._length = math.nan
		self._angle_radians = math.nan

	def get_angle_radians(self) -> float:
		if math.isnan(self._angle_radians):
			self._angle_radians = math.acos(self.x / self.get_length())
			if self.y < 0:
				self._angle_radians = -self._angle_radians
		return self._angle_radians

	def get_length(self) -> float:
		if math.isnan(self._length):
			self._length = math.sqrt(self.x ** 2 + self.y ** 2)
		return self._length

	def normalize(self):
		length = self.get_length()
		self.x /= length
		self.y /= length

	def set_length(self, length):
		self.normalize()
		self.x *= length
		self.y *= length
		self._length = length

	def scalar_mult(self, scalar: float) -> Vector2:
		return Vector2(self.x * scalar, self.y * scalar)

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

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

	def cross_product(self, other: Vector2) -> float:
		return self.x * other.y - self.y * other.x

	def dot_product(self, other: Vector2) -> float:
		return self.x * other.x + self.y * other.y

	def set_angle_radians(self, angle_radians: float):
		length = self.get_length()
		self.x = math.cos(angle_radians) * length
		self.y = math.sin(angle_radians) * length

	def invert(self):
		self.x = -self.x
		self.y = -self.y