import processing.core.*; 
import processing.data.*; 
import processing.event.*; 
import processing.opengl.*; 

import ddf.minim.*; 

import java.util.HashMap; 
import java.util.ArrayList; 
import java.io.File; 
import java.io.BufferedReader; 
import java.io.PrintWriter; 
import java.io.InputStream; 
import java.io.OutputStream; 
import java.io.IOException; 

public class playforfreedom extends PApplet {


 
AudioPlayer song;
AudioPlayer song2;
AudioPlayer song3;
AudioPlayer song4;


Minim minim;
int boundary_l = 100;
int boundary_r = 1200 - 100;

Ball[] balls = new Ball[10];

Racket racket_l = new Racket(boundary_l, 0, 1500, 1500);
Racket racket_r = new Racket(boundary_r, 0, 1500, 1500);

Flock flock;

int last_direction = 0;
int rounds = 0;
int flocks = 0;

boolean hit_ball = false;


public void setup() {
  size(1200,400);
  background(0);
  minim = new Minim(this);

  song = minim.loadFile("hit.mp3");
  song2 = minim.loadFile("bird-1.mp3.mp3");
  song3 = minim.loadFile("fish-1.mp3.mp3");
  song4 = minim.loadFile("last.mp3");


  smooth();
  noStroke();
  frameRate(500);

  for (int i = 0; i < balls.length; ++i) {
    float x = 200;
    float y = height/2;
    float d = 30;
    balls[i] = new Ball(x,y,d);
  }

  racket_l.shape_ = loadShape("Julian-Racket.svg");
  racket_r.shape_ = loadShape("Julian-Racket.svg");

  last_direction = balls[0].direction_;
  
  flock = new Flock();   

}



public void ball_move_1() {
  for (int i = 0; i < balls.length; ++i) {    
    balls[i].move();

    pushMatrix();        
    translate(balls[i].display_x_, balls[i].display_y_);
    if (balls[i].direction_ == 1) {
      rotate(radians(30));
    } else {
      rotate(radians(-30));
    }    
    balls[i].draw(0);    
    popMatrix();
  }  
}


public void ball_move_2() {
  for (int i = 0; i < balls.length; ++i) {    
    balls[i].move();
    balls[i].breathing();

    pushMatrix();        
    translate(balls[i].display_x_, balls[i].display_y_);
    if (balls[i].direction_ == 1) {
      rotate(radians(30));
    } else {
      rotate(radians(-30));
    }    
    balls[i].draw(0);    
    popMatrix();
  }  
}

public void ball_move_3() {
  for (int i = 0; i < balls.length; ++i) {    
    balls[i].move();
    balls[i].breathing();
    balls[i].diverging((i-4)*20);

    pushMatrix();        
    translate(balls[i].display_x_, balls[i].display_y_);
    rotate(radians(5*sin(0.1f*balls[i].x_)));
    balls[i].draw(0);    
    popMatrix();
  }  
} 

public void ball_move_4() {
  for (int i = 0; i < balls.length; ++i) {    
    balls[i].move();
    balls[i].breathing();
    balls[i].morphing();
    balls[i].diverging((i-4)*20);
    //balls[i].floatingX();

    pushMatrix();        
    translate(balls[i].display_x_, balls[i].display_y_);
    rotate(radians(5*sin(0.1f*balls[i].x_)));
    balls[i].draw(0);    
    popMatrix();
  }  
} 

public void ball_move_5() {
  for (int i = 0; i < balls.length; ++i) {    
    balls[i].move();
    balls[i].breathing();
    balls[i].trianguling();
    balls[i].diverging((i-4)*20);
    //balls[i].floatingX();

    pushMatrix();        
    translate(balls[i].display_x_, balls[i].display_y_);
    if (balls[i].direction_ == 1) {
      rotate(radians(30));
    } else {
      rotate(radians(-30));
    }    

    balls[i].draw(0);    
    popMatrix();
  }  
} 

public void ball_move_6() {
  for (int i = 0; i < balls.length; ++i) {    
    balls[i].move();
    balls[i].breathing();
    balls[i].edges(3);
    balls[i].diverging((i-4)*20);
    //balls[i].floatingX();

    pushMatrix();        
    translate(balls[i].display_x_, balls[i].display_y_);
    if (balls[i].direction_ == 1) {
      rotate(radians(30));
    } else {
      rotate(radians(-30));
    }    
    
  //  rotate(radians(5*sin(0.1*balls[i].x_)));
    balls[i].draw(0);    
    popMatrix();
  }  
} 




public void ball_move_7() {
  for (int i = 0; i < balls.length; ++i) {    
    balls[i].move();
    balls[i].breathing();
    balls[i].edges(3);
    balls[i].diverging((i-4)*20);
    balls[i].floatingX();

    pushMatrix();        
    translate(balls[i].display_x_, balls[i].display_y_);
    if (balls[i].direction_ == 1) {
      rotate(radians(30));
    } else {
      rotate(radians(-30));
    }    
    
  //  rotate(radians(5*sin(0.1*balls[i].x_)));
    balls[i].draw(0);    
    popMatrix();
  }  
} 

public void ball_move_8() {
  for (int i = 0; i < balls.length; ++i) {    
    balls[i].move2();
    balls[i].breathing();
    balls[i].edges(3);
    balls[i].diverging((i-4)*20);
    balls[i].floatingX();

    pushMatrix();        
    translate(balls[i].display_x_, balls[i].display_y_);
    if (balls[i].direction_ == 1) {
      rotate(radians(30));
    } else {
      rotate(radians(-30));
    }    
    
  //  rotate(radians(5*sin(0.1*balls[i].x_)));
    balls[i].draw(0);    
    popMatrix();
  }  
} 


public void draw() {
  fill(255);
  background(0);


  if (rounds < 2) {  

    ball_move_1();
    pushMatrix();
    translate(-450, -600);
    rotate(radians(30));
    racket_l.display();
    popMatrix();


    pushMatrix();
    translate(50, 300);
    rotate(radians(-30));
    racket_r.display();
    popMatrix();  
  } else if (rounds >= 2 && rounds < 4) {
    ball_move_2();
    pushMatrix();
    translate(-450, -600);
    rotate(radians(30));
    racket_l.display();
    popMatrix();

    pushMatrix();
    translate(50, 300);
    rotate(radians(-30));
    racket_r.display();
    popMatrix();  
  } else if (rounds >= 4 && rounds < 6) {
    ball_move_3();
    pushMatrix();
    translate(-450, -600);
    rotate(radians(30));
    racket_l.display();
    popMatrix();

    pushMatrix();
    translate(50, 300);
    rotate(radians(-30));
    racket_r.display();
    popMatrix();  
  } else if (rounds >= 6 && rounds < 8) {
    ball_move_4();
    pushMatrix();
    translate(-450, -600);
    rotate(radians(30));
    racket_l.display();
    popMatrix();

    pushMatrix();
    translate(50, 300);
    rotate(radians(-30));
    racket_r.display();
    popMatrix();  
  } else if (rounds >= 8 && rounds < 9) {
    ball_move_5();
    pushMatrix();
    translate(-450, -600);
    rotate(radians(30));
    racket_l.display();
    popMatrix();

    pushMatrix();
    translate(50, 300);
    rotate(radians(-30));
    racket_r.display();
    popMatrix();  
  } else if (rounds >= 9 && rounds < 11) {
    ball_move_6();
    pushMatrix();
    translate(-450, -600);
    rotate(radians(30));
    racket_l.display();
    popMatrix();

    pushMatrix();
    translate(50, 300);
    rotate(radians(-30));
    racket_r.display();
    popMatrix();  
  } else if (rounds >= 11 && rounds < 13) {
    ball_move_7();
    pushMatrix();
    translate(-450, -600);
    rotate(radians(30));
    racket_l.display();
    popMatrix();

    pushMatrix();
    translate(50, 300);
    rotate(radians(-30));
    racket_r.display();
    popMatrix();  

  } else if (rounds >= 12 && rounds < 16) {
    ball_move_8();

    if (!song2.isPlaying()) {
      song2.play();
    }
    
  } else if (rounds >= 16 && rounds < 100) {

    if (song2.isPlaying()) {
      song2.close();
    }

    if (!song3.isPlaying()) {
      song3.play(0);
    }
    
    if (flocks == 0) {
      for (int i = 0; i < balls.length; ++i) {    
        flock.addBoid(new Boid(balls[i].display_x_,balls[i].display_y_));
      }
      flocks += balls.length;
    } else if (frameCount % 100 == 0  && flocks < 100) {      
      flock.addBoid(new Boid(balls[0].display_x_,balls[0].display_y_));
      ++flocks;
      ++rounds;
    } 

    flock.run();     

  } else if (rounds >= 100) {
    if (song3.isPlaying()) {
      song3.close();
    }    
    if (!song4.isPlaying()) {
      song4.play();
    }
    for (Boid b : flock.boids) {      
      b.maxforce = 0;
    }
    flock.run();     
  } 

  if (rounds < 20) {

      if (rounds < 13) {
          float dist_x =  min((balls[0].x_ - 100), (width - 100 - balls[0].x_));

          println("dist_x: "+dist_x);
          if (hit_ball == false && dist_x == 20) {
            song.rewind();
            song.play();
            hit_ball = true;
          }

          if (dist_x == 200) {
            hit_ball = false;
          }
    }


    if (last_direction != balls[0].direction_) {
      ++rounds;

        if (rounds < 14) {

          
        } else if (rounds >= 14 && rounds < 16){
          song2.play(0);
        }        
      
    } else if (balls[0].x_ == 0) {
      ++rounds; 
      //song.play(1);
    }
    last_direction = balls[0].direction_;
  }

}


class Ball {
  float width_;
  float x_;
  float y_;
  float speed;
  float display_x_;
  float display_y_;
  float display_width_;
  float degree_;
  int direction_;

