package pointstream;

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

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics2D;
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.IOException;
import java.util.ArrayList;

import main.MathHelper;
import main.NoIntersectionException;
import main.ShoeLaceFrame;

public class QuadricPointStream extends LinearPointStream 
{
	private ArrayList<DoubleVector> pointDerivatives;
	private ArrayList<DoublePoint> controlPoints;
	private ArrayList<DoublePoint> renderPoints;
	
	private ArrayList<DoublePoint> connectingCurveRenderPoints;
	
	public QuadricPointStream(Color c, float thickness, double responsiveness, boolean isSpeedInfluenced)
	{
		super(c, thickness, responsiveness, isSpeedInfluenced);
		this.pointDerivatives = new ArrayList<DoubleVector>();
		this.controlPoints = new ArrayList<DoublePoint>();
		this.renderPoints = new ArrayList<DoublePoint>();
		
		this.connectingCurveRenderPoints = new ArrayList<DoublePoint>();
		
		this.type = QUADRIC_STREAM;
	}
	
	public synchronized int controlPointSize()
	{
		return this.controlPoints.size();
	}
	
	public synchronized int pointDerivativeSize()
	{
		return this.pointDerivatives.size();
	}
	
	public synchronized DoublePoint getControlPoint(int i)
	{
		return this.controlPoints.get(i);
	}
	
	public synchronized DoubleVector getPointDerivative(int i)
	{
		return this.pointDerivatives.get(i);
	}
	
	public synchronized void addPointDerivative(DoubleVector v)
	{
		this.pointDerivatives.add(v);
	}
	
	public synchronized void addControlPoint(DoublePoint p)
	{
		this.controlPoints.add(p);
	}
	
	public synchronized void addRenderPoint(DoublePoint p)
	{
		if(this.renderPoints.isEmpty())
			this.renderPoints.add(p);
		else
		{
			if(this.renderPoints.get(this.renderPoints.size() - 1).distanceTo(p) >= 1/*CubicCurveMouse.DISTANCE_THRESHOLD*/)
			{
				this.renderPoints.add(p);
			}
		}
	}
	
	public synchronized void clearCurve()
	{
		super.clearCurve();
		this.renderPoints.clear();
	}
	
