/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
*                                                                     *
* \                                                                   *
*  |- <APPLET CODE="toothpick2.class" WIDTH=691 HEIGHT=484></APPLET>  *
* /                                                                   *
* \                                                                   *
*  |- Toothpick2.java                                                 *
*  |- Author: Michael McKelvey (mmckelve@mail.mste.uiuc.edu)          *
*  |- Purpose: This applet is meant to be used as an activity for     *
*  |-  students who are learning about geometry and for anyone        *
*  |-  interested in solving puzzles.                                 *
* /                                                                   *
* \                                                                   *
*  |- Adapted from the code for the applet "ToothPick.java",          *
*  |- written by Nick Exner 3/3/01                                    *
* /                                                                   *
* \                                                                   *
*  |- Last updated: 11/15/02                                          *
*  |- By Michael McKelvey                                             *
* /                                                                   *
*                                                                     *
\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */


import java.applet.*;
import java.awt.*;
import java.util.*;
import java.awt.event.*;
import java.awt.image.*;

public class toothpick2 extends Applet
{
	Graphics h;
	Image offscreenImg;
	Image sign;
	Image grid;
	Image bg;
	Pick p;
	boolean pickInit;
	boolean lclick=false, rclick=false; //tell whether left/right mouse buttons are down
	int currentX, currentY;
	Button ResetButton = new Button("Start Over");
	Checkbox GridBox = new Checkbox("Show Grid", true);
	Checkbox SnapGridBox = new Checkbox("Snap to Grid", false);
	Color bgColor = new Color(255,255,255);
	Color gridLineColor = new Color(204,204,255); //(204,204,153);
	Color borderColor = new Color(153,153,255);
	int gridWidth = 69;
	int gridHeight = 69;

	public void init()
	{
		Pick.loadImages((Applet)this,(ImageObserver)this,this.size().width,this.size().height);
		offscreenImg = createImage(this.size().width,this.size().height);
		h = offscreenImg.getGraphics();
		sign = getImage(getCodeBase(),"images/sign.gif");
		grid = getImage(getCodeBase(),"images/grid1.jpg");
		bg = getImage(getCodeBase(),"images/gradbg.gif");
		add(ResetButton);
		add(GridBox);
		add(SnapGridBox);
		
		setBackground(bgColor);
		repaint();
	}

	public void paint(Graphics g)
	{
		h.drawImage(bg,0,0,this);

		if(GridBox.getState())
		{
			//h.setColor(bgColor);
			//h.fillRect(0,0,this.size().width,this.size().height);
			h.setColor(gridLineColor);
			for(int i=0;i<this.size().height;i+=gridHeight)
				h.drawLine(0,i,this.size().width,i);
			for(int i=0;i<this.size().width;i+=gridWidth)
				h.drawLine(i,0,i,this.size().height);
		}

		h.setColor(borderColor);
		h.drawRect(0,0,this.size().width-1,this.size().height-1);
		h.drawRect(1,1,this.size().width-3,this.size().height-3);

		h.drawImage(sign,2,2,this);
		h.setColor(new Color(112,88,63));
		h.setFont(new Font("TimesRoman",Font.BOLD,18));
		if(p != null)
		{
			h.drawString(""+p.numpicks+"",70,77);
			p.show(h);
		}
		else
			h.drawString("0",70,77);
		g.drawImage(offscreenImg,0,0,this);
	}
	
	public boolean action(Event evt, Object arg)
	{
		if (evt.target instanceof Button && evt.target == ResetButton)
		{
			p = null;
			Pick.reset();
		}

		repaint();
		return true;
	}

