/**
 * @file MatrixFunc.h
 * @author Petr Egert (egertpet@fel.cvut.cz)
 * @brief Contains static function for creation of various Matrix matrices
 */

#ifndef MATRIXFUNC_H
#define MATRIXFUNC_H
/*
    This file is a part of Matrix class declaration and should never be included directly!
*/

/**
 * @brief Creates an identity matrix
 * @return Identity matrix
 */
static Matrix<ValType> Identity()
{
    Matrix<ValType> M;
    memset(M.f, 0, 16 * sizeof(ValType));
    M.m[0][0] = M.m[1][1] = M.m[2][2] = M.m[3][3] = 1.0;
    return M;
}

/**
 * @brief Creates a 3D translation matrix
 * @param x Translation in the X direction
 * @param y Translation in the Y direction
 * @param z Translation in the Z direction
 * @return Resulting translation matrix
 */
static Matrix<ValType> Translation(ValType x, ValType y, ValType z)
{
    Matrix<ValType> M = Identity();
    M.f[12] = x;
    M.f[13] = y;
    M.f[14] = z;
    return M;
}

/**
 * @brief Helper function to create a translation matrix using float[3]
 * @param f Float array of size 3
 * @return Resulting translation matrix
 */
static Matrix<ValType> Translation(ValType *f)
{
    return Translation(f[0], f[1], f[2]);
}

/**
 * @brief Helper function to create a translation matrix using Vector3
 * @param v Translation vector
 * @return Resulting translation matrix
 */
static Matrix<ValType> Translation(const Vector3<ValType> &v)
{
    return Translation(v.x, v.y, v.z);
}

/**
 * @brief Creates a 3D scaling matrix
 * @param x Scale in the X direction
 * @param y Scale in the Y direction
 * @param z Scale in the Z direction
 * @return Resulting scaling matrix
 */
static Matrix<ValType> Scaling(ValType x, ValType y, ValType z)
{
    Matrix<ValType> M = Identity();
    M.f[0] = x;
    M.f[5] = y;
    M.f[10] = z;
    return M;
}

/**
 * @brief Helper function to create a scaling matrix using float[3]
 * @param f Float array of size 3
 * @return Resulting scaling matrix
 */
static Matrix<ValType> Scaling(ValType *f)
{
    return Scaling(f[0], f[1], f[2]);
}

/**
 * @brief Helper function to create a scaling matrix using Vector3
 * @param v Scaling vector
 * @return Resulting scaling matrix
 */
static Matrix<ValType> Scaling(const Vector3<ValType> &v)
{
    return Scaling(v.x, v.y, v.z);
}

/**
 * @brief Creates a matrix representing a rotation around the X axis
 * @param angle Angle to rotate (in radians)
 * @return Resulting rotation matrix
 */
static Matrix<ValType> RotationX(ValType angle)
{
    Matrix<ValType> M = Identity();
    M.m[1][1] = M.m[2][2] = cos(angle);
    M.m[1][2] = sin(angle);
    M.m[2][1] = -M.m[1][2];
    return M;
}

/**
 * @brief Creates a matrix representing a rotation around the Y axis
 * @param angle Angle to rotate (in radians)
 * @return Resulting rotation matrix
 */
static Matrix<ValType> RotationY(ValType angle)
{
    Matrix<ValType> M = Identity();
    M.m[0][0] = M.m[2][2] = cos(angle);
    M.m[0][2] = sin(angle);
    M.m[2][0] = -M.m[0][2];
    return M;
}

/**
 * @brief Creates a matrix representing a rotation around the Z axis
 * @param angle Angle to rotate (in radians)
 * @return Resulting rotation matrix
 */
static Matrix<ValType> RotationZ(ValType angle)
{
    Matrix<ValType> M = Identity();
    M.m[0][0] = M.m[1][1] = cos(angle);
    M.m[0][1] = sin(angle);
    M.m[1][0] = -M.m[0][1];
    return M;
}

/**
 * @brief Creates a matrix representing a rotation around the Z axis with an offset center
 *
 * Used mainly for drawing screen space elements
 * @param angle Angle to rotate (in radians)
 * @param offset Offset of the center point
 * @return Resulting rotation matrix
 */
static Matrix<ValType> RotationZOffset(ValType angle, const Vector2<ValType> &offset)
{
    Matrix<ValType> M = Identity();
    M.m[0][0] = M.m[1][1] = cos(angle);
    M.m[0][1] = sin(angle);
    M.m[1][0] = -M.m[0][1];

    M.f[12] = offset.x - M.m[0][0] * offset.x - M.m[1][0] * offset.y;
    M.f[13] = offset.y - M.m[0][1] * offset.x - M.m[1][1] * offset.y;
    return M;
}

/**
 * @brief Creates a 3D rotation matrix
 *
 * Beware of Gimbal lock!
 * @param x Rotation around the X axis (in radians)
 * @param y Rotation around the Y axis (in radians)
 * @param z Rotation around the Z axis (in radians)
 * @return Resulting rotation matrix
 */
