/*
** 2011 April 5
**
** The author disclaims copyright to this source code.  In place of
** a legal notice, here is a blessing:
**    May you do good and not evil.
**    May you find forgiveness for yourself and forgive others.
**    May you share freely, never taking more than you give.
*/

package info.ata4.bsplib.vector;

import info.ata4.io.DataReader;
import info.ata4.io.DataWriter;
import java.io.IOException;

/**
 * An immutable fluent interface three-dimensional vector class for float values.
 * 
 * Original class name: unmap.Vec
 * Original author: Bob (Mellish?)
 * Original creation date: January 20, 2005, 7:41 PM
 *
 * @author Nico Bergemann <barracuda415 at yahoo.de>
 */
public final class Vector3f extends VectorXf {

    public static Vector3f read(DataReader in) throws IOException {
        float x = in.readFloat();
        float y = in.readFloat();
        float z = in.readFloat();
        return new Vector3f(x, y, z);
    }

    public static void write(DataWriter out, Vector3f vec) throws IOException {
        out.writeFloat(vec.x);
        out.writeFloat(vec.y);
        out.writeFloat(vec.z);
    }

    // frequently used pre-defined vectors
    public static final Vector3f NULL = new Vector3f(0, 0, 0);
    public static final Vector3f MAX_VALUE = new Vector3f(Float.MAX_VALUE, Float.MAX_VALUE, Float.MAX_VALUE);
    public static final Vector3f MIN_VALUE = MAX_VALUE.scalar(-1); // don't use Float.MIN_VALUE here

    // base unit vectors
    public static final Vector3f BASE_VECTOR_X = new Vector3f(1, 0, 0);
    public static final Vector3f BASE_VECTOR_Y = new Vector3f(0, 1, 0);
    public static final Vector3f BASE_VECTOR_Z = new Vector3f(0, 0, 1);

    // vector values
    public final float x;
    public final float y;
    public final float z;

    /**
     * Replicating contructor
     * Constructs new Vector3f by copying an existing Vector3f
     * 
     * @param v The Vector3f to copy
     */
    public Vector3f(Vector3f v) {
        this(v.x, v.y, v.z);
    }

    /**
     * Constructs a new Vector3f using the values out of an array.
     * The array must have a size of at least 3.
     * 
     * @param v float array
     */
    public Vector3f(float[] v) {
        this(v[0], v[1], v[2]);
    }

    /**
     * Constructs a new Vector3f from x, y and z components
     * 
     * @param x the vector x component
     * @param y the vector y component
     * @param z the vector z component
     */
    public Vector3f(float x, float y, float z) {
        super(3);
        this.x = x;
        this.y = y;
        this.z = z;
    }

    /**
     * Returns the value of the n'th component.
     * 
     * @param index component index
     * @return component value
     */
    @Override
    public float get(int index) {
        switch (index) {
            case 0:
                return this.x;
            case 1:
                return this.y;
            case 2:
                return this.z;
            default:
                return 0;
        }
    }

    /**
     * Set the value of the n'th component.
     * 
     * @param index component index
     * @param value new component value
     * @return vector with new value
     */
    @Override
    public Vector3f set(int index, float value) {
        switch (index) {
            case 0:
                return new Vector3f(value, y, z);
            case 1:
                return new Vector3f(x, value, z);
            case 2:
                return new Vector3f(x, y, value);
            default:
                return this;
        }
    }

    /**
     * Vector dot product: this . that
     * 
     * @param that the Vector3f to take dot product with
     * @return the dot product of the two vectors
     */
    public float dot(Vector3f that) {
        return this.x * that.x + this.y * that.y + this.z * that.z;
    }

    /**
     * Vector cross product: this x that
     * 
     * @param that the vector to take a cross product
     * @return the cross-product vector
     */
    public Vector3f cross(Vector3f that) {
        float rx = this.y * that.z - this.z * that.y;
        float ry = this.z * that.x - this.x * that.z;
        float rz = this.x * that.y - this.y * that.x;

        return new Vector3f(rx, ry, rz);
    }

    /**
     * Vector normalisation: ^this
     * 
     * @return the normalised vector
     */
    public Vector3f normalize() {
        float len = length();
        float rx = x / len;
        float ry = y / len;
        float rz = z / len;

        return new Vector3f(rx, ry, rz);
    }

    /**
     * Vector addition: this + that
     * 
     * @param that The vector to add
     * @return The sum of the two vectors
     */
    public Vector3f add(Vector3f that) {
        float rx = this.x + that.x;
        float ry = this.y + that.y;
        float rz = this.z + that.z;

        return new Vector3f(rx, ry, rz);
    }

