import java.applet.Applet;
import java.awt.*;
import java.awt.event.*;
import java.util.*;

public class PacmanApplet extends Applet
implements Runnable, MouseListener, MouseMotionListener, ActionListener,
AdjustmentListener
{
    GameObject Pacman, Ghost, Dot;
    private Thread thread;
    private boolean isRunning = false;
    Dimension boardSize;
    Image dotPic = null;
    Random rand;
    Panel pacBoard;
    Checkbox [] chbBehavior;
    Panel controlPanel = new Panel ();
    Label statusLabel = new Label ("Applet is loading, please wait...");
    Button startButton = new Button ("Reset");
    final int repaintTime = 25;
    final int DUMB = 0, SMARTER = 1, AVOIDING = 2;
    boolean debugging = false;
    Scrollbar hsbPacSpeed, hsbGhostSpeed;
     

    public void init ()
    {
	rand = new Random ();

	setLayout (new BorderLayout(2, 2));

	setupControlPanel ();
	add ("South", controlPanel);
	//int h = Integer.parseInt (getParameter ("height"));
	//int w = Integer.parseInt (getParameter ("width"));
	pacBoard = new Panel ();
	add("Center", pacBoard);

	//Make graphics
	Image dotPic = createImage (20, 20);
	Graphics dotGraph = dotPic.getGraphics ();

	dotGraph.fillRect (0, 0, 20, 20);
	dotGraph.drawLine (5, 5, 10, 10);

	Pacman = new GameObject (getImage (getCodeBase (), "pacman.jpg"),
	    pacBoard, 2.5);
	Ghost = new GameObject (getImage (getCodeBase (), "ghost.jpg"),
	    pacBoard, 2.0);
	Dot = new GameObject (getImage (getCodeBase (), "dot.jpg"),
	    pacBoard);
	hsbPacSpeed.setValue (5);
	hsbGhostSpeed.setValue (4);

	thread = null;

	pacBoard.addMouseListener (this);
	pacBoard.addMouseMotionListener (this);
    }

    public void setupControlPanel ()
    {
	controlPanel.setLayout(new BorderLayout (2, 2));

	//center panel contains status label and speed scrollbars
	Panel midPanel = new Panel ();
	controlPanel.add ("Center", midPanel);
	midPanel.setLayout(new BorderLayout (2, 2));
	statusLabel.setAlignment (Label.CENTER);
	/*
	Font f = getFont ().deriveFont (Font.BOLD);
	statusLabel.setFont (f);
	*/
	midPanel.add ("North", statusLabel);
	Panel barsPanel = new Panel ();
	midPanel.add ("Center", barsPanel);
	Label l1 = new Label ("Pacman's Speed"),
	    l2 = new Label ("Ghost's Speed");
	l1.setAlignment (Label.RIGHT);
	l2.setAlignment (Label.RIGHT);
	barsPanel.setLayout(new GridLayout(2, 2, 4, 4));
	hsbPacSpeed = new Scrollbar (Scrollbar.HORIZONTAL, 0, 1, 0, 20);
	hsbGhostSpeed = new Scrollbar (Scrollbar.HORIZONTAL, 0, 1, 0, 20);
	hsbPacSpeed.addAdjustmentListener (this);
	hsbGhostSpeed.addAdjustmentListener (this);
	barsPanel.add (l1);
	barsPanel.add (hsbPacSpeed);
	barsPanel.add (l2);
	barsPanel.add (hsbGhostSpeed);

	Panel behaviorPanel = new Panel ();
	controlPanel.add ("West", behaviorPanel);
	Panel movementPanel = new Panel ();
	behaviorPanel.add ("West", movementPanel);
	movementPanel.setLayout (new BorderLayout ());
	movementPanel.add ("West", new Label ("Movement type:"));
	Panel checkboxPanel = new Panel ();
	movementPanel.add ("Center", checkboxPanel);
	checkboxPanel.setLayout(new GridLayout(3, 1));
	CheckboxGroup cbg = new CheckboxGroup ();
	chbBehavior = new Checkbox [3];
	chbBehavior [0] = new Checkbox ("Diagonals and lines", cbg, false);
	chbBehavior [1] = new Checkbox ("Straight lines", cbg, false);
	chbBehavior [2] = new Checkbox ("Avoid enemy", cbg, true);
	for (int i = 0; i < 3; i ++)
	{
	    checkboxPanel.add (chbBehavior [i]);
	}

	//right side of control panel contains the "reset" button
	controlPanel.add ("East", startButton);
	startButton.addActionListener (this);

    }

    public void initObjs ()
    {
//	debugMsg ("size is " + boardSize.width + ", " + boardSize.height);
	Pacman.setRandomly ();
	Ghost.setRandomly ();
	Dot.setRandomly ();
    }

    public void begin ()
    {
	initObjs ();
	iLive = true;
	statusMsg ("Running...");
    }

    public void start ()
    {
	boardSize = pacBoard.getSize ();
	begin ();
	if (thread == null)
	{
	    thread = new Thread (this);
	}
	isRunning = true;
	thread.start ();
    }

    boolean iLive = true;
    public void run ()
    {
	while (isRunning)
	{
	    if (iLive)
	        action ();

	    try 
            {
	        thread.sleep (20);
	    }
	    catch (InterruptedException e)
	    {}
	}
    }
    public void stop ()
    {
	if ((thread != null) && thread.isAlive ())
	{
	    //thread.stop ();
	    isRunning = false;
	    thread = null;
	}
    }
    
    public void paint (Graphics g)
    {
	setForeground (Color.black);
	g.fillRect (0, 0, boardSize.width, boardSize.height);

	Dot.draw (g);
	Pacman.draw (g);
	Ghost.draw (g);
    }

    Image Buffer = null;
    Graphics bufGraphics = null;
    public final void update (Graphics g)
    {
	if (Buffer == null)
	{
	    Buffer = createImage (boardSize.width, boardSize.height);
	    bufGraphics = Buffer.getGraphics ();
	}
	paint (bufGraphics);

	Graphics boardGraphics = pacBoard.getGraphics ();
	boardGraphics.drawImage (Buffer, 0, 0, null);
	//paint (g);
    }

    public void statusMsg (String msg)
    {
	if (!debugging)
	{
	    statusLabel.setText ("Status:     " + msg);
	}
    }

    public void debugMsg (String msg)
    {
	debugging = true;
	statusLabel.setText ("Debug: " + msg);
    }

    boolean mouseDown = false;
    public void action ()
    {
	double dist;

	if (chbBehavior [2].getState ())
	    dist = Ghost.chaseWhileAvoiding (Pacman, Dot);
	else if (chbBehavior [1].getState ())
	    dist = Ghost.chase (Pacman);
	else
	    dist = Ghost.chase2 (Pacman);
	if (dist < 25.0)
	{
	    iLive = false;
	    statusMsg ("Pacman is dead!!!");
	}
	else
	{
	}
	if (!mouseDown)
	{
	    if (chbBehavior [2].getState ())
		dist = Pacman.chaseWhileAvoiding (Dot, Ghost);
	    else if (chbBehavior [1].getState ())
		dist = Pacman.chase (Dot);
	    else
		dist = Pacman.chase2 (Dot);
	    if (dist < 2.0)
	    {
		Dot.setRandomly ();
	    }
	}
	repaint ();
    }

	//////////////////////////////////////////
	//	ActionListener actions
	//////////////////////////////////////////
    public void actionPerformed (ActionEvent e)
    {
	begin ();
    }

	//////////////////////////////////////////
	//	AdjustmentListener actions
	//////////////////////////////////////////
    public void adjustmentValueChanged(AdjustmentEvent e)
    {
	Pacman.setSpeed (hsbPacSpeed.getValue () / 2.0);
	Ghost.setSpeed (hsbGhostSpeed.getValue () / 2.0);
    }

	//////////////////////////////////////////
	//	MouseListener actions
	//////////////////////////////////////////
    public void mouseEntered (MouseEvent e)
    {
    }
    public void mouseExited (MouseEvent e)
    {
    }
    public void mousePressed (MouseEvent e)
    {
	mouseDown = true;
    }
    public void mouseReleased (MouseEvent e)
    {
	mouseDown = false;
    }
    public void mouseClicked (MouseEvent e)
    {
	//Pacman.setLocation (e.getX (), e.getY ());
    }

	//////////////////////////////////////////
	//	MouseEventListener actions
	//////////////////////////////////////////
    public void mouseMoved (MouseEvent e)
    {
	if (mouseDown)
	{
	    //Pacman.setLocation (e.getX (), e.getY ());
	    //repaint ();
	}
    }
    public void mouseDragged (MouseEvent e)
    {
	Pacman.setLocation (e.getX (), e.getY ());
	//repaint ();
    }

    private class GameObject
    {
	protected Vector2d pos;
	protected Image pic;
	protected double speed;
	protected Dimension size;
	protected boolean sizeSet;
	protected Component location;
	
	public void init (Image p, Component l, double s)
	{
	    pic = p;
	    location = l;
	    speed = s;
	    pos = new Vector2d ();
	    sizeSet = false;

	    while (pic.getWidth (l) == -1)
		;    //have to sit and wait for image to load
	    size = new Dimension (pic.getWidth (l), pic.getHeight (l));
	    //debugMsg ("size is " + size.width + " " + size.height);
	}
	public GameObject (Image p, Component l, double s)
	{
	    init (p, l, s);
	}
	public GameObject (Image p, Component l)
	{
	    init (p, l, 0.);
	}

	public void draw (Graphics g)
	{
	    g.drawImage (pic, (int)pos.x - size.width/2,
		(int)pos.y - size.height/2, location);
	}

	public void setSpeed (double s)
	{
	    speed = s;
	}

	public void setLocation (int x, int y)
	{
	    setLocation ((double)x, (double)y);
	}

	public void setLocation (double x, double y)
	{
	    pos.x = x;
	    pos.y = y;
	    repaint ();
//	    repaint (repaintTime);
	}

	public void setRandomly ()
	{
	    setLocation (rand.nextDouble () * (boardSize.width-20.0) +10.0,
	        rand.nextDouble () * (boardSize.height-20.0)+10.0);
	}

	public void move (double amountX, double amountY)
	{
	    int oldX = (int)pos.x, oldY = (int)pos.y,
		maxX, minX, maxY, minY;
	    
	    pos.x += amountX;
	    pos.y += amountY;

	    repaint (repaintTime);
	    //repaint (oldX, oldY, (int)amountX, (int)amountY);
	    //debugMsg ("Redrawing at " + oldX + "  " + oldY +
	//	"  " + (int)amountX + "  " + (int)amountY);

//	    minX = amountX < 0 ? pos.x : oldX;
	    //maxX = amountX > 0 ? pos.x : oldX;
//	    minY = amountY < 0 ? pos.y : oldY;
	    //maxY = amountY > 0 ? pos.y : oldY;
	    //repaint ((int)minX, (int)minY, (int)Math.abs (amountX), (int)Math.abs (amountY));
	    //repaint ();

	}

	public void move (Vector2d moving)
	{
	    move (moving.x, moving.y);
	}

	//getDistance function:
	//Returns the distance between this and another GameObject.
	//To do this, subtract position of this from position of
	//other, then get the length of the resulting vector
	protected double getDistance (GameObject other)
	{
	    return other.pos.sub (pos).getLength ();
	}

	//isBetween function:
	//This function will determine whether the object is in between
	//the first and second objects that are past as parameters.
	//I will define "between" to mean that the angle between
	//first-this-second is between 90 and -90 degrees.
	private boolean isBetween (GameObject first, GameObject second)
	{
	    double vx1 = first.pos.x-pos.x;
	    double vy1 = first.pos.y-pos.y;
	    double vx2 = second.pos.x-pos.x;
	    double vy2 = second.pos.y-pos.y;

	    double dotProduct = vx1*vx2 + vy1*vy2;
             //double l1 = Math.sqrt (vx1*vx1 + vy1*vy1);
//double l2 = Math.sqrt (vx2*vx2 + vy2*vy2);
	    //double cosAngle = dotProduct / (l1*l2);

	    //debugMsg ("cosAngle is " + cosAngle);
	    //debugMsg ("l1 " + l1 + ", l2 " + l2);
	    return (dotProduct < 0.0);
	}

	public double chase (GameObject victim)
	{
	    double newX = pos.x, newY = pos.y;
	    double totalDist = getDistance (victim);

	    if (totalDist < speed)
	    {
		setLocation (victim.pos.x, victim.pos.y);
            }
	    else
            {
	        double xDist = (victim.pos.x-pos.x) / totalDist,
		    yDist = (victim.pos.y-pos.y) / totalDist;
		move (speed*xDist, speed*yDist);
	    }

	    return totalDist;
	}

	public double chase2 (GameObject victim)
	{
	    double newX = pos.x, newY = pos.y;
	    if (pos.x < victim.pos.x)
	    {
		newX += speed;
		if (newX > victim.pos.x)
		    newX = victim.pos.x;
	    }
	    else if (pos.x > victim.pos.x)
	    if (pos.x > victim.pos.x)
	    {
		newX -= speed;
		if (newX < victim.pos.x)
		    newX = victim.pos.x;
	    }
	    if (pos.y < victim.pos.y)
	    {
		newY += speed;
		if (newY > victim.pos.y)
		    newY = victim.pos.y;
	    }
	    else if (pos.y > victim.pos.y)
	    {
		newY -= speed;
		if (newY < victim.pos.y)
		    newY = victim.pos.y;
	    }
	    setLocation (newX, newY);
	    return (Math.abs (newX - victim.pos.x) +
		    Math.abs(newY - victim.pos.y));
	}

	public double chaseWhileAvoiding (GameObject victim, GameObject pursuer)
	{
	    final int WarningZone = 80;
	    final double DangerZone = 27.1;

	    double purDist = getDistance (pursuer);
	    double fearFactor;
	    if (purDist < DangerZone)
		fearFactor = 1.0;
	    else
		fearFactor = 1.0 / (Math.pow (purDist-DangerZone, .4));

	    Vector2d attack = victim.pos.sub (pos);
	    double dist = attack.getLength ();
	    //	    attack.x /= dist;
	    //attack.y /= dist;

	    if (dist < speed)
	    {
		setLocation (victim.pos.x, victim.pos.y);
            }
	    else
            {
		//statusMsg ("Fear factor is " + fearFactor);
		attack.normalize ();
		Vector2d escape = pos.sub (pursuer.pos);
		escape.normalize ();

		Vector2d movement = escape.mult (fearFactor).add
		    (attack.mult (1.0-fearFactor));
		movement.normalize ();
		movement = keepInBounds (movement);

		movement = movement.mult (speed);

		move (movement);
	    }

	    return getDistance (victim);
	}

	public Vector2d keepInBounds (Vector2d movement)
	{
	    double xMin = size.width / 2.,
		xMax = boardSize.width - xMin,
		yMin = size.height / 2.,
		yMax = boardSize.height - yMin;
	    double newX = pos.x+movement.x, newY = pos.y+movement.y;
	    Vector2d awayFromXMin = new Vector2d (),
		awayFromXMax = new Vector2d (),
		awayFromYMin = new Vector2d (),
		awayFromYMax = new Vector2d ();

	    double acceptableDist = 2.;
	    double factor;
	   

	    if (pos.x-xMin < acceptableDist)
	    {
		awayFromXMin.x = 1 - ((pos.x-xMin) / (acceptableDist));
		awayFromXMin.x *= awayFromXMin.x;
		if (awayFromXMin.x > 1.0)
		    awayFromXMin.x = 1.0;
		movement = movement.mult (1.-awayFromXMin.x);
		movement = movement.add (awayFromXMin);
		movement.normalize ();
	    }
	    if (xMax-pos.x < acceptableDist)
	    {
		awayFromXMax.x = 1 - ((xMax-pos.x) / (acceptableDist));
		awayFromXMax.x *= awayFromXMax.x;
		if (awayFromXMax.x > 1.0)
		    awayFromXMax.x = 1.0;
		movement = movement.mult (1-awayFromXMax.x);
		movement = movement.sub (awayFromXMax);
		movement.normalize ();
	    }
	    if (pos.y-yMin < acceptableDist)
	    {
		awayFromYMin.y = 1 - ((pos.y-yMin) / (acceptableDist));
		awayFromYMin.y *= awayFromYMin.y;
		if (awayFromYMin.y > 1.0)
		    awayFromYMin.y = 1.0;
		movement = movement.mult (1-awayFromYMin.y);
		movement = movement.add (awayFromYMin);
		movement.normalize ();
	    }
	    if (yMax-pos.y < acceptableDist)
	    {
		awayFromYMax.y = 1 - ((yMax-pos.y) / (acceptableDist));
		awayFromYMax.y *= awayFromYMax.y;
		if (awayFromYMax.y > 1.0)
		    awayFromYMax.y = 1.0;
		movement = movement.mult (1-awayFromYMax.y);
		movement = movement.sub (awayFromYMax);
		movement.normalize ();
	    }
	    return movement;
	}
	public double chaseWhileAvoiding2 (GameObject victim, GameObject pursuer)
	{
	    final int WarningZone = 80;
	    final int DangerZone = 40;
	    double purDist = getDistance (pursuer);

	    if (purDist > WarningZone || !pursuer.isBetween (victim, this))
	    {
		statusMsg ("totally cool");
		//it is safe to pursue food, since pursuer is far away...
		return chase (victim);
	    }
	    else if (purDist > DangerZone) 
	    {
		statusMsg ("a little nervous");
		//the monster is close, but not TOO close... we want to sort
		//of sidle around it while still heading in the general
		//direction of the food.
		double xDir = speed*(pos.y-pursuer.pos.y) / purDist;
		double yDir = speed*(pursuer.pos.x-pos.x) / purDist;
		double newx = pos.x+xDir, newy = pos.y+yDir;
		double vicDist = getDistance (victim);

		if (Math.abs ((newx-victim.pos.x)*(newx-victim.pos.x)
		    + (newy-victim.pos.y)*(newy-victim.pos.y))
		    < vicDist*vicDist)
		{
		    move (xDir, yDir);
		}
		else
		{
		    move (-xDir, -yDir);
		}
	    }
	    else
	    {
		statusMsg ("really scared!");
		//The monster is very close!  Move directly away from it as
		//fast as you can!!
		double xDir = speed*(pos.x-pursuer.pos.x) / purDist;
		double yDir = speed*(pos.y-pursuer.pos.y) / purDist;
		move (xDir, yDir);
	    }
	    return getDistance (victim);
	}
    };
};

