// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved.

#include "Quaternion.h"
#include "Vector3.h"
#include "Vector4.h"
#include "Matrix.h"
#include "Matrix3x3.h"
#include "Math.h"
#include "../Types/String.h"
#include "Engine/Core/Math/Transform.h"

Quaternion Quaternion::Zero(0, 0, 0, 0);
Quaternion Quaternion::One(1, 1, 1, 1);
Quaternion Quaternion::Identity(0, 0, 0, 1);

Quaternion::Quaternion(const Float4& value)
    : X(value.X)
    , Y(value.Y)
    , Z(value.Z)
    , W(value.W)
{
}

String Quaternion::ToString() const
{
    return String::Format(TEXT("{}"), *this);
}

float Quaternion::GetAngle() const
{
    const float length = X * X + Y * Y + Z * Z;
    if (Math::IsZero(length))
        return 0.0f;
    return 2.0f * acosf(Math::Clamp(W, -1.0f, 1.0f));
}

Float3 Quaternion::GetAxis() const
{
    const float length = X * X + Y * Y + Z * Z;
    if (Math::IsZero(length))
        return Float3::UnitX;
    const float inv = 1.0f / Math::Sqrt(length);
    return Float3(X * inv, Y * inv, Z * inv);
}

Float3 Quaternion::GetEuler() const
{
    Float3 result;
    const float sqw = W * W;
    const float sqx = X * X;
    const float sqy = Y * Y;
    const float sqz = Z * Z;
    const float unit = sqx + sqy + sqz + sqw; // if normalised is one, otherwise is correction factor
    const float test = X * W - Y * Z;

    if (test > 0.499995f * unit)
    {
        // singularity at north pole

        // yaw pitch roll
        result.Y = 2.0f * Math::Atan2(Y, X);
        result.X = PI_OVER_2;
        result.Z = 0;
    }
    else if (test < -0.499995f * unit)
    {
        // singularity at south pole

        // yaw pitch roll
        result.Y = -2.0f * Math::Atan2(Y, X);
        result.X = -PI_OVER_2;
        result.Z = 0;
    }
    else
    {
        // yaw pitch roll
        const Quaternion q = Quaternion(W, Z, X, Y);
        result.Y = Math::Atan2(2.0f * q.X * q.W + 2.0f * q.Y * q.Z, 1 - 2.0f * (q.Z * q.Z + q.W * q.W));
        result.X = Math::Asin(2.0f * (q.X * q.Z - q.W * q.Y));
        result.Z = Math::Atan2(2.0f * q.X * q.Y + 2.0f * q.Z * q.W, 1 - 2.0f * (q.Y * q.Y + q.Z * q.Z));
    }

    return Math::UnwindDegrees(result * RadiansToDegrees);
}

void Quaternion::Multiply(const Quaternion& other)
{
    const float a = Y * other.Z - Z * other.Y;
    const float b = Z * other.X - X * other.Z;
    const float c = X * other.Y - Y * other.X;
    const float d = X * other.X + Y * other.Y + Z * other.Z;
    X = X * other.W + other.X * W + a;
    Y = Y * other.W + other.Y * W + b;
    Z = Z * other.W + other.Z * W + c;
    W = W * other.W - d;
}

Float3 Quaternion::operator*(const Float3& vector) const
{
    return Float3::Transform(vector, *this);
}

void Quaternion::Add(const Quaternion& left, const Quaternion& right, Quaternion& result)
{
    result.X = left.X + right.X;
    result.Y = left.Y + right.Y;
    result.Z = left.Z + right.Z;
    result.W = left.W + right.W;
}

void Quaternion::Subtract(const Quaternion& left, const Quaternion& right, Quaternion& result)
{
    result.X = left.X - right.X;
    result.Y = left.Y - right.Y;
    result.Z = left.Z - right.Z;
    result.W = left.W - right.W;
}

void Quaternion::Multiply(const Quaternion& left, const Quaternion& right, Quaternion& result)
{
    const float a = left.Y * right.Z - left.Z * right.Y;
    const float b = left.Z * right.X - left.X * right.Z;
    const float c = left.X * right.Y - left.Y * right.X;
    const float d = left.X * right.X + left.Y * right.Y + left.Z * right.Z;
    result.X = left.X * right.W + right.X * left.W + a;
    result.Y = left.Y * right.W + right.Y * left.W + b;
    result.Z = left.Z * right.W + right.Z * left.W + c;
    result.W = left.W * right.W - d;
}

