"""This module holds classes relating to geometry."""

from __future__ import annotations

import importlib.util
import math

from enum import IntEnum
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from collections.abc import Iterator

    from typing_extensions import Self

if importlib.util.find_spec("pyglm"):
    from pyglm.glm import mat4, vec2, vec3, vec4
elif importlib.util.find_spec("glm"):
    from glm import mat4, vec2, vec3, vec4
elif not TYPE_CHECKING:
    vec2, vec3, vec4, mat4 = object, object, object, object


class Vector2(vec2):
    """Represents a 2 dimensional vector.

    Attributes:
    ----------
        x: The x component.
        y: The y component.
    """

    def __init__(
        self,
        x: float | Vector2 | Vector3 | Vector4 = 0.0,
        y: float | None = None,
    ):
        # Handle construction from other vector types
        if isinstance(x, (Vector2, Vector3, Vector4)):
            self.x: float = float(x.x)
            self.y: float = float(x.y)
            return
        # Handle scalar construction
        if y is None:
            y = x
        self.x = float(x)
        self.y = float(y)

    def __iter__(self) -> Iterator[float]:
        return iter((self.x, self.y))

    def __repr__(self) -> str:
        return f"{self.__class__.__name__}({self.x}, {self.y})"

    def __str__(self):
        """Returns the individual components separated by whitespace."""
        return f"{self.x} {self.y}"

    def __eq__(
        self,
        other,
    ):
        """Two Vector2 components are equal if their components are approximately the same."""
        if self is other:
            return True
        if not isinstance(other, Vector2):
            return NotImplemented  # type: ignore[no-any-return]

        isclose_x: bool = math.isclose(self.x, other.x)
        isclose_y: bool = math.isclose(self.y, other.y)
        return isclose_x and isclose_y

    def __add__(
        self,
        other,
    ):
        """Adds the components of two Vector2 objects."""
        if not isinstance(other, Vector2):
            return NotImplemented  # type: ignore[no-any-return]

        new: Self = self.__class__.from_vector2(self)
        new.x += other.x
        new.y += other.y
        return new

    def __radd__(
        self,
        other,
    ):
        """Right addition: scalar + Vector2 (returns Vector2)."""
        if isinstance(other, (int, float)):
            new = self.__class__.from_vector2(self)
            new.x = other + new.x
            new.y = other + new.y
            return new
        return NotImplemented  # type: ignore[no-any-return]

    def __sub__(
        self,
        other,
    ):
        """Subtracts the components of two Vector2 objects."""
        if not isinstance(other, Vector2):
            return NotImplemented  # type: ignore[no-any-return]

        new = self.__class__.from_vector2(self)
        new.x -= other.x
        new.y -= other.y
        return new

    def __rsub__(
        self,
        other,
    ):
        """Right subtraction: scalar - Vector2."""
        if isinstance(other, (int, float)):
            new = self.__class__.from_vector2(self)
            new.x = other - new.x
            new.y = other - new.y
            return new
        return NotImplemented  # type: ignore[no-any-return]

    def __mul__(
        self,
        other,
    ):
        """Multiplies the components by a scalar."""
        if isinstance(other, (int, float)):
            new = self.__class__.from_vector2(self)
            new.x *= other
            new.y *= other
            return new
        return NotImplemented  # type: ignore[no-any-return]

    def __rmul__(
        self,
        other,
    ):
        """Right multiplication: scalar * Vector2."""
        if isinstance(other, (int, float)):
            new = self.__class__.from_vector2(self)
            new.x *= other
            new.y *= other
            return new
        return NotImplemented  # type: ignore[no-any-return]

    def __truediv__(
        self,
        other,
    ):
        """Performs element-wise true division of vector by scalar value.

        Args:
        ----
            self: The vector to be divided
            other: The scalar value to divide elements by

        Returns:
        -------
            new: A new vector with elements divided by the scalar value

        Processing Logic:
        ----------------
            - Check if other is a scalar
            - Create a new vector from self
            - Divide x element by other
            - Divide y element by other
            - Return the new vector.
        """
        if isinstance(other, (int, float)):
            new = self.__class__.from_vector2(self)
            new.x /= other
            new.y /= other
            return new
        return NotImplemented  # type: ignore[no-any-return]

    def __rtruediv__(
        self,
        other,
    ):
        """Right division: scalar / Vector2."""
        if isinstance(other, (int, float)):
            new = self.__class__.from_vector2(self)
            new.x = other / new.x if new.x != 0 else 0.0
            new.y = other / new.y if new.y != 0 else 0.0
            return new
        return NotImplemented  # type: ignore[no-any-return]

    def __getitem__(
        self,
        item,
    ):
        if isinstance(item, int):
            if item == 0:
                return self.x
            if item == 1:
                return self.y
            raise KeyError
        return NotImplemented  # type: ignore[no-any-return]

    def __setitem__(
        self,
        key,
        value,
    ):
        if isinstance(key, int) and (isinstance(value, (float, int))):
            if key == 0:
                self.x = value
            elif key == 1:
                self.y = value
        return NotImplemented  # type: ignore[no-any-return]

    @classmethod
    def from_vector2(
        cls,
        other: Vector2,
    ) -> Self:
        """Returns a duplicate of the specified vector.

        Args:
        ----
            other: The vector to be duplicated.

        Returns:
        -------
            A new Vector2 object.
        """
        return cls(other.x, other.y)

    @classmethod
    def from_vector3(
        cls,
        other: Vector3,
    ) -> Self:
        """Returns a Vector2 object from the Vector3 object, discarding the Z-component.

        Args:
        ----
            other: The vector to be copied.

        Returns:
        -------
            A new Vector2 object.
        """
        return cls(other.x, other.y)

    @classmethod
    def from_vector4(
        cls,
        other: Vector4,
    ) -> Self:
        """Returns a Vector2 object from the Vector4 object, discarding the Z-component and W-components.

        Args:
        ----
            other: The vector to be copied.

        Returns:
        -------
            A new Vector2 object.
        """
        return cls(other.x, other.y)

    @classmethod
    def from_null(
        cls,
    ) -> Self:
        """Returns a new vector with a magnitude of zero.

        Returns:
        -------
            A new Vector2 instance.
        """
        return cls(0.0, 0.0)

    @classmethod
    def from_angle(
        cls,
        angle: float,
    ) -> Self:
        """Returns a unit vector based on the specified angle.

        Args:
        ----
            angle: The angle of the new vector in radians.

        Returns:
        -------
            A new Vector2 instance.
        """
        x = math.cos(angle)
        y = math.sin(angle)
        return cls(x, y).normal()

    def set_data(
        self,
        x: float,
        y: float,
    ):
        """Sets the components of the vector.

        Args:
        ----
            x: The new x component.
            y: The new y component.
        """
        self.x = x
        self.y = y

    def normalize(self):
        """Normalizes the vector so that the magnitude is equal to one while maintaining the same angle."""
        magnitude = self.magnitude()
        if magnitude == 0:
            self.x = 0
            self.y = 0
        else:
            self.x /= magnitude
            self.y /= magnitude

    def magnitude(self) -> float:
        """Returns the magnitude of the vector.

        Returns:
        -------
            The magnitude of the vector.
        """
        return math.sqrt(self.x**2 + self.y**2)

    def normal(self) -> Self:
        vec2 = self.__class__.from_vector2(self)
        vec2.normalize()
        return vec2

    def dot(
        self,
        other: Vector2,
    ) -> float:
        """Returns the dot product between the two specified vectors.

        Args:
        ----
            other: The other vector.

        Returns:
        -------
            The dot product.
        """
        a = self.x * other.x
        b = self.y * other.y
        return a + b

    def distance(
        self,
        other: Vector2,
    ) -> float:
        """Returns the distance between two vectors.

        Args:
        ----
            other: The other vector.

        Returns:
        -------
            The distance between the vectors.
        """
        a = (self.x - other.x) ** 2
        b = (self.y - other.y) ** 2
        return math.sqrt(a + b)

    def within(
        self,
        container: list,
    ) -> bool:
        """Checks to see if the same Vector2 object in located in the specified list.

        This differs from using the 'in' keyword as that will return True for Vector2 objects that have simular coordinates.

        Args:
        ----
            container: The list to search in.

        Returns:
        -------
            True if the Vector2 exists in the container.
        """
        return any(item is self for item in container)

    def angle(self) -> float:
        """Returns the angle of the vector.

        Returns:
        -------
            The angle of the vector in radians.
        """
        return math.atan2(self.y, self.x)

    def copy(self) -> Self:
        """Return a shallow copy of this vector."""
        return self.__class__.from_vector2(self)


