import java.awt.*;
import java.awt.event.*;
import java.util.*;
import Geometry.*;

class ControlPoint
{
	int number;
	int x;
	int y;
}

public class Splines extends GenericApplet
{
	public void render(Graphics g)
	{
		//Clear out the screen
		g.setColor(Color.white);
		g.fillRect(0, 0, bounds().width, bounds().height);

		//Write the mode to the top of the screen
		String modeString = "Mode: ";
		if( smooth )
			modeString += "Smooth, ";
		else
			modeString += "Non-Smooth, ";
		
		if( point )
			modeString += "Show Points, ";
		else
			modeString += "Hide Points, ";
		
		if( lines )
			modeString += "Show Lines, ";
		else
			modeString += "Hide Lines, ";
		
		if( curve )
			modeString += "Show Curve";
		else
			modeString += "Hide Curve";
		
		g.setColor(Color.blue);
		g.drawString(modeString, 0, 20);

		//Do nothing if no points
		if( points.size() == 0 )
			return;

		//Draw the pink lines between the points
		if( lines )
		{
			for(int i=1; i<points.size(); i++)
			{
				ControlPoint pt = (ControlPoint)points.elementAt(i);
				ControlPoint lastPt = (ControlPoint)points.elementAt(i-1);
				g.setColor(Color.pink);
				g.drawLine(pt.x, pt.y, lastPt.x, lastPt.y);
			}
		}

		//Draw the curves
		if( curve )
		{
			g.setColor(Color.cyan);
			int size = points.size();
			for(int i=0; i< ((size-1)/3)*3; i+=3)
			{
				ControlPoint[] pts = new ControlPoint[] 
					{ 
						(ControlPoint)points.elementAt(i),
						(ControlPoint)points.elementAt(i+1),
						(ControlPoint)points.elementAt(i+2),
						(ControlPoint)points.elementAt(i+3)
					};

				Vector4 vecX = new Vector4(pts[0].x, pts[1].x, pts[2].x, pts[3].x);
				Vector4 vecY = new Vector4(pts[0].y, pts[1].y, pts[2].y, pts[3].y);

				double[] coeffX = bezier.mul(vecX).array();
				double[] coeffY = bezier.mul(vecY).array();

				int lastX = pts[0].x;
				int lastY = pts[0].y;
				for(double t=0; t<1.0+INCREMENT; t+=INCREMENT)
				{
					double doubleX = coeffX[0]*t*t*t + coeffX[1]*t*t + coeffX[2]*t + coeffX[3];
					double doubleY = coeffY[0]*t*t*t + coeffY[1]*t*t + coeffY[2]*t + coeffY[3];

					g.drawLine(lastX, lastY, (int)doubleX, (int)doubleY);
					lastX = (int)doubleX;
					lastY = (int)doubleY;
				} //end for
			} //end for
		}

		//Draw the large dots
		if( point )
		{
			for(int i=0; i<points.size(); i++)
			{
				ControlPoint pt = (ControlPoint)points.elementAt(i);
				g.setColor(new Color(255, 0, 0));
				int s;
				s = ( i % 3 == 0 ? THRESHOLD : THRESHOLD/2 );
				//g.fillOval(pt.x-(THRESHOLD/2), pt.y-(THRESHOLD/2), THRESHOLD, THRESHOLD);
				g.fillOval(pt.x-(s/2), pt.y-(s/2), s, s);
			}
		}
	}

	Matrix4x4 bezier = new Matrix4x4(-1, 3, -3, 1,
									 3, -6, 3, 0,
									 -3, 3, 0, 0, 
									 1, 0, 0, 0);

	static final int THRESHOLD = 10;
	static final double INCREMENT = 0.05;
	public boolean mouseDown(Event e, int x, int y) 
	{		 
		//Loop through all of the points and see if we have a hit
		int hitPoint = -1;
		for(int i=0; i<points.size(); i++)
		{
			ControlPoint pt = (ControlPoint)points.elementAt(i);
			if( Math.abs(pt.x - x) < THRESHOLD && Math.abs(pt.y - y) < THRESHOLD )
			{
				//System.out.println("Hit point " + i);
				hitPoint = i;
				break;
			}
		}

		//If no hit, then add another point
		if( hitPoint == -1 )
		{
			ControlPoint pt = new ControlPoint();
			pt.x = x;
			pt.y = y;
			pt.number = points.size();
			points.addElement(pt);

			//If smoothing is on, and we added the 4th pt, add another
			if( smooth && (points.size()) % 3 == 1 && points.size() > 1 )
			{
				ControlPoint lastPt, newPt = new ControlPoint();
				lastPt = (ControlPoint)points.elementAt(points.size()-2);
				newPt.x = pt.x - (lastPt.x - pt.x);
				newPt.y = pt.y - (lastPt.y - pt.y);
				points.addElement(newPt);
			}
		}

		//If we have a hit, record this so we can later move it
		else
		{
			selectedPoint = hitPoint;
		}

		//System.out.println("pt % 3 = " + selectedPoint%3);
		setDamage();
		return true;
	}

	int threshold(int in, int min, int max)
	{
		if( in < min )
			return min;
		else if( in > max )
			return max;
		else 
			return in;

	}

