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

import ddf.minim.*; 
import processing.serial.*; 

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

//import processing.sound.*;

//import ddf.minim.analysis.*;
//import ddf.minim.effects.*;
//import ddf.minim.signals.*;
//import ddf.minim.spi.*;
//import ddf.minim.ugens.*;

Minim minim = new Minim(this);

public class Button{
  public float x;
  public float y;
  public float w;
  public float h;
  public String text;
  public boolean pressed;
  public boolean lastpressed;
}

final int LAMPS=32;
final int TIMEOUT=20;
final int PACKET_TIMEOUT=20;

File seqFile;
byte[] seqData;
File wavFile;
AudioPlayer aFile;
long seqt=0;        //how far into sequence (millis)
long seql=0;        //sequence length
long playt=0;
int seqPtr=0;
boolean playState=false;  //for sequence only playback
boolean pauseState=false;
long seqt0=0;
String stateString="STOPPED";
long pLast;    //timing for playback

//outputs and timing
int[] lamps = new int[LAMPS];    //for outputs, only 32 in current LSQ code
int[] rampSteps = new int[LAMPS];  //steps needed for ramp (signed to ramp up and down)
int[] rampTime = new int[LAMPS];   //time to ramp over
long mLast;        //timing

int foreColour=color(192,192,192);
int backColour=color(64,64,64);
int textColour=color(0,0,0);
Button openButton=new Button();
Button playButton=new Button();
Button pauseButton=new Button();
Button stopButton=new Button();


//for serial port management
Serial DLC;
int serialIndex=0;
String portName="-no ports-";
String currentPort="";
boolean portConnect=false;

Button upButton=new Button();
Button downButton=new Button();
Button conButton=new Button();
Button discButton=new Button();

public void setup() {
  int i;
    
  surface.setTitle("Silicon Chip Digital Lighting Controller "+stateString);
  surface.setResizable(false);          //doesn't prevent maximise
  background(color(128,128,128));      //cls
  noStroke();    //transparent stroke
  textSize(32);   //gives 24 pixels high text
  openButton.x=10;
  openButton.y=10;
  openButton.w=80;
  openButton.h=40;
  openButton.text="Open";
  openButton.pressed=false;
  openButton.lastpressed=false;

  playButton.x=10;
  playButton.y=60;
  playButton.w=80;
  playButton.h=40;
  playButton.text="Play";
  playButton.pressed=false;
  playButton.lastpressed=false;

  pauseButton.x=100;
  pauseButton.y=60;
  pauseButton.w=80;
  pauseButton.h=40;
  pauseButton.text="Pause";
  pauseButton.pressed=false;
  pauseButton.lastpressed=false;

  stopButton.x=190;
  stopButton.y=60;
  stopButton.w=80;
  stopButton.h=40;
  stopButton.text="Stop";
  stopButton.pressed=false;
  stopButton.lastpressed=false;

  for(i=0;i<LAMPS;i++){
    lamps[i]=0;    //set as off
    rampSteps[i]=0;
    rampTime[i]=0;
  }

  if(Serial.list().length>0){portName=Serial.list()[0];}  

  upButton.x=290;
  upButton.y=10;
  upButton.w=80;
  upButton.h=40;
  upButton.text="Up";
  upButton.pressed=false;
  upButton.lastpressed=false;

  downButton.x=290;
  downButton.y=60;
  downButton.w=80;
  downButton.h=40;
  downButton.text="Down";
  downButton.pressed=false;
  downButton.lastpressed=false;

  conButton.x=380;
  conButton.y=10;
  conButton.w=90;
  conButton.h=40;
  conButton.text=portName;
  conButton.pressed=false;
  conButton.lastpressed=false;

  discButton.x=380;
  discButton.y=60;
  discButton.w=90;
  discButton.h=40;
  discButton.text="=//=";
  discButton.pressed=false;
  discButton.lastpressed=false;

  mLast=millis();
}

