from typing import Self
import math as m

type all_real = int | float
type all_quad = int | float | complex | Quaternion


class Quaternion:
    """A four dimensional number system implemented in python, with all basic operations supported.
    You can instantiate this class by doing `Quaternion(1,2,3,4)` (this example will generate a quaternion with a bolded I part of 1, and so on.)
    The equation format that this number system can sometimes be put in is a + bi + cj + dk.

    ## Attributes:
        `big_i`: `[a,0,0,0]` = In a mathematical definition, this should work the same as multiplying a by one, but to keep the `quaternion` behavior, it's defined as a quaternion with the a part being a.
        `i`: `[0,b,0,0]` The value of b put in the i part of a quaternion.
        `j`: `[0,0,c,0]` The value of c put into the j part of a quaternion.
        `k`: `[0,0,0,d]` The value of d put into the k part of a quaternion.
        `i_im, j_im, k_im`: `complex`. All of these are equivalent to 1j (or just j/i), and have a real part of zero, for the "equation" representation of a quaternion.
        `coord`: `(a,b,c,d)`. The quaternion represented as an ordered pair, being able to be put on a graph. This is equivalent to what `__repr__()` returns.

    ## Behaviors:

    `__repr__`: (format representation) Returns a string in the form of `(a,b,c,d)`. \n
    `__str__`: (string conversion) Returns a string in the form of `a + bi + cj + dk`. \n
    `__eq__`: (= operator) Checks the self.coord attribute against another quaternion's self.coord attribute. \n
    `__add__`: (+ operator) Follows rules for adding with quaternions. It will simply add between both quaternions, by just adding in pairs between each one. A formula for this would be `(a+e) + i ( b+f) + j ( c+g) + k ( d+h)` with `a, b, c, d` being the first quaternion, and the other variables being the second one. \n
    `__int__`: (int conversion) Returns the self.big_i. \n
    `__mul__`: (* operator) Follows rules for multiplying with quaternions. It will simply follow the distributive property for each quaternion, as if it was put into parentheses, and multiplied. A formula for this would be `(a + bi + cj + dk)(e + fi + gj + hk)`.
    """

    def __init__(self, a: all_real, b: all_real, c: all_real, d: all_real):
        self.big_i: list[all_real] = [a, 0, 0, 0]
        self.i: list[all_real] = [0, b, 0, 0]
        self.j: list[all_real] = [0, 0, c, 0]
        self.k: list[all_real] = [0, 0, 0, d]
        self.i_im = 1j
        self.j_im = 1j
        self.k_im = 1j
        self._i = self.i[1]
        self._j = self.j[2]
        self._k = self.k[3]
        self._a = self.big_i[0]
        self.coord = (self._a, self._i, self._j, self._k)

    def __int__(self):
        return self.big_i[0]

    def __str__(self):
        return (
            f"{self.coord[0]} + {self.coord[1]}i + {self.coord[2]}j + {self.coord[3]}k"
        )

    def __repr__(self):
        return str(self.coord)

    def __eq__(self, value: Self):
        return self.coord == value.coord

    def __radd__(self, value: Self):
        value.big_i[0] += self.big_i[0]
        value.i[1] += self.i[1]
        value.j[2] += self.j[2]
        value.k[3] += self.k[3]
        value.coord = (
            value.coord[0] + self.coord[0],
            value.coord[1] + self.coord[1],
            value.coord[2] + self.coord[2],
            value.coord[3] + self.coord[3],
        )
        return value

    def __add__(self, value: Self):
        self.big_i[0] += value.big_i[0]
        self.i[1] += value.i[1]
        self.j[2] += value.j[2]
        self.k[3] += value.k[3]
        self.coord = (
            self.coord[0] + value.coord[0],
            self.coord[1] + value.coord[1],
            self.coord[2] + value.coord[2],
            self.coord[3] + value.coord[3],
        )
        return self

    def __rsub__(self, value: Self) -> Self:
        value.big_i[0] -= self.big_i[0]
        value.i[1] -= self.i[1]
        value.j[2] -= self.j[2]
        value.k[3] -= self.k[3]
        value.coord = (
            value.coord[0] - self.coord[0],
            value.coord[1] - self.coord[1],
            value.coord[2] - self.coord[2],
            value.coord[3] - self.coord[3],
        )
        return value

    def __sub__(self, value: Self):
        self.big_i[0] -= value.big_i[0]
        self.i[1] -= value.i[1]
        self.j[2] -= value.j[2]
        self.k[3] -= value.k[3]
        self.coord = (
            self.coord[0] - value.coord[0],
            self.coord[1] - value.coord[1],
            self.coord[2] - value.coord[2],
            self.coord[3] - value.coord[3],
        )
        return self

    def __mul__(self, number: Self) -> Self:
        """ixi = jxj = kxk = -1
        ixj = k,
        jxi = -k
        jxk = i,
        kxj = -i
        kxi = j,
        ixk = -j
        """
        # ty random medium article i found!
        a = (
            self._a * number._a
            - self._i * number._i
            - self._j * number._j
            - self._k * number._k
        )
        i = (
            self._a * number._i
            + self._i * number._a
            + self._j * number._k
            - self._k * number._j
        )
        j = (
            self._a * number._j
            - self._i * number._k
            + self._j * number._a
            + self._k * number._i
        )
        k = (
            self._a * number._k
            + self._i * number._j
            - self._j * number._i
            + self._k * number._a
        )
        self.big_i = [a, 0, 0, 0]
        self.i = [0, i, 0, 0]
        self.j = [0, 0, j, 0]
        self.k = [0, 0, 0, k]
        self._a = a
        self._i = i
        self._j = j
        self._k = k
        return self

    def __neg__(self) -> Self:
        old_a = self._a
        old_i = self._i
        old_j = self._j
        old_k = self._k
        self._a = -old_a
        self._i = -old_i
        self._j = -old_j
        self._k = -old_k
        self.big_i = [self._a, 0, 0, 0]
        self.i = [0, self._i, 0, 0]
        self.j = [0, 0, self._j, 0]
        self.k = [0, 0, self._k, 0]
        return self

    def __abs__(self) -> Self:
        self.big_i = [m.sqrt(self._a**2), 0, 0, 0]
        self.i = [0, m.sqrt(self._i**2), 0, 0]
        self.j = [0, 0, m.sqrt(self._j**2), 0]
        self.k = [0, 0, 0, m.sqrt(self._k**2)]
        self._a = self.big_i[0]
        self._i = self.i[1]
        self._j = self.j[2]
        self._k = self.k[3]
        return self

    def conjugate(self):
        """The conjugate of a quaternion is basically the negation of all its terms."""
        return Quaternion(-self._a, -self._i, -self._j, -self._k)

    def square_root(self):
        """Returns the square root of this quaternion, meaning all of its terms are square rooted."""
        return Quaternion(
            m.sqrt(self._a), m.sqrt(self._i), m.sqrt(self._j), m.sqrt(self._k)
        )

    def normalize(self):
        """The normalization of a quaternion (also called a *norm*), is defined as the square root of the product of the quaternion and it's conjugate."""
        old_a = self._a
        old_i = self._i
        old_j = self._j
        old_k = self._k
        self._a = old_a * -old_a
        self._i = old_i * -old_i
        self._j = old_j * -old_j
        self._k = old_k * -old_k
        self.big_i = [self._a, 0, 0, 0]
        self.i = [0, self._i, 0, 0]
        self.j = [0, 0, self._j, 0]
        self.k = [0, 0, self._k, 0]

        return self.square_root()