static Matrix<ValType> RotationEuler(ValType x, ValType y, ValType z)
{
    Matrix<ValType> M;
    float sx = sin(x);
    float cx = cos(x);
    float sy = sin(y);
    float cy = cos(y);
    float sz = sin(z);
    float cz = cos(z);

    M.f[0] = sy * sz;
    M.f[1] = -sy * cz;
    M.f[2] = cy;
    M.f[4] = cx * cy * sz + sx * cz;
    M.f[5] = -cx * cy * cz + sx * sz;
    M.f[6] = -cx * sy;
    M.f[8] = -sx * cy * sz + cx * cz;
    M.f[9] = sx * cy * cz + cx * sz;
    M.f[10] = sx * sy;
    M.f[3] = M.f[7] = M.f[11] = M.f[12] = M.f[13] = M.f[14] = 0;
    M.f[15] = 1;

    return M;
}

/**
 * @brief Helper function to create a rotation matrix using float[3]
 * @param f Float array of size 3
 * @return Resulting rotation matrix
 */
static Matrix<ValType> RotationEuler(ValType *f)
{
    return RotationEuler(f[0], f[1], f[2]);
}

/**
 * @brief Helper function to create a rotation matrix using Vector3
 * @param v Rotation vector
 * @return Resulting rotation matrix
 */
static Matrix<ValType> RotationEuler(const Vector3<ValType> &v)
{
    return RotationEuler(v.x, v.y, v.z);
}

/**
 * @brief Creates a matrix representing rotation around an arbitrary axis
 *
 * Axis vector should be normalized
 * @param angle Angle to rotate (in radians)
 * @param x Rotation axis X direction
 * @param y Rotation axis Z direction
 * @param z Rotation axis Z direction
 * @return Resulting rotation matrix
 */
static Matrix<ValType> RotationAxisAngle(ValType angle, ValType x, ValType y, ValType z)
{
    Matrix<ValType> M = Identity();
    float rcos = cos(angle);
    float rsin = sin(angle);
    M.m[0][0] = rcos + x * x * (1.0 - rcos);
    M.m[1][0] = z * rsin + y * x * (1.0 - rcos);
    M.m[2][0] = -y * rsin + z * x * (1.0 - rcos);
    M.m[0][1] = -z * rsin + x * y * (1.0 - rcos);
    M.m[1][1] = rcos + y * y * (1.0 - rcos);
    M.m[2][1] = x * rsin + z * y * (1.0 - rcos);
    M.m[0][2] = y * rsin + x * z * (1.0 - rcos);
    M.m[1][2] = -x * rsin + y * z * (1.0 - rcos);
    M.m[2][2] = rcos + z * z * (1.0 - rcos);
    return M;
}

/**
 * @brief Helper function to create an axis-angle rotation matrix using angle and float[3]
 *
 * Axis vector should be normalized
 * @param angle Angle to rotate (in radians)
 * @param f Float array of size 3
 * @return Resulting rotation matrix
 */
static Matrix<ValType> RotationAxisAngle(ValType angle, ValType *f)
{
    return RotationAxisAngle(angle, f[0], f[1], f[2]);
}

/**
 * @brief Helper function to create an axis-angle rotation matrix using angle and Vector3
 * @param angle Angle to rotate (in radians)
 * @param v Rotation vector (should be normalized)
 * @return Resulting rotation matrix
 */
static Matrix<ValType> RotationAxisAngle(ValType angle, const Vector3<ValType> &v)
{
    return RotationAxisAngle(angle, v.x, v.y, v.z);
}

/**
 * @brief Helper function to create an axis-angle rotation matrix using float[4]
 *
 * Axis vector should be normalized
 * @param f Float array of size 4 - elements f[0-2] specify axis, f[3] specifies angle
 * @return Resulting rotation matrix
 */
static Matrix<ValType> RotationAxisAngle(ValType *f)
{
    return RotationAxisAngle(f[3], f[0], f[1], f[2]);
}

/**
 * @brief Helper function to create an axis-angle rotation matrix using Vector4
 * @param v Rotation vector. v.xyz specify angle, v.w specifies angle
 * @return Resulting rotation matrix
 */
static Matrix<ValType> RotationAxisAngle(const Vector4<ValType> &v)
{
    return RotationAxisAngle(v.w, v.x, v.y, v.z);
}

/**
 * @brief Creates a look-at matrix
 * @param pos Camera position
 * @param at Camera target
 * @param up Camera up vector
 * @return Resulting look-at matrix
 */
static Matrix<ValType> LookAt(Vector3<ValType> pos, Vector3<ValType> at, Vector3<ValType> up)
{
    Matrix<ValType> M = Identity();
    Vector3<ValType> Z = pos - at;
    Z.normalize();
    Vector3<ValType> X = up.cross(Z);
    X.normalize();
    Vector3<ValType> Y = Z.cross(X);

    M.m[0][0] = X.x; M.m[0][1] = Y.x; M.m[0][2] = Z.x;
    M.m[1][0] = X.y; M.m[1][1] = Y.y; M.m[1][2] = Z.y;
    M.m[2][0] = X.z; M.m[2][1] = Y.z; M.m[2][2] = Z.z;
    M.m[3][0] = -X.dot(pos);
    M.m[3][1] = -Y.dot(pos);
    M.m[3][2] = -Z.dot(pos);

    return M;
}

#endif // MATRIXFUNC_H
