// 2:1 aspect ratio
int width = 1200;
int height = width/2;

// All of these are for regulation 9x4.5ft table
// and are values proportionate to the width
float tableBorderWidth = width/19.5;     // 5.5 inches
float feltBorderWidth = width/54.0;      // 2 inches
float billiardBallDiameter = width/48.0; // 2.25 inches
float billiardBallRadius = billiardBallDiameter/2;
float ballReturnWidth = billiardBallDiameter*1.5;
float playingAreaWidth = width-(tableBorderWidth*2)-ballReturnWidth;
float playingAreaHeight = playingAreaWidth/2;
float sideMarkerDiameter = width/275;    // 1 cm
float pocketDiameter = width/24.0;       // 4.5 inches
float pocketRadius = pocketDiameter/2;   // 2.25 inches
float centerOfPocketToEdge = width/21.6; // 5 inches
float pocketMouth = width/19.5;          // 5.5 inches

float rackBuffer = 0;
float circlePackingHeight = billiardBallRadius*sqrt(3.0)+.0001+rackBuffer;

color tableColor = color(36, 109, 56);

float strikingForce = .1;

float tableFriction = .02;
float bumperAbsorb = .2;

double previousTime, currentTime, elapsedTime, lag;
int ticks = 50;
int millisPerUpdate = 1000/ticks;

boolean paused = false;
boolean pauseCheck = false;
boolean frameByFrame = false;

Pocket[] pockets = new Pocket[6];
int numberOfBalls = 16;
int randomBalls = 0;
int totalBalls = numberOfBalls+randomBalls;
int ballsInBallReturn = 0;
BilliardBall[] balls = new BilliardBall[totalBalls];
String[] billiardBallPictureList = 
{"cue_ball.png", "one_ball_alt.png", "two_ball.png", "three_ball.png", "four_ball.png", 
 "five_ball.png", "six_ball.png", "seven_ball.png", "eight_ball.png", "nine_ball.png", "ten_ball.png", 
 "eleven_ball.png", "twelve_ball.png", "thirteen_ball.png", "fourteen_ball.png", "fifteen_ball.png"};

void setup() {
  frameRate(60);
  size(1200, 600);
  //frame.setResizable(true);
  
  //Top-left
  pockets[0] = new Pocket(0+centerOfPocketToEdge, 0+centerOfPocketToEdge);
  //Top-mid
  pockets[1] = new Pocket(width/2, tableBorderWidth-pocketRadius);
  //Top-right
  pockets[2] = new Pocket(width-centerOfPocketToEdge-ballReturnWidth, 0+centerOfPocketToEdge);
  //Bottom-left
  pockets[3] = new Pocket(0+centerOfPocketToEdge, height-centerOfPocketToEdge);
  //Bottom-mid
  pockets[4] = new Pocket(width/2, height-tableBorderWidth+pocketRadius);
  //Bottom-right
  pockets[5] = new Pocket(width-centerOfPocketToEdge-ballReturnWidth, height-centerOfPocketToEdge);
  
  balls[0] = new BilliardBall(0, width/4, height/2, "cue_ball.png");
  balls[8] = new BilliardBall(8, width/(4.0/3), height/2, "eight_ball.png");
  balls[1] = new BilliardBall(1, width/(4.0/3), (height/2)-billiardBallDiameter-rackBuffer, "one_ball_alt.png");
  balls[15] = new BilliardBall(15, width/(4.0/3), (height/2)+billiardBallDiameter+rackBuffer, "fifteen_ball.png");
  balls[12] = new BilliardBall(12, width/(4.0/3) - circlePackingHeight, (height/2)-billiardBallRadius-(rackBuffer/2), "twelve_ball.png");
  balls[7] = new BilliardBall(7, width/(4.0/3) - circlePackingHeight, (height/2)+billiardBallRadius+(rackBuffer/2), "seven_ball.png");
  balls[9] = new BilliardBall(9, width/(4.0/3) - (circlePackingHeight*2), height/2, "nine_ball.png");
  balls[14] = new BilliardBall(14, width/(4.0/3) + circlePackingHeight, (height/2)+billiardBallDiameter+billiardBallRadius+(rackBuffer*1.5), "fourteen_ball.png");
  balls[3] = new BilliardBall(3, width/(4.0/3) + circlePackingHeight, (height/2)+billiardBallRadius+(rackBuffer/2), "three_ball.png");
  balls[10] = new BilliardBall(10, width/(4.0/3) + circlePackingHeight, (height/2)-billiardBallRadius-(rackBuffer/2), "ten_ball.png");
  balls[6] = new BilliardBall(6, width/(4.0/3) + circlePackingHeight, (height/2)-billiardBallDiameter-billiardBallRadius-(rackBuffer*1.5), "six_ball.png");
  balls[5] = new BilliardBall(5, width/(4.0/3) + (circlePackingHeight*2), (height/2)-(billiardBallDiameter*2)-(rackBuffer*2), "five_ball.png");
  balls[4] = new BilliardBall(4, width/(4.0/3) + (circlePackingHeight*2), (height/2)-billiardBallDiameter-rackBuffer, "four_ball.png");
  balls[13] = new BilliardBall(13, width/(4.0/3) + (circlePackingHeight*2), height/2, "thirteen_ball.png");
  balls[2] = new BilliardBall(2, width/(4.0/3) + (circlePackingHeight*2), (height/2)+billiardBallDiameter+rackBuffer, "two_ball.png");
  balls[11] = new BilliardBall(11, width/(4.0/3) + (circlePackingHeight*2), (height/2)+(billiardBallDiameter*2)+(rackBuffer*2), "eleven_ball.png");
  
  for (int i=numberOfBalls; i<totalBalls; i++) {
    balls[i] = new BilliardBall(i, random((0+tableBorderWidth+billiardBallRadius), (width-tableBorderWidth-ballReturnWidth-billiardBallRadius)), 
                                random((0+tableBorderWidth+billiardBallRadius), (height-tableBorderWidth-billiardBallRadius)), 
                                billiardBallPictureList[(int)random(16)]);
  }
  
  previousTime = millis();
  lag = 0.0;
}

