/******************************************************************************/
/*
  Project   - MudBun
  Publisher - Long Bunny Labs
              http://LongBunnyLabs.com
  Author    - Ming-Lun "Allen" Chou
              http://AllenChou.net
*/
/******************************************************************************/

using UnityEngine;

namespace MudBun
{
  public class QuaternionUtil
  {
    // basic stuff
    // ------------------------------------------------------------------------

    public static float Magnitude(Quaternion q)
    {
      return Mathf.Sqrt(q.x * q.x + q.y * q.y + q.z * q.z + q.w * q.w);
    }

    public static float MagnitudeSqr(Quaternion q)
    {
      return q.x * q.x + q.y * q.y + q.z * q.z + q.w * q.w;
    }

    public static Quaternion Normalize(Quaternion q)
    {
      float magInv = 1.0f / Magnitude(q);
      return new Quaternion(magInv * q.x, magInv * q.y, magInv * q.z, magInv * q.w);
    }

    public static Quaternion AngularVector(Vector3 v)
    {
      float len = v.magnitude;
      if (len < MathUtil.Epsilon)
        return Quaternion.identity;

      v /= len;

      float h = 0.5f * len;
      float s = Mathf.Sin(h);
      float c = Mathf.Cos(h);

      return new Quaternion(s * v.x, s * v.y, s * v.z, c);
    }

    // axis must be normalized
    public static Quaternion AxisAngle(Vector3 axis, float angle)
    {
      float h = 0.5f * angle;
      float s = Mathf.Sin(h);
      float c = Mathf.Cos(h);

      return new Quaternion(s * axis.x, s * axis.y, s * axis.z, c);
    }

    public static Vector3 GetAxis(Quaternion q)
    {
      Vector3 v = new Vector3(q.x, q.y, q.z);
      float len = v.magnitude;
      if (len < MathUtil.Epsilon)
        return Vector3.left;

      return v / len;
    }

    public static float GetAngle(Quaternion q)
    {
      return 2.0f * Mathf.Acos(Mathf.Clamp(q.w, -1.0f, 1.0f));
    }

    public static Quaternion Pow(Quaternion q, float exp)
    {
      Vector3 axis = GetAxis(q);
      float angle = GetAngle(q);
      return AxisAngle(axis, angle * exp);
    }

    // v: derivative of q
    public static Quaternion Integrate(Quaternion q, Quaternion v, float dt)
    {
      return Pow(v, dt) * q;
    }

    // omega: angular velocity (direction is axis, magnitude is angle)
    // https://www.ashwinnarayan.com/post/how-to-integrate-quaternions/
    // https://gafferongames.com/post/physics_in_3d/
    public static Quaternion Integrate(Quaternion q, Vector3 omega, float dt)
    {
      omega *= 0.5f;
      Quaternion p = (new Quaternion(omega.x, omega.y, omega.z, 0.0f)) * q;
      return Normalize(new Quaternion(q.x + p.x * dt, q.y + p.y * dt, q.z + p.z * dt, q.w + p.w * dt));
    }

    public static Vector4 ToVector4(Quaternion q)
    {
      return new Vector4(q.x, q.y, q.z, q.w);
    }

    public static Quaternion FromVector4(Vector4 v, bool normalize = true)
    {
      if (normalize)
      {
        float magSqr = v.sqrMagnitude;
        if (magSqr < MathUtil.Epsilon)
          return Quaternion.identity;

        v /= Mathf.Sqrt(magSqr);
      }

      return new Quaternion(v.x, v.y, v.z, v.w);
    }

    // ------------------------------------------------------------------------
    // end: basic stuff


    // swing-twist decomposition & interpolation
    // ------------------------------------------------------------------------