	public boolean mouseDown(Event evt, int x, int y)
	{
		currentX = x;
		currentY = y;
		

		//****************************************************************************
		//Some day, right clicking should bring up a pop-up menu with these options:
		//  _R_otate ->
		//    45 degrees
		//    90 degrees
		//    180 degrees
		//    [Free Rotate]
		//  _D_elete
		// 
		
		if(evt.modifiers == evt.ALT_MASK || evt.modifiers == evt.META_MASK)
		{ //RIGHT click
			rclick = true;
			//User needn't click on the toothpick to rotate it; it just has to be selected
			if(pickSelected())
				rotate(false);
		}
		else
		{ //LEFT click
			lclick = true;
			if(p == null)
			{
				//check whether clicked on toothpick box
				if(x > 158 && x < 239 && y > 44 && y < 110)
				{
					p = new Pick(x-p.tpSize[0].width/2,y-p.tpSize[0].height/2);
				}
			}
			else
			{
				p = p.getLast().mouseDown(x,y);
				if(!pickSelected())
					if(x > 158 && x < 239 && y > 44 && y < 110) //clicked on toothpick box
					{
						p.getLast().add(new Pick(x-p.tpSize[0].width/2,y-p.tpSize[0].height/2));
						p = p.getLast().mouseDown(x,y);
					}
			}
		}

		repaint();
		return true;
	}

	public boolean mouseDrag(Event evt, int x, int y)
	{
		//if(evt.modifiers != evt.ALT_MASK && evt.modifiers != evt.META_MASK)
		if(lclick)
		{//Left button can drag, right can't
			currentX = x;
			currentY = y;
			if(pickSelected())
				p.getCurrent().mouseDrag(x,y);
			repaint();
		}
		return true;
	}
	
	public boolean mouseUp(Event evt, int x, int y)
	{
		if(evt.modifiers == evt.ALT_MASK || evt.modifiers == evt.META_MASK)
		{ //RIGHT unclick
			rclick = false;
		}
		else
		{
			lclick = false;
			currentX = x;
			currentY = y;
			//if(snapToGrid)
			if(SnapGridBox.getState())
				p.getCurrent().snapToGrid(gridWidth,gridHeight);
			repaint();
		}
		return true;
	}
	
	public boolean keyDown(Event evt, int key)
	{
		if(pickSelected())
		{
			if (key == 'r' || key == 'R')
				rotate(false);
			else if(key == 'w' || key == 'W' || key == evt.UP)
				p.getCurrent().nudgeUp();
			else if(key == 'a' || key == 'A' || key == evt.LEFT)
				p.getCurrent().nudgeLeft();
			else if(key == 's' || key == 'S' || key == evt.DOWN)
				p.getCurrent().nudgeDown();
			else if(key == 'd' || key == 'D' || key == evt.RIGHT)
				p.getCurrent().nudgeRight();
		}
		repaint();
		return true;
	}
	
	public boolean pickSelected()
	{
		if(p != null && p.getCurrent().isSelected())
			return true;
		else
			return false;
	}
	
	public void rotate(boolean useCurrentXY)
	{
		if(pickSelected())
			if(useCurrentXY)
				p.getCurrent().rotate(currentX,currentY);
			else
				p.getCurrent().rotate();
	}

	public void update(Graphics g)
	{
		paint(g);
	}

} // END CLASS toothpick2

////////////////////////////////////////////////////////////
/////////////////// *****CLASS Pick***** ///////////////////
////////////////////////////////////////////////////////////

class Pick
{
	int x,
	    y,
	    pointOnImageX,
	    pointOnImageY;
	double preciseX, preciseY;
	double precisePOIX, precisePOIY;
	
	//orientation used to tell in which direction the pick is pointing
	// 180/ORIENTATION_MAX = angle of rotation
	// only need to work with 180 degrees, instead of 360
	int orientation;
	public static final int ORIENTATION_MAX = 4;

	Pick next, prev;	//doubly-linked list
	boolean select;
	static Image tp[];
	static Image tpSel[];
	static Dimension[] tpSize;
	static ImageObserver iobp;
	static Pick firstPick = null;
	static Pick curPick = null;
	static Pick lastPick = null;
	static int numpicks = 0;
	static int appletWidth;
	static int appletHeight;
	static Color selectedColor = new Color(0,51,153);
	