  Ball(float TempX, float TempY, float TempD) {
    x_ = TempX;
    y_ = TempY;
    width_ = TempD;
    speed = 1;
    display_x_ = x_;
    display_y_ = y_;
    display_width_ = width_;
    degree_ = 5;
    direction_ = 1;
  }

  public void draw(int shape) {
    noStroke();    
    beginShape();
    if (shape == 0) {
      for (int angle = 0; angle <= 360; angle+=degree_) {
        float x = display_width_*sin(radians(angle));
        float y = display_width_*cos(radians(angle));
        vertex(x, y);
      }
    } else {      
      vertex(display_width_, -display_width_/2);
      vertex(display_width_, display_width_/2);
      vertex(-display_width_, 0);

    }
    endShape();
    fill(255);
  }


  public void breathing() {
    
    float offset = min((x_ - boundary_l), (boundary_r - x_));
    display_width_ = width_ + sin(offset*0.01f)*20;    
   
  }

  public void floatingX() {
    display_x_ = x_ + 100*noise(0.01f*display_y_);
  }

  public void floatingY() {
    display_y_ = y_ + 50*noise(0.01f*x_);
  }
  
  public void edges(int edges) {
    degree_ = 360 / edges;
  }

  public void diverging(float y) {
    float ball_x = x_;
    float offset_y = min((ball_x - boundary_l), (boundary_r - ball_x));
    display_y_ = y_ + 0.01f*(y)*(offset_y);
  }

