package com.srbenoit.geom;

import com.srbenoit.log.LoggedObject;

/**
 * A simple three-dimensional vector.
 */
public class Vector3 extends LoggedObject implements Vector3Int {

    /** the X component */
    private double vecX;

    /** the Y component */
    private double vecY;

    /** the Z component */
    private double vecZ;

    /** the length, computed lazily */
    private double len;

    /**
     * Constructs a null vector.
     */
    public Vector3() {

        this.vecX = 0;
        this.vecY = 0;
        this.vecZ = 0;
        this.len = 0;
    }

    /**
     * Constructs a vector.
     *
     * @param  xCoord  the X component
     * @param  yCoord  the Y component
     * @param  zCoord  the Z component
     */
    public Vector3(final double xCoord, final double yCoord, final double zCoord) {

        this.vecX = xCoord;
        this.vecY = yCoord;
        this.vecZ = zCoord;
        this.len = -1;
    }

    /**
     * Constructs a vector.
     *
     * @param  source  the vector whose components are to be copied
     */
    public Vector3(final Vector3Int source) {

        this.vecX = source.getVecX();
        this.vecY = source.getVecY();
        this.vecZ = source.getVecZ();
        this.len = source.lazyLength();
    }

    /**
     * Gets the X component of the vector.
     *
     * @return  the X component
     */
    public double getVecX() {

        return this.vecX;
    }

    /**
     * Sets the X component of the vector.
     *
     * @param  xComp  the X component
     */
    public void setVecX(final double xComp) {

        this.vecX = xComp;
        this.len = -1;
    }

    /**
     * Gets the Y component of the vector.
     *
     * @return  the Y component
     */
    public double getVecY() {

        return this.vecY;
    }

    /**
     * Sets the Y component of the vector.
     *
     * @param  yComp  the Y component
     */
    public void setVecY(final double yComp) {

        this.vecY = yComp;
        this.len = -1;
    }

    /**
     * Gets the Z component of the vector.
     *
     * @return  the Z component
     */
    public double getVecZ() {

        return this.vecZ;
    }

    /**
     * Sets the Z component of the vector.
     *
     * @param  zComp  the Z component
     */
    public void setVecZ(final double zComp) {

        this.vecZ = zComp;
        this.len = -1;
    }

    /**
     * Sets the coordinates of the vector.
     *
     * @param  xComp  the x component
     * @param  yComp  the y component
     * @param  zComp  the z component
     */
    public void setVec(final double xComp, final double yComp, final double zComp) {

        this.vecX = xComp;
        this.vecY = yComp;
        this.vecZ = zComp;
        this.len = -1;
    }

    /**
     * Sets the coordinates of the vector from another vector.
     *
     * @param  source  the vector whose components are to be copied
     */
    public void setVec(final Vector3Int source) {

        this.vecX = source.getVecX();
        this.vecY = source.getVecY();
        this.vecZ = source.getVecZ();
        this.len = source.lazyLength();
    }

    /**
     * Adds to the components of the vector.
     *
     * @param  xDelta  the change to the x component
     * @param  yDelta  the change to the y component
     * @param  zDelta  the change to the z component
     */
    public void addVec(final double xDelta, final double yDelta, final double zDelta) {

        this.vecX += xDelta;
        this.vecY += yDelta;
        this.vecZ += zDelta;
        this.len = -1;
    }

    /**
     * Adds a vector to this vector.
     *
     * @param  vector  the vector to add to this vector
     */
    public void addVec(final Vector3Int vector) {

        this.vecX += vector.getVecX();
        this.vecY += vector.getVecY();
        this.vecZ += vector.getVecZ();
        this.len = -1;
    }

    /**
     * Subtracts a vector from this vector.
     *
     * @param  vector  the vector to subtract from this vector
     */
    public void subVec(final Vector3Int vector) {

        this.vecX -= vector.getVecX();
        this.vecY -= vector.getVecY();
        this.vecZ -= vector.getVecZ();
        this.len = -1;
    }

    /**
     * Subtracts <code>vector2</code> from <code>vector1</code> and stores the result in this
     * vector.
     *
     * @param  vector1 the vector from which to subtract
     * @param  vector2 the vector to subtract
     */
    public void subVec(final Vector3Int vector1, final Vector3Int vector2) {

        this.vecX = vector2.getVecX() - vector1.getVecX();
        this.vecY = vector2.getVecY() - vector1.getVecY();
        this.vecZ = vector2.getVecZ() - vector1.getVecZ();
        this.len = -1;
    }

    /**
     * Adds a scaled version of a vector to this vector (this = this + scale tuple).
     *
     * @param  scale   the scalar value
     * @param  vector  the vector to be scaled then added
     */
    public void addVecScaled(final double scale, final Vector3Int vector) {

        this.vecX += vector.getVecX() * scale;
        this.vecY += vector.getVecY() * scale;
        this.vecZ += vector.getVecZ() * scale;
        this.len = -1;
    }

