package main;

import geometry.DoublePoint;

import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.geom.Line2D;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;

import pointstream.GuideStream;
import pointstream.LaplacianPointStream;
import pointstream.LinearPointStream;
import pointstream.QuadricCirclePointStream;
import pointstream.QuadricPointStream;

public class StreamMonitor 
{
	private ArrayList<GuideStream> qPointStreams;
    private ArrayList<LinearPointStream> pPointStreams;
    
    private ArrayList<GuideStream> finalizedQStreams;
    private ArrayList<LinearPointStream> finalizedPStreams;
    
    private GuideStream qPoints;
    private LinearPointStream pPoints;
    
    public StreamMonitor()
    {
    	qPointStreams = new ArrayList<GuideStream>();
        pPointStreams = new ArrayList<LinearPointStream>();
   
        finalizedQStreams = new ArrayList<GuideStream>();
        finalizedPStreams = new ArrayList<LinearPointStream>();
    }
    
    private ArrayList<LinearPointStream> getAllGenCurves()
    {
    	ArrayList<LinearPointStream> gens = new ArrayList<LinearPointStream>();
    	if(pPoints != null)
    		gens.add(pPoints);
    	
    	gens.addAll(pPointStreams);
    	gens.addAll(finalizedPStreams);
    	
    	return gens;
    }
    
    private ArrayList<GuideStream> getAllGuideCurves()
    {
    	ArrayList<GuideStream> guides = new ArrayList<GuideStream>();
    	if(qPoints != null)
    		guides.add(qPoints);
    	
    	guides.addAll(qPointStreams);
    	guides.addAll(finalizedQStreams);
    	
    	return guides;
    }
    
    private void doCreateStreamPair()
    {
    	if(qPoints != null)
    	{
    		qPointStreams.add(qPoints);
    		qPoints = null;
    	}
    	
    	if(pPoints != null)
    	{
    		synchronized(pPointStreams)
    		{
    			pPointStreams.add(pPoints);
    		}
    		pPoints = null;
    	}
    	
    	Color guideColor,genColor;
    	
    	if(ShoeLaceFrame.getInstance().transparentGuideCurveCheckBox.isSyncSelected())
    	{
    		guideColor = new Color(1.0f,0.0f,0.0f,0.125f);
    	}
    	else
    	{
    		guideColor = Color.RED;//new Color(1.0f,0.0f,0.0f,0.25f);//
    	}

    	genColor = ShoeLaceFrame.getInstance().generatedStrokeColorChooser.getColor();
    	
    	//genColor = new Color(genColor.getRed(), genColor.getGreen(), genColor.getBlue(), 0.5f);
    	
    	int deltaTime = ShoeLaceFrame.getInstance().guideCurveSampleSlider.getSyncValue();
    	int pauseTime = ShoeLaceFrame.getInstance().guideCurvePauseTimeSlider.getSyncValue();

    	Object type = ShoeLaceFrame.getInstance().curveTypeComboBox.getSyncSelectedItem();
    	double responsiveness = ((double)ShoeLaceFrame.getInstance().responsivenessSlider.getSyncValue())/100.0;
    	float thickness = (float)ShoeLaceFrame.getInstance().strokeThicknessSlider.getSyncValue();
    	boolean isSpeedInfluenced = GuideStream.USE_DISTANCE_FOR_GUIDE_CURVE;//ShoeLaceFrame.getInstance().speedInfluenceCheckBox.isSyncSelected();
    	
    	if(type.equals("LINEAR"))
    		pPoints = new LinearPointStream(genColor, thickness, responsiveness, isSpeedInfluenced);

    	if(type.equals("QUADRIC"))
    		pPoints = new QuadricPointStream(genColor, thickness, responsiveness, isSpeedInfluenced);

    	if(type.equals("LAPLACIAN"))
    		pPoints = new LaplacianPointStream(genColor, thickness, responsiveness, isSpeedInfluenced);
    	
    	if(type.equals("CIRCLE"))
    		pPoints = new QuadricCirclePointStream(genColor, thickness, responsiveness, isSpeedInfluenced);

    	qPoints = new GuideStream(pPoints, deltaTime, deltaTime + pauseTime, thickness, guideColor);
    }
    
    private boolean addPPoint(LinearPointStream pStream, GuideStream qStream, boolean continueToEnd)
    { 
    	return pStream.addPoint(qStream, continueToEnd, false, -1);
    }
    
