package pointstream;

import geometry.DoublePoint;
import geometry.DoubleVector;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Stroke;
import java.awt.geom.GeneralPath;
import java.awt.geom.Line2D;
import java.awt.geom.QuadCurve2D;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;

import main.ShoeLaceFrame;

public class LinearPointStream implements GuideStreamControlled
{
	ArrayList<GeneratedPoint> points;
	int nextIndex;
	
	int type;
	
	double responsiveness;
	float thickness;
	
	boolean speedInfluenced;
	
	public static final int LINEAR_STREAM = 1;
	public static final int QUADRIC_STREAM = 2;
	public static final int CIRCLE_STREAM = 3;
	public static final int LAPLACIAN_STREAM = 4;
	
	Color streamColor;
	
	public static double DISTANCE_THRESHOLD = 0.01;
	
	DoublePoint lastGuidePoint = null;
	
	public static boolean DRAW_CONNECTING_CURVE = true;
	
	boolean curvaturesCalculated;
	
	public LinearPointStream(Color sColor, float thickness, double responsiveness, boolean isSpeedInfluenced)
	{
		this.streamColor = sColor;
		this.thickness = thickness;
		this.responsiveness = responsiveness;
		this.speedInfluenced = isSpeedInfluenced;
		points = new ArrayList<GeneratedPoint>();
		nextIndex = 0;
		type = LINEAR_STREAM;
		curvaturesCalculated = false;
	}
	
	public synchronized int size()
	{
		return points.size();
	}
	
	public int getNextIndex()
	{
		return nextIndex;
	}
	
	public int getType()
	{
		return this.type;
	}
	
	public synchronized DoublePoint getPoint(int i)
	{
		return points.get(i);
	}
	
	public double getResponsiveness() 
	{
		return this.responsiveness;
	}
	
	protected double getAppropriateResponsiveness(GuidePoint nextGuidePoint)
	{
		if(!this.speedInfluenced)
			return responsiveness;
		else
		{
			if(GuideStream.USE_CURVATURE_SPEED)
			{
				double nextSpeed = nextGuidePoint.getSpeed();
				
				if(nextSpeed > 0.2)
					nextSpeed = 0.2;
				
				if(nextSpeed < -1.0)
					nextSpeed = -1.0;
				
				double a = -0.7/1.2;
				double b = 0.8 + a;
				double speedResp = a*nextSpeed + b;
				
//				System.out.println("Responsiveness : " + speedResp);
				
				return speedResp;
			}
			else
			{
				double nextSpeed = nextGuidePoint.getSpeed();
				if(nextSpeed > 2.0)
					nextSpeed = 2.0;
				
				if(nextSpeed < 0.0)
					nextSpeed = 0.0;
				
				double a = -0.7/2.0;
				double b = 0.8;
				double speedResp = a*nextSpeed + b;
				
//				System.out.println("Responsiveness : " + speedResp);
				
				return speedResp;
			}
		}
	}
	
