package com.srbenoit.math.linear;

/**
 * A tuple characterized by N coordinates.
 */
public class TupleN implements Cloneable {

    /** true if tuple transforms as a point; false to transform as a vector */
    private boolean asPoint;

    /** the coordinate data */
    private double[] data;

    /** the length, computed lazily */
    private transient double len;

    /**
     * Constructs a new <code>TupleN</code> with all three coordinates zero.
     *
     * @param  numCoords  the number of coordinates in the tuple
     * @param  isPoint    <code>true</code> if tuple transforms as a point; <code>false</code> to
     *                    transform as a vector
     */
    public TupleN(final int numCoords, final boolean isPoint) {

        if (numCoords < 1) {
            throw new IllegalArgumentException("Invalid number of coordinates for tuple: "
                + numCoords);
        }

        this.data = new double[numCoords];

        this.asPoint = isPoint;
        this.len = 0f;
    }

    /**
     * Constructs and initializes a <code>TupleN</code> from the specified coordinates.
     *
     * @param  numCoords  the number of coordinates in the tuple
     * @param  isPoint    <code>true</code> if tuple transforms as a point; <code>false</code> to
     *                    transform as a vector
     * @param  values     the initial values for the tuple; if there are fewer values than <code>
     *                    numCoords</code>, then the remaining coordinates after these values have
     *                    been installed are initialized to zero; if there are more values than
     *                    <code>numCoords</code>, the extra values are ignored
     */
    public TupleN(final int numCoords, final boolean isPoint, final double... values) {

        this(numCoords, isPoint);

        int count;

        count = (numCoords < values.length) ? numCoords : values.length;
        System.arraycopy(values, 0, this.data, 0, count);

        this.len = -1;
    }

    /**
     * Constructs and initializes a <code>TupleN</code> from the specified <code>TupleN</code>.
     *
     * @param  tuple  the <code>TupleN</code> containing the initialization data
     */
    public TupleN(final TupleN tuple) {

        this(tuple.getDimension(), tuple.isAsPoint());

        this.data = tuple.get();
        this.len = tuple.getCurLength();
    }

    /**
     * Gets the number of coordinates in the tuple.
     *
     * @return  the number of coordinates
     */
    public int getDimension() {

        return this.data.length;
    }

    /**
     * Checks that the dimension of this tuple matches that of another tuple, and returns that
     * dimension if so.
     *
     * @param   other  the <code>TupleN</code>to check
     * @return  the dimension of the tuples
     * @throws  IllegalArgumentException  if dimensions fail to match
     */
    public int dimCheck(final TupleN other) {

        if (getDimension() != other.getDimension()) {
            throw new IllegalArgumentException("Dimension mismatch");
        }

        return getDimension();
    }

    /**
     * Test whether the tuple transforms as a point or as a vector.
     *
     * @return  <code>true</code> if tuple transforms as a point; <code>false</code> to transform
     *          as a vector
     */
    public boolean isAsPoint() {

        return this.asPoint;
    }

    /**
     * Sets the flag that determines whether the tuple transforms as a point or as a vector.
     *
     * @param  isPoint  <code>true</code> if tuple transforms as a point; <code>false</code> to
     *                  transform as a vector
     */
    public void setAsPoint(final boolean isPoint) {

        this.asPoint = isPoint;
    }

    /**
     * Gets a coordinate value.
     *
     * @param   index  the index of the coordinate value to retrieve
     * @return  the coordinate
     */
    public double get(final int index) {

        return this.data[index];
    }

    /**
     * Gets all coordinate values.
     *
     * @return  the coordinates
     */
    public double[] get() {

        return this.data.clone();
    }

    /**
     * Sets a coordinate value.
     *
     * @param  index  the index of the coordinate value to set
     * @param  coord  the new coordinate value
     */
    public void set(final int index, final double coord) {

        this.data[index] = coord;
    }

    /**
     * Sets the coordinate values. This can change the dimension of the tuple.
     *
     * @param  coords  the new coordinate values
     */
    public void set(final double[] coords) {

        this.data = coords.clone();
        this.len = -1;
    }

    /**
     * Returns a string that contains the values of this <code>TupleN</code>. The form is (p1, p2,
     * ..., pN) for points, [p1, p2, ..., pN] for vectors.
     *
     * @return  the <code>String</code> representation
     */
    @Override public String toString() {

        StringBuilder str;

        str = new StringBuilder(20);
        str.append(this.asPoint ? '(' : '[');

        for (int i = 0; i < getDimension(); i++) {

            if (i > 0) {
                str.append(", ");
            }

            str.append(this.data[i]);
        }

        str.append(this.asPoint ? ')' : ']');

        return str.toString();
    }