class Vector3(vec3):
    """Represents a 3 dimensional vector.

    Attributes:
    ----------
        x: The x component.
        y: The y component.
        z: The z component.
    """

    def __init__(
        self,
        x: float | Vector2 | Vector3 | Vector4 = 0.0,
        y: float | None = None,
        z: float | None = None,
    ):
        # Handle construction from other vector types
        if isinstance(x, (Vector2, Vector3, Vector4)):
            self.x = float(x.x)
            self.y = float(x.y)
            self.z = float(getattr(x, "z", 0.0))
            return
        # Handle scalar construction
        if y is None:
            y = x
        if z is None:
            z = x
        self.x = float(x)
        self.y = float(y)
        self.z = float(z)

    def __iter__(self) -> Iterator[float]:
        return iter((self.x, self.y, self.z))

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

    def __str__(self):
        """Returns the individual components as a string separated by whitespace."""
        return f"{self.x} {self.y} {self.z}"

    def __eq__(
        self,
        other: Vector3 | object,
    ) -> bool:
        """Two Vector3 components are equal if their components are approximately the same."""
        if self is other:
            return True
        if not isinstance(other, Vector3):
            return NotImplemented  # type: ignore[no-any-return]

        isclose_x = math.isclose(self.x, other.x)
        isclose_y = math.isclose(self.y, other.y)
        isclose_z = math.isclose(self.z, other.z)
        return isclose_x and isclose_y and isclose_z

    def __hash__(self):
        return hash((round(self.x, 9), round(self.y, 9), round(self.z, 9)))

    def __add__(
        self,
        other: Vector3,
    ):
        """Adds the components of two Vector3 objects."""
        if not isinstance(other, Vector3):
            return NotImplemented  # type: ignore[no-any-return]

        new = self.__class__.from_vector3(self)
        new.x += other.x
        new.y += other.y
        new.z += other.z
        return new

    def __radd__(
        self,
        other,
    ):
        """Right addition: scalar + Vector3 (returns Vector3)."""
        if isinstance(other, (int, float)):
            new = self.__class__.from_vector3(self)
            new.x = other + new.x
            new.y = other + new.y
            new.z = other + new.z
            return new
        return NotImplemented  # type: ignore[no-any-return]

    def __sub__(
        self,
        other,
    ):
        """Subtracts the components of two Vector3 objects."""
        if not isinstance(other, Vector3):
            return NotImplemented  # type: ignore[no-any-return]

        new = self.__class__.from_vector3(self)
        new.x -= other.x
        new.y -= other.y
        new.z -= other.z
        return new

    def __rsub__(
        self,
        other,
    ):
        """Right subtraction: scalar - Vector3."""
        if isinstance(other, (int, float)):
            new = self.__class__.from_vector3(self)
            new.x = other - new.x
            new.y = other - new.y
            new.z = other - new.z
            return new
        return NotImplemented  # type: ignore[no-any-return]

    def __mul__(
        self,
        other: float | Vector3,
    ):
        """Multiplies the components by a scalar or element-wise with another Vector3."""
        if isinstance(other, (int, float)):
            new = self.__class__.from_vector3(self)
            new.x *= other
            new.y *= other
            new.z *= other
            return new
        if isinstance(other, Vector3):
            new = self.__class__.from_vector3(self)
            new.x *= other.x
            new.y *= other.y
            new.z *= other.z
            return new
        return NotImplemented  # type: ignore[no-any-return]

    def __rmul__(
        self,
        other: float,
    ):
        """Right multiplication: scalar * Vector3."""
        if isinstance(other, (int, float)):
            new = self.__class__.from_vector3(self)
            new.x *= other
            new.y *= other
            new.z *= other
            return new
        return NotImplemented  # type: ignore[no-any-return]

    def __truediv__(
        self,
        other: int | float,
    ):
        """Performs element-wise true division of vector by scalar value."""
        if isinstance(other, (int, float)):
            new = self.__class__.from_vector3(self)
            new.x /= other
            new.y /= other
            new.z /= other
            return new
        return NotImplemented  # type: ignore[no-any-return]

    def __rtruediv__(
        self,
        other,
    ):
        """Right division: scalar / Vector3."""
        if isinstance(other, (int, float)):
            new = self.__class__.from_vector3(self)
            new.x = other / new.x if new.x != 0 else 0.0
            new.y = other / new.y if new.y != 0 else 0.0
            new.z = other / new.z if new.z != 0 else 0.0
            return new
        return NotImplemented  # type: ignore[no-any-return]

    def __getitem__(
        self,
        item: int,
    ) -> float:
        if isinstance(item, int):
            if item == 0:
                return self.x
            if item == 1:
                return self.y
            if item == 2:  # noqa: PLR2004
                return self.z
            raise KeyError
        return NotImplemented  # type: ignore[no-any-return]

    def __setitem__(
        self,
        key,
        value,
    ):
        if isinstance(key, int) and (isinstance(value, (float, int))):
            if key == 0:
                self.x = value
                return None
            if key == 1:
                self.y = value
                return None
            if key == 2:  # noqa: PLR2004
                self.z = value
                return None
            return None
        return NotImplemented  # type: ignore[no-any-return]

    @classmethod
    def from_vector2(
        cls,
        other: Vector2,
    ) -> Self:
        """Returns a Vector3 object from the Vector2 object, setting the Z-component to zero.

        Args:
        ----
            other: The vector to be copied.

        Returns:
        -------
            A new Vector3 object.
        """
        return cls(other.x, other.y, 0.0)

    @classmethod
    def from_vector3(
        cls,
        other: Vector3,
    ) -> Self:
        """Returns a duplicate of the specified vector.

        Args:
        ----
            other: The vector to be duplicated.

        Returns:
        -------
            A new Vector3 instance.
        """
        return cls(other.x, other.y, other.z)

    @classmethod
    def from_vector4(
        cls,
        other: Vector4,
    ) -> Self:
        """Returns a Vector3 object from the Vector4 object, discarding the W-component.

        Args:
        ----
            other: The vector to be copied.

        Returns:
        -------
            A new Vector3 object.
        """
        return cls(other.x, other.y, other.z)

    @classmethod
    def from_null(
        cls,
    ) -> Self:
        """Returns a new vector with a magnitude of zero.

        Returns:
        -------
            A new Vector3 instance.
        """
        return cls(0.0, 0.0, 0.0)

    def set_vector_coords(
        self,
        x: float,
        y: float,
        z: float,
    ):
        """Sets the components of the vector.

        Args:
        ----
            x: The new x component.
            y: The new y component.
            z: The new y component.
        """
        self.x = x
        self.y = y
        self.z = z

    def normalize(self) -> Self:
        """Normalizes the vector so that the magnitude is equal to one while maintaining the same angle."""
        magnitude = self.magnitude()
        if magnitude == 0:
            self.x = 0
            self.y = 0
            self.z = 0
        else:
            self.x /= magnitude
            self.y /= magnitude
            self.z /= magnitude
        return self

    def magnitude(self) -> float:
        """Returns the magnitude of the vector.

        Returns:
        -------
            The magnitude of the vector.
        """
        return math.sqrt(self.x**2 + self.y**2 + self.z**2)

    def normal(self) -> Self:
        vec3 = self.__class__.from_vector3(self)
        vec3.normalize()
        return vec3

    def dot(
        self,
        other: Vector3,
    ) -> float:
        """Returns the dot product between the two specified vectors.

        Args:
        ----
            other: The other vector.

        Returns:
        -------
            The dot product.
        """
        a = self.x * other.x
        b = self.y * other.y
        c = self.z * other.z
        return a + b + c

    def distance(
        self,
        other: Vector3,
    ) -> float:
        """Returns the distance between two vectors.

        Args:
        ----
            other: The other vector.

        Returns:
        -------
            The distance between the vectors.
        """
        a = (self.x - other.x) ** 2
        b = (self.y - other.y) ** 2
        c = (self.z - other.z) ** 2
        return math.sqrt(a + b + c)

    def within(
        self,
        container: list,
    ) -> bool:
        """Checks to see if the same Vector3 object in located in the specified list.

        This differs from using the 'in' keyword as that will return True for Vector3 objects that have similar coordinates.

        Args:
        ----
            container: The list to search in.

        Returns:
        -------
            True if the Vector3 exists in the container.
        """
        return any(item is self for item in container)

    def serialize(self) -> dict[str, float]:
        """Serialize a Vector3 to JSON-compatible dict.

        Returns:
        -------
            Dictionary with x, y, z components as floats.
        """
        return {"x": float(self.x), "y": float(self.y), "z": float(self.z)}

    def copy(self) -> Self:
        """Return a shallow copy of this vector."""
        return self.__class__.from_vector3(self)


