package com.flowpowered.math.matrix;

import java.io.Serializable;

import com.flowpowered.math.GenericMath;
import com.flowpowered.math.HashFunctions;
import com.flowpowered.math.TrigMath;
import com.flowpowered.math.imaginary.Complexd;
import com.flowpowered.math.imaginary.Quaterniond;
import com.flowpowered.math.vector.Vector3d;
import com.flowpowered.math.vector.Vector4d;

public class Matrix4d implements Matrixd, Serializable, Cloneable {
    private static final long serialVersionUID = 1;
    public static final Matrix4d ZERO = new Matrix4d(
            0, 0, 0, 0,
            0, 0, 0, 0,
            0, 0, 0, 0,
            0, 0, 0, 0);
    public static final Matrix4d IDENTITY = new Matrix4d();
    private final double m00, m01, m02, m03;
    private final double m10, m11, m12, m13;
    private final double m20, m21, m22, m23;
    private final double m30, m31, m32, m33;
    private transient volatile boolean hashed = false;
    private transient volatile int hashCode = 0;

    public Matrix4d() {
        this(
                1, 0, 0, 0,
                0, 1, 0, 0,
                0, 0, 1, 0,
                0, 0, 0, 1);
    }

    public Matrix4d(Matrix2d m) {
        this(
                m.get(0, 0), m.get(0, 1), 0, 0,
                m.get(1, 0), m.get(1, 1), 0, 0,
                0, 0, 0, 0,
                0, 0, 0, 0);
    }

    public Matrix4d(Matrix3d m) {
        this(
                m.get(0, 0), m.get(0, 1), m.get(0, 2), 0,
                m.get(1, 0), m.get(1, 1), m.get(1, 2), 0,
                m.get(2, 0), m.get(2, 1), m.get(2, 2), 0,
                0, 0, 0, 0);
    }

    public Matrix4d(Matrix4d m) {
        this(
                m.m00, m.m01, m.m02, m.m03,
                m.m10, m.m11, m.m12, m.m13,
                m.m20, m.m21, m.m22, m.m23,
                m.m30, m.m31, m.m32, m.m33);
    }

    public Matrix4d(MatrixNd m) {
        m00 = m.get(0, 0);
        m01 = m.get(0, 1);
        m10 = m.get(1, 0);
        m11 = m.get(1, 1);
        if (m.size() > 2) {
            m02 = m.get(0, 2);
            m12 = m.get(1, 2);
            m20 = m.get(2, 0);
            m21 = m.get(2, 1);
            m22 = m.get(2, 2);
            if (m.size() > 3) {
                m03 = m.get(0, 3);
                m13 = m.get(1, 3);
                m23 = m.get(2, 3);
                m30 = m.get(3, 0);
                m31 = m.get(3, 1);
                m32 = m.get(3, 2);
                m33 = m.get(3, 3);
            } else {
                m03 = 0;
                m13 = 0;
                m23 = 0;
                m30 = 0;
                m31 = 0;
                m32 = 0;
                m33 = 0;
            }
        } else {
            m02 = 0;
            m12 = 0;
            m20 = 0;
            m21 = 0;
            m22 = 0;
            m03 = 0;
            m13 = 0;
            m23 = 0;
            m30 = 0;
            m31 = 0;
            m32 = 0;
            m33 = 0;
        }
    }

    public Matrix4d(
            float m00, float m01, float m02, float m03,
            float m10, float m11, float m12, float m13,
            float m20, float m21, float m22, float m23,
            float m30, float m31, float m32, float m33) {
        this(
                (double) m00, (double) m01, (double) m02, (double) m03,
                (double) m10, (double) m11, (double) m12, (double) m13,
                (double) m20, (double) m21, (double) m22, (double) m23,
                (double) m30, (double) m31, (double) m32, (double) m33);
    }

    public Matrix4d(
            double m00, double m01, double m02, double m03,
            double m10, double m11, double m12, double m13,
            double m20, double m21, double m22, double m23,
            double m30, double m31, double m32, double m33) {
        this.m00 = m00;
        this.m01 = m01;
        this.m02 = m02;
        this.m03 = m03;
        this.m10 = m10;
        this.m11 = m11;
        this.m12 = m12;
        this.m13 = m13;
        this.m20 = m20;
        this.m21 = m21;
        this.m22 = m22;
        this.m23 = m23;
        this.m30 = m30;
        this.m31 = m31;
        this.m32 = m32;
        this.m33 = m33;
    }

    @Override
    public double get(int row, int col) {
        switch (row) {
        case 0:
            switch (col) {
            case 0:
                return m00;
            case 1:
                return m01;
            case 2:
                return m02;
            case 3:
                return m03;
            }
        case 1:
            switch (col) {
            case 0:
                return m10;
            case 1:
                return m11;
            case 2:
                return m12;
            case 3:
                return m13;
            }
        case 2:
            switch (col) {
            case 0:
                return m20;
            case 1:
                return m21;
            case 2:
                return m22;
            case 3:
                return m23;
            }
        case 3:
            switch (col) {
            case 0:
                return m30;
            case 1:
                return m31;
            case 2:
                return m32;
            case 3:
                return m33;
            }
        }
        throw new IllegalArgumentException(
                (row < 0 || row > 2 ? "row must be greater than zero and smaller than 3. " : "") +
                (col < 0 || col > 2 ? "col must be greater than zero and smaller than 3." : ""));
    }

    @Override
    public Vector4d getRow(int row) {
        return new Vector4d(get(row, 0), get(row, 1), get(row, 2), get(row, 3));
    }

    @Override
    public Vector4d getColumn(int col) {
        return new Vector4d(get(0, col), get(1, col), get(2, col), get(3, col));
    }

    public Matrix4d add(Matrix4d m) {
        return new Matrix4d(
                m00 + m.m00, m01 + m.m01, m02 + m.m02, m03 + m.m03,
                m10 + m.m10, m11 + m.m11, m12 + m.m12, m13 + m.m13,
                m20 + m.m20, m21 + m.m21, m22 + m.m22, m23 + m.m23,
                m30 + m.m30, m31 + m.m31, m32 + m.m32, m33 + m.m33);
    }

    public Matrix4d sub(Matrix4d m) {
        return new Matrix4d(
                m00 - m.m00, m01 - m.m01, m02 - m.m02, m03 - m.m03,
                m10 - m.m10, m11 - m.m11, m12 - m.m12, m13 - m.m13,
                m20 - m.m20, m21 - m.m21, m22 - m.m22, m23 - m.m23,
                m30 - m.m30, m31 - m.m31, m32 - m.m32, m33 - m.m33);
    }

    public Matrix4d mul(float a) {
        return mul((double) a);
    }

    @Override
    public Matrix4d mul(double a) {
        return new Matrix4d(
                m00 * a, m01 * a, m02 * a, m03 * a,
                m10 * a, m11 * a, m12 * a, m13 * a,
                m20 * a, m21 * a, m22 * a, m23 * a,
                m30 * a, m31 * a, m32 * a, m33 * a);
    }

    public Matrix4d mul(Matrix4d m) {
        return new Matrix4d(
                m00 * m.m00 + m01 * m.m10 + m02 * m.m20 + m03 * m.m30,
                m00 * m.m01 + m01 * m.m11 + m02 * m.m21 + m03 * m.m31,
                m00 * m.m02 + m01 * m.m12 + m02 * m.m22 + m03 * m.m32,
                m00 * m.m03 + m01 * m.m13 + m02 * m.m23 + m03 * m.m33,
                m10 * m.m00 + m11 * m.m10 + m12 * m.m20 + m13 * m.m30,
                m10 * m.m01 + m11 * m.m11 + m12 * m.m21 + m13 * m.m31,
                m10 * m.m02 + m11 * m.m12 + m12 * m.m22 + m13 * m.m32,
                m10 * m.m03 + m11 * m.m13 + m12 * m.m23 + m13 * m.m33,
                m20 * m.m00 + m21 * m.m10 + m22 * m.m20 + m23 * m.m30,
                m20 * m.m01 + m21 * m.m11 + m22 * m.m21 + m23 * m.m31,
                m20 * m.m02 + m21 * m.m12 + m22 * m.m22 + m23 * m.m32,
                m20 * m.m03 + m21 * m.m13 + m22 * m.m23 + m23 * m.m33,
                m30 * m.m00 + m31 * m.m10 + m32 * m.m20 + m33 * m.m30,
                m30 * m.m01 + m31 * m.m11 + m32 * m.m21 + m33 * m.m31,
                m30 * m.m02 + m31 * m.m12 + m32 * m.m22 + m33 * m.m32,
                m30 * m.m03 + m31 * m.m13 + m32 * m.m23 + m33 * m.m33);
    }

    public Matrix4d div(float a) {
        return div((double) a);
    }

    @Override
    public Matrix4d div(double a) {
        return new Matrix4d(
                m00 / a, m01 / a, m02 / a, m03 / a,
                m10 / a, m11 / a, m12 / a, m13 / a,
                m20 / a, m21 / a, m22 / a, m23 / a,
                m30 / a, m31 / a, m32 / a, m33 / a);
    }

    public Matrix4d div(Matrix4d m) {
        return mul(m.invert());
    }

    public Matrix4d pow(float pow) {
        return pow((double) pow);
    }

    @Override
    public Matrix4d pow(double pow) {
        return new Matrix4d(
                Math.pow(m00, pow), Math.pow(m01, pow), Math.pow(m02, pow), Math.pow(m03, pow),
                Math.pow(m10, pow), Math.pow(m11, pow), Math.pow(m12, pow), Math.pow(m13, pow),
                Math.pow(m20, pow), Math.pow(m21, pow), Math.pow(m22, pow), Math.pow(m23, pow),
                Math.pow(m30, pow), Math.pow(m31, pow), Math.pow(m32, pow), Math.pow(m33, pow));
    }

    public Matrix4d translate(Vector3d v) {
        return translate(v.getX(), v.getY(), v.getZ());
    }

    public Matrix4d translate(float x, float y, float z) {
        return translate((double) x, (double) y, (double) z);
    }

    public Matrix4d translate(double x, double y, double z) {
        return createTranslation(x, y, z).mul(this);
    }

    public Matrix4d scale(float scale) {
        return scale((double) scale);
    }

    public Matrix4d scale(double scale) {
        return scale(scale, scale, scale, scale);
    }

    public Matrix4d scale(Vector4d v) {
        return scale(v.getX(), v.getY(), v.getZ(), v.getW());
    }

    public Matrix4d scale(float x, float y, float z, float w) {
        return scale((double) x, (double) y, (double) z, (double) w);
    }

