/*
 * Copyright 2010-2015 Institut Pasteur.
 * 
 * This file is part of Icy.
 * 
 * Icy is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Icy 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 General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with Icy. If not, see <http://www.gnu.org/licenses/>.
 */
package icy.type.point;

import java.awt.Point;
import java.awt.geom.Point2D;
import java.util.List;

/**
 * Point3D class.<br>
 * Incomplete implementation (work in progress...)
 * 
 * @author Stephane
 */
public abstract class Point3D implements Cloneable
{
    /**
     * Returns distance between 2 Point2D using specified scale factor for x/y/z dimension.
     */
    public static double getDistance(Point3D pt1, Point3D pt2, double factorX, double factorY, double factorZ)
    {
        double px = (pt2.getX() - pt1.getX()) * factorX;
        double py = (pt2.getY() - pt1.getY()) * factorY;
        double pz = (pt2.getZ() - pt1.getZ()) * factorZ;
        return Math.sqrt(px * px + py * py + pz * pz);
    }

    /**
     * Returns total distance of the specified list of points.
     */
    public static double getTotalDistance(List<Point3D> points, double factorX, double factorY, double factorZ,
            boolean connectLastPoint)
    {
        final int size = points.size();
        double result = 0d;

        if (size > 1)
        {
            for (int i = 0; i < size - 1; i++)
                result += getDistance(points.get(i), points.get(i + 1), factorX, factorY, factorZ);

            // add last to first point distance
            if (connectLastPoint)
                result += getDistance(points.get(size - 1), points.get(0), factorX, factorY, factorZ);
        }

        return result;
    }

    /**
     * Cache the hash code to make computing hashes faster.
     */
    int hash = 0;

    /**
     * Returns the X coordinate of this <code>Point3D</code> in <code>double</code> precision.
     * 
     * @return the X coordinate of this <code>Point3D</code>.
     */
    public abstract double getX();

    /**
     * Returns the Y coordinate of this <code>Point3D</code> in <code>double</code> precision.
     * 
     * @return the Y coordinate of this <code>Point3D</code>.
     */
    public abstract double getY();

    /**
     * Returns the Z coordinate of this <code>Point3D</code> in <code>double</code> precision.
     * 
     * @return the Z coordinate of this <code>Point3D</code>.
     */
    public abstract double getZ();

    /**
     * Sets the X coordinate of this <code>Point3D</code> in <code>double</code> precision.
     */
    public abstract void setX(double x);

    /**
     * Sets the Y coordinate of this <code>Point3D</code> in <code>double</code> precision.
     */
    public abstract void setY(double y);

    /**
     * Sets the Z coordinate of this <code>Point3D</code> in <code>double</code> precision.
     */
    public abstract void setZ(double z);

    /**
     * Sets the location of this <code>Point3D</code> to the
     * specified <code>double</code> coordinates.
     * 
     * @param x
     *        the new X coordinate of this {@code Point3D}
     * @param y
     *        the new Y coordinate of this {@code Point3D}
     * @param z
     *        the new Z coordinate of this {@code Point3D}
     */
    public void setLocation(double x, double y, double z)
    {
        setX(x);
        setY(y);
        setZ(z);
    }

    /**
     * Sets the location of this <code>Point3D</code> to the same
     * coordinates as the specified <code>Point3D</code> object.
     * 
     * @param p
     *        the specified <code>Point3D</code> to which to set
     *        this <code>Point3D</code>
     */
    public void setLocation(Point3D p)
    {
        setLocation(p.getX(), p.getY(), p.getZ());
    }

    /**
     * Translate this <code>Point3D</code> by the specified <code>double</code> coordinates.
     * 
     * @param x
     *        the X translation value
     * @param y
     *        the Y translation value
     * @param z
     *        the Z translation value
     */
    public void translate(double x, double y, double z)
    {
        setX(getX() + x);
        setY(getY() + y);
        setZ(getZ() + z);
    }

    /**
     * Translate this <code>Point3D</code> by the specified <code>Point3D</code> coordinates.
     * 
     * @param p
     *        the specified <code>Point3D</code> used to translate this <code>Point3D</code>
     */
    public void translate(Point3D p)
    {
        translate(p.getX(), p.getY(), p.getZ());
    }

    /**
     * Convert to 2D point
     */
    public abstract Point2D toPoint2D();