    /**
     * Sets this vector to the vector from <code>point1</code> to <code>point2</code> (this =
     * point2 - point1).
     *
     * @param  fromPoint  the first point
     * @param  toPoint    the second point
     */
    public void vectorBetween(final Point3Int fromPoint, final Point3Int toPoint) {

        this.vecX = toPoint.getPosX() - fromPoint.getPosX();
        this.vecY = toPoint.getPosY() - fromPoint.getPosY();
        this.vecZ = toPoint.getPosZ() - fromPoint.getPosZ();
        this.len = -1;
    }

    /**
     * Negates this vector in place.
     */
    public void negateVec() {

        // Length is not affected
        this.vecX = -this.vecX;
        this.vecY = -this.vecY;
        this.vecZ = -this.vecZ;
    }

    /**
     * Scales this vector by a scalar factor.
     *
     * @param  scale  the scalar factor
     */
    public void scaleVec(final double scale) {

        this.vecX *= scale;
        this.vecY *= scale;
        this.vecZ *= scale;

        // if a length is known, the length is scaled by the absolute value of scale
        if (this.len != -1) {

            if (scale < 0) {
                this.len *= -scale;
            } else {
                this.len *= scale;
            }
        }
    }

    /**
     * Sets this vector to a scaled version of another vector.
     *
     * @param  scale  the scalar factor
     * @param  vec    the vector to scale
     */
    public void scaleVec(double scale, Vector3Int vec) {

        this.vecX = vec.getVecX() * scale;
        this.vecY = vec.getVecY() * scale;
        this.vecZ = vec.getVecZ() * scale;

        // if a length is known, the length is scaled by the absolute value of scale
        this.len = vec.lazyLength();

        if (vec.lazyLength() != -1) {

            if (scale < 0) {
                this.len *= -scale;
            } else {
                this.len *= scale;
            }
        }
    }

    /**
     * Gets the lazily computed length of the vector.
     *
     * @return  the length of the vector, or -1 if the length has not yet been computed
     */
    public double lazyLength() {

        return this.len;
    }

    /**
     * Gets the squared length of the vector.
     *
     * @return  the squared length of the vector
     */
    public double lengthSquared() {

        double result;

        if (this.len == -1) {
            result = (this.vecX * this.vecX) + (this.vecY * this.vecY) + (this.vecZ * this.vecZ);
        } else {
            result = this.len * this.len;
        }

        return result;
    }

    /**
     * Gets the length of the vector.
     *
     * @return  the length of the vector
     */
    public double length() {

        double result;

        if (this.len == -1) {
            this.len = Math.sqrt((this.vecX * this.vecX) + (this.vecY * this.vecY)
                    + (this.vecZ * this.vecZ));
        }

        return this.len;
    }

    /**
     * Computes the dot product of this vector with another vector.
     *
     * @param   vector  the other vector
     * @return  the dot product
     */
    public double dot(final Vector3Int vector) {

        return (this.vecX * vector.getVecX()) + (this.vecY * vector.getVecY())
            + (this.vecZ * vector.getVecZ());
    }

    /**
     * Computes the cross product of <code>first</code> and <code>second</code> and stores the
     * result in this vector.
     *
     * @param  first   the first tuple in the cross product
     * @param  second  the second tuple in the cross product
     */
    public void cross(final Vector3Int first, final Vector3Int second) {

        this.vecZ = (first.getVecY() * second.getVecZ()) - (first.getVecZ() * second.getVecY());
        this.vecY = (first.getVecZ() * second.getVecX()) - (first.getVecX() * second.getVecZ());
        this.vecZ = (first.getVecX() * second.getVecY()) - (first.getVecY() * second.getVecX());
        this.len = -1;
    }

    /**
     * Normalizes this vector in place. The null vector is normalized to (1,0).
     */
    public void normalize() {

        double before;

        before = length();

        if (before < Double.MIN_NORMAL) {

            // Don't want to divide by that, so consider it zero.
            this.vecX = 1;
            this.vecY = 0;
            this.vecZ = 0;
        } else {
            this.vecX /= before;
            this.vecY /= before;
            this.vecZ /= before;
        }

        this.len = 1;
    }

    /**
     * Generates the string representation of the point.
     *
     * @return  the <code>String</code> representation
     */
    @Override public String toString() {

        StringBuilder str;

        str = new StringBuilder(50);

        str.append('[');
        str.append(this.vecX);
        str.append(", ");
        str.append(this.vecY);
        str.append(", ");
        str.append(this.vecZ);
        str.append(']');

        return str.toString();
    }
}