	public Pick(int x, int y)
	{
		this.x = x;
		this.y = y;
		this.preciseX = x;
		this.preciseY = y;
		pointOnImageX = 0;
		pointOnImageY = 0;
		precisePOIX = 0.0;
		precisePOIY = 0.0;
		this.next = null;
		this.prev = null;
		orientation = 0; //horizontal toothpick
		select = false; //??? true?
		if(numpicks == 0)
		{
			firstPick = this;
			lastPick = this;
			curPick = this;
			numpicks++;
			mouseDown(x,y);
		}
		System.out.println("X "+x+"\tY1: "+y+"\tnumpicks:   "+numpicks);
	}

	public Pick(int x, int y, Pick next)
	{
		this(x,y);
		add(next);
	}

	public Pick(int x, int y, Pick next, Pick prev)
	{
		this(x,y);
		add(next);
	}
	
	public void add(Pick next)
	{
		if (this.next == null)
		{
			this.next = next;
			next.prev = this;
			lastPick = next;
			numpicks++;
		}
		else
			this.next.add(next);
	}
	
	public void moveToEnd()
	{
		//check whether this is already the last Pick
		if(next != null)
		{
			//check whether this is the first Pick
			if(prev != null)
				prev.next = next;
			else
				firstPick = next;
			next.prev = prev;
			this.prev = lastPick;
			this.next = null;
			lastPick.next = this;
			lastPick = this;
		}
	}

	public static void loadImages(Applet parent, ImageObserver iob, int width, int height)
	{
		iobp = iob;
		MediaTracker tracker = new MediaTracker(parent);
		
		tp = new Image[ORIENTATION_MAX]; //4
		tp[0] = parent.getImage(parent.getCodeBase(),"images/pickh.gif");
		tp[1] = parent.getImage(parent.getCodeBase(),"images/pick45.gif");
		tp[2] = parent.getImage(parent.getCodeBase(),"images/pickv.gif");
		tp[3] = parent.getImage(parent.getCodeBase(),"images/pick135.gif");
		tracker.addImage(tp[0],0);
		tracker.addImage(tp[1],1);
		tracker.addImage(tp[2],2);
		tracker.addImage(tp[3],3);
		
		tpSel = new Image[ORIENTATION_MAX]; //4
		tpSel[0] = parent.getImage(parent.getCodeBase(),"images/pickha.gif");
		tpSel[1] = parent.getImage(parent.getCodeBase(),"images/pick45a.gif");
		tpSel[2] = parent.getImage(parent.getCodeBase(),"images/pickva.gif");
		tpSel[3] = parent.getImage(parent.getCodeBase(),"images/pick135a.gif");
		tracker.addImage(tpSel[0],4);
		tracker.addImage(tpSel[1],5);
		tracker.addImage(tpSel[2],6);
		tracker.addImage(tpSel[3],7);
		
		
		tpSize = new Dimension[ORIENTATION_MAX]; //4
		tpSize[0] = new Dimension(68,8); //horizontal
		tpSize[1] = new Dimension(48,48); //45 degrees
		tpSize[2] = new Dimension(8,68); //vertical
		tpSize[3] = new Dimension(48,48); //135 degrees
		
		tracker.checkAll(true);
		
		appletWidth = width;
		appletHeight = height;
	}

	public void show(Graphics g)
	{
		if(tp[orientation] != null)
		{
			if(select)
			{
				//g.setColor(selectedColor);
				//g.drawRoundRect(x-3,y-3,tpSize[orientation].width+6,tpSize[orientation].height+6,16,16);
				//g.drawImage(tp[orientation],x,y,iobp);
				g.drawImage(tpSel[orientation],x,y,iobp);
			}
			else
			{
				//g.drawImage(tp[orientation],(int)preciseX,(int)preciseY,iobp);
				g.drawImage(tp[orientation],x,y,iobp);
			}
		}
		if (next != null)
			next.show(g);
	}

	public void setX(int x)
	{
		this.x = x;
	}

	public void setY(int y)
	{
		this.y = y;
	}

	public void setPreciseX(double x)
	{
		this.preciseX = x;
		setX((int)x);
	}

	public void setPreciseY(double y)
	{
		this.preciseY = y;
		setX((int)y);
	}

	public int getX()
	{
		return x;
	}