  public void morphing() {
    int edges = floor(abs(x_ - width/2) / (width/2) * 30) + 3;
    this.edges(edges);
  }

  public void trianguling() {
    int edges;
    if (direction_ == 1) {
      edges = max(floor((width/2 - x_)/ (width/2) * 30) + 3, 3);
    } else {
      edges = max(floor((x_ - width/2)/ (width/2) * 30) + 3, 3);
    }

    this.edges(edges);
  }  


  public void move() {
    x_ += speed;     
    if (x_ > boundary_r || x_ < boundary_l) {
      speed *= -1;
      direction_ = - direction_; 
    }
    display_x_ = x_;
  }

  public void move2() {
    x_ += speed;     
    if (x_ > width) {
      x_ = 0;      
      
    } 
    if (x_ < 0) {
      x_ = width;    
      
    }
    display_x_ = x_;
  }


};

class Racket {
  float x_;
  float y_;
  float width_;
  float height_;
  PShape shape_;

  Racket(float x, float y, float w, float h) {
    x_ = x;
    y_ = y;
    width_ = w; 
    height_ = h;
  }

  public void display() {

    for (int i = 0; i < balls.length; ++i) {    
      float ball_x = balls[i].x_;    
      if (abs(ball_x - x_) < 10) {
        stroke(10);
        shapeMode(CENTER);
        shape(shape_, width/2, height/2, width_, height_);        
        fill(255);
      }
    }
  }
};



class Flock {
  ArrayList<Boid> boids; // An ArrayList for all the boids