void draw() {
  if (!paused) {
    updateGame();
  }
  
  renderTable();
  
  if (paused && !frameByFrame) {
    drawPauseOverlay();
  }
}

void updateGame() {
  currentTime = millis();
  elapsedTime = currentTime-previousTime;
  previousTime = currentTime;
  lag += elapsedTime;
  
  while (lag >= millisPerUpdate) {
    updateBalls();
    lag -= millisPerUpdate;
  }
}

void updateBalls() {
  for(int i=0; i<totalBalls; i++) {
    for(int j = i+1; j<totalBalls; j++){
      if (!balls[i].inPocket && !balls[j].inPocket) {
        balls[i].resolveOverlap(balls[j]);
        balls[i].predictCollision(balls[j]);
      }
    }
  }
  
  for(BilliardBall b : balls) {
    if (!b.inPocket) {
      b.update();
    } else if (b.goingToPocket) {
      b.sendToPocket();
    } else if (b.settledInPocket) {
      b.sendToBallReturn();
    }
  }
}

void renderTable() {
  //tableColor = color((int)random(255));
  background(tableColor);
  
  drawTableBorders();
  drawBumpers();
  drawSideMarkers();
  drawCornerCaps();
  drawPocketPathways();
  drawBallReturn();
  
  pockets[0].position = new PVector(0+centerOfPocketToEdge, 0+centerOfPocketToEdge);
  pockets[1].position = new PVector(width/2, tableBorderWidth-pocketRadius);
  pockets[2].position = new PVector(width-centerOfPocketToEdge-ballReturnWidth, 0+centerOfPocketToEdge);
  pockets[3].position = new PVector(0+centerOfPocketToEdge, height-centerOfPocketToEdge);
  pockets[4].position = new PVector(width/2, height-tableBorderWidth+pocketRadius);
  pockets[5].position = new PVector(width-centerOfPocketToEdge-ballReturnWidth, height-centerOfPocketToEdge);
  for(Pocket p : pockets) {
    p.drawPocket();
  }
  
  for(BilliardBall b : balls) {
    if (!b.settledInPocket) {
      b.drawBall((float)(lag/millisPerUpdate));
    }
  }
}

void mousePressed(){
  for(BilliardBall b : balls) {
    if(dist(mouseX, mouseY, b.position.x, b.position.y) < billiardBallRadius) {
      if (b.inBallReturn) {
        b.grabbed = true;
      } else {
        b.shooting = true;
      }
    }
  }
}

void mouseReleased(){
  for(BilliardBall b : balls) {
    if(b.shooting) {
      b.shooting = false;
      b.hit();
    } else if(b.grabbed) {
      if (mouseX > (0+tableBorderWidth+billiardBallRadius) && 
          mouseX < (width-tableBorderWidth-ballReturnWidth-billiardBallRadius) && 
          mouseY > (0+tableBorderWidth+billiardBallRadius) && 
          mouseY < (height-tableBorderWidth-billiardBallRadius) ){
        b.position.x = mouseX;
        b.position.y = mouseY;
        
        b.velocity.x = 0;
        b.velocity.y = 0;
        
        b.inBallReturn = false;
      }
      b.grabbed = false;
    }
  }
}

void keyPressed() {
  if (key == 'p' || key == 'P') {
    if (!pauseCheck) {
      paused = true;
      println("paused");
    }
  }
  if (key == CODED) {
    if (keyCode == RIGHT && paused) {
      frameByFrame = true;
      updateBalls();
      renderTable();
    }
  }
}

void keyReleased() {
  if (key == 'p' || key == 'P') {
    if (pauseCheck) {
      paused = false;
      previousTime = millis();
      pauseCheck = false;
      println("unpaused");
      frameByFrame = false;
    } else {
      pauseCheck = true;
    }
  }
}

