/*
 * Copyright (C) 2011-2019, Peter Abeles. All Rights Reserved.
 *
 * This file is part of Geometric Regression Library (GeoRegression).
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package georegression.geometry;

import georegression.struct.GeoTuple2D_F64;
import georegression.struct.point.Point2D_F64;
import georegression.struct.shapes.Rectangle2D_F64;
import georegression.struct.shapes.RectangleLength2D_F64;
import org.ddogleg.sorting.QuickSort_F64;
import org.ejml.data.DMatrix;
import org.ejml.data.ReshapeMatrix;

import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;

/**
 *
 *
 */
public class UtilPoint2D_F64 {

	public static List<Point2D_F64> copy( List<Point2D_F64> pts ) {
		List<Point2D_F64> ret = new ArrayList<Point2D_F64>();

		for( Point2D_F64 p : pts ) {
			ret.add( p.copy() );
		}

		return ret;
	}

	public static void noiseNormal( List<Point2D_F64> pts, double sigma, Random rand ) {
		for( Point2D_F64 p : pts ) {
			p.x += rand.nextGaussian() * sigma;
			p.y += rand.nextGaussian() * sigma;
		}
	}

	public static Point2D_F64 noiseNormal(Point2D_F64 mean ,
										  double sigmaX , double sigmaY,
										  Random rand ,
										  @Nullable Point2D_F64 output )
	{
		if( output == null )
			output = new Point2D_F64();

		output.x = mean.x + rand.nextGaussian()*sigmaX;
		output.y = mean.y + rand.nextGaussian()*sigmaY;

		return output;
	}

	public static double distance( double x0, double y0, double x1, double y1 ) {
		double dx = x1 - x0;
		double dy = y1 - y0;

		return Math.sqrt( dx * dx + dy * dy );
	}

	public static double distanceSq( double x0, double y0, double x1, double y1 ) {
		double dx = x1 - x0;
		double dy = y1 - y0;

		return dx * dx + dy * dy;
	}

	/**
	 * Finds the point which has the mean location of all the points in the list. This is also known
	 * as the centroid.
	 *
	 * @param list List of points
	 * @param mean Storage for mean point.  If null then a new instance will be declared
	 * @return The found mean
	 */
	public static Point2D_F64 mean( List<Point2D_F64> list , Point2D_F64 mean ) {
		if( mean == null )
			mean = new Point2D_F64();

		double x = 0;
		double y = 0;

		for( Point2D_F64 p : list ) {
			x += p.getX();
			y += p.getY();
		}

		x /= list.size();
		y /= list.size();

		mean.set(x, y);
		return mean;
	}

	/**
	 * Finds the point which has the mean location of all the points in the array. This is also known
	 * as the centroid.
	 *
	 * @param list List of points
	 * @param offset First index in list
	 * @param length Length of elements in list
	 * @param mean Storage for mean point.  If null then a new instance will be declared
	 * @return The found mean
	 */
	public static Point2D_F64 mean( Point2D_F64[] list , int offset , int length , Point2D_F64 mean ) {
		if( mean == null )
			mean = new Point2D_F64();

		double x = 0;
		double y = 0;

		for (int i = 0; i < length; i++) {
			Point2D_F64 p = list[offset+i];
			x += p.getX();
			y += p.getY();
		}

		x /= length;
		y /= length;

		mean.set(x, y);
		return mean;
	}

	/**
	 * Computes the mean/average of two points.
	 *
	 * @param a (input) Point A
	 * @param b (input) Point B
	 * @param mean (output) average of 'a' and 'b'
	 *
	 */
	public static Point2D_F64 mean( Point2D_F64 a , Point2D_F64 b, Point2D_F64 mean ) {
		if( mean == null )
			mean = new Point2D_F64();

		mean.x = (a.x + b.x)/2.0;
		mean.y = (a.y + b.y)/2.0;

		return mean;
	}

	public static List<Point2D_F64> random( double min, double max, int num, Random rand ) {
		List<Point2D_F64> ret = new ArrayList<Point2D_F64>();

		double d = max - min;

		for( int i = 0; i < num; i++ ) {
			Point2D_F64 p = new Point2D_F64();
			p.x = rand.nextDouble() * d + min;
			p.y = rand.nextDouble() * d + min;

			ret.add( p );
		}

		return ret;
	}

	public static boolean isEquals( GeoTuple2D_F64 a, GeoTuple2D_F64 b, double tol ) {
		return ( Math.abs( a.x - b.x ) <= tol && Math.abs( a.x - b.x ) <= tol );
	}

	/**
	 * Finds the minimal volume {@link georegression.struct.shapes.RectangleLength2D_F64} which contains all the points.
	 *
	 * @param points Input: List of points.
	 * @param bounding Output: Bounding rectangle
	 */
	public static RectangleLength2D_F64 bounding(List<Point2D_F64> points, RectangleLength2D_F64 bounding) {
		if( bounding == null )
			bounding = new RectangleLength2D_F64();

		double minX=Double.MAX_VALUE,maxX=-Double.MAX_VALUE;
		double minY=Double.MAX_VALUE,maxY=-Double.MAX_VALUE;

		for( int i = 0; i < points.size(); i++ ) {
			Point2D_F64 p = points.get(i);
			if( p.x < minX )
				minX = p.x;
			if( p.x > maxX )
				maxX = p.x;
			if( p.y < minY )
				minY = p.y;
			if( p.y > maxY )
				maxY = p.y;
		}

		bounding.x0 = minX;
		bounding.y0 = minY;
		bounding.width = maxX-minX;
		bounding.height = maxY-minY;

		// make sure rounding doesn't cause a point to be out of bounds
		bounding.width  += Math.max(0,(maxX-(bounding.x0+bounding.width ))*10.0);
		bounding.height += Math.max(0,(maxY-(bounding.y0+bounding.height))*10.0);

		return bounding;
	}

	/**
	 * Finds the minimal volume {@link georegression.struct.shapes.RectangleLength2D_F64} which contains all the points.
	 *
	 * @param points Input: List of points.
	 * @param bounding Output: Bounding rectangle
	 */
	public static Rectangle2D_F64 bounding(List<Point2D_F64> points, Rectangle2D_F64 bounding) {
		if( bounding == null )
			bounding = new Rectangle2D_F64();

		double minX=Double.MAX_VALUE,maxX=-Double.MAX_VALUE;
		double minY=Double.MAX_VALUE,maxY=-Double.MAX_VALUE;

		for( int i = 0; i < points.size(); i++ ) {
			Point2D_F64 p = points.get(i);
			if( p.x < minX )
				minX = p.x;
			if( p.x > maxX )
				maxX = p.x;
			if( p.y < minY )
				minY = p.y;
			if( p.y > maxY )
				maxY = p.y;
		}

		bounding.set(minX,minY,maxX,maxY);

		return bounding;
	}

	/**
	 * Puts the points into counter-clockwise order around their center.
	 *
	 * @param points List of points. Not modified.
	 * @return ordered list
	 */
	public static List<Point2D_F64> orderCCW( List<Point2D_F64> points ) {
		Point2D_F64 center = mean(points,null);

		double angles[] = new double[ points.size() ];
		for (int i = 0; i < angles.length; i++) {
			Point2D_F64 p = points.get(i);

			double dx = p.x - center.x;
			double dy = p.y - center.y;

			angles[i] = Math.atan2(dy,dx);
		}

		int order[] = new int[ points.size() ];

		QuickSort_F64 sorter = new QuickSort_F64();
		sorter.sort(angles,0,points.size(),order);

		List<Point2D_F64> out = new ArrayList<Point2D_F64>(points.size());
		for (int i = 0; i < points.size(); i++) {
			out.add(points.get(order[i]));
		}

		return out;
	}

	/**
	 * Computes the mean and covariance matrix from the set of points. This describes a normal distribution
	 * @param points (Input) points
	 * @param mean (Output) mean of the points
	 * @param covariance (Output) 2x2 covariance matrix
	 */
	public static void computeNormal(List<Point2D_F64> points , Point2D_F64 mean , DMatrix covariance ) {
		if( covariance.getNumCols() != 2 || covariance.getNumRows() != 2 ) {
			if (covariance instanceof ReshapeMatrix) {
				((ReshapeMatrix)covariance).reshape(2,2);
			} else {
				throw new IllegalArgumentException("Must be a 2x2 matrix");
			}
		}

		mean(points,mean);

		double xx=0,xy=0,yy=0;

		for (int i = 0; i < points.size(); i++) {
			Point2D_F64 p = points.get(i);
			double dx = p.x-mean.x;
			double dy = p.y-mean.y;

			xx += dx*dx;
			xy += dx*dy;
			yy += dy*dy;
		}
		xx /= points.size();
		xy /= points.size();
		yy /= points.size();

		covariance.unsafe_set(0,0,xx);
		covariance.unsafe_set(0,1,xy);
		covariance.unsafe_set(1,0,xy);
		covariance.unsafe_set(1,1,yy);
	}

	/**
	 * Randomly generates points from the specified normal distribution
	 * @param mean (Input) mean
	 * @param covariance (Output) 2x2 covariance matrix
	 * @param count (Input) Number of points to create
	 * @param rand (Input) Random number generator
	 * @param output (Output) Optional storage for points. If null a new list is created
	 * @return List containing points.
	 */
	public static List<Point2D_F64> randomNorm( Point2D_F64 mean , DMatrix covariance , int count ,
												Random rand ,
												@Nullable List<Point2D_F64> output )
	{
		if( output == null )
			output = new ArrayList<>();

		// extract values of covariance
		double cxx = covariance.get(0,0);
		double cxy = covariance.get(0,1);
		double cyy = covariance.get(1,1);

		// perform cholesky decomposition
		double sxx = Math.sqrt(cxx);
		double sxy = cxy/cxx;
		double syy = Math.sqrt(cyy-sxy*sxy);

		for (int i = 0; i < count; i++) {
			Point2D_F64 p = new Point2D_F64();

			double x = rand.nextGaussian();
			double y = rand.nextGaussian();

			p.x = mean.x + sxx*x + sxy*y;
			p.y = mean.y + sxy*x + syy*y;

			output.add(p);
		}

		return output;
	}
}


/*
 * Copyright (C) 2011-2019, Peter Abeles. All Rights Reserved.
 *
 * This file is part of Geometric Regression Library (GeoRegression).
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package georegression.struct.line;

import georegression.geometry.UtilLine2D_F64;
import org.ejml.FancyPrint;

import java.io.Serializable;

/**
 *

 * Represents the line using three parameters such that any point on the line obeys the
 * following formula, A*x + B*y + C = 0.  Any 2D line can be represented using this notation.
 * This formulation is also known as standard and implicit.  The slope is -A/B.
 *

 *

 * If it is said the line is normalized that refers to it being scaled such that A*A + B*B = 1.  To
 * normalize a line call {@link #normalize()}.  After normalization several operations become less expensive.
 *

 *
 * @see UtilLine2D_F64
 *
 * @author Peter Abeles
 */
public class LineGeneral2D_F64 implements Serializable {

	/**
	 * Coefficients which define the line.
	 */
	public double A,B,C;

	/**
	 * A*x + B*y + C = 0
	 *
	 * @param a parameter A
	 * @param b parameter B
	 * @param c parameter C
	 */
	public LineGeneral2D_F64(double a, double b, double c) {
		set(a,b,c);
	}

