/*
 * Gogh Bike Canvas (Layered)
 * Gogh Bike by Ben Cohen, Ryan Greenberg, Joyce Tsai
 */
 
import processing.serial.*;
import ddf.minim.*;

// The current layer implementation is very slow because of the slow JAVA 2D library.
// An alternative might be to use GLGraphics
// See http://users.design.ucla.edu/~acolubri/processing/glgraphics/home/index.html
// import processing.opengl.*;

// Sound effects
Minim minim;
String SOUND_FX_PATH_1 = "pour1.mp3";
String SOUND_FX_PATH_2 = "pour2.mp3";
String SOUND_FX_PATH_3 = "pour3.mp3";
String SOUND_FX_PATH_4 = "pour4.mp3";
AudioSample pourSoundFx1;
AudioSample pourSoundFx2;
AudioSample pourSoundFx3;
AudioSample pourSoundFx4;

boolean DEBUG = false;

// Screen size, constants, and buffers
final static int SCREEN_WIDTH = 1024;
final static int SCREEN_HEIGHT = 768;
final static int CANVAS_WIDTH = 768;
final static int CANVAS_HEIGHT = 768;
final static int FPS = 30; // For whatever reason, not accurate
final static boolean WRAP_CANVAS = false; // Whether the brush stops or moves to the other side of the canvas
static boolean ROTATE_CANVAS = true; // Whether the canvas is rotated with the change in the brush angle
PGraphics canvas, overlay;

float drawInterval; // milliseconds between canvas updates

// Bike variables
final static int RPM_WINDOW = 3;

// Expected range of values from potentiometer
final static int READING_CENTER = 199;
final static int READING_UPPER_BOUND = 85;
final static int READING_LOWER_BOUND = 340;

// Defined range for steering bicycle
final static int DIRECTION_LOWER_BOUND = -150;
final static int DIRECTION_CENTER = 0;
final static int DIRECTION_UPPER_BOUND = 150;
final static boolean CORRECT_ANGLE_CHANGE = false; // Change this to true when using an actual bicycle

// Paint variables
final static int VOLUME_OF_CAN = 100; // Maximum volume of paint can in units of paint
final static int VOLUME_PER_POUR = 10; // Units of paint added to bucket when a color is poured in
final static int VOLUME_PER_USAGE = 1; // Units of paint used when bike travels PAINT_USAGE_DISTANCE
final static int PAINT_USAGE_DISTANCE = 50; // Distance in pixels that bike must travel to use VOLUME_PER_USAGE paint
final static int PAINT_OPACITY_THRESHOLD = 10; // Volume of can at which brush begins to become transparent
final static int PAINT_BRUSH_WIDTH = 5; // Width of brush used on canvas
boolean fakePaintMode = false;
boolean serialPaintEnabled = true;

// Serial communication
boolean bikeSerialReady = false;
String bikePortname = "/dev/tty.usbserial-A7006RZH"; // or "COM5"
Serial bikePort;
boolean paintSerialReady = false;
String paintPortname = "/dev/tty.usbserial-A7006TaC"; // or "COM5"
Serial paintPort;
boolean lightSerialReady = false;
String lightPortname = "/dev/tty.usbserial-A7006SoU"; // or "COM5"
Serial lightPort;
String buf="";
String stringBuf="";
String bikeBuf="";
String paintBuf="";
int cr = 13;  // ASCII return   == 13
int lf = 10;  // ASCII linefeed == 10

// Simulation objects
Bike bike = new Bike(CANVAS_WIDTH / 2, CANVAS_HEIGHT / 2); // Initially place the bike in the center of the canvas
Recorder recorder;
Player player;
final static String FILE_PATH = "recordings/"; // Path to save bike recordings
final static boolean RECORDING_ENABLED = false;

long i = 0;
float actualFrames = 0;
long startActualFrames = 0;