void drawTableBorders() {
  noStroke();
  fill(114, 80, 53);
  //fill(74, 50, 23);
  rectMode(CORNER);
  //Top
  rect(0, 0, width, tableBorderWidth);
  //Left
  rect(0, 0, tableBorderWidth, height);
  //Bottom
  rect(0, height-tableBorderWidth, width, tableBorderWidth);
  //Right
  rect(width-tableBorderWidth-ballReturnWidth, 0, tableBorderWidth, height);
}

void drawBumpers() {
  noStroke();
  fill(16, 69, 36);
  rectMode(CORNER);
  //Top
  rect(0+tableBorderWidth, 0+tableBorderWidth-feltBorderWidth+1, width-(tableBorderWidth*2), feltBorderWidth);
  //Left
  rect(0+tableBorderWidth-feltBorderWidth+1, 0+tableBorderWidth, feltBorderWidth, height-(tableBorderWidth*2));
  //Bottom
  rect(0+tableBorderWidth, height-tableBorderWidth, width-(tableBorderWidth*2), feltBorderWidth);
  //Right
  rect(width-tableBorderWidth-ballReturnWidth, 0+tableBorderWidth, feltBorderWidth, height-(tableBorderWidth*2));
}

void drawSideMarkers() {
  ellipseMode(CENTER);
  noStroke();
  fill(230, 230, 230);
  
  //Left
  ellipse(0+(tableBorderWidth/2), (height/2)-(playingAreaHeight/4), sideMarkerDiameter, sideMarkerDiameter);
  ellipse(0+(tableBorderWidth/2), height/2, sideMarkerDiameter, sideMarkerDiameter);
  ellipse(0+(tableBorderWidth/2), (height/2)+(playingAreaHeight/4), sideMarkerDiameter, sideMarkerDiameter);
  
  //Top Left
  ellipse((width/4)-(playingAreaWidth/8), 0+(tableBorderWidth/2), sideMarkerDiameter, sideMarkerDiameter);
  ellipse(width/4, 0+(tableBorderWidth/2), sideMarkerDiameter, sideMarkerDiameter);
  ellipse((width/4)+(playingAreaWidth/8), 0+(tableBorderWidth/2), sideMarkerDiameter, sideMarkerDiameter);
  //Top Right
  ellipse((width/(4.0/3))-(playingAreaWidth/8), 0+(tableBorderWidth/2), sideMarkerDiameter, sideMarkerDiameter);
  ellipse(width/(4.0/3), 0+(tableBorderWidth/2), sideMarkerDiameter, sideMarkerDiameter);
  ellipse((width/(4.0/3))+(playingAreaWidth/8), 0+(tableBorderWidth/2), sideMarkerDiameter, sideMarkerDiameter);
  
  //Right
  ellipse(width-(tableBorderWidth/2)-ballReturnWidth, (height/2)-(playingAreaHeight/4), sideMarkerDiameter, sideMarkerDiameter);
  ellipse(width-(tableBorderWidth/2)-ballReturnWidth, height/2, sideMarkerDiameter, sideMarkerDiameter);
  ellipse(width-(tableBorderWidth/2)-ballReturnWidth, (height/2)+(playingAreaHeight/4), sideMarkerDiameter, sideMarkerDiameter);
  
  //Bottom Left
  ellipse((width/4)-(playingAreaWidth/8), height-(tableBorderWidth/2), sideMarkerDiameter, sideMarkerDiameter);
  ellipse(width/4, height-(tableBorderWidth/2), sideMarkerDiameter, sideMarkerDiameter);
  ellipse((width/4)+(playingAreaWidth/8), height-(tableBorderWidth/2), sideMarkerDiameter, sideMarkerDiameter);
  //Bottom Right
  ellipse((width/(4.0/3))-(playingAreaWidth/8), height-(tableBorderWidth/2), sideMarkerDiameter, sideMarkerDiameter);
  ellipse(width/(4.0/3), height-(tableBorderWidth/2), sideMarkerDiameter, sideMarkerDiameter);
  ellipse((width/(4.0/3))+(playingAreaWidth/8), height-(tableBorderWidth/2), sideMarkerDiameter, sideMarkerDiameter);
}

void drawCornerCaps() {
  rectMode(CORNER);
  noStroke();
  fill(150, 150, 150);
  
  //Top-left
  rect(0, 0, tableBorderWidth, tableBorderWidth);
  //Top-right
  rect(width-tableBorderWidth-ballReturnWidth, 0, tableBorderWidth, tableBorderWidth);
  //Bottom-left
  rect(0, height-tableBorderWidth, tableBorderWidth, tableBorderWidth);
  //Bottom-right
  rect(width-tableBorderWidth-ballReturnWidth, height-tableBorderWidth, tableBorderWidth, tableBorderWidth);
}