    /**
     * Computes the distance between this point and point {@code (x1, y1, z1)}.
     *
     * @param x1
     *        the x coordinate of other point
     * @param y1
     *        the y coordinate of other point
     * @param z1
     *        the z coordinate of other point
     * @return the distance between this point and point {@code (x1, y1, z1)}.
     */
    public double distance(double x1, double y1, double z1)
    {
        double a = getX() - x1;
        double b = getY() - y1;
        double c = getZ() - z1;
        return Math.sqrt(a * a + b * b + c * c);
    }

    /**
     * Computes the distance between this point and the specified {@code point}.
     *
     * @param point
     *        the other point
     * @return the distance between this point and the specified {@code point}.
     * @throws NullPointerException
     *         if the specified {@code point} is null
     */
    public double distance(Point3D point)
    {
        return distance(point.getX(), point.getY(), point.getZ());
    }

    /**
     * Normalizes the relative magnitude vector represented by this instance.
     * Returns a vector with the same direction and magnitude equal to 1.
     * If this is a zero vector, a zero vector is returned.
     * 
     * @return the normalized vector represented by a {@code Point3D} instance
     */
    public Point3D normalize()
    {
        final double mag = magnitude();

        if (mag == 0d)
            return new Point3D.Double(0d, 0d, 0d);

        return new Point3D.Double(getX() / mag, getY() / mag, getZ() / mag);
    }

    /**
     * Returns a point which lies in the middle between this point and the
     * specified coordinates.
     * 
     * @param x
     *        the X coordinate of the second end point
     * @param y
     *        the Y coordinate of the second end point
     * @param z
     *        the Z coordinate of the second end point
     * @return the point in the middle
     */
    public Point3D midpoint(double x, double y, double z)
    {
        return new Point3D.Double(x + (getX() - x) / 2d, y + (getY() - y) / 2d, z + (getZ() - z) / 2d);
    }

    /**
     * Returns a point which lies in the middle between this point and the
     * specified point.
     * 
     * @param point
     *        the other end point
     * @return the point in the middle
     * @throws NullPointerException
     *         if the specified {@code point} is null
     */
    public Point3D midpoint(Point3D point)
    {
        return midpoint(point.getX(), point.getY(), point.getZ());
    }

    /**
     * Computes the angle (in degrees) between the vector represented
     * by this point and the specified vector.
     * 
     * @param x
     *        the X magnitude of the other vector
     * @param y
     *        the Y magnitude of the other vector
     * @param z
     *        the Z magnitude of the other vector
     * @return the angle between the two vectors measured in degrees
     */
    public double angle(double x, double y, double z)
    {
        final double ax = getX();
        final double ay = getY();
        final double az = getZ();

        final double delta = (ax * x + ay * y + az * z)
                / Math.sqrt((ax * ax + ay * ay + az * az) * (x * x + y * y + z * z));

        if (delta > 1d)
            return 0d;
        if (delta < -1d)
            return 180d;

        return Math.toDegrees(Math.acos(delta));
    }

    /**
     * Computes the angle (in degrees) between the vector represented
     * by this point and the vector represented by the specified point.
     * 
     * @param vector
     *        the other vector
     * @return the angle between the two vectors measured in degrees, {@code NaN} if any of the two vectors is a zero
     *         vector
     * @throws NullPointerException
     *         if the specified {@code vector} is null
     */
    public double angle(Point3D vector)
    {
        return angle(vector.getX(), vector.getY(), vector.getZ());
    }

    /**
     * Computes the angle (in degrees) between the three points with this point
     * as a vertex.
     * 
     * @param p1
     *        one point
     * @param p2
     *        other point
     * @return angle between the vectors (this, p1) and (this, p2) measured
     *         in degrees, {@code NaN} if the three points are not different
     *         from one another
     * @throws NullPointerException
     *         if the {@code p1} or {@code p2} is null
     */
    public double angle(Point3D p1, Point3D p2)
    {
        final double x = getX();
        final double y = getY();
        final double z = getZ();

        final double ax = p1.getX() - x;
        final double ay = p1.getY() - y;
        final double az = p1.getZ() - z;
        final double bx = p2.getX() - x;
        final double by = p2.getY() - y;
        final double bz = p2.getZ() - z;

        final double delta = (ax * bx + ay * by + az * bz)
                / Math.sqrt((ax * ax + ay * ay + az * az) * (bx * bx + by * by + bz * bz));

        if (delta > 1.0)
            return 0.0;

        if (delta < -1.0)
            return 180.0;

        return Math.toDegrees(Math.acos(delta));
    }

