/* Billiard Ball Computer

   James Lin
   Caltech - CS 138
   April 30, 1996

   Consilidated constants into BBallConsts interface,
   added accessor functions to Critter class,
   removed underscore from data member names:         July 27, 1996
*/

import java.awt.*;
import java.lang.*;
import java.net.*;
import java.util.Vector;

interface BBallConsts {
  static final int BALL_N = 0;
  static final int BALL_E = 1;
  static final int BALL_S = 2;
  static final int BALL_W = 3;

  static final int MIRROR_NW = -1;
  static final int MIRROR_NE = -2;

  static final int UP = 0;
  static final int RIGHT = 1;
  static final int DOWN = 2;
  static final int LEFT = 3;

  static final int BALL_ERASE = -10;
  static final int MIRROR_ERASE = -11;
  
  static final int GRIDSIZE = 10;
}

class Critter implements BBallConsts {
  private Point  p;
  private int cr_type;

  public Critter()
  {
    p = new Point(1,1);
    cr_type = BALL_E;
  }

  public Critter(int x, int y, int curType)
  {
    if (x < 0)
      x = 0;
    if (y < 0)
      y = 0;
    p = new Point(x, y);
    cr_type = curType;
  }

  public Critter copy()
  {
    return new Critter(p.x, p.y, cr_type);
  }

  public boolean isBall()
  {
    return (cr_type == BALL_N || cr_type == BALL_S ||
      cr_type == BALL_E || cr_type == BALL_W);
  }

  public boolean isMirror()
  {
    return (cr_type == MIRROR_NW || cr_type == MIRROR_NE);
  }

  public void setX(int x)
  {
    p.x = x;
  }

  public int getX()
  {
    return p.x;
  }

  public void setY(int y)
  {
    p.y = y;
  }

  public int getY()
  {
    return p.y;
  }

  public void setType(int type)
  {
    cr_type = type;
  }

  public int getType()
  {
    return cr_type;
  }
}

public class BBall extends java.applet.Applet implements Runnable, BBallConsts {
  private Thread thread;
  private boolean isSuspended = true;
  private int speed;
  public Image nBallImage;
  public Image sBallImage;
  public Image eBallImage;
  public Image wBallImage;
  public Image nwMirrorImage;
  public Image neMirrorImage;
  public Vector  critters;
  BBallCanvas  canvas;
  BBallControls controls;


  public void init()
  {
    String speedParam = getParameter("speed");
    if (speedParam == null)
      speed = 250;
    else
      speed = 100 * Integer.valueOf(speedParam).intValue();

    nBallImage = getImage(getDocumentBase(), "blue.gif");
    sBallImage = getImage(getDocumentBase(), "green.gif");
    eBallImage = getImage(getDocumentBase(), "red.gif");
    wBallImage = getImage(getDocumentBase(), "orange.gif");
    nwMirrorImage = getImage(getDocumentBase(), "nwmirror.gif");
    neMirrorImage = getImage(getDocumentBase(), "nemirror.gif");

    int w = nBallImage.getWidth(this);    // dummy reads
    int h = nBallImage.getHeight(this);

    critters = new Vector();
    setLayout(new BorderLayout());

    controls = new BBallControls(this);
    canvas = new BBallCanvas(this);

    add("Center", canvas);
    add("West", controls);
  }

  public boolean handleEvent(Event e) {
    if (e.id == Event.WINDOW_DESTROY) {
      System.exit(0);
    }
    return false;
  }
  
  public void start() {
    thread = new Thread(this);
    thread.start();
  }

  public void stop() {
    thread.stop();
  }

  public void addCritter(int x, int y, int new_type)
  {
    synchronized(critters) {
      critters.addElement(new Critter(x, y, new_type));      
    }

    canvas.repaint();
  }

  public void moveBalls()
  {
    synchronized(critters) {
      for (int i = 0; i < critters.size(); i++) {
        Critter critter = (Critter)critters.elementAt(i);

        switch (critter.getType()) {
        case BALL_N:
          critter.setY(critter.getY() - GRIDSIZE);
          break;
        case BALL_S:
          critter.setY(critter.getY() + GRIDSIZE);
          break;
        case BALL_E:
          critter.setX(critter.getX() + GRIDSIZE);
          break;
        case BALL_W:
          critter.setX(critter.getX() - GRIDSIZE);
          break;
        }

        if (critter.getX() > canvas.size().width || critter.getX() < 0 ||
            critter.getY() > canvas.size().height || critter.getY() < 0) {
          critters.removeElementAt(i);
        }
      }
    }
    canvas.repaint();
  }

  private int absDirection(int origDirection, int relChange)
  {
    return (origDirection + relChange) % 4;
  }