void drawPocketPathways() {
  rectMode(CENTER);
  noStroke();
  fill(tableColor);
  
  //Top-left
  pushMatrix();
  translate(centerOfPocketToEdge+pocketRadius-(width/108), centerOfPocketToEdge+pocketRadius-(width/108));
  rotate(radians(-45));
  rect(0, 0, pocketDiameter, pocketDiameter);
  triangle(0-pocketRadius, 0-pocketRadius, 0-pocketRadius, 0+pocketRadius, 0-(pocketMouth/2), 0+pocketRadius);
  triangle(0+pocketRadius, 0-pocketRadius, 0+pocketRadius, 0+pocketRadius, 0+(pocketMouth/2), 0+pocketRadius);
  popMatrix();
  
  //Top-mid
  pushMatrix();
  translate(width/2, tableBorderWidth);
  rect(0, 0, pocketDiameter, pocketDiameter);
  triangle(0-pocketRadius, 0-pocketRadius, 0-pocketRadius, 0+pocketRadius, 0-(pocketMouth/2+(width/108)), 0+pocketRadius);
  triangle(0+pocketRadius, 0-pocketRadius, 0+pocketRadius, 0+pocketRadius, 0+(pocketMouth/2+(width/108)), 0+pocketRadius);
  popMatrix();
  
  //Top-right
  pushMatrix();
  translate(width-centerOfPocketToEdge-pocketRadius+(width/108)-ballReturnWidth, centerOfPocketToEdge+pocketRadius-(width/108));
  rotate(radians(45));
  rect(0, 0, pocketDiameter, pocketDiameter);
  triangle(0-pocketRadius, 0-pocketRadius, 0-pocketRadius, 0+pocketRadius, 0-(pocketMouth/2), 0+pocketRadius);
  triangle(0+pocketRadius, 0-pocketRadius, 0+pocketRadius, 0+pocketRadius, 0+(pocketMouth/2), 0+pocketRadius);
  popMatrix();
  
  //Bottom-left
  pushMatrix();
  translate(centerOfPocketToEdge+pocketRadius-(width/108), height-centerOfPocketToEdge-pocketRadius+(width/108));
  rotate(radians(135));
  rect(0, 0, pocketDiameter, pocketDiameter);
  triangle(0-pocketRadius, 0-pocketRadius, 0-pocketRadius, 0+pocketRadius, 0-(pocketMouth/2), 0+pocketRadius);
  triangle(0+pocketRadius, 0-pocketRadius, 0+pocketRadius, 0+pocketRadius, 0+(pocketMouth/2), 0+pocketRadius);
  popMatrix();
  
  //Bottom-mid
  pushMatrix();
  translate(width/2, height-tableBorderWidth);
  rotate(radians(180));
  rect(0, 0, pocketDiameter, pocketDiameter);
  triangle(0-pocketRadius, 0-pocketRadius, 0-pocketRadius, 0+pocketRadius, 0-(pocketMouth/2+(width/108)), 0+pocketRadius);
  triangle(0+pocketRadius, 0-pocketRadius, 0+pocketRadius, 0+pocketRadius, 0+(pocketMouth/2+(width/108)), 0+pocketRadius);
  popMatrix();
  
  //Bottom-right
  pushMatrix();
  translate(width-centerOfPocketToEdge-pocketRadius+(width/108)-ballReturnWidth, height-centerOfPocketToEdge-pocketRadius+(width/108));
  rotate(radians(135));
  rect(0, 0, pocketDiameter, pocketDiameter);
  triangle(0-pocketRadius, 0-pocketRadius, 0-pocketRadius, 0+pocketRadius, 0-(pocketMouth/2), 0+pocketRadius);
  triangle(0+pocketRadius, 0-pocketRadius, 0+pocketRadius, 0+pocketRadius, 0+(pocketMouth/2), 0+pocketRadius);
  popMatrix();
}

void drawBallReturn() {
  rectMode(CORNER);
  fill(50);
  noStroke();
  rect(width-ballReturnWidth, 0, ballReturnWidth, height);
}

void drawPauseOverlay() {
  textSize((int)width/20);
  fill(255);
  text("PAUSED", width/2-width/11, height/2);
  rectMode(CORNER);
  fill(0, 50);
  rect(0, 0, width, height);
}
class BilliardBall {
  int identity;
  
  PVector position;
  PVector velocity;
  PVector normal;
  PVector tangential;
  PVector angle;
  PVector angularVelocity;
  
  PVector ballReturnAcceleration;
  
  PVector normalVelocity;
  PVector tangentialVelocity;
  
  float unitNormalVelocity;
  float unitTangentialVelocity;
  
  PVector smoothTransitionDiff;
  
  Pocket containingPocket;
  PVector velocityTowardPocket;
  
  float mass;
  float slidingFriction;
  float rollingFriction;
  float g;
  
  boolean shooting = false;
  boolean grabbed = false;
  boolean inPocket = false;
  boolean goingToPocket = false;
  boolean settledInPocket = false;
  boolean inBallReturn = false;
  PImage img;
  
