/*
 * Copyright (c) 2016 Vivid Solutions.
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * and Eclipse Distribution License v. 1.0 which accompanies this distribution.
 * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html
 * and the Eclipse Distribution License is available at
 *
 * http://www.eclipse.org/org/documents/edl-v10.php.
 */
package org.locationtech.jts.generator;

import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.LinearRing;
import org.locationtech.jts.geom.Polygon;
import org.locationtech.jts.operation.valid.IsValidOp;

/**
 * 
 * This class is used to create a polygon within the specified bounding box.
 * 
 * Successive calls to create may or may not return the same geometry topology.
 *
 * @author David Zwiers, Vivid Solutions. 
 */
public class PolygonGenerator extends GeometryGenerator {
	protected int numberPoints = 4;
	protected int numberHoles = 0;
	protected int generationAlgorithm = 0;
	
	/**
	 * Creates rectangular polygons
	 */
	public static final int BOX = 0;
	
	/**
	 * Creates polygons whose points will not be rectangular when there are more than 4 points 
	 */
	public static final int ARC = 1;
	
	private static final int RUNS = 5;
	
	/**
	 * As the user increases the number of points, the probability of creating a random valid polygon decreases. 
	 * Please take not of this when selecting the generation style, and the number of points. 
	 * 
	 * May return null if a geometry could not be created.
	 * 
	 * @see #getNumberPoints()
	 * @see #setNumberPoints(int)
	 * @see #getGenerationAlgorithm()
	 * @see #setGenerationAlgorithm(int)
	 * 
	 * @see #BOX
	 * @see #ARC
	 * 
	 * @see org.locationtech.jts.generator.GeometryGenerator#create()
	 * 
	 * @throws IllegalStateException When the alg is not valid or the number of points is invalid
	 * @throws NullPointerException when either the Geometry Factory, or the Bounding Box are undefined.
	 */
	public Geometry create() {

		if(geometryFactory == null){
			throw new NullPointerException("GeometryFactory is not declared");
		}
		if(boundingBox == null || boundingBox.isNull()){
			throw new NullPointerException("Bounding Box is not declared");
		}
		if(numberPoints<4){
			throw new IllegalStateException("Too few points");
		}
		
		double x = boundingBox.getMinX(); // base x
		double dx = boundingBox.getMaxX()-x;
		
		double y = boundingBox.getMinY(); // base y
		double dy = boundingBox.getMaxY()-y;
		
		Polygon p = null;
		
		for(int i=0;i<RUNS;i++){
			switch(getGenerationAlgorithm()){
			case BOX:
				p = createBox(x,dx,y,dy,numberHoles,numberPoints,geometryFactory);
				break;
			case ARC:
				p = createArc(x,dx,y,dy,numberHoles,numberPoints,geometryFactory);
				break;
			default:
				throw new IllegalStateException("Invalid Alg. Specified");
			}
			
			IsValidOp valid = new IsValidOp(p);
			if(valid.isValid()){
				return p;
			}
		}
		return null;
	}
	
	private static Polygon createArc(double x, double dx, double y, double dy, int nholes, int npoints, GeometryFactory gf){
		// make outer ring first
		double radius = dx<dy?dx/3:dy/3;
		
		double cx = x+(dx/2); // center
		double cy = y+(dy/2); // center
		
		LinearRing outer = createArc(cx,cy,radius,npoints,gf);
		
		if(nholes == 0){
			return gf.createPolygon(outer,null);
		}
		
		LinearRing[] inner = new LinearRing[nholes];
		
		radius *= .75;
		int degreesPerHole = 360/(nholes+1);
		int degreesPerGap = degreesPerHole/nholes;
		degreesPerGap = degreesPerGap<2?2:degreesPerGap;
		degreesPerHole = (360-(degreesPerGap*nholes))/nholes;
		
		if(degreesPerHole < 2)
			throw new RuntimeException("Slices too small for poly. Use Box alg.");
		
		int start = degreesPerGap/2;
		for(int i=0;i<nholes;i++){
			int st = start+(i*(degreesPerHole+degreesPerGap)); // start angle 
			inner[i] = createTri(cx,cy,st,st+degreesPerHole,radius,gf);
		}
		
		
		return gf.createPolygon(outer,inner);
	}
	
	private static LinearRing createTri(double cx, double cy ,int startAngle, int endAngle, double radius, GeometryFactory gf){

		Coordinate[] coords = new Coordinate[4];
		
		double fx1,fx2,fy1,fy2;
		
		double angle = Math.toRadians(startAngle);
		fx1 = Math.sin(angle)*radius; // may be neg.
		fy1 = Math.cos(angle)*radius; // may be neg.
		
		angle = Math.toRadians(endAngle);
		fx2 = Math.sin(angle)*radius; // may be neg.
		fy2 = Math.cos(angle)*radius; // may be neg.
		
		coords[0] = new Coordinate(cx,cy);
		gf.getPrecisionModel().makePrecise(coords[0]);
		coords[1] = new Coordinate(cx+fx1,cy+fy1);
		gf.getPrecisionModel().makePrecise(coords[1]);
		coords[2] = new Coordinate(cx+fx2,cy+fy2);
		gf.getPrecisionModel().makePrecise(coords[2]);
		coords[3] = new Coordinate(cx,cy);
		gf.getPrecisionModel().makePrecise(coords[3]);
		
		return gf.createLinearRing(coords);
	}
	
