/*-
 *  Copyright 2020 Beuth Hochschule für Technik Berlin, Hochschule für Technik Stuttgart
 * 
 *  This file is part of CityDoctor2.
 *
 *  CityDoctor2 is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Lesser General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  CityDoctor2 is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public License
 *  along with CityDoctor2.  If not, see <https://www.gnu.org/licenses/>.
 */
package de.hft.stuttgart.citydoctor2.math;

import java.io.Serializable;
import java.util.Arrays;

/**
 * A three dimensional vector
 * 
 * @author Matthias Betz
 *
 */
public class Vector3d implements Serializable {

	private static final long serialVersionUID = 3495650092142761365L;

	private static final Vector3d ORIGIN = new Vector3d();

	public static final int X = 0;
	public static final int Y = 1;
	public static final int Z = 2;

	private double[] coords;

	public Vector3d() {
		this(0d, 0d, 0d);
	}

	public Vector3d(double x, double y, double z) {
		coords = new double[3];
		coords[0] = x;
		coords[1] = y;
		coords[2] = z;
	}

	public Vector3d(double[] coords) {
		if (coords == null) {
			throw new IllegalArgumentException("Coordinates can not be null");
		}
		this.coords = coords;
	}

	public Vector3d(Vector3d vec) {
		coords = new double[3];
		coords[0] = vec.getX();
		coords[1] = vec.getY();
		coords[2] = vec.getZ();
	}

	public double getX() {
		return coords[0];
	}

	public double getY() {
		return coords[1];
	}

	public double getZ() {
		return coords[2];
	}

	public void setX(double x) {
		coords[0] = x;
	}

	public void setY(double y) {
		coords[1] = y;
	}

	public void setZ(double z) {
		coords[2] = z;
	}

	/**
	 * Getter for all coordinates. The changes made to this array are reflected in
	 * this vector.
	 * 
	 * @return the coordiantes array
	 */
	public double[] getCoordinates() {
		return coords;
	}

	public double getLength() {
		return getDistance(ORIGIN);
	}

	public double getSquaredLength() {
		double x = coords[0];
		double y = coords[1];
		double z = coords[2];
		return x * x + y * y + z * z;
	}

	public double getDistance(Vector3d other) {
		return Math.sqrt(getDistanceSquare(other));
	}

	public double getDistanceSquare(Vector3d other) {
		double x = coords[0] - other.getX();
		double y = coords[1] - other.getY();
		double z = coords[2] - other.getZ();
		return x * x + y * y + z * z;
	}

	/**
	 * adds another vector to this one. The result is
	 * <code><br>(a1 + b1)<br>(a2 + b2)<br>(a3 + b3)</code>
	 * 
	 * @param b the other vector
	 * @return the sum of both vectors as a new vector. Values in this instance
	 *         remain unchanged.
	 */
	public Vector3d plus(Vector3d b) {
		double x = coords[0] + b.getX();
		double y = coords[1] + b.getY();
		double z = coords[2] + b.getZ();
		return new Vector3d(x, y, z);
	}

	/**
	 * add a value to a coordinate. A new instance is created as result.
	 * 
	 * @param coordinateIndex one of <code>Vector3d.X, Vector3d.Y, Vector3d.Z</code>
	 * @param value           the added value
	 * @return a new vector instance is created
	 */
	public Vector3d plus(int coordinateIndex, double value) {
		if (coordinateIndex < 0 || coordinateIndex > 2) {
			throw new IllegalArgumentException("coordinateIndex has to be between 0 and 2");
		}
		Vector3d copy = new Vector3d(this);
		copy.coords[coordinateIndex] += value;
		return copy;
	}

	/**
	 * subtract a value to a coordinate. A new instance is created
	 * 
	 * @param coordinateIndex one of <code>Vector3d.X, Vector3d.Y, Vector3d.Z</code>
	 * @param value           the subtracted value
	 * @return the new vector instance
	 */
	public Vector3d minus(int coordinateIndex, double value) {
		if (coordinateIndex < 0 || coordinateIndex > 2) {
			throw new IllegalArgumentException("coordinateIndex has to be between 0 and 2");
		}
		Vector3d copy = new Vector3d(this);
		copy.coords[coordinateIndex] -= value;
		return copy;
	}