    public synchronized void createStreamPair()
    {
    	doCreateStreamPair();
    }
    
    public synchronized LinearPointStream getClickedStream(DoublePoint clickPosition)
    {
    	int clickedPointIndex = -1;
    	
    	synchronized(pPointStreams)
    	{
    		synchronized(finalizedPStreams)
    		{
		    	for(LinearPointStream stream : getAllGenCurves())
		    	{
		    		clickedPointIndex = stream.getClickedPoint(clickPosition);
		    		
		    		if(clickedPointIndex != -1)
		    			return stream;
		    	}
    		}
    	}
    	
    	return null;
    }
    
    public synchronized void addQPoint(DoublePoint p, long time, int px, int py)
    {
    	if(qPoints != null)
    		qPoints.addPoint(p, time, px, py);
    }
    
    public synchronized void addPPoints()
    {
    	if(qPoints != null && pPoints != null)
    	{
    		boolean pointAdded = addPPoint(pPoints,qPoints,qPoints.isPaused());

    		if(!pointAdded && qPoints.isPaused() && qPoints.size() > 1)
    		{
    			// Create a new stream is no point was added. The guide curve is paused. And the guide curve has more than 1 point in
    			// it (this prevents the app from constantly creating new streams if the user keep the cursor in the same location).
    			doCreateStreamPair();
    		}
    	}

    	if(qPointStreams.size() != pPointStreams.size())
    		return;

    	Iterator<GuideStream> guideIter = qPointStreams.iterator();
    	ArrayList<GuideStream> guidesToRemove = new ArrayList<GuideStream>();
    	ArrayList<LinearPointStream> gensToRemove = new ArrayList<LinearPointStream>();

    	for(LinearPointStream p : pPointStreams)
    	{
    		GuideStream curGuide = guideIter.next();
    		if(!addPPoint(p,curGuide,true))
    		{
    			curGuide.finalizeStream(); // No point was added, so we can now finalize the stream.

    			finalizedQStreams.add(curGuide);
    			finalizedPStreams.add(p);

    			guidesToRemove.add(curGuide);
    			gensToRemove.add(p);

    			//p.calculateCurvatures();
    		}
    	}

    	qPointStreams.removeAll(guidesToRemove);
    	pPointStreams.removeAll(gensToRemove);
    }
    
    public synchronized void clearStreams()
    {
    	if(pPoints != null)
    		pPoints = null;
    	
    	if(qPoints != null)
    		qPoints = null;

   		qPointStreams.clear();
    	finalizedQStreams.clear();
    	
    	pPointStreams.clear();
    	finalizedPStreams.clear();
    }
    
    public synchronized void clearStream(DoublePoint position)
    {   	
    	LinearPointStream cStream = getClickedStream(position);

    	if(cStream != null)
    	{
    		int streamIndex = -1;
    		boolean inFinalized = false;

    		if(pPointStreams.contains(cStream))
    		{
    			streamIndex = pPointStreams.indexOf(cStream);
    			inFinalized = false;
    		}


    		if(finalizedPStreams.contains(cStream))
    		{
    			streamIndex = finalizedPStreams.indexOf(cStream);
    			inFinalized = true;
    		}
    		
    		clearStream(cStream,streamIndex,inFinalized);
    	}
    }
    