	private static LinearRing createArc(double cx, double cy ,double radius, int npoints, GeometryFactory gf){

		Coordinate[] coords = new Coordinate[npoints+1];
		
		double theta = 360/npoints;
		
		for(int i=0;i<npoints;i++){
			double angle = Math.toRadians(theta*i);
			
			double fx = Math.sin(angle)*radius; // may be neg.
			double fy = Math.cos(angle)*radius; // may be neg.
			
			coords[i] = new Coordinate(cx+fx,cy+fy);
			gf.getPrecisionModel().makePrecise(coords[i]);
		}
		
		coords[npoints] = new Coordinate(coords[0]);
		gf.getPrecisionModel().makePrecise(coords[npoints]);
		
		return gf.createLinearRing(coords);
	}
	
	private static Polygon createBox(double x, double dx, double y, double dy, int nholes, int npoints, GeometryFactory gf){
		// make outer ring first
		LinearRing outer = createBox(x,dx,y,dy,npoints,gf);
		
		if(nholes == 0){
			return gf.createPolygon(outer,null);
		}
		
		LinearRing[] inner = new LinearRing[nholes];
		
		int nrow = (int)Math.sqrt(nholes);
		int ncol = nholes/nrow;
		
		double ddx = dx/(ncol+1);
		double ddy = dy/(nrow+1);
		
		// spacers
		double spx = ddx/(ncol+1);
		double spy = ddy/(nrow+1);
		
		// should have more grids than required
		int cindex = 0;
		for(int i=0;i<nrow;i++){
			for(int j=0;j<ncol;j++){
				if(cindex<nholes){
					// make another box
					int pts = npoints/2;
					pts = pts<4?4:pts;
					
					inner[cindex++] = createBox(spx+x+j*(ddx+spx),ddx,spy+y+i*(ddy+spy),ddy,pts,gf);
				}
			}
		}
		
		return gf.createPolygon(outer,inner);
	}
	
	private static LinearRing createBox(double x, double dx, double y, double dy, int npoints, GeometryFactory gf){

		//figure out the number of points per side
		int ptsPerSide = npoints/4;
		int rPtsPerSide = npoints%4;
		Coordinate[] coords = new Coordinate[npoints+1];
		coords[0] = new Coordinate(x,y); // start
		gf.getPrecisionModel().makePrecise(coords[0]);
		
		int cindex = 1;
		for(int i=0;i<4;i++){ // sides
			int npts = ptsPerSide+(rPtsPerSide-->0?1:0);
			// npts atleast 1
			
			if(i%2 == 1){ // odd vert
				double cy = dy/npts;
				if(i > 1) // down
					cy *=-1;
				double tx = coords[cindex-1].x;
				double sy = coords[cindex-1].y;
				
				for(int j=0;j<npts;j++){
					coords[cindex] = new Coordinate(tx,sy+(j+1)*cy);
					gf.getPrecisionModel().makePrecise(coords[cindex++]);
				}
			}else{ // even horz
				double cx = dx/npts;
				if(i > 1) // down
					cx *=-1;
				double ty = coords[cindex-1].y;
				double sx = coords[cindex-1].x;
				
				for(int j=0;j<npts;j++){
					coords[cindex] = new Coordinate(sx+(j+1)*cx,ty);
					gf.getPrecisionModel().makePrecise(coords[cindex++]);
				}
			}
		}
		coords[npoints] = new Coordinate(x,y); // end
		gf.getPrecisionModel().makePrecise(coords[npoints]);
		
		return gf.createLinearRing(coords);
	}

	/**
	 * @return Returns the generationAlgorithm.
	 */
	public int getGenerationAlgorithm() {
		return generationAlgorithm;
	}

	/**
	 * @param generationAlgorithm The generationAlgorithm to set.
	 */
	public void setGenerationAlgorithm(int generationAlgorithm) {
		this.generationAlgorithm = generationAlgorithm;
	}

	/**
	 * @return Returns the numberHoles.
	 */
	public int getNumberHoles() {
		return numberHoles;
	}

	/**
	 * @param numberHoles The numberHoles to set.
	 */
	public void setNumberHoles(int numberHoles) {
		this.numberHoles = numberHoles;
	}

	/**
	 * @return Returns the numberPoints.
	 */
	public int getNumberPoints() {
		return numberPoints;
	}

	/**
	 * @param numberPoints The numberPoints to set.
	 */
	public void setNumberPoints(int numberPoints) {
		this.numberPoints = numberPoints;
	}
	
}