void setup() {
  // Create canvas and graphics buffers
  size(SCREEN_WIDTH, SCREEN_HEIGHT);
  background(0);
  smooth();
  canvas = createGraphics(CANVAS_WIDTH, CANVAS_HEIGHT, JAVA2D); // Background layer where bike paints
  overlay = createGraphics(CANVAS_WIDTH, CANVAS_HEIGHT, P3D); // Overlay layer with indicators
  canvas.beginDraw();
  canvas.background(255);
  canvas.smooth();
  canvas.endDraw();

  // Calculate interval between draws based on specified frames per second
  // This could be done using the frameRate() option, but I think this offers
  // more precise control over timing.
  drawInterval = (1.0 / FPS) * 1000.0;
  println("For " + FPS + " frames per second, each frame displayed for " + drawInterval + "ms");
  startActualFrames = millis();

  // For testing set fixed bike speed
  bike.setSpeed(0);
  bike.setAngle(270);
  bike.setBrushWidth(PAINT_BRUSH_WIDTH);

  recorder = new Recorder(FILE_PATH, bike);

  // Load sound effects
  minim = new Minim(this);
  pourSoundFx1 = minim.loadSample(SOUND_FX_PATH_1, 2048);
  pourSoundFx2 = minim.loadSample(SOUND_FX_PATH_2, 2048);
  pourSoundFx3 = minim.loadSample(SOUND_FX_PATH_3, 2048);
  pourSoundFx4 = minim.loadSample(SOUND_FX_PATH_4, 2048);

  // Connect to Arduino boards
  println("Connecting to serial devices...");
  println(Serial.list());

  paintPort = new Serial(this, paintPortname, 115200);
  bikePort = new Serial(this, bikePortname, 9600);
  lightPort = new Serial(this, lightPortname, 9600);

  bikePort.bufferUntil(lf);
  paintPort.bufferUntil(lf);
  lightPort.bufferUntil(lf);

  frameRate(FPS);
}

void draw() {
    actualFrames++;
    background(0);
    canvas.beginDraw();
    
  if (bike.playback) {
    // For each call of advance(), use this.playbackSpeed
    //  to determine how many frames to draw
    for (int i = 0; i < player.playbackSpeed; i++) {    
      player.advance();
      bike.draw();
    }
  } else {
    bike.frame();
    recorder.write();
  }
    canvas.endDraw();
    
  if (ROTATE_CANVAS && !bike.playback) {
      // We translate the screen to put 0,0 at the center of the screen
      // Then we rotate the canvas and place the canvas image on the screen so that the brush position remains constant
      translate(SCREEN_WIDTH/2, SCREEN_HEIGHT/2);
      rotate(radians(270 - bike.angle));
      image(canvas, (0 - (CANVAS_WIDTH / 2)) + ((CANVAS_WIDTH / 2) - bike.x), (0 - (CANVAS_HEIGHT / 2)) + ((CANVAS_WIDTH / 2) - bike.y));
  } else {
      image(canvas, int((SCREEN_WIDTH - CANVAS_WIDTH)/2), 0);
  }

  // Framerate for debugging
//  i++;
//  if (i % 100 == 0) {
//    println(frameRate);
//    println("Actual frame rate: " + ((actualFrames / (millis() - startActualFrames)) * 1000));
//    startActualFrames = millis();
//    actualFrames = 0;
//  }
}


// In debug mode, the up and down arrow keys control the speed.
// Left and right arrows control steering
// r, g, b, and w pour that color of paint into the can
// e empties the can
void keyPressed() {
  // In MacOS X, the key codes for the arrow keys are:
  // 37 left
  // 38 up
  // 39 right
  // 40 down
  switch(keyCode) {
  case 37: // Left arrow
    // println("left");
    bike.steerLeft(5);
    break;
  case 38: // Up arrow
    // println("up");
    bike.speedUp(10);
    break;
  case 39: // Right arrow
    //println("right");
    bike.steerRight(5);
    break;
  case 40: // Down arrow
    // println("down");
    bike.slowDown(10);
    break;
  case 8: // Delete key
    resetCanvas();
    resetBike();
    break;
  default:
  }

  // Adding and removing paint
  if (key == 'r') {
    bike.can.addRed();
  } 
  else if (key == 'y') {
    bike.can.addYellow();
  } 
  else if (key == 'b') {
    bike.can.addBlue();
  } 
  else if (key == 'w') {
    bike.can.addWhite();
  }
  else if (key == 'e') {
    bike.can.empty();
  } 
  else if (key == 'q') {
    bike.toggleBrushDown();
  } 
  else if (key == 'f') {
    toggleFakeMode();
  } 
  else if (key == 'd') {
    toggleSerialPaintEnabled();
  }
  else if (key == 'z') {
    toggleRotatingCanvas();
  }
  else if (key == 'c') {
    println("Closing recording...NOT IMPLEMENTED");
    //    closeRecording();
  }
  else if (key == 's') {
    println("Sending serial event...");
    serialEvent(paintPort);
  }
  else if (key == 'o') {
    openRecording();
  } else if (key == '-') {
    if (player != null) {
      player.decreasePlaybackSpeed();
    }
  } else if (key == '=' || key == '+') {
    if (player != null) {
      player.increasePlaybackSpeed();
    }
  }

  // Debugging
  //    println("keyCode: " + keyCode);
  //    println("key: " + key);
}