    public synchronized void clearStream(LinearPointStream stream, int index, boolean inFinalized)
    {
    	DoublePoint lastPoint = stream.getPoint(stream.size() - 1);
    	float snappingDistance;
    	
    	if(!inFinalized)
    	{
    		snappingDistance = qPointStreams.get(index).getPoint(0).getSnapToRadius();
    		
    		qPointStreams.remove(index);
	    	pPointStreams.remove(index);
    	}
    	else
    	{
    		snappingDistance = finalizedQStreams.get(index).getPoint(0).getSnapToRadius();
    		
    		finalizedQStreams.remove(index);
    		finalizedPStreams.remove(index);
    	}
    	
    	boolean needsToBeDeleted;
    	ArrayList<LinearPointStream> pToRemove = new ArrayList<LinearPointStream>();
    	ArrayList<GuideStream> qToRemove = new ArrayList<GuideStream>();
    	
    	for(LinearPointStream oStream : pPointStreams)
    	{
    		needsToBeDeleted = true;
    		
    		for(int i = 0; i < oStream.size(); i++)
    		{
    			if(oStream.getPoint(i).distanceTo(lastPoint) > snappingDistance)
    			{
    				needsToBeDeleted = false;
    				break;
    			}
    		}
    		
    		if(needsToBeDeleted)
    		{
    			int toRemIndex = pPointStreams.indexOf(oStream);
    			pToRemove.add(oStream);
    			qToRemove.add(qPointStreams.get(toRemIndex));
    		}
    	}
    	
    	pPointStreams.removeAll(pToRemove);
    	qPointStreams.removeAll(qToRemove);
    	
    	pToRemove.clear();
    	qToRemove.clear();
    	
    	for(LinearPointStream oStream : finalizedPStreams)
    	{
    		needsToBeDeleted = true;
    		
    		for(int i = 0; i < oStream.size(); i++)
    		{
    			if(oStream.getPoint(i).distanceTo(lastPoint) > snappingDistance)
    			{
    				needsToBeDeleted = false;
    				break;
    			}
    		}
    		
    		if(needsToBeDeleted)
    		{
    			int toRemIndex = finalizedPStreams.indexOf(oStream);
    			pToRemove.add(oStream);
    			qToRemove.add(finalizedQStreams.get(toRemIndex));
    		}
    	}
    	
    	finalizedPStreams.removeAll(pToRemove);
    	finalizedQStreams.removeAll(qToRemove);
    }
    
    public synchronized void clearCurrentStreams()
    {
    	if(pPoints != null)
    	{
    		pPointStreams.add(pPoints);
    		pPoints = null;
    	}
    	
    	if(qPoints != null)
    	{
    		qPointStreams.add(qPoints);
    		qPoints = null;
    	}
    }
    