	public int getY()
	{
		return y;
	}
	
	public double getPreciseX()
	{
		return preciseX;
	}

	public double getPreciseY()
	{
		return preciseY;
	}

	public Pick getFirst()
	{
		return firstPick;
	}
	
	public Pick getLast()
	{
		return lastPick;
	}
	
	public Pick getCurrent()
	{
		return curPick;
	}
	
	public boolean isSelected()
	{
		if(select)
			return true;
		else
			return false;
	}
	
	public boolean contains(int x1, int y1)
	{
		if (x1 >= x && x1 <= x+tpSize[orientation].width)
			if (y1 >= y && y1 <=y+tpSize[orientation].height)
				return true;
		
		return false;
	}

	public void rotate(int curX, int curY)
	{
		//check that the point (curX,curY) is located on this object
		if(this.contains(curX,curY))
			RotateBy45(curX,curY);

//		else if(next != null)
//			next.rotate(curX,curY);
	}

	public void rotate()
	{
		RotateBy45(x + tpSize[orientation].width/2,y + tpSize[orientation].height/2);
	}

	public void RotateBy45(int curX, int curY)
	{ //rotate 45 degrees about the center
//		int centerX = preciseX + tpSize[orientation].width/2;
//		int centerY = preciseY + tpSize[orientation].height/2;

		//rotate 45 degrees about the current mouse pointer position
//		int centerX = curX;
//		int centerY = curY;
		
		orientation = (orientation+1)%ORIENTATION_MAX;
		
		preciseX = curX - tpSize[orientation].width/2;
		preciseY = curY - tpSize[orientation].height/2;
		
		//change Point On Image
		select(curX,curY);
	}
	
	public Pick mouseDown(int x1, int y1)
	{
		int curPOIx = x1 - x;
		int curPOIy = y1 - y;
		select = false;
		
		if(this.contains(x1,y1))
		{
			switch(orientation)
			{
				case 0: //0,180 degrees
				case 2: //90,270 degrees
					select(x1,y1);
					break;
				
				case 1: //45,225 degrees
				//translate into a 135 degree problem, kind of...
				int transX = tpSize[orientation].width - 1 - curPOIx;
				if(Math.abs(transX-curPOIy) <= 5)
					select(x1,y1);
				break;
				
				case 3: //135,315 degrees
					if(Math.abs(curPOIx-curPOIy) <= 5)
						select(x1,y1);
					break;
				
				default: select = false;
			}
		}
		
		//work backwards so the first Pick we find is the top one
		//returning pick, mainly in case the first one was selected, since moving it to the back
		// will destroy the relation btwn. the applet class and the list of Picks. This way, we
		// can make sure that we have a pointer to the front of the list each time we select a Pick
		if(!select && prev != null)
			return prev.mouseDown(x1,y1);
		else
			return firstPick;
	}

	public void mouseDrag(int x1, int y1)
	{
	 	if (select) // && stillInBounds(x1-pointOnImageX,y1-pointOnImageY))
		{
			this.x=(x1-pointOnImageX);
			this.y=(y1-pointOnImageY);
			preciseX = x1 - precisePOIX;
			preciseY = y1 - precisePOIY;
		}
	}

	public void select(int x1,int y1)
	{
		pointOnImageX = x1-x;
		pointOnImageY = y1-y;
		precisePOIX = x1-preciseX;
		precisePOIY = y1-preciseY;
		curPick.deselect();
		select = true;
		curPick = this;
		moveToEnd();
	}
	
	public void deselect()
	{
		select = false;
	}

//	public void mouseUp()
//	{
//		select = false;
//		if (next != null)
//			next.mouseUp();
//	}
//

	public void nudgeUp()
	{
		if(stillInBounds(x,y-1))
			y--;
	}

	public void nudgeLeft()
	{
		if(stillInBounds(x-1,y))
			x--;
	}

	public void nudgeDown()
	{
		if(stillInBounds(x,y+1))
			y++;
	}
	
	public void nudgeRight()
	{
		if(stillInBounds(x+1,y))
			x++;
	}
	
