import math

class Point:
	'''
	Point - class to work with point in 2d space
	Containing parameters: 
	- 'x' coordinate in space
	- 'y' coordinate in space
	'''
	ROUND = 3

	#Initializer
	def __init__(self, x: float = 0, y: float = 0):
		self.x = x
		self.y = y

	#Getters

	def get_distance(self, point2: 'Point'):
		'''Distance between two points (by default - distance between point and coordinate system center)'''
		
		if not isinstance(point2, Point):
			point2 = Point(0, 0)

		dx = self.x - point2.x
		dy = self.y - point2.y
		return (dx**2 + dy**2)**(1/2)

	def get_horizontal_angle(self, point2: 'Point'):
		'''Angle (in radians modulo PI) between line by 2 points and horizontal axis (by default - angle between line connect coordinate system center and horizontal axis)'''
		
		if not isinstance(point2, Point):
			point2 = Point(0, 0)

		dx = self.x - point2.x
		dy = self.y - point2.y
		
		#To except division by zero, set default value for arctan(infty)
		if dx == 0:
			return math.pi/2

		return math.atan(dy/dx)%math.pi

	def get_second_point_from_angle_distance(self, angle: float, distance: float):
		'''
		Generate second point from first, distance and angle:
		- 'angle' - angle in radians of oriented line (between 0 and PI)
		- 'distance' - distance between points (can be negative to orientate it in negative direction)
		'''
		deltapoint = Point(
			round(distance*math.cos(angle%math.pi), 6), 
			round(distance*math.sin(angle%math.pi), 6)
		)
		return self+deltapoint

	def get_nearest_point(self, points: list):
		'''Function to get nearest point to this from list of points'''
		nearest = points[0]
		for point in points:
			if self.get_distance(point) < self.get_distance(nearest):
				nearest = point

		return nearest

	def get_scaled_point_to_factor(self, point: 'Point', scale: float = 1):
		'''Function to get given point with scaled distance on the same line'''
		if not isinstance(point, Point):
			point2 = Point(0, 0)

		return self + (point - self)*(scale/self.get_distance(point))

	def get_xy_lists(self):
		return [self.x], [self.y]

	def get_coordinates(self):
		return self.x, self.y

	def round(self, signs: int = 0):
		return Point(round(self.x, signs), round(self.y, signs))

	def int(self):
		return Point(int(self.x), int(self.y))

	#Other classic methods

	def __add__(self, point2: 'Point'):
		if not isinstance(point2, Point):
			point2 = Point(0, 0)

		return Point(self.x + point2.x, self.y + point2.y)

	def __mul__(self, number: float):
		return Point(self.x*number, self.y*number)

	def __radd__(self, point2: 'Point'):
		return self.__add__(point2)

	def __sub__(self, point2: 'Point'):
		return self.__add__(point2.__mul__(-1))

	def __rsub__(self, point2: 'Point'):
		return point2.__add__(self.__mul__(-1))

	def __truediv__(self, number: float):
		return self.__mul__(1/number)

	def __str__(self):
		x = self.x
		y = self.y
		if self.ROUND >= 0:
			x = round(x, self.ROUND)
			y = round(y, self.ROUND)

		return f"Point({x}, {y})"

	def __repr__(self):
		return self.__str__()

	def __iter__(self):
		'''Method for converting coordinates to tuples and lists'''
		for i in [self.x, self.y]:
			yield i

class Direction:
	'''
	Direction - class to work with line direction. Containing comparator of direction, and possibility to switch the direction 
	'''

	#Possible directions in terms of X and Y
	no = (0, 0)
	up = (0, 1)
	down = (0, -1)
	left = (-1, 0)
	right = (1, 0)

	top_left = (-1, 1)
	top_right = (1, 1)
	bottom_left = (-1, -1)
	bottom_right = (1, -1)

	def __init__(self, line: 'Line'):
		self.line = line
		self.direction = self.get_direction()

	def get_direction(self):
		delta = self.line.point2 - self.line.point1

		factor_x = 0
		factor_y = 0

		if delta.x != 0:
			factor_x = int(delta.x/abs(delta.x))
		if delta.x != 0:
			factor_x = int(delta.x/abs(delta.x))

		return (factor_x, factor_y)

	def __eq__(self, direction: 'Direction'):
		return self.get_direction() == direction.get_direction()