    public synchronized void loadQStreamsFromMayaCurves(String path)
    {
    	FileReader f = null;
    	
    	try {
			f = new FileReader(new File(path));
		} catch (FileNotFoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
    	BufferedReader reader = new BufferedReader(f);
    	String line;
    	String[] parts;
    	
    	try {
			while((line = reader.readLine()) != null)
			{	
				if(line.contains("\"nurbsCurve\""))
				{
					// Begin Reading in the curve.
					
					// Skip one line.
					reader.readLine();
					
					line = reader.readLine();
					parts = line.trim().split(" ");
					int numPoints = Integer.parseInt(parts[0].trim());
					
					while(!(line = reader.readLine()).contains(Integer.toString(numPoints)));
						// Skip another line.
					
					doCreateStreamPair();
					
					// Make the guide stream accept all the points asked.
					qPoints.setForceAcceptAllPoints(true);
					
					DoublePoint prevPoint = null;
					double minDistance = Double.MAX_VALUE;
					for(int i = 0; i < numPoints; i++)
					{
						line = reader.readLine();
						parts = line.trim().split(" ");
						DoublePoint curPoint = new DoublePoint(Double.parseDouble(parts[0]), Double.parseDouble(parts[1]));
						
						if(prevPoint == null)
							prevPoint = curPoint;
						else	
						{
							if(curPoint.distanceTo(prevPoint) < minDistance)
								minDistance = curPoint.distanceTo(prevPoint);
							
							prevPoint = curPoint;
						}
						
						qPoints.addPoint(curPoint, i*100, 0, 0); // Here we can't find the speed as timing information isn't specified.
					}
					
					LinearPointStream.DISTANCE_THRESHOLD = minDistance / 1000.0;
					
					while(pPoints.addPoint(qPoints,true, false, -1));
					
					LinearPointStream.DISTANCE_THRESHOLD = 0.01;
				}
			}
			
			// This is just to move the qPoints and pPoints into the completed streams lists.
			doCreateStreamPair();
			
			reader.close();
		} 
    	catch (Exception e) 
		{
    		e.printStackTrace();
    		ShoeLaceFrame.getInstance().showMessage("There was an error reading the maya file.");
    		return;
		}
    }
    
    public synchronized void loadQStreams(String path)
    {
    	if(!path.endsWith(".elasti"))
    	{
    		ShoeLaceFrame.getInstance().showMessage("This file is not a Elasticurve file.  ShoeLace files have the .elasti extension.  Aborting.");
    		return;
    	}
    		
    	FileReader f = null;
    	
    	try {
			f = new FileReader(new File(path));
		} catch (FileNotFoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
    	BufferedReader reader = new BufferedReader(f);
    	
    	GuideStream qStr = null;
    	
    	String line;
    	String[] parts, guideParts;
    	
    	Color guideColor,genColor;
    	
    	if(ShoeLaceFrame.getInstance().transparentGuideCurveCheckBox.isSyncSelected())
		{
			  guideColor = new Color(1.0f,0.0f,0.0f,0.25f);
		}
		else
		{
			  guideColor = Color.RED;
		}
    	
    	LinearPointStream pStr = null;
		
    	try 
    	{
			while((line = reader.readLine()) != null)
			{
				guideParts = line.split(" ");
				
				line = reader.readLine();
				parts = line.split(" ");
//				genColor = Color.BLACK;
				genColor = new Color(Float.parseFloat(parts[3])/255.0f, Float.parseFloat(parts[4])/255.0f, Float.parseFloat(parts[5])/255.0f, (Float.parseFloat(parts[6])/255.0f));
				boolean isSpeedInfluenced = false;
				
				if(parts.length > 7)
					isSpeedInfluenced = Boolean.parseBoolean(parts[7]);
				
				switch(Integer.parseInt(parts[0]))
				{	
					case LinearPointStream.LINEAR_STREAM:
						pStr = new LinearPointStream(genColor, Float.parseFloat(parts[2])/**2.0f*/, Double.parseDouble(parts[1]), isSpeedInfluenced);
						break;
					case LinearPointStream.QUADRIC_STREAM:
						pStr = new QuadricPointStream(genColor, Float.parseFloat(parts[2])/**2.0f*/, Double.parseDouble(parts[1]), isSpeedInfluenced);
						break;
					case LinearPointStream.LAPLACIAN_STREAM:
						pStr = new LaplacianPointStream(genColor, Float.parseFloat(parts[2])/**2.0f*/, Double.parseDouble(parts[1]), isSpeedInfluenced);
						break;
					case LinearPointStream.CIRCLE_STREAM:
						pStr = new QuadricCirclePointStream(genColor, Float.parseFloat(parts[2])/**2.0f*/, Double.parseDouble(parts[1]), isSpeedInfluenced);
						break;
				}
				
				qStr = new GuideStream(pStr, Long.parseLong(guideParts[0]), Long.parseLong(guideParts[1]), Float.parseFloat(parts[2])/**2.0f*/, guideColor);
				
				while(!((line = reader.readLine()).equals("---")))
				{				
					parts = line.split(" ");
					
					DoublePoint p = new DoublePoint(Double.parseDouble(parts[0]), Double.parseDouble(parts[1]));
					
					if(parts.length == 4)
					{
						qStr.addPoint(p, Long.parseLong(parts[2]), 0, 0); // Old formats, don't have timing information, so simply ignore in this case.
						
						if(Boolean.parseBoolean(parts[3]))
						{
							//  Need to continue the curve until we reach this point.
							while(pStr.addPoint(qStr,true, false, -1));
	
						}
						else
							pStr.addPoint(qStr,true,false,-1);
					}
					else
					{
						qStr.addPoint(p, Long.parseLong(parts[2]), Integer.parseInt(parts[3]), Integer.parseInt(parts[4]));
	
						if(Boolean.parseBoolean(parts[5]))
						{
							//  Need to continue the curve until we reach this point.
							while(pStr.addPoint(qStr,true, false, -1));
	
						}
						else
							pStr.addPoint(qStr,true,false,-1);
					}
				}
			    
				qPointStreams.add(qStr);  //Yannick : Could maybe put this in finalized list.....
			    pPointStreams.add(pStr);  //Yannick : Could maybe put this in finalized list.....
			}
		} 
    	catch (NumberFormatException e) 
    	{
			// TODO Auto-generated catch block
			e.printStackTrace();
		} 
    	catch (IOException e) 
    	{
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
    }
    
    public synchronized void savePStreamsAsMayaCurves(String path)
    {
    	FileWriter w = null;

    	try {
    		w = new FileWriter(new File(path + ".ma"));
    	} catch (IOException e) {
    		// TODO Auto-generated catch block
    		e.printStackTrace();
    	}

    	BufferedWriter writer = new BufferedWriter(w);

    	try {
    		writer.write("requires maya \"4.0\";");
    		writer.newLine();

    		int i = 0;
    		for(LinearPointStream p : this.getAllGenCurves())
    		{
    			if(p.size() != 1)
    			{
    				writer.write("createNode nurbsCurve -n elasticurve"+i+";");
    				writer.newLine();
    				writer.write("setAttr -k off \".v\";");
    				writer.newLine();
    				writer.write("setAttr \".cc\" -type \"nurbsCurve\"");
    				writer.newLine();

    				p.writeCurveToMayaFormat(writer);
    			}

    			i++;
    		}

    		writer.close();

    	} catch (IOException e) {
    		// TODO Auto-generated catch block
    		e.printStackTrace();
    	}
    }
    
    public synchronized void saveQStreams(String path)
    {    
		FileWriter w = null;
		
		try {
			w = new FileWriter(new File(path + ".elasti"));
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
		BufferedWriter writer = new BufferedWriter(w);
		
    	for(GuideStream p : this.getAllGuideCurves())
    	{	
			p.writeToFile(writer);
			
			try {
				writer.write("---");
				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();
		}
    }
    
    public synchronized void renderSpringyConnector(Graphics2D g2D, double maxLength)
    {
    	if(pPoints != null && pPoints.size() > 0 && qPoints != null && qPoints.size() > 0 && pPoints.getNextIndex() - 1 < qPoints.size())
        {
	    	  DoublePoint lastQPoint = qPoints.getPoint(pPoints.getNextIndex()-1);
	    	  DoublePoint lastPPoint = pPoints.getPoint(pPoints.size() - 1);
	    	  
	    	  Line2D.Double connector = new Line2D.Double(lastPPoint.getX(), -lastPPoint.getY(), lastQPoint.getX(), -lastQPoint.getY());
	    	  
	    	  double connectorLength = lastQPoint.distanceTo(lastPPoint);
	    	  
	    	  Color renderColor = new Color(0.0f,0.0f,1.0f, 1.0f - (float)(connectorLength/maxLength));
	    	  Color oldColor = g2D.getColor();
	    	  
	    	  g2D.setColor(renderColor);
	    	  g2D.draw(connector);
	    	  g2D.setColor(oldColor);
        }
    }
    
//    public synchronized void renderQStreamsToSVG(BufferedWriter writer, boolean drawPoints) throws IOException
//    {
//    	if(qPoints != null)
//	      	qPoints.renderToSVG(writer, drawPoints);
//	      
//    	synchronized(qPointStreams)
//    	{
//    		for(GuideStream stream : qPointStreams)
//    			stream.renderToSVG(writer, drawPoints);
//    	}
//
//    	synchronized(finalizedQStreams)
//    	{
//    		for(GuideStream stream : finalizedQStreams)
//    			stream.renderToSVG(writer, drawPoints);
//    	}
//    }
    
    public synchronized void renderQStreams(Graphics2D g2D, boolean drawPoints)
    {
    	if(qPoints != null)
	      	qPoints.render(g2D, drawPoints);
	      
    	synchronized(qPointStreams)
    	{
    		for(GuideStream stream : qPointStreams)
    			stream.render(g2D, drawPoints);
    	}

    	synchronized(finalizedQStreams)
    	{
    		for(GuideStream stream : finalizedQStreams)
    			stream.render(g2D, drawPoints);
    	}
    }
    
//    public synchronized void renderPStreamsToSVG(BufferedWriter writer, boolean drawPoints) throws IOException
//    {
//    	if(pPoints != null)
//	      	pPoints.renderToSVG(writer, drawPoints);
//	      
//    	synchronized(pPointStreams)
//    	{
//    		for(LinearPointStream stream : pPointStreams)
//    			stream.renderToSVG(writer, drawPoints);
//    	}
//
//    	synchronized(finalizedPStreams)
//    	{
//    		for(LinearPointStream stream : finalizedPStreams)
//    			stream.renderToSVG(writer, drawPoints);
//    	}
//    }
    
    public synchronized void renderPStreams(Graphics2D g2D, boolean drawPoints)
    {
    	if(pPoints != null)
	      	pPoints.render(g2D, drawPoints);
	      
	      synchronized(pPointStreams)
	      {
	    	  for(LinearPointStream stream : pPointStreams)
	    		  stream.render(g2D, drawPoints);
	      }
	      
	      synchronized(finalizedPStreams)
	      {
	    	  for(LinearPointStream stream : finalizedPStreams)
	    		  stream.render(g2D, drawPoints);
	      }
    }
}