	/**
	 * 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(GeneratedPoint 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 incrementNextIndex()
	{
		nextIndex++;
	}
	
	protected synchronized void addPoint(GeneratedPoint p)
	{
		this.points.add(p);
	}
	
	public synchronized void clearCurve()
	{
		this.points.clear();
		this.nextIndex = 0;
	}
	
	public synchronized void writeCurveAttributes(BufferedWriter w)
	{
		try {
			w.write(type + " " + responsiveness + " " + thickness + " " + streamColor.getRed() + " " + streamColor.getGreen() + " " + streamColor.getBlue() + " " + streamColor.getAlpha() + " " + speedInfluenced);
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	public synchronized void writeCurveToMayaFormat(BufferedWriter w)
	{
		try {
			w.write("1 " + (this.size() - 1) + " 0 no 3");
			w.newLine();
			w.write(""+this.size());
			for(int i = 0; i < this.size(); i++)
			{
				w.write(" " + i);
			}
			w.newLine();
			w.write(""+this.size());
			w.newLine();
			
			if(this.size() == 1)
			{
				GeneratedPoint p = (GeneratedPoint)this.getPoint(0);
				w.write(p.getX() + " " + p.getY() + " 0.00000000");
				w.newLine();
				w.write(p.getX() + " " + p.getY() + " 0.00000000");
				w.newLine();
			}
			else
			{
				for(GeneratedPoint p : this.points)
				{
					w.write(p.getX() + " " + p.getY() + " 0.00000000");
					w.newLine();
				}
			}
			
			w.newLine();
			w.write(";");
			w.newLine();
			
			
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	public synchronized void onGuideCurveChanged(GuideStream s) 
	{
		ArrayList<GeneratedPoint> oldPoints = new ArrayList<GeneratedPoint>();
		
		for(GeneratedPoint p : this.points)
			oldPoints.add(p);
		
		int numPoints = this.size();
		
		clearCurve();
		
		for(int i = 0; i < numPoints; i++)
		{
			GeneratedPoint oldPoint = oldPoints.get(i);
			addPoint(s,true, oldPoint.isContinueToEndPoint(), oldPoint.getGuidePointIndex());
		}
	}
	
	public synchronized void pollForResults()
	{
		int contPointNum = 0;
		int regPointNum = 0;
		
		for(GeneratedPoint p : points)
		{
			if(p.isContinueToEndPoint())
				contPointNum++;
			else
				regPointNum++;
		}
		
		System.out.println("Regular Points : " + regPointNum);
		System.out.println("Continue To End Points : " + contPointNum);
	}
	
	public synchronized void calculateCurvatures()
	{
		if(curvaturesCalculated)
			return;
		
		curvaturesCalculated = true;
		
		FileWriter w = null;
		
		try {
			w = new FileWriter(new File("Curvatures" + System.currentTimeMillis() + ".txt"));
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
		BufferedWriter writer = new BufferedWriter(w);
		
		for(int i = 1; i < this.size() - 1; i++)
		{
			try {
				writer.write(i + " " + getCurvature(i));
				writer.newLine();
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
			
		try {
			writer.close();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	protected double getCurvature(int i)
	{
		if(i == 0 || i == this.size() - 1)
			return 0.0;
		
		DoubleVector v1 = new DoubleVector(this.getPoint(i-1), this.getPoint(i));
		DoubleVector v2 = new DoubleVector(this.getPoint(i), this.getPoint(i+1));
		
		double denom = Math.sqrt(v1.length()*v2.length());
		v1.unit();
		v2.unit();
		double theta = Math.acos(v1.dot(v2));
		double numerat = 2*Math.sin(theta/2.0);
		
		return numerat/denom;
	}
	
	/**
     * Add a point to this stream using qStream as a guide curve and with the responsiveness provided.  If continueToEnd is true, if q(i+1) does not exist, then q(i+1) = qN. 
     * Returns true if a point was added.
     * @param qStream
     * @param responsiveness
     * @param continueToEnd
     * @return
     */
    public synchronized boolean addPoint(GuideStream qStream, boolean continueToEnd, boolean forceContinueToEnd, int forceContinueToEndQIndex)
    {
    	if(qStream == null || qStream.size() == 0)
    		return false;
    	
    	if(qStream.size() > 1 && this.size() > 0)
    	{
    		int index = this.size() - 1;
    		int qIndexUsed = -1;
    		boolean isConnectingPoint = false;
    		DoublePoint pi = this.getPoint(index);
    		GuidePoint qiPlus1;

    		if(this.getNextIndex() < qStream.size() && !forceContinueToEnd)
    		{
    			qiPlus1 = qStream.getPoint(this.getNextIndex());
    			qIndexUsed = this.getNextIndex();
    			this.incrementNextIndex();
    		}
    		else
    		{
    			if(continueToEnd)
    			{
    				if(forceContinueToEnd)
    				{
    					qiPlus1 = qStream.getPoint(forceContinueToEndQIndex);
    					qIndexUsed = forceContinueToEndQIndex;
    				}
    				else
    				{
    					qiPlus1 = qStream.getPoint(qStream.size()-1);
    					qIndexUsed = qStream.size()-1;
    				}
    				
					isConnectingPoint = true;
    			}
				else
				{
					return false;
				}
    		}
    		
    		DoublePoint diff = qiPlus1.subtract(pi);
    		DoublePoint s = diff.multiply(this.getAppropriateResponsiveness(qiPlus1));
    		DoublePoint result = pi.add(s);
    		
    		if(result.distanceTo(pi) >= DISTANCE_THRESHOLD)
    		{
    			if(qIndexUsed == -1)
    				System.out.println("Bad New Bears...");
    			
    			//lastGuidePoint = qStream.getPoint(qIndexUsed+1);
    			lastGuidePoint = qiPlus1;
    			
    			this.addPoint(new GeneratedPoint(result, this.thickness/2.0f, qIndexUsed,isConnectingPoint));
    			return true;
    		}
    		else
    			return false;
    	}
    	else
    	{
    		if(this.size() < 1)
    		{
    			//lastGuidePoint = qStream.getPoint(1);
    			lastGuidePoint = qStream.getPoint(0); 
    			
    			this.addPoint(new GeneratedPoint(qStream.getPoint(0), this.thickness/2.0f, 0,false));
    			this.incrementNextIndex();
    			return true;
    		}
    		else
    			return false;
    	}
    }
	