    public Matrix4d scale(double x, double y, double z, double w) {
        return createScaling(x, y, z, w).mul(this);
    }

    public Matrix4d rotate(Complexd rot) {
        return createRotation(rot).mul(this);
    }

    public Matrix4d rotate(Quaterniond rot) {
        return createRotation(rot).mul(this);
    }

    public Vector4d transform(Vector4d v) {
        return transform(v.getX(), v.getY(), v.getZ(), v.getW());
    }

    public Vector4d transform(float x, float y, float z, float w) {
        return transform((double) x, (double) y, (double) z, (double) w);
    }

    public Vector4d transform(double x, double y, double z, double w) {
        return new Vector4d(
                m00 * x + m01 * y + m02 * z + m03 * w,
                m10 * x + m11 * y + m12 * z + m13 * w,
                m20 * x + m21 * y + m22 * z + m23 * w,
                m30 * x + m31 * y + m32 * z + m33 * w);
    }

    @Override
    public Matrix4d floor() {
        return new Matrix4d(
                GenericMath.floor(m00), GenericMath.floor(m01), GenericMath.floor(m02), GenericMath.floor(m03),
                GenericMath.floor(m10), GenericMath.floor(m11), GenericMath.floor(m12), GenericMath.floor(m13),
                GenericMath.floor(m20), GenericMath.floor(m21), GenericMath.floor(m22), GenericMath.floor(m23),
                GenericMath.floor(m30), GenericMath.floor(m31), GenericMath.floor(m32), GenericMath.floor(m33));
    }

    @Override
    public Matrix4d ceil() {
        return new Matrix4d(
                Math.ceil(m00), Math.ceil(m01), Math.ceil(m02), Math.ceil(m03),
                Math.ceil(m10), Math.ceil(m11), Math.ceil(m12), Math.ceil(m13),
                Math.ceil(m20), Math.ceil(m21), Math.ceil(m22), Math.ceil(m23),
                Math.ceil(m30), Math.ceil(m31), Math.ceil(m32), Math.ceil(m33));
    }

    @Override
    public Matrix4d round() {
        return new Matrix4d(
                Math.round(m00), Math.round(m01), Math.round(m02), Math.round(m03),
                Math.round(m10), Math.round(m11), Math.round(m12), Math.round(m13),
                Math.round(m20), Math.round(m21), Math.round(m22), Math.round(m23),
                Math.round(m30), Math.round(m31), Math.round(m32), Math.round(m33));
    }

    @Override
    public Matrix4d abs() {
        return new Matrix4d(
                Math.abs(m00), Math.abs(m01), Math.abs(m02), Math.abs(m03),
                Math.abs(m10), Math.abs(m11), Math.abs(m12), Math.abs(m13),
                Math.abs(m20), Math.abs(m21), Math.abs(m22), Math.abs(m23),
                Math.abs(m30), Math.abs(m31), Math.abs(m32), Math.abs(m33));
    }

    @Override
    public Matrix4d negate() {
        return new Matrix4d(
                -m00, -m01, -m02, -m03,
                -m10, -m11, -m12, -m13,
                -m20, -m21, -m22, -m23,
                -m30, -m31, -m32, -m33);
    }

    @Override
    public Matrix4d transpose() {
        return new Matrix4d(
                m00, m10, m20, m30,
                m01, m11, m21, m31,
                m02, m12, m22, m32,
                m03, m13, m23, m33);
    }

    @Override
    public double trace() {
        return m00 + m11 + m22 + m33;
    }

    @Override
    public double determinant() {
        return m00 * (m11 * m22 * m33 + m21 * m32 * m13 + m31 * m12 * m23 - m31 * m22 * m13 - m11 * m32 * m23 - m21 * m12 * m33)
                - m10 * (m01 * m22 * m33 + m21 * m32 * m03 + m31 * m02 * m23 - m31 * m22 * m03 - m01 * m32 * m23 - m21 * m02 * m33)
                + m20 * (m01 * m12 * m33 + m11 * m32 * m03 + m31 * m02 * m13 - m31 * m12 * m03 - m01 * m32 * m13 - m11 * m02 * m33)
                - m30 * (m01 * m12 * m23 + m11 * m22 * m03 + m21 * m02 * m13 - m21 * m12 * m03 - m01 * m22 * m13 - m11 * m02 * m23);
    }

    @Override
    public Matrix4d invert() {
        final double det = determinant();
        if (Math.abs(det) < GenericMath.DBL_EPSILON) {
            throw new ArithmeticException("Cannot inverse a matrix with a zero determinant");
        }
        return new Matrix4d(
                det3(m11, m21, m31, m12, m22, m32, m13, m23, m33) / det, -det3(m01, m21, m31, m02, m22, m32, m03, m23, m33) / det,
                det3(m01, m11, m31, m02, m12, m32, m03, m13, m33) / det, -det3(m01, m11, m21, m02, m12, m22, m03, m13, m23) / det,
                -det3(m10, m20, m30, m12, m22, m32, m13, m23, m33) / det, det3(m00, m20, m30, m02, m22, m32, m03, m23, m33) / det,
                -det3(m00, m10, m30, m02, m12, m32, m03, m13, m33) / det, det3(m00, m10, m20, m02, m12, m22, m03, m13, m23) / det,
                det3(m10, m20, m30, m11, m21, m31, m13, m23, m33) / det, -det3(m00, m20, m30, m01, m21, m31, m03, m23, m33) / det,
                det3(m00, m10, m30, m01, m11, m31, m03, m13, m33) / det, -det3(m00, m10, m20, m01, m11, m21, m03, m13, m23) / det,
                -det3(m10, m20, m30, m11, m21, m31, m12, m22, m32) / det, det3(m00, m20, m30, m01, m21, m31, m02, m22, m32) / det,
                -det3(m00, m10, m30, m01, m11, m31, m02, m12, m32) / det, det3(m00, m10, m20, m01, m11, m21, m02, m12, m22) / det);
    }

    public Matrix2d toMatrix2() {
        return new Matrix2d(this);
    }

    public Matrix3d toMatrix3() {
        return new Matrix3d(this);
    }

    public MatrixNd toMatrixN() {
        return new MatrixNd(this);
    }

    public double[] toArray() {
        return toArray(false);
    }

    @Override
    public double[] toArray(boolean columnMajor) {
        if (columnMajor) {
            return new double[]{
                    m00, m10, m20, m30,
                    m01, m11, m21, m31,
                    m02, m12, m22, m32,
                    m03, m13, m23, m33
            };
        } else {
            return new double[]{
                    m00, m01, m02, m03,
                    m10, m11, m12, m13,
                    m20, m21, m22, m23,
                    m30, m31, m32, m33
            };
        }
    }

    @Override
    public Matrix4f toFloat() {
        return new Matrix4f(
                m00, m01, m02, m03,
                m10, m11, m12, m13,
                m20, m21, m22, m23,
                m30, m31, m32, m33
        );
    }

    @Override
    public Matrix4d toDouble() {
        return new Matrix4d(
                m00, m01, m02, m03,
                m10, m11, m12, m13,
                m20, m21, m22, m23,
                m30, m31, m32, m33
        );
    }

    @Override
    public String toString() {
        return m00 + " " + m01 + " " + m02 + " " + m03 + "\n"
                + m10 + " " + m11 + " " + m12 + " " + m13 + "\n"
                + m20 + " " + m21 + " " + m22 + " " + m23 + "\n"
                + m30 + " " + m31 + " " + m32 + " " + m33 + "\n";
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (!(o instanceof Matrix4d)) {
            return false;
        }
        final Matrix4d matrix4 = (Matrix4d) o;
        if (Double.compare(matrix4.m00, m00) != 0) {
            return false;
        }
        if (Double.compare(matrix4.m01, m01) != 0) {
            return false;
        }
        if (Double.compare(matrix4.m02, m02) != 0) {
            return false;
        }
        if (Double.compare(matrix4.m03, m03) != 0) {
            return false;
        }
        if (Double.compare(matrix4.m10, m10) != 0) {
            return false;
        }
        if (Double.compare(matrix4.m11, m11) != 0) {
            return false;
        }
        if (Double.compare(matrix4.m12, m12) != 0) {
            return false;
        }
        if (Double.compare(matrix4.m13, m13) != 0) {
            return false;
        }
        if (Double.compare(matrix4.m20, m20) != 0) {
            return false;
        }
        if (Double.compare(matrix4.m21, m21) != 0) {
            return false;
        }
        if (Double.compare(matrix4.m22, m22) != 0) {
            return false;
        }
        if (Double.compare(matrix4.m23, m23) != 0) {
            return false;
        }
        if (Double.compare(matrix4.m30, m30) != 0) {
            return false;
        }
        if (Double.compare(matrix4.m31, m31) != 0) {
            return false;
        }
        if (Double.compare(matrix4.m32, m32) != 0) {
            return false;
        }
        if (Double.compare(matrix4.m33, m33) != 0) {
            return false;
        }
        return true;
    }

    @Override
    public int hashCode() {
        if (!hashed) {
            int result = (m00 != +0.0f ? HashFunctions.hash(m00) : 0);
            result = 31 * result + (m01 != +0.0f ? HashFunctions.hash(m01) : 0);
            result = 31 * result + (m02 != +0.0f ? HashFunctions.hash(m02) : 0);
            result = 31 * result + (m03 != +0.0f ? HashFunctions.hash(m03) : 0);
            result = 31 * result + (m10 != +0.0f ? HashFunctions.hash(m10) : 0);
            result = 31 * result + (m11 != +0.0f ? HashFunctions.hash(m11) : 0);
            result = 31 * result + (m12 != +0.0f ? HashFunctions.hash(m12) : 0);
            result = 31 * result + (m13 != +0.0f ? HashFunctions.hash(m13) : 0);
            result = 31 * result + (m20 != +0.0f ? HashFunctions.hash(m20) : 0);
            result = 31 * result + (m21 != +0.0f ? HashFunctions.hash(m21) : 0);
            result = 31 * result + (m22 != +0.0f ? HashFunctions.hash(m22) : 0);
            result = 31 * result + (m23 != +0.0f ? HashFunctions.hash(m23) : 0);
            result = 31 * result + (m30 != +0.0f ? HashFunctions.hash(m30) : 0);
            result = 31 * result + (m31 != +0.0f ? HashFunctions.hash(m31) : 0);
            result = 31 * result + (m32 != +0.0f ? HashFunctions.hash(m32) : 0);
            hashCode = 31 * result + (m33 != +0.0f ? HashFunctions.hash(m33) : 0);
            hashed = true;
        }
        return hashCode;
    }

