package com.srbenoit.geom;

import com.srbenoit.log.LoggedObject;

/**
 * A simple three-dimensional based vector, combining a base point with a vector.
 */
public class BasedVector3 extends LoggedObject implements Point3Int, Vector3Int {

    /** the X coordinate of the base point */
    private double posX;

    /** the Y coordinate of the base point */
    private double posY;

    /** the Z coordinate of the base point */
    private double posZ;

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

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

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

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

    /**
     * Constructs a <code>BasedVector3</code> with base point at the origin and null vector.
     */
    public BasedVector3() {

        this.posX = 0;
        this.posY = 0;
        this.posZ = 0;
        this.vecX = 0;
        this.vecY = 0;
        this.vecZ = 0;
        this.len = 0;
    }

    /**
     * Constructs a <code>BasedVector3</code> with specified base point coordinates and vector
     * components.
     *
     * @param  baseX  the X coordinate of the base point
     * @param  baseY  the Y coordinate of the base point
     * @param  baseZ  the Z coordinate of the base point
     * @param  vecX   the X component of the vector
     * @param  vecY   the Y component of the vector
     * @param  vecZ   the Z component of the vector
     */
    public BasedVector3(final double baseX, final double baseY, final double baseZ,
        final double vecX, final double vecY, final double vecZ) {

        this.posX = baseX;
        this.posY = baseY;
        this.posZ = baseZ;
        this.vecX = vecX;
        this.vecY = vecY;
        this.vecZ = vecZ;
        this.len = -1;
    }

    /**
     * Constructs a <code>BasedVector3</code> with specified base point and vector.
     *
     * @param  base  the point whose coordinates are to be copied into the base point
     * @param  vec   the vector whose components are to be copied into the vector
     */
    public BasedVector3(final Point3Int base, final Vector3Int vec) {

        this.posX = base.getPosX();
        this.posY = base.getPosY();
        this.posZ = base.getPosZ();
        this.vecX = vec.getVecX();
        this.vecY = vec.getVecY();
        this.vecZ = vec.getVecZ();
        this.len = vec.lazyLength();
    }

    /**
     * Gets the X coordinate of the base point.
     *
     * @return  the X coordinate
     */
    public double getPosX() {

        return this.posX;
    }

    /**
     * Sets the X coordinate of the base point.
     *
     * @param  xCoord  the X coordinate
     */
    public void setPosX(final double xCoord) {

        this.posX = xCoord;
    }

    /**
     * Gets the Y coordinate of the base point.
     *
     * @return  the Y coordinate
     */
    public double getPosY() {

        return this.posY;
    }

    /**
     * Sets the Y coordinate of the base point.
     *
     * @param  yCoord  the Y coordinate
     */
    public void setPosY(final double yCoord) {

        this.posY = yCoord;
    }

    /**
     * Gets the Z coordinate of the base point.
     *
     * @return  the Z coordinate
     */
    public double getPosZ() {

        return this.posZ;
    }

    /**
     * Sets the Z coordinate of the base point.
     *
     * @param  zCoord  the Z coordinate
     */
    public void setPosZ(final double zCoord) {

        this.posZ = zCoord;
    }

    /**
     * Sets the coordinates of the base point.
     *
     * @param  xCoord  the x coordinate
     * @param  yCoord  the y coordinate
     * @param  zCoord  the z coordinate
     */
    public void setPos(final double xCoord, final double yCoord, final double zCoord) {

        this.posX = xCoord;
        this.posY = yCoord;
        this.posZ = zCoord;
    }

    /**
     * Sets the coordinates of the base point from another point.
     *
     * @param  source  the point whose position is to be copied
     */
    public void setPos(final Point3Int source) {

        this.posX = source.getPosX();
        this.posY = source.getPosY();
        this.posZ = source.getPosZ();
    }

    /**
     * Moves the coordinates of the base point.
     *
     * @param  xDelta  the change to the x coordinate
     * @param  yDelta  the change to the y coordinate
     * @param  zDelta  the change to the z coordinate
     */
    public void move(final double xDelta, final double yDelta, final double zDelta) {

        this.posX += xDelta;
        this.posY += yDelta;
        this.posZ += yDelta;
    }

    /**
     * Moves the coordinates of the base point by a vector.
     *
     * @param  vector  the vector by which to move the point
     */
    public void move(final Vector3Int vector) {

        this.posX += vector.getVecX();
        this.posY += vector.getVecY();
        this.posZ += vector.getVecZ();
    }

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

        this.posX += vector.getVecX() * scale;
        this.posY += vector.getVecY() * scale;
        this.posZ += vector.getVecZ() * scale;
    }

    /**
     * Computes the square of the Euclidean distance between the base point and another point.
     *
     * @param   otherPoint  the other point
     * @return  the square of the distance
     */
    public double distSquared(final Point3Int otherPoint) {

        double distX;
        double distY;
        double distZ;

        distX = this.posX - otherPoint.getPosX();
        distY = this.posY - otherPoint.getPosY();
        distZ = this.posZ - otherPoint.getPosZ();

        return (distX * distX) + (distY * distY) + (distZ * distZ);
    }

    /**
     * Computes the Euclidean distance between the base point and another point.
     *
     * @param   otherPoint  the other point
     * @return  the distance
     */
    public double dist(final Point3Int otherPoint) {

        double distX;
        double distY;
        double distZ;

        distX = this.posX - otherPoint.getPosX();
        distY = this.posY - otherPoint.getPosY();
        distZ = this.posZ - otherPoint.getPosZ();

        return Math.sqrt((distX * distX) + (distY * distY) + (distZ * distZ));
    }

    /**
     * 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  point1  the first point
     * @param  point2  the second point
     */
    public void vectorBetween(final Point3Int point1, final Point3Int point2) {

        this.vecX = point2.getPosX() - point1.getPosX();
        this.vecY = point2.getPosY() - point1.getPosY();
        this.vecZ = point2.getPosZ() - point1.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() {

        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(140);

        str.append("Base: (");
        str.append(this.posX);
        str.append(", ");
        str.append(this.posY);
        str.append(", ");
        str.append(this.posZ);
        str.append(") Vector: [");
        str.append(this.vecX);
        str.append(", ");
        str.append(this.vecY);
        str.append(", ");
        str.append(this.vecZ);
        str.append(']');

        return str.toString();
    }
}