	public LineGeneral2D_F64(LineGeneral2D_F64 line ) {
		set(line);
	}

	public LineGeneral2D_F64() {
	}

	public double getA() {
		return A;
	}

	public double getB() {
		return B;
	}

	public double getC() {
		return C;
	}

	public void set( LineGeneral2D_F64 original ) {
		this.A = original.A;
		this.B = original.B;
		this.C = original.C;
	}

	/**
	 * A*x + B*y + C = 0
	 *
	 * @param a parameter A
	 * @param b parameter B
	 * @param c parameter C
	 */
	public void set(double a, double b, double c) {
		this.A = a;
		this.B = b;
		this.C = c;
	}

	public void setA(double a) {
		A = a;
	}

	public void setB(double b) {
		B = b;
	}

	public void setC(double c) {
		C = c;
	}

	/**
	 * Ensures that A*A + B*B == 1
	 */
	public void normalize() {
		double d = Math.sqrt(A*A + B*B);
		A /= d;
		B /= d;
		C /= d;
	}

	/**
	 * Returns the result of A*x + B*y + C.  If the line is normalized then this is also the signed distance
	 * away from the line.
	 *
	 * @param x x-coordinate of a point
	 * @param y y-coordinate of a point
	 * @return result of line equation
	 */
	public double evaluate( double x , double y ) {
		return A*x + B*y + C;
	}

	public LineGeneral2D_F64 copy() {
		return new LineGeneral2D_F64(this);
	}

	@Override
	public String toString() {
		FancyPrint f = new FancyPrint();
		return getClass().getSimpleName()+"{ A="+f.s(A)+" B="+f.s(B)+" C="+f.s(C)+" }";
	}

	@Override
	public boolean equals(Object obj) {
		try {
			LineGeneral2D_F64 o = (LineGeneral2D_F64) obj;
			return A==o.A && B ==o.B && C==o.C;
		} catch( RuntimeException e ) {
			return super.equals(obj);
		}
	}
}


/*
 * Copyright (C) 2011-2018, Peter Abeles. All Rights Reserved.
 *
 * This file is part of Geometric Regression Library (GeoRegression).
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package georegression.struct.point;

import georegression.geometry.UtilVector2D_F64;
import georegression.struct.GeoTuple2D_F64;

/**
 * Spacial vector in 2D
 */
@SuppressWarnings({"unchecked"})
public class Vector2D_F64 extends GeoTuple2D_F64<Vector2D_F64> {

	public Vector2D_F64( GeoTuple2D_F64 orig ) {
		this(orig.x,orig.y);
	}

	public Vector2D_F64( double x, double y ) {
		set( x, y );
	}

	public Vector2D_F64() {
	}

	public Vector2D_F64( Vector2D_F64 pt ) {
		set( pt.x, pt.y );
	}

	@Override
	public Vector2D_F64 createNewInstance() {
		return new Vector2D_F64();
	}

	public void set( Vector2D_F64 orig ) {
		_set( orig );
	}

	public Vector2D_F64 copy() {
		return new Vector2D_F64( this );
	}

	public void normalize() {
		double r = norm();
		x /= r;
		y /= r;
	}

	/**
	 * In-place minus operation. this = a - b.
	 *
	 * @param a Point
	 * @param b Point
	 */
	public void minus( Point2D_F64 a , Point2D_F64 b ) {
		x = a.x - b.x;
		y = a.y - b.y;
	}

	@Override
	public String toString() {
		return toString("V");
	}

	/**
	 * Dot product between this and 'a' = this.x * a.x + this.y * a.y
	 *
	 * @param a A vector
	 * @return dot product.
	 */
	public double dot( Vector2D_F64 a ) {
		return x * a.x + y * a.y;
	}

	/**
	 * Returns the acute angle between the two vectors.  Computed using the dot product.
	 * @param a Vector
	 * @return Acute angle in radians between 'this' and 'a'.
	 */
	public double acute( Vector2D_F64 a ) {
		return UtilVector2D_F64.acute(this, a);
	}
}


/*
 * Copyright (C) 2011-2018, Peter Abeles. All Rights Reserved.
 *
 * This file is part of Geometric Regression Library (GeoRegression).
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package georegression.geometry;

import georegression.struct.point.Point2D_I32;
import georegression.struct.shapes.Polygon2D_I32;
import georegression.struct.shapes.Rectangle2D_I32;

import java.util.List;

/**
 * Various functions related to polygons.
 *
 * @author Peter Abeles
 */
public class UtilPolygons2D_I32 {

	/**
	 * Finds the minimum area bounding rectangle which is aligned to the x and y axis around the list of points.
	 * Note (x0,y0) is inclusive and (x1,y1) is exclusive.
	 *
	 * @param points (Input) Quadrilateral
	 * @param rectangle (Output) Minimum area rectangle
	 */
	public static void bounding( List<Point2D_I32> points , Rectangle2D_I32 rectangle ) {

		rectangle.x0 = Integer.MAX_VALUE;
		rectangle.y0 = Integer.MAX_VALUE;
		rectangle.x1 = Integer.MIN_VALUE;
		rectangle.y1 = Integer.MIN_VALUE;

		for( int i = 0; i < points.size(); i++ ) {
			Point2D_I32 p = points.get(i);

			if( p.x < rectangle.x0 )
				rectangle.x0 = p.x;
			if( p.x > rectangle.x1 )
				rectangle.x1 = p.x;
			if( p.y < rectangle.y0 )
				rectangle.y0 = p.y;
			if( p.y > rectangle.y1 )
				rectangle.y1 = p.y;
		}

		rectangle.x1++;
		rectangle.y1++;
	}

	/**
	 * Finds the minimum area bounding rectangle which is aligned to the x and y axis around the polygon.
	 * Note (x0,y0) is inclusive and (x1,y1) is exclusive.
	 *
	 * @param quad (Input) Quadrilateral
	 * @param rectangle (Output) Minimum area rectangle
	 */
	public static void bounding( Polygon2D_I32 quad , Rectangle2D_I32 rectangle ) {

		UtilPolygons2D_I32.bounding(quad.vertexes.toList(),rectangle);
	}

	/**
	 * Returns true if the polygon is ordered in a counter-clockwise order.  This is done by summing up the interior
	 * angles.
	 *
	 * @param polygon List of ordered points which define a polygon
	 * @return true if CCW and false if CW
	 */
	public static boolean isCCW( List<Point2D_I32> polygon ) {
		final int N = polygon.size();
		int sign = 0;
		for (int i = 0; i < N; i++) {
			int j = (i+1)%N;
			int k = (i+2)%N;

			if( isPositiveZ(polygon.get(i),polygon.get(j),polygon.get(k))) {
				sign++;
			} else {
				sign--;
			}
		}

		return sign < 0;
	}

	/**
	 * Flips the order of points inside the polygon.  The first index will remain the same will otherwise be reversed
	 *
	 * @param a Polygon of order 3 or more.
	 */
	public static void flip( Polygon2D_I32 a ) {
		int N = a.size();
		int H = N/2;

		for (int i = 1; i <= H; i++) {
			int j = N-i;
			Point2D_I32 tmp = a.vertexes.data[i];
			a.vertexes.data[i] = a.vertexes.data[j];
			a.vertexes.data[j] = tmp;
		}
	}

	/**
	 * Determines if the polugon is convex or concave.
	 *
	 * @param poly Polygon
	 * @return true if convex and false if concave
	 */
	public static boolean isConvex( Polygon2D_I32 poly ) {
		// if the cross product of all consecutive triples is positive or negative then it is convex

		final int N = poly.size();
		int numPositive = 0;
		for (int i = 0; i < N; i++) {
			int j = (i+1)%N;
			int k = (i+2)%N;

			Point2D_I32 a = poly.vertexes.data[i];
			Point2D_I32 b = poly.vertexes.data[j];
			Point2D_I32 c = poly.vertexes.data[k];

			int dx0 = a.x-b.x;
			int dy0 = a.y-b.y;

			int dx1 = c.x-b.x;
			int dy1 = c.y-b.y;

			int z = dx0 * dy1 - dy0 * dx1;
			if( z > 0 )
				numPositive++;
			// z can be zero if there are duplicate points.
			// not sure if it should throw an exception if its "bad" or not
		}

		return( numPositive == 0 || numPositive == N );
	}

	/**
	 * Returns true if the cross product would result in a strictly positive z (e.g. z > 0 ). If true then
	 * the order is clockwise.
	 *
	 * v0 = a-b
	 * v1 = c-b
	 *
	 * @param a first point in sequence
	 * @param b second point in sequence
	 * @param c third point in sequence
	 * @return true if positive z
	 */
	public static boolean isPositiveZ(Point2D_I32 a, Point2D_I32 b, Point2D_I32 c  ) {
		int dx0 = a.x-b.x;
		int dy0 = a.y-b.y;

		int dx1 = c.x-b.x;
		int dy1 = c.y-b.y;

		int z = dx0 * dy1 - dy0 * dx1;

		return z > 0 ;
	}

	/**
	 * Checks to see if the vertexes of the two polygon's are the same up to the specified tolerance
	 *
	 * @param a Polygon
	 * @param b Polygon
	 * @return true if identical up to tolerance or false if not
	 */
	public static boolean isIdentical(Polygon2D_I32 a , Polygon2D_I32 b  ) {
		if( a.size() != b.size())
			return false;

		for (int i = 0; i < a.size(); i++) {
			if( !a.get(i).equals(b.get(i))  )
				return false;
		}
		return true;
	}

	/**
	 * Checks to see if the vertexes of the two polygon's are the same up to the specified tolerance and allows
	 * for a shift in their order
	 *
	 * @param a Polygon
	 * @param b Polygon
	 * @return true if identical up to tolerance or false if not
	 */
	public static boolean isEquivalent( Polygon2D_I32 a , Polygon2D_I32 b  ) {
		if( a.size() != b.size())
			return false;

		// first find two vertexes which are the same
		Point2D_I32 a0 = a.get(0);
		int match = -1;
		for (int i = 0; i < b.size(); i++) {
			if( a0.equals(b.get(i)) ) {
				match = i;
				break;
			}
		}

		if( match < 0 )
			return false;

		// now go in a circle and see if they all line up
		for (int i = 1; i < b.size(); i++) {
			Point2D_I32 ai = a.get(i);
			Point2D_I32 bi = b.get((match+i)%b.size());

			if( !ai.equals(bi)  ) {
				return false;
			}
		}
		return true;
	}

}


/*
 * Copyright (C) 2011-2019, Peter Abeles. All Rights Reserved.
 *
 * This file is part of Geometric Regression Library (GeoRegression).
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package georegression.geometry;

import georegression.struct.point.Point2D_F32;
import georegression.struct.point.Point2D_F64;
import georegression.struct.point.Point2D_I32;

import java.util.List;

/**
 *
 *
 */
public class UtilPoint2D_I32 {

	public static double distance( Point2D_I32 a , Point2D_I32 b ) {
		int dx = b.x - a.x;
		int dy = b.y - a.y;

		return Math.sqrt( dx * dx + dy * dy );
	}

	public static int distanceSq( Point2D_I32 a , Point2D_I32 b ) {
		int dx = b.x - a.x;
		int dy = b.y - a.y;

		return dx * dx + dy * dy;
	}