    /**
     * Makes a copy of the tuple.
     *
     * @return  the copy
     */
    public TupleN copy() {

        TupleN obj;

        try {
            obj = (TupleN) clone();
            obj.set(this.data);
        } catch (CloneNotSupportedException e) {
            obj = new TupleN(this);
        }

        return obj;
    }

    /**
     * Generates a hash code for the object.
     *
     * @return  the hash code
     */
    @Override public int hashCode() {

        int hash = 0;

        for (int i = 0; i < getDimension(); i++) {
            hash += (int) this.data[i];
        }

        return hash;
    }

    /**
     * Tests whether this object is equal to another object. To be equal, the other object must
     * also be a <code>TupleN</code> and must have the same number of coordinates, the same
     * coordinate values, and the same <code>isPoint</code> value.
     *
     * <p>The lazily computed length value is not used in the comparison.
     *
     * @param   obj  the object to test for equality
     * @return  <code>true</code> of the objects are equal; <code>false</code> if not
     */
    @Override public boolean equals(final Object obj) {

        TupleN other;
        boolean equal;

        if (obj instanceof TupleN) {
            other = (TupleN) obj;

            if ((other.isAsPoint() == isAsPoint()) && (other.getDimension() == getDimension())) {
                equal = true;

                for (int i = 0; i < getDimension(); i++) {

                    if (other.get(i) != get(i)) {
                        equal = false;

                        break;
                    }
                }
            } else {
                equal = false;
            }
        } else {
            equal = false;
        }

        return equal;
    }

    /**
     * Adds a scaled version of a tuple to this tuple (this = this + scale * tuple).
     *
     * @param  scale  the scalar value
     * @param  tuple  the tuple to be scaled then added
     */
    public void addScaled(final double scale, final TupleN tuple) {

        if (getDimension() != tuple.getDimension()) {
            throw new IllegalArgumentException(
                "addScaled: Tuples being added must be same dimension");
        }

        for (int i = 0; i < getDimension(); i++) {
            set(i, get(i) + (scale * tuple.get(i)));
        }

        this.len = -1.0;

        // Vector + vector = vector
        // Vector + point = point
        // Point + vector = point
        // Point + point = point
        this.asPoint |= tuple.isAsPoint();
    }

    /**
     * Adds a tuple offset to the tuple.
     *
     * @param  tuple  the tuple offset
     */
    public void add(final TupleN tuple) {

        if (getDimension() != tuple.getDimension()) {
            throw new IllegalArgumentException("add: Tuples being added must be same dimension");
        }

        for (int i = 0; i < getDimension(); i++) {
            set(i, get(i) + tuple.get(i));
        }

        this.len = -1.0;

        // Vector + vector = vector
        // Vector + point = point
        // Point + vector = point
        // Point + point = point
        this.asPoint |= tuple.isAsPoint();
    }

    /**
     * Sets the value of this <code>TupleN</code> to the sum of <code>tuple1</code> and <code>
     * tuple2</code> (this = tuple1 + tuple2).
     *
     * @param  tuple1  the first tuple
     * @param  tuple2  the second tuple
     */
    public void add(final TupleN tuple1, final TupleN tuple2) {

        if (tuple1.getDimension() != tuple2.getDimension()) {
            throw new IllegalArgumentException("add: Tuples being added must be same dimension");
        }

        if (this.getDimension() != tuple1.getDimension()) {
            throw new IllegalArgumentException(
                "add: Tuples being added must be same dimension as destination tuple");
        }

        for (int i = 0; i < tuple1.getDimension(); i++) {
            set(i, tuple1.get(i) + tuple2.get(i));
        }

        this.len = -1.0;

        // Vector + vector = vector
        // Vector + point = point
        // Point + vector = point
        // Point + point = point
        this.asPoint = tuple1.isAsPoint() | tuple2.isAsPoint();
    }

    /**
     * Subtracts a tuple offset from the tuple.
     *
     * @param  tuple  the tuple offset
     */
    public void sub(final TupleN tuple) {

        if (getDimension() != tuple.getDimension()) {
            throw new IllegalArgumentException(
                "sub: Tuples being subtracted must be same dimension");
        }

        for (int i = 0; i < getDimension(); i++) {
            set(i, get(i) - tuple.get(i));
        }

        this.len = -1.0;

        // Vector - vector = vector
        // Vector - point = point
        // Point - vector = point
        // Point - point = vector
        this.asPoint = isAsPoint() != tuple.isAsPoint();
    }