    @Override
    public Matrix4d clone() {
        return new Matrix4d(this);
    }

    public static Matrix4d from(double n) {
         return n == 0 ? ZERO : new Matrix4d(n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n);
    }

    public static Matrix4d from(double m00, double m01, double m02, double m03,
                                double m10, double m11, double m12, double m13,
                                double m20, double m21, double m22, double m23,
                                double m30, double m31, double m32, double m33) {
         return m00 == 0 && m01 == 0 && m02 == 0 && m03 == 0
             && m10 == 0 && m11 == 0 && m12 == 0 && m13 == 0
             && m20 == 0 && m21 == 0 && m22 == 0 && m23 == 0
             && m30 == 0 && m31 == 0 && m32 == 0 && m33 == 0
         ? ZERO : new Matrix4d(m00, m01, m02, m03,
                                 m10, m11, m12, m13,
                                 m20, m21, m22, m23,
                                 m30, m31, m32, m33);
    }

    public static Matrix4d fromDiagonal(double m00, double m11, double m22, double m33) {
         return m00 == 0 && m11 == 0 && m22 == 0 && m33 == 0 ? ZERO : new Matrix4d(m00, 0, 0, 0,
                                                                                0, m11, 0, 0,
                                                                                0, 0, m22, 0,
                                                                                0, 0, 0, m33);
    }

    public static Matrix4d createScaling(float scale) {
        return createScaling((double) scale);
    }

    public static Matrix4d createScaling(double scale) {
        return createScaling(scale, scale, scale, scale);
    }

    public static Matrix4d createScaling(Vector4d v) {
        return createScaling(v.getX(), v.getY(), v.getZ(), v.getW());
    }

    public static Matrix4d createScaling(float x, float y, float z, float w) {
        return createScaling((double) x, (double) y, (double) z, (double) w);
    }

    public static Matrix4d createScaling(double x, double y, double z, double w) {
        return new Matrix4d(
                x, 0, 0, 0,
                0, y, 0, 0,
                0, 0, z, 0,
                0, 0, 0, w);
    }

    public static Matrix4d createTranslation(Vector3d v) {
        return createTranslation(v.getX(), v.getY(), v.getZ());
    }

    public static Matrix4d createTranslation(float x, float y, float z) {
        return createTranslation((double) x, (double) y, (double) z);
    }

    public static Matrix4d createTranslation(double x, double y, double z) {
        return new Matrix4d(
                1, 0, 0, x,
                0, 1, 0, y,
                0, 0, 1, z,
                0, 0, 0, 1);
    }

    public static Matrix4d createRotation(Complexd rot) {
        rot = rot.normalize();
        return new Matrix4d(
                rot.getX(), -rot.getY(), 0, 0,
                rot.getY(), rot.getX(), 0, 0,
                0, 0, 1, 0,
                0, 0, 0, 1);
    }

    public static Matrix4d createRotation(Quaterniond rot) {
        rot = rot.normalize();
        return new Matrix4d(
                1 - 2 * rot.getY() * rot.getY() - 2 * rot.getZ() * rot.getZ(),
                2 * rot.getX() * rot.getY() - 2 * rot.getW() * rot.getZ(),
                2 * rot.getX() * rot.getZ() + 2 * rot.getW() * rot.getY(), 0,
                2 * rot.getX() * rot.getY() + 2 * rot.getW() * rot.getZ(),
                1 - 2 * rot.getX() * rot.getX() - 2 * rot.getZ() * rot.getZ(),
                2 * rot.getY() * rot.getZ() - 2 * rot.getW() * rot.getX(), 0,
                2 * rot.getX() * rot.getZ() - 2 * rot.getW() * rot.getY(),
                2 * rot.getY() * rot.getZ() + 2 * rot.getX() * rot.getW(),
                1 - 2 * rot.getX() * rot.getX() - 2 * rot.getY() * rot.getY(), 0,
                0, 0, 0, 1);
    }

    /**
     * Creates a "look at" matrix for the given eye point.
     *
     * @param eye The position of the camera
     * @param at The point that the camera is looking at
     * @param up The "up" vector
     * @return A rotational transform that corresponds to a camera looking at the given point
     */
    public static Matrix4d createLookAt(Vector3d eye, Vector3d at, Vector3d up) {
        final Vector3d f = at.sub(eye).normalize();
        final Vector3d s = f.cross(up).normalize();
        final Vector3d u = s.cross(f);
        final Matrix4d mat = new Matrix4d(
                s.getX(), s.getY(), s.getZ(), 0,
                u.getX(), u.getY(), u.getZ(), 0,
                -f.getX(), -f.getY(), -f.getZ(), 0,
                0, 0, 0, 1);
        return mat.translate(eye.negate());
    }

    /**
     * Creates a perspective projection matrix with the given (x) FOV, aspect, near and far planes
     *
     * @param fov The field of view in the x direction
     * @param aspect The aspect ratio, usually width/height
     * @param near The near plane, cannot be 0
     * @param far the far plane, far cannot equal near
     * @return A perspective projection matrix built from the given values
     */
    public static Matrix4d createPerspective(float fov, float aspect, float near, float far) {
        return createPerspective((double) fov, (double) aspect, (double) near, (double) far);
    }

    /**
     * Creates a perspective projection matrix with the given (x) FOV, aspect, near and far planes
     *
     * @param fov The field of view in the x direction
     * @param aspect The aspect ratio, usually width/height
     * @param near The near plane, cannot be 0
     * @param far the far plane, far cannot equal near
     * @return A perspective projection matrix built from the given values
     */
    public static Matrix4d createPerspective(double fov, double aspect, double near, double far) {
        final double scale = 1 / TrigMath.tan(fov * (double) TrigMath.HALF_DEG_TO_RAD);
        return new Matrix4d(
                scale / aspect, 0, 0, 0,
                0, scale, 0, 0,
                0, 0, (far + near) / (near - far), 2 * far * near / (near - far),
                0, 0, -1, 0);
    }

    /**
     * Creates an orthographic viewing frustum built from the provided values
     *
     * @param right the right most plane of the viewing frustum
     * @param left the left most plane of the viewing frustum
     * @param top the top plane of the viewing frustum
     * @param bottom the bottom plane of the viewing frustum
     * @param near the near plane of the viewing frustum
     * @param far the far plane of the viewing frustum
     * @return A viewing frustum built from the provided values
     */
    public static Matrix4d createOrthographic(float right, float left, float top, float bottom,
            float near, float far) {
        return createOrthographic((double) right, (double) left, (double) top, (double) bottom, (double) near, (double) far);
    }

    /**
     * Creates an orthographic viewing frustum built from the provided values
     *
     * @param right the right most plane of the viewing frustum
     * @param left the left most plane of the viewing frustum
     * @param top the top plane of the viewing frustum
     * @param bottom the bottom plane of the viewing frustum
     * @param near the near plane of the viewing frustum
     * @param far the far plane of the viewing frustum
     * @return A viewing frustum built from the provided values
     */
    public static Matrix4d createOrthographic(double right, double left, double top, double bottom,
            double near, double far) {
        return new Matrix4d(
                2 / (right - left), 0, 0, -(right + left) / (right - left),
                0, 2 / (top - bottom), 0, -(top + bottom) / (top - bottom),
                0, 0, -2 / (far - near), -(far + near) / (far - near),
                0, 0, 0, 1);
    }

    private static double det3(double m00, double m01, double m02,
            double m10, double m11, double m12,
            double m20, double m21, double m22) {
        return m00 * (m11 * m22 - m12 * m21) - m01 * (m10 * m22 - m12 * m20) + m02 * (m10 * m21 - m11 * m20);
    }
}


package com.flowpowered.math.vector;

import java.io.Serializable;
import java.util.Random;

import com.flowpowered.math.GenericMath;
import com.flowpowered.math.HashFunctions;
import com.flowpowered.math.TrigMath;

public class Vector3d implements Vectord, Comparable<Vector3d>, Serializable, Cloneable {
    private static final long serialVersionUID = 1;
    public static final Vector3d ZERO = new Vector3d(0, 0, 0);
    public static final Vector3d UNIT_X = new Vector3d(1, 0, 0);
    public static final Vector3d UNIT_Y = new Vector3d(0, 1, 0);
    public static final Vector3d UNIT_Z = new Vector3d(0, 0, 1);
    public static final Vector3d ONE = new Vector3d(1, 1, 1);
    public static final Vector3d RIGHT = UNIT_X;
    public static final Vector3d UP = UNIT_Y;
    public static final Vector3d FORWARD = UNIT_Z;
    private final double x;
    private final double y;
    private final double z;
    private transient volatile boolean hashed = false;
    private transient volatile int hashCode = 0;

    public Vector3d() {
        this(0, 0, 0);
    }

    public Vector3d(Vector2d v) {
        this(v, 0);
    }

    public Vector3d(Vector2d v, float z) {
        this(v, (double) z);
    }

    public Vector3d(Vector2d v, double z) {
        this(v.getX(), v.getY(), z);
    }

    public Vector3d(Vector3d v) {
        this(v.x, v.y, v.z);
    }

    public Vector3d(Vector4d v) {
        this(v.getX(), v.getY(), v.getZ());
    }

    public Vector3d(VectorNd v) {
        this(v.get(0), v.get(1), v.size() > 2 ? v.get(2) : 0);
    }

    public Vector3d(float x, float y, float z) {
        this((double) x, (double) y, (double) z);
    }

    public Vector3d(double x, double y, double z) {
        this.x = x;
        this.y = y;
        this.z = z;
    }

    public double getX() {
        return x;
    }

    public double getY() {
        return y;
    }

    public double getZ() {
        return z;
    }

    public int getFloorX() {
        return GenericMath.floor(x);
    }

    public int getFloorY() {
        return GenericMath.floor(y);
    }

    public int getFloorZ() {
        return GenericMath.floor(z);
    }

    public Vector3d add(Vector3d v) {
        return add(v.x, v.y, v.z);
    }

    public Vector3d add(float x, float y, float z) {
        return add((double) x, (double) y, (double) z);
    }

    public Vector3d add(double x, double y, double z) {
        return new Vector3d(this.x + x, this.y + y, this.z + z);
    }

    public Vector3d sub(Vector3d v) {
        return sub(v.x, v.y, v.z);
    }

    public Vector3d sub(float x, float y, float z) {
        return sub((double) x, (double) y, (double) z);
    }