	public static double distance( int x0, int y0, int x1, int y1 ) {
		int dx = x1 - x0;
		int dy = y1 - y0;

		return Math.sqrt( dx * dx + dy * dy );
	}

	public static int distanceSq( int x0, int y0, int x1, int y1 ) {
		int dx = x1 - x0;
		int dy = y1 - y0;

		return dx * dx + dy * dy;
	}

	/**
	 * Finds the point which has the mean location of all the points in the list. This is also known
	 * as the centroid.
	 *
	 * @param list List of points
	 * @param mean Storage for mean point.  If null then a new instance will be declared
	 * @return The found mean
	 */
	public static Point2D_I32 mean(List<Point2D_I32> list, Point2D_I32 mean) {
		if( mean == null )
			mean = new Point2D_I32();

		int sumX = 0, sumY = 0;
		int N = list.size();

		for( int i = 0; i < N; i++ ) {
			Point2D_I32 p = list.get(i);

			sumX += p.x;
			sumY += p.y;
		}

		mean.x = sumX/N;
		mean.y = sumY/N;

		return mean;
	}

	/**
	 * Finds the point which has the mean location of all the points in the list. This is also known
	 * as the centroid.
	 *
	 * @param list List of points
	 * @param mean Storage for mean point.  If null then a new instance will be declared
	 * @return The found mean
	 */
	public static Point2D_F32 mean(List<Point2D_I32> list, Point2D_F32 mean) {
		if (mean == null)
			mean = new Point2D_F32();

		int sumX = 0, sumY = 0;
		int N = list.size();

		for (int i = 0; i < N; i++) {
			Point2D_I32 p = list.get(i);

			sumX += p.x;
			sumY += p.y;
		}

		mean.x = sumX / (float) N;
		mean.y = sumY / (float) N;

		return mean;
	}

	/**
	 * Finds the point which has the mean location of all the points in the list. This is also known
	 * as the centroid.
	 *
	 * @param list List of points
	 * @param mean Storage for mean point.  If null then a new instance will be declared
	 * @return The found mean
	 */
	public static Point2D_F64 mean(List<Point2D_I32> list, Point2D_F64 mean) {
		if (mean == null)
			mean = new Point2D_F64();

		int sumX = 0, sumY = 0;
		int N = list.size();

		for (int i = 0; i < N; i++) {
			Point2D_I32 p = list.get(i);

			sumX += p.x;
			sumY += p.y;
		}

		mean.x = sumX / (double) N;
		mean.y = sumY / (double) N;

		return mean;
	}
}


/*
 * Copyright (C) 2011-2018, Peter Abeles. All Rights Reserved.
 *
 * This file is part of Geometric Regression Library (GeoRegression).
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package georegression.struct;

import org.ejml.UtilEjml;
import org.ejml.ops.MatrixIO;

import java.text.DecimalFormat;
import java.util.Objects;

/**
 * Generic Tuple for geometric objects that store (x,y,z,w)
 *
 * @author Peter Abeles
 */
public abstract class GeoTuple4D_F64 <T extends GeoTuple4D_F64> extends GeoTuple_F64<T> {
	public double x;
	public double y;
	public double z;
	public double w;

	public GeoTuple4D_F64(double x, double y, double z, double w) {
		this.x = x;
		this.y = y;
		this.z = z;
		this.w = w;
	}

	public GeoTuple4D_F64() {
	}

	@Override
	public int getDimension() {
		return 4;
	}

	protected void _set( GeoTuple4D_F64 a ) {
		x = a.x;
		y = a.y;
		z = a.z;
		w = a.w;
	}

	public void set( double x, double y, double z , double w ) {
		this.x = x;
		this.y = y;
		this.z = z;
		this.w = w;
	}

	public boolean isIdentical( double x, double y, double z , double w ) {
		return ( this.x == x && this.y == y && this.z == z && this.w == w);
	}

	public boolean isIdentical( double x, double y, double z, double w , double tol ) {
		return ( Math.abs( this.x - x ) <= tol &&
				Math.abs( this.y - y ) <= tol &&
				Math.abs( this.z - z ) <= tol &&
				Math.abs( this.w - w ) <= tol );
	}

	public boolean isIdentical( GeoTuple4D_F64 t, double tol ) {
		return ( Math.abs( this.x - t.x ) <= tol &&
				Math.abs( this.y - t.y ) <= tol &&
				Math.abs( this.z - t.z ) <= tol &&
				Math.abs( this.w - t.w ) <= tol );
	}

	public double getX() {
		return x;
	}

	public double getY() {
		return y;
	}

	public double getZ() {
		return z;
	}

	public double getW() {
		return w;
	}

	public double getIdx(int index ) {
		switch( index ) {
			case 0: return x;
			case 1: return y;
			case 2: return z;
			case 3: return w;
			default: throw new IllegalArgumentException( "Invalid index" );
		}
	}

	public void setIdx(int index, double value ) {
		switch( index ) {
			case 0: x = value; break;
			case 1: y = value; break;
			case 2: z = value; break;
			case 3: w = value; break;
			default: throw new IllegalArgumentException( "Invalid index "+index );
		}
	}

	public void scale( double scalar ) {
		x *= scalar;
		y *= scalar;
		z *= scalar;
		w *= scalar;
	}

	/**
	 *
In-place addition

	 *
	 * this.x = this.x + a.x;
	 *
	 * @param a value which is to be added
	 */
	public void plusIP( GeoTuple4D_F64 a ) {
		x += a.x;
		y += a.y;
		z += a.z;
		w += a.w;
	}

	public T times( double scalar ) {
		T ret = createNewInstance();
		ret.x = x*scalar;
		ret.y = y*scalar;
		ret.z = z*scalar;
		ret.w = w*scalar;
		return ret;
	}

	/**
	 * In-place scalar multiplication
	 * @param scalar value that it is multiplied by
	 */
	public void timesIP( double scalar ) {
		x *= scalar;
		y *= scalar;
		z *= scalar;
		w *= scalar;
	}

	public void divideIP( double scalar ) {
		x /= scalar;
		y /= scalar;
		z /= scalar;
		w /= scalar;
	}

	public void normalize() {
		divideIP( norm() );
	}

	@Override
	public double norm() {
		return Math.sqrt( x * x + y * y + z * z + w * w);
	}

	@Override
	public double normSq() {
		return x * x + y * y + z * z + w * w;
	}

	@Override
	public double distance( GeoTuple4D_F64 t ) {
		double dx = t.x - x;
		double dy = t.y - y;
		double dz = t.z - z;
		double dw = t.w - w;

		return Math.sqrt( dx * dx + dy * dy + dz * dz + dw * dw);
	}

	@Override
	public double distance2( GeoTuple4D_F64 t ) {
		double dx = t.x - x;
		double dy = t.y - y;
		double dz = t.z - z;
		double dw = t.w - w;

		return dx * dx + dy * dy + dz * dz + dw * dw;
	}

	public void print() {
		System.out.println( this );
	}

	public boolean isNaN() {
		return ( Double.isNaN( x ) || Double.isNaN( y ) || Double.isNaN( z ) || Double.isNaN( w ));
	}

	public void setX( double x ) {
		this.x = x;
	}

	public void setY( double y ) {
		this.y = y;
	}

	public void setZ( double z ) {
		this.z = z;
	}

	public void setW( double w ) {
		this.w = w;
	}

	/**
	 * Returns the absolute value of the component with the largest absolute value
	 * @return max absolute value
	 */
	public double maxAbs() {
		double absX = Math.abs(x);
		double absY = Math.abs(y);
		double absZ = Math.abs(z);
		double absW = Math.abs(w);

		double found = Math.max(absX,absY);
		if( found < absZ )
			found = absZ;
		if( found < absW )
			found = absW;
		return found;
	}

	@Override
	public boolean equals(Object obj) {
		if( this.getClass() != obj.getClass() )
			return false;
		GeoTuple4D_F64 p = (GeoTuple4D_F64)obj;
		return Double.compare(x,p.x)==0 && Double.compare(y,p.y)==0 &&
				Double.compare(z,p.z)==0 && Double.compare(w,p.w)==0;
	}

	protected String toString( String name ) {
		DecimalFormat format = new DecimalFormat("#");
		String sx = UtilEjml.fancyString(x,format, MatrixIO.DEFAULT_LENGTH,4);
		String sy = UtilEjml.fancyString(y,format, MatrixIO.DEFAULT_LENGTH,4);
		String sz = UtilEjml.fancyString(z,format, MatrixIO.DEFAULT_LENGTH,4);
		String sw = UtilEjml.fancyString(w,format, MatrixIO.DEFAULT_LENGTH,4);

		return name+"( " + sx + " " + sy + " " + sz + " " + sw + " )";
	}

	@Override
	public int hashCode() {
		return Objects.hash(x, y, z, w);
	}
}


/*
 * Copyright (C) 2011-2019, Peter Abeles. All Rights Reserved.
 *
 * This file is part of Geometric Regression Library (GeoRegression).
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package georegression.fitting.curves;

import georegression.struct.curve.PolynomialCubic1D_F64;
import georegression.struct.curve.PolynomialQuadratic1D_F64;
import georegression.struct.curve.PolynomialQuadratic2D_F64;
import org.ejml.data.DMatrix3x3;
import org.ejml.data.DMatrix4x4;
import org.ejml.data.DMatrix6x6;
import org.ejml.data.DMatrixRMaj;
import org.ejml.dense.fixed.CommonOps_DDF3;
import org.ejml.dense.fixed.CommonOps_DDF4;
import org.ejml.dense.row.CommonOps_DDRM;
import org.ejml.dense.row.factory.LinearSolverFactory_DDRM;
import org.ejml.interfaces.linsol.LinearSolverDense;
import org.ejml.ops.ConvertDMatrixStruct;

import javax.annotation.Nullable;

/**
 * Fits different types of curves to points
 *
 * @author Peter Abeles
 */
public class FitCurve_F64 {

	/**
	 *
Fits points to the quadratic polynomial. It's recommended that you apply a linear transform to the points
	 * to ensure that they have zero mean and a standard deviation of 1. Then reverse the transform on the result.
	 * A solution is found using the pseudo inverse and inverse by minor matrices.

	 *
	 *
For a more numerically stable algorithm see {@link #fitQRP(double[], int, int, PolynomialQuadratic1D_F64)}

	 *
	 * @param data Interleaved data [input[0], output[0], ....
	 * @param offset first index in data
	 * @param length number of elements in data that are to be read. must be divisible by 2
	 * @param output (Optional) storage for the curve
	 * @return The fitted curve
	 */
	public static boolean fitMM(double[] data, int offset , int length ,
								PolynomialQuadratic1D_F64 output , @Nullable DMatrix3x3 work ) {
		if( work == null )
			work = new DMatrix3x3();

		final int N = length/2;

		// Unrolled pseudo inverse
		// coef = inv(A^T*A)*A^T*y
		double sx0=N,sx1=0,sx2=0;
		double             sx3=0;
		double             sx4=0;

		double b0=0,b1=0,b2=0;

		int end = offset+length;
		for (int i = offset; i < end; i += 2 ) {
			double x = data[i];
			double y = data[i+1];

			double x2 = x*x;

			sx1 += x;
			sx2 += x2;
			sx3 += x2*x;
			sx4 += x2*x2;

			b0 += y;
			b1 += x*y;
			b2 += x2*y;
		}

		DMatrix3x3 A = work;
		A.set(  sx0,sx1,sx2,
				sx1,sx2,sx3,
				sx2,sx3,sx4);

		if( !CommonOps_DDF3.invert(A,A) ) // TODO use a symmetric inverse. Should be slightly faster
			return false;

		// output = inv(A)*B      Unrolled here for speed
		output.a = A.a11*b0 + A.a12*b1 + A.a13*b2;
		output.b = A.a21*b0 + A.a22*b1 + A.a23*b2;
		output.c = A.a31*b0 + A.a32*b1 + A.a33*b2;

		return true;
	}