  private int absMirrorOrientation(int origDirection, int relMirror)
  {
    if (relMirror == MIRROR_NW) {
      if (origDirection == BALL_N ||
          origDirection == BALL_S) {
        return MIRROR_NW;
      }
      else {
        return MIRROR_NE;
      }
    }
    else {
      if (origDirection == BALL_N ||
          origDirection == BALL_S) {
        return MIRROR_NE;
      }
      else {
        return MIRROR_NW;
      }
    }
  }  

  private int absXOffset(int direction, int relDx, int relDy)
  {
    if (direction == UP)
      return relDx;
    else if (direction == RIGHT)
      return -relDy;
    else if (direction == DOWN)
      return -relDx;
    else
      return relDy;
  }

  private int absYOffset(int direction, int relDx, int relDy)
  {
    if (direction == UP)
      return relDy;
    else if (direction == RIGHT)
      return relDx;
    else if (direction == DOWN)
      return -relDy;
    else
      return -relDx; 
  }

  public void rotateBalls()
  {
    synchronized(critters) {
      
      Vector newCritters = new Vector();
      for (int i = 0; i < critters.size(); i++) {
        newCritters.addElement(((Critter)critters.elementAt(i)).copy());
      }
      
      for (int i = 0; i < critters.size(); i++) {
        Critter critter = (Critter)critters.elementAt(i);
        Critter newCritter = (Critter)newCritters.elementAt(i);
        int x = critter.getX();
        int y = critter.getY();
        int dir = critter.getType();
        Critter critterL = null;
        Critter critterR = null;

        for (int j = 0; j < critters.size(); j++) {
          if (i != j) {
            Critter critter2 = (Critter)critters.elementAt(j);
            int x2 = critter2.getX();
            int y2 = critter2.getY();

            if (x2 == x + absXOffset(dir, -GRIDSIZE, -GRIDSIZE) &&
                y2 == y + absYOffset(dir, -GRIDSIZE, -GRIDSIZE)) {
              if (critterL == null && critter2.isBall())
                critterL = critter2;
            }
            else if (x2 == x + absXOffset(dir, -GRIDSIZE/2, -GRIDSIZE/2) &&
               y2 == y + absYOffset(dir, -GRIDSIZE/2, -GRIDSIZE/2)) {
              if (critter2.getType() == absMirrorOrientation(dir, MIRROR_NE))                  
                critterL = critter2;
            }
            else if (x2 == x + absXOffset(dir, GRIDSIZE/2, -GRIDSIZE/2) &&
               y2 == y + absYOffset(dir, GRIDSIZE/2, -GRIDSIZE/2)) {
              if (critter2.getType() == absMirrorOrientation(dir, MIRROR_NW))
                critterR = critter2;
            }
            else if (x2 == x + absXOffset(dir, GRIDSIZE, -GRIDSIZE) &&
               y2 == y + absYOffset(dir, GRIDSIZE, -GRIDSIZE)) {
              if (critterR == null && critter2.isBall())
                critterR = critter2;
            }

            if (critterL != null & critterR != null) {
              if ((critterL.getType() == absDirection(dir, DOWN) ||
                  critterL.getType() == absDirection(dir, RIGHT) ||
                  critterL.getType() == absMirrorOrientation(dir, MIRROR_NE)) &&
            
                  (critterR.getType() == absDirection(dir, DOWN) ||
                  critterR.getType() == absDirection(dir, LEFT) ||
                  critterR.getType() == absMirrorOrientation(dir, MIRROR_NW))) {
                newCritter.setType(absDirection(dir, DOWN));
              }
            }
            else if (critterL != null) {
              if (critterL.getType() == absDirection(dir, RIGHT) ||
                  critterL.getType() == absDirection(dir, DOWN) ||
                  critterL.getType() == absMirrorOrientation(dir, MIRROR_NE)) {
                newCritter.setType(absDirection(dir, RIGHT));
              }
            }
            else if (critterR != null) {
              if (critterR.getType() == absDirection(dir, LEFT) ||
                  critterR.getType() == absDirection(dir, DOWN) ||
                  critterR.getType() == absMirrorOrientation(dir, MIRROR_NW)) {
                newCritter.setType(absDirection(dir, LEFT));
              }
            }
          }
        }
      }
      critters = (Vector)newCritters.clone();
    }
    canvas.repaint();
  }

  public void run()
  {
    while (true) {
      if (!isSuspended) {
        rotateBalls();
        moveBalls();
        try {
          Thread.currentThread().sleep(speed);
        } catch (InterruptedException e) {}
      }
      if (isSuspended) {
        thread.suspend();
      }
    }
  }