class Line:
	'''
	Line - class to work with lines and line sectors in 2d space
	Containing parameters: 
	- 'point1': first point in 2d space (order doesn't matter)
	- 'point2': second point in 2d space
	Also independent parameters:
	- 'angle': angle in radians between line and horizontal axis
	- 'distance': distance between two points
	'''

	#Initializer
	def __init__(self, 
			point1: Point = Point(0, 0), 
			point2: Point = Point(1, 1)
	):
		self.point1 = point1
		self.point2 = point2
		self.angle = Point.get_horizontal_angle(self.point1, self.point2)
		self.distance = round(Point.get_distance(self.point1, self.point2), 6)

	@staticmethod
	def from_angle_distance_point(
			angle: float, 
			distance: float, 
			point: Point = Point(0, 0)
	):
		'''
		Generate line from one point, angle and distance:
		- 'angle' - angle in radians of oriented line (between 0 and PI)
		- 'distance' - distance between points (can be negative to orientate it in negative direction)
		- 'point' - first point in 2d space
		'''
		if not isinstance(point, Point):
			point = Point(0, 0)

		point2 = point.get_second_point_from_angle_distance(angle, distance)
		return Line(point, point2)

	#Setters
	def reverse_direction(self):
		'''Reverse line direction'''
		self.point1, self.point2 = self.point2, self.point1

	#Getters

	def get_equation(self):
		'''
		Function to get line equation coefficients from available points
		Looks like: Ax + By + C = 0
		Returns LineEquation class
		'''
		return LineEquation(self)

	def get_boundaries(self):
		'''Function to get boundary points of line'''
		return self.point1, self.point2

	def get_points_by_distance_on_line(self, distance: float):
		'''Function to get points distant in the specific distance from boundaries, and located inside line'''
		point1 = self.point1.get_scaled_point_to_factor(self.point2, distance)
		point2 = self.point2.get_scaled_point_to_factor(self.point1, distance)
		return point1, point2

	def get_central_point(self):
		'''Function to get point at center between boundaries of the line'''
		return self.get_subdivision_points(2)[0]

	def get_subdivision_points(self, subdivisions_count: int = 1):
		'''Function to get subdivision points for line. Returns points of subdivision inside. Count of this points is one less then subdivisions count.'''
		if subdivisions_count <= 1:
			return []
		else:
			points = []
			for i in range(1, subdivisions_count):
				point = self.point1.get_scaled_point_to_factor(self.point2, i*self.distance/subdivisions_count)
				points.append(point)
			return points

	def get_parallel_line(self, point: Point = Point(0, 0), distance: float = 1):
		'''Function to generate parallel line sector to this at the specific central point with specific distance from center to boundaries'''

		if not isinstance(point, BasicElements.Point):
			point = BasicElements.Point(0, 0)

		point1 = point.get_second_point_from_angle_distance(self.angle, distance)
		point2 = point.get_second_point_from_angle_distance(self.angle, -distance)
		return Line(point1, point2)

	def get_distance(self):
		'''Function to get distance between boundary points'''
		return self.distance

	def get_angle(self):
		'''Function to get angle between line and horizontal axis'''
		return self.angle

	def is_point_on_line(self, point: Point = Point(0, 0)):
		'''
		Function to check is point (x, y) located on the line
		By default checks is line intersect center of coordinate system
		'''
		return self.get_equation(point) == 0

	def is_point_on_object(self, point: Point = Point(0, 0)):
		'''
		Function to check is point (x, y) located on the sector of line, defined by two boundary points
		'''
		xmin, xmax = min(self.point1.x, self.point2.x), max(self.point1.x, self.point2.x)
		ymin, ymax = min(self.point1.y, self.point2.y), max(self.point1.y, self.point2.y)
		
		xcond = (xmin <= point.x <= xmax)
		ycond = (ymin <= point.y <= ymax)

		return xcond and ycond and self.is_point_on_line(point)

	def is_lines_in_one_direction(self, line: 'Line'):
		return Direction(self) == Direction(line)

	def get_xy_lists(self):
		return [self.point1.x, self.point2.x], [self.point1.y, self.point2.y]

	#Other classic methods

	def __add__(self, point: Point):
		if not isinstance(point, Point):
			point = Point(0, 0)

		return Line(self.point1 + point, self.point2 + point)

	def __mul__(self, number: float):
		return Line(self.point1*number, self.point2*number)

	def __sub__(self, point: Point):
		return self.__add__(point.__mul__(-1))

	def __truediv__(self, number: float):
		return self.__mul__(1/number)

	def __str__(self):
		return f"""Line:
		Point 1: \t{self.point1}, 
		Point 2: \t{self.point2},
		Distance: \t{self.distance},
		Angle:  \t{self.angle}"""

	def __repr__(self):
		return f"Line[{self.point1}, {self.point2}]"