void Quaternion::Negate(const Quaternion& value, Quaternion& result)
{
    result.X = -value.X;
    result.Y = -value.Y;
    result.Z = -value.Z;
    result.W = -value.W;
}

void Quaternion::Lerp(const Quaternion& start, const Quaternion& end, float amount, Quaternion& result)
{
    const float inverse = 1.0f - amount;
    if (Dot(start, end) >= 0.0f)
    {
        result.X = inverse * start.X + amount * end.X;
        result.Y = inverse * start.Y + amount * end.Y;
        result.Z = inverse * start.Z + amount * end.Z;
        result.W = inverse * start.W + amount * end.W;
    }
    else
    {
        result.X = inverse * start.X - amount * end.X;
        result.Y = inverse * start.Y - amount * end.Y;
        result.Z = inverse * start.Z - amount * end.Z;
        result.W = inverse * start.W - amount * end.W;
    }
    result.Normalize();
}

void Quaternion::RotationAxis(const Float3& axis, float angle, Quaternion& result)
{
    Float3 normalized;
    Float3::Normalize(axis, normalized);

    const float half = angle * 0.5f;
    const float sinHalf = Math::Sin(half);
    const float cosHalf = Math::Cos(half);

    result.X = normalized.X * sinHalf;
    result.Y = normalized.Y * sinHalf;
    result.Z = normalized.Z * sinHalf;
    result.W = cosHalf;
}

void Quaternion::RotationCosAxis(const Float3& axis, float cos, Quaternion& result)
{
    Float3 normalized;
    Float3::Normalize(axis, normalized);

    const float cosHalf2 = (1.0f + cos) * 0.5f;
    const float sinHalf2 = 1.0f - cosHalf2;
    const float cosHalf = Math::Sqrt(cosHalf2);
    const float sinHalf = Math::Sqrt(sinHalf2);

    result.X = normalized.X * sinHalf;
    result.Y = normalized.Y * sinHalf;
    result.Z = normalized.Z * sinHalf;
    result.W = cosHalf;
}

void Quaternion::RotationMatrix(const Matrix& matrix, Quaternion& result)
{
    float sqrtV;
    float half;
    const float scale = matrix.M11 + matrix.M22 + matrix.M33;

    if (scale > 0.0f)
    {
        sqrtV = Math::Sqrt(scale + 1.0f);
        result.W = sqrtV * 0.5f;
        sqrtV = 0.5f / sqrtV;

        result.X = (matrix.M23 - matrix.M32) * sqrtV;
        result.Y = (matrix.M31 - matrix.M13) * sqrtV;
        result.Z = (matrix.M12 - matrix.M21) * sqrtV;
    }
    else if (matrix.M11 >= matrix.M22 && matrix.M11 >= matrix.M33)
    {
        sqrtV = Math::Sqrt(1.0f + matrix.M11 - matrix.M22 - matrix.M33);
        half = 0.5f / sqrtV;

        result = Quaternion(
            0.5f * sqrtV,
            (matrix.M12 + matrix.M21) * half,
            (matrix.M13 + matrix.M31) * half,
            (matrix.M23 - matrix.M32) * half);
    }
    else if (matrix.M22 > matrix.M33)
    {
        sqrtV = Math::Sqrt(1.0f + matrix.M22 - matrix.M11 - matrix.M33);
        half = 0.5f / sqrtV;

        result = Quaternion(
            (matrix.M21 + matrix.M12) * half,
            0.5f * sqrtV,
            (matrix.M32 + matrix.M23) * half,
            (matrix.M31 - matrix.M13) * half);
    }
    else
    {
        sqrtV = Math::Sqrt(1.0f + matrix.M33 - matrix.M11 - matrix.M22);
        half = 0.5f / sqrtV;

        result = Quaternion(
            (matrix.M31 + matrix.M13) * half,
            (matrix.M32 + matrix.M23) * half,
            0.5f * sqrtV,
            (matrix.M12 - matrix.M21) * half);
    }

    result.Normalize();
}