public void draw(){
  int i;
  if(playState){
    if(pauseState){
      stateString="PAUSED";
      pLast=millis();      //only advance during playback
    }else{
      stateString="PLAYING";
    }
  }else{
    stateString="STOPPED";
    pLast=millis();      //only advance during playback
  }
  surface.setTitle("Silicon Chip Digital Lighting Controller "+stateString);
  background(color(128,128,128));      //cls
  textSize(32);   //gives 24 pixels high text
  textAlign(CENTER, CENTER);
  if(checkClick(openButton)){
    //same as stop sequence following
    //print("STOP\r\n");
    playState=false;      
    pauseState=false;  
    if(aFile != null){
      aFile.pause();
      aFile.rewind();
      seqt=0;
      seqPtr=0;
      for(i=0;i<LAMPS;i++){lamps[i]=0;}    //set as off
    }else{
      //print("NULL\r\n");
      seqPtr=0;
      seqt=0;
      playt=0;    
      for(i=0;i<LAMPS;i++){lamps[i]=0;}    //set as off
    }

    //print("CLICK\r\n");
    selectInput("Select a file to process:", "fileSelected");    
  }
  if(checkClick(playButton)){
    playState=true;      
    pauseState=false;
    //print("PLAY\r\n");
    if(aFile != null){
      aFile.play();
    }else{
      //print("NULL\r\n");            
    }
  }
  if(checkClick(pauseButton)){
    //print("PAUSE\r\n");
    if(playState){pauseState=true;}
    if(aFile != null){
      aFile.pause();
    }else{
      //print("NULL\r\n");
    }
  }
  if(checkClick(stopButton)){
    //print("STOP\r\n");
    playState=false;      
    pauseState=false;  
    if(aFile != null){
      aFile.pause();
      aFile.rewind();
      seqt=0;
      seqPtr=0;
      for(i=0;i<LAMPS;i++){lamps[i]=0;}    //set as off
    }else{
      //print("NULL\r\n");
      seqPtr=0;
      seqt=0;
      playt=0;    
      for(i=0;i<LAMPS;i++){lamps[i]=0;}    //set as off
    }
  }
  if(checkClick(upButton)&&portConnect==false){
    serialIndex++;
  }
  if(checkClick(downButton)&&portConnect==false){
    serialIndex--;
    if(serialIndex<0){serialIndex=0;}    
  }
  if(serialIndex>Serial.list().length-1){serialIndex=Serial.list().length-1;}
  if(Serial.list().length>0){
    portName=Serial.list()[serialIndex];
    conButton.text=portName;
  }
  if(checkClick(conButton)){
    if(portConnect==false){
      try{
        currentPort=portName;      //save so it can't change
        DLC=new Serial(this, currentPort, 38400);
        portConnect=true;           
        
      } catch(Exception e){
        portConnect=false;              //failed to connect        
      }
    }else{
      DLC.stop();
      portConnect=false;              
    }      
  }
  //disc button shows status only
  if(checkClick(discButton)&&portConnect){
    DLC.stop();
    portConnect=false;                  
  }
  if(portConnect){
    discButton.text="=<>=";
  }else{
    discButton.text="=//=";
  }    
  if(aFile != null){
    int p,l;
    String pp,ll;
    p=aFile.position()/1000;
    l=aFile.length()/1000;    //convert to seconds
    pp=str(p%60);
    if(pp.length()<2){pp="0"+pp;}    
    ll=str(l%60);
    if(ll.length()<2){ll="0"+ll;}    
    text(PApplet.parseInt(p/60)+":"+pp+"/"+PApplet.parseInt(l/60)+":"+ll,190,30);
    if((aFile.position()==aFile.length())&& (aFile.isPlaying() == false)){aFile.rewind();}    //reset at end of playback
    //process LSQ data
    while((aFile.position()>seqt) && (seqPtr<seqData.length) && aFile.isPlaying()){
      seqt=seqt+parsePair(seqPtr);
      seqPtr=seqPtr+2; 
    }
  }else{
    int p,l;
    String pp,ll;
    p=PApplet.parseInt(playt/1000);
    l=PApplet.parseInt(seql/1000);    //convert to seconds
    pp=str(p%60);
    if(pp.length()<2){pp="0"+pp;}    
    ll=str(l%60);
    if(ll.length()<2){ll="0"+ll;}    
    text(PApplet.parseInt(p/60)+":"+pp+"/"+PApplet.parseInt(l/60)+":"+ll,190,30);
    //process LSN data
    if(playt>seql){    //stop due to end
      playState=false;      
      pauseState=false;  
      seqPtr=0;
      seqt=0;
      playt=0;    
    }
    if(seqData != null){
      while((playt>seqt) && (seqPtr<seqData.length) && playState && (!pauseState)){
        seqt=seqt+parsePair(seqPtr);
        seqPtr=seqPtr+2; 
      }
    }    
  }
  drawButton(openButton);
  drawButton(playButton);
  drawButton(pauseButton);
  drawButton(stopButton);
  drawButton(upButton);
  drawButton(downButton);
  drawButton(conButton);
  drawButton(discButton);
  textSize(16);
  noStroke();    //transparent stroke
  fill(textColour);
  textAlign(CENTER, CENTER);
  if(seqFile != null){    
    text(seqFile.getName(),240,280);
  }else{
    text("---",240,300);
  }
  if(wavFile != null){    
    text(wavFile.getName(),240,300);
  }else{
    text("---",240,300);
  }
  for(i=0;i<LAMPS;i++){
    stroke(color(192,192,0));    //yellow for contrast
    fill(color(lamps[i],lamps[i],lamps[i]));
    circle((i&7)*40+100,(i/8)*40+130,30);    
  }  
  if(millis()-mLast>PACKET_TIMEOUT){
    checkRamps();
    mLast=mLast+PACKET_TIMEOUT;
    sendPacket();
  }
  while(millis()-mLast>PACKET_TIMEOUT){    //in case we fall behind, do extra time updates
    checkRamps();
    mLast=mLast+PACKET_TIMEOUT;
  }
  while(millis()-pLast>TIMEOUT){
    pLast=pLast+TIMEOUT;
    if(playState && (!pauseState)){playt=playt+TIMEOUT;}
    //println( millis()-playt);
  }
}