    /**
     * Sets the value of this <code>TupleN</code> to the difference of <code>tuple1</code> and
     * <code>tuple2</code> (this = tuple1 - tuple2).
     *
     * @param  tuple1  the first tuple
     * @param  tuple2  the second tuple
     */
    public void sub(final TupleN tuple1, final TupleN tuple2) {

        if (tuple1.getDimension() != tuple2.getDimension()) {
            throw new IllegalArgumentException(
                "sub: Tuples being subtracted must be same dimension");
        }

        if (getDimension() != tuple1.getDimension()) {
            throw new IllegalArgumentException(
                "sub: Tuples being subtracted must be same dimension as destination tuple");
        }

        for (int i = 0; i < tuple1.getDimension(); i++) {
            set(i, tuple1.get(i) - tuple2.get(i));
        }

        this.len = -1.0;

        // Vector - vector = vector
        // Vector - point = point
        // Point - vector = point
        // Point - point = vector
        this.asPoint = tuple1.isAsPoint() != tuple2.isAsPoint();
    }

    /**
     * Computes the square of the Euclidean distance between this tuple and <code>tuple</code>.
     *
     * @param   tuple  the other tuple
     * @return  the square of the distance
     */
    public double distSquared(final TupleN tuple) {

        if (getDimension() != tuple.getDimension()) {
            throw new IllegalArgumentException(
                "distSquared: Tuples being compared must be same dimension");
        }

        double delta;
        double sum;

        sum = 0;

        for (int i = 0; i < getDimension(); i++) {
            delta = get(i) - tuple.get(i);
            sum += delta * delta;
        }

        return sum;
    }

    /**
     * Computes the Euclidean distance between this tuple and <code>tuple</code>.
     *
     * @param   tuple  the other tuple
     * @return  the distance
     */
    public double dist(final TupleN tuple) {

        if (getDimension() != tuple.getDimension()) {
            throw new IllegalArgumentException(
                "dist: Tuples being compared must be same dimension");
        }

        double delta;
        double sum;

        sum = 0;

        for (int i = 0; i < getDimension(); i++) {
            delta = get(i) - tuple.get(i);
            sum += delta * delta;
        }

        return Math.sqrt(sum);
    }

    /**
     * Negates this <code>TupleN</code> in place.
     */
    public void negate() {

        for (int i = 0; i < getDimension(); i++) {
            set(i, -get(i));
        }
    }

    /**
     * Sets this <code>TupleN</code> to the scalar multiplication of the scale factor with this.
     *
     * @param  scale  the scalar value
     */
    public void scale(final double scale) {

        for (int i = 0; i < getDimension(); i++) {
            this.data[i] *= scale;
        }

        if (this.len > 0) {

            if (scale > 0) {
                this.len *= scale;
            } else {
                this.len *= -scale;
            }
        }
    }

    /**
     * Sets this <code>TupleN</code> to the scalar multiplication of <code>tuple</code>.
     *
     * @param  scale  the scalar value
     * @param  tuple  the source tuple
     */
    public void scale(final double scale, final TupleN tuple) {

        if (getDimension() != tuple.getDimension()) {
            throw new IllegalArgumentException(
                "scale: Tuple being scaled must be same dimension as destination tuple");
        }

        for (int i = 0; i < getDimension(); i++) {
            set(i, scale * tuple.get(i));
        }

        if (tuple.getCurLength() > 0) {

            if (scale > 0) {
                this.len = tuple.getCurLength() * scale;
            } else {
                this.len = -tuple.getCurLength() * scale;
            }
        } else {
            this.len = -1.0;
        }

        this.asPoint = tuple.isAsPoint();
    }

    /**
     * Sets this <code>TupleN</code> to the scalar multiplication of itself and then adds <code>
     * tuple</code> (this = scale * this + tuple).
     *
     * @param  scale  the scalar value
     * @param  tuple  the tuple to be added
     */
    public void scaleAdd(final double scale, final TupleN tuple) {

        if (getDimension() != tuple.getDimension()) {
            throw new IllegalArgumentException(
                "scaleAdd: Tuples being added must be same dimension");
        }

        for (int i = 0; i < getDimension(); i++) {
            set(i, (scale * get(i)) + tuple.get(i));
        }

        this.len = -1.0;

        // Vector + vector = vector
        // Vector + point = point
        // Point + vector = point
        // Point + point = point
        this.asPoint |= tuple.isAsPoint();
    }