  BilliardBall(int id, float x, float y, String image_url) {
    identity = id;
    
    position = new PVector(x, y);
    velocity = new PVector(0, 0);
    normal = new PVector(0, 0);
    tangential = new PVector(0, 0);
    angle = new PVector(random(360), 0);
    angularVelocity = new PVector(0, 0);
    
    ballReturnAcceleration = new PVector(0, 0.2);
    
    normalVelocity = new PVector();
    tangentialVelocity = new PVector();
    
    smoothTransitionDiff = new PVector();
    
    mass = .5; //Mass of cue ball
    slidingFriction = 0.2;
    rollingFriction = 0.01;
    
    g=-10;
    
    img = loadImage(image_url);
  }
  
  void drawBall(float percentThroughUpdate) {
    if (shooting) {
      stroke(0);
      line(mouseX, mouseY, position.x, position.y);
    }
    
    if (!inPocket && !paused) {
      smoothTransitionDiff.x = (velocity.x*percentThroughUpdate);
      smoothTransitionDiff.y = (velocity.y*percentThroughUpdate);
    } else {
      smoothTransitionDiff.x = 0;
      smoothTransitionDiff.y = 0;
    }
    
    pushMatrix();
    if (grabbed) {
      translate(mouseX, mouseY);
    } else {
      translate(position.x+smoothTransitionDiff.x, position.y+smoothTransitionDiff.y);
    }
    rotate(angle.x);
    imageMode(CENTER);
    image(img, 0, 0, billiardBallDiameter, billiardBallDiameter);
    popMatrix();
    
    if (frameByFrame) {
      if (abs(velocity.x) > 0 || abs(velocity.y) > 0) {
        stroke(0);
        line(position.x, position.y, position.x+velocity.x, position.y+velocity.y);
        stroke(255, 50);
        fill(0, 50);
        ellipse(position.x+velocity.x, position.y+velocity.y, billiardBallDiameter, billiardBallDiameter);
      }
    }
  }
  
  void update() {
    //angle.x += .03;
    position.add(velocity);
    angle.add(angularVelocity);
    
    if (inBallReturn) {
      for(int i=0; i<totalBalls; i++) {
        for(int j = i+1; j<totalBalls; j++){
          if (balls[i].inBallReturn && balls[j].inBallReturn) {
            balls[i].resolveOverlap(balls[j]);
            balls[i].velocity.x = 0;
            balls[j].velocity.x = 0;
          }
        }
      }
      
      //Bottom
      if (position.y+billiardBallRadius > height) {
        velocity.y *= -1+.5;
        position.y = height-billiardBallRadius;
        
        if (velocity.y > 0 && velocity.y < 0.05) {
          velocity.y = 0;
        }
      }
      
      //println("inPocket: "+inPocket+", goingToPocket: "+goingToPocket+", settledInPocket: "+settledInPocket+", inBallReturn: "+inBallReturn);
      velocity.add(ballReturnAcceleration);
    } else {
      checkBoundaryCollision();
      
      velocity.mult(1-tableFriction);
      if (abs(velocity.x) < .05) {
        velocity.x = 0;
      }
      if (abs(velocity.y) < .05) {
        velocity.y = 0;
      }
      
      angularVelocity.mult(1-tableFriction);
      if (abs(angularVelocity.x) < .001) {
        angularVelocity.x = 0;
      }
      if (abs(angularVelocity.y) < .001) {
        angularVelocity.y = 0;
      }
      /*if(angularVelocity.y > 0 || angularVelocity.x > 0) {
        print("angularVelocity.x: "+angularVelocity.x);
        println(", angularVelocity.y: "+angularVelocity.y);
      }*/
    }
    
    //print("velocity.x: "+velocity.x);
    //println(", velocity.y: "+velocity.y);
    
    //updated = false;
  }
  
