package pointstream;

import geometry.Circle;
import geometry.DoublePoint;
import geometry.DoubleVector;
import geometry.NotACircleException;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.geom.Ellipse2D;
import java.awt.geom.GeneralPath;
import java.awt.geom.Line2D;
import java.io.BufferedWriter;
import java.io.IOException;
import java.util.ArrayList;

import main.ShoeLaceFrame;

public class GuideStream
{
	Color streamColor,pointColor;
	
	private ArrayList<GuidePoint> points;
	
	private long timeBetweenPoints, pauseTime;
	private boolean isPaused;
	private boolean isFinalized;
	
	private boolean exitedSnappingZone;
	
	private boolean forceAcceptAllPoints;
	
	GuideStreamControlled controlledCurve;
	
	private float thickness;
	
	public static final boolean USE_CURVE_SNAPPING = true;
	public static final boolean RENDER_SNAPPING_ZONE = USE_CURVE_SNAPPING && false;
	public static final boolean USE_TIMING_FOR_GUIDE_CURVE = true;
	public static final boolean USE_DISTANCE_FOR_GUIDE_CURVE = !USE_TIMING_FOR_GUIDE_CURVE;
	public static final boolean USE_CURVATURE_SPEED = USE_DISTANCE_FOR_GUIDE_CURVE && false;
	
	public GuideStream(GuideStreamControlled controlledCurve, long timeBetweenPoints, long pauseTime, float thickness, Color c)
	{
		this.streamColor = c;
		
		this.controlledCurve = controlledCurve;
		
		this.timeBetweenPoints = timeBetweenPoints;
		this.pauseTime = pauseTime;
		this.isPaused = false;
		this.isFinalized = false;
		
		this.exitedSnappingZone = false;
		this.forceAcceptAllPoints = false;
		
		this.thickness = thickness;
		
		points = new ArrayList<GuidePoint>();
	}
	
	public synchronized int size()
	{
		return points.size();
	}
	
	public boolean isPaused()
	{
		return this.isPaused;
	}
	
	public boolean isFinalized()
	{
		return this.isFinalized;
	}
	
	public boolean isForceAcceptAllPoints() {
		return forceAcceptAllPoints;
	}

	public void setForceAcceptAllPoints(boolean forceAcceptAllPoints) {
		this.forceAcceptAllPoints = forceAcceptAllPoints;
	}
	