	/**
	 * subtract another vector from this one. The result is
	 * <code><br>(a1 - b1)<br>(a2 - b2)<br>(a3 - b3)</code>
	 * 
	 * @param b the other vector
	 * @return the result as a new vector. Values in this instance remain unchanged.
	 */
	public Vector3d minus(Vector3d b) {
		double x = coords[0] - b.getX();
		double y = coords[1] - b.getY();
		double z = coords[2] - b.getZ();
		return new Vector3d(x, y, z);
	}

	/**
	 * multiply this vector with a scalar.
	 * 
	 * @param scalar the scalar
	 * @return the result as a new vector. Values in this instance remain unchanged.
	 */
	public Vector3d mult(double scalar) {
		double x = coords[0] * scalar;
		double y = coords[1] * scalar;
		double z = coords[2] * scalar;
		return new Vector3d(x, y, z);
	}

	/**
	 * normalizes this vector. This method changes the coordinates of this instance.
	 */
	public UnitVector3d normalize() {
		return UnitVector3d.of(this);
	}

	/**
	 * calculates the cross product. The result is
	 * <code><br>(a2 * b3 - a3 * b2)<br>(a3 * b1 - a1 * b3)<br>(a1 * b2 - a2 * b1)</code>
	 * 
	 * @param b the other vector
	 * @return the result as a new vector. Values in this instance remain unchanged.
	 */
	public Vector3d cross(Vector3d b) {
		double x = coords[1] * b.getZ() - coords[2] * b.getY();
		double y = coords[2] * b.getX() - coords[0] * b.getZ();
		double z = coords[0] * b.getY() - coords[1] * b.getX();
		return new Vector3d(x, y, z);
	}

	/**
	 * Calculates the dot product. The result is
	 * <code><br>(a1 * b1) + (a2 * b2) + (a3 * b3)<br></code>
	 * 
	 * @param b the other vector
	 * @return the dot product
	 */
	public double dot(Vector3d b) {
		return coords[0] * b.getX() + coords[1] * b.getY() + coords[2] * b.getZ();
	}

	/**
	 * creates a new copy of this vector.
	 * 
	 * @return a copy of this vector
	 */
	public Vector3d copyVector() {
		return new Vector3d(coords[0], coords[1], coords[2]);
	}

	@Override
	public String toString() {
		final int maxLen = 5;
		StringBuilder builder = new StringBuilder();
		builder.append("Vector3d [coords=");
		builder.append(coords != null ? Arrays.toString(Arrays.copyOf(coords, Math.min(coords.length, maxLen))) : null);
		builder.append("]");
		return builder.toString();
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + Arrays.hashCode(coords);
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Vector3d other = (Vector3d) obj;
		return Arrays.equals(coords, other.coords);
	}

	/**
	 * Two vectors are equals, when the distance between both is less than epsilon
	 * 
	 * @param other   the other vector
	 * @param epsilon the distance where the vectors are considered the same
	 * @return true if they are within epsilon distance, false otherwise.
	 */
	public boolean equalsWithEpsilon(Vector3d other, double epsilon) {
		if (other == this) {
			return true;
		}
		Vector3d dif = this.minus(other);
		double sqLength = dif.getSquaredLength();
		return sqLength < (epsilon * epsilon);
	}

	public double getCoordinate(int axis) {
		return coords[axis];
	}

	/**
	 * Adds a scalar to this vector, a new instance is returned
	 * 
	 * @param scalar
	 */
	public Vector3d plus(double scalar) {
		Vector3d v = new Vector3d(this);
		v.coords[0] += scalar;
		v.coords[1] += scalar;
		v.coords[2] += scalar;
		return v;
	}

}