void Quaternion::RotationMatrix(const Matrix3x3& matrix, Quaternion& result)
{
    float sqrtV;
    float half;
    const float scale = matrix.M11 + matrix.M22 + matrix.M33;

    if (scale > 0.0f)
    {
        sqrtV = Math::Sqrt(scale + 1.0f);
        result.W = sqrtV * 0.5f;
        sqrtV = 0.5f / sqrtV;

        result.X = (matrix.M23 - matrix.M32) * sqrtV;
        result.Y = (matrix.M31 - matrix.M13) * sqrtV;
        result.Z = (matrix.M12 - matrix.M21) * sqrtV;
    }
    else if (matrix.M11 >= matrix.M22 && matrix.M11 >= matrix.M33)
    {
        sqrtV = Math::Sqrt(1.0f + matrix.M11 - matrix.M22 - matrix.M33);
        half = 0.5f / sqrtV;

        result = Quaternion(
            0.5f * sqrtV,
            (matrix.M12 + matrix.M21) * half,
            (matrix.M13 + matrix.M31) * half,
            (matrix.M23 - matrix.M32) * half);
    }
    else if (matrix.M22 > matrix.M33)
    {
        sqrtV = Math::Sqrt(1.0f + matrix.M22 - matrix.M11 - matrix.M33);
        half = 0.5f / sqrtV;

        result = Quaternion(
            (matrix.M21 + matrix.M12) * half,
            0.5f * sqrtV,
            (matrix.M32 + matrix.M23) * half,
            (matrix.M31 - matrix.M13) * half);
    }
    else
    {
        sqrtV = Math::Sqrt(1.0f + matrix.M33 - matrix.M11 - matrix.M22);
        half = 0.5f / sqrtV;

        result = Quaternion(
            (matrix.M31 + matrix.M13) * half,
            (matrix.M32 + matrix.M23) * half,
            0.5f * sqrtV,
            (matrix.M12 - matrix.M21) * half);
    }

    result.Normalize();
}

void Quaternion::LookAt(const Float3& eye, const Float3& target, const Float3& up, Quaternion& result)
{
    Matrix matrix;
    Matrix::LookAt(eye, target, up, matrix);
    RotationMatrix(matrix, result);
}

void Quaternion::RotationLookAt(const Float3& forward, const Float3& up, Quaternion& result)
{
    LookAt(Float3::Zero, forward, up, result);
}

void Quaternion::Billboard(const Float3& objectPosition, const Float3& cameraPosition, const Float3& cameraUpVector, const Float3& cameraForwardVector, Quaternion& result)
{
    Matrix matrix;
    Matrix::Billboard(objectPosition, cameraPosition, cameraUpVector, cameraForwardVector, matrix);
    RotationMatrix(matrix, result);
}

Quaternion Quaternion::FromDirection(const Float3& direction)
{
    CHECK_RETURN_DEBUG(direction.IsNormalized(), Quaternion::Identity);
    Quaternion orientation;
    if (Float3::Dot(direction, Float3::Up) >= 0.999f)
    {
        RotationAxis(Float3::Left, PI_OVER_2, orientation);
    }
    else if (Float3::Dot(direction, Float3::Down) >= 0.999f)
    {
        RotationAxis(Float3::Right, PI_OVER_2, orientation);
    }
    else
    {
        Float3 right, up;
        Float3::Cross(direction, Float3::Up, right);
        Float3::Cross(right, direction, up);
        LookRotation(direction, up, orientation);
    }
    return orientation;
}