class Vector4(vec4):
    """Represents a 4 dimensional vector.

    Attributes:
    ----------
        x: The x component.
        y: The y component.
        z: The z component.
        w: The w component.
    """

    def __init__(
        self,
        x: float | Vector2 | Vector3 | Vector4 = 0.0,
        y: float | None = None,
        z: float | None = None,
        w: float | None = None,
    ):
        # Handle construction from other vector types
        if isinstance(x, Vector2):
            self.x: float = float(x.x)
            self.y: float = float(x.y)
            self.z: float = 0.0
            self.w: float = float(y) if y is not None else 0.0
            return
        if isinstance(x, Vector3):
            self.x = float(x.x)
            self.y = float(x.y)
            self.z = float(x.z)
            self.w = float(y) if y is not None else 0.0
            return
        if isinstance(x, Vector4):
            self.x = float(x.x)
            self.y = float(x.y)
            self.z = float(x.z)
            self.w = float(x.w)
            return
        # Handle scalar construction
        if y is None:
            y = x
        if z is None:
            z = x
        if w is None:
            w = x
        self.x = float(x)
        self.y = float(y)
        self.z = float(z)
        self.w = float(w)

    def __iter__(self) -> Iterator[float]:
        return iter((self.x, self.y, self.z, self.w))

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

    def __str__(self):
        """Returns the individual components as a string separated by whitespace."""
        return f"{self.x} {self.y} {self.z} {self.w}"

    def __eq__(
        self,
        other,
    ):
        """Two Vector4 components are equal if their components are approximately the same."""
        if self is other:
            return True
        if not isinstance(other, Vector4):
            return NotImplemented  # type: ignore[no-any-return]

        isclose_x: bool = math.isclose(self.x, other.x)
        isclose_y: bool = math.isclose(self.y, other.y)
        isclose_z: bool = math.isclose(self.z, other.z)
        isclose_w: bool = math.isclose(self.w, other.w)
        return isclose_x and isclose_y and isclose_z and isclose_w

    def __add__(
        self,
        other,
    ):
        """Adds the components of two Vector4 objects."""
        if not isinstance(other, Vector4):
            return NotImplemented  # type: ignore[no-any-return]

        new = self.__class__.from_vector4(self)
        new.x += other.x
        new.y += other.y
        new.z += other.z
        new.w += other.w
        return new

    def __radd__(
        self,
        other,
    ):
        """Right addition: scalar + Vector4 (returns Vector4)."""
        if isinstance(other, (int, float)):
            new = self.__class__.from_vector4(self)
            new.x = other + new.x
            new.y = other + new.y
            new.z = other + new.z
            new.w = other + new.w
            return new
        return NotImplemented  # type: ignore[no-any-return]

    def __getitem__(
        self,
        item: int,
    ) -> float:
        """Get element by index (0=x, 1=y, 2=z, 3=w)."""
        if isinstance(item, int):
            if item == 0:
                return self.x
            if item == 1:
                return self.y
            if item == 2:  # noqa: PLR2004
                return self.z
            if item == 3:  # noqa: PLR2004
                return self.w
            raise IndexError(f"Vector4 index out of range: {item}")
        return NotImplemented  # type: ignore[no-any-return]

    def __setitem__(
        self,
        key: int,
        value: float,
    ):
        """Set element by index (0=x, 1=y, 2=z, 3=w)."""
        if isinstance(key, int) and isinstance(value, (float, int)):
            if key == 0:
                self.x = value
                return None
            if key == 1:
                self.y = value
                return None
            if key == 2:  # noqa: PLR2004
                self.z = value
                return None
            if key == 3:  # noqa: PLR2004
                self.w = value
                return None
        return NotImplemented  # type: ignore[no-any-return]

    def __sub__(
        self,
        other,
    ):
        """Subtracts the components of two Vector4 objects."""
        if not isinstance(other, Vector4):
            return NotImplemented  # type: ignore[no-any-return]

        new = self.__class__.from_vector4(self)
        new.x -= other.x
        new.y -= other.y
        new.z -= other.z
        new.w -= other.w
        return new

    def __rsub__(
        self,
        other,
    ):
        """Right subtraction: scalar - Vector4."""
        if isinstance(other, (int, float)):
            new = self.__class__.from_vector4(self)
            new.x = other - new.x
            new.y = other - new.y
            new.z = other - new.z
            new.w = other - new.w
            return new
        return NotImplemented  # type: ignore[no-any-return]

    def __mul__(
        self,
        other: int | float | Vector4,
    ):
        """Multiplies the components by a scalar or element-wise with another Vector4."""
        if isinstance(other, (int, float)):
            new = self.__class__.from_vector4(self)
            new.x *= other
            new.y *= other
            new.z *= other
            new.w *= other
            return new
        if isinstance(other, Vector4):
            new = self.__class__.from_vector4(self)
            new.x *= other.x
            new.y *= other.y
            new.z *= other.z
            new.w *= other.w
            return new
        return NotImplemented  # type: ignore[no-any-return]

    def __rmul__(
        self,
        other: int | float,
    ):
        """Right multiplication: scalar * Vector4."""
        if isinstance(other, (int, float)):
            new = self.__class__.from_vector4(self)
            new.x *= other
            new.y *= other
            new.z *= other
            new.w *= other
            return new
        return NotImplemented  # type: ignore[no-any-return]

    def __truediv__(
        self,
        other: int | float,
    ):
        """Performs element-wise true division of vector by scalar value."""
        if isinstance(other, (int, float)):
            new = self.__class__.from_vector4(self)
            new.x /= other
            new.y /= other
            new.z /= other
            new.w /= other
            return new
        return NotImplemented  # type: ignore[no-any-return]

    def __rtruediv__(
        self,
        other,
    ):
        """Right division: scalar / Vector4."""
        if isinstance(other, (int, float)):
            new = self.__class__.from_vector4(self)
            new.x = other / new.x if new.x != 0 else 0.0
            new.y = other / new.y if new.y != 0 else 0.0
            new.z = other / new.z if new.z != 0 else 0.0
            new.w = other / new.w if new.w != 0 else 0.0
            return new
        return NotImplemented  # type: ignore[no-any-return]

    @classmethod
    def from_vector2(
        cls,
        other: Vector2,
    ) -> Self:
        """Returns a Vector4 object from the Vector2 object, setting the Z-component and W-component to zero.

        Args:
        ----
            other: The vector to be copied.

        Returns:
        -------
            A new Vector4 object.
        """
        return cls(other.x, other.y, 0.0, 0.0)

    @classmethod
    def from_vector3(
        cls,
        other: Vector3,
    ) -> Self:
        """Returns a Vector4 object from the Vector3 object, setting the W-component to zero.

        Args:
        ----
            other: The vector to be copied.

        Returns:
        -------
            A new Vector4 object.
        """
        return cls(other.x, other.y, other.z, 0.0)

    @classmethod
    def from_vector4(
        cls,
        other: Vector4,
    ) -> Self:
        """Returns a duplicate of the specified vector.

        Args:
        ----
            other: The vector to be duplicated.

        Returns:
        -------
            A new Vector4 instance.
        """
        return cls(other.x, other.y, other.z, other.w)

    @classmethod
    def from_null(
        cls,
    ) -> Self:
        """Returns a new vector with a magnitude of zero.

        Returns:
        -------
            A new Vector4 instance.
        """
        return cls(0.0, 0.0, 0.0, 0.0)

    @classmethod
    def from_compressed(
        cls,
        data: int,
    ) -> Self:
        """Decompress a packed quaternion from a 32-bit integer.

        KotOR uses compressed quaternions for orientation controllers to save space.
        The compression packs X, Y, Z components into 11, 11, and 10 bits respectively,
        with W calculated from the constraint that |q| = 1.

        Args:
        ----
            data: 32-bit packed quaternion value

        Returns:
        -------
            Vector4: Decompressed quaternion (x, y, z, w)

        References:
        ----------
        Based on swkotor.exe GFF structure:
        - CResGFF::CreateGFFFile @ 0x00411260 - Creates GFF file structure
        - Vector3/Vector4 structures used in GFF fields
        Original BioWare engine binaries
        Derivations and Other Implementations:
        ----------
        https://github.com/th3w1zard1/kotorblender/tree/master/io_scene_kotor/format/mdl/reader.py:850-868


            Formula: X uses bits 0-10 (11 bits), Y uses bits 11-21 (11 bits),
                     Z uses bits 22-31 (10 bits), W computed from magnitude
        """
        # Extract components from packed integer (kotorblender:855-858)
        # X component: bits 0-10 (11 bits, mask 0x7FF = 2047)
        # Maps [0, 2047] -> [-1, 1] via (value/1023 - 1)
        x = ((data & 0x7FF) / 1023.0) - 1.0

        # Y component: bits 11-21 (11 bits, shift 11 then mask 0x7FF)
        y = (((data >> 11) & 0x7FF) / 1023.0) - 1.0

        # Z component: bits 22-31 (10 bits, shift 22, max value 1023)
        z = ((data >> 22) / 511.0) - 1.0

        # Calculate W from quaternion unit constraint (kotorblender:859-863)
        temp = x**2 + y**2 + z**2
        if temp < 1.0:
            w = math.sqrt(1.0 - temp)
        else:
            temp = math.sqrt(temp)
            x /= temp
            y /= temp
            z /= temp
            w = 0

        return cls(x, y, z, w)

    @classmethod
    def from_euler(
        cls,
        x: float,
        y: float,
        z: float,
    ) -> Self:
        """Creates a Vector3 object from x/y/z rotations (in radians).

        Args:
        ----
            x: Roll
            y: Pitch
            z: Yaw

        Returns:
        -------
            A new Vector3 object.
        """
        roll: float = x
        pitch: float = y
        yaw: float = z

        qx: float = math.sin(roll / 2) * math.cos(pitch / 2) * math.cos(yaw / 2) - math.cos(roll / 2) * math.sin(pitch / 2) * math.sin(yaw / 2)
        qy: float = math.cos(roll / 2) * math.sin(pitch / 2) * math.cos(yaw / 2) + math.sin(roll / 2) * math.cos(pitch / 2) * math.sin(yaw / 2)
        qz: float = math.cos(roll / 2) * math.cos(pitch / 2) * math.sin(yaw / 2) - math.sin(roll / 2) * math.sin(pitch / 2) * math.cos(yaw / 2)
        qw: float = math.cos(roll / 2) * math.cos(pitch / 2) * math.cos(yaw / 2) + math.sin(roll / 2) * math.sin(pitch / 2) * math.sin(yaw / 2)

        return cls(qx, qy, qz, qw)

    def to_compressed(self) -> int:
        """Compress this quaternion into a 32-bit integer.

        Inverse of from_compressed. Packs X, Y, Z components into a single
        32-bit value. The W component is not stored as it can be recomputed from
        the quaternion unit constraint.

        Returns:
        -------
            int: 32-bit packed quaternion value

        References:
        ----------
        Based on swkotor.exe GFF structure:
        - CResGFF::CreateGFFFile @ 0x00411260 - Creates GFF file structure
        - Vector3/Vector4 structures used in GFF fields
        Original BioWare engine binaries
        Derivations and Other Implementations:
        ----------
        https://github.com/th3w1zard1/kotorblender/tree/master/io_scene_kotor/format/mdl/reader.py:850-868 (decompression)
        Inverse operation derived from decompression algorithm


        Notes:
        -----
            Values are clamped to [-1, 1] range before packing to prevent overflow.
        """
        # Clamp values to valid range
        x = max(-1.0, min(1.0, self.x))
        y = max(-1.0, min(1.0, self.y))
        z = max(-1.0, min(1.0, self.z))

        # Map from [-1, 1] to integer ranges and pack
        # X: [-1, 1] -> [0, 2047] (11 bits) via (value + 1) * 1023
        x_packed = int((x + 1.0) * 1023.0) & 0x7FF

        # Y: [-1, 1] -> [0, 2047] (11 bits)
        y_packed = int((y + 1.0) * 1023.0) & 0x7FF

        # Z: [-1, 1] -> [0, 1023] (10 bits)
        z_packed = int((z + 1.0) * 511.0) & 0x3FF

        # Pack into single 32-bit integer
        return x_packed | (y_packed << 11) | (z_packed << 22)

    def to_euler(self) -> Vector3:
        """Converts a quaternion to Euler angles.

        Args:
        ----
            self: Quaternion to convert

        Returns:
        -------
            Vector3: Converted Euler angles as roll, pitch, yaw

        Processing Logic:
        ----------------
            - Calculate roll by taking the atan2 of t0/t1 where t0 and t1 are functions of self.w, self.x, self.y, self.z
            - Calculate pitch by taking the asin of t2 where t2 is a function of self.w, self.y, self.z, with bounds checking
            - Calculate yaw by taking the atan2 of t3/t4 where t3 and t4 are functions of self.w, self.x, self.y, self.z
            - Return a Vector3 containing the calculated roll, pitch, yaw.
        """
        t0 = 2.0 * (self.w * self.x + self.y * self.z)
        t1 = 1 - 2 * (self.x * self.x + self.y * self.y)
        roll = math.atan2(t0, t1)

        t2 = 2 * (self.w * self.y - self.z * self.x)
        t2 = min(t2, 1)
        t2 = max(t2, -1)
        pitch = math.asin(t2)

        t3 = 2 * (self.w * self.z + self.x * self.y)
        t4 = 1 - 2 * (self.y * self.y + self.z * self.z)
        yaw = math.atan2(t3, t4)

        return Vector3(roll, pitch, yaw)

    def magnitude(self) -> float:
        """Returns the magnitude of the vector.

        Returns:
        -------
            The magnitude of the vector.
        """
        return math.sqrt(self.x**2 + self.y**2 + self.z**2 + self.w**2)

    def normalize(self) -> Self:
        """Normalizes the vector so that the magnitude is equal to one while maintaining the same angle.

        Returns:
        -------
            The same vector.
        """
        magnitude: float = self.magnitude()
        if magnitude == 0:
            self.x = 0
            self.y = 0
            self.z = 0
            self.w = 0
        else:
            self.x /= magnitude
            self.y /= magnitude
            self.z /= magnitude
            self.w /= magnitude
        return self

    def set_vector_coords(
        self,
        x: float,
        y: float,
        z: float,
        w: float,
    ):
        """Sets the components of the vector.

        Args:
        ----
            x: The new x component.
            y: The new y component.
            z: The new y component.
            w: The new w component.
        """
        self.x = x
        self.y = y
        self.z = z
        self.w = w

    def serialize(self) -> dict[str, float]:
        """Serialize a Vector4 (quaternion) to JSON-compatible dict.

        Returns:
        -------
            Dictionary with x, y, z, w components as floats.
        """
        return {"x": float(self.x), "y": float(self.y), "z": float(self.z), "w": float(self.w)}

    def copy(self) -> Self:
        """Return a shallow copy of this vector."""
        return self.__class__.from_vector4(self)