    public static void DecomposeSwingTwist
    (
      Quaternion q, 
      Vector3 twistAxis, 
      out Quaternion swing, 
      out Quaternion twist
    )
    {
      Vector3 r = new Vector3(q.x, q.y, q.z); // (rotaiton axis) * cos(angle / 2)

      // singularity: rotation by 180 degree
      if (r.sqrMagnitude < MathUtil.Epsilon)
      {
        Vector3 rotatedTwistAxis = q * twistAxis;
        Vector3 swingAxis = Vector3.Cross(twistAxis, rotatedTwistAxis);

        if (swingAxis.sqrMagnitude > MathUtil.Epsilon)
        {
          float swingAngle = Vector3.Angle(twistAxis, rotatedTwistAxis);
          swing = Quaternion.AngleAxis(swingAngle, swingAxis);
        }
        else
        {
          // more singularity: rotation axis parallel to twist axis
          swing = Quaternion.identity; // no swing
        }

        // always twist 180 degree on singularity
        twist = Quaternion.AngleAxis(180.0f, twistAxis);
        return;
      }

      // formula & proof: 
      // http://www.euclideanspace.com/maths/geometry/rotations/for/decomposition/
      Vector3 p = Vector3.Project(r, twistAxis);
      twist = new Quaternion(p.x, p.y, p.z, q.w);
      twist = Normalize(twist);
      swing = q * Quaternion.Inverse(twist);
    }

    public enum SterpMode
    {
      // non-constant angular velocity, faster
      // use if interpolating across small angles or constant angular velocity is not important
      Nlerp,

      // constant angular velocity, slower
      // use if interpolating across large angles and constant angular velocity is important
      Slerp, 
    };

    // same swing & twist parameters
    public static Quaternion Sterp
    (
      Quaternion a, 
      Quaternion b, 
      Vector3 twistAxis, 
      float t, 
      SterpMode mode = SterpMode.Slerp
    )
    {
      Quaternion swing;
      Quaternion twist;
      return Sterp(a, b, twistAxis, t, out swing, out twist, mode);
    }

    // same swing & twist parameters with individual interpolated swing & twist outputs
    public static Quaternion Sterp
    (
      Quaternion a, 
      Quaternion b, 
      Vector3 twistAxis, 
      float t, 
      out Quaternion swing, 
      out Quaternion twist, 
      SterpMode mode = SterpMode.Slerp
    )
    {
      return Sterp(a, b, twistAxis, t, t, out swing, out twist, mode);
    }

    // different swing & twist parameters
    public static Quaternion Sterp
    (
      Quaternion a, 
      Quaternion b, 
      Vector3 twistAxis, 
      float tSwing, 
      float tTwist, 
      SterpMode mode = SterpMode.Slerp
    )
    {
      Quaternion swing;
      Quaternion twist;
      return Sterp(a, b, twistAxis, tSwing, tTwist, out swing, out twist, mode);
    }

    // master sterp function
    public static Quaternion Sterp
    (
      Quaternion a, 
      Quaternion b, 
      Vector3 twistAxis, 
      float tSwing, 
      float tTwist, 
      out Quaternion swing, 
      out Quaternion twist, 
      SterpMode mode
    )
    {
      Quaternion q = b * Quaternion.Inverse(a);
      Quaternion swingFull;
      Quaternion twistFull;
      QuaternionUtil.DecomposeSwingTwist(q, twistAxis, out swingFull, out twistFull);

      switch (mode)
      {
        default:
        case SterpMode.Nlerp:
          swing = Quaternion.Lerp(Quaternion.identity, swingFull, tSwing);
          twist = Quaternion.Lerp(Quaternion.identity, twistFull, tTwist);
          break;
        case SterpMode.Slerp:
          swing = Quaternion.Slerp(Quaternion.identity, swingFull, tSwing);
          twist = Quaternion.Slerp(Quaternion.identity, twistFull, tTwist);
          break;
      }

      return twist * swing;
    }

    // ------------------------------------------------------------------------
    // end: swing-twist decomposition & interpolation
  }
}