	public synchronized void writeToFile(BufferedWriter writer)
	{
		for(DoublePoint p : points)
		{
			try {
				writer.write(p.getX() + " " + p.getY());
				writer.newLine();
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}
	
//	public synchronized void renderToSVG(BufferedWriter writer, boolean drawPoints) throws IOException
//	{
//		//Add DRAW_CONNECTING_STROKE stuff.
//		
//		if(this.points.size() > 0)
//		{
//			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();
//			
//			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(this.lastGuidePoint != null && DRAW_CONNECTING_CURVE)
//			{
//				GeneratedPoint lastPoint = this.points.get(this.size() - 1);
//				
//				writer.write("<path");
//				writer.newLine();
//				
//				writer.write(" style=\"fill:none;stroke-dasharray:20 20;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)/2.0)+"\"");
//				writer.newLine();
//				
//				writer.write(" d=\"m " + lastPoint.getX() + "," + (-lastPoint.getY()));
//				writer.write(" L " + lastGuidePoint.getX() + "," + (-lastGuidePoint.getY()));
//				
//				writer.write("\"");
//				writer.newLine();
//				
//				writer.write("/> ");
//				writer.newLine();
//			}
//		}
//		
//		if(drawPoints)
//		{
//			for(GeneratedPoint p : points)
//			{
//				p.renderToSVG(writer);
//			}
//		}	
//	}
	
	public synchronized void render(Graphics2D g2D, boolean drawPoints)
	{
		Color old = g2D.getColor();
		Stroke befStroke = g2D.getStroke();
		
		g2D.setColor(streamColor);
		g2D.setStroke(new BasicStroke(this.thickness, ShoeLaceFrame.STROKE_CAP_TYPE, ShoeLaceFrame.STROKE_JOIN_TYPE));
		
		//System.out.println("Height : " + height) ;
		
		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(),-p1.getY(),p2.getX(),-p2.getY()); //-y to reflect back into pixel plane. 
				
				path.append(line, true);
			}
			
			GeneratedPoint lastPoint = this.points.get(this.size() - 1);
			if(this.lastGuidePoint != null && DRAW_CONNECTING_CURVE)
			{
				float dash1[] = {20.0f};
				BasicStroke dashed = new BasicStroke(this.thickness, ShoeLaceFrame.STROKE_CAP_TYPE, ShoeLaceFrame.STROKE_JOIN_TYPE, 20.0f, dash1, 0.0f);
				Line2D.Double line = new Line2D.Double(lastPoint.getX(),-lastPoint.getY(),lastGuidePoint.getX(),-lastGuidePoint.getY());
				Stroke oldStroke = g2D.getStroke();
				g2D.setStroke(dashed);
				Color oColor = new Color(streamColor.getRed(),streamColor.getGreen(), streamColor.getBlue(), streamColor.getAlpha()/2);
				g2D.setColor(oColor);
				g2D.draw(line);
				g2D.setStroke(oldStroke);
				g2D.setColor(streamColor);
			}
				
			//Yannick : Can use fill as well.
			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);
				
				//Yannick : REMOVE THIS STUFF !!!
//				float dash1[] = {10.0f};
//				BasicStroke dashed = new BasicStroke(4.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_BEVEL, 10.0f, dash1, 0.0f);
//				Line2D.Double line2 = new Line2D.Double(p1.getX(),-p1.getY(),lastGuidePoint.getX(),-lastGuidePoint.getY());
//				Stroke oldStroke = g2D.getStroke();
//				g2D.setStroke(dashed);
//				Color oColor = new Color(streamColor.getRed(),streamColor.getGreen(), streamColor.getBlue(), streamColor.getAlpha()/2);
//				g2D.setColor(oColor);
//				g2D.draw(line2);
//				g2D.setStroke(oldStroke);
//				g2D.setColor(streamColor);
			}
		}
		
		if(drawPoints)
		{
			for(GeneratedPoint p : points)
			{
				p.render(g2D);
			}
		}
		
		g2D.setStroke(befStroke);
		g2D.setColor(old);
	}
}