  Flock() {
    boids = new ArrayList<Boid>(); // Initialize the ArrayList
  }

  public void run() {
    for (Boid b : boids) {
      b.run(boids);  // Passing the entire list of boids to each boid individually
    }
  }

  public void addBoid(Boid b) {
    boids.add(b);
  }
};




// The Boid class

class Boid {
  PVector location;
  PVector velocity;
  PVector acceleration;
  float r;
  float maxforce;    // Maximum steering force
  float maxspeed;    // Maximum speed

  Boid(float x, float y) {
    acceleration = new PVector(0, 0);
    float angle = random(TWO_PI);
    velocity = new PVector(cos(angle), sin(angle));

    location = new PVector(x, y);
    r = 20;
    maxspeed = 2;
    maxforce = 0.03f;
  }

  public void run(ArrayList<Boid> boids) {
    flock(boids);
    update();
    borders();
    render();
  }

  public void applyForce(PVector force) {
    // We could add mass here if we want A = F / M
    acceleration.add(force);
  }

  // We accumulate a new acceleration each time based on three rules
  public void flock(ArrayList<Boid> boids) {
    PVector sep = separate(boids);   // Separation
    PVector ali = align(boids);      // Alignment
    PVector coh = cohesion(boids);   // Cohesion
    // Arbitrarily weight these forces
    sep.mult(1.5f);
    ali.mult(1.0f);
    coh.mult(1.0f);
    // Add the force vectors to acceleration
    applyForce(sep);
    applyForce(ali);
    applyForce(coh);
  }

  // Method to update location
  public void update() {
    // Update velocity
    velocity.add(acceleration);
    // Limit speed
    velocity.limit(maxspeed);
    location.add(velocity);
    // Reset accelertion to 0 each cycle
    acceleration.mult(0);
  }

  // A method that calculates and applies a steering force towards a target
  // STEER = DESIRED MINUS VELOCITY
  public PVector seek(PVector target) {
    PVector desired = PVector.sub(target, location);  // A vector pointing from the location to the target
    // Scale to maximum speed
    desired.normalize();
    desired.mult(maxspeed);

    // Above two lines of code below could be condensed with new PVector setMag() method
    // Not using this method until Processing.js catches up
    // desired.setMag(maxspeed);

    // Steering = Desired minus Velocity
    PVector steer = PVector.sub(desired, velocity);
    steer.limit(maxforce);  // Limit to maximum steering force
    return steer;
  }

