# ==================================================================================================================== #
#             _____           _ _               ____           _            _             _____ ____                   #
#  _ __  _   |_   _|__   ___ | (_)_ __   __ _  / ___|__ _ _ __| |_ ___  ___(_) __ _ _ __ |___ /|  _ \                  #
# | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` || |   / _` | '__| __/ _ \/ __| |/ _` | '_ \  |_ \| | | |                 #
# | |_) | |_| || | (_) | (_) | | | | | | (_| || |__| (_| | |  | ||  __/\__ \ | (_| | | | |___) | |_| |                 #
# | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____\__,_|_|   \__\___||___/_|\__,_|_| |_|____/|____/                  #
# |_|    |___/                          |___/                                                                          #
# ==================================================================================================================== #
# Authors:                                                                                                             #
#   Patrick Lehmann                                                                                                    #
#                                                                                                                      #
# License:                                                                                                             #
# ==================================================================================================================== #
# Copyright 2025-2026 Patrick Lehmann - Bötzingen, Germany                                                             #
#                                                                                                                      #
# Licensed under the Apache License, Version 2.0 (the "License");                                                      #
# you may not use this file except in compliance with the License.                                                     #
# You may obtain a copy of the License at                                                                              #
#                                                                                                                      #
#   http://www.apache.org/licenses/LICENSE-2.0                                                                         #
#                                                                                                                      #
# Unless required by applicable law or agreed to in writing, software                                                  #
# distributed under the License is distributed on an "AS IS" BASIS,                                                    #
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.                                             #
# See the License for the specific language governing permissions and                                                  #
# limitations under the License.                                                                                       #
#                                                                                                                      #
# SPDX-License-Identifier: Apache-2.0                                                                                  #
# ==================================================================================================================== #
#
"""An implementation of 3D cartesian data structures for Python."""

from math   import sqrt, acos
from typing import Union, Generic, Any, Tuple

from pyTooling.Decorators  import readonly, export
from pyTooling.MetaClasses import ExtendedType
from pyTooling.Common      import getFullyQualifiedName
from pyTooling.Cartesian2D import Coordinate