	/**
	 *
Fits points to the quadratic polynomial using a QRP linear solver.

	 *
	 * @see FitPolynomialSolverTall_F64
	 *
	 * @param data Interleaved data [input[0], output[0], ....
	 * @param offset first index in data
	 * @param length number of elements in data that are to be read. must be divisible by 2
	 * @param output (Optional) storage for the curve
	 * @return The fitted curve
	 */
	public static boolean fitQRP( double[] data, int offset , int length ,
								  PolynomialQuadratic1D_F64 output ) {
		FitPolynomialSolverTall_F64 solver = new FitPolynomialSolverTall_F64();

		return solver.process(data,offset,length,output);
	}

	/**
	 *
Fits points to the cubic polynomial. It's recommended that you apply a linear transform to the points
	 * to ensure that they have zero mean and a standard deviation of 1. Then reverse the transform on the result.
	 * A solution is found using the pseudo inverse and inverse by minor matrices.

	 *
	 *
For a more numerically stable algorithm see {@link #fitQRP(double[], int, int, PolynomialQuadratic1D_F64)}

	 *
	 * @param data Interleaved data [input[0], output[0], ....
	 * @param offset first index in data
	 * @param length number of elements in data that are to be read. must be divisible by 2
	 * @param output (Optional) storage for the curve
	 * @return The fitted curve
	 */
	public static boolean fitMM(double[] data, int offset , int length , PolynomialCubic1D_F64 output , @Nullable DMatrix4x4 A ) {
		if( A == null )
			A = new DMatrix4x4();

		final int N = length/2;
		if( N < 4 )
			throw new IllegalArgumentException("Need at least 4 points and not "+N);

		// Unrolled pseudo inverse
		// coef = inv(A^T*A)*A^T*y
		double sx1 = 0, sx2 = 0, sx3 = 0, sx4 = 0, sx5 = 0, sx6 = 0;

		double b0=0,b1=0,b2=0,b3=0;

		int end = offset+length;
		for (int i = offset; i < end; i += 2 ) {
			double x = data[i];
			double y = data[i+1];

			double x2 = x*x;
			double x3 = x2*x;
			double x4 = x2*x2;

			sx1 += x; sx2 += x2; sx3 += x3;
			sx4 += x4; sx5 += x4*x;
			sx6 += x4*x2;

			b0 += y;
			b1 += x*y;
			b2 += x2*y;
			b3 += x3*y;
		}

		A.set(  N,  sx1,sx2,sx3,
				sx1,sx2,sx3,sx4,
				sx2,sx3,sx4,sx5,
				sx3,sx4,sx5,sx6);

		if( !CommonOps_DDF4.invert(A,A) )// TODO use a symmetric inverse. Should be slightly faster
			return false;

		// output = inv(A)*B      Unrolled here for speed
		output.a = A.a11*b0 + A.a12*b1 + A.a13*b2 + A.a14*b3;
		output.b = A.a21*b0 + A.a22*b1 + A.a23*b2 + A.a24*b3;
		output.c = A.a31*b0 + A.a32*b1 + A.a33*b2 + A.a34*b3;
		output.d = A.a41*b0 + A.a42*b1 + A.a43*b2 + A.a44*b3;

		return true;
	}

	/**
	 *
Fits points to a 2D quadratic polynomial. There are two inputs and one output for each data point.
	 * It's recommended that you apply a linear transform to the points.

	 *
	 * @param data Interleaved data [inputA[0], inputB[0], output[0], ....
	 * @param offset first index in data
	 * @param length number of elements in data that are to be read. must be divisible by 2
	 * @param output (Optional) storage for the curve
	 * @return The fitted curve
	 */
	public static boolean fit(double[] data, int offset , int length , PolynomialQuadratic2D_F64 output ) {

		final int N = length/3;
		if( N < 6 )
			throw new IllegalArgumentException("Need at least 6 points and not "+N);

		// Unrolled pseudo inverse
		// coef = inv(A^T*A)*A^T*y
		double sx1=0, sy1=0, sx1y1=0, sx2=0, sy2=0, sx2y1=0, sx1y2=0, sx2y2=0, sx3=0, sy3=0, sx3y1=0, sx1y3=0, sx4=0, sy4=0;

		double b0=0,b1=0,b2=0,b3=0,b4=0,b5=0;

		int end = offset+length;
		for (int i = offset; i < end; i += 3 ) {
			double x = data[i];
			double y = data[i+1];
			double z = data[i+2];

			double x2 = x*x;
			double x3 = x2*x;
			double x4 = x2*x2;
			double y2 = y*y;
			double y3 = y2*y;
			double y4 = y2*y2;

			sx1 += x; sx2 += x2; sx3 += x3;sx4 += x4;
			sy1 += y; sy2 += y2; sy3 += y3;sy4 += y4;
			sx1y1 += x*y; sx2y1 += x2*y; sx1y2 += x*y2; sx2y2 +=x2*y2;
			sx3y1 += x3*y; sx1y3 += x*y3;

			b0 += z;
			b1 += x*z;
			b2 += y*z;
			b3 += x*y*z;
			b4 += x2*z;
			b5 += y2*z;
		}

		// using a fixed size matrix because the notation is much nicer
		DMatrix6x6 A = new DMatrix6x6();
		A.set(  N    ,sx1,  sy1  ,sx1y1,sx2  ,sy2  ,
				sx1  ,sx2,  sx1y1,sx2y1,sx3  ,sx1y2,
				sy1  ,sx1y1,sy2  ,sx1y2,sx2y1,sy3  ,
				sx1y1,sx2y1,sx1y2,sx2y2,sx3y1,sx1y3,
				sx2  ,sx3  ,sx2y1,sx3y1,sx4  ,sx2y2,
				sy2  ,sx1y2,sy3  ,sx1y3,sx2y2,sy4   );

		DMatrixRMaj _A = new DMatrixRMaj(6,6);
		ConvertDMatrixStruct.convert(A,_A);

		// pseudo inverse is required to handle degenerate matrices, e.g. lines
		LinearSolverDense<DMatrixRMaj> solver = LinearSolverFactory_DDRM.pseudoInverse(true);
		if( !solver.setA(_A) )
			return false;
		solver.invert(_A);

		DMatrixRMaj B = new DMatrixRMaj(6,1,true,b0,b1,b2,b3,b4,b5);
		DMatrixRMaj Y = new DMatrixRMaj(6,1);
		CommonOps_DDRM.mult(_A,B,Y);

		// output = inv(A)*B      Unrolled here for speed
		output.a = Y.data[0];
		output.b = Y.data[1];
		output.c = Y.data[2];
		output.d = Y.data[3];
		output.e = Y.data[4];
		output.f = Y.data[5];

		return true;
	}
}


/*
 * Copyright (C) 2011-2018, Peter Abeles. All Rights Reserved.
 *
 * This file is part of Geometric Regression Library (GeoRegression).
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package georegression.geometry;

import georegression.struct.plane.PlaneNormal3D_F64;
import georegression.struct.point.Point3D_F64;
import georegression.struct.point.Vector3D_F64;
import georegression.struct.shapes.Box3D_F64;

import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;

/**
 *
 *
 */
public class UtilPoint3D_F64 {

	/**
	 * Euclidean distance between the two specified points
	 * @param x0 x-axis on Point 0
	 * @param y0 y-axis on Point 0
	 * @param z0 z-axis on Point 0
	 * @param x1 x-axis on Point 1
	 * @param y1 y-axis on Point 1
	 * @param z1 z-axis on Point 1
	 * @return Euclidean distance
	 */
	public static double distance( double x0 , double y0 , double z0 ,
								   double x1 , double y1 , double z1) {
		return norm(x1-x0,y1-y0,z1-z0);
	}

	/**
	 * Euclidean distance squared between the two specified points
	 * @param x0 x-axis on Point 0
	 * @param y0 y-axis on Point 0
	 * @param z0 z-axis on Point 0
	 * @param x1 x-axis on Point 1
	 * @param y1 y-axis on Point 1
	 * @param z1 z-axis on Point 1
	 * @return Euclidean distance squared
	 */
	public static double distanceSq( double x0 , double y0 , double z0 ,
								   double x1 , double y1 , double z1) {
		double dx = x1-x0;
		double dy = y1-y0;
		double dz = z1-z0;

		return dx*dx + dy*dy + dz*dz;
	}

	public static double norm( double x , double y , double z ) {
		return Math.sqrt(x*x + y*y + z*z);
	}

	public static List<Point3D_F64> copy( List<Point3D_F64> pts ) {
		List<Point3D_F64> ret = new ArrayList<Point3D_F64>();

		for( Point3D_F64 p : pts ) {
			ret.add( p.copy() );
		}

		return ret;
	}

	public static Point3D_F64 noiseNormal( Point3D_F64 mean ,
										   double sigmaX , double sigmaY, double sigmaZ,
										   Random rand ,
										   @Nullable Point3D_F64 output )
	{
		if( output == null )
			output = new Point3D_F64();

		output.x = mean.x + rand.nextGaussian()*sigmaX;
		output.y = mean.y + rand.nextGaussian()*sigmaY;
		output.z = mean.z + rand.nextGaussian()*sigmaZ;

		return output;
	}

	public static void noiseNormal( List<Point3D_F64> pts, double sigma, Random rand ) {
		for( Point3D_F64 p : pts ) {
			p.x += rand.nextGaussian() * sigma;
			p.y += rand.nextGaussian() * sigma;
			p.z += rand.nextGaussian() * sigma;
		}
	}

	/**
	 * Randomly generates a set of points on the plane centered at the plane's origin
	 * using a uniform distribution.
	 *
	 * @param plane Plane
	 * @param max Maximum distance from center
	 * @param rand random number generator
	 * @return set of points on the plane
	 */
	public static List<Point3D_F64> random(PlaneNormal3D_F64 plane , double max , int num, Random rand ) {

		List<Point3D_F64> ret = new ArrayList<>();

		Vector3D_F64 axisX = new Vector3D_F64();
		Vector3D_F64 axisY = new Vector3D_F64();

		UtilPlane3D_F64.selectAxis2D(plane.n,axisX,axisY);

		for (int i = 0; i < num; i++) {
			double x = 2*max*(rand.nextDouble()-0.5);
			double y = 2*max*(rand.nextDouble()-0.5);

			Point3D_F64 p = new Point3D_F64();
			p.x = plane.p.x + axisX.x*x + axisY.x*y;
			p.y = plane.p.y + axisX.y*x + axisY.y*y;
			p.z = plane.p.z + axisX.z*x + axisY.z*y;

			ret.add( p );
		}

		return ret;
	}