class AxisAngle:
    """Represents a rotation in 3D space.

    Attributes:
    ----------
        axis: The axis of rotation.
        angle: The magnitude of the rotation.
    """

    def __init__(
        self,
        axis: Vector3 | None = None,
        angle: float | None = None,
    ):
        self.axis: Vector3 = axis if axis is not None else Vector3()
        self.angle: float = angle if angle is not None else 0.0

    @classmethod
    def from_quaternion(
        cls,
        quaternion: Vector4,
    ) -> Self:
        """Returns a AxisAngle converted from a Vector4 quaternion.

        Args:
        ----
            quaternion: The quaternion.

        Returns:
        -------
            A new AxisAngle instance.
        """
        aa: Self = cls.from_null()
        quaternion.normalize()

        aa.angle = 2.0 * math.atan2(
            Vector3(quaternion.x, quaternion.y, quaternion.z).magnitude(),
            quaternion.w,
        )

        s = math.sin(aa.angle / 2.0)
        if s < 0.001:
            aa.axis = Vector3(1.0, 0.0, 0.0)
        else:
            aa.axis.x = quaternion.x / s
            aa.axis.y = quaternion.y / s
            aa.axis.z = quaternion.z / s

        return aa

    @classmethod
    def from_null(
        cls,
    ) -> Self:
        """Returns a AxisAngle that contains no rotation.

        Returns:
        -------
            A new AxisAngle instance.
        """
        return cls(Vector3.from_null(), 0.0)

    def copy(self) -> Self:
        """Return a copy of this axis-angle rotation."""
        return self.__class__(self.axis.copy(), self.angle)