    public Vector3d sub(double x, double y, double z) {
        return new Vector3d(this.x - x, this.y - y, this.z - z);
    }

    public Vector3d mul(float a) {
        return mul((double) a);
    }

    @Override
    public Vector3d mul(double a) {
        return mul(a, a, a);
    }

    public Vector3d mul(Vector3d v) {
        return mul(v.x, v.y, v.z);
    }

    public Vector3d mul(float x, float y, float z) {
        return mul((double) x, (double) y, (double) z);
    }

    public Vector3d mul(double x, double y, double z) {
        return new Vector3d(this.x * x, this.y * y, this.z * z);
    }

    public Vector3d div(float a) {
        return div((double) a);
    }

    @Override
    public Vector3d div(double a) {
        return div(a, a, a);
    }

    public Vector3d div(Vector3d v) {
        return div(v.x, v.y, v.z);
    }

    public Vector3d div(float x, float y, float z) {
        return div((double) x, (double) y, (double) z);
    }

    public Vector3d div(double x, double y, double z) {
        return new Vector3d(this.x / x, this.y / y, this.z / z);
    }

    public double dot(Vector3d v) {
        return dot(v.x, v.y, v.z);
    }

    public double dot(float x, float y, float z) {
        return dot((double) x, (double) y, (double) z);
    }

    public double dot(double x, double y, double z) {
        return this.x * x + this.y * y + this.z * z;
    }

    public Vector3d project(Vector3d v) {
        return project(v.x, v.y, v.z);
    }

    public Vector3d project(float x, float y, float z) {
        return project((double) x, (double) y, (double) z);
    }

    public Vector3d project(double x, double y, double z) {
        final double lengthSquared = x * x + y * y + z * z;
        if (Math.abs(lengthSquared) < GenericMath.DBL_EPSILON) {
            throw new ArithmeticException("Cannot project onto the zero vector");
        }
        final double a = dot(x, y, z) / lengthSquared;
        return new Vector3d(a * x, a * y, a * z);
    }

    public Vector3d cross(Vector3d v) {
        return cross(v.x, v.y, v.z);
    }

    public Vector3d cross(float x, float y, float z) {
        return cross((double) x, (double) y, (double) z);
    }

    public Vector3d cross(double x, double y, double z) {
        return new Vector3d(this.y * z - this.z * y, this.z * x - this.x * z, this.x * y - this.y * x);
    }

    public Vector3d pow(float pow) {
        return pow((double) pow);
    }

    @Override
    public Vector3d pow(double power) {
        return new Vector3d(Math.pow(x, power), Math.pow(y, power), Math.pow(z, power));
    }

    @Override
    public Vector3d ceil() {
        return new Vector3d(Math.ceil(x), Math.ceil(y), Math.ceil(z));
    }

    @Override
    public Vector3d floor() {
        return new Vector3d(GenericMath.floor(x), GenericMath.floor(y), GenericMath.floor(z));
    }

    @Override
    public Vector3d round() {
        return new Vector3d(Math.round(x), Math.round(y), Math.round(z));
    }

    @Override
    public Vector3d abs() {
        return new Vector3d(Math.abs(x), Math.abs(y), Math.abs(z));
    }

    @Override
    public Vector3d negate() {
        return new Vector3d(-x, -y, -z);
    }

    public Vector3d min(Vector3d v) {
        return min(v.x, v.y, v.z);
    }

    public Vector3d min(float x, float y, float z) {
        return min((double) x, (double) y, (double) z);
    }

    public Vector3d min(double x, double y, double z) {
        return new Vector3d(Math.min(this.x, x), Math.min(this.y, y), Math.min(this.z, z));
    }

    public Vector3d max(Vector3d v) {
        return max(v.x, v.y, v.z);
    }

    public Vector3d max(float x, float y, float z) {
        return max((double) x, (double) y, (double) z);
    }

    public Vector3d max(double x, double y, double z) {
        return new Vector3d(Math.max(this.x, x), Math.max(this.y, y), Math.max(this.z, z));
    }

    public double distanceSquared(Vector3d v) {
        return distanceSquared(v.x, v.y, v.z);
    }

    public double distanceSquared(float x, float y, float z) {
        return distanceSquared((double) x, (double) y, (double) z);
    }

    public double distanceSquared(double x, double y, double z) {
        final double dx = this.x - x;
        final double dy = this.y - y;
        final double dz = this.z - z;
        return dx * dx + dy * dy + dz * dz;
    }

    public double distance(Vector3d v) {
        return distance(v.x, v.y, v.z);
    }

    public double distance(float x, float y, float z) {
        return distance((double) x, (double) y, (double) z);
    }

    public double distance(double x, double y, double z) {
        return (double) Math.sqrt(distanceSquared(x, y, z));
    }

    @Override
    public double lengthSquared() {
        return x * x + y * y + z * z;
    }

    @Override
    public double length() {
        return (double) Math.sqrt(lengthSquared());
    }

    @Override
    public Vector3d normalize() {
        final double length = length();
        if (Math.abs(length) < GenericMath.DBL_EPSILON) {
            throw new ArithmeticException("Cannot normalize the zero vector");
        }
        return new Vector3d(x / length, y / length, z / length);
    }

    /**
     * Returns the axis with the minimal value.
     *
     * @return {@link int} axis with minimal value
     */
    @Override
    public int getMinAxis() {
        return x < y ? (x < z ? 0 : 2) : (y < z ? 1 : 2);
    }

    /**
     * Returns the axis with the maximum value.
     *
     * @return {@link int} axis with maximum value
     */
    @Override
    public int getMaxAxis() {
        return x < y ? (y < z ? 2 : 1) : (x < z ? 2 : 0);
    }

    public Vector2d toVector2() {
        return new Vector2d(this);
    }

    public Vector2d toVector2(boolean useZ) {
        return new Vector2d(x, useZ ? z : y);
    }

    public Vector4d toVector4() {
        return toVector4(0);
    }

    public Vector4d toVector4(float w) {
        return toVector4((double) w);
    }

    public Vector4d toVector4(double w) {
        return new Vector4d(this, w);
    }

    public VectorNd toVectorN() {
        return new VectorNd(this);
    }

    @Override
    public double[] toArray() {
        return new double[]{x, y, z};
    }

    @Override
    public Vector3i toInt() {
        return new Vector3i(x, y, z);
    }

    @Override
    public Vector3l toLong() {
        return new Vector3l(x, y, z);
    }

    @Override
    public Vector3f toFloat() {
        return new Vector3f(x, y, z);
    }

    @Override
    public Vector3d toDouble() {
        return new Vector3d(x, y, z);
    }

    @Override
    public int compareTo(Vector3d v) {
        return (int) Math.signum(lengthSquared() - v.lengthSquared());
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (!(o instanceof Vector3d)) {
            return false;
        }
        final Vector3d vector3 = (Vector3d) o;
        if (Double.compare(vector3.x, x) != 0) {
            return false;
        }
        if (Double.compare(vector3.y, y) != 0) {
            return false;
        }
        if (Double.compare(vector3.z, z) != 0) {
            return false;
        }
        return true;
    }

    @Override
    public int hashCode() {
        if (!hashed) {
            int result = (x != +0.0f ? HashFunctions.hash(x) : 0);
            result = 31 * result + (y != +0.0f ? HashFunctions.hash(y) : 0);
            hashCode = 31 * result + (z != +0.0f ? HashFunctions.hash(z) : 0);
            hashed = true;
        }
        return hashCode;
    }

    @Override
    public Vector3d clone() {
        return new Vector3d(this);
    }

    @Override
    public String toString() {
        return "(" + x + ", " + y + ", " + z + ")";
    }

    public static Vector3d from(double n) {
         return n == 0 ? ZERO : new Vector3d(n, n, n);
    }

    public static Vector3d from(double x, double y, double z) {
         return x == 0 && y == 0 && z == 0 ? ZERO : new Vector3d(x, y, z);
    }

    /**
     * Gets the direction vector of a random pitch and yaw using the random specified.
     *
     * @param random to use
     * @return the random direction vector
     */
    public static Vector3d createRandomDirection(Random random) {
        return createDirectionRad(random.nextDouble() * (double) TrigMath.TWO_PI,
                random.nextDouble() * (double) TrigMath.TWO_PI);
    }

    /**
     * Gets the direction vector of a certain theta and phi in degrees. This uses the standard math spherical coordinate system.
     *
     * @param theta in degrees
     * @param phi in degrees
     * @return the direction vector
     */
    public static Vector3d createDirectionDeg(float theta, float phi) {
        return createDirectionDeg((double) theta, (double) phi);
    }

    /**
     * Gets the direction vector of a certain theta and phi in degrees. This uses the standard math spherical coordinate system.
     *
     * @param theta in degrees
     * @param phi in degrees
     * @return the direction vector
     */
    public static Vector3d createDirectionDeg(double theta, double phi) {
        return createDirectionRad((double) Math.toRadians(theta), (double) Math.toRadians(phi));
    }

    /**
     * Gets the direction vector of a certain theta and phi in radians. This uses the standard math spherical coordinate system.
     *
     * @param theta in radians
     * @param phi in radians
     * @return the direction vector
     */
    public static Vector3d createDirectionRad(float theta, float phi) {
        return createDirectionRad((double) theta, (double) phi);
    }

    /**
     * Gets the direction vector of a certain theta and phi in radians. This uses the standard math spherical coordinate system.
     *
     * @param theta in radians
     * @param phi in radians
     * @return the direction vector
     */
    public static Vector3d createDirectionRad(double theta, double phi) {
        final double f = TrigMath.sin(phi);
        return new Vector3d(f * TrigMath.cos(theta), f * TrigMath.sin(theta), TrigMath.cos(phi));
    }
}


package com.flowpowered.math.vector;

import java.io.Serializable;

import com.flowpowered.math.GenericMath;
import com.flowpowered.math.HashFunctions;

public class Vector4i implements Vectori, Comparable<Vector4i>, Serializable, Cloneable {
    private static final long serialVersionUID = 1;
    public static final Vector4i ZERO = new Vector4i(0, 0, 0, 0);
    public static final Vector4i UNIT_X = new Vector4i(1, 0, 0, 0);
    public static final Vector4i UNIT_Y = new Vector4i(0, 1, 0, 0);
    public static final Vector4i UNIT_Z = new Vector4i(0, 0, 1, 0);
    public static final Vector4i UNIT_W = new Vector4i(0, 0, 0, 1);
    public static final Vector4i ONE = new Vector4i(1, 1, 1, 1);
    private final int x;
    private final int y;
    private final int z;
    private final int w;
    private transient volatile boolean hashed = false;
    private transient volatile int hashCode = 0;