	public static List<Point3D_F64> random( double min, double max, int num, Random rand ) {
		List<Point3D_F64> ret = new ArrayList<Point3D_F64>();

		double d = max - min;

		for( int i = 0; i < num; i++ ) {
			Point3D_F64 p = new Point3D_F64();
			p.x = rand.nextDouble() * d + min;
			p.y = rand.nextDouble() * d + min;
			p.z = rand.nextDouble() * d + min;

			ret.add( p );
		}

		return ret;
	}

	/**
	 * Creates a list of random points from a uniform distribution along each axis
	 */
	public static List<Point3D_F64> random(Point3D_F64 mean ,
										   double minX , double maxX ,
										   double minY , double maxY ,
										   double minZ , double maxZ ,
										   int num, Random rand )
	{
		List<Point3D_F64> ret = new ArrayList<>();

		for( int i = 0; i < num; i++ ) {
			Point3D_F64 p = new Point3D_F64();
			p.x = mean.x + rand.nextDouble() * (maxX-minX) + minX;
			p.y = mean.y + rand.nextDouble() * (maxY-minY) + minY;
			p.z = mean.z + rand.nextDouble() * (maxZ-minZ) + minZ;

			ret.add( p );
		}

		return ret;
	}

	public static List<Point3D_F64> random(Point3D_F64 mean ,
										   double min , double max ,
										   int num, Random rand )
	{
		return random(mean,min,max,min,max,min,max,num,rand);
	}

	/**
	 * Creates a list of random points from a normal distribution along each axis
	 */
	public static List<Point3D_F64> randomN(Point3D_F64 mean ,
											double stdX , double stdY , double stdZ ,
											int num, Random rand )
	{
		List<Point3D_F64> ret = new ArrayList<>();

		for( int i = 0; i < num; i++ ) {
			Point3D_F64 p = new Point3D_F64();
			p.x = mean.x + rand.nextGaussian() * stdX;
			p.y = mean.y + rand.nextGaussian() * stdY;
			p.z = mean.z + rand.nextGaussian() * stdZ;

			ret.add( p );
		}

		return ret;
	}

	public static List<Point3D_F64> randomN(Point3D_F64 mean , double stdev, int num, Random rand )
	{
		return randomN(mean,stdev,stdev,stdev,num,rand);
	}

	/**
	 * Computes the mean of the list of points.
	 *
	 * @param points List of points
	 * @param mean (Optional) storage for the mean.  Can be null
	 * @return Mean
	 */
	public static Point3D_F64 mean( List<Point3D_F64> points , Point3D_F64 mean ) {
      if( mean == null )
         mean = new Point3D_F64();

		double x = 0, y = 0, z = 0;

		for( Point3D_F64 p : points ) {
			x += p.x;
			y += p.y;
			z += p.z;
		}

		mean.x = x / points.size();
		mean.y = y / points.size();
		mean.z = z / points.size();

		return mean;
	}

	/**
	 * Computes the mean of the list of points up to element num.
	 *
	 * @param points List of points
	 * @param num use points up to num, exclusive.
	 * @param mean (Optional) storage for the mean.  Can be null
	 * @return Mean
	 */
	public static Point3D_F64 mean( List<Point3D_F64> points , int num , Point3D_F64 mean ) {
		if( mean == null )
			mean = new Point3D_F64();

		double x = 0, y = 0, z = 0;

		for( int i = 0; i < num; i++ ) {
			Point3D_F64 p = points.get(i);
			x += p.x;
			y += p.y;
			z += p.z;
		}

		mean.x = x / num;
		mean.y = y / num;
		mean.z = z / num;

		return mean;
	}

	/**
	 * Finds the minimal volume {@link Box3D_F64} which contains all the points.
	 *
	 * @param points Input: List of points.
	 * @param bounding Output: Bounding box
	 */
	public static void boundingBox(List<Point3D_F64> points, Box3D_F64 bounding) {
		double minX=Double.MAX_VALUE,maxX=-Double.MAX_VALUE;
		double minY=Double.MAX_VALUE,maxY=-Double.MAX_VALUE;
		double minZ=Double.MAX_VALUE,maxZ=-Double.MAX_VALUE;

		for( int i = 0; i < points.size(); i++ ) {
			Point3D_F64 p = points.get(i);
			if( p.x < minX )
				minX = p.x;
			if( p.x > maxX )
				maxX = p.x;
			if( p.y < minY )
				minY = p.y;
			if( p.y > maxY )
				maxY = p.y;
			if( p.z < minZ )
				minZ = p.z;
			if( p.z > maxZ )
				maxZ = p.z;
		}

		bounding.p0.set(minX,minY,minZ);
		bounding.p1.set(maxX, maxY, maxZ);
	}
}


package com.github.toastshaman.dropwizard.auth.jwt;

import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.Timer;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheBuilderSpec;
import com.google.common.cache.CacheStats;
import io.dropwizard.auth.AuthenticationException;
import io.dropwizard.auth.Authenticator;
import org.jose4j.jwt.consumer.JwtContext;

import java.security.Principal;
import java.util.AbstractMap.SimpleEntry;
import java.util.Optional;
import java.util.function.Predicate;

import static com.codahale.metrics.MetricRegistry.name;

public class CachingJwtAuthenticator<P extends Principal> implements Authenticator<JwtContext, P> {

    private final Authenticator<JwtContext, P> authenticator;
    private final Cache<String, SimpleEntry<JwtContext, Optional<P>>> cache;
    private final Meter cacheMisses;
    private final Timer gets;

    /**
     * Creates a new cached authenticator.
     *
     * @param metricRegistry the application's registry of metrics
     * @param authenticator  the underlying authenticator
     * @param cacheSpec      a {@link CacheBuilderSpec}
     */
    public CachingJwtAuthenticator(final MetricRegistry metricRegistry,
                                   final Authenticator<JwtContext, P> authenticator,
                                   final CacheBuilderSpec cacheSpec) {
        this(metricRegistry, authenticator, CacheBuilder.from(cacheSpec));
    }

    /**
     * Creates a new cached authenticator.
     *
     * @param metricRegistry the application's registry of metrics
     * @param authenticator  the underlying authenticator
     * @param builder        a {@link CacheBuilder}
     */
    public CachingJwtAuthenticator(final MetricRegistry metricRegistry,
                                   final Authenticator<JwtContext, P> authenticator,
                                   final CacheBuilder<Object, Object> builder) {
        this.authenticator = authenticator;
        this.cacheMisses = metricRegistry.meter(name(authenticator.getClass(), "cache-misses"));
        this.gets = metricRegistry.timer(name(authenticator.getClass(), "gets"));
        this.cache = builder.recordStats().build();
    }

    @Override
    public Optional<P> authenticate(JwtContext context) throws AuthenticationException {
        final Timer.Context timer = gets.time();
        try {
            final SimpleEntry<JwtContext, Optional<P>> cacheEntry = cache.getIfPresent(context.getJwt());
            if (cacheEntry != null) {
                return cacheEntry.getValue();
            }

            cacheMisses.mark();
            final Optional<P> principal = authenticator.authenticate(context);
            if (principal.isPresent()) {
                cache.put(context.getJwt(), new SimpleEntry<>(context, principal));
            }
            return principal;
        }
        finally { timer.stop(); }
    }

    /**
     * Discards any cached principal for the given credentials.
     *
     * @param credentials a set of credentials
     */
    public void invalidate(JwtContext credentials) {
        cache.invalidate(credentials.getJwt());
    }

    /**
     * Discards any cached principal for the given collection of credentials.
     *
     * @param credentials a collection of credentials
     */
    public void invalidateAll(Iterable<JwtContext> credentials) {
        credentials.forEach(context -> cache.invalidate(context.getJwt()));
    }

    /**
     * Discards any cached principal for the collection of credentials satisfying the given predicate.
     *
     * @param predicate a predicate to filter credentials
     */
    public void invalidateAll(Predicate<? super JwtContext> predicate) {
        cache.asMap().entrySet().stream()
            .map(entry -> entry.getValue().getKey())
            .filter(predicate::test)
            .map(JwtContext::getJwt)
            .forEach(cache::invalidate);
    }

    /**
     * Discards all cached principals.
     */
    public void invalidateAll() {
        cache.invalidateAll();
    }

    /**
     * Returns the number of cached principals.
     *
     * @return the number of cached principals
     */
    public long size() {
        return cache.size();
    }

    /**
     * Returns a set of statistics about the cache contents and usage.
     *
     * @return a set of statistics about the cache contents and usage
     */
    public CacheStats stats() {
        return cache.stats();
    }
}


/**
 * (C) Copyright IBM Corporation 2018.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package net.wasdev.wlp.common.plugins.util;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.security.AccessController;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Scanner;
import java.util.Set;
import java.util.jar.JarFile;
import java.util.logging.Level;
import java.util.regex.MatchResult;

import javax.xml.parsers.ParserConfigurationException;

import org.apache.commons.io.FileUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import net.wasdev.wlp.common.plugins.config.XmlDocument;

/**
 * Utility class to install features from Maven repositories.
 */
public abstract class InstallFeatureUtil {

    public static final String OPEN_LIBERTY_GROUP_ID = "io.openliberty.features";
    public static final String REPOSITORY_RESOLVER_ARTIFACT_ID = "repository-resolver";
    public static final String INSTALL_MAP_ARTIFACT_ID = "install-map";

    private final File installDirectory;

    private final File installJarFile;

    private final List<ProductProperties> propertiesList;

    private final String to;

    private final Set<File> downloadedJsons;

    private static final String INSTALL_MAP_PREFIX = "com.ibm.ws.install.map";
    private static final String INSTALL_MAP_SUFFIX = ".jar";
    private static final String OPEN_LIBERTY_PRODUCT_ID = "io.openliberty";
    private static final int COPY_FILE_TIMEOUT_MILLIS = 5 * 60 * 1000;
    private String openLibertyVersion;

    /**
     * Initialize the utility and check for unsupported scenarios.
     *
     * @param installDirectory The install directory
     * @param from The "from" parameter specified in the plugin configuration, or null if not specified
     * @param to The "to" parameter specified in the plugin configuration, or null if not specified
     * @param pluginListedEsas The list of ESAs specified in the plugin configuration, or null if not specified
     * @throws PluginScenarioException If the current scenario is not supported
     * @throws PluginExecutionException If properties files cannot be found in the installDirectory/lib/versions
     */
    public InstallFeatureUtil(File installDirectory, String from, String to, Set<String> pluginListedEsas) throws PluginScenarioException, PluginExecutionException {
        this.installDirectory = installDirectory;
        this.to = to;
        propertiesList = loadProperties(new File(installDirectory, "lib/versions"));
        installJarFile = loadInstallJarFile(installDirectory);
        if (installJarFile == null) {
            throw new PluginScenarioException("Install map jar not found.");
        }
        downloadedJsons = downloadProductJsons();
        if (downloadedJsons.isEmpty()) {
            throw new PluginScenarioException("Cannot find JSONs for to the installed runtime from the Maven repository.");
        }
        if (hasUnsupportedParameters(from, pluginListedEsas)) {
            throw new PluginScenarioException("Cannot install features from a Maven repository when using the 'to' or 'from' parameters or when specifying ESA files.");
        }
    }

    private File loadInstallJarFile(File installDirectory) {
        if (openLibertyVersion != null) {
            File installJarOverride = downloadOverrideJar(OPEN_LIBERTY_GROUP_ID, INSTALL_MAP_ARTIFACT_ID);
            if (installJarOverride != null && installJarOverride.exists()) {
                return installJarOverride;
            }
        }
        return getMapBasedInstallKernelJar(new File(installDirectory, "lib"));
    }