class SurfaceMaterial(IntEnum):
    """The surface materials for walkmeshes found in both games."""

    # as according to 'surfacemat.2da'
    UNDEFINED = 0
    DIRT = 1
    OBSCURING = 2
    GRASS = 3
    STONE = 4
    WOOD = 5
    WATER = 6
    NON_WALK = 7
    TRANSPARENT = 8
    CARPET = 9
    METAL = 10
    PUDDLES = 11
    SWAMP = 12
    MUD = 13
    LEAVES = 14
    LAVA = 15
    BOTTOMLESS_PIT = 16
    DEEP_WATER = 17
    DOOR = 18
    NON_WALK_GRASS = 19
    SURFACE_MATERIAL_20 = 20
    SURFACE_MATERIAL_21 = 21
    SURFACE_MATERIAL_22 = 22
    SURFACE_MATERIAL_23 = 23
    SURFACE_MATERIAL_24 = 24
    SURFACE_MATERIAL_25 = 25
    SURFACE_MATERIAL_26 = 26
    SURFACE_MATERIAL_27 = 27
    SURFACE_MATERIAL_28 = 28
    SURFACE_MATERIAL_29 = 29
    TRIGGER = 30

    def walkable(self) -> bool:
        """Returns True if the surface material is walkable, False otherwise."""
        return self in {
            SurfaceMaterial.CARPET,
            SurfaceMaterial.DIRT,
            SurfaceMaterial.DOOR,
            SurfaceMaterial.GRASS,
            SurfaceMaterial.LEAVES,
            SurfaceMaterial.METAL,
            SurfaceMaterial.MUD,
            SurfaceMaterial.PUDDLES,
            SurfaceMaterial.STONE,
            SurfaceMaterial.SWAMP,
            SurfaceMaterial.TRIGGER,
            SurfaceMaterial.WATER,
            SurfaceMaterial.WOOD,
        }

    def is_walkable(self) -> bool:
        """Alias for walkable() to satisfy toolset expectations."""
        return self.walkable()