  void checkBoundaryCollision() {
    //Left
    if (position.x-billiardBallRadius < 0+tableBorderWidth) {
      if (position.y-billiardBallRadius < centerOfPocketToEdge+pocketRadius) {
        pockets[0].ballInPocket(this);
        inPocket = true;
        //println("In top-left!");
        return;
      }
      if (position.y+billiardBallRadius > height-centerOfPocketToEdge-pocketRadius) {
        pockets[3].ballInPocket(this);
        inPocket = true;
        //println("In bottom-left!");
        return;
      }
      
      velocity.x *= -1+bumperAbsorb;
      position.x = tableBorderWidth+billiardBallRadius;
    }
    //Right
    if (position.x+billiardBallRadius > width-tableBorderWidth-ballReturnWidth) {
      if (position.y-billiardBallRadius < centerOfPocketToEdge+pocketRadius) {
        pockets[2].ballInPocket(this);
        inPocket = true;
        //println("In top-right!");
        return;
      }
      if (position.y+billiardBallRadius > height-centerOfPocketToEdge-pocketRadius) {
        pockets[5].ballInPocket(this);
        inPocket = true;
        //println("In bottom-right!");
        return;
      }
      
      velocity.x *= -1+bumperAbsorb;
      position.x = width-tableBorderWidth-ballReturnWidth-billiardBallRadius;
    }
    
    //Top
    if (position.y-billiardBallRadius < 0+tableBorderWidth) {
      if (position.x-billiardBallRadius < centerOfPocketToEdge+pocketRadius) {
        pockets[0].ballInPocket(this);
        inPocket = true;
        //println("In top-left!");
        return;
      }
      if ((position.x > width/2-pocketRadius) && (position.x < width/2+pocketRadius)) {
        pockets[1].ballInPocket(this);
        inPocket = true;
        //println("In top-mid!");
        return;
      }
      if (position.x+billiardBallRadius > width-centerOfPocketToEdge-ballReturnWidth-pocketRadius) {
        pockets[2].ballInPocket(this);
        inPocket = true;
        //println("In top-right!");
        return;
      }
      
      velocity.y *= -1+bumperAbsorb;
      position.y = tableBorderWidth+billiardBallRadius;
    }
    //Bottom
    if (position.y+billiardBallRadius > height-tableBorderWidth) {
      if (position.x-billiardBallRadius < centerOfPocketToEdge+pocketRadius) {
        pockets[3].ballInPocket(this);
        inPocket = true;
        //println("In bottom-left!");
        return;
      }
      if ((position.x > width/2-pocketRadius) && (position.x < width/2+pocketRadius)) {
        pockets[4].ballInPocket(this);
        inPocket = true;
        //println("In bottom-mid!");
        return;
      }
      if (position.x+billiardBallRadius > width-centerOfPocketToEdge-ballReturnWidth-pocketRadius) {
        pockets[5].ballInPocket(this);
        inPocket = true;
        //println("In bottom-right!");
        return;
      }
      
      velocity.y *= -1+bumperAbsorb;
      position.y = height-tableBorderWidth-billiardBallRadius;
    }
  }
  
  void sendToPocket(Pocket pocket, PVector velocityInDirection) {
    containingPocket = pocket;
    velocityTowardPocket = velocityInDirection;
    float distance = dist(containingPocket.position.x, containingPocket.position.y, position.x, position.y);
    if (position != containingPocket.position) {
      if (distance > velocity.mag()) {
        position.add(velocityTowardPocket);
      } else {
        position = containingPocket.position;
        goingToPocket = false;
        settledInPocket = true;
      }
    } else {
      goingToPocket = false;
      settledInPocket = true;
    }
  }
  
  void sendToPocket() {
    float distance = dist(containingPocket.position.x, containingPocket.position.y, position.x, position.y);
    if (position != containingPocket.position) {
      if (distance > velocity.mag()) {
        position.add(velocityTowardPocket);
      } else {
        position = containingPocket.position;
        goingToPocket = false;
        settledInPocket = true;
      }
    }
  }
  
  void sendToBallReturn() {
    inPocket = false;
    settledInPocket = false;
    inBallReturn = true;
    
    position.x = width-(ballReturnWidth/2);
    position.y = 0-(billiardBallDiameter*1.5);
    
    velocity.x = 0;
    velocity.y = 0;
    
    angularVelocity.x = 0;
    angularVelocity.y = 0;
    //print("Containing pocket x: "+containingPocket.position.x+", Containing pocket y: "+containingPocket.position.y);
  }
  