public boolean checkClick(Button b){
  boolean t=false;
  b.pressed=checkPress(b);
  if(b.pressed && (b.lastpressed==false)){
    t=true;
  }
  b.lastpressed=b.pressed;
  return t;
}

public void drawButton(Button b){
  noStroke();    //transparent stroke
  if(b.pressed){
    fill(foreColour);
  }else{
    fill(backColour);
  }    
  rect(b.x,b.y,b.w,b.h);  
  if(b.pressed){
    stroke(backColour);
  }else{
    stroke(foreColour);
  }    
  strokeWeight(2);
  line(b.x,b.y,b.x+b.w-1,b.y);
  line(b.x,b.y+b.h-1,b.x+b.w-1,b.y+b.h-1);
  line(b.x,b.y,b.x,b.y+b.h-1);
  line(b.x+b.w-1,b.y,b.x+b.w-1,b.y+b.h-1);
  noStroke();    //transparent stroke
  fill(textColour);
  textAlign(CENTER, CENTER);
  textSize(24);   //gives 18 pixels high text
  text(b.text,b.x+b.w/2,b.y+b.h/2-4);    //fudge factor to centre text      
}

public boolean checkPress(Button b){
  if((mouseX>b.x)&&(mouseX<b.x+b.w-1)&&(mouseY>b.y)&&(mouseY<b.y+b.h-1)&&(mouseButton==LEFT)&&mousePressed){
    return true;
  }else{
    return false;
  }    
}

public void fileSelected(File selection) {
  String wName,t;
  int i;
  long tt;
  if (selection == null) {
    //println("Window was closed or the user hit cancel.");
    wavFile=null;
    aFile=null;
    seqFile=null;
  } else {
    seqFile=selection;
    //println("User selected: " + selection.getAbsolutePath());
    t=seqFile.getAbsolutePath().substring(seqFile.getAbsolutePath().length()-3).toUpperCase();
    seqData = loadBytes(seqFile.getAbsolutePath());
    //print(seqData.length);
    //println(" bytes in file");
    seqt=0;
    seqPtr=0;
    tt=0;
    for(i=0;i<seqData.length;i=i+2){
      tt=tt+parsePair(i);
    }
    seql=tt;
    //print(tt);
    //println(" ms in file");
    if(t.equals("LSQ")){
      wName=seqFile.getAbsolutePath().substring(0,seqFile.getAbsolutePath().length()-3)+"wav";
      //print("Looking for:");
      //println(wName);
      aFile = minim.loadFile(wName);
      wavFile= new File(wName);
    }else{
      //println("non-LSQ selected");
      wavFile = null;
    }
  }
}

