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

import toxi.math.conversion.*; 
import toxi.geom.*; 
import toxi.math.*; 
import toxi.geom.mesh2d.*; 
import toxi.util.datatypes.*; 
import toxi.util.events.*; 
import toxi.geom.mesh.subdiv.*; 
import toxi.geom.mesh.*; 
import toxi.math.waves.*; 
import toxi.util.*; 
import toxi.math.noise.*; 
import java.util.*; 
import ddf.minim.spi.*; 
import ddf.minim.signals.*; 
import ddf.minim.*; 
import ddf.minim.analysis.*; 
import ddf.minim.ugens.*; 
import ddf.minim.effects.*; 
import toxi.geom.*; 

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 demo extends PApplet {
















//window related constants
//int CANVAS_WIDTH = 480; int CANVAS_HEIGHT = 360;
int CANVAS_WIDTH = 1920; int CANVAS_HEIGHT = 1080;
//int CANVAS_WIDTH = 640; int CANVAS_HEIGHT = 360;
int SONG_SKIP_MILLISECONDS = 2000;

//audio related constants
String SONG_FILE = "Freaky_-_For_a_Better_Days.wav";
int FFT_BUFFER_SIZE = 1024; // soittopuskurin koko
int FFT_WINDOW_SIZE = 2048; //2048; // fft-puskurin koko, muutettavissa napeilla a ja s

//graphics related constants
int GRID_SIZE = 5;
int GRID_DISTANCE = 450;

float TIME_GREETS_START = 22.5f; //synced with music
float TIME_GREETS_STOP  = 40.0f;
float TIME_INFO_STOP = 10;

float cameraPosMark1 = 0;
float cameraTimeMark1 = 0;

float WORM_BEGIN_TIME = 40.0f;
float WORM_BEGIN_ROTATIONS_TIME = 45.0f;

float DEMO_END_TIME = 103.2f;

BallGrid fractal;
Worm worm;
AudioManager audioManager;

public void setup() {  
  println("setup start");
  // The P3D parameter enables accelerated 3D rendering.
  size(displayWidth, displayHeight, P3D);
  rectMode(CENTER);
     
  // Drawing options that don't change, modify as you wish
  frameRate(30);
  //noStroke();
  //fill(255);
  smooth();

  //setups audio
  audioManager = new AudioManager(this, SONG_FILE, FFT_BUFFER_SIZE, FFT_WINDOW_SIZE);
  fractal = new BallGrid(this, audioManager);
  worm = new Worm(this, 0, 0 ,0);
  audioManager.play();
  println("setup end");
}

public void draw() {
  audioManager.update();
  
  //center view
  resetMatrix();
  background(255); 
  
  drawDemo(audioManager.currentTimeSeconds());
}


public void keyPressed() {
  if (key == CODED) {
    // Left/right arrow keys: seek song
    if (keyCode == LEFT) {
      audioManager.skip(-SONG_SKIP_MILLISECONDS);
    } 
    else if (keyCode == RIGHT) {
      audioManager.skip(SONG_SKIP_MILLISECONDS);
    }
  }
  // Space bar: play/payse
  else if (key == ' ') {
    if (audioManager.isPlaying())
      audioManager.pause();
    else
      audioManager.play();
  }
}

public void setCameraPosition(float seconds){
  // one degree rotation in radians per seconds
  Vec3D eye = new Vec3D(0.0f, 0.0f, 0.0f);
  Vec3D cen = new Vec3D(0.0f, 0.0f, 0.0f);
  Vec3D up = new Vec3D(0.0f, 1.0f, 0.0f);
  
  
  // camera movement logic here..
  if(seconds < TIME_GREETS_START){
    eye.z = -2000 + cos(seconds * 0.3f)*-1000;
    eye.rotateY(sin(seconds*0.3f) * PI/2);

  } else if (seconds < TIME_GREETS_STOP) {
    eye.z = 2500;
    eye.x = GRID_DISTANCE * cos(seconds);
    eye.y = GRID_DISTANCE * sin(seconds);    
    eye.rotateY(sin((seconds-TIME_GREETS_START))*0.5f);
    
  } else if (seconds < WORM_BEGIN_ROTATIONS_TIME){    
    eye.z = 2500;    
  } else if( seconds < DEMO_END_TIME){
    float timeT = seconds-WORM_BEGIN_ROTATIONS_TIME;
    eye.z = 2500;
    eye.x = GRID_DISTANCE * sin(timeT);
    eye.y = GRID_DISTANCE * -sin(timeT);  
    eye.rotateX(sin(timeT*0.3f)*PI/2);
    eye.rotateY(-sin(timeT*0.2f)*PI/2);
    eye.rotateZ(sin(timeT*0.1f)*PI/2);
  } else{
    // do nothing -> blank 
  }
  
  
  
  beginCamera();
  camera(eye.x, eye.y, eye.z, cen.x, cen.y, cen.z, up.x, up.y, up.z);  
  endCamera();
  
  /*
  float dps = radians(seconds % 360);
  if(seconds < 22.5){   
    
    translate(cos(seconds) * 200, 0, -2000 + cos(seconds * 0.3)*-1000);//+ -sin(seconds*0.5) * -1000);
    rotateY(dps*5);
    rotateZ(dps*10 * sin(dps));
  } else if (seconds < 32){
    cameraPosMark1 = -4000 + (seconds -22.5)*500;
    translate(GRID_DISTANCE/2, GRID_DISTANCE/2, cameraPosMark1); //move camera to position   
  } else if (seconds < 36) {
    translate(GRID_DISTANCE/2, GRID_DISTANCE/2, cameraPosMark1); //hold camera at position
    cameraTimeMark1 = seconds;
  } else if (seconds < 40){
    translate(GRID_DISTANCE/2, GRID_DISTANCE/2, cameraPosMark1); //rotate camera at position
    float rotation_amount = (seconds - cameraTimeMark1) * 30;
    if(rotation_amount > 180){
      rotation_amount = 180; 
    }
    rotateX(radians(rotation_amount));
    
  } else {
    translate(GRID_DISTANCE/2, GRID_DISTANCE/2,0);
    translate(cos(seconds) * 200, 0, -2000 + cos(seconds * 0.3)*-1000);
    rotateY(dps);
    rotateZ(dps*5);
  }*/
  
  
  //rotateY(seconds*sin(seconds*0.2)*0.8);
  //rotateZ(seconds*sin(cos(seconds))*0.1);
  //rotateX(seconds*0.2);
}

public void drawBackground(){
  background(0);
}

public void drawDemo(float seconds) {
  println("TIME: ", seconds );
  setCameraPosition(seconds);
  drawBackground();

  if(seconds > DEMO_END_TIME){
    audioManager.pause(); 
  }


  //start text  
  if(seconds < TIME_INFO_STOP){
    pushMatrix();
    rotateY(PI+sin(seconds)*0.7f);
    
    fill(255);
    textSize(40);
    textAlign(CENTER);
    text("Graffathon 2014", 0, -60, 0);
    text("CubeGrid", 0, 0, 0);
    text("By BJAKKE", 0, +60, 0);
    popMatrix();
  }
  
  //Greets 
  if(seconds > TIME_GREETS_START && seconds < TIME_GREETS_STOP){
    fill(255);
    textSize(60);
    textAlign(CENTER);
    text("GREETINGS TO EVERYONE!", 200*sin(seconds*0.5f), cos(seconds*0.9f)*300, 0);
  }


  fractal.draw(GRID_SIZE,GRID_SIZE,GRID_SIZE, GRID_DISTANCE);

   
  if(seconds >= WORM_BEGIN_TIME){
    pushMatrix();
    translate(GRID_DISTANCE/2, GRID_DISTANCE/2, 0); 
    worm.setStartTime(seconds ); // will set only once
    worm.draw(seconds );
    popMatrix();
  }

}








class AudioManager{
  PApplet applet;
  
  Minim minim;
  AudioPlayer audioplayer; // soittaa musat, doh
  MultiChannelBuffer buffers; // sama biisi bufferissa kerrallaan
  float[] sampledata; // bufferin samplet raakana
  FFT fft;
  
  int bufsize; // soittopuskurin koko
  int windowsize; // fft-puskurin koko
  int rate; // samplerate
  
  BeatDetect bdetect;

  AudioManager(PApplet _applet, String songName, int _bufsize, int _windowsize){
    applet = _applet;
    bufsize = _bufsize;
    windowsize = _windowsize;
     
    minim = new Minim(applet);    
    audioplayer = minim.loadFile(songName, bufsize);
    buffers = new MultiChannelBuffer(2, 2); // initnumeroilla ei v\u00c3\u00a4li\u00c3\u00a4
    
    rate = (int)audioplayer.sampleRate();
    minim.loadFileIntoBuffer(songName, buffers);
    sampledata = buffers.getChannel(0); // vain vasen kanava
    fft = new FFT(windowsize, rate);
    fft.window(FFT.HAMMING); 
    
    bdetect = new BeatDetect(windowsize, rate);
    bdetect.detectMode(BeatDetect.FREQ_ENERGY);
  
  }

  public float currentTimeSeconds(){  
     return audioplayer.position() / 1000.0f; 
  }

  public void play(){
      audioplayer.play();  
  }
  
  public void pause(){
     audioplayer.pause(); 
  }
  
  public boolean isPlaying(){
     return audioplayer.isPlaying();
  }
  
  public void update(){
    analyze();
  }  
  
  public float analyzeRange(int f_a, int f_b){
    float fftsum = 0;
    for(int i = f_a; i <= f_b; i++){
      fftsum += fft.getBand(i);      
    }
    return fftsum / (f_b - f_a +1.0f); //linearize
   }
  
  public BeatDetect beatDetector(){
     return bdetect; 
  }
  
  
  //must be called in draw
  public void analyze() {
    int samplepos = player_samplepos();
    float[] window = Arrays.copyOfRange(sampledata, samplepos, samplepos + windowsize);
    fft.forward(window);
    bdetect.detect(window);
  }
  
  // mist\u00c3\u00a4 kohdasta luetaan fft-sampleikkuna
  public int player_samplepos() {
    // minim palauttaa millisekunteja
    float secs = audioplayer.position() / 1000.0f;
    // aika sampleyksik\u00c3\u00b6iss\u00c3\u00a4
    int samplepos = (int)(secs * audioplayer.sampleRate());
    // nykyhetki ikkunan keskelle, vaihtoehtona mm.
    // koko windowsize eli aika ikkunan lopussa 
    samplepos -= windowsize/2;
    // ei vahingossakaan negatiiviseks
    samplepos = max(samplepos, 0);
    // eik\u00c3\u00a4 toisenkaan reunan yli
    samplepos = min(samplepos, sampledata.length - windowsize);
    return samplepos;
  }
  
  public void skip(int milliseconds){
     audioplayer.skip(milliseconds); 
  } 


}
class BallGrid{
  PApplet applet;
  PShape shape;
  PShape trigger_shape;
  AudioManager audioManager;
  
  int FFT_RANGE_BEGIN = 1;
  int FFT_RANGE_END = 1;
  int FFT_TRIGGER = 20;
  
  int FFT_ROTATE_RANGE_BEGIN = 92;
  int FFT_ROTATE_RANGE_END = 98;
  
  float fade = 0.0f;
  
  BallGrid(PApplet _applet, AudioManager _audioManager){
    applet = _applet;  
    audioManager = _audioManager;
    
    shape = createShape(PConstants.BOX, 35);
    shape.setFill(color(200, 0, 255));
    shape.setStroke(color(255, 255, 255));
    shape.setStrokeWeight(1);
    //shape.setStroke(0);

    
    //trigger_shape = createShape(PConstants.SPHERE, 30);
    trigger_shape = createShape(PConstants.SPHERE, 25);
    //trigger_shape.setFill(color(0, 0, 255));
    trigger_shape.setFill(color(200, 0, 255));
    trigger_shape.setStroke(color(255, 255, 255));
    trigger_shape.setStrokeWeight(1);
    //trigger_shape.setStroke(0);
    
  }
  
  public void draw(int xlevel, int ylevel, int zlevel, float distance){
    float pwr = audioManager.analyzeRange(FFT_RANGE_BEGIN, FFT_RANGE_END);
   
   
   
    //if(pwr > FFT_TRIGGER){
    //if(audioManager.beatDetector().isHat()){  
    if(pwr > 20){ 
      println("BEAT", pwr, "NOW"); 
      fade = 1;
      
    }else{
      fade *= 0.90f;
      println("BEAT", pwr);
    }
    
    /*PShape cen = createShape(PConstants.SPHERE,30 );
    cen.setFill(color(255, 255, 255));
    cen.setStroke(color(255, 255, 255));
    cen.setStrokeWeight(0);
    cen.scale(fade);
    shape(cen);
    */
    
    pushMatrix();
    translate(GRID_DISTANCE/2, GRID_DISTANCE/2, GRID_DISTANCE/2);
    
    //temp shape for drawing
    PShape sh = createShape(shape);
    sh.scale(fade);
    //ambientLight(51, 102, 126);
 
 
    for(int z = -zlevel; z < zlevel; z++){
       for(int y = -ylevel; y < ylevel; y++){
          for(int x = -xlevel; x < xlevel; x++){
             pushMatrix();
             translate(x* distance, y*distance, z*distance);
             //if(fade > 0.1){
               //translate(20,20,20); // displacement
               triggerShape(sh);
             //}else{
             //  triggerShape(shape );
             //}
             popMatrix();              
          }
       } 
    }
    
      popMatrix();
  }
  
  public void triggerShape(PShape _shape){
    rotateX(audioManager.currentTimeSeconds()*1.5f);
    rotateZ(sin(audioManager.currentTimeSeconds()));
    shape(_shape); 
  }

}


class Worm{
  
  PApplet applet;
  Vec3D pos;
  PShape shape;
  float startTime = 0;

  Worm(PApplet _applet, float x, float y, float z){
    applet = _applet;
    pos = new Vec3D(x, y, z);    
    
    shape = createShape(PConstants.SPHERE, 5);
    shape.setFill(color(255, 255, 255));
    shape.setStroke(color(0, 255, 255));
    shape.setStrokeWeight(1);
  }
  
  public void setStartTime(float seconds){
    if(startTime <1){
      startTime = seconds;
    }
  }
  
  public void drawInternal(){
    shape(shape);
  }
  
  public void draw(float seconds){
    pushMatrix();
    translate(pos.x, pos.y, pos.z);
    drawInternal();
    
    
    float stepsize = 0.1f;    
    for(float t = startTime; t < seconds; t+=stepsize){
       translate(20, 50, 80*sin(t));
       rotateX(t*5);
       rotateY(t*4);
       rotateZ(sin(t)*1);
       drawInternal();      
    }
    
    
    popMatrix();
    
  }
  
  
}
  static public void main(String[] passedArgs) {
    String[] appletArgs = new String[] { "--full-screen", "--bgcolor=#666666", "--hide-stop", "demo" };
    if (passedArgs != null) {
      PApplet.main(concat(appletArgs, passedArgs));
    } else {
      PApplet.main(appletArgs);
    }
  }
}