    /**
     * Computes norm2 (square length) of the vector represented by this Point3D.
     * 
     * @return norm2 length of the vector
     */
    public double norm2()
    {
        final double x = getX();
        final double y = getY();
        final double z = getZ();

        return x * x + y * y + z * z;
    }

    /**
     * Computes length of the vector represented by this Point3D.
     * 
     * @return length of the vector
     */
    public double length()
    {
        return Math.sqrt(norm2());
    }

    /**
     * Same as {@link #length()}
     */
    public double magnitude()
    {
        return length();
    }

    /**
     * Computes dot (scalar) product of the vector represented by this instance
     * and the specified vector.
     * 
     * @param x
     *        the X magnitude of the other vector
     * @param y
     *        the Y magnitude of the other vector
     * @param z
     *        the Z magnitude of the other vector
     * @return the dot product of the two vectors
     */
    public double dotProduct(double x, double y, double z)
    {
        return getX() * x + getY() * y + getZ() * z;
    }

    /**
     * Computes dot (scalar) product of the vector represented by this instance
     * and the specified vector.
     * 
     * @param vector
     *        the other vector
     * @return the dot product of the two vectors
     * @throws NullPointerException
     *         if the specified {@code vector} is null
     */
    public double dotProduct(Point3D vector)
    {
        return dotProduct(vector.getX(), vector.getY(), vector.getZ());
    }

    /**
     * Computes cross product of the vector represented by this instance
     * and the specified vector.
     * 
     * @param x
     *        the X magnitude of the other vector
     * @param y
     *        the Y magnitude of the other vector
     * @param z
     *        the Z magnitude of the other vector
     * @return the cross product of the two vectors
     */
    public Point3D crossProduct(double x, double y, double z)
    {
        final double ax = getX();
        final double ay = getY();
        final double az = getZ();

        return new Point3D.Double(ay * z - az * y, az * x - ax * z, ax * y - ay * x);
    }

    /**
     * Computes cross product of the vector represented by this instance
     * and the specified vector.
     * 
     * @param vector
     *        the other vector
     * @return the cross product of the two vectors
     * @throws NullPointerException
     *         if the specified {@code vector} is null
     */
    public Point3D crossProduct(Point3D vector)
    {
        return crossProduct(vector.getX(), vector.getY(), vector.getZ());
    }

    @Override
    public boolean equals(Object obj)
    {
        if (obj == this)
            return true;

        if (obj instanceof Point3D)
        {
            final Point3D pt = (Point3D) obj;
            return (getX() == pt.getX()) && (getY() == pt.getY()) && (getZ() == pt.getZ());
        }

        return super.equals(obj);
    }

    /**
     * Creates a new object of the same class as this object.
     * 
     * @return a clone of this instance.
     * @exception OutOfMemoryError
     *            if there is not enough memory.
     * @see java.lang.Cloneable
     */
    @Override
    public Object clone()
    {
        try
        {
            return super.clone();
        }
        catch (CloneNotSupportedException e)
        {
            // this shouldn't happen, since we are Cloneable
            throw new InternalError();
        }
    }

    /**
     * @return a hash code for this {@code Point3D} object.
     */
    @Override
    public int hashCode()
    {
        if (hash == 0)
        {
            long bits = 7L;
            bits = 31L * bits + java.lang.Double.doubleToLongBits(getX());
            bits = 31L * bits + java.lang.Double.doubleToLongBits(getY());
            bits = 31L * bits + java.lang.Double.doubleToLongBits(getZ());
            hash = (int) (bits ^ (bits >> 32));
            // so hash could never be 0 when computed
            hash |= 1;
        }

        return hash;
    }

    /**
     * Returns a string representation of this {@code Point3D}.
     * This method is intended to be used only for informational purposes.
     */
    @Override
    public String toString()
    {
        return getClass().getName() + "[" + getX() + "," + getY() + "," + getZ() + "]";
    }

