package com.srbenoit.geom;

import com.srbenoit.log.LoggedObject;

/**
 * A simple two-dimensional based vector, combining a base point with a vector.
 */
public class BasedVector2 extends LoggedObject implements Point2Int, Vector2Int {

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

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

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

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

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

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

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

    /**
     * Constructs a <code>BasedVector2</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  vecX   the X component of the vector
     * @param  vecY   the Y component of the vector
     */
    public BasedVector2(final double baseX, final double baseY, final double vecX,
        final double vecY) {

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

    /**
     * Constructs a <code>BasedVector2</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 BasedVector2(final Point2Int base, final Vector2Int vec) {

        this.posX = base.getPosX();
        this.posY = base.getPosY();
        this.vecX = vec.getVecX();
        this.vecY = vec.getVecY();
        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;
    }

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

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

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

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

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

        this.posX += xDelta;
        this.posY += 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 Vector2Int vector) {

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

    /**
     * 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 Vector2Int vector) {

        this.posX += vector.getVecX() * scale;
        this.posY += vector.getVecY() * 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 Point2Int otherPoint) {

        double distX;
        double distY;

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

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

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

        double distX;
        double distY;

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

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

    /**
     * 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;
    }

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

        this.vecX = xComp;
        this.vecY = yComp;
        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 Vector2Int source) {

        this.vecX = source.getVecX();
        this.vecY = source.getVecY();
        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
     */
    public void addVec(final double xDelta, final double yDelta) {

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

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

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

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

        this.vecX -= vector.getVecX();
        this.vecY -= vector.getVecY();
        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 Vector2Int vector1, final Vector2Int vector2) {

        this.vecX = vector2.getVecX() - vector1.getVecX();
        this.vecY = vector2.getVecY() - vector1.getVecY();
        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 Vector2Int vector) {

        this.vecX += vector.getVecX() * scale;
        this.vecY += vector.getVecY() * 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 Point2Int point1, final Point2Int point2) {

        this.vecX = point2.getPosX() - point1.getPosX();
        this.vecY = point2.getPosY() - point1.getPosY();
        this.len = -1;
    }

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

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

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

        this.vecX *= scale;
        this.vecY *= 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, Vector2Int vec) {

        this.vecX = vec.getVecX() * scale;
        this.vecY = vec.getVecY() * 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);
        } 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));
        }

        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 Vector2Int vector) {

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

    /**
     * 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;
        } else {
            this.vecX /= before;
            this.vecY /= 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(100);

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

        return str.toString();
    }
}