    /**
     * Log debug
     * @param msg
     */
    public abstract void debug(String msg);

    /**
     * Log debug
     * @param msg
     * @param e
     */
    public abstract void debug(String msg, Throwable e);

    /**
     * Log debug
     * @param e
     */
    public abstract void debug(Throwable e);

    /**
     * Log warning
     * @param msg
     */
    public abstract void warn(String msg);

    /**
     * Log info
     * @param msg
     */
    public abstract void info(String msg);

    /**
     * Returns whether debug is enabled by the current logger
     * @return whether debug is enabled
     */
    public abstract boolean isDebugEnabled();

    /**
     * Download the artifact from the specified Maven coordinates, or retrieve it from the cache if it already exists.
     *
     * @param groupId The group ID
     * @param artifactId The artifact ID
     * @param type The type e.g. esa
     * @param version The version
     * @return The file corresponding to the downloaded artifact
     * @throws PluginExecutionException If the artifact could not be downloaded
     */
    public abstract File downloadArtifact(String groupId, String artifactId, String type, String version)
            throws PluginExecutionException;

    /**
     * Combine the given String collections into a set using case-insensitive matching.
     * If there are multiple instances of the same string but with different capitalization,
     * only the first one found will be included.
     *
     * @param collections a collection of strings
     * @return the combined set of strings, ignoring case
     */
    @SafeVarargs
    public static Set<String> combineToSet(Collection<String>... collections) {
        Set<String> result = new HashSet<String>();
        Set<String> lowercaseSet = new HashSet<String>();
        for (Collection<String> collection : collections) {
            if (collection != null) {
                for (String value : collection) {
                    if (!lowercaseSet.contains(value.toLowerCase())) {
                        lowercaseSet.add(value.toLowerCase());
                        result.add(value);
                    }
                }
            }
        }
        return result;
    }

    /**
     * Get the set of features defined in the server.xml
     * @param serverDirectory The server directory containing the server.xml
     * @return the set of features that should be installed from server.xml, or empty set if nothing should be installed
     */
    public Set<String> getServerFeatures(File serverDirectory) {
        Set<String> result = getConfigDropinsFeatures(null, serverDirectory, "defaults");
        result = getServerXmlFeatures(result, new File(serverDirectory, "server.xml"), null);
        // add the overrides at the end since they should not be replaced by any previous content
        return getConfigDropinsFeatures(result, serverDirectory, "overrides");
    }

    /**
     * Gets features from the configDropins's defaults or overrides directory
     *
     * @param origResult
     *            The features that have been parsed so far.
     * @param serverDirectory
     *            The server directory
     * @param folderName
     *            The folder under configDropins: either "defaults" or
     *            "overrides"
     * @return The set of features to install, or empty set if the cumulatively
     *         parsed xml files only have featureManager sections but no
     *         features to install, or null if there are no valid xml files or
     *         they have no featureManager section
     */
    private Set<String> getConfigDropinsFeatures(Set<String> origResult, File serverDirectory, String folderName) {
        Set<String> result = origResult;
        File configDropinsFolder;
        try {
            configDropinsFolder = new File(new File(serverDirectory, "configDropins"), folderName).getCanonicalFile();
        } catch (IOException e) {
            // skip this directory if its path cannot be queried
            warn("The " + serverDirectory + "/configDropins/" + folderName + " directory cannot be accessed. Skipping its server features.");
            debug(e);
            return result;
        }
        File[] configDropinsXmls = configDropinsFolder.listFiles(new FilenameFilter() {
            @Override
            public boolean accept(File dir, String name) {
                return name.endsWith(".xml");
            }
        });
        if (configDropinsXmls == null || configDropinsXmls.length == 0) {
            return result;
        }
        // sort the files in alphabetical order so that overrides will happen in the proper order
        Comparator<File> comparator = new Comparator<File>() {
            @Override
            public int compare(File left, File right) {
                return left.getAbsolutePath().toLowerCase().compareTo(right.getAbsolutePath().toLowerCase());
            }
        };
        Collections.sort(Arrays.asList(configDropinsXmls), comparator);

        for (File xml : configDropinsXmls) {
            Set<String> features = getServerXmlFeatures(result, xml, null);
            if (features != null) {
                result = features;
            }
        }
        return result;
    }

    /**
     * Adds features from the given server file into the origResult or a new set
     * if origResult is null.
     *
     * @param origResult
     *            The features that have been parsed so far.
     * @param serverFile
     *            The server XML file.
     * @param parsedXmls
     *            The list of XML files that have been parsed so far.
     * @return The set of features to install, or empty set if the cumulatively
     *         parsed xml files only have featureManager sections but no
     *         features to install, or null if there are no valid xml files or
     *         they have no featureManager section
     */
    private Set<String> getServerXmlFeatures(Set<String> origResult, File serverFile, List<File> parsedXmls) {
        Set<String> result = origResult;
        List<File> updatedParsedXmls = parsedXmls != null ? parsedXmls : new ArrayList<File>();
        File canonicalServerFile;
        try {
            canonicalServerFile = serverFile.getCanonicalFile();
        } catch (IOException e) {
            // skip this server.xml if its path cannot be queried
            warn("The server file " + serverFile + " cannot be accessed. Skipping its features.");
            debug(e);
            return result;
        }
        updatedParsedXmls.add(canonicalServerFile);
        if (canonicalServerFile.exists()) {
            try {
                Document doc = new XmlDocument() {
                    public Document getDocument(File file) throws IOException, ParserConfigurationException, SAXException {
                        createDocument(file);
                        return doc;
                    }
                }.getDocument(canonicalServerFile);
                Element root = doc.getDocumentElement();
                NodeList nodes = root.getChildNodes();

                for (int i = 0; i < nodes.getLength(); i++) {
                    if (nodes.item(i) instanceof Element) {
                        Element child = (Element) nodes.item(i);
                        if ("featureManager".equals(child.getNodeName())) {
                            if (result == null) {
                                result = new HashSet<String>();
                            }
                            result.addAll(parseFeatureManagerNode(child));
                        } else if ("include".equals(child.getNodeName())){
                            result = parseIncludeNode(result, canonicalServerFile, child, updatedParsedXmls);
                        }
                    }
                }
            } catch (IOException | ParserConfigurationException | SAXException e) {
                // just skip this server.xml if it cannot be parsed
                warn("The server file " + serverFile + " cannot be parsed. Skipping its features.");
                debug(e);
                return result;
            }
        }
        return result;
    }

    /**
     * Parse features from an include node.
     *
     * @param origResult
     *            The features that have been parsed so far.
     * @param serverFile
     *            The parent server XML file containing the include node.
     * @param node
     *            The include node.
     * @param updatedParsedXmls
     *            The list of XML files that have been parsed so far.
     * @return The set of features to install, or empty set if the cumulatively
     *         parsed xml files only have featureManager sections but no
     *         features to install, or null if there are no valid xml files or
     *         they have no featureManager section
     */
    private Set<String> parseIncludeNode(Set<String> origResult, File serverFile, Element node,
            List<File> updatedParsedXmls) {
        Set<String> result = origResult;
        String includeFileName = node.getAttribute("location");
        if (includeFileName == null || includeFileName.trim().isEmpty()) {
            return result;
        }
        File includeFile = null;
        if (isURL(includeFileName)) {
            try {
                File tempFile = File.createTempFile("serverFromURL", ".xml");
                FileUtils.copyURLToFile(new URL(includeFileName), tempFile, COPY_FILE_TIMEOUT_MILLIS, COPY_FILE_TIMEOUT_MILLIS);
                includeFile = tempFile;
            } catch (IOException e) {
                // skip this xml if it cannot be accessed from URL
                warn("The server file " + serverFile + " includes a URL " + includeFileName + " that cannot be accessed. Skipping the included features.");
                debug(e);
                return result;
            }
        } else {
            includeFile = new File(includeFileName);
        }
        try {
            if (!includeFile.isAbsolute()) {
                includeFile = new File(serverFile.getParentFile().getAbsolutePath(), includeFileName)
                        .getCanonicalFile();
            } else {
                includeFile = includeFile.getCanonicalFile();
            }
        } catch (IOException e) {
            // skip this xml if its path cannot be queried
            warn("The server file " + serverFile + " includes a file " + includeFileName + " that cannot be accessed. Skipping the included features.");
            debug(e);
            return result;
        }
        if (!updatedParsedXmls.contains(includeFile)) {
            String onConflict = node.getAttribute("onConflict");
            Set<String> features = getServerXmlFeatures(null, includeFile, updatedParsedXmls);
            result = handleOnConflict(result, onConflict, features);
        }
        return result;
    }

    private Set<String> handleOnConflict(Set<String> origResult, String onConflict, Set<String> features) {
        Set<String> result = origResult;
        if ("replace".equalsIgnoreCase(onConflict)) {
            if (features != null && !features.isEmpty()) {
                // only replace if the child has features
                result = features;
            }
        } else if ("ignore".equalsIgnoreCase(onConflict)) {
            if (result == null) {
                // parent has no results (i.e. no featureManager section), so use the child's results
                result = features;
            } // else the parent already has some results (even if it's empty), so ignore the child
        } else {
            // anything else counts as "merge", even if the onConflict value is invalid
            if (features != null) {
                if (result == null) {
                    result = features;
                } else {
                    result.addAll(features);
                }
            }
        }
        return result;
    }

    /**
     * Parse feature elements from a featureManager node, trimming whitespace
     * and treating everything as lowercase.
     *
     * @param node
     *            The featureManager node
     * @return Set of trimmed lowercase feature names
     */
    private static Set<String> parseFeatureManagerNode(Element node) {
        Set<String> result = new HashSet<String>();
        NodeList features = node.getElementsByTagName("feature");
        if (features != null) {
            for (int j = 0; j < features.getLength(); j++) {
                String content = features.item(j).getTextContent();
                if (content != null) {
                    if (content.contains(":")) {
                        String[] split = content.split(":", 2);
                        result.add(split[1].trim().toLowerCase());
                    } else {
                        result.add(content.trim().toLowerCase());
                    }
                }
            }
        }
        return result;
    }

    /**
     * Get the JSON files corresponding to the product properties from the lib/versions/*.properties files
     * @return the set of JSON files for the product
     * @throws PluginExecutionException if properties files could not be found from lib/versions
     */
    private Set<File> downloadProductJsons() throws PluginExecutionException {
        // download JSONs
        Set<File> downloadedJsons = new HashSet<File>();
        for (ProductProperties properties : propertiesList) {
            File json = downloadJsons(properties.getId(), properties.getVersion());
            if (json != null) {
                downloadedJsons.add(json);
            }
        }
        return downloadedJsons;
    }

    /**
     * Download the JSON file for the given product.
     *
     * @param productId The product ID from the runtime's properties file
     * @param productVersion The product version from the runtime's properties file
     * @return The JSON file, or null if not found
     */
    private File downloadJsons(String productId, String productVersion) {
        String jsonGroupId = productId + ".features";
        try {
            return downloadArtifact(jsonGroupId, "features", "json", productVersion);
        } catch (PluginExecutionException e) {
            debug("Cannot find json for productId " + productId + ", productVersion " + productVersion, e);
            return null;
        }
    }