	/*
	 * x1 and y1 specify the top left corner of the object
	 */
	public boolean stillInBounds(int x1, int y1)
	{
		if(x1 >= 4 && x1+tpSize[orientation].width < appletWidth-4 &&
			y1 >= 4 && y1+tpSize[orientation].height < appletHeight-4)
				return true;
		
		return false;
	}

	public static void reset()
	{
		numpicks=0;
	}
	
	public void snapToGrid(int gridWidth, int gridHeight)
	{
		double tol = 7;
		int tempX=0-(int)tol,tempY=0-(int)tol,xFactor,yFactor;
		int tempX1=0-(int)tol,tempY1=0-(int)tol,x1Factor,y1Factor;
		double d, d1;

		tol = Math.max(tol,.2 * Math.min(Math.min(gridWidth,gridHeight),Math.max(tpSize[orientation].width,tpSize[orientation].height)));

		//calculate the x,y-distances from the nearest grid point for both ends of the toothpick
		switch(orientation)
		{
			case 0: //0,180 degrees
				tempX = x;
				tempY = y + tpSize[orientation].height/2;
				tempX1 = x + tpSize[orientation].width;
				tempY1 = y + tpSize[orientation].height/2;
				break;
			case 1: //45,225 degrees
				tempX = x + tpSize[orientation].width;
				tempY = y;
				tempX1 = x;
				tempY1 = y + tpSize[orientation].height;
				break;
			case 2: //90,180 degrees
				tempX = x + tpSize[orientation].width/2;
				tempY = y;
				tempX1 = x + tpSize[orientation].width/2;
				tempY1 = y + tpSize[orientation].height;
				break;
			case 3: //135,315 degrees
				tempX = x;
				tempY = y;
				tempX1 = x + tpSize[orientation].width;
				tempY1 = y + tpSize[orientation].height;
				break;
		}

		xFactor = Math.round((float)tempX/(float)gridWidth);
		yFactor = Math.round((float)tempY/(float)gridHeight);
		xFactor *= gridWidth;
		yFactor *= gridHeight;
		tempX -= xFactor;
		tempY -= yFactor;
		
		//find the distance btwn. one end of the toothpick and the nearest gridmark
		d = Math.sqrt(tempX*tempX + tempY*tempY);
		
		x1Factor = Math.round((float)tempX1/(float)gridWidth);
		y1Factor = Math.round((float)tempY1/(float)gridHeight);
		x1Factor *= gridWidth;
		y1Factor *= gridHeight;
		tempX1 = Math.abs(tempX1-x1Factor);
		tempY1 = Math.abs(tempY1-y1Factor);
		
		//find the distance btwn. the other end of the toothpick and the nearest gridmark
		d1 = Math.sqrt(tempX1*tempX1 + tempY1*tempY1);
		
		switch(orientation)
		{
			case 0: //0,180 degrees
				if(d < tol)
				{
					setX(xFactor+1);
					setY(yFactor-tpSize[orientation].height/2);
				}
				else if(d1 < tol)
				{
					setX(x1Factor-tpSize[orientation].width);
					setY(y1Factor-tpSize[orientation].height/2);
				}
				break;
			case 1: //45,225 degrees
				if(d < tol)
				{
					setX(xFactor-tpSize[orientation].width);
					setY(yFactor);
				}
				else if(d1 < tol)
				{
					setX(x1Factor);
					setY(y1Factor-tpSize[orientation].height);
				}
				break;
			case 2: //90,180 degrees
				if(d < tol)
				{
					setX(xFactor-tpSize[orientation].width/2);
					setY(yFactor+1);
				}
				else if(d1 < tol)
				{
					setX(x1Factor-tpSize[orientation].width/2);
					setY(y1Factor-tpSize[orientation].height);
				}
				break;
			case 3: //135,315 degrees
				if(d < tol)
				{
					setX(xFactor);
					setY(yFactor);
				}
				else if(d1 < tol)
				{
					setX(x1Factor-tpSize[orientation].width);
					setY(y1Factor-tpSize[orientation].height);
				}
				break;
		}
	}

} // END CLASS Pick