	public synchronized void writeCurveToMayaFormat(BufferedWriter w)
	{
		try {
			w.write("1 " + (this.renderPoints.size() - 1) + " 0 no 3");
			w.newLine();
			w.write(""+this.renderPoints.size());
			for(int i = 0; i < this.renderPoints.size(); i++)
			{
				w.write(" " + i);
			}
			w.newLine();
			w.write(""+this.renderPoints.size());
			w.newLine();
			
			if(this.size() == 1)
			{
				DoublePoint p = this.renderPoints.get(0);
				w.write(p.getX() + " " + p.getY() + " 0.00000000");
				w.newLine();
				w.write(p.getX() + " " + p.getY() + " 0.00000000");
				w.newLine();
			}
			else
			{
				for(DoublePoint p : this.renderPoints)
				{
					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();
		}
	}
	
	/**
     * 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;
    	
    	QuadricPointStream pQuadStream = this;
    	
    	if(qStream.size() > 2 && pQuadStream.size() > 1)
    	{
    		int index = pQuadStream.size() - 1;
    		int qIndexUsed = -1;
    		boolean isConnectingPoint = false;
    		DoublePoint pi = pQuadStream.getPoint(index);
    		DoublePoint piMinus1 = pQuadStream.getPoint(index-1);
    		
    		GuidePoint qiPlus1,qi;
    		
    		if(pQuadStream.getNextIndex() < qStream.size() && !forceContinueToEnd)
    		{
    			qiPlus1 = qStream.getPoint(pQuadStream.getNextIndex());
    			qIndexUsed = this.getNextIndex();
    			pQuadStream.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;
    		}
    		
    		double neededResponsiveness = this.getAppropriateResponsiveness(qiPlus1);
    		
//    		qi = qStream.getPoint(qIndexUsed-1);
//    		
//    		DoubleVector tangent = pQuadStream.getPointDerivative(index);//new DoubleVector(piMinus1,pi);
////    		System.out.println("Tangent : " + tangent + " index : " + index);
//    		
//    		tangent.unit();
//    		tangent.multiply(qi.distanceTo(qiPlus1));
//    		
//    		DoublePoint b = new DoublePoint(tangent.getX(), tangent.getY());
//    		DoublePoint a = qiPlus1.subtract(pi).subtract(b);
//    		DoublePoint c = pi;
//    		
//    		DoublePoint part1 = b.multiply(neededResponsiveness);
//    		DoublePoint part2 = a.multiply(neededResponsiveness*neededResponsiveness);
//    		
//    		DoublePoint result = c.add(part1).add(part2);
//    		
//    		part1 = a.multiply(2*neededResponsiveness);
//    		DoubleVector derivative = new DoubleVector(part1.getX() + b.getX(), part1.getY() + b.getY());
//    		derivative.unit();
    		
    		DoublePoint a = qiPlus1.add(piMinus1).subtract((pi.multiply(2)));
    		DoublePoint b = pi.subtract(piMinus1);
    		DoublePoint c = pi;
    		
    		DoublePoint part1 = b.multiply(neededResponsiveness);
    		DoublePoint part2 = a.multiply(neededResponsiveness*neededResponsiveness);
    		
    		DoublePoint result = c.add(part1).add(part2);
			
			if(result.distanceTo(pi) >= DISTANCE_THRESHOLD)
			{	
				if(qIndexUsed == -1)
    				System.out.println("Bad New Bears...");
				
				//lastGuidePoint = qStream.getPoint(qIndexUsed + 1);
				lastGuidePoint = qiPlus1;
				pQuadStream.addPoint(new GeneratedPoint(result, this.thickness/2.0f, qIndexUsed, isConnectingPoint));
				
//				pQuadStream.addPointDerivative(derivative);
				
				
				for(double i = 0.01; i < neededResponsiveness; i=i+0.01)
				{
//					tangent = pQuadStream.getPointDerivative(index);//new DoubleVector(piMinus1,pi);
//		    		tangent.unit();
//		    		tangent.multiply(qi.distanceTo(qiPlus1));
//		    		
//		    		b = new DoublePoint(tangent.getX(), tangent.getY());
//		    		a = qiPlus1.subtract(pi).subtract(b);
//		    		c = pi;
//		    		
//		    		part1 = b.multiply(i);
//		    		part2 = a.multiply(i*i);
//		    		
//		    		DoublePoint renderP = c.add(part1).add(part2);
					
					a = qiPlus1.add(piMinus1).subtract((pi.multiply(2)));
		    		b = pi.subtract(piMinus1);
		    		c = pi;
		    		
		    		part1 = b.multiply(i);
		    		part2 = a.multiply(i*i);
		    		
		    		DoublePoint renderP = c.add(part1).add(part2);
		    		
		    		pQuadStream.addRenderPoint(renderP);
				}
				
				pQuadStream.addRenderPoint(result);
				
				if(DRAW_CONNECTING_CURVE)
				{
					connectingCurveRenderPoints.clear();
					//////
//					piMinus1 = pi;
//					pi = result;
					//////
					for(double i = 0.01; i <= 1.0; i=i+0.01)
					{
//						tangent = pQuadStream.getPointDerivative(index);//new DoubleVector(piMinus1,pi);
//			    		tangent.unit();
//			    		tangent.multiply(qi.distanceTo(qiPlus1));
//			    		
//			    		b = new DoublePoint(tangent.getX(), tangent.getY());
//			    		a = qiPlus1.subtract(pi).subtract(b);
//			    		c = pi;
//			    		
//			    		part1 = b.multiply(i);
//			    		part2 = a.multiply(i*i);
//			    		
//			    		DoublePoint renderP = c.add(part1).add(part2);
						
						a = lastGuidePoint.add(piMinus1).subtract((pi.multiply(2)));
			    		b = pi.subtract(piMinus1);
			    		c = pi;
			    		
			    		part1 = b.multiply(i);
			    		part2 = a.multiply(i*i);
			    		
			    		DoublePoint renderP = c.add(part1).add(part2);
			    		
			    		connectingCurveRenderPoints.add(renderP);
					}
				}
				
				return true;
			}
			else
			{
				return false;
			}
    	}
    	else
    	{
    		if(pQuadStream.size() < 2)
    		{	
    			boolean added = super.addPoint(qStream, continueToEnd, forceContinueToEnd, forceContinueToEndQIndex);
    			
    			if(added && this.size() == 1)
    				this.addPointDerivative(new DoubleVector(0,0));
    			
    			if(added && this.size() == 2)
    			{
    				DoublePoint pi = this.getPoint(1);
    				DoublePoint piMinus1 = this.getPoint(0);
    				DoubleVector deriv = new DoubleVector(piMinus1,pi);
    				deriv.unit();
    				this.addPointDerivative(deriv);
    			}
    			///////////////////////
//    			if(this.size() == 2)
//    			{
//	    			connectingCurveRenderPoints.clear();
//					DoublePoint piMinus1 = this.points.get(0);
//					DoublePoint pi = this.points.get(1);
//					DoublePoint a,b,c, part1, part2;
//					for(double i = 0.01; i <= 1.0; i=i+0.01)
//					{
//						a = lastGuidePoint.add(piMinus1).subtract((pi.multiply(2)));
//			    		b = pi.subtract(piMinus1);
//			    		c = pi;
//			    		
//			    		part1 = b.multiply(i);
//			    		part2 = a.multiply(i*i);
//			    		
//			    		DoublePoint renderP = c.add(part1).add(part2);
//			    		
//			    		connectingCurveRenderPoints.add(renderP);
//					}
//    			}
				///////////////////////////
    			return added;
    		}
    		else
    			return false;
    	}
    }

//    public synchronized void renderToSVG(BufferedWriter writer, boolean drawPoints) throws IOException
//    {
//    	if(this.points.size() > 1 && lastGuidePoint != null && DRAW_CONNECTING_CURVE)
//		{	
//			if(points.size() == 2)
//			{
//				DoublePoint p1 = this.points.get(0);
//				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 " + p1.getX() + "," + (-p1.getY()));
//				writer.write(" L " + lastGuidePoint.getX() + "," + (-lastGuidePoint.getY()));
//				
//				writer.write("\"");
//				writer.newLine();
//				
//				writer.write("/> ");
//				writer.newLine();
//			}
//			else
//			{					
//				if(this.connectingCurveRenderPoints.size() > 1)
//				{
//					DoublePoint startPoint = this.points.get(this.size() - 2);
//					DoublePoint lastPoint = startPoint;
//					double curDistance = 0;
//					boolean lineDraw = true;
//					
//					for(int i = 0; i < connectingCurveRenderPoints.size() - 1; i++)
//					{
//						DoublePoint p1 = this.connectingCurveRenderPoints.get(i);
//						
//						if(startPoint.distanceTo(p1) > 20.0)
//						{
//							DoubleVector vector = new DoubleVector(startPoint,p1);
//							vector.unit();
//							double distRequired = 20.0 - curDistance;
//							vector.multiply(distRequired);
//							
//							DoublePoint otherPoint = new DoublePoint(p1.getX() + vector.getX(),p1.getY() + vector.getY());
//							
//							if(lineDraw)
//							{	
//								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)/2.0)+"\"");
//								writer.newLine();
//								
//								writer.write(" d=\"m " + startPoint.getX() + "," + (-startPoint.getY()));
//								writer.write(" L " + otherPoint.getX() + "," + (-otherPoint.getY()));
//								
//								writer.write("\"");
//								writer.newLine();
//								
//								writer.write("/> ");
//								writer.newLine();
//								
//								startPoint = otherPoint;
//								lastPoint = startPoint;
//								curDistance = 0;
//								lineDraw = false;
//							}
//							else
//							{
//								startPoint = otherPoint;
//								lastPoint = startPoint;
//								curDistance = 0;
//								lineDraw = true;
//							}
//						}
//						else
//						{
//							curDistance += lastPoint.distanceTo(p1);
//							lastPoint = p1;
//						}
//					}					
//				}
//			}
//		}
//    	
//    	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();
//		
//		//Add DRAW_CONNECTING_STROKE stuff.
//		
//		if(this.points.size() > 1)
//		{
//			writer.write(" d=\"m " + this.points.get(0).getX() + "," + (-this.points.get(0).getY()));
//			writer.write(" L " + this.points.get(1).getX() + "," + (-this.points.get(1).getY()));
//			for(int i = 0; i < this.renderPoints.size(); i++)
//				writer.write(" L " + this.renderPoints.get(i).getX() + "," + (-this.renderPoints.get(i).getY()));
//		}
//		else
//		{
//			if(this.points.size() > 0)
//			{
//				writer.write(" d=\"m " + this.points.get(0).getX() + "," + (-this.points.get(0).getY()));
//				writer.write(" L " + this.points.get(0).getX() + "," + (-this.points.get(0).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));
		
		if(this.points.size() > 1)
		{
			GeneralPath path = new GeneralPath(GeneralPath.WIND_EVEN_ODD);
			
			DoublePoint p1 = this.points.get(0);
			DoublePoint p2 = this.points.get(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, false);
			
			if(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);
				Color oColor = new Color(streamColor.getRed(),streamColor.getGreen(), streamColor.getBlue(), streamColor.getAlpha()/2);
				Stroke oldStroke = g2D.getStroke();
				g2D.setColor(oColor);
				
//				if(this.connectingCurveRenderPoints.size() > 1)
//				{
//					DoublePoint startPoint = this.points.get(this.size() - 1);
//					DoublePoint lastPoint = startPoint;
//					double curDistance = 0;
//					boolean lineDraw = true;
//					
//					for(int i = 0; i < connectingCurveRenderPoints.size() - 1; i++)
//					{
//						p1 = this.connectingCurveRenderPoints.get(i);
//						
//						if(startPoint.distanceTo(p1) > 10.0)
//						{
//							DoubleVector vector = new DoubleVector(startPoint,p1);
//							vector.unit();
//							double distRequired = 10.0 - curDistance;
//							vector.multiply(distRequired);
//							
//							DoublePoint otherPoint = new DoublePoint(p1.getX() + vector.getX(),p1.getY() + vector.getY());
//							
//							if(lineDraw)
//							{	
//								line = new Line2D.Double(startPoint.getX(),-startPoint.getY(),otherPoint.getX(),-otherPoint.getY()); //-y to reflect back into pixel plane.
//								g2D.draw(line);
//								startPoint = otherPoint;
//								lastPoint = startPoint;
//								curDistance = 0;
//								lineDraw = false;
//							}
//							else
//							{
//								startPoint = otherPoint;
//								lastPoint = startPoint;
//								curDistance = 0;
//								lineDraw = true;
//							}
//						}
//						else
//						{
//							curDistance += lastPoint.distanceTo(p1);
//							lastPoint = p1;
//						}
//					}
//				}
				
				if(points.size() == 2)
				{
					g2D.setStroke(dashed);
					line = new Line2D.Double(p1.getX(),-p1.getY(),lastGuidePoint.getX(),-lastGuidePoint.getY());
					g2D.draw(line);
				}
				else
				{
					g2D.setStroke(oldStroke);
					
					if(this.connectingCurveRenderPoints.size() > 1)
					{
						DoublePoint startPoint = this.points.get(this.size() - 2);
						DoublePoint lastPoint = startPoint;
						double curDistance = 0;
						boolean lineDraw = true;
						
						for(int i = 0; i < connectingCurveRenderPoints.size() - 1; i++)
						{
							p1 = this.connectingCurveRenderPoints.get(i);
							
							if(startPoint.distanceTo(p1) > 20.0)
							{
								DoubleVector vector = new DoubleVector(startPoint,p1);
								vector.unit();
								double distRequired = 20.0 - curDistance;
								vector.multiply(distRequired);
								
								DoublePoint otherPoint = new DoublePoint(p1.getX() + vector.getX(),p1.getY() + vector.getY());
								
								if(lineDraw)
								{	
									line = new Line2D.Double(startPoint.getX(),-startPoint.getY(),otherPoint.getX(),-otherPoint.getY()); //-y to reflect back into pixel plane.
									g2D.draw(line);
									startPoint = otherPoint;
									lastPoint = startPoint;
									curDistance = 0;
									lineDraw = false;
								}
								else
								{
									startPoint = otherPoint;
									lastPoint = startPoint;
									curDistance = 0;
									lineDraw = true;
								}
							}
							else
							{
								curDistance += lastPoint.distanceTo(p1);
								lastPoint = p1;
							}
						}					
					}
				}
				
				g2D.setColor(streamColor);
				g2D.setStroke(oldStroke);
			}
			
			if(renderPoints.size() > 1)
			{
				p1 = this.points.get(1);
				p2 = this.renderPoints.get(0);
				line = new Line2D.Double(p1.getX(),-p1.getY(),p2.getX(),-p2.getY()); //-y to reflect back into pixel plane.
				
				path.append(line, false);
				
				for(int i = 0; i < renderPoints.size() - 1; i++)
				{
					p1 = this.renderPoints.get(i);
					p2 = this.renderPoints.get(i+1);
					line = new Line2D.Double(p1.getX(),-p1.getY(),p2.getX(),-p2.getY()); //-y to reflect back into pixel plane.
					
					path.append(line, false);  //g2D.draw(line);
				}
				
				
			}
			
			g2D.draw(path);
		}
		else
		{
			if(this.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)
		{
			int i = 0;
			for(GeneratedPoint p : points)
			{
				if(i < this.size())
					p.render(g2D);
				i++;
			}
		}
		
		g2D.setStroke(befStroke);
		g2D.setColor(old);
	}
}