@export
class Point3D(Generic[Coordinate], metaclass=ExtendedType, slots=True):
	"""An implementation of a 3D cartesian point."""

	x: Coordinate  #: The x-direction coordinate.
	y: Coordinate  #: The y-direction coordinate.
	z: Coordinate  #: The z-direction coordinate.

	def __init__(self, x: Coordinate, y: Coordinate, z: Coordinate) -> None:
		"""
		Initializes a 3-dimensional point.

		:param x: X-coordinate.
		:param y: Y-coordinate.
		:param z: Z-coordinate.
		:raises TypeError: If x/y/z-coordinate is not of type integer or float.
		"""
		if not isinstance(x, (int, float)):
			ex = TypeError(f"Parameter 'x' is not of type integer or float.")
			ex.add_note(f"Got type '{getFullyQualifiedName(x)}'.")
			raise ex
		if not isinstance(y, (int, float)):
			ex = TypeError(f"Parameter 'y' is not of type integer or float.")
			ex.add_note(f"Got type '{getFullyQualifiedName(y)}'.")
			raise ex
		if not isinstance(z, (int, float)):
			ex = TypeError(f"Parameter 'z' is not of type integer or float.")
			ex.add_note(f"Got type '{getFullyQualifiedName(z)}'.")
			raise ex

		self.x = x
		self.y = y
		self.z = z

	def Copy(self) -> "Point3D[Coordinate]":  # TODO: Python 3.11: -> Self:
		"""
		Create a new 3D-point as a copy of this 3D point.

		:return: Copy of this 3D-point.

		.. seealso::

		   :meth:`+ operator <__add__>`
		     Create a new 3D-point moved by a positive 3D-offset.
		   :meth:`- operator <__sub__>`
		     Create a new 3D-point moved by a negative 3D-offset.
		"""
		return self.__class__(self.x, self.y, self.z)

	def ToTuple(self) -> Tuple[Coordinate, Coordinate, Coordinate]:
		"""
		Convert this 3D-Point to a simple 3-element tuple.

		:return: ``(x, y, z)`` tuple.
		"""
		return self.x, self.y, self.z

	def __add__(self, other: Any) -> "Point3D[Coordinate]":
		"""
		Adds a 3D-offset to this 3D-point and creates a new 3D-point.

		:param other:      A 3D-offset as :class:`Offset3D` or :class:`tuple`.
		:return:           A new 3D-point shifted by the 3D-offset.
		:raises TypeError: If parameter 'other' is not a :class:`Offset3D` or :class:`tuple`.
		"""
		if isinstance(other, Offset3D):
			return self.__class__(
				self.x + other.xOffset,
				self.y + other.yOffset,
				self.z + other.zOffset
			)
		elif isinstance(other, tuple):
			return self.__class__(
				self.x + other[0],
				self.y + other[1],
				self.z + other[2]
			)
		else:
			ex = TypeError(f"Parameter 'other' is not of type Offset3D or tuple.")
			ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
			raise ex

	def __iadd__(self, other: Any) -> "Point3D[Coordinate]":  # TODO: Python 3.11: -> Self:
		"""
		Adds a 3D-offset to this 3D-point (inplace).

		:param other:      A 3D-offset as :class:`Offset3D` or :class:`tuple`.
		:return:           This 3D-point.
		:raises TypeError: If parameter 'other' is not a :class:`Offset3D` or :class:`tuple`.
		"""
		if isinstance(other, Offset3D):
			self.x += other.xOffset
			self.y += other.yOffset
			self.z += other.zOffset
		elif isinstance(other, tuple):
			self.x += other[0]
			self.y += other[1]
			self.z += other[2]
		else:
			ex = TypeError(f"Parameter 'other' is not of type Offset3D or tuple.")
			ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
			raise ex

		return self

	def __sub__(self, other: Any) -> Union["Offset3D[Coordinate]", "Point3D[Coordinate]"]:
		"""
		Subtract two 3D-Points from each other and create a new 3D-offset.

		:param other:      A 3D-point as :class:`Point3D`.
		:return:           A new 3D-offset representing the distance between these two points.
		:raises TypeError: If parameter 'other' is not a :class:`Point3D`.
		"""
		if isinstance(other, Point3D):
			return Offset3D(
				self.x - other.x,
				self.y - other.y,
				self.z - other.z
			)
		else:
			ex = TypeError(f"Parameter 'other' is not of type Point3D.")
			ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
			raise ex

	def __isub__(self, other: Any) -> "Point3D[Coordinate]":  # TODO: Python 3.11: -> Self:
		"""
		Subtracts a 3D-offset to this 3D-point (inplace).

		:param other:      A 3D-offset as :class:`Offset3D` or :class:`tuple`.
		:return:           This 3D-point.
		:raises TypeError: If parameter 'other' is not a :class:`Offset3D` or :class:`tuple`.
		"""
		if isinstance(other, Offset3D):
			self.x -= other.xOffset
			self.y -= other.yOffset
			self.z -= other.zOffset
		elif isinstance(other, tuple):
			self.x -= other[0]
			self.y -= other[1]
			self.z -= other[2]
		else:
			ex = TypeError(f"Parameter 'other' is not of type Offset3D or tuple.")
			ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
			raise ex

		return self

	def __repr__(self) -> str:
		"""
		Returns the 3D point's string representation.

		:returns: The string representation of the 3D point.
		"""
		return f"Point3D({self.x}, {self.y}, {self.z})"

	def __str__(self) -> str:
		"""
		Returns the 3D point's string equivalent.

		:returns: The string equivalent of the 3D point.
		"""
		return f"({self.x}, {self.y}, {self.z})"


@export
class Origin3D(Point3D[Coordinate], Generic[Coordinate]):
	"""An implementation of a 3D cartesian origin."""

	def __init__(self) -> None:
		"""
		Initializes a 3-dimensional origin.
		"""
		super().__init__(0, 0, 0)

	def Copy(self) -> "Origin3D[Coordinate]":  # TODO: Python 3.11: -> Self:
		"""
		:raises RuntimeError: Because an origin can't be copied.
		"""
		raise RuntimeError(f"An origin can't be copied.")

	def __repr__(self) -> str:
		"""
		Returns the 3D origin's string representation.

		:returns: The string representation of the 3D origin.
		"""
		return f"Origin3D({self.x}, {self.y}, {self.z})"