    public static class Double extends Point3D
    {
        /**
         * Create an array of Point3D.Double from the input double array.<br>
         * <br>
         * The format of the input array should be as follow:<br>
         * <code>input.lenght</code> = number of point * 3.<br>
         * <code>input[(pt * 3) + 0]</code> = X coordinate for point <i>pt</i><br>
         * <code>input[(pt * 3) + 1]</code> = Y coordinate for point <i>pt</i><br>
         * <code>input[(pt * 3) + 2]</code> = Z coordinate for point <i>pt</i><br>
         */
        public static Point3D.Double[] toPoint3D(double[] input)
        {
            final Point3D.Double[] result = new Point3D.Double[input.length / 3];

            int pt = 0;
            for (int i = 0; i < input.length; i += 3)
                result[pt++] = new Point3D.Double(input[i + 0], input[i + 1], input[i + 2]);

            return result;
        }

        /**
         * Create an array of double from the input Point3D.Double array.<br>
         * <br>
         * The format of the output array is as follow:<br>
         * <code>result.lenght</code> = number of point * 3.<br>
         * <code>result[(pt * 3) + 0]</code> = X coordinate for point <i>pt</i><br>
         * <code>result[(pt * 3) + 1]</code> = Y coordinate for point <i>pt</i><br>
         * <code>result[(pt * 3) + 2]</code> = Z coordinate for point <i>pt</i><br>
         */
        public static double[] toDoubleArray(Point3D.Double[] input)
        {
            final double[] result = new double[input.length * 3];

            int off = 0;
            for (Point3D.Double pt : input)
            {
                result[off++] = pt.x;
                result[off++] = pt.y;
                result[off++] = pt.z;
            }

            return result;
        }

        public double x;
        public double y;
        public double z;

        public Double(double x, double y, double z)
        {
            super();

            this.x = x;
            this.y = y;
            this.z = z;
        }

        public Double(double[] xyz)
        {
            final int len = xyz.length;

            if (len > 0)
                this.x = xyz[0];
            if (len > 1)
                this.y = xyz[1];
            if (len > 2)
                this.z = xyz[2];
        }

        public Double()
        {
            this(0, 0, 0);
        }

        @Override
        public double getX()
        {
            return x;
        }

        @Override
        public void setX(double x)
        {
            this.x = x;
            hash = 0;
        }

        @Override
        public double getY()
        {
            return y;
        }

        @Override
        public void setY(double y)
        {
            this.y = y;
            hash = 0;
        }

        @Override
        public double getZ()
        {
            return z;
        }

        @Override
        public void setZ(double z)
        {
            this.z = z;
            hash = 0;
        }

        @Override
        public String toString()
        {
            return "Point3D.Double[" + x + "," + y + "," + z + "]";
        }

        @Override
        public Point2D toPoint2D()
        {
            return new Point2D.Double(x, y);
        }
    }

    public static class Float extends Point3D
    {
        /**
         * Create an array of Point3D.Float from the input float array.<br>
         * <br>
         * The format of the input array should be as follow:<br>
         * <code>input.lenght</code> = number of point * 3.<br>
         * <code>input[(pt * 3) + 0]</code> = X coordinate for point <i>pt</i><br>
         * <code>input[(pt * 3) + 1]</code> = Y coordinate for point <i>pt</i><br>
         * <code>input[(pt * 3) + 2]</code> = Z coordinate for point <i>pt</i><br>
         */
        public static Point3D.Float[] toPoint3D(float[] input)
        {
            final Point3D.Float[] result = new Point3D.Float[input.length / 3];

            int pt = 0;
            for (int i = 0; i < input.length; i += 3)
                result[pt++] = new Point3D.Float(input[i + 0], input[i + 1], input[i + 2]);

            return result;
        }

        /**
         * Create an array of float from the input Point3D.Float array.<br>
         * <br>
         * The format of the output array is as follow:<br>
         * <code>result.lenght</code> = number of point * 3.<br>
         * <code>result[(pt * 3) + 0]</code> = X coordinate for point <i>pt</i><br>
         * <code>result[(pt * 3) + 1]</code> = Y coordinate for point <i>pt</i><br>
         * <code>result[(pt * 3) + 2]</code> = Z coordinate for point <i>pt</i><br>
         */
        public static float[] toFloatArray(Point3D.Float[] input)
        {
            final float[] result = new float[input.length * 3];

            int off = 0;
            for (Point3D.Float pt : input)
            {
                result[off++] = pt.x;
                result[off++] = pt.y;
                result[off++] = pt.z;
            }

            return result;
        }

