#ifndef Vec3_H
#define Vec3_H

#include "FrameworkMath.h"

namespace Framework
{
    template<typename T>
    //! A 3D vector
    class Vec3
    {
    public:
        T x; //! X component of the vector
        T y; //! Y component of the vector
        T z; //! Z component of the vector

        //! Constructor
        inline Vec3() {}

        //! Constructor
        //! \param x The X component of the new vector
        //! \param y The Y component of the new vector
        //! \param z The Z component of the new vector
        inline Vec3(T x, T y, T z)
            : x(x),
              y(y),
              z(z)
        {}

        //! Constructor
        //! \param vect A vector whose values should be copied
        inline Vec3(const Vec3& vect)
            : Vec3(vect.x, vect.y, vect.z)
        {}

        //! Scales the vector so that it has length 1
        inline Vec3& normalize()
        {
            const T length = getLength();
            x /= length;
            y /= length;
            z /= length;
            return *this;
        }

        //! Swaps the values of the vector with those of another vector
        //! \param vect The other vector
        inline Vec3& swap(Vec3& vect)
        {
            const Vec3 tmp = vect;
            vect = *this;
            *this = tmp;
            return *this;
        }

        //! Calculates an angle between this and another vector
        inline float angle(Vec3 vect)
        {
            return lowPrecisionACos(
                (float)(*this * vect)
                / ((float)getLength() * (float)vect.getLength()));
        }

        //! Copies the values of another vector
        //! \param r The other vector
        inline Vec3 operator=(const Vec3& r)
        {
            x = r.x;
            y = r.y;
            z = r.z;
            return *this;
        }

        //! Adds another vector to this one
        //! \param r The other vector
        inline Vec3 operator+=(const Vec3& r)
        {
            x += r.x;
            y += r.y;
            z += r.z;
            return *this;
        }

        //! Subtracts another vector from this one
        //! \param r The other vector
        inline Vec3 operator-=(const Vec3& r)
        {
            x -= r.x;
            y -= r.y;
            z -= r.z;
            return *this;
        }

        //! Scales this vector
        //! \param r The factor
        inline Vec3 operator*=(const T& r)
        {
            x *= r;
            y *= r;
            z *= r;
            return *this;
        }

        //! Scales this vector by 1/factor
        //! \param r The factor
        inline Vec3 operator/=(const T& r)
        {
            x /= r;
            y /= r;
            z /= r;
            return *this;
        }

        //! Calculates the square of the distance between two vectors
        //! \param p The other vector
        inline T distanceSq(const Vec3& p) const
        {
            return (x - p.x) * (x - p.x) + (y - p.y) * (y - p.y)
                 + (z - p.z) * (z - p.z);
        }

        //! Calculates the distance between two vectors
        //! \param p The other vector
        inline T distance(const Vec3& p) const
        {
            return sqrt(distanceSq(p));
        }

        //! Returns a new vector that is the negation of this one
        inline Vec3 operator-() const
        {
            return {-x, -y, -z};
        }

        template<typename T2>
        //! Converts the vector type to another
        inline operator Vec3<T2>() const
        {
            return {(T2)x, (T2)y, (T2)z};
        }

        //! Calculates the square of the vector's length
        inline T getLengthSq() const
        {
            return *this * *this;
        }

        //! Calculates the length of the vector
        inline T getLength() const
        {
            return (T)sqrt(getLengthSq());
        }

        //! Calculates the dot product between two vectors
        //! \param r The other vector
        inline T operator*(const Vec3& r) const
        {
            return x * r.x + y * r.y + z * r.z;
        }

        //! Calculates the sum of two vectors
        //! \param r The other vector
        inline Vec3 operator+(const Vec3& r) const
        {
            return Vec3(*this) += r;
        }

        //! Subtracts two vectors from each other
        //! \param r The other vector
        inline Vec3 operator-(const Vec3& r) const
        {
            return Vec3(*this) -= r;
        }

        //! Scales the vector without modifying it
        //! \param r The factor
        inline Vec3 operator*(const T& r) const
        {
            return Vec3(*this) *= r;
        }

        //! Scales the vector by 1/factor without modifying it
        //! \param r The factor
        inline Vec3 operator/(const T& r) const
        {
            return Vec3(*this) /= r;
        }

        //! Checks two vectors for equality
        //! \param r The other vector
        inline bool operator==(const Vec3& r) const
        {
            return x == r.x && y == r.y && z == r.z;
        }

        //! Checks two vectors for inequality
        //! \param r The other vector
        inline bool operator!=(const Vec3& r) const
        {
            return !(*this == r);
        }

        //! Returns the cross product between this and another vector
        //! \param b The other vector
        inline Vec3 crossProduct(const Vec3& b) const
        {
            return Vec3(
                y * b.z - z * b.y, z * b.x - x * b.z, x * b.y - y * b.x);
        }

        inline Vec3& rotateY(float radian)
        {
            const float cosTheta = lowPrecisionCos(radian);
            const float sinTheta = lowPrecisionSin(radian);
            float x = cosTheta * this->x + sinTheta * this->z;
            float z = -sinTheta * this->x + cosTheta * this->z;
            this->x = (T)x;
            this->z = (T)z;
            return *this;
        }

        inline Vec3& rotateX(float radian)
        {
            const float cosTheta = lowPrecisionCos(radian);
            const float sinTheta = lowPrecisionSin(radian);
            float y = cosTheta * this->y + -sinTheta * this->z;
            float z = sinTheta * this->y + cosTheta * this->z;
            this->y = (T)y;
            this->z = (T)z;
            return *this;
        }

        inline Vec3& rotateZ(float radian)
        {
            const float cosTheta = lowPrecisionCos(radian);
            const float sinTheta = lowPrecisionSin(radian);
            float x = cosTheta * this->x + -sinTheta * this->y;
            float y = sinTheta * this->x + cosTheta * this->y;
            this->x = (T)x;
            this->y = (T)y;
            return *this;
        }
    };
} // namespace Framework

#endif