    public Vector4i() {
        this(0, 0, 0, 0);
    }

    public Vector4i(Vector2i v) {
        this(v, 0, 0);
    }

    public Vector4i(Vector2i v, double z, double w) {
        this(v, GenericMath.floor(z), GenericMath.floor(w));
    }

    public Vector4i(Vector2i v, int z, int w) {
        this(v.getX(), v.getY(), z, w);
    }

    public Vector4i(Vector3i v) {
        this(v, 0);
    }

    public Vector4i(Vector3i v, double w) {
        this(v, GenericMath.floor(w));
    }

    public Vector4i(Vector3i v, int w) {
        this(v.getX(), v.getY(), v.getZ(), w);
    }

    public Vector4i(Vector4i v) {
        this(v.x, v.y, v.z, v.w);
    }

    public Vector4i(VectorNi v) {
        this(v.get(0), v.get(1), v.size() > 2 ? v.get(2) : 0, v.size() > 3 ? v.get(3) : 0);
    }

    public Vector4i(double x, double y, double z, double w) {
        this(GenericMath.floor(x), GenericMath.floor(y), GenericMath.floor(z), GenericMath.floor(w));
    }

    public Vector4i(int x, int y, int z, int w) {
        this.x = x;
        this.y = y;
        this.z = z;
        this.w = w;
    }

    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }

    public int getZ() {
        return z;
    }

    public int getW() {
        return w;
    }

    public Vector4i add(Vector4i v) {
        return add(v.x, v.y, v.z, v.w);
    }

    public Vector4i add(double x, double y, double z, double w) {
        return add(GenericMath.floor(x), GenericMath.floor(y), GenericMath.floor(z), GenericMath.floor(w));
    }

    public Vector4i add(int x, int y, int z, int w) {
        return new Vector4i(this.x + x, this.y + y, this.z + z, this.w + w);
    }

    public Vector4i sub(Vector4i v) {
        return sub(v.x, v.y, v.z, v.w);
    }

    public Vector4i sub(double x, double y, double z, double w) {
        return sub(GenericMath.floor(x), GenericMath.floor(y), GenericMath.floor(z), GenericMath.floor(w));
    }

    public Vector4i sub(int x, int y, int z, int w) {
        return new Vector4i(this.x - x, this.y - y, this.z - z, this.w - w);
    }

    public Vector4i mul(double a) {
        return mul(GenericMath.floor(a));
    }

    @Override
    public Vector4i mul(int a) {
        return mul(a, a, a, a);
    }

    public Vector4i mul(Vector4i v) {
        return mul(v.x, v.y, v.z, v.w);
    }

    public Vector4i mul(double x, double y, double z, double w) {
        return mul(GenericMath.floor(x), GenericMath.floor(y), GenericMath.floor(z), GenericMath.floor(w));
    }

    public Vector4i mul(int x, int y, int z, int w) {
        return new Vector4i(this.x * x, this.y * y, this.z * z, this.w * w);
    }

    public Vector4i div(double a) {
        return div(GenericMath.floor(a));
    }

    @Override
    public Vector4i div(int a) {
        return div(a, a, a, a);
    }

    public Vector4i div(Vector4i v) {
        return div(v.x, v.y, v.z, v.w);
    }

    public Vector4i div(double x, double y, double z, double w) {
        return div(GenericMath.floor(x), GenericMath.floor(y), GenericMath.floor(z), GenericMath.floor(w));
    }

    public Vector4i div(int x, int y, int z, int w) {
        return new Vector4i(this.x / x, this.y / y, this.z / z, this.w / w);
    }

    public int dot(Vector4i v) {
        return dot(v.x, v.y, v.z, v.w);
    }

    public int dot(double x, double y, double z, double w) {
        return dot(GenericMath.floor(x), GenericMath.floor(y), GenericMath.floor(z), GenericMath.floor(w));
    }

    public int dot(int x, int y, int z, int w) {
        return this.x * x + this.y * y + this.z * z + this.w * w;
    }

    public Vector4i project(Vector4i v) {
        return project(v.x, v.y, v.z, v.w);
    }

    public Vector4i project(double x, double y, double z, double w) {
        return project(GenericMath.floor(x), GenericMath.floor(y), GenericMath.floor(z), GenericMath.floor(w));
    }

    public Vector4i project(int x, int y, int z, int w) {
        final int lengthSquared = x * x + y * y + z * z + w * w;
        if (lengthSquared == 0) {
            throw new ArithmeticException("Cannot project onto the zero vector");
        }
        final float a = (float) dot(x, y, z, w) / lengthSquared;
        return new Vector4i(a * x, a * y, a * z, a * w);
    }

    public Vector4i pow(double pow) {
        return pow(GenericMath.floor(pow));
    }

    @Override
    public Vector4i pow(int power) {
        return new Vector4i(Math.pow(x, power), Math.pow(y, power), Math.pow(z, power), Math.pow(w, power));
    }

    @Override
    public Vector4i abs() {
        return new Vector4i(Math.abs(x), Math.abs(y), Math.abs(z), Math.abs(w));
    }

    @Override
    public Vector4i negate() {
        return new Vector4i(-x, -y, -z, -w);
    }

    public Vector4i min(Vector4i v) {
        return min(v.x, v.y, v.z, v.w);
    }

    public Vector4i min(double x, double y, double z, double w) {
        return min(GenericMath.floor(x), GenericMath.floor(y), GenericMath.floor(z), GenericMath.floor(w));
    }

    public Vector4i min(int x, int y, int z, int w) {
        return new Vector4i(Math.min(this.x, x), Math.min(this.y, y), Math.min(this.z, z), Math.min(this.w, w));
    }

    public Vector4i max(Vector4i v) {
        return max(v.x, v.y, v.z, v.w);
    }

    public Vector4i max(double x, double y, double z, double w) {
        return max(GenericMath.floor(x), GenericMath.floor(y), GenericMath.floor(z), GenericMath.floor(w));
    }

    public Vector4i max(int x, int y, int z, int w) {
        return new Vector4i(Math.max(this.x, x), Math.max(this.y, y), Math.max(this.z, z), Math.max(this.w, w));
    }

    public int distanceSquared(Vector4i v) {
        return distanceSquared(v.x, v.y, v.z, v.w);
    }

    public int distanceSquared(double x, double y, double z, double w) {
        return distanceSquared(GenericMath.floor(x), GenericMath.floor(y), GenericMath.floor(z), GenericMath.floor(w));
    }

    public int distanceSquared(int x, int y, int z, int w) {
        final int dx = this.x - x;
        final int dy = this.y - y;
        final int dz = this.z - z;
        final int dw = this.w - w;
        return dx * dx + dy * dy + dz * dz + dw * dw;
    }

    public float distance(Vector4i v) {
        return distance(v.x, v.y, v.z, v.w);
    }

    public float distance(double x, double y, double z, double w) {
        return distance(GenericMath.floor(x), GenericMath.floor(y), GenericMath.floor(z), GenericMath.floor(w));
    }

    public float distance(int x, int y, int z, int w) {
        return (float) Math.sqrt(distanceSquared(x, y, z, w));
    }

    @Override
    public int lengthSquared() {
        return x * x + y * y + z * z + w * w;
    }

    @Override
    public float length() {
        return (float) Math.sqrt(lengthSquared());
    }

    /**
     * Return the axis with the minimal value.
     *
     * @return {@link int} axis with minimal value
     */
    @Override
    public int getMinAxis() {
        int value = x;
        int axis = 0;
        if (y < value) {
            value = y;
            axis = 1;
        }
        if (z < value) {
            value = z;
            axis = 2;
        }
        if (w < value) {
            axis = 3;
        }
        return axis;
    }

    /**
     * Return the axis with the maximum value.
     *
     * @return {@link int} axis with maximum value
     */
    @Override
    public int getMaxAxis() {
        int value = x;
        int axis = 0;
        if (y > value) {
            value = y;
            axis = 1;
        }
        if (z > value) {
            value = z;
            axis = 2;
        }
        if (w > value) {
            axis = 3;
        }
        return axis;
    }

    public Vector2i toVector2() {
        return new Vector2i(this);
    }

    public Vector3i toVector3() {
        return new Vector3i(this);
    }

    public VectorNi toVectorN() {
        return new VectorNi(this);
    }

    @Override
    public int[] toArray() {
        return new int[]{x, y, z, w};
    }

    @Override
    public Vector4i toInt() {
        return new Vector4i(x, y, z, w);
    }

    @Override
    public Vector4l toLong() {
        return new Vector4l(x, y, z, w);
    }

    @Override
    public Vector4f toFloat() {
        return new Vector4f(x, y, z, w);
    }

    @Override
    public Vector4d toDouble() {
        return new Vector4d(x, y, z, w);
    }

    @Override
    public int compareTo(Vector4i v) {
        return (int) (lengthSquared() - v.lengthSquared());
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (!(o instanceof Vector4i)) {
            return false;
        }
        final Vector4i vector4 = (Vector4i) o;
        if (vector4.x != x) {
            return false;
        }
        if (vector4.y != y) {
            return false;
        }
        if (vector4.z != z) {
            return false;
        }
        if (vector4.w != w) {
            return false;
        }
        return true;
    }

    @Override
    public int hashCode() {
        if (!hashed) {
            int result = (x != +0.0f ? HashFunctions.hash(x) : 0);
            result = 31 * result + (y != +0.0f ? HashFunctions.hash(y) : 0);
            result = 31 * result + (z != +0.0f ? HashFunctions.hash(z) : 0);
            hashCode = 31 * result + (w != +0.0f ? HashFunctions.hash(w) : 0);
            hashed = true;
        }
        return hashCode;
    }

    @Override
    public Vector4i clone() {
        return new Vector4i(this);
    }

    @Override
    public String toString() {
        return "(" + x + ", " + y + ", " + z + ", " + w + ")";
    }

    public static Vector4i from(int n) {
         return n == 0 ? ZERO : new Vector4i(n, n, n, n);
    }

    public static Vector4i from(int x, int y, int z, int w) {
         return x == 0 && y == 0 && z == 0 && w == 0 ? ZERO : new Vector4i(x, y, z, w);
    }
}


package com.flowpowered.math.vector;

import java.io.Serializable;
import java.lang.Override;
import java.util.Random;

import com.flowpowered.math.GenericMath;
import com.flowpowered.math.HashFunctions;
import com.flowpowered.math.TrigMath;

