/*
	Project: Balls2D
	Author: Simon Gayton
	Copyright 2007 (C) Simon Gayton
*/

import java.applet.*;
import java.awt.*;
import java.awt.event.*;

/*
  This applet implements a simple simulation of a group of 2D balls.
*/
public class Balls2D extends Applet
implements Runnable, KeyListener, MouseListener, MouseMotionListener {
  Thread mainThread;
  Image backImage;
  Graphics backGraphics;
  Vector2D wallPt[];
  Edge2D edge[];
  Ball2D ball[];
  Vector2D mousePt;
  Ball2D activeBall;
  Ball2D dragBall;
  int mode; // 0 = shooting, 1 = dragging.
  static Color black;
  static Color gray;
  static Color white;
  static Color red;
  static Color green;
  static Color blue;
  static Vector2D gravity;
  static float e;
  static float spring;
  
  /*
    Initialize the simulation.
  */
  public void init() {
    // Register this applet as having event listeners.
    addKeyListener(this);
    addMouseListener(this);
    addMouseMotionListener(this);
    
    // Allocate the backbuffer.
    backImage = createImage(640, 480);
    backGraphics = backImage.getGraphics();
    
    // Define the vertices of the walls.
    wallPt = new Vector2D[8];

    wallPt[0] = new Vector2D(420.0f, 50.0f);
    wallPt[1] = new Vector2D(520.0f, 150.0f);
    wallPt[2] = new Vector2D(520.0f, 350.0f);
    wallPt[3] = new Vector2D(420.0f, 450.0f);
    wallPt[4] = new Vector2D(220.0f, 450.0f);
    wallPt[5] = new Vector2D(120.0f, 350.0f);
    wallPt[6] = new Vector2D(120.0f, 150.0f);
    wallPt[7] = new Vector2D(220.0f, 50.0f);

    // Generate the edges using the wall vertices.
    edge = new Edge2D[8];

    int v1 = 7;
    for (int v2 = 0; v2 < 8; ++v2) {
      edge[v2] = new Edge2D(wallPt[v1], wallPt[v2]);

      v1 = v2;
    }

    // Generate all 64 balls in an 8x8 grid.
    ball = new Ball2D[64];
    
    for (int y = 0; y < 8; ++y) {
      for (int x = 0; x < 8; ++x) {
        ball[y*8+x] = new Ball2D();
        
        ball[y*8+x].pos.x = 28.0f*(float)x+240.0f;
        ball[y*8+x].pos.y = 28.0f*(float)y+150.0f;
        
        ball[y*8+x].rad = 12.0f;
      }
    }
    
    // Initialize some state variables.
    mousePt = new Vector2D();
    activeBall = null;
    dragBall = null;
    mode = 0;

    // Define some color objects that will be used a lot.
    black = new Color(0, 0, 0);
    gray = new Color(128, 128, 128);
    white = new Color(255, 255, 255);
    red = new Color(255, 0, 0);
    green = new Color(0, 255, 0);
    blue = new Color(0, 0, 255);
    
    // Define some global physical quantities.
    gravity = new Vector2D();
    e = 0.65f;
    spring = 500.0f;
  }
  
  /*
    Start the animation/simulation thread.
  */
  public void start() {
    mainThread = new Thread(this);
    mainThread.start();
  }

  /*
    Stop the animation/simulation thread.
  */
  public void stop() {
    if (mainThread != null) {
      mainThread.stop();
      mainThread = null;
    }
  }

  /*
    Run the animation/simulation.
  */
  public void run() {
    while (mainThread != null) {
      updateSimulation(1.0f/30.0f);
      
      repaint();
      
      try {
        mainThread.sleep(1000/30);
      } catch (InterruptedException e) {
        
      }
    }
  }
  
  public void mouseDragged(MouseEvent e) {
    
  }
  
  public void mouseMoved(MouseEvent e) {
    mousePt.x = (float)e.getX();
    mousePt.y = (float)e.getY();
  }
  
  public void mouseClicked(MouseEvent e) {
    mousePt.x = (float)e.getX();
    mousePt.y = (float)e.getY();
    
    if (activeBall != null) {
      activeBall.vel.diff(activeBall.pos, mousePt);
      activeBall = null;
    } else if (dragBall != null) {
      dragBall = null;
    } else {
      Vector2D mouseToBall = new Vector2D();
    
      // Check to see if a ball is hit.
      for (int i = 0; i < 64; ++i) {
        mouseToBall.diff(ball[i].pos, mousePt);
      
        float distSqrd = Vector2D.dot(mouseToBall, mouseToBall);
      
        if (distSqrd < (ball[i].rad*ball[i].rad)) {
          if (mode == 0) {
            activeBall = ball[i];
            dragBall = null;
          } else if (mode == 1) {
            activeBall = null;
            dragBall = ball[i];
          }
          return;
        }
      }
    }
  }
  
  public void mousePressed(MouseEvent e) {
    
  }
  
  public void mouseReleased(MouseEvent e) {
    
  }
  
  public void mouseEntered(MouseEvent e) {
  
  }
  
  public void mouseExited(MouseEvent e) {
  
  }
  
  public void keyPressed(KeyEvent e) {
    switch (e.getKeyCode()) {
    case KeyEvent.VK_G:
      gravity.x = 0.0f;
      gravity.y = 0.0f;
      break;
      
    case KeyEvent.VK_LEFT:
      gravity.x = -10.0f;
      gravity.y = 0.0f;
      break;
      
    case KeyEvent.VK_UP:
      gravity.x = 0.0f;
      gravity.y = -10.0f;
      break;
      
    case KeyEvent.VK_RIGHT:
      gravity.x = 10.0f;
      gravity.y = 0.0f;
      break;
      
    case KeyEvent.VK_DOWN:
      gravity.x = 0.0f;
      gravity.y = 10.0f;
      break;
      
    case KeyEvent.VK_S:
      mode = 0;
      break;
      
    case KeyEvent.VK_D:
      mode = 1;
      break;
    }
  }
  
  public void keyReleased(KeyEvent e) {
  
  }
  
  public void keyTyped(KeyEvent e) {
  
  }

  public void update(Graphics g) {
    backGraphics.setColor(gray);
    backGraphics.fillRect(0, 0, 640, 480);
    backGraphics.setColor(black);
  
    int v1 = 7;
    for (int v2 = 0; v2 < 8; ++v2) {
      backGraphics.drawLine((int)(wallPt[v1].x),
        (int)(wallPt[v1].y),
        (int)(wallPt[v2].x),
        (int)(wallPt[v2].y));

      v1 = v2;
    }
                                           
    for (int i = 0; i < 64; ++i) {
       ball[i].draw(backGraphics);
    }
    
    if (mode == 0) {
      backGraphics.drawString("Mode: Shooting", 10, 50);
    } else if (mode == 1) {
      backGraphics.drawString("Mode: Dragging", 10, 50);
    }
    
    if (gravity.x == -10.0f) {
      backGraphics.drawString("Gravity: Left", 10, 65);
    } else if (gravity.y == -10.0f) {
      backGraphics.drawString("Gravity: Up", 10, 65);
    } else if (gravity.x == 10.0f) {
      backGraphics.drawString("Gravity: Right", 10, 65);
    } else if (gravity.y == 10.0f) {
      backGraphics.drawString("Gravity: Down", 10, 65);
    } else {
      backGraphics.drawString("Gravity: None", 10, 65);
    }
    
    if (activeBall != null) {
      backGraphics.setColor(blue);
      backGraphics.drawLine((int)(activeBall.pos.x),
        (int)(activeBall.pos.y),
        (int)(mousePt.x),
        (int)(mousePt.y));
    } else if (dragBall != null) {
      backGraphics.setColor(green);
      backGraphics.drawLine((int)(dragBall.pos.x),
        (int)(dragBall.pos.y),
        (int)(mousePt.x),
        (int)(mousePt.y));
    }
    
    g.drawImage(backImage, 0, 0, this);
  }

  void updateSimulation(float deltaTime) {
    // Apply a spring force to the dragBall if it is selected.
    if (dragBall != null) {
      Vector2D dragBallToMouse = new Vector2D();
      dragBallToMouse.diff(mousePt, dragBall.pos);
      dragBall.thrust.addScaled(1.0f, dragBallToMouse);
    }
    
    for (int i = 0; i < 64; ++i) {
      // Apply gravity.
      ball[i].calculateForces();

      // Integrate over the frame's time.
      ball[i].integrate(deltaTime);

      // Test for collision with all other balls.
      for (int j = 0; j < 64; ++j) {
        if (i != j) {
          collideBallBall(ball[i], ball[j]);
        }
      }

      // Test for collision with each edge.
      for (int j = 0; j < 8; ++j) {
        collideBallEdge(ball[i], edge[j]);
      }
    }
  }
                              
  void collideBallBall(Ball2D A, Ball2D B) {
    Vector2D ballToBall = new Vector2D();
    ballToBall.diff(A.pos, B.pos);
    
    float distSqrd = Vector2D.dot(ballToBall, ballToBall);
    
    if (distSqrd < ((A.rad+B.rad)*(A.rad+B.rad))) {
      Vector2D normal = new Vector2D(ballToBall);
      normal.normalize();
    
      Vector2D relVel = new Vector2D();
      relVel.diff(A.vel, B.vel);
      float relVelProj = Vector2D.dot(relVel, normal);
      
      if (relVelProj < 0.0f) {
        float velProjA = Vector2D.dot(A.vel, normal);
        float velProjB = Vector2D.dot(B.vel, normal);
        
        A.vel.addScaled(velProjB*e - velProjA, normal);
        B.vel.addScaled(velProjA*e - velProjB, normal);
        
        // Apply the penalty force, normal to the collision plane, to each ball.
        float depth = (float)Math.sqrt(distSqrd) - (A.rad + B.rad);
        A.thrust.addScaled(-depth*spring, normal);
        B.thrust.addScaled(depth*spring, normal);
      }
    }
  }

  void collideBallEdge(Ball2D ball, Edge2D edge) {
    Vector2D edgeToBall = new Vector2D();
    edgeToBall.diff(ball.pos, edge.v1);

    float ballDot = Vector2D.dot(edgeToBall, edge.edge);

    if (0.0f <= ballDot && ballDot <= edge.magSqrd) {
      float dist = Vector2D.dot(edgeToBall, edge.normal);

      if (dist < ball.rad) {
        // Project the velocity onto the normal.
        float normVel = Vector2D.dot(ball.vel, edge.normal);
        
        if (normVel < 0.0f) {
          // Add -(1.0+e) * normVel*edge.normal to ball velocity.
          ball.vel.addScaled(-(1.0f+e)*normVel, edge.normal);
          
          // Apply the penalty force, normal to the edge, to the ball.
          ball.thrust.addScaled((ball.rad-dist)*spring, edge.normal);
        }
      }
    }   
  }
}
