/*
 * 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.LineString;
import org.locationtech.jts.operation.valid.IsValidOp;

/**
 * 
 * This class is used to create a line string 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 LineStringGenerator extends GeometryGenerator {
	protected int numberPoints = 2;
	protected int generationAlgorithm = 0;
	
	/**
	 * Create the points in a vertical line
	 */
	public static final int VERT = 1;
	
	/**
	 * Create the points in a horizontal line
	 */
	public static final int HORZ = 2;
	
	/**
	 * Create the points in an approximation of an open circle (one edge will not be included).
	 * 
	 * Note: this requires the number of points to be greater than 2.
	 * 
	 * @see #getNumberPoints()
	 * @see #setNumberPoints(int)
	 */
	public static final int ARC = 0;
	
	/**
	 * Number of iterations attempting to create a valid line string
	 */
	private static final int RUNS = 5;

	/**
	 * As the user increases the number of points, the probability of creating a random valid linestring 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 #VERT
	 * @see #HORZ
	 * @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<2){
			throw new IllegalStateException("Too few points");
		}
		
		Coordinate[] coords = new Coordinate[numberPoints];

		double x = boundingBox.getMinX(); // base x
		double dx = boundingBox.getMaxX()-x;
		
		double y = boundingBox.getMinY(); // base y
		double dy = boundingBox.getMaxY()-y;
		
		
		for(int i=0;i<RUNS;i++){
			switch(getGenerationAlgorithm()){
			case VERT:
				fillVert(x,dx,y,dy,coords,geometryFactory);
				break;
			case HORZ:
				fillHorz(x,dx,y,dy,coords,geometryFactory);
				break;
			case ARC:
				fillArc(x,dx,y,dy,coords,geometryFactory);
				break;
			default:
				throw new IllegalStateException("Invalid Alg. Specified");
			}
			
			LineString ls = geometryFactory.createLineString(coords);
			IsValidOp valid = new IsValidOp(ls);
			if(valid.isValid()){
				return ls;
			}
		}
		return null;
	}
	
	private static void fillVert(double x, double dx, double y, double dy, Coordinate[] coords, GeometryFactory gf){
		double fx = x+Math.random()*dx;
		double ry = dy; // remainder of y distance
		coords[0] = new Coordinate(fx,y);
		gf.getPrecisionModel().makePrecise(coords[0]);
		for(int i=1;i<coords.length-1;i++){
			ry -= Math.random()*ry;
			coords[i] = new Coordinate(fx,y+dy-ry);
			gf.getPrecisionModel().makePrecise(coords[i]);
		}
		coords[coords.length-1] = new Coordinate(fx,y+dy);
		gf.getPrecisionModel().makePrecise(coords[coords.length-1]);
	}
	
	private static void fillHorz(double x, double dx, double y, double dy, Coordinate[] coords, GeometryFactory gf){
		double fy = y+Math.random()*dy;
		double rx = dx; // remainder of x distance
		coords[0] = new Coordinate(x,fy);
		gf.getPrecisionModel().makePrecise(coords[0]);
		for(int i=1;i<coords.length-1;i++){
			rx -= Math.random()*rx;
			coords[i] = new Coordinate(x+dx-rx,fy);
			gf.getPrecisionModel().makePrecise(coords[i]);
		}
		coords[coords.length-1] = new Coordinate(x+dx,fy);
		gf.getPrecisionModel().makePrecise(coords[coords.length-1]);
	}
	
	private static void fillArc(double x, double dx, double y, double dy, Coordinate[] coords, GeometryFactory gf){
		if(coords.length == 2)
			throw new IllegalStateException("Too few points for Arc");
		
		double theta = 360/coords.length;
		double start = theta/2;
		
		double radius = dx<dy?dx/3:dy/3;
		
		double cx = x+(dx/2); // center
		double cy = y+(dy/2); // center
		
		for(int i=0;i<coords.length;i++){
			double angle = Math.toRadians(start+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]);
		}
	}

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

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

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

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