	public synchronized void writeToFile(BufferedWriter writer)
	{	
		try
		{
			writer.write(timeBetweenPoints + " " + pauseTime);
			writer.newLine();
			controlledCurve.writeCurveAttributes(writer);
			writer.newLine();
		}
		catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
		for(GuidePoint p : points)
		{
			try {
				writer.write(p.getX() + " " + p.getY() + " " + p.getAdditionTime() + " " + p.getPixelX() + " " + p.getPixelY() + " " + p.isContinueToEndPoint()+ " " + p.getSpeed());
				writer.newLine();
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}
	
	/**
	 * Returns the index of the guide point (if there was one), that was clicked.  Will return -1 if there exists no guide point clicked.
	 * @param p
	 * @return
	 */
	public synchronized int getClickedPoint(DoublePoint p)
	{
		ArrayList<DoublePoint> clickedPoints = new ArrayList<DoublePoint>();
		
		for(GuidePoint g : points)
		{
			if(g.contains(p))
				clickedPoints.add(g);
		}
		
		if(clickedPoints.isEmpty())
			return -1;
		else
		{
			double minDist = Double.MAX_VALUE;
			int minIndex = Integer.MAX_VALUE;
		
			int index = 0;
			for(DoublePoint g : clickedPoints)
			{
				if(g.distanceTo(p) < minDist)
				{
					minDist = g.distanceTo(p);
					minIndex = index;
				}
				
				index++;
			}
			
			return this.points.indexOf(clickedPoints.get(minIndex));
		}
	}
	
	public synchronized void setPoint(DoublePoint origP, DoublePoint newP)
	{
		this.setPoint(this.points.indexOf(origP), newP);
	}
	
	public synchronized void setPoint(int i, DoublePoint p)
	{
		if(i < 0 || i >= this.points.size())
			return;
		
		long time = this.points.get(i).getAdditionTime();
		
		//Yannick : This method needs some fixing as PIXELX and PIXELY will likely move upon an edit, affecting the speed
		//          of the point as well.
		int pixelX = this.points.get(i).getPixelX();
		int pixelY = this.points.get(i).getPixelY();
		double speed = this.points.get(i).getSpeed();
		float radius = this.points.get(i).getRadius();
		
		this.points.set(i, new GuidePoint(p, radius, time, pixelX, pixelY, speed));
		
		this.controlledCurve.onGuideCurveChanged(this);
	}
	
	public synchronized GuidePoint getPoint(int i)
	{
		return points.get(i);
	}
	
	public synchronized void finalizeStream()
	{
		if(this.isFinalized)
			return;
		
		// Finalization should only happen once.
		this.isFinalized = true;
		ShoeLaceFrame.getInstance().setConsoleText(" ");
		
		if(this.USE_CURVE_SNAPPING && this.points.size() > 2 && this.points.get(0).equals(this.points.get(this.points.size() - 1)) && this.controlledCurve.getResponsiveness() < 0.3)
		{
			//  Closed curve, so check to see if we should make tangents equal as well.
			GuidePoint p0 = this.points.get(0);
			
//			int numCentroidPoints = 5;
//			double sumX = 0;
//			double sumY = 0;
//			for(int i = 1; i < numCentroidPoints + 1; i++)
//			{
//				sumX += this.points.get(i).getX();
//				sumY += this.points.get(i).getY();
//			}
//			
//			DoublePoint p1 = new DoublePoint(sumX/(double)numCentroidPoints, sumY/(double)numCentroidPoints);
//			
//			sumX = 0;
//			sumY = 0;
//			for(int i = 1; i < numCentroidPoints + 1; i++)
//			{
//				sumX += this.points.get(this.points.size() - 1 - i).getX();
//				sumY += this.points.get(this.points.size() - 1 - i).getY();
//			}
//			
//			DoublePoint pN = new DoublePoint(sumX/(double)numCentroidPoints, sumY/(double)numCentroidPoints);
			
			DoublePoint p1 = this.points.get(1);
			DoublePoint pN = this.points.get(this.size() - 2);
			
			DoubleVector v1 = new DoubleVector(p0, p1);
			v1.unit();
			DoubleVector v2 = new DoubleVector(p0, pN);
			double distanceToLastPoint = v2.length();
			v2.unit();
			
			double newX = 0;
			double newY = 0;
			boolean haveNewPosition = false;
			
			double angleTolerance = 0.25;
			
			if(Math.abs(v1.dot(v2) - 1) < angleTolerance)
			{
				//System.out.println("Cos = 1 case.");
				newX = p0.getX() + distanceToLastPoint*v1.getX();
				newY = p0.getY() + distanceToLastPoint*v1.getY();
				haveNewPosition = true;
			}
			
			if(Math.abs(v1.dot(v2) + 1) < angleTolerance)
			{
				//System.out.println("Cos = -1 case.");
				newX = p0.getX() - distanceToLastPoint*v1.getX();
				newY = p0.getY() - distanceToLastPoint*v1.getY();
				haveNewPosition = true;
			}
			
			v1 = v1.perp();
			
			if(Math.abs(v1.dot(v2) - 1) < angleTolerance)
			{
				//System.out.println("Cos = 1 perp case.");
				newX = p0.getX() + distanceToLastPoint*v1.getX();
				newY = p0.getY() + distanceToLastPoint*v1.getY();
				haveNewPosition = true;
			}
			
			if(Math.abs(v1.dot(v2) + 1) < angleTolerance)
			{
				//System.out.println("Cos = -1 perp case.");
				newX = p0.getX() - distanceToLastPoint*v1.getX();
				newY = p0.getY() - distanceToLastPoint*v1.getY();
				haveNewPosition = true;
			}
			
			if(!haveNewPosition)
				return;
			
			double moveX = newX - pN.getX();
			double moveY = newY - pN.getY();
			
			double factor = 1.0f;
			int index = this.points.size() - 2;
			
			while(factor > 0.05 && index > 0)
			{
				GuidePoint curPoint = this.points.get(index);
				
				newX = curPoint.getX() + factor*moveX;
				newY = curPoint.getY() + factor*moveY;
				DoublePoint newPosition = new DoublePoint(newX, newY);
				
				this.points.set(index, new GuidePoint(newPosition, curPoint.getRadius(), curPoint.getAdditionTime(), curPoint.getPixelX(), curPoint.getPixelY(), curPoint.getSpeed()));
				
				index--;
				factor = (0.75)*factor;
			}
			
			this.controlledCurve.onGuideCurveChanged(this);
		}
	}
	
	/**
	 * Calculates the speed at a new point in pixels/mS
	 * @param newPX
	 * @param newPY
	 * @param newAddTime
	 * @return
	 */
	private synchronized double findCurvatureSpeed(double newX, double newY, int newPX, int newPY, long newAddTime)
	{
		if(this.size() > 2)
		{
			int numPoints = this.points.size();
			
			Circle c;
			double max_radius = 800;
			double radius;
			try 
			{
				if(this.size() >= 4)
				{
					Circle initCircle = new Circle(this.points.get(numPoints-1).getX(), this.points.get(numPoints-1).getY(),this.points.get(numPoints-2).getX(), this.points.get(numPoints-2).getY(),this.points.get(numPoints-3).getX(), this.points.get(numPoints-3).getY());
					double pointsX[] = new double[5];
					double pointsY[] = new double[5];
					
					pointsX[0] = newX;
					pointsY[0] = newY;
					
					for(int i = 0; i < 4; i++)
					{
						pointsX[i+1] = this.getPoint(this.size()-1-i).getX();
						pointsY[i+1] = this.getPoint(this.size()-1-i).getY();
					}
					
					c = new Circle(pointsX, pointsY, initCircle);
				}
				else
					c = new Circle(this.points.get(numPoints-1).getX(), this.points.get(numPoints-1).getY(),this.points.get(numPoints-2).getX(), this.points.get(numPoints-2).getY(),this.points.get(numPoints-3).getX(), this.points.get(numPoints-3).getY());
				
				radius = c.getRadius();
				
				if(radius > max_radius)
					radius = max_radius;
				
			} catch (NotACircleException e) {
				radius = max_radius;
				//return findLinearSpeed(newPX, newPY, newAddTime);
			}
			
			double curvature = 1.0/radius;
			double curvatureSpeed = 0.2/(Math.pow(curvature, 1.0/3.0));
			double measuredSpeed = findSpeed(newPX, newPY, newAddTime);
			
			double intendedSpeed = measuredSpeed - curvatureSpeed;
			
//			System.out.println("Radius : " + radius + " Measured Speed : "  + measuredSpeed + " Curvature Speed : " + curvatureSpeed + " Intended Speed : " + intendedSpeed);
			
			return intendedSpeed;//findLinearSpeed(newPX, newPY, newAddTime); //Yannick : Change this....
		}
		else
			return 0;//findLinearSpeed(newPX, newPY, newAddTime);
	}
	
	private synchronized double findSpeed(int newPX, int newPY, long newAddTime)
	{
		double speed = 0.0;
		
		if(this.size() > 0)
		{
			GuidePoint lastPoint = this.points.get(this.size() - 1);
			double lengthTravelled = new DoubleVector(newPX - lastPoint.getPixelX(), newPY - lastPoint.getPixelY()).length();
			double timeTaken = (double)(newAddTime - lastPoint.getAdditionTime()); 
			
			speed = lengthTravelled / timeTaken;
		}
		
		if(Double.isInfinite(speed))
			return 9999;
		else
			return speed;
	}
	
	public synchronized boolean addPoint(DoublePoint p, long time, int px, int py)
	{	
		if(this.isForceAcceptAllPoints())
		{
			double speed = this.findSpeed(px, py, time);
			this.points.add(new GuidePoint(p, this.thickness/2.0f, time, px, py, speed));
			return true;
		}
		else
		{
			if(this.points.isEmpty())
			{
				this.points.add(new GuidePoint(p, this.thickness/2.0f, time, px, py, 0));
				this.isPaused = false;
				return true;
			}
			else
			{
				long timingDelta = time - this.points.get(this.size() - 1).getAdditionTime();
				
				if(this.points.get(this.size() - 1).equals(p))
				{
					if(timingDelta > this.pauseTime)
					{
						this.isPaused = true;
						this.points.get(this.size() - 1).setContinueToEndPoint(true);
					}
					
					return false;  //  Make sure not to add points too close to each other.
				}
				else
				{
					if(USE_TIMING_FOR_GUIDE_CURVE)
					{
						if(timingDelta > this.timeBetweenPoints)
						{
							//  Need to add a guide point.
							if(USE_CURVE_SNAPPING && this.points.size() > 0 && p.distanceTo(this.points.get(0)) < this.points.get(0).getSnapToRadius() && this.exitedSnappingZone)
							{		
								ShoeLaceFrame.getInstance().setConsoleText("You are in a snapping zone. Releasing the cursor will create a closed curve.");
								
								if(!this.points.get(0).equals(this.points.get(this.points.size() - 1)))
								{
									double speed = this.findSpeed(px, py, time);
									this.points.add(new GuidePoint(this.points.get(0), this.thickness/2.0f, time, px, py, speed));
								}
							}
							else
							{
								ShoeLaceFrame.getInstance().setConsoleText(" ");
								
								if(p.distanceTo(this.points.get(0)) > this.points.get(0).getSnapToRadius())
									this.exitedSnappingZone = true;
								
								double speed = this.findSpeed(px, py, time);
								this.points.add(new GuidePoint(p, this.thickness/2.0f, time, px, py, speed));
							}
							
							this.isPaused = false;
							return true;
						}
						else
						{
							return false;
						}
					}
					
					if(USE_DISTANCE_FOR_GUIDE_CURVE)
					{
						//System.out.println("Distance : " + this.points.get(this.size() - 1).distanceTo(p));
						double distanceBetweenPoints = ShoeLaceFrame.getInstance().guideCurveDistanceSampleSlider.getSyncValue();;
						
						if(this.points.get(this.size() - 1).distanceTo(p) > distanceBetweenPoints)
						{
							//  Need to add a guide point.
//							if(USE_CURVE_SNAPPING && this.points.size() > 0 && p.distanceTo(this.points.get(0)) < this.points.get(0).getSnapToRadius() && this.exitedSnappingZone)
//							{		
//								ShoeLaceFrame.getInstance().setConsoleText("You are in a snapping zone. Releasing the cursor will create a closed curve.");
//								
//								if(!this.points.get(0).equals(this.points.get(this.points.size() - 1)))
//								{
//									double speed = this.findSpeed(px, py, time);
//									this.points.add(new GuidePoint(this.points.get(0), this.thickness/2.0f, time, px, py, speed));
//								}
//							}
//							else
//							{
								ShoeLaceFrame.getInstance().setConsoleText(" ");
								
//								if(p.distanceTo(this.points.get(0)) > this.points.get(0).getSnapToRadius())
//									this.exitedSnappingZone = true;
								
								//Determine the distance to the cursor.
								double distanceToCursor = this.points.get(this.size() - 1).distanceTo(p);
								
								//Get the data from the last point we'll use in the interpolation.
								double lastPX = this.points.get(this.size() - 1).getX();
								double lastPY = this.points.get(this.size() - 1).getY();
								long lastTime = this.points.get(this.size() - 1).getAdditionTime();
								int lastPixelX = this.points.get(this.size() - 1).getPixelX();
								int lastPixelY = this.points.get(this.size() - 1).getPixelY();
								double lastSpeed = this.points.get(this.size() - 1).getMeasuredSpeed();
								double lastIntendedSpeed = this.points.get(this.size() - 1).getIntendedSpeed();
								
								double speedToReach = this.findSpeed(px, py, time);
								double intendedSpeedToReach = this.findCurvatureSpeed(p.getX(), p.getY(), px, py, time);
								
								//Determine how many point we must add.
								int pointsToAdd = (int)(distanceToCursor/distanceBetweenPoints);
								
//								System.out.println("Last Speed : " + lastIntendedSpeed);
//								System.out.println("Speed to Reach : " + intendedSpeedToReach);
//								System.out.println("Points to add : " + pointsToAdd);
								
								for(int i = 1; i <= pointsToAdd; i++)
								{
									// What fraction of the distance was covered ?
									double fractionCovered = (i*distanceBetweenPoints)/distanceToCursor;
									
//									System.out.println("Fraction Covered : " + fractionCovered);
									
									double newPX = lastPX - fractionCovered*(lastPX - p.getX());
									double newPY = lastPY - fractionCovered*(lastPY - p.getY());
									long newTime = lastTime + Math.round(fractionCovered*(time - lastTime));
									int newPixelX = lastPixelX + (int)Math.round(fractionCovered*(px - lastPixelX));
								    int newPixelY = lastPixelY + (int)Math.round(fractionCovered*(py - lastPixelY));
								    
//								    System.out.println("Last Time : " + lastTime + " Time : " + time + " New Time : " + newTime);
								    
								    double newSpeed = lastSpeed + fractionCovered*(speedToReach - lastSpeed);//this.findSpeed(newPixelX, newPixelY, newTime);
								    
								    GuidePoint newGuide = new GuidePoint(new DoublePoint(newPX, newPY), this.thickness/2.0f, newTime, newPixelX, newPixelY, newSpeed);
								    
								    if(USE_CURVATURE_SPEED)
								    {
								    	double newIntendedSpeed = lastIntendedSpeed + fractionCovered*(intendedSpeedToReach - lastIntendedSpeed);
//								    	System.out.println("Adding Speed : " + newIntendedSpeed);
								    	newGuide.setIntendedSpeed(newIntendedSpeed/*this.findCurvatureSpeed(newPX, newPY, newPixelX, newPixelY, newTime)*/);
								    }
								    
								    this.points.add(newGuide);
								}
								
//								double speed = this.findSpeed(px, py, time);
//								this.points.add(new GuidePoint(p, this.thickness/2.0f, time, px, py, speed));
//							}
							
							this.isPaused = false;
							return true;
						}
						else
						{
							return false;
						}
					}
					
					//This case should never occur.
					return false;
					
				}
			}
		}
	}
	
//	public synchronized void renderToSVG(BufferedWriter writer, boolean drawPoints) throws IOException
//	{
//		writer.write("<path");
//		writer.newLine();
//		
//		writer.write(" style=\"fill:none;stroke:#"+ShoeLaceFrame.getInstance().getColorHexString(this.streamColor)+";stroke-width:"+(int)this.thickness+"px;stroke-linecap:round;stroke-linejoin:bevel;stroke-opacity:"+this.streamColor.getAlpha()/255.0+"\"");
//		writer.newLine();
//		
//		if(this.points.size() > 0)
//		{
//			writer.write(" d=\"m " + this.points.get(0).getX() + "," + (-this.points.get(0).getY()));
//			for(int i = 0; i < this.points.size(); i++)
//				writer.write(" L " + this.points.get(i).getX() + "," + (-this.points.get(i).getY()));
//		}
//		writer.write("\"");
//		writer.newLine();
//		
//		writer.write("/> ");
//		writer.newLine();
//		
//		if(drawPoints)
//		{
//			for(GuidePoint p : points)
//			{
//				p.renderToSVG(writer);
//			}
//		}	
//	}
	
	public synchronized void renderSnappingZone(Graphics2D g2D)
	{
		Color old = g2D.getColor();
		g2D.setColor(new Color(0,0,1,0.5f));
		
		if(this.points.size() > 0)
		{
			GuidePoint firstPoint = this.points.get(0);
			float radius = firstPoint.getSnapToRadius();
			
			Ellipse2D cir = new Ellipse2D.Double(firstPoint.getX()-radius, -firstPoint.getY()-radius, 2*radius, 2*radius);//-y to reflect back into pixel plane. 
			g2D.fill(cir);
		}
		
		g2D.setColor(old);
	}
	
	public synchronized void render(Graphics2D g2D, boolean drawPoints/*, double height*/)
	{
		Color old = g2D.getColor();
		
		if(RENDER_SNAPPING_ZONE)
			renderSnappingZone(g2D);
		
		g2D.setColor(streamColor);
		g2D.setStroke(new BasicStroke(this.thickness, ShoeLaceFrame.STROKE_CAP_TYPE, ShoeLaceFrame.STROKE_JOIN_TYPE));
		
		if(points.size() > 1)
		{
			GeneralPath path = new GeneralPath(GeneralPath.WIND_EVEN_ODD);
			
			for(int i = 0; i < points.size() - 1; i++)
			{
				DoublePoint p1 = this.points.get(i);
				DoublePoint p2 = this.points.get(i+1);
				Line2D.Double line = new Line2D.Double(p1.getX(),/*height - */-p1.getY(),p2.getX(),/*height - */-p2.getY());
				
				path.append(line, false);
			}
			
			g2D.draw(path);
		}
		else
		{
			if(points.size() > 0)
			{
				DoublePoint p1 = this.points.get(0);
				Line2D.Double line = new Line2D.Double(p1.getX(),-p1.getY(),p1.getX(),-p1.getY());//-y to reflect back into pixel plane.
				g2D.draw(line);	
			}
		}
		
		if(drawPoints)
		{
			for(GuidePoint p : points)
			{
				p.render(g2D/*,height*/);
			}
		}
		
		g2D.setColor(old);
	}
}