    private List<ProductProperties> loadProperties(File dir) throws PluginExecutionException {
        List<ProductProperties> list = new ArrayList<ProductProperties>();

        File[] propertiesFiles = dir.listFiles(new FilenameFilter() {
            @Override
            public boolean accept(File dir, String name) {
                return name.endsWith(".properties");
            }
        });

        if (propertiesFiles != null) {
            for (File propertiesFile : propertiesFiles) {
                Properties properties = new Properties();
                InputStream input = null;
                try {
                    input = new FileInputStream(propertiesFile);
                    properties.load(input);
                    String productId = properties.getProperty("com.ibm.websphere.productId");
                    String productVersion = properties.getProperty("com.ibm.websphere.productVersion");
                    if (productId == null) {
                        throw new PluginExecutionException(
                                "Cannot find the \"com.ibm.websphere.productId\" property in the file "
                                        + propertiesFile.getAbsolutePath()
                                        + ". Ensure the file is valid properties file for the Liberty product or extension.");
                    }
                    if (productVersion == null) {
                        throw new PluginExecutionException(
                                "Cannot find the \"com.ibm.websphere.productVersion\" property in the file "
                                        + propertiesFile.getAbsolutePath()
                                        + ". Ensure the file is valid properties file for the Liberty product or extension.");
                    }
                    if (productId.equals(OPEN_LIBERTY_PRODUCT_ID)) {
                        openLibertyVersion = productVersion;
                    }
                    list.add(new ProductProperties(productId, productVersion));
                } catch (IOException e) {
                    throw new PluginExecutionException("Cannot read the product properties file " + propertiesFile.getAbsolutePath(), e);
                } finally {
                    if (input != null) {
                        try {
                            input.close();
                        } catch (IOException e) {
                        }
                    }
                }
            }
        }

        if (list.isEmpty()) {
            throw new PluginExecutionException("Could not find any properties file in the " + dir
                    + " directory. Ensure the directory " + installDirectory + " contains a Liberty installation.");
        }

        return list;
    }

    private class ProductProperties {
        private String id;
        private String version;

        public ProductProperties(String id, String version) {
            this.id = id;
            this.version = version;
        }

        public String getId() {
            return id;
        }

        public String getVersion() {
            return version;
        }
    }

    /**
     * Returns true if this scenario is not supported for installing from Maven
     * repository, which is one of the following conditions: "from" parameter is
     * specified (don't need Maven repositories), or esa files are specified in
     * the configuration (not supported with Maven for now)
     *
     * @param from
     *            the "from" parameter specified in the plugin
     * @param pluginListedEsas
     *            the ESA files specified in the plugin configuration
     * @return true if the fallback scenario occurred, false otherwise
     */
    private boolean hasUnsupportedParameters(String from, Set<String> pluginListedEsas) {
        boolean hasFrom = from != null;
        boolean hasPluginListedEsas = !pluginListedEsas.isEmpty();
        debug("hasFrom: " + hasFrom);
        debug("hasPluginListedEsas: " + hasPluginListedEsas);
        return hasFrom || hasPluginListedEsas;
    }

    private File downloadEsaArtifact(String mavenCoordinates) throws PluginExecutionException {
        String[] mavenCoordinateArray = mavenCoordinates.split(":");
        String groupId = mavenCoordinateArray[0];
        String artifactId = mavenCoordinateArray[1];
        String version = mavenCoordinateArray[2];
        return downloadArtifact(groupId, artifactId, "esa", version);
    }

    private List<File> downloadEsas(Collection<?> mavenCoordsList) throws PluginExecutionException{
        List<File> repoPaths = new ArrayList<File>();
        for (Object coordinate : mavenCoordsList) {
            repoPaths.add(downloadEsaArtifact((String) coordinate));
        }
        return repoPaths;
    }

    /**
     * Gets the set of all Open Liberty features by scanning the product JSONs.
     *
     * @param jsons The set of product JSON files to scan
     * @return set of all Open Liberty features
     * @throws PluginExecutionException if any of the JSONs could not be found
     */
    public static Set<String> getOpenLibertyFeatureSet(Set<File> jsons) throws PluginExecutionException {
        Set<String> libertyFeatures = new HashSet<String>();
        for (File file : jsons) {
            Scanner s = null;
            try {
                s = new Scanner(file);
                // scan Maven coordinates for artifactIds that belong to the Open Liberty groupId
                while (s.findWithinHorizon(OPEN_LIBERTY_GROUP_ID + ":([^:]*):", 0) != null) {
                    MatchResult match = s.match();
                    if (match.groupCount() >= 1) {
                        libertyFeatures.add(match.group(1));
                    }
                }
            } catch (FileNotFoundException e) {
                throw new PluginExecutionException("The JSON file is not found at " + file.getAbsolutePath(), e);
            } finally {
                if (s != null) {
                    s.close();
                }
            }
        }
        return libertyFeatures;
    }

    /**
     * Returns true if all features in featuresToInstall are Open Liberty features.
     *
     * @param featuresToInstall list of features to check
     * @return true if featureToInstall has only Open Liberty features
     * @throws PluginExecutionException if any of the downloaded JSONs could not be found
     */
    private boolean isOnlyOpenLibertyFeatures(List<String> featuresToInstall) throws PluginExecutionException {
        boolean result = containsIgnoreCase(getOpenLibertyFeatureSet(downloadedJsons), featuresToInstall);
        debug("Is installing only Open Liberty features? " + result);
        return result;
    }

    /**
     * Returns whether the reference collection contains all of the strings in the target collection, ignoring case.
     *
     * @param reference The reference collection
     * @param target The target collection
     * @return true if reference contains all Strings from target, ignoring case
     */
    public static boolean containsIgnoreCase(Collection<String> reference, Collection<String> target) {
        return toLowerCase(reference).containsAll(toLowerCase(target));
    }

    private static Set<String> toLowerCase(Collection<String> strings) {
        Set<String> result = new HashSet<String>(strings.size());
        for (String s : strings) {
            result.add(s.toLowerCase());
        }
        return result;
    }

    /**
     * Resolve, download, and install features from a Maven repository. This
     * method calls the resolver with the given JSONs and feature list,
     * downloads the ESAs corresponding to the resolved features, then installs
     * those features.
     *
     * @param jsonRepos
     *            JSON files, each containing an array of metadata for all
     *            features in a Liberty release.
     * @param featuresToInstall
     *            The list of features to install.
     * @throws PluginExecutionException
     *             if any of the features could not be installed
     */
    @SuppressWarnings("unchecked")
    public void installFeatures(boolean isAcceptLicense, List<String> featuresToInstall) throws PluginExecutionException {
        List<File> jsonRepos = new ArrayList<File>(downloadedJsons);
        debug("JSON repos: " + jsonRepos);
        info("Installing features: " + featuresToInstall);

        // override license acceptance if installing only Open Liberty features
        boolean acceptLicenseMapValue = isOnlyOpenLibertyFeatures(featuresToInstall) ? true : isAcceptLicense;

        try {
            Map<String, Object> mapBasedInstallKernel = createMapBasedInstallKernelInstance(installDirectory);
            mapBasedInstallKernel.put("install.local.esa", true);
            mapBasedInstallKernel.put("single.json.file", jsonRepos);
            mapBasedInstallKernel.put("features.to.resolve", featuresToInstall);
            mapBasedInstallKernel.put("license.accept", acceptLicenseMapValue);

            if (isDebugEnabled()) {
                mapBasedInstallKernel.put("debug", Level.FINEST);
            }

            Collection<?> resolvedFeatures = (Collection<?>) mapBasedInstallKernel.get("action.result");
            if (resolvedFeatures == null) {
                debug("action.exception.stacktrace: "+mapBasedInstallKernel.get("action.exception.stacktrace"));
                String exceptionMessage = (String) mapBasedInstallKernel.get("action.error.message");
                throw new PluginExecutionException(exceptionMessage);
            } else if (resolvedFeatures.isEmpty()) {
                debug("action.exception.stacktrace: "+mapBasedInstallKernel.get("action.exception.stacktrace"));
                String exceptionMessage = (String) mapBasedInstallKernel.get("action.error.message");
                if (exceptionMessage == null) {
                    debug("resolvedFeatures was empty but the install kernel did not issue any messages");
                    info("The features are already installed, so no action is needed.");
                    return;
                } else if (exceptionMessage.contains("CWWKF1250I")) {
                    info(exceptionMessage);
                    info("The features are already installed, so no action is needed.");
                    return;
                } else {
                    throw new PluginExecutionException(exceptionMessage);
                }
            }
            Collection<File> artifacts = downloadEsas(resolvedFeatures);

            StringBuilder installedFeaturesBuilder = new StringBuilder();
            Collection<String> actionReturnResult = new ArrayList<String>();
            for (File esaFile: artifacts ){
                mapBasedInstallKernel.put("license.accept", acceptLicenseMapValue);
                mapBasedInstallKernel.put("action.install", esaFile);
                if (to != null) {
                    mapBasedInstallKernel.put("to.extension", to);
                    debug("Installing to extension: " + to);
                }
                Integer ac = (Integer) mapBasedInstallKernel.get("action.result");
                debug("action.result: "+ac);
                debug("action.error.message: "+mapBasedInstallKernel.get("action.error.message"));
                if (mapBasedInstallKernel.get("action.error.message") != null) {
                    debug("action.exception.stacktrace: "+mapBasedInstallKernel.get("action.exception.stacktrace"));
                    String exceptionMessage = (String) mapBasedInstallKernel.get("action.error.message");
                    debug(exceptionMessage);
                    throw new PluginExecutionException(exceptionMessage);
                } else if (mapBasedInstallKernel.get("action.install.result") != null) {
                    actionReturnResult.addAll((Collection<String>) mapBasedInstallKernel.get("action.install.result"));
                }
            }
            for (String installResult : actionReturnResult) {
                installedFeaturesBuilder.append(installResult).append(" ");
            }
            productInfoValidate();
            info("The following features have been installed: " + installedFeaturesBuilder.toString());
        } catch (PrivilegedActionException e) {
            throw new PluginExecutionException("Could not load the jar " + installJarFile.getAbsolutePath(), e);
        }
    }

    private Map<String, Object> createMapBasedInstallKernelInstance(File installDirectory) throws PrivilegedActionException, PluginExecutionException {
        Map<String, Object> mapBasedInstallKernel = AccessController.doPrivileged(new PrivilegedExceptionAction<Map<String, Object>>() {
            @SuppressWarnings({ "unchecked", "resource" })
            @Override
            public Map<String, Object> run() throws Exception {
                ClassLoader loader = new URLClassLoader(new URL[] { installJarFile.toURI().toURL() }, getClass().getClassLoader());
                Class<Map<String, Object>> clazz;
                clazz = (Class<Map<String, Object>>) loader.loadClass("com.ibm.ws.install.map.InstallMap");
                return clazz.newInstance();
            }
        });
        if (mapBasedInstallKernel == null){
            throw new PluginExecutionException("Cannot run install jar file " + installJarFile);
        }

        // Init
        String bundle = getOverrideBundleDescriptor(OPEN_LIBERTY_GROUP_ID, REPOSITORY_RESOLVER_ARTIFACT_ID);
        if (bundle != null) {
            List<String> bundles = new ArrayList<String>();
            bundles.add(bundle);
            debug("Overriding jar using: " + bundle);
            mapBasedInstallKernel.put("override.jar.bundles", bundles);
        }
        mapBasedInstallKernel.put("runtime.install.dir", installDirectory);
        try {
            mapBasedInstallKernel.put("install.map.jar.file", installJarFile);
            debug("install.map.jar.file: " + installJarFile);
        } catch (RuntimeException e) {
            debug("This version of the install map does not support the key \"install.map.jar.file\"", e);
            String installJarFileSubpath = installJarFile.getParentFile().getName() + File.separator + installJarFile.getName();
            mapBasedInstallKernel.put("install.map.jar", installJarFileSubpath);
            debug("install.map.jar: " + installJarFileSubpath);
        }
        debug("install.kernel.init.code: " + mapBasedInstallKernel.get("install.kernel.init.code"));
        debug("install.kernel.init.error.message: " + mapBasedInstallKernel.get("install.kernel.init.error.message"));
        File usrDir = new File(installDirectory, "usr/tmp");
        mapBasedInstallKernel.put("target.user.directory", usrDir);
        return mapBasedInstallKernel;
    }