  public void stopMoving()
  {
    isSuspended = true;
  }

  public void startMoving()
  {
    isSuspended = false;
    thread.resume();
  }
}

class BBallControls extends Panel implements BBallConsts {
  private BBall app;
  private CheckboxGroup toolbox;
  
  public BBallControls(BBall theApp) {
    app = theApp;
    setLayout(new GridLayout(0,1));

    add(new Label("Balls:"));

    toolbox = new CheckboxGroup();
    add(new Checkbox("North", toolbox, true));
    add(new Checkbox("South", toolbox, false));
    add(new Checkbox("East", toolbox, false));
    add(new Checkbox("West", toolbox, false));
    add(new Checkbox("erase ball", toolbox, false));

    add(new Label(""));
    add(new Label("Mirrors:"));

    add(new Checkbox("NE to SW", toolbox, false));
    add(new Checkbox("NW to SE", toolbox, false));
    add(new Checkbox("erase mirror", toolbox, false));

    add(new Label(""));

    add(new Button("Go"));
    add(new Button("Stop"));

    add(new Label(""));

    add(new Button("Step"));
  }
  
  public boolean action(Event ev, Object arg) {
    if (ev.target instanceof Button) {
      String label = (String)arg;

      if (arg == "Step") {
        app.rotateBalls();
        app.moveBalls();
      }
      else if (arg == "Go") {
        app.startMoving();
      }
      else if (arg == "Stop") {
        app.stopMoving();
      }
      return true;
    }
    
    return false;
  }

  public int curType()
  {
    int id;

    id = BALL_ERASE;

    if (toolbox != null) {
      String selectedLabel = toolbox.getCurrent().getLabel();
      if (selectedLabel == "North")
        id = BALL_N;
      else if (selectedLabel == "South")
        id = BALL_S;
      else if (selectedLabel == "East")
        id = BALL_E;
      else if (selectedLabel == "West")
        id = BALL_W;
      else if (selectedLabel == "erase ball")
        id = BALL_ERASE;
      else if (selectedLabel == "NW to SE")
        id = MIRROR_NW;
      else if (selectedLabel == "NE to SW")
        id = MIRROR_NE;
      else // selectedLabel == "erase mirror"
        id = MIRROR_ERASE;
    }
    return id;
  }

  public boolean curTypeIsBall()
  {
    int type = curType();

    return (type == BALL_N || type == BALL_S ||
      type == BALL_E || type == BALL_W ||
      type == BALL_ERASE);
  }

  public boolean curTypeIsMirror()
  {
    int type = curType();

    return (type == MIRROR_NW || type == MIRROR_NE || type == MIRROR_ERASE);
  }

}

class BBallCanvas extends Canvas implements BBallConsts {
  private BBall app;
  private int prevw, prevh;
  private Image offimage;

  private int gridX = -1;
  private int gridY = -1;

  public BBallCanvas(BBall theApp) {
    app = theApp;
  }
  
  public void paint(Graphics g) {
    update(g);
  }

  public synchronized void update(Graphics g) {
    Dimension d = size();
    try {
      if (offimage == null || d.width != prevw || d.height != prevh) {
        prevw = d.width;
        prevh = d.height;
        offimage = createImage(d.width, d.height);
      }
      Graphics offg = offimage.getGraphics();
      offPaint(offg);
      g.drawImage(offimage, 0, 0, this);
    } catch (java.lang.OutOfMemoryError e) {
      System.out.println("Applet Banner ran out of memory");
      System.exit(1);
    }
  }

  void offPaint(Graphics g) {
    Dimension d = size();

    g.clearRect(0, 0, d.width, d.height);
    g.setColor(Color.white);
    g.fillRect(0, 0, d.width, d.height);
    g.setColor(Color.gray);

    for (int x = 0; x < d.width; x += GRIDSIZE) {
      g.drawLine(x, 0, x, d.height - 1);
    }

    for (int y = 0; y < d.height; y += GRIDSIZE) {
      g.drawLine(0, y, d.width - 1, y);
    }

    g.setColor(Color.red);
    g.drawLine(0, 0, d.width-1, 0);
    g.drawLine(d.width-1, 0, d.width-1, d.height-1);
    g.drawLine(d.width-1, d.height-1, 0, d.height-1);
    g.drawLine(0, d.height-1, 0, 0);

    // draw outline of where to put next object
    if (gridX >= 0 && gridY >= 0) {
      if (app.controls.curTypeIsMirror())
        g.drawRect(gridX, gridY, GRIDSIZE, GRIDSIZE);
      else {
        g.drawRoundRect(gridX - GRIDSIZE/2, gridY - GRIDSIZE/2,
                        GRIDSIZE, GRIDSIZE, GRIDSIZE, GRIDSIZE);
      }
    }
  
    // draw the balls and mirrors
    int  i;
    synchronized(app.critters) {
      for (i = 0; i < app.critters.size(); i++) {
        Critter  critter = (Critter)app.critters.elementAt(i);
        Image critterImage;
        int x = critter.getX();
        int y = critter.getY();

        switch (critter.getType()) {
        case BALL_N:
          critterImage = app.nBallImage;
          break;
        case BALL_S:
          critterImage = app.sBallImage;
          break;
        case BALL_E:
          critterImage = app.eBallImage;
          break;
        case BALL_W:
          critterImage = app.wBallImage;
          break;
        case MIRROR_NW:
          critterImage = app.nwMirrorImage;
          break;
        default: //case MIRROR_NE:
          critterImage = app.neMirrorImage;
          break;
        }

        int w = critterImage.getWidth(this);
        int h = critterImage.getHeight(this);

        x -= w/2;
        y -= h/2;

        g.drawImage(critterImage, x, y, this);
      }
    }
    app.controls.repaint();
  }