class Face:
    """Represents a triangle in 3D space.

    Attributes:
    ----------
        v1: First point of the triangle.
        v2: Second point of the triangle.
        v3: Third point of the triangle.
        material: Material of the triangle, for usage in-game.
    """

    def __init__(
        self,
        v1: Vector3 | None = None,
        v2: Vector3 | None = None,
        v3: Vector3 | None = None,
        material: SurfaceMaterial = SurfaceMaterial.UNDEFINED,
    ):
        self.v1: Vector3 = Vector3() if v1 is None else v1
        self.v2: Vector3 = Vector3() if v2 is None else v2
        self.v3: Vector3 = Vector3() if v3 is None else v3
        self.material: SurfaceMaterial = material

    def __eq__(self, other: object) -> bool:
        """Check equality by comparing vertices and material.

        Two faces are equal if they have the same three vertices (by value)
        and the same material. This is value-based equality, not identity-based.
        """
        if not isinstance(other, Face):
            return NotImplemented  # type: ignore[no-any-return]
        return self.v1 == other.v1 and self.v2 == other.v2 and self.v3 == other.v3 and self.material == other.material

    def __hash__(self) -> int:
        """Hash based on vertices and material for use in sets/dicts."""
        return hash((self.v1, self.v2, self.v3, self.material))

    def normal(self) -> Vector3:
        """Returns the normal for the face (cross product of edges, normalized).

        Reference: KotOR.js src/odyssey/OdysseyWalkMesh.ts:741-746 (rebuild: cb = v3-v2, ab = v1-v2, normal = cb.cross(ab)).
        Equivalent: (v2-v1)×(v3-v2) = (v3-v2)×(v1-v2).
        """
        u: Vector3 = self.v2 - self.v1
        v: Vector3 = self.v3 - self.v2

        normal: Vector3 = Vector3.from_null()
        normal.x = (u.y * v.z) - (u.z * v.y)
        normal.y = (u.z * v.x) - (u.x * v.z)
        normal.z = (u.x * v.y) - (u.y * v.x)
        normal.normalize()

        return normal

    def area(self) -> float:
        a = self.v1.distance(self.v2)
        b = self.v1.distance(self.v3)
        c = self.v2.distance(self.v3)
        return 0.25 * math.sqrt((a + b + c) * (-a + b + c) * (a - b + c) * (a + b - c))

    def planar_distance(self) -> float:
        """Plane coefficient d for plane equation n·x + d = 0 (n = normal, v1 on plane).
        Reference: KotOR.js src/odyssey/OdysseyWalkMesh.ts:749 (rebuild: coeff = -dot(v1, normal)).
        """
        return -1.0 * (self.normal().dot(self.v1))

    def centre(self) -> Vector3:
        return (self.v1 + self.v2 + self.v3) / 3

    def average(self) -> Vector3:
        """Returns the average point of the face.

        Returns:
        -------
            A vector representing the average point of the face.
        """
        return (self.v1 + self.v2 + self.v3) / 3

    def determine_z(
        self,
        x: float,
        y: float,
    ) -> float:
        """Returns the Z-component determined from the given X and Y components.

        This method does not check if the point exists within the face, that must be done separately with inside().

        Returns:
        -------
            The Z-component.
        """
        dx1 = x - self.v1.x
        dy1 = y - self.v1.y
        dx2 = self.v2.x - self.v1.x
        dy2 = self.v2.y - self.v1.y
        dx3 = self.v3.x - self.v1.x
        dy3 = self.v3.z - self.v1.y
        scale = dx3 * dy2 - dx2 * dy3
        nx = (dx1 * dy2 - dy1 * dx2) / scale
        ny = (dy1 * dx3 - dx1 * dy3) / scale
        return self.v1.z + ny * (self.v2.z - self.v1.z) + nx * (self.v3.z - self.v1.z)

    def copy(self) -> Face:
        """Return a copy of this face and its vertices."""
        return self.__class__(self.v1.copy(), self.v2.copy(), self.v3.copy(), self.material)