public class Vector2d implements Vectord, Comparable<Vector2d>, Serializable, Cloneable {
    private static final long serialVersionUID = 1;
    public static final Vector2d ZERO = new Vector2d(0, 0);
    public static final Vector2d UNIT_X = new Vector2d(1, 0);
    public static final Vector2d UNIT_Y = new Vector2d(0, 1);
    public static final Vector2d ONE = new Vector2d(1, 1);
    private final double x;
    private final double y;
    private transient volatile boolean hashed = false;
    private transient volatile int hashCode = 0;

    public Vector2d() {
        this(0, 0);
    }

    public Vector2d(Vector2d v) {
        this(v.x, v.y);
    }

    public Vector2d(Vector3d v) {
        this(v.getX(), v.getY());
    }

    public Vector2d(Vector4d v) {
        this(v.getX(), v.getY());
    }

    public Vector2d(VectorNd v) {
        this(v.get(0), v.get(1));
    }

    public Vector2d(float x, float y) {
        this((double) x, (double) y);
    }

    public Vector2d(double x, double y) {
        this.x = x;
        this.y = y;
    }

    public double getX() {
        return x;
    }

    public double getY() {
        return y;
    }

    public int getFloorX() {
        return GenericMath.floor(x);
    }

    public int getFloorY() {
        return GenericMath.floor(y);
    }

    public Vector2d add(Vector2d v) {
        return add(v.x, v.y);
    }

    public Vector2d add(float x, float y) {
        return add((double) x, (double) y);
    }

    public Vector2d add(double x, double y) {
        return new Vector2d(this.x + x, this.y + y);
    }

    public Vector2d sub(Vector2d v) {
        return sub(v.x, v.y);
    }

    public Vector2d sub(float x, float y) {
        return sub((double) x, (double) y);
    }

    public Vector2d sub(double x, double y) {
        return new Vector2d(this.x - x, this.y - y);
    }

    public Vector2d mul(float a) {
        return mul((double) a);
    }

    @Override
    public Vector2d mul(double a) {
        return mul(a, a);
    }

    public Vector2d mul(Vector2d v) {
        return mul(v.x, v.y);
    }

    public Vector2d mul(float x, float y) {
        return mul((double) x, (double) y);
    }

    public Vector2d mul(double x, double y) {
        return new Vector2d(this.x * x, this.y * y);
    }

    public Vector2d div(float a) {
        return div((double) a);
    }

    @Override
    public Vector2d div(double a) {
        return div(a, a);
    }

    public Vector2d div(Vector2d v) {
        return div(v.x, v.y);
    }

    public Vector2d div(float x, float y) {
        return div((double) x, (double) y);
    }

    public Vector2d div(double x, double y) {
        return new Vector2d(this.x / x, this.y / y);
    }

    public double dot(Vector2d v) {
        return dot(v.x, v.y);
    }

    public double dot(float x, float y) {
        return dot((double) x, (double) y);
    }

    public double dot(double x, double y) {
        return this.x * x + this.y * y;
    }

    public Vector2d project(Vector2d v) {
        return project(v.x, v.y);
    }

    public Vector2d project(float x, float y) {
        return project((double) x, (double) y);
    }

    public Vector2d project(double x, double y) {
        final double lengthSquared = x * x + y * y;
        if (Math.abs(lengthSquared) < GenericMath.DBL_EPSILON) {
            throw new ArithmeticException("Cannot project onto the zero vector");
        }
        final double a = dot(x, y) / lengthSquared;
        return new Vector2d(a * x, a * y);
    }

    public Vector2d pow(float pow) {
        return pow((double) pow);
    }

    @Override
    public Vector2d pow(double power) {
        return new Vector2d(Math.pow(x, power), Math.pow(y, power));
    }

    @Override
    public Vector2d ceil() {
        return new Vector2d(Math.ceil(x), Math.ceil(y));
    }

    @Override
    public Vector2d floor() {
        return new Vector2d(GenericMath.floor(x), GenericMath.floor(y));
    }

    @Override
    public Vector2d round() {
        return new Vector2d(Math.round(x), Math.round(y));
    }

    @Override
    public Vector2d abs() {
        return new Vector2d(Math.abs(x), Math.abs(y));
    }

    @Override
    public Vector2d negate() {
        return new Vector2d(-x, -y);
    }

    public Vector2d min(Vector2d v) {
        return min(v.x, v.y);
    }

    public Vector2d min(float x, float y) {
        return min((double) x, (double) y);
    }

    public Vector2d min(double x, double y) {
        return new Vector2d(Math.min(this.x, x), Math.min(this.y, y));
    }

    public Vector2d max(Vector2d v) {
        return max(v.x, v.y);
    }

    public Vector2d max(float x, float y) {
        return max((double) x, (double) y);
    }

    public Vector2d max(double x, double y) {
        return new Vector2d(Math.max(this.x, x), Math.max(this.y, y));
    }

    public double distanceSquared(Vector2d v) {
        return distanceSquared(v.x, v.y);
    }

    public double distanceSquared(float x, float y) {
        return distanceSquared((double) x, (double) y);
    }

    public double distanceSquared(double x, double y) {
        final double dx = this.x - x;
        final double dy = this.y - y;
        return dx * dx + dy * dy;
    }

    public double distance(Vector2d v) {
        return distance(v.x, v.y);
    }

    public double distance(float x, float y) {
        return distance((double) x, (double) y);
    }

    public double distance(double x, double y) {
        return (double) Math.sqrt(distanceSquared(x, y));
    }

    @Override
    public double lengthSquared() {
        return x * x + y * y;
    }

    @Override
    public double length() {
        return (double) Math.sqrt(lengthSquared());
    }

    @Override
    public Vector2d normalize() {
        final double length = length();
        if (Math.abs(length) < GenericMath.DBL_EPSILON) {
            throw new ArithmeticException("Cannot normalize the zero vector");
        }
        return new Vector2d(x / length, y / length);
    }

    /**
     * Return the axis with the minimal value.
     *
     * @return {@link int} axis with minimal value
     */
    @Override
    public int getMinAxis() {
        return x < y ? 0 : 1;
    }

    /**
     * Return the axis with the maximum value.
     *
     * @return {@link int} axis with maximum value
     */
    @Override
    public int getMaxAxis() {
        return x > y ? 0 : 1;
    }

    public Vector3d toVector3() {
        return toVector3(0);
    }

    public Vector3d toVector3(float z) {
        return toVector3((double) z);
    }

    public Vector3d toVector3(double z) {
        return new Vector3d(this, z);
    }

    public Vector4d toVector4() {
        return toVector4(0, 0);
    }

    public Vector4d toVector4(float z, float w) {
        return toVector4((double) z, (double) w);
    }

    public Vector4d toVector4(double z, double w) {
        return new Vector4d(this, z, w);
    }

    public VectorNd toVectorN() {
        return new VectorNd(this);
    }

    @Override
    public double[] toArray() {
        return new double[]{x, y};
    }

    @Override
    public Vector2i toInt() {
        return new Vector2i(x, y);
    }

    @Override
    public Vector2l toLong() {
        return new Vector2l(x, y);
    }

    @Override
    public Vector2f toFloat() {
        return new Vector2f(x, y);
    }

    @Override
    public Vector2d toDouble() {
        return new Vector2d(x, y);
    }

    @Override
    public int compareTo(Vector2d v) {
        return (int) Math.signum(lengthSquared() - v.lengthSquared());
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (!(o instanceof Vector2d)) {
            return false;
        }
        final Vector2d vector2 = (Vector2d) o;
        if (Double.compare(vector2.x, x) != 0) {
            return false;
        }
        if (Double.compare(vector2.y, y) != 0) {
            return false;
        }
        return true;
    }

    @Override
    public int hashCode() {
        if (!hashed) {
            final int result = (x != +0.0f ? HashFunctions.hash(x) : 0);
            hashCode = 31 * result + (y != +0.0f ? HashFunctions.hash(y) : 0);
            hashed = true;
        }
        return hashCode;
    }

    @Override
    public Vector2d clone() {
        return new Vector2d(this);
    }

    @Override
    public String toString() {
        return "(" + x + ", " + y + ")";
    }

    public static Vector2d from(double n) {
         return n == 0 ? ZERO : new Vector2d(n, n);
    }

    public static Vector2d from(double x, double y) {
         return x == 0 && y == 0 ? ZERO : new Vector2d(x, y);
    }

    /**
     * Gets the direction vector of a random angle using the random specified.
     *
     * @param random to use
     * @return the random direction vector
     */
    public static Vector2d createRandomDirection(Random random) {
        return createDirectionRad(random.nextDouble() * (double) TrigMath.TWO_PI);
    }

    /**
     * Gets the direction vector of a certain angle in degrees.
     *
     * @param angle in degrees
     * @return the direction vector
     */
    public static Vector2d createDirectionDeg(float angle) {
        return createDirectionDeg((double) angle);
    }

    /**
     * Gets the direction vector of a certain angle in degrees.
     *
     * @param angle in degrees
     * @return the direction vector
     */
    public static Vector2d createDirectionDeg(double angle) {
        return createDirectionRad((double) Math.toRadians(angle));
    }

    /**
     * Gets the direction vector of a certain angle in radians.
     *
     * @param angle in radians
     * @return the direction vector
     */
    public static Vector2d createDirectionRad(float angle) {
        return createDirectionRad((double) angle);
    }

    /**
     * Gets the direction vector of a certain angle in radians.
     *
     * @param angle in radians
     * @return the direction vector
     */
    public static Vector2d createDirectionRad(double angle) {
        return new Vector2d(TrigMath.cos(angle), TrigMath.sin(angle));
    }
}


package com.flowpowered.math.matrix;

import java.io.Serializable;
import java.util.Arrays;

import com.flowpowered.math.GenericMath;
import com.flowpowered.math.TrigMath;
import com.flowpowered.math.imaginary.Complexd;
import com.flowpowered.math.imaginary.Quaterniond;
import com.flowpowered.math.vector.Vector3d;
import com.flowpowered.math.vector.VectorNd;

public class MatrixNd implements Matrixd, Serializable, Cloneable {
    private static final long serialVersionUID = 1;
    public static final MatrixNd IDENTITY_2 = new ImmutableIdentityMatrixN(2);
    public static final MatrixNd IDENTITY_3 = new ImmutableIdentityMatrixN(3);
    public static final MatrixNd IDENTITY_4 = new ImmutableIdentityMatrixN(4);
    private final double[][] mat;

    public MatrixNd(int size) {
        if (size < 2) {
            throw new IllegalArgumentException("Minimum matrix size is 2");
        }
        mat = new double[size][size];
        setIdentity();
    }