@export
class Offset3D(Generic[Coordinate], metaclass=ExtendedType, slots=True):
	"""An implementation of a 3D cartesian offset."""

	xOffset: Coordinate  #: The x-direction offset
	yOffset: Coordinate  #: The y-direction offset
	zOffset: Coordinate  #: The z-direction offset

	def __init__(self, xOffset: Coordinate, yOffset: Coordinate, zOffset: Coordinate) -> None:
		"""
		Initializes a 3-dimensional offset.

		:param xOffset:    x-direction offset.
		:param yOffset:    y-direction offset.
		:param zOffset:    z-direction offset.
		:raises TypeError: If x/y/z-offset is not of type integer or float.
		"""
		if not isinstance(xOffset, (int, float)):
			ex = TypeError(f"Parameter 'xOffset' is not of type integer or float.")
			ex.add_note(f"Got type '{getFullyQualifiedName(xOffset)}'.")
			raise ex
		if not isinstance(yOffset, (int, float)):
			ex = TypeError(f"Parameter 'yOffset' is not of type integer or float.")
			ex.add_note(f"Got type '{getFullyQualifiedName(yOffset)}'.")
			raise ex
		if not isinstance(zOffset, (int, float)):
			ex = TypeError(f"Parameter 'zOffset' is not of type integer or float.")
			ex.add_note(f"Got type '{getFullyQualifiedName(zOffset)}'.")
			raise ex

		self.xOffset = xOffset
		self.yOffset = yOffset
		self.zOffset = zOffset

	def Copy(self) -> "Offset3D[Coordinate]":  # TODO: Python 3.11: -> Self:
		"""
		Create a new 3D-offset as a copy of this 3D-offset.

		:returns: Copy of this 3D-offset.

		.. seealso::

		   :meth:`+ operator <__add__>`
		     Create a new 3D-offset moved by a positive 3D-offset.
		   :meth:`- operator <__sub__>`
		     Create a new 3D-offset moved by a negative 3D-offset.
		"""
		return self.__class__(self.xOffset, self.yOffset, self.zOffset)

	def ToTuple(self) -> Tuple[Coordinate, Coordinate, Coordinate]:
		"""
		Convert this 3D-offset to a simple 3-element tuple.

		:returns: ``(x, y, z)`` tuple.
		"""
		return self.xOffset, self.yOffset, self.zOffset

	def __eq__(self, other) -> bool:
		"""
		Compare two 3D-offsets for equality.

		:param other:      Parameter to compare against.
		:returns:          ``True``, if both 3D-offsets are equal.
		:raises TypeError: If parameter ``other`` is not of type :class:`Offset3D` or :class:`tuple`.
		"""
		if isinstance(other, Offset3D):
			return self.xOffset == other.xOffset and self.yOffset == other.yOffset and self.zOffset == other.zOffset
		elif isinstance(other, tuple):
			return self.xOffset == other[0] and self.yOffset == other[1] and self.zOffset == other[2]
		else:
			ex = TypeError(f"Parameter 'other' is not of type Offset3D or tuple.")
			ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
			raise ex

	def __ne__(self, other) -> bool:
		"""
		Compare two 3D-offsets for inequality.

		:param other:      Parameter to compare against.
		:returns:          ``True``, if both 3D-offsets are unequal.
		:raises TypeError: If parameter ``other`` is not of type :class:`Offset3D` or :class:`tuple`.
		"""
		return not self.__eq__(other)

	def __neg__(self) -> "Offset3D[Coordinate]":
		"""
		Negate all components of this 3D-offset and create a new 3D-offset.

		:returns: 3D-offset with negated offset components.
		"""
		return self.__class__(
			-self.xOffset,
			-self.yOffset,
			-self.zOffset
		)

	def __add__(self, other: Any) -> "Offset3D[Coordinate]":
		"""
		Adds a 3D-offset to this 3D-offset and creates a new 3D-offset.

		:param other:      A 3D-offset as :class:`Offset3D` or :class:`tuple`.
		:returns:          A new 3D-offset extended by the 3D-offset.
		:raises TypeError: If parameter 'other' is not a :class:`Offset3D` or :class:`tuple`.
		"""
		if isinstance(other, Offset3D):
			return self.__class__(
				self.xOffset + other.xOffset,
				self.yOffset + other.yOffset,
				self.zOffset + other.zOffset
			)
		elif isinstance(other, tuple):
			return self.__class__(
				self.xOffset + other[0],
				self.yOffset + other[1],
				self.zOffset + other[2]
			)
		else:
			ex = TypeError(f"Parameter 'other' is not of type Offset3D or tuple.")
			ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
			raise ex

	def __iadd__(self, other: Any) -> "Offset3D[Coordinate]":  # TODO: Python 3.11: -> Self:
		"""
		Adds a 3D-offset to this 3D-offset (inplace).

		:param other:      A 3D-offset as :class:`Offset3D` or :class:`tuple`.
		:returns:          This 3D-point.
		:raises TypeError: If parameter 'other' is not a :class:`Offset3D` or :class:`tuple`.
		"""
		if isinstance(other, Offset3D):
			self.xOffset += other.xOffset
			self.yOffset += other.yOffset
			self.zOffset += other.zOffset
		elif isinstance(other, tuple):
			self.xOffset += other[0]
			self.yOffset += other[1]
			self.zOffset += other[2]
		else:
			ex = TypeError(f"Parameter 'other' is not of type Offset3D or tuple.")
			ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
			raise ex

		return self

	def __sub__(self, other: Any) -> "Offset3D[Coordinate]":
		"""
		Subtracts a 3D-offset from this 3D-offset and creates a new 3D-offset.

		:param other:      A 3D-offset as :class:`Offset3D` or :class:`tuple`.
		:returns:          A new 3D-offset reduced by the 3D-offset.
		:raises TypeError: If parameter 'other' is not a :class:`Offset3D` or :class:`tuple`.
		"""
		if isinstance(other, Offset3D):
			return self.__class__(
				self.xOffset - other.xOffset,
				self.yOffset - other.yOffset,
				self.zOffset - other.zOffset
			)
		elif isinstance(other, tuple):
			return self.__class__(
				self.xOffset - other[0],
				self.yOffset - other[1],
				self.zOffset - other[2]
			)
		else:
			ex = TypeError(f"Parameter 'other' is not of type Offset3D or tuple.")
			ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
			raise ex

	def __isub__(self, other: Any) -> "Offset3D[Coordinate]":  # TODO: Python 3.11: -> Self:
		"""
		Subtracts a 3D-offset from this 3D-offset (inplace).

		:param other:      A 3D-offset as :class:`Offset3D` or :class:`tuple`.
		:returns:          This 3D-point.
		:raises TypeError: If parameter 'other' is not a :class:`Offset3D` or :class:`tuple`.
		"""
		if isinstance(other, Offset3D):
			self.xOffset -= other.xOffset
			self.yOffset -= other.yOffset
			self.zOffset -= other.zOffset
		elif isinstance(other, tuple):
			self.xOffset -= other[0]
			self.yOffset -= other[1]
			self.zOffset -= other[2]
		else:
			ex = TypeError(f"Parameter 'other' is not of type Offset3D or tuple.")
			ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
			raise ex

		return self

	def __repr__(self) -> str:
		"""
		Returns the 3D offset's string representation.

		:returns: The string representation of the 3D offset.
		"""
		return f"Offset3D({self.xOffset}, {self.yOffset}, {self.zOffset})"

	def __str__(self) -> str:
		"""
		Returns the 3D offset's string equivalent.

		:returns: The string equivalent of the 3D offset.
		"""
		return f"({self.xOffset}, {self.yOffset}, {self.zOffset})"