  void hit() {
    //PVector x = PVector.sub(position, new PVector(mouseX, mouseY)); //Distance between cue tip and ball
    //float k = 5; //Spring constant of cue stick: kg*m/s^2
    //float time = .01; //Time cue stick was in contact with ball
    //velocity.add(PVector.mult(x, (k*time)/mass));
    
    velocity.x += (position.x-mouseX)/(1/strikingForce);
    velocity.y += (position.y-mouseY)/(1/strikingForce);
  }
  
  
  void resolveBallCollision(BilliardBall other) {
    //update();
    //other.update();
    //placeBallAtCollisionEvent(other);
    
    PVector normal = PVector.sub(position, other.position);
    PVector unitNormal = PVector.div(normal, normal.mag());
    PVector unitTangent = new PVector(unitNormal.y*-1, unitNormal.x);
    
    unitNormalVelocity = PVector.dot(unitNormal, velocity);
    unitTangentialVelocity = PVector.dot(unitTangent, velocity);
    other.unitNormalVelocity = PVector.dot(unitNormal, other.velocity);
    other.unitTangentialVelocity = PVector.dot(unitTangent, other.velocity);
    
    float newUnitNormalVelocity = ((unitNormalVelocity*mass-other.mass) + (2*other.mass*other.unitNormalVelocity)) / (mass+other.mass);
    float otherNewUnitNormalVelocity = ((other.unitNormalVelocity*other.mass-mass) + (2*mass*unitNormalVelocity)) / (mass+other.mass);
    float newUnitTangentialVelocity = unitTangentialVelocity;
    float otherNewUnitTangentialVelocity = other.unitNormalVelocity;
    
    normalVelocity = PVector.mult(unitNormal, other.unitNormalVelocity); //newUnitNormalVelocity
    other.normalVelocity = PVector.mult(unitNormal, unitNormalVelocity); //otherNewUnitNormalVelocity
    tangentialVelocity = PVector.mult(unitTangent, unitTangentialVelocity);
    other.tangentialVelocity = PVector.mult(unitTangent, other.unitTangentialVelocity);
    
    velocity = PVector.add(normalVelocity, tangentialVelocity);
    other.velocity = PVector.add(other.normalVelocity, other.tangentialVelocity);
    
    angularVelocity.add(PVector.div(tangentialVelocity, 10));
    //angularVelocity.add(PVector.div(other.tangentialVelocity, 20));
    other.angularVelocity.add(PVector.div(other.tangentialVelocity, 10));
    //other.angularVelocity.add(PVector.div(tangentialVelocity, 20));
    
    /*if (velocity.x == 0 || velocity.y == 0) {
      placeBallAtCollisionEvent(this);
    }*/
    
    //update();
    //other.update();
  }
  
  
  public boolean predictCollision(BilliardBall other) {
    // Coordinates of circle
    float x0 = other.position.x+other.velocity.x;
    float y0 = other.position.y+other.velocity.y;
    //float x0 = other.position.x;
    //float y0 = other.position.y;
    
    // Coordinates of velocity line
    float x1 = position.x;
    float x2 = position.x+velocity.x;
    float y1 = position.y;
    float y2 = position.y+velocity.y;
    
    // Equations to simplify things
    float a = x2-x1;
    float b = y1-y2;
    float c = y1*(x1-x2)+x1*(y2-y1);
    
    float d = x1-x2;
    float e = y2-y1;
    
    float distance;
    // Distance between line and point
    if (dist(x1, y1, x0, y0) <= billiardBallDiameter || dist(x2, y2, x0, y0) <= billiardBallDiameter) {
      //placeBallAtCollisionEvent(other);
      resolveBallCollision(other);
      distance = 100;
    }
    //float distance = abs(a*x0 + b*y0 + d*y1 + e*x1)/sqrt(sq(a)+sq(b));
    else if (!(billiardBallDiameter<=sqrt(sq(x1-x0)+sq(y1-y0))) && !(billiardBallDiameter<=sqrt(sq(x2-x0)+sq(y2-y0)))) {
      distance = abs(d*(x0-x1)+e*(y0-y1))/sqrt(sq(d)+sq(e));
    } else {
      distance = 100;
    }
    
    if (distance <= billiardBallDiameter) {
      //println("distance: "+distance+", ball 1: "+getIdentity()+", ball 2: "+other.getIdentity());
      //print("about to collide!");
      placeBallAtCollisionEvent(other);
      resolveBallCollision(other);
      return true;
    } else {
      return false;
    }
  }
  
  /*
  public boolean predictCollision(BilliardBall other) {
    // Coordinates of circle
    float x0 = other.position.x;
    float y0 = other.position.y;
    
    // Coordinates of velocity line
    float x2 = position.x;
    float x1 = position.x+velocity.x;
    float y2 = position.y;
    float y1 = position.y+velocity.y;
    
    float m = (y2-y1)/(x2-x1);
    float im = -1/m;
    
    float b = (m*-x1)+y1;
    float bp = y0-im*x0;
    
    float xp = (b-bp)/(im-m);
    float yp = m*xp+b;
    
    float distance = dist(x0, y0, xp, yp);
    if (this == balls[0] && other == balls[9]) {
      println("distance: "+distance+", ball 1: "+getIdentity()+", ball 2: "+other.getIdentity());
      //println("b: "+b+", bp: "+bp);
    }
    
    if (distance <= billiardBallDiameter) {
      //println("distance: "+distance+", ball 1: "+getIdentity()+", ball 2: "+other.getIdentity());
      print("about to collide!");
      //placeBallAtCollisionEvent(other);
      return true;
    } else {
      return false;
    }
  }*/
  
  void placeBallAtCollisionEvent(BilliardBall other) {
    // Distance between the points
    float distance = dist(position.x, position.y, other.position.x, other.position.y);
    
    float a = other.position.x-position.x;
    float b = other.position.y-position.y;
    
    float t = sqrt(sq(distance) / (sq(a)+sq(b)));
    //println(t);
    
    position.x -= (velocity.x*t);
    position.y -= (velocity.y*t);
  }
  
  void resolveOverlap(BilliardBall other) {
    if (dist(position.x, position.y, other.position.x, other.position.y) < abs(billiardBallRadius*2)) {
      PVector normal = PVector.sub(position, other.position);
      PVector unitNormal = PVector.div(normal, normal.mag());
      PVector correctDistance = PVector.mult(unitNormal, billiardBallDiameter);
      PVector currentDistance = PVector.mult(unitNormal, dist(position.x, position.y, other.position.x, other.position.y));
      PVector correctionDistance = PVector.div(PVector.sub(correctDistance, currentDistance), 2);
      position.add(correctionDistance);
      other.position.sub(correctionDistance);
    }
  }
  