class Circle:
	'''
	Circle - class to work with circles in 2d space
	Containing parameters: 
	- 'center': central point of circle in 2d space
	- 'radius': radius of the circle
	'''

	#Initializer
	def __init__(self, 
			center: Point = Point(0, 0), 
			radius: float = 1
	):
		self.center = center
		self.radius = radius

	#Getters

	def get_equation(self):
		'''
		Function to get circle equation from available parameters
		Looks like: (x-x0)^2 + (y-y0)^2 - r^2 = 0
		Returns function f(Point(x, y)) = (x-x0)^2 + (y-y0)^2 - r^2
		'''
		return CircleEquation(self)

	def get_center(self):
		'''Function to get center point of the circle'''
		return self.center

	def get_radius(self):
		'''Function to get radius of the circle'''
		return self.radius

	def scale(self, scale_factor: float = 1):
		self.radius *= scale_factor

	def get_boundary_box(self, scale_factor: float = 1):
		delta = Point(self.radius, self.radius)*scale_factor
		return self.center - delta, self.center + delta 

	def is_point_inside(self, point: Point = Point(0, 0)):
		'''
		Function to get information - is point located inside circle
		'''
		return self.get_equation()(point) < 0

	def is_point_on_object(self, point: Point = Point(0, 0)):
		'''
		Function to get information - is point located on circle boundaries
		'''
		return self.get_equation()(point) == 0

	#Other classic methods

	def __add__(self, point: Point):
		if not isinstance(point, Point):
			point = Point(0, 0)

		return Circle(self.center + point, self.radius)

	def __mul__(self, number: float):
		return Circle(self.center*number, self.radius*number)

	def __sub__(self, point: Point):
		return self.__add__(point.__mul__(-1))

	def __truediv__(self, number: float):
		return self.__mul__(1/number)

	def __str__(self):
		return f"""Circle:
		Center: \t{self.center}, 
		Radius: \t{self.radius}"""

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


class LineEquation:
	'''
	LineEquation - class to work with line equations in 2d space
	Containing parameters: 
	- 'line': line in 2d space
	'''
	def __init__(self, 
			line: Line = Line(Point(0, 0), Point(1, 1))
	):
		self.line = line
		self.coefficients = self.generate_equation_coeffitients()
		self.equation = self.generate_equation()

	#Generators

	def generate_equation_coeffitients(self):
		'''
		Function to get line equation coefficients from available points
		Looks like: Ax + By + C = 0
		Returns list of coefficients - [A, B, C]
		'''
		deltas = self.line.point2 - self.line.point1
		A = deltas.y
		B = -deltas.x
		C = self.line.point1.y * deltas.x - self.line.point1.x * deltas.y
		return [A, B, C]

	def generate_equation(self):
		'''
		Function to generate line equation from available points
		Looks like: Ax + By + C = 0
		Returns function f(Point(x, y)) = Ax + By + C
		'''
		A, B, C = self.generate_equation_coeffitients()

		def function(point: Point = Point(0, 0)):
			'''Function of the line f(Point(x, y)) = Ax + By + C'''
			if not isinstance(point, Point):
				point = Point(0, 0)

			return A*point.x + B*point.y + C

		return function

	#Getters

	def get_equation(self):
		return self.equation

	def get_coefficients(self):
		return self.coefficients

	#Other classic methods:

	def __str__(self):
		return f"Line equation: {self.coefficients[0]} x + {self.coefficients[1]} y + {self.coefficients[2]} = 0"

	def __repr__(self):
		return self.__str__()

	def __call__(self, point: Point = Point(0, 0)):
		return self.equation(point)

class CircleEquation:
	'''
	CircleEquation - class to work with circle equations in 2d space
	Containing parameters: 
	- 'circle': circle in 2d space
	'''
	def __init__(self, circle: Circle = Circle(Point(0, 0), 1)):
		self.circle = circle
		self.coefficients = self.generate_equation_coeffitients()
		self.equation = self.generate_equation()

	#Generator

	def generate_equation_coeffitients(self):
		'''
		Function to generate circle equation coeffitients from available center and radius
		Looks like: (x-x0)^2 + (y-y0)^2 - r^2 = 0, but in terms of coefficients x^2 + y^2 + Ax + By + C = 0
		Returns list of coeffitients [A, B, C]
		For x^2 and y^2 there are always coefficients equals 1, only for linear terms
		'''
		A = -2*self.circle.center.x
		B = -2*self.circle.center.y
		C = self.circle.center.x**2 + self.circle.center.y**2 - self.circle.radius**2
		return [A, B, C]

	def generate_equation(self):
		'''
		Function to generate circle equation from available center and radius
		Looks like: (x-x0)^2 + (y-y0)^2 - r^2 = 0
		Returns function f(Point(x, y)) = (x-x0)^2 + (y-y0)^2 - r^2
		'''
		def function(point: Point = Point(0, 0)):
			'''Function of the circle f(Point(x, y)) = (x-x0)^2 + (y-y0)^2 - r^2'''
			if not isinstance(point, Point):
				point = Point(0, 0)

			deltas = self.point-self.circle.center
			return deltas.x**2 + deltas.y**2 - self.circle.radius**2

		return function

	#Getters

	def get_equation(self):
		return self.equation

	def get_coefficients(self):
		return self.coefficients

	#Other classic methods:

	def __str__(self):
		return f"Circle equation: (x - {self.circle.center.x})^2 + (y - {self.circle.center.y})^2 - {self.circle.radius}^2 = 0"

	def __repr__(self):
		return self.__str__()

	def __call__(self, point: Point = Point(0, 0)):
		return self.equation(point)