    /**
     * Vector subtraction: this - that
     * 
     * @param that The vector to subtract
     * @return The difference of the two vectors
     */
    public Vector3f sub(Vector3f that) {
        float rx = this.x - that.x;
        float ry = this.y - that.y;
        float rz = this.z - that.z;

        return new Vector3f(rx, ry, rz);
    }

    /**
     * Snap vector to nearest value: round(this / value) * value
     * 
     * @param value snap value
     * @return This vector snapped to the nearest values of 'value'
     */
    public Vector3f snap(float value) {
        float rx = Math.round(x / value) * value;
        float ry = Math.round(y / value) * value;
        float rz = Math.round(z / value) * value;

        return new Vector3f(rx, ry, rz);
    }

    /**
     * Calculate the length of this vector
     * 
     * @return length of this vector
     */
    public float length() {
        return (float) Math.sqrt(Math.pow(this.x, 2) + Math.pow(this.y, 2) + Math.pow(this.z, 2));
    }

    /**
     * Performs a scalar multiplication on this vector: this * mul
     * 
     * @param mul multiplicator
     * @return scalar multiplied vector
     */
    public Vector3f scalar(float mul) {
        float rx = this.x * mul;
        float ry = this.y * mul;
        float rz = this.z * mul;

        return new Vector3f(rx, ry, rz);
    }

    /**
     * Performs a scalar multiplication on this vector: this * that
     * 
     * @param that multiplicator vector
     * @return scalar multiplied vector
     */
    public Vector3f scalar(Vector3f that) {
        float rx = this.x * that.x;
        float ry = this.y * that.y;
        float rz = this.z * that.z;

        return new Vector3f(rx, ry, rz);
    }

    /**
     * Rotates the vector.
     * 
     * @param angles angles for each component in degrees
     * @return rotated vector
     */
    public Vector3f rotate(Vector3f angles) {
        if (angles.x == 0 && angles.y == 0 && angles.z == 0) {
            // nothing to do here
            return this;
        }

        double rx = x;
        double ry = y;
        double rz = z;

        // rotate x (pitch)
        if (angles.x != 0) {
            Point2d p = new Point2d(ry, rz).rotate(angles.x);
            ry = p.x;
            rz = p.y;
        }

        // rotate y (yaw)
        if (angles.y != 0) {
            Point2d p = new Point2d(rx, rz).rotate(angles.y);
            rx = p.x;
            rz = p.y;
        }

        // rotate z (roll)
        if (angles.z != 0) {
            Point2d p = new Point2d(rx, ry).rotate(angles.z);
            rx = p.x;
            ry = p.y;
        }

        return new Vector3f((float)rx, (float)ry, (float)rz);
    }

    /**
     * Returns the minima between this vector and another vector.
     * 
     * @param that other vector to compare
     * @return minima of this and that vector
     */
    public Vector3f min(Vector3f that) {
        float rx = Math.min(this.x, that.x);
        float ry = Math.min(this.y, that.y);
        float rz = Math.min(this.z, that.z);

        return new Vector3f(rx, ry, rz);
    }

    /**
     * Returns the maxima between this vector and another vector.
     * 
     * @param that other vector to compare
     * @return maxima of this and that vector
     */
    public Vector3f max(Vector3f that) {
        float rx = Math.max(this.x, that.x);
        float ry = Math.max(this.y, that.y);
        float rz = Math.max(this.z, that.z);

        return new Vector3f(rx, ry, rz);
    }

    public Vector2f getAsPointOnPlane(Vector3f origin, Vector3f axis1, Vector3f axis2)
    {
        return new Vector2f(axis1.dot(this.sub(origin)), axis2.dot(this.sub(origin)));
    }

    /**
     * Private helper class for rotation
     */
    private class Point2d {

        private final double x;
        private final double y;

        private Point2d(double x, double y) {
            this.x = x;
            this.y = y;
        }

        private Point2d rotate(double angle) {
            // normalize angle
            angle %= 360;

            // special cases
            if (angle == 0) {
                return this;
            }
            if (angle == 90) {
                return new Point2d(-y, x);
            }
            if (angle == 180) {
                return new Point2d(-x, -y);
            }
            if (angle == 270) {
                return new Point2d(y, -x);
            }

            // convert degrees to radians
            angle = Math.toRadians(angle);

            double r = Math.hypot(x, y);
            double theta = Math.atan2(y, x);

            double rx = r * Math.cos(theta + angle);
            double ry = r * Math.sin(theta + angle);

            return new Point2d(rx, ry);
        }
    }
}
