defmodule Graphmath.Quatern do
  @moduledoc """
  This is the 3D mathematics.

  This submodule handles Quaternion using tuples of floats.

  Quaternions represent an angle of theta around a unit axis vector {nx, ny, nz} as `{ cos(theta/2), nx * sin(theta/2), ny * sin(theta/2), nz * sin(theta/2) }`.

  """

  alias Graphmath.Mat33, as: Mat33

  @type quatern :: {float, float, float, float}
  @type vec3 :: {float, float, float}
  @type mat33 :: {float, float, float, float, float, float, float, float, float}
  @type mat44 ::
          {float, float, float, float, float, float, float, float, float, float, float, float,
           float, float, float, float}

  # https://en.wikipedia.org/wiki/Machine_epsilon
  # note that BEAM uses doubles internally, so this is a bit of a kludge
  @machine_small_float 5.96e-08

  @doc """
  `identity()` creates the identity `quatern`.

  It takes no arguments.

  It returns a `quatern` of the form `{1.0, 0.0, 0.0, 0.0}`.
  """
  @spec identity() :: quatern
  def identity(), do: {1.0, 0.0, 0.0, 0.0}

  @doc """
  `zero()` creates a zero `quatern`.

  It takes no arguments.

  It returns a `quatern` of the form `{0.0, 0.0, 0.0, 0.0}`.

  Note that the zero quaternion is almost definetely not something you ever use.
  """
  @spec zero() :: quatern
  def zero(), do: {0.0, 0.0, 0.0, 0.0}

  @doc """
  `equal_elements(a,b)` checks to see if two quaternions a and b are element-wise equal.

  `a` is the first quaternion.

  `b` is the second quaternion.

  It returns true if the quaternions have the same elements, false otherwise.

  **This function does not require normalized quaternions.**

  Note that orientation quaternions exist where a == -b...that is, where the axes are equivalent but the angle is opposite in sign.

  In such cases, prefer the `equal/2` function.
  """
  @spec equal_elements(quatern, quatern) :: boolean()
  def equal_elements({aw, ax, ay, az} = _a, {bw, bx, by, bz} = _b)
      when is_float(aw) and is_float(ax) and is_float(ay) and is_float(az) and
             is_float(bw) and is_float(bx) and is_float(by) and is_float(bz),
      do: aw == bw and ax == bx and ay == by and az == bz

  def equal_elements({aw, ax, ay, az} = _a, {bw, bx, by, bz} = _b),
    do: aw == bw and ax == bx and ay == by and az == bz

  @doc """
  `equal_elements(a,b, eps)` checks to see if two quaternions a and b are element-wise equal to some epsilon

  `a` is the first quaternion.

  `b` is the second quaternion.

  `eps` is the float of the epsilon for comparison.

  It returns true if the quaternions have the same element-wise values up to and including some epsilon.

  **This function does not require normalized quaternions.**

  Note that orientation quaternions exist where a == -b...that is, where the axes are equivalent but the angle is opposite in sign.

  In such cases, prefer the `equal/3` function.
  """
  @spec equal_elements(quatern, quatern, float) :: boolean()
  def equal_elements({aw, ax, ay, az} = _a, {bw, bx, by, bz} = _b, eps)
      when is_float(aw) and is_float(ax) and is_float(ay) and is_float(az) and
             is_float(bw) and is_float(bx) and is_float(by) and is_float(bz) and is_float(eps),
      do:
        abs(aw - bw) <= eps and
          abs(ax - bx) <= eps and
          abs(ay - by) <= eps and
          abs(az - bz) <= eps

  def equal_elements({aw, ax, ay, az} = _a, {bw, bx, by, bz} = _b, eps),
    do:
      abs(aw - bw) <= eps and
        abs(ax - bx) <= eps and
        abs(ay - by) <= eps and
        abs(az - bz) <= eps

  @doc """
  `equal(a,b)` checks to see if two orientation quaternions a and b are equivalent.

  `a` is the first quaternion.

  `b` is the second quaternion.

  It returns true if the quaternions represent the same orientation.

  **This function expects normalized quaternions.**

  Note that orientation quaternions exist where a == -b...that is, where the axes are equivalent but the angle is opposite in sign.
  """
  @spec equal(quatern, quatern) :: boolean()
  def equal({aw, ax, ay, az} = a, {bw, bx, by, bz} = b)
      when is_float(aw) and is_float(ax) and is_float(ay) and is_float(az) and
             is_float(bw) and is_float(bx) and is_float(by) and is_float(bz),
      do: abs(dot(a, b)) >= 1.0

  def equal(a, b), do: abs(dot(a, b)) >= 1.0

  @doc """
  `equal(a,b,eps)` checks to see if two orientation quaternions a and b are equivalent up to some epsilon

  `a` is the first quaternion.

  `b` is the second quaternion.

  `eps` is the epsilon, on the interval [0,1].

  It returns true if the quaternions represent the same orientation.

  **This function expects normalized quaternions.**

  Note that orientation quaternions exist where a == -b...that is, where the axes are equivalent but the angle is opposite in sign.
  """
  @spec equal(quatern, quatern, float) :: boolean()

  def equal({aw, ax, ay, az} = a, {bw, bx, by, bz} = b, eps)
      when is_float(aw) and is_float(ax) and is_float(ay) and is_float(az) and
             is_float(bw) and is_float(bx) and is_float(by) and is_float(bz) and is_float(eps),
      do: abs(dot(a, b)) >= 1.0 - eps

  def equal(a, b, eps), do: abs(dot(a, b)) >= 1.0 - eps

  @doc """
  `create(w,x,y,z)` creates a `quatern` of value (w,x,y,z).

  `w` is the rotation around the axis in radians.

  `x` is the first element of the `vec3` representing the axis to be created.

  `y` is the second element of the `vec3` representing the axis to be created.

  `z` is the third element of the `vec3` representing the axis to be created.

  It returns a `quatern` of the form `{w,x,y,z}`.
  """
  @spec create(float, float, float, float) :: quatern
  def create(w, x, y, z) when is_float(w) and is_float(x) and is_float(y) and is_float(z),
    do: {w, x, y, z}

  def create(w, x, y, z), do: {1.0 * w, 1.0 * x, 1.0 * y, 1.0 * z}

  @doc """
  `create(quatern)` creates a `quatern` from a list of 4 or more floats.

  `quatern` is a list of 4 or more floats.

  It returns a `quatern` of the form `{w,x,y,z}`, where `w`, `x`, `y`, and `z` are the first four elements in `quatern`.
  """
  @spec from_list([float]) :: quatern
  def from_list([w, x, y, z | _])
      when is_float(w) and is_float(x) and is_float(y) and is_float(z),
      do: {w, x, y, z}

  def from_list([w, x, y, z | _]), do: {w, x, y, z}

  @doc """
  `create(w, vec)` creates a `quatern` from an angle and an axis.

  `w` is the angle in radians.

  `vec` is the axis `vec3` of the form {x,y,z}.

  It returns a `quatern` of the form `{w,x,y,z}`.
  """
  @spec from_axis_angle(float, vec3) :: quatern
  def from_axis_angle(theta, {x, y, z})
      when is_float(theta) and is_float(x) and is_float(y) and is_float(z) do
    half_theta = theta / 2.0
    ct = :math.cos(half_theta)
    st = :math.sin(half_theta)
    {ct, st * x, st * y, st * z}
  end

  def from_axis_angle(theta, {x, y, z}) do
    half_theta = theta / 2.0
    ct = :math.cos(half_theta)
    st = :math.sin(half_theta)
    {ct, st * x, st * y, st * z}
  end

  @doc """
  `add(lhs, rhs)` add two quaternions.

  `lhs` is the first `quatern`

  `rhs` is the second `quatern`

  It returns a `quatern` of the form { lhs<sub>w</sub> + rhs<sub>w</sub>, lhs<sub>x</sub> + rhs<sub>x</sub>, lhs<sub>y</sub> + rhs<sub>y</sub>, lhs<sub>z</sub> + rhs<sub>z</sub> }.
  """
  @spec add(quatern, quatern) :: quatern
  def add({w, x, y, z}, {a, b, c, d})
      when is_float(w) and is_float(x) and is_float(y) and is_float(z) and
             is_float(a) and is_float(b) and is_float(c) and is_float(d),
      do: {w + a, x + b, y + c, z + d}

  def add({w, x, y, z}, {a, b, c, d}), do: {1.0 * w + a, 1.0 * x + b, 1.0 * y + c, 1.0 * z + d}

  @doc """
  `subtract(lhs, rhs)` subtract two quaternions.

   `lhs` is the first `quatern`

   `rhs` is the second `quatern`

   It returns a `quatern` of the form { lhs<sub>w</sub> - rhs<sub>w</sub>, lhs<sub>x</sub> - rhs<sub>x</sub>, lhs<sub>y</sub> - rhs<sub>y</sub>, lhs<sub>z</sub> - rhs<sub>z</sub> }.
  """
  @spec subtract(quatern, quatern) :: quatern
  def subtract({w, x, y, z}, {a, b, c, d})
      when is_float(w) and is_float(x) and is_float(y) and is_float(z) and
             is_float(a) and is_float(b) and is_float(c) and is_float(d),
      do: {w - a, x - b, y - c, z - d}

  def subtract({w, x, y, z}, {a, b, c, d}),
    do: {1.0 * w - a, 1.0 * x - b, 1.0 * y - c, 1.0 * z - d}

  @doc """
  `multiply(lhs, rhs)` multiply two quaternions.

   `lhs` is the first `quatern`

   `rhs` is the second `quatern`

   It returns a `quatern` resultant of the multiplication
   NOTE: Multiplication is not generally commutative, so in most cases p*q != q*p.
  """
  @spec multiply(quatern, quatern) :: quatern
  def multiply({w, x, y, z}, {a, b, c, d})
      when is_float(w) and is_float(x) and is_float(y) and is_float(z) and
             is_float(a) and is_float(b) and is_float(c) and is_float(d),
      do:
        {w * a - x * b - y * c - z * d, w * b + x * a + y * d - z * c,
         w * c + y * a + z * b - x * d, w * d + z * a + x * c - y * b}

  def multiply({w, x, y, z}, {a, b, c, d}),
    do:
      {1.0 * w * a - x * b - y * c - z * d, 1.0 * w * b + x * a + y * d - z * c,
       1.0 * w * c + y * a + z * b - x * d, 1.0 * w * d + z * a + x * c - y * b}

  @doc """
  `scale(quat, scalar)` multiply a `quatern` for a scalar.

   `quat` is the `quatern`

   `scalar` is the scalar

   It returns a `quatern` of the form
      { a<sub>w</sub> * scalar, a<sub>x</sub> * scalar, a<sub>y</sub> * scalar, a<sub>z</sub> * scalar}.
  """
  @spec scale(quatern, float) :: quatern
  def scale({w, x, y, z}, scalar)
      when is_float(w) and is_float(x) and is_float(y) and is_float(z) and is_float(scalar),
      do: {w * scalar, x * scalar, y * scalar, z * scalar}

  def scale({w, x, y, z}, scalar), do: {w * scalar, x * scalar, y * scalar, z * scalar}

  @doc """
  `roll(quat)` Calculate the local roll element of a quaternion.

  `quat` is the quatern

  It returns a `float` representing the roll of the quaternion in Radians.
  """
  @spec get_roll(quatern) :: float
  def get_roll({w, x, y, z}) when is_float(w) and is_float(x) and is_float(y) and is_float(z) do
    f_t_y = 2.0 * y
    f_t_z = 2.0 * z
    f_t_wz = f_t_z * w
    f_t_xy = f_t_y * x
    f_t_yy = f_t_y * y
    f_t_zz = f_t_z * z

    :math.atan2(f_t_xy + f_t_wz, 1.0 - (f_t_yy + f_t_zz))
  end

  def get_roll({w, x, y, z}) do
    f_t_y = 2.0 * y
    f_t_z = 2.0 * z
    f_t_wz = f_t_z * w
    f_t_xy = f_t_y * x
    f_t_yy = f_t_y * y
    f_t_zz = f_t_z * z

    :math.atan2(f_t_xy + f_t_wz, 1.0 - (f_t_yy + f_t_zz))
  end

  @doc """
  `pitch(quat)` Calculate the local pitch element of a quaternion.

  `quat` is the quatern

  It returns a `float` representing the pitch of the quaternion in Radians.
  """
  @spec get_pitch(quatern) :: float
  def get_pitch({w, x, y, z}) when is_float(w) and is_float(x) and is_float(y) and is_float(z) do
    f_t_x = 2.0 * x
    f_t_z = 2.0 * z
    f_t_wx = f_t_x * w
    f_t_xx = f_t_x * x
    f_t_yz = f_t_z * y
    f_t_zz = f_t_z * z

    :math.atan2(f_t_yz + f_t_wx, 1.0 - (f_t_xx + f_t_zz))
  end

  def get_pitch({w, x, y, z}) do
    f_t_x = 2.0 * x
    f_t_z = 2.0 * z
    f_t_wx = f_t_x * w
    f_t_xx = f_t_x * x
    f_t_yz = f_t_z * y
    f_t_zz = f_t_z * z

    :math.atan2(f_t_yz + f_t_wx, 1.0 - (f_t_xx + f_t_zz))
  end

  @doc """
  `yaw(quat)` Calculate the local yaw element of a quaternion.

  `quat` is the quatern

  It returns a `float` representing the yaw of the quaternion in Radians.
  """
  @spec get_yaw(quatern) :: float
  def get_yaw({w, x, y, z}) when is_float(w) and is_float(x) and is_float(y) and is_float(z) do
    f_t_x = 2.0 * x
    f_t_y = 2.0 * y
    f_t_z = 2.0 * z
    f_t_wy = f_t_y * w
    f_t_xx = f_t_x * x
    f_t_xz = f_t_z * x
    f_t_yy = f_t_y * y

    :math.atan2(f_t_xz + f_t_wy, 1.0 - (f_t_xx + f_t_yy))
  end

  def get_yaw({w, x, y, z}) do
    f_t_x = 2.0 * x
    f_t_y = 2.0 * y
    f_t_z = 2.0 * z
    f_t_wy = f_t_y * w
    f_t_xx = f_t_x * x
    f_t_xz = f_t_z * x
    f_t_yy = f_t_y * y

    :math.atan2(f_t_xz + f_t_wy, 1.0 - (f_t_xx + f_t_yy))
  end

  @doc """
  `from_rotation_matrix(mat)` creates a `quatern` from a rotation matrix.

  `mat` is the matrix

  It returns a `quatern` of the form `{w,x,y,z}`.
  """
  @spec from_rotation_matrix(mat33) :: quatern
  def from_rotation_matrix({a11, a12, a13, a21, a22, a23, a31, a32, a33} = mat)
      when is_float(a11) and is_float(a12) and is_float(a13) and
             is_float(a21) and is_float(a22) and is_float(a23) and
             is_float(a31) and is_float(a32) and is_float(a33) do
    # Why does the trace matter? Consult here:
    # http://www.euclideanspace.com/maths/geometry/rotations/conversions/matrixToQuaternion/
    f_trace = a11 + a22 + a33

    if f_trace > 0.0 do
      f_root = :math.sqrt(f_trace + 1.0)
      w = 0.5 * f_root
      f_root = 0.5 / f_root
      {w, (a32 - a23) * f_root, (a13 - a31) * f_root, (a21 - a12) * f_root}
    else
      i_next = {1, 2, 0}

      i =
        cond do
          a22 > a11 and a33 > Mat33.at(mat, 1, 1) -> 2
          a33 > Mat33.at(mat, 0, 0) -> 2
          true -> 0
        end

      j = elem(i_next, i)
      k = elem(i_next, j)

      f_root = :math.sqrt(Mat33.at(mat, i, i) - Mat33.at(mat, j, j) - Mat33.at(mat, k, k) + 1.0)
      apk_quat = {0.0, 0.0, 0.0}
      apk_quat = put_elem(apk_quat, i, 0.5 * f_root)
      f_root = 0.5 / f_root
      apk_quat = put_elem(apk_quat, j, (Mat33.at(mat, j, i) + Mat33.at(mat, i, j)) * f_root)
      apk_quat = put_elem(apk_quat, k, (Mat33.at(mat, k, i) + Mat33.at(mat, i, k)) * f_root)

      {x, y, z} = apk_quat

      {x, y, z, (Mat33.at(mat, k, j) - Mat33.at(mat, j, k)) * f_root}
    end
  end

  def from_rotation_matrix({a11, a12, a13, a21, a22, a23, a31, a32, a33} = mat) do
    # Why does the trace matter? Consult here:
    # http://www.euclideanspace.com/maths/geometry/rotations/conversions/matrixToQuaternion/
    f_trace = a11 + a22 + a33

    if f_trace > 0.0 do
      f_root = :math.sqrt(f_trace + 1.0)
      w = 0.5 * f_root
      f_root = 0.5 / f_root
      {w, (a32 - a23) * f_root, (a13 - a31) * f_root, (a21 - a12) * f_root}
    else
      i_next = {1, 2, 0}

      i =
        cond do
          a22 > a11 and a33 > Mat33.at(mat, 1, 1) -> 2
          a33 > Mat33.at(mat, 0, 0) -> 2
          true -> 0
        end

      j = elem(i_next, i)
      k = elem(i_next, j)

      f_root = :math.sqrt(Mat33.at(mat, i, i) - Mat33.at(mat, j, j) - Mat33.at(mat, k, k) + 1.0)
      apk_quat = {0.0, 0.0, 0.0}
      apk_quat = put_elem(apk_quat, i, 0.5 * f_root)
      f_root = 0.5 / f_root
      apk_quat = put_elem(apk_quat, j, (Mat33.at(mat, j, i) + Mat33.at(mat, i, j)) * f_root)
      apk_quat = put_elem(apk_quat, k, (Mat33.at(mat, k, i) + Mat33.at(mat, i, k)) * f_root)

      {x, y, z} = apk_quat

      {x, y, z, (Mat33.at(mat, k, j) - Mat33.at(mat, j, k)) * f_root}
    end
  end

  @doc """
  `to_rotation_matrix_33(quat)` creates a `mat33` from a quatern.

  `quat` is the quatern

  It returns a `mat33` representing a rotation.
  """
  @spec to_rotation_matrix_33(quatern) :: mat33
  def to_rotation_matrix_33({w, x, y, z})
      when is_float(w) and is_float(x) and is_float(y) and is_float(z) do
    f_tx = x + x
    f_ty = y + y
    f_tz = z + z
    f_t_wx = f_tx * w
    f_t_wy = f_ty * w
    f_t_wz = f_tz * w
    f_t_xx = f_tx * x
    f_t_xy = f_ty * x
    f_t_xz = f_tz * x
    f_t_yy = f_ty * y
    f_t_yz = f_tz * y
    f_t_zz = f_tz * z

    a11 = 1.0 - (f_t_yy + f_t_zz)
    a12 = f_t_xy - f_t_wz
    a13 = f_t_xz + f_t_wy
    a21 = f_t_xy + f_t_wz
    a22 = 1.0 - (f_t_xx + f_t_zz)
    a23 = f_t_yz - f_t_wx
    a31 = f_t_xz - f_t_wy
    a32 = f_t_yz + f_t_wx
    a33 = 1.0 - (f_t_xx + f_t_yy)

    {a11, a12, a13, a21, a22, a23, a31, a32, a33}
  end

  def to_rotation_matrix_33({w, x, y, z}) do
    f_tx = 1.0 * x + x
    f_ty = 1.0 * y + y
    f_tz = 1.0 * z + z
    f_t_wx = f_tx * w
    f_t_wy = f_ty * w
    f_t_wz = f_tz * w
    f_t_xx = f_tx * x
    f_t_xy = f_ty * x
    f_t_xz = f_tz * x
    f_t_yy = f_ty * y
    f_t_yz = f_tz * y
    f_t_zz = f_tz * z

    a11 = 1.0 - (f_t_yy + f_t_zz)
    a12 = f_t_xy - f_t_wz
    a13 = f_t_xz + f_t_wy
    a21 = f_t_xy + f_t_wz
    a22 = 1.0 - (f_t_xx + f_t_zz)
    a23 = f_t_yz - f_t_wx
    a31 = f_t_xz - f_t_wy
    a32 = f_t_yz + f_t_wx
    a33 = 1.0 - (f_t_xx + f_t_yy)

    {a11, a12, a13, a21, a22, a23, a31, a32, a33}
  end

  @doc """
  `to_rotation_matrix_44(quat)` creates a `mat44` from a quatern.

  `quat` is the quatern

  It returns a `mat44` representing a rotation.
  """
  @spec to_rotation_matrix_44(quatern) :: mat44
  def to_rotation_matrix_44({w, x, y, z})
      when is_float(w) and is_float(x) and is_float(y) and is_float(z) do
    f_tx = x + x
    f_ty = y + y
    f_tz = z + z
    f_t_wx = f_tx * w
    f_t_wy = f_ty * w
    f_t_wz = f_tz * w
    f_t_xx = f_tx * x
    f_t_xy = f_ty * x
    f_t_xz = f_tz * x
    f_t_yy = f_ty * y
    f_t_yz = f_tz * y
    f_t_zz = f_tz * z

    a11 = 1.0 - (f_t_yy + f_t_zz)
    a12 = f_t_xy - f_t_wz
    a13 = f_t_xz + f_t_wy
    a21 = f_t_xy + f_t_wz
    a22 = 1.0 - (f_t_xx + f_t_zz)
    a23 = f_t_yz - f_t_wx
    a31 = f_t_xz - f_t_wy
    a32 = f_t_yz + f_t_wx
    a33 = 1.0 - (f_t_xx + f_t_yy)

    {a11, a12, a13, 0.0, a21, a22, a23, 0.0, a31, a32, a33, 0.0, 0.0, 0.0, 0.0, 1.0}
  end

  def to_rotation_matrix_44({w, x, y, z}) do
    f_tx = 1.0 * x + x
    f_ty = 1.0 * y + y
    f_tz = 1.0 * z + z
    f_t_wx = f_tx * w
    f_t_wy = f_ty * w
    f_t_wz = f_tz * w
    f_t_xx = f_tx * x
    f_t_xy = f_ty * x
    f_t_xz = f_tz * x
    f_t_yy = f_ty * y
    f_t_yz = f_tz * y
    f_t_zz = f_tz * z

    a11 = 1.0 - (f_t_yy + f_t_zz)
    a12 = f_t_xy - f_t_wz
    a13 = f_t_xz + f_t_wy
    a21 = f_t_xy + f_t_wz
    a22 = 1.0 - (f_t_xx + f_t_zz)
    a23 = f_t_yz - f_t_wx
    a31 = f_t_xz - f_t_wy
    a32 = f_t_yz + f_t_wx
    a33 = 1.0 - (f_t_xx + f_t_yy)

    {a11, a12, a13, 0.0, a21, a22, a23, 0.0, a31, a32, a33, 0.0, 0.0, 0.0, 0.0, 1.0}
  end

  @doc """
  `dot(lhs, rhs)` returns a `float` resultant of the dot product bectween two quaterns.

  `lhs` is a quatern

  `rhs` is a quatern

  It returns a `float` representing the dot product.
  """
  @spec dot(quatern, quatern) :: float
  def dot({w, x, y, z}, {a, b, c, d})
      when is_float(w) and is_float(x) and is_float(y) and is_float(z) and
             is_float(a) and is_float(b) and is_float(c) and is_float(d),
      do: w * a + x * b + y * c + z * d

  def dot({w, x, y, z}, {a, b, c, d}), do: 1.0 * w * a + x * b + y * c + z * d

  @doc """
  `norm(quat)` Returns the L2 norm of a quaternion.

  `quat` is a `quatern` to find the norm of.

  It returns a `float` representing the L2 norm.
  """
  @spec norm(quatern) :: float
  def norm({w, x, y, z}) when is_float(w) and is_float(x) and is_float(y) and is_float(z),
    do: :math.sqrt(w * w + x * x + y * y + z * z)

  def norm({w, x, y, z}), do: :math.sqrt(w * w + x * x + y * y + z * z)

  @doc """
  `normalize_strict(q)` returns a normalized verison of a quaternion.

  `q` is the `quatern` to be normalized.

  This returns a `quatern` of unit length in the same direction as `q`.

  If the magnitude of the quaternion is 0, it will explode.
  """
  @spec normalize_strict(quatern) :: quatern
  def normalize_strict({w, x, y, z} = _q)
      when is_float(w) and is_float(x) and is_float(y) and is_float(z) do
    inv_mag = 1.0 / :math.sqrt(w * w + x * x + y * y + z * z)
    {w * inv_mag, x * inv_mag, y * inv_mag, z * inv_mag}
  end

  def normalize_strict({w, x, y, z} = _q) do
    inv_mag = 1.0 / :math.sqrt(w * w + x * x + y * y + z * z)
    {w * inv_mag, x * inv_mag, y * inv_mag, z * inv_mag}
  end

  @doc """
  `normalize(q)` returns a normalized verison of a quaternion.

  `q` is the `quatern` to be normalized.

  This returns a `quatern` of unit length in the same direction as `q`.

  If the magnitude of the quaternion is 0, it will return the zero quaternion.
  """
  @spec normalize(quatern) :: quatern
  def normalize({w, x, y, z} = _q)
      when is_float(w) and is_float(x) and is_float(y) and is_float(z) do
    mag = :math.sqrt(w * w + x * x + y * y + z * z)

    if mag > 0 do
      inv_mag = 1.0 / :math.sqrt(w * w + x * x + y * y + z * z)
      {w * inv_mag, x * inv_mag, y * inv_mag, z * inv_mag}
    else
      {0.0, 0.0, 0.0, 0.0}
    end
  end

  def normalize({w, x, y, z} = _q) do
    mag = :math.sqrt(w * w + x * x + y * y + z * z)

    if mag > 0 do
      inv_mag = 1.0 / :math.sqrt(w * w + x * x + y * y + z * z)
      {w * inv_mag, x * inv_mag, y * inv_mag, z * inv_mag}
    else
      {0.0, 0.0, 0.0, 0.0}
    end
  end

  @doc """
  `inverse(quat)` returns the inverse of a quaternion.

  `quat` is the quaternion

  It returns a `quatern` representing the inverse of the parameter quaternion.

  If the `quat` is less than or equal to zero, the quaternion returned is a zero quaternion.
  """
  @spec inverse(quatern) :: quatern
  def inverse({w, x, y, z}) when is_float(w) and is_float(x) and is_float(y) and is_float(z) do
    f_norm = w * w + x * x + y * y + z * z

    if f_norm > 0.0 do
      f_inv_norm = 1.0 / f_norm
      {w * f_inv_norm, -x * f_inv_norm, -y * f_inv_norm, -z * f_inv_norm}
    else
      {0.0, 0.0, 0.0, 0.0}
    end
  end

  def inverse({w, x, y, z}) do
    f_norm = 1.0 * w * w + x * x + y * y + z * z

    if f_norm > 0.0 do
      f_inv_norm = 1.0 / f_norm
      {w * f_inv_norm, -x * f_inv_norm, -y * f_inv_norm, -z * f_inv_norm}
    else
      {0.0, 0.0, 0.0, 0.0}
    end
  end

  @doc """
  `conjugate(quat)` returns the conjugate of a quaternion.

  `quat` is the quaternion to get the conjugate of.

  It returns a `quatern` representing the inverse of the unit quatern.

  Note that the conjugate of a unit quaternion is its inverse.
  """
  @spec conjugate(quatern) :: quatern
  def conjugate({w, x, y, z}) when is_float(w) and is_float(x) and is_float(y) and is_float(z),
    do: {w, -x, -y, -z}

  def conjugate({w, x, y, z}), do: {w, -x, -y, -z}

  @doc """
  `slerp(lhs, rhs, t)` Performs Spherical linear interpolation between two quaternions, and returns the result.

  `lhs` is the first `quatern`

  `rhs` is the second `quatern`

  `t` is the interpolation parameter that will interpolate to `lhs` when `t = 0` and to `rhs` when `t = 1`.

  It returns a `quatern` representing the normalized interpolation point.

  Note: `slerp` has the proprieties of performing the interpolation at constant velocity However, it's NOT commutative, which means
  `slerp( A, B, 0.75 ) != slerp( B, A, 0.25 )`
  therefore be careful if your code relies in the order of the operands.
  This is specially important in IK animation.
  """
  @spec slerp(quatern, quatern, float) :: quatern
  def slerp({w, x, y, z} = lhs, {a, b, c, d} = rhs, t)
      when is_float(w) and is_float(x) and is_float(y) and is_float(z) and
             is_float(a) and is_float(b) and is_float(c) and is_float(d) do
    f_cos = dot(lhs, rhs)

    # There are two situations:
    # 1. "rhs" and "lhs" are very close (fCos ~= +1), so we can do a linear
    #    interpolation safely.
    # 2. "rhs" and "lhs" are almost inverse of each other (fCos ~= -1), there
    #    are an infinite number of possibilities interpolation. but we haven't
    #    have method to fix this case, so just use linear interpolation here.

    if abs(f_cos) < 1 - 1.0e-03 do
      f_sin = :math.sqrt(1 - f_cos * f_cos)
      f_angle = :math.atan2(f_sin, f_cos)
      f_inv_sin = 1.0 / f_sin
      f_coeff0 = :math.sin((1.0 - t) * f_angle) * f_inv_sin
      f_coeff1 = :math.sin(t * f_angle) * f_inv_sin
      normalize(add(scale(lhs, f_coeff0), scale(rhs, f_coeff1)))
    else
      r = add(scale(lhs, 1.0 - t), scale(rhs, t))
      # taking the complement requires renormalisation
      normalize(r)
    end
  end

  def slerp(lhs, rhs, t) do
    f_cos = dot(lhs, rhs)

    # There are two situations:
    # 1. "rhs" and "lhs" are very close (fCos ~= +1), so we can do a linear
    #    interpolation safely.
    # 2. "rhs" and "lhs" are almost inverse of each other (fCos ~= -1), there
    #    are an infinite number of possibilities interpolation. but we haven't
    #    have method to fix this case, so just use linear interpolation here.

    if abs(f_cos) < 1 - 1.0e-03 do
      f_sin = :math.sqrt(1 - f_cos * f_cos)
      f_angle = :math.atan2(f_sin, f_cos)
      f_inv_sin = 1.0 / f_sin
      f_coeff0 = :math.sin((1.0 - t) * f_angle) * f_inv_sin
      f_coeff1 = :math.sin(t * f_angle) * f_inv_sin
      normalize(add(scale(lhs, f_coeff0), scale(rhs, f_coeff1)))
    else
      r = add(scale(lhs, 1.0 - t), scale(rhs, t))
      # taking the complement requires renormalisation
      normalize(r)
    end
  end

  @doc """
  `transform_vector(q,v)` transforms a vector v by an orientation quaternion q.

  `q` is an orientation quaternion.

  `v` is a Vec3 to transform--it need not be normalized.

  It returns a `Vec3` of `v` having undergone the rotation represented by `q`.
  """
  @spec transform_vector(quatern, vec3) :: vec3
  def transform_vector({qw, qx, qy, qz}, {vx, vy, vz})
      when is_float(qw) and is_float(qx) and is_float(qy) and is_float(qz) and
             is_float(vx) and is_float(vy) and is_float(vz) do
    # v' = qvq', but we'll use the rediscovered formula of rodrigues answer from SO ( https://gamedev.stackexchange.com/a/50545 )

    dot_uv = qx * vx + qy * vy + qz * vz
    two_dot_uv = 2.0 * dot_uv
    dot_uu = qx * qx + qy * qy + qz * qz
    v_scalar = qw * qw - dot_uu
    two_qw = 2.0 * qw

    {
      two_dot_uv * qx + v_scalar * vx + two_qw * (qy * vz - qz * vy),
      two_dot_uv * qy + v_scalar * vy + two_qw * (qz * vx - qx * vz),
      two_dot_uv * qz + v_scalar * vz + two_qw * (qx * vy - qy * vx)
    }
  end

  def transform_vector({qw, qx, qy, qz}, {vx, vy, vz}) do
    # v' = qvq', but we'll use the rediscovered formula of rodrigues answer from SO ( https://gamedev.stackexchange.com/a/50545 )

    dot_uv = qx * vx + qy * vy + qz * vz
    two_dot_uv = 2.0 * dot_uv
    dot_uu = qx * qx + qy * qy + qz * qz
    v_scalar = qw * qw - dot_uu
    two_qw = 2.0 * qw

    {
      two_dot_uv * qx + v_scalar * vx + two_qw * (qy * vz - qz * vy),
      two_dot_uv * qy + v_scalar * vy + two_qw * (qz * vx - qx * vz),
      two_dot_uv * qz + v_scalar * vz + two_qw * (qx * vy - qy * vx)
    }
  end

  @doc """
  `integrate(q, omega, dt)` integrates the angular velocty omega over a timestep dt with intial orientation q.

  `q` is an orientation quaternion to use as the initial orientation.

  `omega` is a `vec3` whose direction is the axis of rotation and whose magnitude is the velocity about that axis.

  `dt` is the timestep over which to apply the angular velocity.

  It returns an orientation `quatern`.
  """
  @spec integrate(quatern, vec3, number) :: quatern
  def integrate({qw, qx, qy, qz} = q, {ox, oy, oz} = omega, dt)
      when is_float(qw) and is_float(qx) and is_float(qy) and is_float(qz) and is_float(ox) and
             is_float(oy) and is_float(oz) and is_float(dt) do
    # this routine inspired and adapted from http://physicsforgames.blogspot.com/2010/02/quaternions.html
    # this explains a similar routine in cannon.js

    # get convert angular velocity vector to actual angular displacement by integrating time
    {theta_x, theta_y, theta_z} = theta = Graphmath.Vec3.scale(omega, 0.5 * dt)
    theta_magnitude_squared = Graphmath.Vec3.length_squared(theta)

    # use small-angle approximation for sin/cos if the magnitude is too small
    {delta_q_w, s} =
      if theta_magnitude_squared * theta_magnitude_squared / 24.0 < @machine_small_float do
        # use the more stable Taylor series for low-angle appromixations to sin/cos
        {1.0 - theta_magnitude_squared / 2.0, 1.0 - theta_magnitude_squared / 6.0}
      else
        # we're not too small! use real sin/cos
        theta_magnitude = :math.sqrt(theta_magnitude_squared)
        {:math.cos(theta_magnitude), :math.sin(theta_magnitude) / theta_magnitude}
      end

    multiply({delta_q_w, theta_x * s, theta_y * s, theta_z * s}, q)
    |> normalize()
  end

  def integrate(q, omega, dt) do
    # this routine inspired and adapted from http://physicsforgames.blogspot.com/2010/02/quaternions.html
    # this explains a similar routine in cannon.js

    # get convert angular velocity vector to actual angular displacement by integrating time
    {theta_x, theta_y, theta_z} = theta = Graphmath.Vec3.scale(omega, 0.5 * dt)
    theta_magnitude_squared = Graphmath.Vec3.length_squared(theta)

    # use small-angle approximation for sin/cos if the magnitude is too small
    {delta_q_w, s} =
      if theta_magnitude_squared * theta_magnitude_squared / 24.0 < @machine_small_float do
        # use the more stable Taylor series for low-angle appromixations to sin/cos
        {1.0 - theta_magnitude_squared / 2.0, 1.0 - theta_magnitude_squared / 6.0}
      else
        # we're not too small! use real sin/cos
        theta_magnitude = :math.sqrt(theta_magnitude_squared)
        {:math.cos(theta_magnitude), :math.sin(theta_magnitude) / theta_magnitude}
      end

    multiply({delta_q_w, theta_x * s, theta_y * s, theta_z * s}, q)
    |> normalize()
  end

  @doc """
  Genrate a random quatenrion (rotation on SO3), using the algorithm given [here](http://planning.cs.uiuc.edu/node198.html).

  It returns an orientation `quatern`.
  """
  @spec random() :: quatern
  def random() do
    u1 = :rand.uniform()
    u2 = :rand.uniform()
    u3 = :rand.uniform()

    sqrtomu1 = :math.sqrt(1.0 - u1)
    sqrtu1 = :math.sqrt(u1)
    pi = :math.pi()
    s2pu2 = :math.sin(2.0 * pi * u2)
    c2pu2 = :math.cos(2.0 * pi * u2)
    s2pu3 = :math.sin(2.0 * pi * u3)
    c2pu3 = :math.cos(2.0 * pi * u3)

    {sqrtomu1 * s2pu2, sqrtomu1 * c2pu2, sqrtu1 * s2pu3, sqrtu1 * c2pu3}
  end
end