  public boolean mouseEnter(Event evt, int x, int y)
  {
    mouseMove(evt, x, y);

    return true;
  }

  public boolean mouseMove(Event evt, int x, int y)
  {
    int newGridX;
    int newGridY;

    newGridX = x / GRIDSIZE;
    newGridX = newGridX * GRIDSIZE;

    newGridY = y / GRIDSIZE;
    newGridY = newGridY * GRIDSIZE;

    if (newGridX != gridX || newGridY != gridY) {
      gridX = newGridX;
      gridY = newGridY;
      repaint();
    }
    return true;
  }

  public boolean mouseExit(Event evt, int x, int y)
  {
    gridX = -1;
    gridY = -1;
    repaint();

    return true;
  }

  public boolean mouseUp(Event evt, int x, int y) {
    int curType = app.controls.curType();

    boolean found = false;
    boolean canAdd;

    int newX;
    int newY;
    
    synchronized(app.critters) {
      if (app.controls.curTypeIsMirror()) {
        newX = gridX + GRIDSIZE/2;
        newY = gridY + GRIDSIZE/2;
      }
      else {
        newX = gridX;
        newY = gridY;
      }
      canAdd = true;

      for (int i = app.critters.size() - 1; !found && i >= 0; i--) {
        Critter  critter = (Critter)app.critters.elementAt(i);
        int px = critter.getX();
        int py = critter.getY();

        if (px == newX && py == newY) {
          app.critters.removeElementAt(i);
          found = true;
        }
        
        if (app.controls.curTypeIsBall() &&
            app.controls.curType() != BALL_ERASE) {

          if (critter.getType() == MIRROR_NW &&
              critter.getX() - GRIDSIZE/2 == newX &&
              critter.getY() - GRIDSIZE/2 == newY) {
            canAdd = false;
          }
          else if (critter.getType() == MIRROR_NW &&         
              critter.getX() + GRIDSIZE/2 == newX &&
              critter.getY() + GRIDSIZE/2 == newY) {
            canAdd = false;
          }
          else if (critter.getType() == MIRROR_NE &&
              critter.getX() + GRIDSIZE/2 == newX &&
              critter.getY() - GRIDSIZE/2 == newY) {
            canAdd = false;
          }
          else if (critter.getType() == MIRROR_NE &&
              critter.getX() - GRIDSIZE/2 == newX &&
              critter.getY() + GRIDSIZE/2 == newY) {
            canAdd = false;      
          } 
        }
        else if (app.controls.curType() == MIRROR_NW) {
          if (critter.isBall() &&
              critter.getX() + GRIDSIZE/2 == newX &&
              critter.getY() + GRIDSIZE/2 == newY) {
            canAdd = false;
          }
          else if (critter.isBall() &&
             critter.getX() - GRIDSIZE/2 == newX &&
             critter.getY() - GRIDSIZE/2 == newY) {
            canAdd = false;
          }
        }
        else if (app.controls.curType() == MIRROR_NE) {
          if (critter.isBall() &&
              critter.getX() - GRIDSIZE/2 == newX &&
              critter.getY() + GRIDSIZE/2 == newY) {
            canAdd = false;
          }
          else if (critter.isBall() &&
             critter.getX() + GRIDSIZE/2 == newX &&
             critter.getY() - GRIDSIZE/2 == newY) {
            canAdd = false;
          }
        }
      }

      if (curType != BALL_ERASE && curType != MIRROR_ERASE && canAdd) {
        app.addCritter(newX, newY, curType);
      }      
    }
    repaint();
    return true;
  }
}