  /*
  void resolveBallCollision(BilliardBall other) {
    //if ( (other != this) && (dist(position.x, position.y, other.position.x, other.position.y) <= abs(billiardBallRadius*2)) ) {
      PVector delta = PVector.sub(position, other.position);
      float d = delta.mag();
      PVector mtd = PVector.mult(delta, ((billiardBallDiameter)-d) /d);
      
      // Resolve intersection
      // inverse mass quantities
      float im1 = 1/mass;
      float im2 = 1/other.mass;
      
      // push-pull them apart based on mass
      //position = PVector.add(position, PVector.mult(mtd, (im1 / (im1+im2)) ));
      //other.position = PVector.sub(other.position, PVector.mult(mtd, (im2 / (im1+im2)) ));
      
      // impact speed
      PVector v = PVector.sub(velocity, other.velocity);
      float vn = v.dot(mtd.normalize(null));
      
      // sphere intersecting but moving away from each other already
      if (vn > 0.0f) return;
      
      // collision impulse
      float restitution = 0;
      float i = (-(1.0f + restitution) * vn) / (im1+im2);
      PVector impulse = PVector.mult(mtd, i);
      
      // change in momentum
      velocity = PVector.add(velocity, PVector.mult(mtd, im1)); // mtd used to be impulse
      other.velocity = PVector.sub(other.velocity, PVector.mult(mtd, im2)); // mtd used to be impulse
      
      PVector combinedNormal = PVector.div(PVector.sub(position, other.position), PVector.sub(position, other.position).mag());
      PVector negativeCombinedNormal = PVector.mult(normal, -1);
      angularVelocity.add(PVector.div(combinedNormal, 20));
      other.angularVelocity.add(PVector.div(negativeCombinedNormal, 20));
      
      update();
      other.update();
    //}
  }*/
  
  int getIdentity() {
    return this.identity;
  }
  
  /*
  void resolveBallCollision(BilliardBall other) {
    //if (other != this) {
      //if (dist(position.x, position.y, other.position.x, other.position.y) < abs(billiardBallRadius*2)) {
        
        //PVector oldVel = velocity;
        //PVector oldOtherVel = other.velocity;
          
        PVector combinedNormal = PVector.div(PVector.sub(position, other.position), PVector.sub(position, other.position).mag());
        PVector negativeCombinedNormal = PVector.mult(normal, -1);
        normal = PVector.mult(negativeCombinedNormal, PVector.dot(velocity, negativeCombinedNormal));
        other.normal = PVector.mult(combinedNormal, PVector.dot(other.velocity, combinedNormal));
        tangential = PVector.sub(normal, velocity);
        other.tangential = PVector.sub(other.normal, other.velocity);
        
        angularVelocity.add(PVector.div(combinedNormal, 20));
        other.angularVelocity.add(PVector.div(negativeCombinedNormal, 20));
        
        //PVector newVel = PVector.add(tangential, other.normal);
        //if(Float.isNaN(newVel.x)) {
        //  newVel.x = velocity.x;
        //}
        //if(Float.isNaN(newVel.y)) {
        //  newVel.y = velocity.y;
        //}
        
        //println("newVel.x: "+newVel.x+", newVel.y: "+newVel.y);
        velocity = PVector.add(tangential, other.normal);
      
        PVector otherNewVel = PVector.add(other.tangential, normal);
        //if(Float.isNaN(otherNewVel.x)) {
        //  otherNewVel.x = other.velocity.x;
        //}
        //if(Float.isNaN(otherNewVel.y)) {
        //  otherNewVel.y = other.velocity.y;
        //}
        
        //if(other.updated == false) {
        other.velocity = PVector.add(other.tangential, normal);
          //other.updated = true;
        //}
        //other.checkBallCollisions(balls);
        
        //position.sub(oldVel);
        //other.position.sub(oldOtherVel);
      
        //println("normal.x: "+normal.x+", normal.y: "+normal.y);
        update();
        other.update();
      //}
    //}
  }*/
  
  /*void setVelocity(PVector vel) {
    velocity = vel;
  }
  
  void setNormal(PVector norm) {
    normal = norm;
  }
  
  void setTangential(PVector tang) {
    tangential = tang;
  }*/
}
class Pocket {
  PVector position = new PVector();
  
  Pocket(float _x, float _y) {
    position.x = _x;
    position.y = _y;
  }
  
  void drawPocket() {
    ellipseMode(CENTER);
    noStroke();
    fill(0);
    
    ellipse(position.x, position.y, pocketDiameter, pocketDiameter);
  }
  
  void ballInPocket(BilliardBall ball) {
    PVector difference = PVector.sub(position, ball.position);
    PVector differenceDirection = PVector.div(difference, difference.mag());
    PVector velocityInDirection = PVector.mult(differenceDirection, ball.velocity.mag());
    
    if (ball.position != position) {
      ball.goingToPocket = true;
      ball.sendToPocket(this, velocityInDirection);
    } else {
      ball.settledInPocket = true;
    }
  }
  
}

