from __future__ import annotations
from math import cos, sin, sqrt
from numbers import Real
from typing import Iterable

from ariamath.errors import DimensionalityError


class Vector:
    """An N-th dimensional immutable vector"""
    def __init__(self, *components: float):
        self.components = tuple(components)

    @classmethod
    def from_iter(cls, components: Iterable[float]):
        return cls(*components)

    @property
    def dimensions(self): return len(self.components)

    @property
    def length(self) -> float:
        return sqrt(sum(i * i for i in self))

    def dot(self, other: Vector) -> float:
        """Return a dot product with another `Vector`."""
        self._assert_validity(other)
        return sum(a * b for a, b in zip(self, other))

    def normalized(self) -> Vector:
        """Normalize the Vector into a Unit Vector"""
        return type(self).from_iter(map(lambda c: c / self.length, self))

    def _assert_validity(self, other: Vector) -> None:
        if self.dimensions != other.dimensions:
            raise DimensionalityError("Vectors of different amount of components can't interact.")

    def __add__(self, other: Vector):
        if isinstance(other, Vector):
            self._assert_validity(other)
            return type(self).from_iter(a + b for a, b in zip(self, other))
        return NotImplemented

    def __radd__(self, other: Vector):
        return self.__add__(other)

    def __sub__(self, other: Vector):
        if isinstance(other, Vector):
            self._assert_validity(other)
            return type(self).from_iter(a - b for a, b in zip(self, other))
        return NotImplemented

    def __rsub__(self, other: Vector):
        return self.__sub__(other)

    def __mul__(self, scalar: float):
        if isinstance(scalar, Real):
            return type(self).from_iter(c * scalar for c in self)
        return NotImplemented

    def __rmul__(self, scalar: float):
        return self.__mul__(scalar)

    def __matmul__(self, other: Vector):
        return self.dot(other)

    def __rmatmul__(self, other: Vector):
        return self.dot(other)

    def __len__(self) -> int: return self.dimensions

    def __iter__(self): return iter(self.components)

    def __getitem__(self, i: int): return self.components[i]

    def __str__(self):
        return f"<{', '.join(map(str, self.components))}>"

    def __repr__(self):
       return f"Vector({', '.join(map(str, self.components))})"

    def __abs__(self): return self.length

    def __eq__(self, value: object) -> bool:
        if type(value) is not Vector:
            return NotImplemented
        for a, b in zip(self.components, value.components):
            if a != b:
                return False
        return True


class Vector2D(Vector):
    def __init__(self, x: float, y: float, *args, **kwargs):
        super().__init__(x, y)

    @classmethod
    def from_named(cls, coordinates: dict[str, float]):
        cls(**coordinates)

    @property
    def x(self): return self[0]

    @property
    def y(self): return self[1]

    def asdict(self):
        return {"x": self.x, "y": self.y}

    def rotated(self, angle: float):
        """Rotate a `Vector2D` by `angle` radians."""
        return Vector2D(self.x * cos(angle), self.y * sin(angle))

    def __repr__(self):
        return f"Vector2D(x={self.x}, y={self.y})"


class Vector3D(Vector):
    def __init__(self, x: float, y: float, z: float, *args, **kwargs):
        super().__init__(x, y, z)

    @classmethod
    def from_named(cls, coordinates: dict[str, float]):
        return cls(**coordinates)

    @classmethod
    def from_vec2(cls, vector: Vector2D, z: float = 0):
        return cls(*vector, z=z)

    @property
    def x(self): return self[0]

    @property
    def y(self): return self[1]

    @property
    def z(self): return self[2]

    def cross(self, other: Vector3D) -> Vector3D:
        """Get a cross product with another `Vector3D`."""
        if not isinstance(other, Vector3D):
            return NotImplemented
        a, b, c = self
        d, e, f = other
        return Vector3D(
            b*f - c*e,
            c*d - a*f,
            a*e - b*d
        )

    def asdict(self):
        return {"x": self.x, "y": self.y, "z": self.z}

    def __repr__(self):
        return f"Vector3D(x={self.x}, y={self.y}, z={self.z})"