  public void render() {
    // Draw a triangle rotated in the direction of velocity
    float theta = velocity.heading2D() + radians(90);
    // heading2D() above is now heading() but leaving old syntax until Processing.js catches up
    
    
    noStroke();
    pushMatrix();
    translate(location.x, location.y);
    rotate(theta);
    beginShape(TRIANGLES);
    vertex(0, -r*2);
    vertex(-r, r*2);
    vertex(r, r*2);
    endShape();
    popMatrix();
    fill(255);
  }

  // Wraparound
  public void borders() {
    if (location.x < -r) location.x = width+r;
    if (location.y < -r) location.y = height+r;
    if (location.x > width+r) location.x = -r;
    if (location.y > height+r) location.y = -r;
  }

  // Separation
  // Method checks for nearby boids and steers away
  public PVector separate (ArrayList<Boid> boids) {
    float desiredseparation = 25.0f;
    PVector steer = new PVector(0, 0, 0);
    int count = 0;
    // For every boid in the system, check if it's too close
    for (Boid other : boids) {
      float d = PVector.dist(location, other.location);
      // If the distance is greater than 0 and less than an arbitrary amount (0 when you are yourself)
      if ((d > 0) && (d < desiredseparation)) {
        // Calculate vector pointing away from neighbor
        PVector diff = PVector.sub(location, other.location);
        diff.normalize();
        diff.div(d);        // Weight by distance
        steer.add(diff);
        count++;            // Keep track of how many
      }
    }
    // Average -- divide by how many
    if (count > 0) {
      steer.div((float)count);
    }

    // As long as the vector is greater than 0
    if (steer.mag() > 0) {
      // First two lines of code below could be condensed with new PVector setMag() method
      // Not using this method until Processing.js catches up
      // steer.setMag(maxspeed);

      // Implement Reynolds: Steering = Desired - Velocity
      steer.normalize();
      steer.mult(maxspeed);
      steer.sub(velocity);
      steer.limit(maxforce);
    }
    return steer;
  }

  // Alignment
  // For every nearby boid in the system, calculate the average velocity
  public PVector align (ArrayList<Boid> boids) {
    float neighbordist = 50;
    PVector sum = new PVector(0, 0);
    int count = 0;
    for (Boid other : boids) {
      float d = PVector.dist(location, other.location);
      if ((d > 0) && (d < neighbordist)) {
        sum.add(other.velocity);
        count++;
      }
    }
    if (count > 0) {
      sum.div((float)count);
      // First two lines of code below could be condensed with new PVector setMag() method
      // Not using this method until Processing.js catches up
      // sum.setMag(maxspeed);

      // Implement Reynolds: Steering = Desired - Velocity
      sum.normalize();
      sum.mult(maxspeed);
      PVector steer = PVector.sub(sum, velocity);
      steer.limit(maxforce);
      return steer;
    } 
    else {
      return new PVector(0, 0);
    }
  }

  // Cohesion
  // For the average location (i.e. center) of all nearby boids, calculate steering vector towards that location
  public PVector cohesion (ArrayList<Boid> boids) {
    float neighbordist = 50;
    PVector sum = new PVector(0, 0);   // Start with empty vector to accumulate all locations
    int count = 0;
    for (Boid other : boids) {
      float d = PVector.dist(location, other.location);
      if ((d > 0) && (d < neighbordist)) {
        sum.add(other.location); // Add location
        count++;
      }
    }
    if (count > 0) {
      sum.div(count);
      return seek(sum);  // Steer towards the location
    } 
    else {
      return new PVector(0, 0);
    }
  }
};

  static public void main(String[] passedArgs) {
    String[] appletArgs = new String[] { "--full-screen", "--bgcolor=#666666", "--hide-stop", "playforfreedom" };
    if (passedArgs != null) {
      PApplet.main(concat(appletArgs, passedArgs));
    } else {
      PApplet.main(appletArgs);
    }
  }
}