void Quaternion::LookRotation(const Float3& forward, const Float3& up, Quaternion& result)
{
    Float3 forwardNorm = forward;
    forwardNorm.Normalize();
    Float3 rightNorm;
    Float3::Cross(up, forwardNorm, rightNorm);
    rightNorm.Normalize();
    Float3 upNorm;
    Float3::Cross(forwardNorm, rightNorm, upNorm);

#define m00 rightNorm.X
#define m01 rightNorm.Y
#define m02 rightNorm.Z
#define m10 upNorm.X
#define m11 upNorm.Y
#define m12 upNorm.Z
#define m20 forwardNorm.X
#define m21 forwardNorm.Y
#define m22 forwardNorm.Z

    const float sum = m00 + m11 + m22;
    if (sum > 0)
    {
        const float num = Math::Sqrt(sum + 1);
        const float invNumHalf = 0.5f / num;
        result.X = (m12 - m21) * invNumHalf;
        result.Y = (m20 - m02) * invNumHalf;
        result.Z = (m01 - m10) * invNumHalf;
        result.W = num * 0.5f;
    }
    else if (m00 >= m11 && m00 >= m22)
    {
        const float num = Math::Sqrt(1 + m00 - m11 - m22);
        const float invNumHalf = 0.5f / num;
        result.X = 0.5f * num;
        result.Y = (m01 + m10) * invNumHalf;
        result.Z = (m02 + m20) * invNumHalf;
        result.W = (m12 - m21) * invNumHalf;
    }
    else if (m11 > m22)
    {
        const float num = Math::Sqrt(1 + m11 - m00 - m22);
        const float invNumHalf = 0.5f / num;
        result.X = (m10 + m01) * invNumHalf;
        result.Y = 0.5f * num;
        result.Z = (m21 + m12) * invNumHalf;
        result.W = (m20 - m02) * invNumHalf;
    }
    else
    {
        const float num = Math::Sqrt(1 + m22 - m00 - m11);
        const float invNumHalf = 0.5f / num;
        result.X = (m20 + m02) * invNumHalf;
        result.Y = (m21 + m12) * invNumHalf;
        result.Z = 0.5f * num;
        result.W = (m01 - m10) * invNumHalf;
    }

#undef m00
#undef m01
#undef m02
#undef m10
#undef m11
#undef m12
#undef m20
#undef m21
#undef m22
}

void Quaternion::GetRotationFromTo(const Float3& from, const Float3& to, Quaternion& result, const Float3& fallbackAxis)
{
    // Based on Stan Melax's article in Game Programming Gems

    Float3 v0 = from;
    Float3 v1 = to;
    v0.Normalize();
    v1.Normalize();

    // If dot == 1, vectors are the same
    const float d = Float3::Dot(v0, v1);
    if (d >= 1.0f)
    {
        result = Identity;
        return;
    }

    if (d < 1e-6f - 1.0f)
    {
        if (fallbackAxis != Float3::Zero)
        {
            // Rotate 180 degrees about the fallback axis
            RotationAxis(fallbackAxis, PI, result);
        }
        else
        {
            // Generate an axis
            Float3 axis = Float3::Cross(Float3::UnitX, from);
            if (axis.LengthSquared() < ZeroTolerance) // Pick another if colinear
                axis = Float3::Cross(Float3::UnitY, from);
            axis.Normalize();
            RotationAxis(axis, PI, result);
        }
    }
    else
    {
        const float s = Math::Sqrt((1 + d) * 2);
        const float invS = 1 / s;

        Float3 c;
        Float3::Cross(v0, v1, c);

        result.X = c.X * invS;
        result.Y = c.Y * invS;
        result.Z = c.Z * invS;
        result.W = s * 0.5f;
        result.Normalize();
    }
}

void Quaternion::FindBetween(const Float3& from, const Float3& to, Quaternion& result)
{
    // http://lolengine.net/blog/2014/02/24/quaternion-from-two-vectors-final
    const float normFromNormTo = Math::Sqrt(from.LengthSquared() * to.LengthSquared());
    if (normFromNormTo < ZeroTolerance)
    {
        result = Identity;
        return;
    }
    const float w = normFromNormTo + Float3::Dot(from, to);
    if (w < 1.e-6f * normFromNormTo)
    {
        result = Math::Abs(from.X) > Math::Abs(from.Z)
                     ? Quaternion(-from.Y, from.X, 0.0f, 0.0f)
                     : Quaternion(0.0f, -from.Z, from.Y, 0.0f);
    }
    else
    {
        const Float3 cross = Float3::Cross(from, to);
        result = Quaternion(cross.X, cross.Y, cross.Z, w);
    }
    result.Normalize();
}

void Quaternion::Slerp(const Quaternion& start, const Quaternion& end, float amount, Quaternion& result)
{
    float opposite;
    float inverse;
    const float dot = Dot(start, end);

    if (Math::Abs(dot) > 1.0f - ZeroTolerance)
    {
        inverse = 1.0f - amount;
        opposite = amount * Math::Sign(dot);
    }
    else
    {
        const float acos1 = Math::Acos(Math::Abs(dot));
        const float invSin = 1.0f / Math::Sin(acos1);
        inverse = Math::Sin((1.0f - amount) * acos1) * invSin;
        opposite = Math::Sin(amount * acos1) * invSin * Math::Sign(dot);
    }

    result.X = inverse * start.X + opposite * end.X;
    result.Y = inverse * start.Y + opposite * end.Y;
    result.Z = inverse * start.Z + opposite * end.Z;
    result.W = inverse * start.W + opposite * end.W;
}