class Polygon2:
    def __init__(
        self,
        points: list[Vector2] | None = None,
    ):
        self.points: list[Vector2] = [] if points is None else points

    def __iter__(self) -> Iterator[Vector2]:
        yield from self.points

    def __len__(self) -> int:
        return len(self.points)

    def __repr__(self) -> str:
        return f"Polygon2({self.points})"

    def __getitem__(
        self,
        item: int | slice,
    ) -> Vector2 | list[Vector2]:
        if isinstance(item, int):
            return self.points[item]
        if isinstance(item, slice):
            return self.points[item.start : item.stop : item.step]
        return NotImplemented  # type: ignore[no-any-return]

    def __setitem__(
        self,
        key: int,
        value: Vector2,
    ):
        if isinstance(key, int) and isinstance(value, Vector2):
            self.points[key] = value
        return NotImplemented  # type: ignore[no-any-return]

    @classmethod
    def from_polygon3(
        cls,
        poly3: Polygon3,
    ) -> Polygon2:
        """Converts a Polygon3 object into a Polygon2 object. The Z-axis is removed.

        Args:
        ----
            poly3: The polygon3 object to flatten.

        Returns:
        -------
            A Polygon2 object.
        """
        poly2 = Polygon2()
        for point in poly3:
            poly2.points.append(Vector2(point.x, point.y))
        return poly2

    def inside(
        self,
        point: Vector2,
        include_edges: bool = True,
    ) -> bool:
        """Returns if the specified point is inside the polygon.

        Args:
        ----
            point: The point.
            include_edges: If True, a points on edges are considered inside the polygon.

        Returns:
        -------
            True if the point is inside the polygon.
        """
        # Code was adapted from this stackoverflow post:
        # https://stackoverflow.com/a/42306732

        n = len(self.points)
        inside: bool = False

        p1: Vector2 = self.points[0]
        for i in range(1, n + 1):
            p2: Vector2 = self.points[i % n]
            if p1.y == p2.y:
                if point.y == p2.y:
                    if min(p1.x, p2.x) <= point.x <= max(p1.x, p2.x):
                        inside = include_edges
                        break
                    if point.x < min(p1.x, p2.x):
                        inside = not inside
            elif min(p1.y, p2.y) <= point.y <= max(p1.y, p2.y):
                xinters: float = (point.y - p1.y) * (p2.x - p1.x) / float(p2.y - p1.y) + p1.x

                if point.x == xinters:
                    inside = include_edges
                    break

                if point.x < xinters:
                    inside = not inside
            p1 = p2
        return inside

    def area(self) -> float:
        """Calculates the area of a polygon.

        Args:
        ----
            self: The polygon object

        Returns:
        -------
            float: The calculated area of the polygon

        Processing Logic:
        ----------------
            - Loops through points calculating trianglular areas
            - Sums all triangular areas
            - Returns absolute value of the calculated area to ensure positivity.
        """
        # Code was adapted from this stackoverflow post:
        # https://stackoverflow.com/a/34327761

        n: int = len(self.points)
        area: float = 0.0
        for i in range(n - 1):
            area += -self.points[i].y * self.points[i + 1].x + self.points[i].x * self.points[i + 1].y
        area += -self.points[n - 1].y * self.points[0].x + self.points[n - 1].x * self.points[0].y
        return 0.5 * math.fabs(area)

    def append(
        self,
        point: Vector2,
    ):
        self.points.append(point)

    def extend(
        self,
        points: list[Vector2],
    ):
        self.points.extend(points)

    def remove(
        self,
        point: Vector2,
    ):
        self.points.remove(point)

    def index(
        self,
        point: Vector2,
    ) -> int:
        return self.points.index(point)

    def copy(self) -> Polygon2:
        """Return a copy of this polygon and its points."""
        return self.__class__([point.copy() for point in self.points])


class Polygon3:
    def __init__(
        self,
        points: list[Vector3] | None = None,
    ):
        self.points: list[Vector3] = [] if points is None else points

    def __iter__(self) -> Iterator[Vector3]:
        yield from self.points

    def __len__(self) -> int:
        return len(self.points)

    def __repr__(self):
        return f"Polygon3({self.points})"

    def __getitem__(
        self,
        item: int | slice,
    ):
        if isinstance(item, int):
            return self.points[item]
        if isinstance(item, slice):
            return self.points[item.start : item.stop : item.step]
        return NotImplemented  # type: ignore[no-any-return]

    def __setitem__(
        self,
        key: int,
        value: Vector3,
    ):
        if isinstance(key, int) and isinstance(value, Vector3):
            self.points[key] = value
        return NotImplemented  # type: ignore[no-any-return]

    @classmethod
    def from_polygon2(
        cls,
        poly2: Polygon2,
    ) -> Polygon3:
        """Converts a Polygon3 object into a Polygon2 object. Points have their Z-axis set to 0.

        Args:
        ----
            poly2: The polygon3 object to copy.

        Returns:
        -------
            A Polygon3 object with the Z-axis of its points set to 0.
        """
        poly3 = Polygon3()
        for point in poly2.points:
            poly3.points.append(Vector3(point.x, point.y, 0))
        return poly3

    def create_triangle(
        self,
        size: float = 1.0,
        origin: Vector3 | tuple[float, float, float] = (0.0, 0.0, 0.0),
    ):
        """Creates an equilateral triangle in the XY-plane with the given size and the bottom vertex at the specified origin.

        Args:
        ----
            size: The length of each side of the triangle.
            origin: A tuple representing the (x, y, z) coordinates of the bottom vertex of the triangle.

        This method modifies the instance by adding three Vector3 points defining the triangle.
        """
        x, y, z = origin
        height = size * (3**0.5) / 2
        self.points = [Vector3(x, y, z), Vector3(x + size, y, z), Vector3(x + size / 2, y + height, z)]

    def default_square(
        self,
        size: float = 1.0,
        origin: tuple[float, float, float] = (0.0, 0.0, 0.0),
    ):
        """Creates a square in the XY-plane with the given size and the bottom-left corner at the specified origin.

        Args:
        ----
            size: The length of each side of the square.
            origin: A tuple representing the (x, y, z) coordinates of the bottom-left corner of the square.

        This method modifies the instance by adding four Vector3 points defining the square.
        """
        x, y, z = origin
        self.points = [Vector3(x, y, z), Vector3(x + size, y, z), Vector3(x + size, y + size, z), Vector3(x, y + size, z)]

    def append(
        self,
        point: Vector3,
    ):
        self.points.append(point)

    def extend(
        self,
        points: list[Vector3],
    ):
        self.points.extend(points)

    def remove(
        self,
        point: Vector3,
    ):
        self.points.remove(point)

    def index(
        self,
        point: Vector3,
    ) -> int:
        return self.points.index(point)

    def copy(self) -> Polygon3:
        """Return a copy of this polygon and its points."""
        return self.__class__([point.copy() for point in self.points])