public long parsePair(int p){    //return #of milliseconds, so we can do a pass for timekeeping
  int first, second;
  int i;
  if(p>(seqData.length-2)){return 0;}
  first=PApplet.parseInt(seqData[p]&0xff);
  second=PApplet.parseInt(seqData[p+1]&0xff);
  switch(second){
    case 0: for(i=0;i<LAMPS;i++){lamps[i]=0;}  return 0;  //0x00 0x00 = all off
    case 1: return first+1;      //delay for x+1 millis
    case 2: return (first+1)*4;  //delay for (x+1)*4 millis
    case 3: return (first+1)*16; //delay for x+1 millis
    default:
      switch(second>>2){
        case 1: for(i=0;i<8;i++){lamps[i+(second&3)*8]=PApplet.parseBoolean(first&(1<<i))?255:0;} return 0;    //set states of set
        case 2: for(i=0;i<8;i++){if(PApplet.parseBoolean(first&(1<<i))){lamps[i+(second&3)*8]=0;}} return 0;  //turn off selected in set
        case 3: for(i=0;i<8;i++){if(PApplet.parseBoolean(first&(1<<i))){lamps[i+(second&3)*8]=255;}} return 0;  //turn on selected in set
        case 4: lamps[first&31]=255; lamps[(second*8+first/32)&31]=255; return 0;                  //turn on 2 lights
        case 5: lamps[first&31]=0; lamps[(second*8+first/32)&31]=0; return 0;                      //turn off 2 lights
        case 6: lamps[first&31]=255; lamps[(second*8+first/32)&31]=0; return 0;                    //turn 1 on and 1 off
        case 7: lamps[first&31]=255-lamps[first&31]; lamps[(second*8+first/32)&31]=255-lamps[(second*8+first/32)&31]; return 0;  //invert 2 lights
        default:
          switch(second>>5){
            case 1: lamps[second & 31]=first;  return 0;    //set one lamp
            case 2:  rampSteps[second & 31] = 255 - lamps[second & 31]; rampTime[second & 31] = first+1;return 0;  //ramp up over t  
            case 3:  rampSteps[second & 31] = 0 - lamps[second & 31]; rampTime[second & 31] = first+1;return 0;  //ramp down over t  
            case 4:  rampSteps[second & 31] = 255 - lamps[second & 31]; rampTime[second & 31] = (first+1)*4;return 0;  //ramp up over t*4  
            case 5:  rampSteps[second & 31] = 0 - lamps[second & 31]; rampTime[second & 31] = (first+1)*4;return 0;  //ramp down over t*4  
            case 6:  rampSteps[second & 31] = 255 - lamps[second & 31]; rampTime[second & 31] = (first+1)*16;return 0;  //ramp up over t*16
            case 7:  rampSteps[second & 31] = 0 - lamps[second & 31]; rampTime[second & 31] = (first+1)*16;return 0;  //ramp down over t*16 
          }
      }  
  }
  return 0;
}



public void sendPacket(){
  int i;
  if(portConnect){
    try{
       DLC.stop();
       DLC=new Serial(this, currentPort, 9600);
       DLC.write(192);    //MSB set to give MAB
       DLC.stop();
       DLC=new Serial(this, currentPort, 38400);
       DLC.write(0);      //lamp data to follow
       for(i=0;i<LAMPS;i++){
         DLC.write(lamps[i]);
       }
       //leave port open between calls so no-one else can grab it
    }catch(Exception e){        } //quietly ignore
  }
}

public void checkRamps(){
  int i;
  int s;    //temp steps
  for(i=0;i<LAMPS;i++){
    if(rampSteps[i]!=0){
      if(rampTime[i]<=PACKET_TIMEOUT){    //finish, also catches div/0
        lamps[i]=lamps[i]+rampSteps[i];
        rampSteps[i]=0;
        rampTime[i]=0;
      }else{
        s=PApplet.parseInt(((long)(PACKET_TIMEOUT)*(long)(rampSteps[i]))/(long)(rampTime[i]));
        lamps[i]=lamps[i]+s;
        rampSteps[i]=rampSteps[i]-s;
        rampTime[i]=rampTime[i]-TIMEOUT;        
      }
    }
    //catch overflow
    if(lamps[i]<0){lamps[i]=0;}
    if(lamps[i]>255){lamps[i]=255;}
  }
}
  public void settings() {  size(480, 320,P3D); }
  static public void main(String[] passedArgs) {
    String[] appletArgs = new String[] { "Digital_Lighting_Controller" };
    if (passedArgs != null) {
      PApplet.main(concat(appletArgs, passedArgs));
    } else {
      PApplet.main(appletArgs);
    }
  }
}