    /**
     * Download the override bundle from the repository with the given groupId and
     * artifactId, corresponding to the latest version in the range between the
     * current Open Liberty version (inclusive) and the next version
     * (exclusive). Returns a string in the format "filepath;BundleName" where
     * BundleName is the bundle symbolic name from its manifest.
     *
     * @param groupId
     *            the groupId of the bundle to download
     * @param artifactId
     *            the artifactId of the bundle to download
     * @return a String representing the bundle in filepath;BundleName format
     */
    public String getOverrideBundleDescriptor(String groupId, String artifactId) throws PluginExecutionException {
        File overrideJar = downloadOverrideJar(groupId, artifactId);
        if (overrideJar != null && overrideJar.exists()) {
            String symbolicName = extractSymbolicName(overrideJar);
            if (symbolicName != null) {
                return overrideJar.getAbsolutePath() + ";" + symbolicName;
            }
        }
        return null;
    }

    private File downloadOverrideJar(String groupId, String artifactId) {
        try {
            return downloadArtifact(groupId, artifactId, "jar",
                    String.format("[%s)", openLibertyVersion + ", " + getNextProductVersion(openLibertyVersion)));
        } catch (PluginExecutionException e) {
            debug("Could not find override bundle " + groupId + ":" + artifactId
                    + " for the current Open Liberty version " + openLibertyVersion, e);
            return null;
        }
    }

    /**
     * Gets the next product version number.
     *
     * @param version
     *            the product version
     * @return the String representation of the next product version
     */
    public static String getNextProductVersion(String version) throws PluginExecutionException {
        String result = null;
        int versionSplittingIndex = version.lastIndexOf(".") + 1;
        if (versionSplittingIndex == 0) {
            throw new PluginExecutionException("Product version " + version
                    + " is not in the expected format. It must have period separated version segments.");
        }
        String quarterVersion = version.substring(versionSplittingIndex);
        int nextQuarterSpecifier;
        try {
            nextQuarterSpecifier = Integer.parseInt(quarterVersion) + 1;
        } catch (NumberFormatException e) {
            throw new PluginExecutionException("Product version " + version
                    + " is not in the expected format. Its last segment is expected to be an integer.", e);
        }
        result = version.substring(0, versionSplittingIndex) + nextQuarterSpecifier;
        return result;
    }

    /**
     * Extracts the bundle symbolic name from the jar manifest.
     *
     * @param jar
     *            the jar from which the symbolic name will be extracted
     * @return the Bundle-SymbolicName
     */
    public static String extractSymbolicName(File jar) throws PluginExecutionException {
        JarFile jarFile = null;
        try {
            jarFile = new JarFile(jar);
            return jarFile.getManifest().getMainAttributes().getValue("Bundle-SymbolicName");
        } catch (IOException e) {
            throw new PluginExecutionException("Could not load the jar " + jar.getAbsolutePath(), e);
        } finally {
            if (jarFile != null) {
                try {
                    jarFile.close();
                } catch (IOException e) {
                    // nothing to do here
                }
            }
        }
    }

    /**
     * Find latest install map jar from specified directory
     *
     * @return the install map jar file
     */
    public static File getMapBasedInstallKernelJar(File dir) {

        File[] installMapJars = dir.listFiles(new FilenameFilter() {
            @Override
            public boolean accept(File dir, String name) {
                return name.startsWith(INSTALL_MAP_PREFIX) && name.endsWith(INSTALL_MAP_SUFFIX);
            }
        });

        File result = null;
        if (installMapJars != null) {
            for (File jar : installMapJars) {
                if (isReplacementJar(result, jar)) {
                    result = jar;
                }
            }
        }

        return result;
    }

    /**
     * Returns whether file2 can replace file1 as the install map jar.
     *
     * @param file1
     * @param file2
     * @return true if file2 is a replacement jar for file1 false otherwise
     */
    private static boolean isReplacementJar(File file1, File file2) {
        if (file1 == null) {
            return true;
        } else if (file2 == null) {
            return false;
        } else {
            String version1 = extractVersion(file1.getName());
            String version2 = extractVersion(file2.getName());
            return compare(version1, version2) < 0;
        }
    }

    /**
     * Returns the extracted version from fileName
     *
     * @param fileName
     * @return the version extracted from fileName
     */
    private static String extractVersion(String fileName) {
        int startIndex = INSTALL_MAP_PREFIX.length() + 1; // skip the underscore after the prefix
        int endIndex = fileName.lastIndexOf(INSTALL_MAP_SUFFIX);
        if (startIndex < endIndex) {
            return fileName.substring(startIndex, endIndex);
        } else {
            return null;
        }
    }

    /**
     * Performs pairwise comparison of version strings, including nulls and non-integer components.
     * @param version1
     * @param version2
     * @return positive if version1 is greater, negative if version2 is greater, otherwise 0
     */
    private static int compare(String version1, String version2) {
        if (version1 == null && version2 == null) {
            return 0;
        } else if (version1 == null && version2 != null) {
            return -1;
        } else if (version1 != null && version2 == null) {
            return 1;
        }
        String[] components1 = version1.split("\\.");
        String[] components2 = version2.split("\\.");
        for (int i = 0; i < components1.length && i < components2.length; i++) {
            int comparison;
            try {
                comparison = new Integer(components1[i]).compareTo(new Integer(components2[i]));
            } catch (NumberFormatException e) {
                comparison = components1[i].compareTo(components2[i]);
            }
            if (comparison != 0) {
                return comparison;
            }
        }
        return components1.length - components2.length;
    }

    /**
     * Performs product validation by running bin/productInfo validate
     *
     * @throws PluginExecutionException
     *             if product validation failed or could not be run
     */
    private void productInfoValidate() throws PluginExecutionException {
        String output = productInfo(installDirectory, "validate");
        if (output == null) {
            throw new PluginExecutionException(
                    "Could not perform product validation. The productInfo command returned with no output");
        } else if (output.contains("[ERROR]")) {
            throw new PluginExecutionException(output);
        } else {
            info("Product validation completed successfully.");
        }
    }

    /**
     * Runs the productInfo command and returns the output
     *
     * @param installDirectory The directory of the installed runtime
     * @param action The action to perform for the productInfo command
     * @return The command output
     * @throws PluginExecutionException if the exit value of the command was not 0
     */
    public static String productInfo(File installDirectory, String action) throws PluginExecutionException {
        Process pr = null;
        InputStream is = null;
        Scanner s = null;
        Worker worker = null;
        try {
            String command;
            if (OSUtil.isWindows()) {
                command = installDirectory + "\\bin\\productInfo.bat " + action;
            } else {
                command = installDirectory + "/bin/productInfo " + action;
            }
            pr = Runtime.getRuntime().exec(command);
            worker = new Worker(pr);
            worker.start();
            worker.join(300000);
            if (worker.exit == null) {
                throw new PluginExecutionException("productInfo command timed out");
            }
            int exitValue = pr.exitValue();
            if (exitValue != 0) {
                throw new PluginExecutionException("productInfo exited with return code " + exitValue);
            }

            is = pr.getInputStream();
            s = new Scanner(is);
            // use regex to match the beginning of the input
            s.useDelimiter("\\A");
            if (s.hasNext()) {
                return s.next();
            }
            return null;
        } catch (IOException ex) {
            throw new PluginExecutionException("productInfo error: " + ex);
        } catch (InterruptedException ex) {
            worker.interrupt();
            Thread.currentThread().interrupt();
            throw new PluginExecutionException("productInfo error: " + ex);
        } finally {
            if (s != null) {
                s.close();
            }
            if (is != null) {
                try {
                    is.close();
                } catch (IOException e) {
                }
            }
            if (pr != null) {
                pr.destroy();
            }
        }
    }

    private static class Worker extends Thread {
        private final Process process;
        private Integer exit;

        private Worker(Process process) {
            this.process = process;
        }

        public void run() {
            try {
                exit = process.waitFor();
            } catch (InterruptedException ignore) {
                return;
            }
        }
    }

    private static boolean isURL(String url) {
        try {
            new URL(url);
            return true;
        } catch (MalformedURLException ex) {
            return false;
        }
    }

}


/**
 * (C) Copyright IBM Corporation 2018.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package net.wasdev.wlp.common.plugins.util;

import java.io.File;
import java.io.IOException;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.JarEntry;
import java.util.jar.Manifest;

import java.util.Enumeration;

public class SpringBootUtil {

    public static final String BOOT_VERSION_ATTRIBUTE = "Spring-Boot-Version";
    public static final String BOOT_START_CLASS_ATTRIBUTE = "Start-Class";
    public static final String BOOT_JAR_EXPRESSION = "BOOT-INF/lib/spring-boot-\\d[\\.]\\d[\\.]\\d.RELEASE.jar";
    public static final String BOOT_WAR_EXPRESSION = "WEB-INF/lib/spring-boot-\\d[\\.]\\d[\\.]\\d.RELEASE.jar";

    /**
     * Check whether the given artifact is a Spring Boot Uber JAR
     * @param artifact
     * @return true if so, false otherwise
     */
    public static boolean isSpringBootUberJar(File artifact) {
        if (artifact == null || !artifact.exists() || !artifact.isFile()) {
            return false;
        }

        try (JarFile jarFile = new JarFile(artifact)) {
            Manifest manifest = jarFile.getManifest();
            if (manifest != null) {
                Attributes attributes = manifest.getMainAttributes();
                if(attributes.getValue(BOOT_VERSION_ATTRIBUTE) != null
                        && attributes.getValue(BOOT_START_CLASS_ATTRIBUTE) != null) {
                    return true;
                } else { //Checking that there is a spring-boot-VERSION.RELEASE.jar in the BOOT-INF/lib directory
                         //Handles the Gradle case where the spring plugin does not set the properties in the manifest
                    Enumeration<JarEntry> entries = jarFile.entries();
                    while(entries.hasMoreElements()) {
                        JarEntry entry = entries.nextElement();
                        String entryName = entry.getName();
                        if (!entryName.startsWith("org") && (entryName.matches(BOOT_JAR_EXPRESSION) || entryName.matches(BOOT_WAR_EXPRESSION))) {
                            return true;
                        }
                    }
                }
            }
        } catch (IOException e) {}
        return false;
    }

}