class Matrix4(mat4):
    """Represents a 4x4 matrix for 3D transformations.

    This class provides matrix operations compatible with GLM-style APIs.
    Matrices are stored internally in row-major order, but accessed in
    column-major order for GLM compatibility (mat4[i] returns column i).

    Attributes:
    ----------
        _data: 4x4 matrix stored as list of 4 lists, each containing 4 floats.
               Stored in row-major order internally.
    """

    def __init__(
        self,
        value: float | Matrix4 | list[list[float]] | None = None,
    ):
        """Initialize a Matrix4.

        Args:
        ----
            value: If float, creates identity matrix scaled by value.
                   If Matrix4, copies the matrix.
                   If list[list[float]], creates matrix from 4x4 list (row-major).
                   If None, creates identity matrix.
        """
        if value is None:
            value = 1.0
        if isinstance(value, (int, float)):
            # Identity matrix scaled by value
            self._data: list[list[float]] = [
                [float(value), 0.0, 0.0, 0.0],
                [0.0, float(value), 0.0, 0.0],
                [0.0, 0.0, float(value), 0.0],
                [0.0, 0.0, 0.0, float(value)],
            ]
        elif isinstance(value, Matrix4):
            # Copy constructor
            self._data = [[value._data[i][j] for j in range(4)] for i in range(4)]
        elif isinstance(value, list) and len(value) == 4:
            # List of 4 lists (row-major)
            self._data = [[float(value[i][j]) for j in range(4)] for i in range(4)]
        else:
            # Default to identity
            self._data = [
                [1.0, 0.0, 0.0, 0.0],
                [0.0, 1.0, 0.0, 0.0],
                [0.0, 0.0, 1.0, 0.0],
                [0.0, 0.0, 0.0, 1.0],
            ]

    def __repr__(self) -> str:
        return f"Matrix4(\n{self._data}\n)"

    def __eq__(
        self,
        other: object,
    ) -> bool:
        """Two Matrix4 objects are equal if their components are approximately the same."""
        if not isinstance(other, Matrix4):
            return NotImplemented  # type: ignore[no-any-return]
        for i in range(4):
            for j in range(4):
                if not math.isclose(self._data[i][j], other._data[i][j]):
                    return False
        return True

    def __hash__(self) -> int:
        return hash(tuple(tuple(row) for row in self._data))

    def __mul__(
        self,
        other: Matrix4 | Vector3 | Vector4,
    ) -> Matrix4 | Vector3 | Vector4:
        """Multiply matrix by another matrix, Vector3, or Vector4."""
        if isinstance(other, Matrix4):
            # Matrix multiplication
            result = Matrix4(0.0)
            for i in range(4):
                for j in range(4):
                    result._data[i][j] = sum(self._data[i][k] * other._data[k][j] for k in range(4))
            return result
        if isinstance(other, Vector4):
            # Matrix * Vector4
            result_data = [sum(self._data[i][j] * other[j] for j in range(4)) for i in range(4)]
            return Vector4(result_data[0], result_data[1], result_data[2], result_data[3])
        if isinstance(other, Vector3):
            # Treat Vector3 as Vector4 with w=1, return Vector3
            vec4_data = [other.x, other.y, other.z, 1.0]
            result_data = [sum(self._data[i][j] * vec4_data[j] for j in range(4)) for i in range(4)]
            return Vector3(result_data[0], result_data[1], result_data[2])
        return NotImplemented  # type: ignore[no-any-return]

    def __getitem__(
        self,
        index: int,
    ) -> Vector4:
        """Get a column vector from the matrix (GLM-compatible indexing).

        In GLM, matrices are column-major, so Matrix4[i] returns column i as a Vector4.
        This allows syntax like m[0][0] to access element at row 0, column 0.

        Args:
        ----
            index: Column index (0-3).

        Returns:
        -------
            Column vector as Vector4.

        Raises:
        ------
            IndexError: If index is out of range.
        """
        if not 0 <= index < 4:
            raise IndexError(f"Matrix4 column index out of range: {index}")
        # Return column as Vector4 (GLM uses column-major order)
        # _data is stored in row-major, so we transpose for column access
        return Vector4(
            self._data[0][index],
            self._data[1][index],
            self._data[2][index],
            self._data[3][index],
        )

    def __setitem__(
        self,
        index: int,
        value: Vector4,
    ):
        """Set a column vector in the matrix (GLM-compatible).

        Args:
        ----
            index: Column index (0-3).
            value: Vector4 to set as column.
        """
        if not isinstance(value, Vector4):
            return NotImplemented  # type: ignore[no-any-return]
        if not 0 <= index < 4:
            raise IndexError(f"Matrix4 column index out of range: {index}")
        # Set column (transpose for row-major storage)
        self._data[0][index] = value.x
        self._data[1][index] = value.y
        self._data[2][index] = value.z
        self._data[3][index] = value.w

    @classmethod
    def from_identity(
        cls,
    ) -> Self:
        """Returns an identity matrix.

        Returns:
        -------
            A new Matrix4 instance representing the identity matrix.
        """
        return cls(1.0)

    @classmethod
    def from_matrix4(
        cls,
        other: Matrix4,
    ) -> Self:
        """Returns a duplicate of the specified matrix.

        Args:
        ----
            other: The matrix to be duplicated.

        Returns:
        -------
            A new Matrix4 instance.
        """
        return cls(other)

    def copy(self) -> Self:
        """Return a copy of this matrix."""
        return self.__class__.from_matrix4(self)