	public boolean mouseDrag(Event e, int x, int y) 
	{ 
		if( selectedPoint != -1 )
		{
			ControlPoint pt = (ControlPoint)points.elementAt(selectedPoint);
			
			//For the segment endpoints, also translate prev and next point
			if( smooth && (selectedPoint) % 3 == 0 )
			{
				int dx = x - pt.x;
				int dy = y - pt.y;
			
				ControlPoint lastPt = ( selectedPoint - 1 >= 0 ?
					(ControlPoint)points.elementAt(selectedPoint-1):
					null );
				ControlPoint nextPt = ( selectedPoint + 1 < points.size() ? 
					(ControlPoint)points.elementAt(selectedPoint+1):
					null );
				
				pt.x = threshold(x, 0, bounds().width - 1 );
				pt.y = threshold(y, 0, bounds().height - 1 );

				if( lastPt != null )
				{
					lastPt.x = lastPt.x + dx;
					lastPt.y = lastPt.y + dy;
				}

				if( nextPt != null )
				{
					nextPt.x = nextPt.x + dx;
					nextPt.y = nextPt.y + dy;
				}

			}

			//If the next one is an endpoint, keep the slopes the same
			else if( smooth && (selectedPoint % 3) == 2 && selectedPoint + 2 < points.size() )
			{
				//if( selectedPoint + 2 < points.size() )
				//{
					ControlPoint pt1 = (ControlPoint)points.elementAt(selectedPoint+1);
					ControlPoint pt2 = (ControlPoint)points.elementAt(selectedPoint+2);

					pt.x = threshold(x, 0, bounds().width - 1 );
					pt.y = threshold(y, 0, bounds().height - 1 );					
					
					int dx = x - pt1.x;
					int dy = y - pt1.y;
					
					pt2.x = pt1.x - dx;
					pt2.y = pt1.y - dy;
				//}
			}

			//If the prev one is an endpoint, keep the slopes the same
			else if( smooth && (selectedPoint % 3) == 1 && selectedPoint - 2 >= 0 )
			{
				//if( selectedPoint - 2 >= 0 )
				//{
					ControlPoint pt1 = (ControlPoint)points.elementAt(selectedPoint-1);
					ControlPoint pt2 = (ControlPoint)points.elementAt(selectedPoint-2);

					pt.x = threshold(x, 0, bounds().width - 1 );
					pt.y = threshold(y, 0, bounds().height - 1 );					
					
					int dx = x - pt1.x;
					int dy = y - pt1.y;
					
					pt2.x = pt1.x - dx;
					pt2.y = pt1.y - dy;
				//}
			}
			//If we have not selected smooth mode, just put the point where indicated
			else
			{
				pt.x = threshold(x, 0, bounds().width - 1 );
				pt.y = threshold(y, 0, bounds().height - 1 );
			}
		}

		setDamage();
		return true;
	}

	public boolean mouseUp(Event e, int x, int y) 
	{
		selectedPoint = -1;
		setDamage();
		
		return true;
	}

	//Toggle a couple of settings, based on input
	public boolean keyDown(Event evt, int key)
	{
		switch(key)
		{
			case 's':
				smooth = !smooth;
				break;
			case 'l':
				lines = !lines;
				break;
			case 'p':
				point = !point;
				break;
			case 'c':
				curve = !curve;
				break;
			case Event.ESCAPE:
				points.removeAllElements();
				break;
		}

		setDamage();
		return true;
	}
	
	Vector points = new Vector();
	int selectedPoint = -1;
	boolean smooth = true;
	boolean lines = true;
	boolean point = true;
	boolean curve = true;

	public static void main(String[] args)	{		Frame frame = new Frame("Splines");		Splines pnl = new Splines();			pnl.setSize(900, 900);		frame.setSize(900, 900);		frame.add(pnl);		frame.show();		frame.addWindowListener(			new WindowAdapter()			{				public void windowClosing(WindowEvent e)				{					e.getWindow().dispose();					System.exit(0);				}			}		);
		pnl.init();		pnl.start();	}

	private void setDamage()
	{
		damage = true;
	}

} //end class


class GenericApplet extends java.applet.Applet implements Runnable
{
	public boolean damage = true;        // you can force a render
	public void render(Graphics g) { }   // you can define how to render

	private Image image = null;
	private Graphics buffer = null;
	private Thread t;
	private Rectangle r = new Rectangle(0, 0, 0, 0);

	public void start() 
	{
		if (t == null) { 
	   
							  //System.out.println("Got here start");
							  t = new Thread(this); t.start(); } 
	}
	public void stop()  
	{ 
	   
		//System.out.println("Got here stop");
		if (t != null) { t.stop(); t = null; } 
	}
	public void run()   
	{ 
	   
		//System.out.println("Got here run");
		try { while (true) { repaint(); t.sleep(30); } }
		catch(InterruptedException e){}; }

	public void update(Graphics g) 
	{
		//System.out.println("Got here update");
		if (r.width != bounds().width || r.height != bounds().height) 
		{
			image = createImage(bounds().width, bounds().height);
			buffer = image.getGraphics();
			r = bounds();
			damage = true;
		}
		render(buffer);
		damage = false;
		if (image != null)
			g.drawImage(image,0,0,this);
	}
}