@export
class Size3D(Generic[Coordinate], metaclass=ExtendedType, slots=True):
	"""An implementation of a 3D cartesian size."""

	width:  Coordinate  #: width in x-direction.
	height: Coordinate  #: height in y-direction.
	depth:  Coordinate  #: depth in z-direction.

	def __init__(self, width: Coordinate, height: Coordinate, depth: Coordinate) -> None:
		"""
		Initializes a 2-dimensional size.

		:param width:      width in x-direction.
		:param height:     height in y-direction.
		:param depth:      depth in z-direction.
		:raises TypeError: If width/height/depth is not of type integer or float.
		"""
		if not isinstance(width, (int, float)):
			ex = TypeError(f"Parameter 'width' is not of type integer or float.")
			ex.add_note(f"Got type '{getFullyQualifiedName(width)}'.")
			raise ex
		if not isinstance(height, (int, float)):
			ex = TypeError(f"Parameter 'height' is not of type integer or float.")
			ex.add_note(f"Got type '{getFullyQualifiedName(height)}'.")
			raise ex
		if not isinstance(depth, (int, float)):
			ex = TypeError(f"Parameter 'depth' is not of type integer or float.")
			ex.add_note(f"Got type '{getFullyQualifiedName(depth)}'.")
			raise ex

		self.width =  width
		self.height = height
		self.depth =  depth

	def Copy(self) -> "Size3D[Coordinate]":  # TODO: Python 3.11: -> Self:
		"""
		Create a new 3D-size as a copy of this 3D-size.

		:returns: Copy of this 3D-size.
		"""
		return self.__class__(self.width, self.height, self.depth)

	def ToTuple(self) -> Tuple[Coordinate, Coordinate, Coordinate]:
		"""
		Convert this 3D-size to a simple 3-element tuple.

		:return: ``(width, height, depth)`` tuple.
		"""
		return self.width, self.height, self.depth

	def __repr__(self) -> str:
		"""
		Returns the 3D size's string representation.

		:returns: The string representation of the 3D size.
		"""
		return f"Size3D({self.width}, {self.height}, {self.depth})"

	def __str__(self) -> str:
		"""
		Returns the 3D size's string equivalent.

		:returns: The string equivalent of the 3D size.
		"""
		return f"({self.width}, {self.height}, {self.depth})"