    /**
     * Sets this <code>TupleN</code> to the scalar multiplication of <code>tuple1</code> and then
     * adds <code>tuple2</code> (this = scale * tuple1 + tuple2).
     *
     * @param  scale   the scalar value
     * @param  tuple1  the tuple to be scaled and added
     * @param  tuple2  the tuple to be added without a scale
     */
    public void scaleAdd(final double scale, final TupleN tuple1, final TupleN tuple2) {

        if (tuple1.getDimension() != tuple2.getDimension()) {
            throw new IllegalArgumentException(
                "scaleAdd: Tuples being added must be same dimension");
        }

        if (this.getDimension() != tuple1.getDimension()) {
            throw new IllegalArgumentException(
                "scaleAdd: Tuples being added must be same dimension as destination tuple");
        }

        for (int i = 0; i < getDimension(); i++) {
            set(i, (scale * tuple1.get(i)) + tuple2.get(i));
        }

        this.len = -1.0;

        // Vector + vector = vector
        // Vector + point = point
        // Point + vector = point
        // Point + point = point
        this.asPoint = tuple1.isAsPoint() | tuple2.isAsPoint();
    }

    /**
     * Returns the squared length of the tuple.
     *
     * @return  the squared length of the tuple
     */
    public double lengthSquared() {

        double sum;

        if (this.len < 0) {
            sum = 0;

            for (int i = 0; i < getDimension(); i++) {
                sum += this.data[i] * this.data[i];
            }
        } else {
            sum = this.len * this.len;
        }

        return sum;
    }

    /**
     * Returns the length of the tuple.
     *
     * @return  the length of the tuple
     */
    public double length() {

        double sum;

        if (this.len < 0) {
            sum = 0;

            for (int i = 0; i < getDimension(); i++) {
                sum += this.data[i] * this.data[i];
            }

            this.len = Math.sqrt(sum);
        }

        return this.len;
    }

    /**
     * Returns the current length of the tuple.
     *
     * @return  the current length, or -1 if the length has not yet been computed
     */
    public double getCurLength() {

        return this.len;
    }

    /**
     * Computes the dot product of this <code>TupleN</code> and <code>tuple</code>.
     *
     * @param   tuple  the other tuple
     * @return  the dot product
     */
    public double dot(final TupleN tuple) {

        double prod;

        if (getDimension() != tuple.getDimension()) {
            throw new IllegalArgumentException(
                "dot: Tuples in dot product must be same dimension");
        }

        prod = 0;

        for (int i = 0; i < getDimension(); i++) {
            prod += get(i) * tuple.get(i);
        }

        return prod;
    }

    /**
     * Computes the angle between this tuple and another tuple (where both are considered as
     * vectors).
     *
     * @param   tuple  The other tuple.
     * @return  the angle between this tuple and <code>tuple</code>, in radians
     */
    public double angle(final TupleN tuple) {

        return Math.acos(this.dot(tuple) / (this.length() * tuple.length()));
    }

    /**
     * Normalizes this <code>TupleN</code> in place.
     */
    public void normalize() {

        double length;
        double recip;

        length = length();

        if (length == 0) {
            this.data[0] = 1;

            for (int i = 1; i < getDimension(); i++) {
                this.data[i] = 0;
            }
        } else if (length != 1.0) {
            recip = 1.0 / length;

            for (int i = 1; i < getDimension(); i++) {
                this.data[i] *= recip;
            }
        }

        this.len = 1.0;
    }

    /**
     * Sets this <code>TupleN</code> to the normalization of another tuple.
     *
     * @param  tuple  the tuple to normalize
     */
    public void normalize(final TupleN tuple) {

        if (getDimension() != tuple.getDimension()) {
            throw new IllegalArgumentException(
                "Tuple being normalized must be same dimension as destination tuple");
        }

        double length;
        double recip;

        length = tuple.length();

        if (length == 0) {
            this.data[0] = 1;

            for (int i = 1; i < getDimension(); i++) {
                this.data[i] = 0;
            }
        } else if (length != 1.0) {
            recip = 1.0 / length;

            for (int i = 1; i < getDimension(); i++) {
                set(i, tuple.get(i) * recip);
            }
        }

        this.len = 1.0;
    }
}