    public MatrixNd(Matrix2d m) {
        mat = new double[][]{
                {m.get(0, 0), m.get(0, 1)},
                {m.get(1, 0), m.get(1, 1)}
        };
    }

    public MatrixNd(Matrix3d m) {
        mat = new double[][]{
                {m.get(0, 0), m.get(0, 1), m.get(0, 2)},
                {m.get(1, 0), m.get(1, 1), m.get(1, 2)},
                {m.get(2, 0), m.get(2, 1), m.get(2, 2)}
        };
    }

    public MatrixNd(Matrix4d m) {
        mat = new double[][]{
                {m.get(0, 0), m.get(0, 1), m.get(0, 2), m.get(0, 3)},
                {m.get(1, 0), m.get(1, 1), m.get(1, 2), m.get(1, 3)},
                {m.get(2, 0), m.get(2, 1), m.get(2, 2), m.get(2, 3)},
                {m.get(3, 0), m.get(3, 1), m.get(3, 2), m.get(3, 3)}
        };
    }

    public MatrixNd(double... m) {
        if (m.length < 4) {
            throw new IllegalArgumentException("Minimum matrix size is 2");
        }
        final int size = (int) Math.ceil(Math.sqrt(m.length));
        mat = new double[size][size];
        for (int row = 0; row < size; row++) {
            for (int col = 0; col < size; col++) {
                final int index = col + row * size;
                if (index < m.length) {
                    mat[row][col] = m[index];
                } else {
                    mat[row][col] = 0;
                }
            }
        }
    }

    public MatrixNd(MatrixNd m) {
        mat = deepClone(m.mat);
    }

    public int size() {
        return mat.length;
    }

    @Override
    public double get(int row, int col) {
        return mat[row][col];
    }

    @Override
    public VectorNd getRow(int row) {
        final int size = size();
        final VectorNd d = new VectorNd(size);
        for (int col = 0; col < size; col++) {
            d.set(col, get(row, col));
        }
        return d;
    }

    @Override
    public VectorNd getColumn(int col) {
        final int size = size();
        final VectorNd d = new VectorNd(size);
        for (int row = 0; row < size; row++) {
            d.set(row, get(row, col));
        }
        return d;
    }

    public void set(int row, int col, float val) {
        set(row, col, (double) val);
    }

    public void set(int row, int col, double val) {
        mat[row][col] = val;
    }

    public final void setIdentity() {
        final int size = size();
        for (int row = 0; row < size; row++) {
            for (int col = 0; col < size; col++) {
                if (row == col) {
                    mat[row][col] = 1;
                } else {
                    mat[row][col] = 0;
                }
            }
        }
    }

    public void setZero() {
        final int size = size();
        for (int row = 0; row < size; row++) {
            Arrays.fill(mat[row], 0);
        }
    }

    public MatrixNd resize(int size) {
        final MatrixNd d = new MatrixNd(size);
        for (int rowCol = size(); rowCol < size; rowCol++) {
            d.set(rowCol, rowCol, 0);
        }
        size = Math.min(size, size());
        for (int row = 0; row < size; row++) {
            System.arraycopy(mat[row], 0, d.mat[row], 0, size);
        }
        return d;
    }

    public MatrixNd add(MatrixNd m) {
        final int size = size();
        if (size != m.size()) {
            throw new IllegalArgumentException("Matrix sizes must be the same");
        }
        final MatrixNd d = new MatrixNd(size);
        for (int row = 0; row < size; row++) {
            for (int col = 0; col < size; col++) {
                d.mat[row][col] = mat[row][col] + m.mat[row][col];
            }
        }
        return d;
    }

    public MatrixNd sub(MatrixNd m) {
        final int size = size();
        if (size != m.size()) {
            throw new IllegalArgumentException("Matrix sizes must be the same");
        }
        final MatrixNd d = new MatrixNd(size);
        for (int row = 0; row < size; row++) {
            for (int col = 0; col < size; col++) {
                d.mat[row][col] = mat[row][col] - m.mat[row][col];
            }
        }
        return d;
    }

    public MatrixNd mul(float a) {
        return mul((double) a);
    }

    @Override
    public MatrixNd mul(double a) {
        final int size = size();
        final MatrixNd d = new MatrixNd(size);
        for (int row = 0; row < size; row++) {
            for (int col = 0; col < size; col++) {
                d.mat[row][col] = mat[row][col] * a;
            }
        }
        return d;
    }

    public MatrixNd mul(MatrixNd m) {
        final int size = size();
        if (size != m.size()) {
            throw new IllegalArgumentException("Matrix sizes must be the same");
        }
        final MatrixNd d = new MatrixNd(size);
        for (int row = 0; row < size; row++) {
            for (int col = 0; col < size; col++) {
                double dot = 0;
                for (int i = 0; i < size; i++) {
                    dot += mat[row][i] * m.mat[i][col];
                }
                d.mat[row][col] = dot;
            }
        }
        return d;
    }

    public MatrixNd div(float a) {
        return div((double) a);
    }

    @Override
    public MatrixNd div(double a) {
        final int size = size();
        final MatrixNd d = new MatrixNd(size);
        for (int row = 0; row < size; row++) {
            for (int col = 0; col < size; col++) {
                d.mat[row][col] = mat[row][col] / a;
            }
        }
        return d;
    }

    public MatrixNd div(MatrixNd m) {
        return mul(m.invert());
    }

    public MatrixNd pow(float pow) {
        return pow((double) pow);
    }

    @Override
    public MatrixNd pow(double pow) {
        final int size = size();
        final MatrixNd d = new MatrixNd(size);
        for (int row = 0; row < size; row++) {
            for (int col = 0; col < size; col++) {
                d.mat[row][col] = (double) Math.pow(mat[row][col], pow);
            }
        }
        return d;
    }

    public MatrixNd translate(VectorNd v) {
        return translate(v.toArray());
    }

    public MatrixNd translate(double... v) {
        return createTranslation(v).mul(this);
    }

    public MatrixNd scale(VectorNd v) {
        return scale(v.toArray());
    }

    public MatrixNd scale(double... v) {
        return createScaling(v).mul(this);
    }

    public MatrixNd rotate(Complexd rot) {
        return createRotation(size(), rot).mul(this);
    }

    public MatrixNd rotate(Quaterniond rot) {
        return createRotation(size(), rot).mul(this);
    }

    public VectorNd transform(VectorNd v) {
        return transform(v.toArray());
    }

    public VectorNd transform(double... vec) {
        final int size = size();
        if (size != vec.length) {
            throw new IllegalArgumentException("Matrix and vector sizes must be the same");
        }
        final VectorNd d = new VectorNd(size);
        for (int row = 0; row < size; row++) {
            double dot = 0;
            for (int col = 0; col < size; col++) {
                dot += mat[row][col] * vec[col];
            }
            d.set(row, dot);
        }
        return d;
    }

    @Override
    public MatrixNd floor() {
        final int size = size();
        final MatrixNd d = new MatrixNd(size);
        for (int row = 0; row < size; row++) {
            for (int col = 0; col < size; col++) {
                d.mat[row][col] = GenericMath.floor(mat[row][col]);
            }
        }
        return d;
    }

    @Override
    public MatrixNd ceil() {
        final int size = size();
        final MatrixNd d = new MatrixNd(size);
        for (int row = 0; row < size; row++) {
            for (int col = 0; col < size; col++) {
                d.mat[row][col] = (double) Math.ceil(mat[row][col]);
            }
        }
        return d;
    }

    @Override
    public MatrixNd round() {
        final int size = size();
        final MatrixNd d = new MatrixNd(size);
        for (int row = 0; row < size; row++) {
            for (int col = 0; col < size; col++) {
                d.mat[row][col] = Math.round(mat[row][col]);
            }
        }
        return d;
    }

    @Override
    public MatrixNd abs() {
        final int size = size();
        final MatrixNd d = new MatrixNd(size);
        for (int row = 0; row < size; row++) {
            for (int col = 0; col < size; col++) {
                d.mat[row][col] = Math.abs(mat[row][col]);
            }
        }
        return d;
    }

    @Override
    public MatrixNd negate() {
        final int size = size();
        final MatrixNd d = new MatrixNd(size);
        for (int row = 0; row < size; row++) {
            for (int col = 0; col < size; col++) {
                d.mat[row][col] = -mat[row][col];
            }
        }
        return d;
    }

    @Override
    public MatrixNd transpose() {
        final int size = size();
        final MatrixNd d = new MatrixNd(size);
        for (int row = 0; row < size; row++) {
            for (int col = 0; col < size; col++) {
                d.mat[row][col] = mat[col][row];
            }
        }
        return d;
    }

    @Override
    public double trace() {
        final int size = size();
        double trace = 0;
        for (int rowCol = 0; rowCol < size; rowCol++) {
            trace += mat[rowCol][rowCol];
        }
        return trace;
    }

    @Override
    public double determinant() {
        final int size = size();
        final double[][] m = deepClone(mat);
        double det;
        for (int i = 0; i < size - 1; i++) {
            for (int col = i + 1; col < size; col++) {
                det = m[i][i] < GenericMath.DBL_EPSILON ? 0 : m[i][col] / m[i][i];
                for (int row = i; row < size; row++) {
                    m[row][col] -= det * m[row][i];
                }
            }
        }
        det = 1;
        for (int i = 0; i < size; i++) {
            det *= m[i][i];
        }
        return det;
    }

    @Override
    public MatrixNd invert() {
        if (Math.abs(determinant()) < GenericMath.DBL_EPSILON) {
            throw new ArithmeticException("Cannot inverse a matrix with a zero determinant");
        }
        final int size = size();
        final AugmentedMatrixN augMat = new AugmentedMatrixN(this);
        final int augmentedSize = augMat.getAugmentedSize();
        for (int i = 0; i < size; i++) {
            for (int row = 0; row < size; row++) {
                if (i != row) {
                    final double ratio = augMat.get(row, i) / augMat.get(i, i);
                    for (int col = 0; col < augmentedSize; col++) {
                        augMat.set(row, col, augMat.get(row, col) - ratio * augMat.get(i, col));
                    }
                }
            }
        }
        for (int row = 0; row < size; row++) {
            final double div = augMat.get(row, row);
            for (int col = 0; col < augmentedSize; col++) {
                augMat.set(row, col, augMat.get(row, col) / div);
            }
        }
        return augMat.getAugmentation();
    }

    public Matrix2d toMatrix2() {
        return new Matrix2d(this);
    }