@export
class Segment3D(Generic[Coordinate], metaclass=ExtendedType, slots=True):
	"""An implementation of a 3D cartesian segment."""

	start: Point3D[Coordinate]  #: Start point of a segment.
	end:   Point3D[Coordinate]  #: End point of a segment.

	def __init__(self, start: Point3D[Coordinate], end: Point3D[Coordinate], copyPoints: bool = True) -> None:
		"""
		Initializes a 3-dimensional segment.

		:param start:      Start point of the segment.
		:param end:        End point of the segment.
		:raises TypeError: If start/end is not of type Point3D.
		"""
		if not isinstance(start, Point3D):
			ex = TypeError(f"Parameter 'start' is not of type Point3D.")
			ex.add_note(f"Got type '{getFullyQualifiedName(start)}'.")
			raise ex
		if not isinstance(end, Point3D):
			ex = TypeError(f"Parameter 'end' is not of type Point3D.")
			ex.add_note(f"Got type '{getFullyQualifiedName(end)}'.")
			raise ex

		self.start = start.Copy() if copyPoints else start
		self.end =   end.Copy()   if copyPoints else end


@export
class LineSegment3D(Segment3D[Coordinate], Generic[Coordinate]):
	"""An implementation of a 3D cartesian line segment."""

	@readonly
	def Length(self) -> float:
		"""
		Read-only property to return the Euclidean distance between start and end point.

		:return: Euclidean distance between start and end point
		"""
		return sqrt((self.end.x - self.start.x) ** 2 + (self.end.y - self.start.y) ** 2 + (self.end.z - self.start.z) ** 2)

	def AngleTo(self, other: "LineSegment3D[Coordinate]") -> float:
		vectorA = self.ToOffset()
		vectorB = other.ToOffset()
		scalarProductAB = vectorA.xOffset * vectorB.xOffset + vectorA.yOffset * vectorB.yOffset + vectorA.zOffset * vectorB.zOffset

		return acos(scalarProductAB / (abs(self.Length) * abs(other.Length)))

	def ToOffset(self) -> Offset3D[Coordinate]:
		"""
		Convert this 3D line segment to a 3D-offset.

		:return: 3D-offset as :class:`Offset3D`
		"""
		return self.end - self.start

	def ToTuple(self) -> Tuple[Tuple[Coordinate, Coordinate, Coordinate], Tuple[Coordinate, Coordinate, Coordinate]]:
		"""
		Convert this 3D line segment to a simple 2-element tuple of 3D-point tuples.

		:return: ``((x1, y1, z1), (x2, y2, z2))`` tuple.
		"""
		return self.start.ToTuple(), self.end.ToTuple()

	def __repr__(self) -> str:
		"""
		Returns the 3D line segment's string representation.

		:returns: The string representation of the 3D line segment.
		"""
		return f"LineSegment3D({self.start}, {self.end})"

	def __str__(self) -> str:
		"""
		Returns the 3D line segment's string equivalent.

		:returns: The string equivalent of the 3D line segment.
		"""
		return f"({self.start} → {self.end})"