Quaternion Quaternion::Euler(float x, float y, float z)
{
    const float halfRoll = z * (DegreesToRadians * 0.5f);
    const float halfPitch = x * (DegreesToRadians * 0.5f);
    const float halfYaw = y * (DegreesToRadians * 0.5f);

    const float sinRollOver2 = Math::Sin(halfRoll);
    const float cosRollOver2 = Math::Cos(halfRoll);
    const float sinPitchOver2 = Math::Sin(halfPitch);
    const float cosPitchOver2 = Math::Cos(halfPitch);
    const float sinYawOver2 = Math::Sin(halfYaw);
    const float cosYawOver2 = Math::Cos(halfYaw);

    return Quaternion(
        cosYawOver2 * sinPitchOver2 * cosRollOver2 + sinYawOver2 * cosPitchOver2 * sinRollOver2,
        sinYawOver2 * cosPitchOver2 * cosRollOver2 - cosYawOver2 * sinPitchOver2 * sinRollOver2,
        cosYawOver2 * cosPitchOver2 * sinRollOver2 - sinYawOver2 * sinPitchOver2 * cosRollOver2,
        cosYawOver2 * cosPitchOver2 * cosRollOver2 + sinYawOver2 * sinPitchOver2 * sinRollOver2
    );
}

Quaternion Quaternion::Euler(const Float3& euler)
{
    const float halfRoll = euler.Z * (DegreesToRadians * 0.5f);
    const float halfPitch = euler.X * (DegreesToRadians * 0.5f);
    const float halfYaw = euler.Y * (DegreesToRadians * 0.5f);

    const float sinRollOver2 = Math::Sin(halfRoll);
    const float cosRollOver2 = Math::Cos(halfRoll);
    const float sinPitchOver2 = Math::Sin(halfPitch);
    const float cosPitchOver2 = Math::Cos(halfPitch);
    const float sinYawOver2 = Math::Sin(halfYaw);
    const float cosYawOver2 = Math::Cos(halfYaw);

    return Quaternion(
        cosYawOver2 * sinPitchOver2 * cosRollOver2 + sinYawOver2 * cosPitchOver2 * sinRollOver2,
        sinYawOver2 * cosPitchOver2 * cosRollOver2 - cosYawOver2 * sinPitchOver2 * sinRollOver2,
        cosYawOver2 * cosPitchOver2 * sinRollOver2 - sinYawOver2 * sinPitchOver2 * cosRollOver2,
        cosYawOver2 * cosPitchOver2 * cosRollOver2 + sinYawOver2 * sinPitchOver2 * sinRollOver2
    );
}

void Quaternion::RotationYawPitchRoll(float yaw, float pitch, float roll, Quaternion& result)
{
    const float halfRoll = roll * 0.5f;
    const float halfPitch = pitch * 0.5f;
    const float halfYaw = yaw * 0.5f;

    const float sinRollOver2 = Math::Sin(halfRoll);
    const float cosRollOver2 = Math::Cos(halfRoll);
    const float sinPitchOver2 = Math::Sin(halfPitch);
    const float cosPitchOver2 = Math::Cos(halfPitch);
    const float sinYawOver2 = Math::Sin(halfYaw);
    const float cosYawOver2 = Math::Cos(halfYaw);

    result.W = cosYawOver2 * cosPitchOver2 * cosRollOver2 + sinYawOver2 * sinPitchOver2 * sinRollOver2;
    result.X = cosYawOver2 * sinPitchOver2 * cosRollOver2 + sinYawOver2 * cosPitchOver2 * sinRollOver2;
    result.Y = sinYawOver2 * cosPitchOver2 * cosRollOver2 - cosYawOver2 * sinPitchOver2 * sinRollOver2;
    result.Z = cosYawOver2 * cosPitchOver2 * sinRollOver2 - sinYawOver2 * sinPitchOver2 * cosRollOver2;
}

Quaternion Quaternion::GetRotationFromNormal(const Vector3& normal, const Transform& reference)
{
    Float3 up = reference.GetUp();
    const Real dot = Vector3::Dot(normal, up);
    if (Math::NearEqual(Math::Abs(dot), 1))
        up = reference.GetRight();
    return Quaternion::LookRotation(normal, up);
}