    public Matrix3d toMatrix3() {
        return new Matrix3d(this);
    }

    public Matrix4d toMatrix4() {
        return new Matrix4d(this);
    }

    public double[] toArray() {
        return toArray(false);
    }

    @Override
    public MatrixNf toFloat() {
        final int size = size();
        final float[] m = new float[size * size];
        for (int row = 0; row < size; row++) {
            for (int col = 0; col < size; col++) {
                m[col + row * size] = (float) get(row, col);
            }
        }
        return new MatrixNf(m);
    }

    @Override
    public MatrixNd toDouble() {
        final int size = size();
        final double[] m = new double[size * size];
        for (int row = 0; row < size; row++) {
            for (int col = 0; col < size; col++) {
                m[col + row * size] = (double) get(row, col);
            }
        }
        return new MatrixNd(m);
    }

    @Override
    public double[] toArray(boolean columnMajor) {
        final int size = size();
        final double[] array = new double[size * size];
        if (columnMajor) {
            for (int col = 0; col < size; col++) {
                for (int row = 0; row < size; row++) {
                    array[row + col * size] = mat[row][col];
                }
            }
        } else {
            for (int row = 0; row < size; row++) {
                System.arraycopy(mat[row], 0, array, row * size, size);
            }
        }
        return array;
    }

    @Override
    public String toString() {
        final int size = size();
        final StringBuilder builder = new StringBuilder();
        for (int row = 0; row < size; row++) {
            for (int col = 0; col < size; col++) {
                builder.append(mat[row][col]);
                if (col < size - 1) {
                    builder.append(' ');
                }
            }
            if (row < size - 1) {
                builder.append('\n');
            }
        }
        return builder.toString();
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == this) {
            return true;
        }
        if (!(obj instanceof MatrixNd)) {
            return false;
        }
        return Arrays.deepEquals(mat, ((MatrixNd) obj).mat);
    }

    @Override
    public int hashCode() {
        return 79 * 5 + Arrays.deepHashCode(mat);
    }

    @Override
    public MatrixNd clone() {
        return new MatrixNd(this);
    }

    public static MatrixNd createScaling(VectorNd v) {
        return createScaling(v.toArray());
    }

    public static MatrixNd createScaling(double... vec) {
        final int size = vec.length;
        final MatrixNd m = new MatrixNd(size);
        for (int rowCol = 0; rowCol < size; rowCol++) {
            m.set(rowCol, rowCol, vec[rowCol]);
        }
        return m;
    }

    public static MatrixNd createTranslation(VectorNd v) {
        return createTranslation(v.toArray());
    }

    public static MatrixNd createTranslation(double... vec) {
        final int size = vec.length;
        final MatrixNd m = new MatrixNd(size + 1);
        for (int row = 0; row < size; row++) {
            m.set(row, size, vec[row]);
        }
        return m;
    }

    public static MatrixNd createRotation(int size, Complexd rot) {
        if (size < 2) {
            throw new IllegalArgumentException("Minimum matrix size is 2");
        }
        final MatrixNd m = new MatrixNd(size);
        rot = rot.normalize();
        m.set(0, 0, rot.getX());
        m.set(0, 1, -rot.getY());
        m.set(1, 0, rot.getY());
        m.set(1, 1, rot.getX());
        return m;
    }

    public static MatrixNd createRotation(int size, Quaterniond rot) {
        if (size < 3) {
            throw new IllegalArgumentException("Minimum matrix size is 3");
        }
        final MatrixNd m = new MatrixNd(size);
        rot = rot.normalize();
        m.set(0, 0, 1 - 2 * rot.getY() * rot.getY() - 2 * rot.getZ() * rot.getZ());
        m.set(0, 1, 2 * rot.getX() * rot.getY() - 2 * rot.getW() * rot.getZ());
        m.set(0, 2, 2 * rot.getX() * rot.getZ() + 2 * rot.getW() * rot.getY());
        m.set(1, 0, 2 * rot.getX() * rot.getY() + 2 * rot.getW() * rot.getZ());
        m.set(1, 1, 1 - 2 * rot.getX() * rot.getX() - 2 * rot.getZ() * rot.getZ());
        m.set(1, 2, 2 * rot.getY() * rot.getZ() - 2 * rot.getW() * rot.getX());
        m.set(2, 0, 2 * rot.getX() * rot.getZ() - 2 * rot.getW() * rot.getY());
        m.set(2, 1, 2 * rot.getY() * rot.getZ() + 2 * rot.getX() * rot.getW());
        m.set(2, 2, 1 - 2 * rot.getX() * rot.getX() - 2 * rot.getY() * rot.getY());
        return m;
    }

    /**
     * Creates a "look at" matrix for the given eye point.
     *
     * @param size The size of the matrix, minimum of 4
     * @param eye The position of the camera
     * @param at The point that the camera is looking at
     * @param up The "up" vector
     * @return A rotational transform that corresponds to a camera looking at the given point
     */
    public static MatrixNd createLookAt(int size, Vector3d eye, Vector3d at, Vector3d up) {
        if (size < 4) {
            throw new IllegalArgumentException("Minimum matrix size is 4");
        }
        final Vector3d f = at.sub(eye).normalize();
        up = up.normalize();
        final Vector3d s = f.cross(up).normalize();
        final Vector3d u = s.cross(f).normalize();
        final MatrixNd mat = new MatrixNd(size);
        mat.set(0, 0, s.getX());
        mat.set(0, 1, s.getY());
        mat.set(0, 2, s.getZ());
        mat.set(1, 0, u.getX());
        mat.set(1, 1, u.getY());
        mat.set(1, 2, u.getZ());
        mat.set(2, 0, -f.getX());
        mat.set(2, 1, -f.getY());
        mat.set(2, 2, -f.getZ());
        return mat.translate(eye.mul(-1).toVectorN());
    }

    /**
     * Creates a perspective projection matrix with the given (x) FOV, aspect, near and far planes
     *
     * @param size The size of the matrix, minimum of 4
     * @param fov The field of view in the x direction
     * @param aspect The aspect ratio, usually width/height
     * @param near The near plane, cannot be 0
     * @param far the far plane, zFar cannot equal zNear
     * @return A perspective projection matrix built from the given values
     */
    public static MatrixNd createPerspective(int size, float fov, float aspect, float near, float far) {
        return createPerspective(size, (double) fov, (double) aspect, (double) near, (double) far);
    }

    /**
     * Creates a perspective projection matrix with the given (x) FOV, aspect, near and far planes
     *
     * @param size The size of the matrix, minimum of 4
     * @param fov The field of view in the x direction
     * @param aspect The aspect ratio, usually width/height
     * @param near The near plane, cannot be 0
     * @param far the far plane, zFar cannot equal zNear
     * @return A perspective projection matrix built from the given values
     */
    public static MatrixNd createPerspective(int size, double fov, double aspect, double near, double far) {
        if (size < 4) {
            throw new IllegalArgumentException("Minimum matrix size is 4");
        }
        final MatrixNd perspective = new MatrixNd(size);
        final double scale = 1 / TrigMath.tan(fov * (double) TrigMath.HALF_DEG_TO_RAD);
        perspective.set(0, 0, scale / aspect);
        perspective.set(1, 1, scale);
        perspective.set(2, 2, (far + near) / (near - far));
        perspective.set(2, 3, 2 * far * near / (near - far));
        perspective.set(3, 2, -1);
        perspective.set(3, 3, 0);
        return perspective;
    }

    /**
     * Creates an orthographic viewing frustum built from the provided values
     *
     * @param size The size of the matrix, minimum of 4
     * @param right the right most plane of the viewing frustum
     * @param left the left most plane of the viewing frustum
     * @param top the top plane of the viewing frustum
     * @param bottom the bottom plane of the viewing frustum
     * @param near the near plane of the viewing frustum
     * @param far the far plane of the viewing frustum
     * @return A viewing frustum built from the provided values
     */
    public static MatrixNd createOrthographic(int size, float right, float left, float top, float bottom,
                                             float near, float far) {
        return createOrthographic(size, (double) right, (double) left, (double) top, (double) bottom, (double) near, (double) far);
    }

    /**
     * Creates an orthographic viewing frustum built from the provided values
     *
     * @param size The size of the matrix, minimum of 4
     * @param right the right most plane of the viewing frustum
     * @param left the left most plane of the viewing frustum
     * @param top the top plane of the viewing frustum
     * @param bottom the bottom plane of the viewing frustum
     * @param near the near plane of the viewing frustum
     * @param far the far plane of the viewing frustum
     * @return A viewing frustum built from the provided values
     */
    public static MatrixNd createOrthographic(int size, double right, double left, double top, double bottom,
                                             double near, double far) {
        if (size < 4) {
            throw new IllegalArgumentException("Minimum matrix size is 4");
        }
        final MatrixNd orthographic = new MatrixNd(size);
        orthographic.set(0, 0, 2 / (right - left));
        orthographic.set(1, 1, 2 / (top - bottom));
        orthographic.set(2, 2, -2 / (far - near));
        orthographic.set(0, 3, -(right + left) / (right - left));
        orthographic.set(1, 3, -(top + bottom) / (top - bottom));
        orthographic.set(2, 3, -(far + near) / (far - near));
        return orthographic;
    }

    private static double[][] deepClone(double[][] array) {
        final int size = array.length;
        final double[][] clone = array.clone();
        for (int i = 0; i < size; i++) {
            clone[i] = array[i].clone();
        }
        return clone;
    }

    private static class ImmutableIdentityMatrixN extends MatrixNd {
        public ImmutableIdentityMatrixN(int size) {
            super(size);
        }

        @Override
        public void set(int row, int col, double val) {
            throw new UnsupportedOperationException("You may not alter this matrix");
        }

        @Override
        public void setZero() {
            throw new UnsupportedOperationException("You may not alter this matrix");
        }
    }

    private static class AugmentedMatrixN {
        private final MatrixNd mat;
        private final MatrixNd aug;
        private final int size;

        private AugmentedMatrixN(MatrixNd mat) {
            this.mat = mat.clone();
            this.size = mat.size();
            aug = new MatrixNd(size);
        }

        private MatrixNd getAugmentation() {
            return aug;
        }

        private int getAugmentedSize() {
            return size * 2;
        }

        private double get(int row, int col) {
            if (col < size) {
                return mat.get(row, col);
            } else {
                return aug.get(row, col - size);
            }
        }

        private void set(int row, int col, double val) {
            if (col < size) {
                mat.set(row, col, val);
            } else {
                aug.set(row, col - size, val);
            }
        }
    }
}