// Serial communication
void serialEvent(Serial p) {
  if (bikePort.available() > 0) {
    try {
      stringBuf = bikePort.readStringUntil(lf);
      stringBuf = stringBuf.substring(0, stringBuf.length()-2); // Strip CR/LF

      if (bikeSerialReady) {
        if (stringBuf.charAt(0) == 'r') {
          // Rotation
          // Push the current reading and shift the reading at the beginning of the ArrayList
          bike.rpms.add(stringBuf.substring(1));
          bike.rpms.remove(0);
          bike.setSpeedFromRpms();
        } 
        else if (stringBuf.charAt(0) == 's') {
          // Steering
          bike.setDirectionFromPotentiometer(stringBuf.substring(1));
        } 
        else if (stringBuf.charAt(0) == 'b') {
          // Brake
          bike.setBrushDown(int(stringBuf.substring(1)));
        } 
        else {
          println("Received: " + stringBuf);
        }
      }

      if (!bikeSerialReady && stringBuf.equals("ready")) {
        bikeSerialReady = true;
        println("Bicycle serial port ready");
      }
      else if (!bikeSerialReady) {
        // Display any information that is received before ready
        // This is debugging information
        println(stringBuf);
      }    
    } 
    catch (Exception e) {
      println("Caught exception: " + e);
    }
  }

  // Serial data from light sensor
  if (lightPort.available() > 0) {
    try {
      stringBuf = lightPort.readStringUntil(lf);
      stringBuf = stringBuf.substring(0, stringBuf.length()-2); // Strip CR/LF

      if (lightSerialReady) {
        if (serialPaintEnabled) {

          if (fakePaintMode) {
            bike.can.addWhite();
          } 
          else {
            if (stringBuf.equals("cr")) {
              bike.can.addRed();
            } 
            else if (stringBuf.equals("cb")) {
              bike.can.addBlue();
            } 
            else if (stringBuf.equals("cy")) {
              bike.can.addYellow();
            } 
            else if (stringBuf.equals("cw")) {
              bike.can.addWhite();
            } 
            else {
              println("Received: " + stringBuf);
            }
          }
        }
      }

      if (!lightSerialReady && stringBuf.equals("ready")) {
        lightSerialReady = true;
        println("Light sensor serial port ready");
      }
      else if (!lightSerialReady) {
        // Display any information that is received before ready
        // This is debugging information
        println(stringBuf);
      }
    } 
    catch (Exception e) {
      println("Caught exception: " + e);
    }
  }

  if (paintPort.available() > 0) {
    try {
      stringBuf = paintPort.readStringUntil(lf);
      stringBuf = stringBuf.substring(0, stringBuf.length()-2); // Strip CR/LF

      if (paintSerialReady) {
        println("Received: " + stringBuf);
      }

      if (!paintSerialReady && stringBuf.equals("ready")) {
        paintSerialReady = true;
        println("Paint bucket serial port ready");
      } 
      else if (!paintSerialReady) {
        // Display any information that is received before ready
        // This is debugging information
        println(stringBuf);
      }
    } 
    catch (Exception e) {
      println("Caught exception: " + e);
    }
  }
}

void toggleFakeMode() {
  fakePaintMode = !fakePaintMode;
  println("Fake paint mode is " + fakePaintMode);
}

void toggleSerialPaintEnabled() {
  serialPaintEnabled = !serialPaintEnabled;
  println("serialPaintEnabled is " + serialPaintEnabled);
}

void toggleRotatingCanvas() {
  ROTATE_CANVAS = !ROTATE_CANVAS;
}

void openRecording() {
  String loadPath = selectInput();  // Opens file chooser
  if (loadPath == null) {
    // If a file was not selected
    println("No file was selected...");
  } 
  else {
    // If a file was selected, reset the canvas and play the file
    resetCanvas();
    // Close the recorder for the current bike if it exists
    if (!recorder.equals(null)) {
      println("Closing existing recorder...");
      recorder.close();
    }
    bike = new Bike(CANVAS_WIDTH / 2, CANVAS_HEIGHT / 2);
    player = new Player(loadPath, bike);
  }
}

void resetCanvas() {
  background(255);
  canvas.beginDraw();
  canvas.background(255);
  canvas.endDraw();
}

void resetBike() {
  // Close the recorder for the current bike if it exists
  if (!recorder.equals(null)) {
    println("Closing existing recorder...");
    recorder.close();
  } else {
    println("No recorder to close...");
  }

  // Create a new bike and recorder
  bike = new Bike(CANVAS_WIDTH / 2, CANVAS_HEIGHT / 2); // Initially place the bike in the center of the canvas
  bike.setSpeed(0);
  bike.setAngle(270);
  bike.setBrushWidth(5);
  recorder = new Recorder(FILE_PATH, bike);
}

void stop() {
  // Close sound effects
  pourSoundFx1.close();
  pourSoundFx2.close();
  pourSoundFx3.close();
  pourSoundFx4.close();
  minim.stop();
  recorder.close();
  super.stop();
}