        public float x;
        public float y;
        public float z;

        public Float(float x, float y, float z)
        {
            super();

            this.x = x;
            this.y = y;
            this.z = z;
        }

        public Float(float[] xyz)
        {
            final int len = xyz.length;

            if (len > 0)
                this.x = xyz[0];
            if (len > 1)
                this.y = xyz[1];
            if (len > 2)
                this.z = xyz[2];
        }

        public Float()
        {
            this(0, 0, 0);
        }

        @Override
        public double getX()
        {
            return x;
        }

        @Override
        public void setX(double x)
        {
            this.x = (float) x;
            hash = 0;
        }

        @Override
        public double getY()
        {
            return y;
        }

        @Override
        public void setY(double y)
        {
            this.y = (float) y;
            hash = 0;
        }

        @Override
        public double getZ()
        {
            return z;
        }

        @Override
        public void setZ(double z)
        {
            this.z = (float) z;
            hash = 0;
        }

        @Override
        public String toString()
        {
            return "Point3D.Float[" + x + "," + y + "," + z + "]";
        }

        @Override
        public Point2D toPoint2D()
        {
            return new Point2D.Float(x, y);
        }
    }

    public static class Integer extends Point3D
    {
        /**
         * Create an array of Point3D.Integer from the input integer array.<br>
         * <br>
         * The format of the input array should be as follow:<br>
         * <code>input.lenght</code> = number of point * 3.<br>
         * <code>input[(pt * 3) + 0]</code> = X coordinate for point <i>pt</i><br>
         * <code>input[(pt * 3) + 1]</code> = Y coordinate for point <i>pt</i><br>
         * <code>input[(pt * 3) + 2]</code> = Z coordinate for point <i>pt</i><br>
         */
        public static Point3D.Integer[] toPoint3D(int[] input)
        {
            final Point3D.Integer[] result = new Point3D.Integer[input.length / 3];

            int pt = 0;
            for (int i = 0; i < input.length; i += 3)
                result[pt++] = new Point3D.Integer(input[i + 0], input[i + 1], input[i + 2]);

            return result;
        }

        /**
         * Create an array of integer from the input Point3D.Integer array.<br>
         * <br>
         * The format of the output array is as follow:<br>
         * <code>result.lenght</code> = number of point * 3.<br>
         * <code>result[(pt * 3) + 0]</code> = X coordinate for point <i>pt</i><br>
         * <code>result[(pt * 3) + 1]</code> = Y coordinate for point <i>pt</i><br>
         * <code>result[(pt * 3) + 2]</code> = Z coordinate for point <i>pt</i><br>
         */
        public static int[] toIntegerArray(Point3D.Integer[] input)
        {
            final int[] result = new int[input.length * 3];

            int off = 0;
            for (Point3D.Integer pt : input)
            {
                result[off++] = pt.x;
                result[off++] = pt.y;
                result[off++] = pt.z;
            }

            return result;
        }

        public int x;
        public int y;
        public int z;

        public Integer(int x, int y, int z)
        {
            super();

            this.x = x;
            this.y = y;
            this.z = z;
        }

        public Integer(int[] xyz)
        {
            final int len = xyz.length;

            if (len > 0)
                this.x = xyz[0];
            if (len > 1)
                this.y = xyz[1];
            if (len > 2)
                this.z = xyz[2];
        }

        public Integer()
        {
            this(0, 0, 0);
        }

        @Override
        public double getX()
        {
            return x;
        }

        @Override
        public void setX(double x)
        {
            this.x = (int) x;
            hash = 0;
        }

        @Override
        public double getY()
        {
            return y;
        }

        @Override
        public void setY(double y)
        {
            this.y = (int) y;
            hash = 0;
        }

        @Override
        public double getZ()
        {
            return z;
        }

        @Override
        public void setZ(double z)
        {
            this.z = (int) z;
            hash = 0;
        }

        @Override
        public String toString()
        {
            return "Point3D.Integer[" + x + "," + y + "," + z + "]";
        }

        @Override
        public Point2D toPoint2D()
        {
            return new Point(x, y);
        }
    }
}