import processing.core.*; 
import processing.xml.*; 

import javax.swing.JColorChooser; 
import java.awt.Color; 
import com.bric.swing.ColorPicker; 
import processing.serial.*; 

import java.applet.*; 
import java.awt.Dimension; 
import java.awt.Frame; 
import java.awt.event.MouseEvent; 
import java.awt.event.KeyEvent; 
import java.awt.event.FocusEvent; 
import java.awt.Image; 
import java.io.*; 
import java.net.*; 
import java.text.*; 
import java.util.*; 
import java.util.zip.*; 
import java.util.regex.*; 

public class RGBmtx extends PApplet {

/*
 * RGBmtx - an RGB Matrix Editor/Controller v1.0
 *
 * hacked from
 *   mtXcontrol - a LED Matrix Editor - version 1.1
 */

//scl
public static class Config {
  public static int xdim = 8;
  public static int ydim = 8;
  public static boolean rgbmode = true;
}
PFont fontA;
PFont fontLetter;

Matrix matrix;
Device device;

int border = 10;

int offY = 30;
int offX = 30;

int current_delay = 0;
int current_speed = 5;
int prev_speed = -1;

// values for current_mode
int mode_record = 0;
int mode_play   = 1;
int mode_plasma = 2;
int mode_snow   = 3;
int mode_meteor = 4;
int mode_music  = 5; // music sync
int mode_cnt = 6;
int current_mode = mode_record;
boolean wait_frame_queue_empty = false;

boolean scanForDevice = true;

boolean record  = true;
boolean color_mode  = false;
boolean rain = true;

boolean keyCtrl = false;
boolean keyMac  = false;
boolean keyAlt  = false;

//scl
int colorBtnStart;
int colorBtnEnd;

boolean update = true;
Button[] buttons;

int hide_button_index;

/* +++++++++++++++++++++++++++++ */

public void setup() {
  frame.setIconImage( getToolkit().getImage("sketch.ico") );

//scl  matrix = new Matrix(8, 8);
  matrix = new Matrix(Config.xdim, Config.ydim);
  matrix.current_color.set_color(255,0,0);

  size(780,720);
  smooth();
  noStroke();
  fontA = loadFont("Courier-Bold-32.vlw");
  fontLetter = loadFont("ArialMT-20.vlw");
  frameRate(15);
}

public void setup_buttons() {
  buttons = new Button[60]; // width + height + ???
  int offset = 10;
  int button_index = 0;
  int y_pos = 0;

int button_color = 0xff333333;
int button_color_over = 0xff999999;
  int button_size = 15;

  for(int i = 0; i < matrix.rows; i++ ) {
    int x = offX + matrix.width() + offset;
    int y = offY + i * matrix.rad + matrix.border / 2;
    buttons[button_index++] = new RectButton( x, y, button_size, matrix.rad - matrix.border, button_color, button_color_over);
  }
  for(int i = 0; i < matrix.cols; i++ ) {
    int x = offX + i * matrix.rad + matrix.border / 2;
    int y = offY + matrix.width() + offset;
    buttons[button_index++] = new RectButton( x, y, matrix.rad - matrix.border, button_size, button_color, button_color_over);
  }
  buttons[button_index++] = new SquareButton( offX + matrix.width() + offset, offY + matrix.width() + offset, button_size, button_color, button_color_over );

  int button_x = offX + matrix.width() + offset + 30;
//  buttons[button_index++] = new ActionToggleButton( "Mode: RECORD",  "Mode: PLAY",    "10",   button_x, y_pos += 30);
  buttons[button_index++] = new ModeButton( "Mode: ",    "10",   button_x, y_pos += 30);
  buttons[button_index++] = new ActionToggleButton( "Device: SLAVE",  "Device: FREE", "a+10", button_x, y_pos += 30);
  if(! (device instanceof StandaloneDevice && device.enabled()) ) buttons[button_index-1].disable();

  buttons[button_index++] = new FrameChooser(offX, offY + matrix.height() + 65, 59, 10);

  hide_button_index = button_index;
  buttons[button_index++] = new TextElement( "Load from:", button_x, y_pos += 30);
  buttons[button_index++] = new ActionButton( "File",    "m+L", button_x,      y_pos += 30, 65, 25);
 //scl buttons[button_index++] = new ActionButton( "Device",  "a+L", button_x + 67, y_pos,       65, 25);
  //  if(! (device instanceof StandaloneDevice && device.enabled()) ) buttons[button_index-1].disable();

  buttons[button_index++] = new TextElement( "Save to:", button_x, y_pos += 30);
  buttons[button_index++] = new ActionButton( "File",    "m+S", button_x,      y_pos += 30, 65, 25);
  buttons[button_index++] = new ActionButton( "Device",  "a+S", button_x + 67, y_pos,       65, 25);
  if(! (device instanceof StandaloneDevice && device.enabled()) ) buttons[button_index-1].disable();

  buttons[button_index++] = new TextElement( "Color:", button_x, y_pos += 40);
  y_pos += 30;
  // scl
  colorBtnStart = button_index;
  int off = 140/8;
  PixelColor pc = new PixelColor(0,0,0);
  int i =0;
  buttons[button_index++] = new MiniColorButton( button_x + i++ * off, y_pos, off, 20, pc );
  pc = new PixelColor(255,0,0);
  buttons[button_index++] = new MiniColorButton( button_x + i++ * off, y_pos, off, 20, pc );
  pc = new PixelColor(0,255,0);
  buttons[button_index++] = new MiniColorButton( button_x + i++ * off, y_pos, off, 20, pc );
  pc = new PixelColor(0,0,255);
  buttons[button_index++] = new MiniColorButton( button_x + i++ * off, y_pos, off, 20, pc );
  pc = new PixelColor(255,255,0);
  buttons[button_index++] = new MiniColorButton( button_x + i++ * off, y_pos, off, 20, pc );
  pc = new PixelColor(0,255,255);
  buttons[button_index++] = new MiniColorButton( button_x + i++ * off, y_pos, off, 20, pc );
  pc = new PixelColor(255,0,255);
  buttons[button_index++] = new MiniColorButton( button_x + i++ * off, y_pos, off, 20, pc );
  pc = new PixelColor(255,255,255);
  buttons[button_index++] = new MiniColorButton( button_x + i++ * off, y_pos, off, 20, pc );
  colorBtnEnd = button_index;


  buttons[button_index++] = new TextElement( "Frame:", button_x, y_pos += 20);  
  buttons[button_index++] = new ActionButton( "Add",    " ", button_x, y_pos += 30);
  buttons[button_index++] = new ActionButton( "Delete", "D", button_x, y_pos += 30);

  buttons[button_index++] = new ActionButton( "^", "c+38", button_x + 47,  y_pos += 50, 40, 25);
  buttons[button_index++] = new ActionButton( "<", "c+37", button_x,       y_pos += 20, 40, 25);
  buttons[button_index++] = new ActionButton( ">", "c+39", button_x + 94,  y_pos,       40, 25);
  buttons[button_index++] = new ActionButton( "v", "c+40", button_x + 47,  y_pos += 15, 40, 25);

  buttons[button_index++] = new ActionButton( "Paste",  "m+V", button_x, y_pos += 50);
  buttons[button_index++] = new ActionButton( "Copy",   "m+C", button_x, y_pos += 30);
  buttons[button_index++] = new ActionButton( "Fill",   "F",   button_x, y_pos += 30);
  buttons[button_index++] = new ActionButton( "Clear",  "X",   button_x, y_pos += 30);

  //buttons[button_index++] = new ActionButton( "Plasma",  "P",   button_x, y_pos += 40);
  //buttons[button_index++] = new ActionButton( "Music Sync",  "M",   button_x, y_pos += 30);
}

/* ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ */

public void draw()
{  

  if (scanForDevice) {
    // can't scan for device in setup() because delay() doesn't work there
    device = new ColorduinoDevice(this); //scl
    device.setColorScheme();
    if(device.enabled()) {
        ((ColorduinoDevice) device).setModePlayFrame();
    }

    setup_buttons();
    scanForDevice = false;
  }

  if(update) {
    background(45);
    fill(50);
    rect(offX - matrix.border / 2, offY - matrix.border / 2, matrix.width() + matrix.border, matrix.height() + matrix.border);
    image(matrix.current_frame_image(), offX, offY);
    
    for(int i = 0; i < buttons.length; i++ ) {
      if(buttons[i] == null) break;
      buttons[i].display();
    }

    fill(255); //white
    if(!record) {
      text("Speed: " + current_speed, offX + matrix.width() + 65, 110);
    }

    if(!device.enabled()) {
      text("No output device found, running in standalone mode", 120, 20);  
    }

    device.write_frame(matrix.current_frame());
    update = false;
  }

  if(!record && (current_mode != mode_plasma)) next_frame();
}


/* +++++++++++++++++++++++++++++ */

public void next_frame() {
    if (wait_frame_queue_empty) {
	// Colorduino queue is full.  It will not return a status value until
	// the queue has space.
	if (((ColorduinoDevice) device).waitForResponse() != -1) { // not timeout
	    // got a response
	    wait_frame_queue_empty = false;
	}
    }
    else {
	if(current_delay < 1) {
	    current_delay = current_speed;
	    if (current_mode == mode_snow) {
		matrix.snow();
	    }
	    else if (current_mode == mode_meteor) {
		matrix.meteor();
	    }
	    else {
		matrix.next_frame();
	    }
	    mark_for_update();
	}
	current_delay--;
    }
}

/* +++++++++++++++ ACTIONS +++++++++++++++ */
public void mouseDragged() {
  if(!record) return; // ! record
  if(matrix.click(mouseX - offX, mouseY - offY, true)) mark_for_update();
}

public void mousePressed() {
  if(!record) return; //!record
  if(matrix.click(mouseX - offX, mouseY - offY, false)) mark_for_update();
}

public void mouseMoved() {
  for(int i = 0; i < buttons.length; i++ ) {
    if(buttons[i] == null) break;
    if(buttons[i].over()) mark_for_update();
  }
}

public void mouseClicked() {
  for(int i = 0; i < buttons.length; i++ ) {
    if(buttons[i] == null) break;
    if(buttons[i].clicked()) mark_for_update();
  }
}


public void keyPressed() {
 // print(keyCode);
  if(keyCode == 17)  keyCtrl = true; //control
  if(keyCode == 18)  keyAlt  = true; //alt
  if(keyCode == 157) keyMac  = true; //mac
  if(keyCode == 67) color_mode = true; //C
  
  //println("pressed " + key + " " + keyCode + " " +keyMac+ " "+  keyCtrl + " "+ keyAlt );

  if(color_mode) {
 //notyet    if(keyCode == 37) matrix.current_color.previous_color(); //arrow left
 //notyet    if(keyCode == 39) matrix.current_color.next_color();  //arrow right   
     mark_for_update();
     return;
  }

  for(int i = 0; i < buttons.length; i++ ) {
    if(buttons[i] == null) break;
    if(buttons[i].key_pressed(keyCode, keyMac, keyCtrl, keyAlt)) {
      mark_for_update();  
      return;
    }
  }
   
  if(keyAlt) {
    if(device instanceof StandaloneDevice) {
      if(keyCtrl) {
        if(keyCode == 37) ((StandaloneDevice) device).brightnessDown();   //arrow left
        if(keyCode == 39) ((StandaloneDevice) device).brightnessUp(); //arrow right        
      }
      else {        
        if(keyCode == 37) ((StandaloneDevice) device).speedUp();   //arrow left
        if(keyCode == 39) ((StandaloneDevice) device).speedDown(); //arrow right        
      }
    }
  }
  else if(keyCtrl) {
    PixelColor pc = null;
    if(keyCode >= 48) pc = matrix.current_frame().set_letter(PApplet.parseChar(keyCode), fontLetter, matrix.current_color);
    if( pc != null )  {
      matrix.current_color = pc;
      mark_for_update(); 
    }
    return;
  }
  else {
    if(!record && (current_mode != mode_music)) {
      if( keyCode == 40) speed_up();     //arrow up
      if( keyCode == 38) speed_down();   //arrow down
    }
  }
}

public void keyReleased() {
  if( keyCode == 17 )  keyCtrl = false;
  if( keyCode == 18 )  keyAlt  = false;
  if( keyCode == 157 ) keyMac  = false;
  if( keyCode == 67 ) color_mode = false;
}

public void mark_for_update() {
  update = true;
}

/* +++++++++++++++ modes +++++++++++++++ */
public void toggle_mode() {
  matrix.current_frame_nr = 0;
  record = !record;
  for(int i = hide_button_index; i < buttons.length; i++ ) {
    if(buttons[i] == null) break;
    if(record) buttons[i].toggle(); 
    else buttons[i].hide();
  }
  if(record) buttons[hide_button_index-1].enable(); 
  else buttons[hide_button_index-1].disable();
}

public void set_mode(int mode) {
  wait_frame_queue_empty = false;

  if (((current_mode == mode_snow) && (mode != mode_snow)) ||
      ((current_mode == mode_meteor) && (mode != mode_meteor))) {
      matrix.delete_first_frame();
  }

  current_mode = mode;
  if (mode == mode_record) {
    record = true;
  }
  else {
    record = false;
  }

  if ((mode == mode_snow) || (mode == mode_meteor)) {
      matrix.insert_first_frame();
  }

  matrix.current_frame_nr = 0;

  for(int i = hide_button_index; i < buttons.length; i++ ) {
    if(buttons[i] == null) break;
    if(mode == 0) { // record
      buttons[i].enable();
       buttons[i].show();
     }
    else buttons[i].hide();
  }
  if(mode == mode_record) buttons[hide_button_index-1].enable();  // record
  else buttons[hide_button_index-1].disable();

  if (prev_speed != -1) {
      current_speed = prev_speed;
      prev_speed = -1;
  }

  
  if (device.enabled())  { 
    if(mode == mode_plasma) { // plasma
      ((ColorduinoDevice) device).setModePlasma();
    }
    else if (mode == mode_music) {
	prev_speed = current_speed;
	current_speed = -1;
      ((ColorduinoDevice) device).setModeMusicSync();
    }
    else {
      ((ColorduinoDevice) device).setModePlayFrame();
    }
  }

  if ((mode == mode_snow) || (mode == mode_meteor)) {
      prev_speed = current_speed;
      current_speed = 1;
  }
}

public void speed_up() {
  if( current_speed <= 0) return;
  current_speed--;
}

public void speed_down() {
  current_speed++;
}







/* Button Class Taken from Processiong.org Tutorials:
 -> http://processing.org/learning/topics/buttons.html

 And modified for lock parameter, added renamed rect to square button and added real rectbutton
  code cleanup
 */

//scl
 



class Button extends GuiElement
{
  public int highlightcolor;
  boolean over = false;
  //  boolean clicked = false;
  String shortcut = null;
  
  Button(int ix, int iy,  int icolor, int ihighlight) {
    super(ix, iy, icolor);
    this.highlightcolor = ihighlight;
  }

  public boolean clicked() {
    return (this.over && !this.disabled);  
  }

  public boolean key_pressed(int key_code, boolean mac, boolean crtl, boolean alt) {
    return (this.shortcut != null &&  !this.disabled);
  }
  
  public boolean over() {
    return false;
  }

  /* ************************************************************************** */
  protected int current_color() {
    return (this.over && !this.disabled) ? highlightcolor : basecolor;
  }

  protected boolean overRect(int x, int y, int width, int height) {
    return (mouseX >= x && mouseX <= x+width && mouseY >= y && mouseY <= y+height);
  }

  protected boolean overCircle(int x, int y, int diameter) {
    float disX = x - mouseX;
    float disY = y - mouseY;
    return (sqrt(sq(disX) + sq(disY)) < diameter/2 );
  }
}

class CircleButton extends Button {
  int size;

  CircleButton(int ix, int iy, int isize, int icolor, int ihighlight) {
    super( ix, iy, icolor, ihighlight);
    this.size = isize;
  }

  public boolean over() {
    boolean old_over = this.over;
    this.over = overCircle(x, y, size);
    return this.over != old_over;
  }

  public boolean display()  {
    if( !super.display() ) return false;
    ellipse(x, y, size, size);
    return true;
  }
}

class RectButton extends Button {
  int width, height;

  RectButton(int ix, int iy, int iwidth, int iheight, int icolor) {
    this( ix, iy, iwidth, iheight, icolor, icolor);
  }

  RectButton(int ix, int iy, int iwidth, int iheight, int icolor, int ihighlight) {
    super( ix, iy, icolor, ihighlight);
    this.width = iwidth;
    this.height = iheight;
  }

  public boolean over() {
    boolean old_over =  this.over;
    this.over = overRect(x, y, width, height);
    return this.over != old_over;
  }

  public boolean display() {
    if( !super.display() ) return false;
    rect(x, y, width, height);
    return true;
  }
}

class SquareButton extends RectButton {

  SquareButton(int ix, int iy, int isize, int icolor, int ihighlight)  {
    super(ix, iy, isize, isize, icolor, ihighlight);
  }
}

class TextButton extends RectButton {
  String button_text;
  public int text_color;
  float x_offset;
  float y_offset;
  
  TextButton(String itext, int ix, int iy, int iwidth, int iheight, int icolor, int ihighlight) {
    super(ix, iy, iwidth, iheight, icolor, ihighlight);
    this.button_text = itext;
  }

  public boolean display() {
    if( !super.display() ) return false;
    textFont(fontA, 15);
    fill(current_text_color());
    update_offset();
    text(this.current_text(), x_offset, y_offset);
    return true;
  }

  protected int current_text_color() {
    return (this.disabled) ? 0xff666666 : 0xffFFFFFF;
  }
  
  protected String current_text() {
    return this.button_text;
  }
  
  protected void update_offset() {
    x_offset = x + (this.width - textWidth(current_text())) / 2;
    y_offset = y + 17;
  }
  
}

class ActionButton extends TextButton {

  ActionButton(String itext, String ishortcut, int ix, int iy) {
    this(itext, ishortcut, ix, iy, 134, 25, 0xff444444, 0xff999999);
  }

  ActionButton(String itext, String ishortcut, int ix, int iy,  int iwidth, int iheight) {
    this(itext, ishortcut, ix, iy, iwidth, iheight, 0xff444444, 0xff999999);
  }

  ActionButton(String itext, String ishortcut, int ix, int iy, int iwidth, int iheight, int icolor, int ihighlight) {
    super(itext, ix, iy, iwidth, iheight, icolor, ihighlight);
    this.shortcut = ishortcut;
  }

  public boolean clicked() {
    if(!super.clicked()) return false;
    perform_action();
    return true;
  }

  public boolean key_pressed(int key_code, boolean mac, boolean crtl, boolean alt) {
    if(!super.key_pressed(key_code, mac, crtl, alt)) return false;
    String code = "";
    if(mac)  code = "m+" + code;
    if(crtl) code = "c+" + code;
    if(alt) code = "a+" + code;
    if(!this.shortcut.equals(code+PApplet.parseChar(key_code)) && !this.shortcut.equals(code+key_code)) return false; //no shortcut defined
    perform_action();
    return true;
  }

  protected void perform_action() {
    if(this.button_text == "^") matrix.current_frame().shift_up();
    if(this.button_text == "v") matrix.current_frame().shift_down();
    if(this.button_text == "<") matrix.current_frame().shift_left();
    if(this.button_text == ">") matrix.current_frame().shift_right();
    if(this.shortcut    == "m+L") { matrix = matrix.load_from_file(); keyMac  = false;}
    if(this.shortcut    == "m+S") { matrix.save_to_file(); keyMac  = false;}
    if(this.shortcut    == "a+L" && device instanceof StandaloneDevice) matrix = ((StandaloneDevice) device).read_matrix();
    if(this.shortcut    == "a+S" && device instanceof StandaloneDevice) ((StandaloneDevice) device).write_matrix(matrix);
    if(this.button_text == "Add")    matrix.add_frame();
    if(this.button_text == "Delete") matrix.delete_frame();
    if(this.button_text == "Copy")   matrix.copy_frame();
    if(this.button_text == "Paste")  matrix.paste_frame();
    if(this.button_text == "Fill")   matrix.current_frame().fill(matrix.current_color);
    if(this.button_text == "Clear")  matrix.current_frame().clear();

    if(this.button_text == "Plasma") {
      if (device instanceof StandaloneDevice)  { 
	//locked = !locked;
      ((ColorduinoDevice) device).setModePlasma();
      }
    }
    if(this.button_text == "Fill") {
      if (device instanceof StandaloneDevice)  { 
	//locked = !locked;
      ((ColorduinoDevice) device).setModeFill();
      }
    }
  }
}

class ActionToggleButton extends ActionButton {
  String button_text2;
  boolean locked = false;

  ActionToggleButton(String itext, String itext2, String ishortcut, int ix, int iy)  {
    this(itext, itext2, ishortcut, ix, iy, 134, 25, 0xff444444, 0xff999999);
  }

  ActionToggleButton(String itext, String itext2, String ishortcut, int ix, int iy, int iwidth, int iheight, int icolor, int ihighlight)  {
    super(itext, ishortcut, ix, iy, iwidth, iheight, icolor, ihighlight);
    this.button_text2 = itext2;
  }

  protected String current_text() {
    return this.locked ?  this.button_text2 : this.button_text;
  }

  protected void perform_action() {
     if(this.shortcut == "10") { locked = !locked; toggle_mode(); } // ENTER
     if(this.shortcut == "a+10" && device instanceof StandaloneDevice)  { locked = !locked; ((StandaloneDevice) device).toggle(); } // ENTER
  }
}


class ModeButton extends ActionButton {
  String[] button_texts;
  int mode_idx;

  ModeButton(String itext, String ishortcut, int ix, int iy)  {
    this(itext, ishortcut, ix, iy, 134, 25, 0xff444444, 0xff999999);
  }

  ModeButton(String itext, String ishortcut, int ix, int iy, int iwidth, int iheight, int icolor, int ihighlight)  {
    super(itext, ishortcut, ix, iy, iwidth, iheight, icolor, ihighlight);
    mode_idx = 0;
    button_texts = new String[mode_cnt];
    button_texts[0] = "Record"; 
    button_texts[1] = "Play"; 
    button_texts[2] = "Plasma"; 
    button_texts[3] = "Snow"; 
    button_texts[4] = "Meteor"; 
    button_texts[5] = "Music"; 
  }

  protected String current_text() {
    return button_text + button_texts[mode_idx];
  }

  protected void perform_action() {
    if (++mode_idx >= mode_cnt) {
      mode_idx=0;
    }
/*    if ((mode_idx > 1) && !device.enabled())  { 
      mode_idx = 0;
    }
    */
   // print("modeidx");print(mode_idx);
    set_mode(mode_idx);
  }
}

class ColorButton extends RectButton {

  ColorButton(int ix, int iy) {
    this(ix, iy, 134, 25);
  }

  ColorButton(int ix, int iy, int iwidth, int iheight) {
    super(ix, iy, iwidth, iheight, matrix.current_color.get_color(), matrix.current_color.get_color());
  }

  protected int current_color() {
    return matrix.current_color.get_color();
  }

  public boolean pressed() {
    if(!this.over) return false;
    return true;
  }
}

class MiniColorButton extends RectButton {

  PixelColor px;
  
  MiniColorButton(int ix, int iy, int iwidth, int iheight, PixelColor icolor) {
    super(ix, iy, iwidth, iheight, icolor.get_color(), icolor.get_color());
    this.px = icolor;    
  }

  public boolean clicked() {
    if(!super.clicked()) return false;
    if (keyCtrl) {
      choose_color();
      keyCtrl = false;
    }
    else {
      matrix.current_color.set_color(this.px);
    }
    return true;
  }

//scl
  public void choose_color() {
    //n.b. passing null instead of frame to ColorPicker/JColorChooser because if we pass in frame we get deadlock periodically
    Color oldjcolor = new Color(matrix.current_color.r,matrix.current_color.g,matrix.current_color.b);
//    Color jColor = ColorPicker.showDialog(frame,oldjcolor);
    Color jColor = ColorPicker.showDialog(null,oldjcolor);
 //   Color jColor  = JColorChooser.showDialog(null,"Choose Color",oldjcolor); 
    if (jColor != null) {
      int c = color(jColor.getRed(),jColor.getGreen(),jColor.getBlue());
      this.px.set_color(c);
      this.basecolor = c;
      this.highlightcolor = c;
      matrix.current_color.set_color(c);
    }
  }

  
  public boolean display() {
    if( !super.display() ) return false;
    if(matrix.current_color.equal(this.px)) {
      stroke(0xffFFFF00);
      strokeWeight(2);
      rect(this.x-1, this.y-1, this.width-1, this.height+2);
      strokeWeight(0);
    }
    return true;
  }  
}


class ColorduinoDevice implements Device, StandaloneDevice {

	PApplet app;
        byte curError = 0;

	int baud = 57600;

	Serial port;
	String port_name;

 private byte SYNC_BYTE = (byte)0xaa;
 private byte OPC_PING = (byte)0x01;
 private byte OPC_PLAY_FRAME = (byte)0x02;
 private byte OPC_QUEUE_FRAME = (byte)0x03;
 private byte OPC_SET_MODE = (byte)0x04;
 private byte OPC_FILL = (byte)0x05;
 
  byte  CMODE_PLASMA = 1;
  byte  CMODE_PLAY_FRAME = 2;
  byte  CMODE_MUSIC_SYNC = 3;
  byte  CMODE_FILL = 4;


	// this gamma table is for a  a color resolution of 4096 colors (12bit)
	// taken from RainbowduinoHelper by neophob.com part of neorainbowduino
	private int[] gammaTab = {       
		0,      0,      0,      0,      0,      0,      0,      0,
		0,      0,      0,      0,      0,      0,      0,      0,
		0,      0,      0,      0,      0,      0,      0,      0,
		0,      0,      0,      0,      0,      0,      0,      0,
		0,      0,      0,      0,      0,      0,      0,      0,
		0,      0,      0,      0,      16,     16,     16,     16,
		16,     16,     16,     16,     16,     16,     16,     16, 
		16,     16,     16,     16,     16,     16,     16,     16, 
		16,     16,     16,     16,     16,     16,     16,     16,
		16,     16,     16,     16,     16,     16,     16,     16,
		32,     32,     32,     32,     32,     32,     32,     32, 
		32,     32,     32,     32,     32,     32,     32,     32, 
		32,     32,     32,     32,     32,     32,     32,     32, 
		32,     32,     32,     32,     32,     32,     32,     32, 
		32,     32,     32,     32,     48,     48,     48,     48, 
		48,     48,     48,     48,     48,     48,     48,     48, 
		48,     48,     48,     48,     48,     48,     48,     48, 
		48,     48,     48,     48,     64,     64,     64,     64, 
		64,     64,     64,     64,     64,     64,     64,     64, 
		64,     64,     64,     64,     64,     64,     64,     64, 
		64,     64,     64,     64,     64,     64,     64,     64, 
		80,     80,     80,     80,     80,     80,     80,     80, 
		80,     80,     80,     80,     80,     80,     80,     80, 
		96,     96,     96,     96,     96,     96,     96,     96, 
		96,     96,     96,     96,     96,     96,     96,     96, 
		112,    112,    112,    112,    112,    112,    112,    112, 
		128,    128,    128,    128,    128,    128,    128,    128, 
		144,    144,    144,    144,    144,    144,    144,    144, 
		160,    160,    160,    160,    160,    160,    160,    160, 
		176,    176,    176,    176,    176,    176,    176,    176, 
		192,    192,    192,    192,    192,    192,    192,    192, 
		208,    208,    208,    208,    224,    224,    224,    224, 
		240,    240,    240,    240,    240,    255,    255,    255 
	};


public boolean running;

  int bright = 4;

	public boolean connected() {
  return (port != null);
}

  // return -1 if timed out
  // wait for response code
  public int waitForResponse()
  {
    int cnt = 100; // max wait time in ms
    while (port.available() < 1) {
      delay(1);
      if (cnt-- <= 0) {
        //print("tmo");
        return -1; // timeout
      }
    }
    curError = (byte)port.read();
    return curError; // return error code
  }
  
// ping the colorduino
// return = -1 = no answer
//        else current error code
public int ping() {
    byte pkt[] = new byte[4];
    pkt[0] = SYNC_BYTE;
    pkt[1] = OPC_PING;
    pkt[2] = 0; // data len
    pkt[3] = (byte)(OPC_PING); // chksum
    port.clear();
    port.write(pkt);
    int rc = waitForResponse();
  //  print("pingrc");print(rc);
    return rc;
}
    /**
     * Open serial port with given name. Send ping to check if port is working.
     * If not port is closed and set back to null
     * 
     * @param port_name port to open
     * @param check whether to perform valid checks
     * @return whether port could be opened sucessfully 
     */
    private boolean openPort(String _port_name, boolean check) {
      //      port_name = _port_name;
      //try to find the port
      String[] ports = Serial.list();
      for (int i=0; port==null && i<ports.length; i++) {
	try {
	  port_name = ports[i];
	  port = new Serial(app, port_name, this.baud);

	  port.buffer(1);
	  delay(250); //give it time to initialize		
// n.b. if running w/o bootloader (i.e. using external programmer, you can use short delay below
//	  delay(100); //give it time to initialize		
	  if (ping() == -1) {
	    // no answer
    	    port.stop();
	    port = null;
	  }
	  else {
	    // found one
            print("Found a Colorduino on: ");
            println(port_name);
	    return true;
	  }
	  
	  //catch all, there are multiple exception to catch (NoSerialPortFoundException, PortInUseException...)
	} catch (Exception e) {
	  // search next port...
	}
      }
      
      if (port==null) {
	println("No Colorduino found!");
      }
      return false;
    }


    public void initPort(String _port_name, int _baud, boolean check) {
	if(_baud > 0) this.baud = _baud;
        openPort(_port_name, check);
    }


  ColorduinoDevice(PApplet app) {
    this(app, null, 0);
  }

  ColorduinoDevice(PApplet app, String port_name) {
    this(app, port_name, 0);
  }

  ColorduinoDevice(PApplet app, int baud_rate) {
    this(app, null, baud_rate);
  }

  ColorduinoDevice(PApplet _app, String port_name, int baud_rate) {
  this.app = _app;
    this.initPort(port_name, baud_rate, true);
    //colorduino.reset();
    running = false;
  }

  public void setColorScheme() {
 //notyet
/*   PixelColorScheme.R = new int[]{
      0,255        };   
    PixelColorScheme.G = new int[]{
      0,255        };   
    PixelColorScheme.B = new int[]{
      0,255        };   
      */
  }

  public boolean draw_as_circle() {
//scl    return true;
	return false;
  }

  public boolean enabled() {
    return connected();
  }    

    private void send(int value) {
	if(!connected()) return;
	port.write((byte)value);
    }

  /* +++++++++++++++++++++++++++ */
  public void write_frame(Frame frame) {    
    write_frame(0, frame);
  }

public int setModePlasma() {
  return set_mode(CMODE_PLASMA);
}

public int setModeMusicSync() {
  return set_mode(CMODE_MUSIC_SYNC);
}

public int setModePlayFrame() {
  return set_mode(CMODE_PLAY_FRAME);
}

public int setModeFill() {
  return set_mode(CMODE_FILL);
}
  
  public int set_mode(byte mode) {
    byte pkt[] = new byte[5];
    pkt[0] = SYNC_BYTE;
    pkt[1] = OPC_SET_MODE;
    pkt[2] = 1; // data len
    pkt[3] = mode;
    pkt[4] = (byte)(pkt[1]+pkt[2]+pkt[3]); // chksum
    port.clear();
    port.write(pkt);
    int rc = waitForResponse();
    return rc;
  }

//set rotate to 1 if your matrix is rotated 90deg CW  
int rotate = 1;
  public void write_frame(int num, Frame frame) {  
    if(frame == null || running || !enabled() ) return;
    
    byte pkt[] = new byte[100];
    int idx = 0;
    pkt[idx++] = SYNC_BYTE; // sync
    pkt[idx++] = (current_mode == mode_music) ? OPC_QUEUE_FRAME : OPC_PLAY_FRAME; // frame opcode
    pkt[idx++] = 96; // datalen
    byte chksum = (byte)(pkt[1]+pkt[2]);
    if (rotate == 1) {
//    for (int y=frame.rows-1;y >=0; y--) {
   for (int y=0;y < frame.rows;y++) {
	for (int x=0;x < frame.cols;x++) {
	    PixelColor p = frame.get_pixel(y,x);
	    int r4,g4,b4;
	    int b;

	    // cvt to 4 bit
	 //   r4 = (gammaTab[p.r] >> 4) & 0x0f;
	//    g4 = (gammaTab[p.g] >> 4) & 0x0f;
	 //   b4 = (gammaTab[p.b] >> 4) & 0x0f;
	    r4 = (p.r >> 4) & 0x0f;
	    g4 = (p.g >> 4) & 0x0f;
	    b4 = (p.b >> 4) & 0x0f;

	    b = (r4 << 4) | g4;
	    chksum += b;
	    pkt[idx++] = (byte)b;
	    b = b4 << 4;

	    p = frame.get_pixel(y,++x);
	    // cvt to 4 bit
	    r4 = (gammaTab[p.r] >> 4) & 0x0f;
	    g4 = (gammaTab[p.g] >> 4) & 0x0f;
	    b4 = (gammaTab[p.b] >> 4) & 0x0f;


	    b |= r4;
	    chksum += b;
	    pkt[idx++] = (byte)b;
	    b = (g4 << 4) | b4;
	    chksum += b;
	    pkt[idx++] = (byte)b;
}
    }
    }
    else {
    // n.b. colorduino origin is bottom left, our origin is upper left,
    // so loop backwards on the y
    for (int y=frame.rows-1;y >=0; y--) {
	for (int x=0;x < frame.cols;x++) {
	    PixelColor p = frame.get_pixel(x,y);
	    int r4,g4,b4;
	    int b;

	    // cvt to 4 bit
	 //   r4 = (gammaTab[p.r] >> 4) & 0x0f;
	//    g4 = (gammaTab[p.g] >> 4) & 0x0f;
	 //   b4 = (gammaTab[p.b] >> 4) & 0x0f;
	    r4 = (p.r >> 4) & 0x0f;
	    g4 = (p.g >> 4) & 0x0f;
	    b4 = (p.b >> 4) & 0x0f;

	    b = (r4 << 4) | g4;
	    chksum += b;
	    pkt[idx++] = (byte)b;
	    b = b4 << 4;

	    p = frame.get_pixel(++x,y);
	    // cvt to 4 bit
	    r4 = (gammaTab[p.r] >> 4) & 0x0f;
	    g4 = (gammaTab[p.g] >> 4) & 0x0f;
	    b4 = (gammaTab[p.b] >> 4) & 0x0f;


	    b |= r4;
	    chksum += b;
	    pkt[idx++] = (byte)b;
	    b = (g4 << 4) | b4;
	    chksum += b;
	    pkt[idx++] = (byte)b;

/*
	    int b;
	    b = (p.r & 0xf0)|((p.g >> 4) & 0x0f);
	    chksum += b;
	    pkt[idx++] = (byte)b;
	    b = (p.b & 0xf0);
	    p = frame.get_pixel(++x,y);

	    b |= ((p.r >> 4) & 0x0f);
	    chksum += b;
	    pkt[idx++] = (byte)b;
	    b = (p.g & 0xf0)|((p.b >> 4) & 0x0f);
	    chksum += b;
	    pkt[idx++] = (byte)b;
*/
}
	}
    }

    pkt[idx] = chksum;
    port.clear(); // empty the input buffer
    port.write(pkt);
    
    int rc = waitForResponse();
    if ((current_mode == mode_music) && (rc == -1)) {
	// timed out means frame queue is full.
	// tell draw() not to send the next frame until we
	// get a response
	wait_frame_queue_empty = true;
    }
  
   // print("frc");print(rc);
  }

  public void write_matrix(Matrix matrix) {
    print("Start Writing Matrix - ");
    for(int f = 0; f < matrix.num_frames(); f++) {
      write_frame(f, matrix.frame(f));
    }
    println("Done");
  }

  public Matrix read_matrix() {
    Matrix matrix = new Matrix(8,8);
    /*
    print("Start Reading Matrix - ");    
    int frames =  colorduino.bufferLoad(); //return num length
    println( "Frames:" + frames);

    for( int frame_nr = 0; frame_nr < frames; frame_nr++ ) {    
      //println("Frame Nr: " + frame_nr);
      Frame frame = matrix.current_frame();
      int frame_byte[] = colorduino.bufferGetAt(frame_nr);
      for(int y = 0; y < 8; y++ ) {
        for(int x = 0; x < 8; x++ ) {
          frame.set_pixel(x,y, new PixelColor((frame_byte[3*y+0] >> x) & 1, (frame_byte[3*y+1] >> x) & 1, (frame_byte[3*y+2] >> x) & 1 ) );          
        }
      }      
      matrix.add_frame();
    }           
    matrix.delete_frame();
    println("Done");
    */
    return matrix;
  }

  public void toggle() {
    if(running) {
      running = false;
      //      colorduino.reset();
      return;
    } 
    running = true;
    //    colorduino.bufferLoad();
    //    colorduino.start();   
  }

  public void speedUp() {
      //    colorduino.speedUp();
  }

  public void speedDown() {
      //    colorduino.speedDown();
  }

  public void brightnessUp() {
      /*
    this.bright++;
    colorduino.brightnessSet(this.bright);
      */
  }

  public void brightnessDown() {
      /*
    this.bright--;
    if(this.bright < 1) this.bright = 1;
    colorduino.brightnessSet(this.bright);
      */
  }
}









public interface Device {

  public void setColorScheme();
  public boolean draw_as_circle();
    
  public boolean enabled();
  public void write_frame(Frame frame);
}

public interface StandaloneDevice {

  public Matrix read_matrix();  
  public void write_matrix(Matrix matrix);
  public void toggle();       
  
  public void speedUp();
  public void speedDown(); 
  
  public void brightnessUp();
  public void brightnessDown();
}
class Frame {

  //static
  PGraphics pg = null;
  int letter_scale = 3;
  PixelColor[] pixs  = null;

  PGraphics frame = null;
  PGraphics thumb = null;

  public int rows = 0;
  public int cols = 0;

  int last_y = 0;
  int last_x = 0;

  Frame(int cols, int rows) {
    this.cols = cols;
    this.rows = rows;
    this.pixs = new PixelColor[rows*cols];
    this.clear();
    this.pg = createGraphics(8*letter_scale, 10*letter_scale, P2D);
  }

  public PGraphics draw_full(int draw_rad, int draw_border) {
    if(this.frame == null) this.frame = draw_canvas(draw_rad, draw_border);
    return this.frame;
  }

  public PGraphics draw_thumb(int draw_rad, int draw_border) {
    if(this.thumb == null) this.thumb = draw_canvas(draw_rad, draw_border);
    return this.thumb;
  }

  public PGraphics draw_canvas(int draw_rad, int draw_border) {
    PGraphics canvas = createGraphics(this.cols * draw_rad, this.rows * draw_rad, P2D);
    canvas.beginDraw();
    canvas.background(55);
    canvas.smooth();
    canvas.noStroke();
    canvas.rectMode(CENTER);
    for(int y = 0; y < this.rows; y++) {
      for(int x = 0; x < this.cols; x++) {
        canvas.fill(this.get_pixel(x,y).get_color());
        if(device.draw_as_circle()) {
          canvas.ellipse( draw_rad * (x + 0.5f), draw_rad * (y + 0.5f), draw_rad-border, draw_rad-border);
        }
        else {  
          canvas.rect( draw_rad * (x + 0.5f), draw_rad * (y + 0.5f), draw_rad-border, draw_rad-border);
        }  
      }
    }
  //  println( "drawn" );
    canvas.endDraw();
    return canvas;
  }

  public void clear() {
    this.set_pixels(new PixelColor());
  }

  public void fill(PixelColor pc) {
    this.set_pixels(pc);
  }
    
  public void set_pixels(PixelColor pc) {
    for( int y = 0; y < this.rows; y++ ) {
      for( int x = 0; x < this.cols; x++ ) {
        set_pixel(x, y, pc);
      }
    }
  }

  public void set_pixels(PixelColor[] pix) {
    for( int y = 0; y < this.rows; y++ ) {
      for( int x = 0; x < this.cols; x++ ) {
        set_pixel(x, y, pix[pos(x,y)]);
      }
    }
  }

  public PixelColor get_pixel(int x, int y) {
    if(x < 0 || y < 0 || x > width || y > height ) return null;
    return pixs[pos(x,y)];
  }

  public PixelColor set_row(int y, PixelColor pc) {
    for( int x = 0; x < this.cols; x++ ) {
      pc = set_colored_pixel(x, y, pc);
    }
    return pc;
  }

  public PixelColor set_col(int x, PixelColor pc) {
    for( int y = 0; y < this.rows; y++ ) {
      pc = set_colored_pixel(x, y, pc);
    }
    return pc;
  }

  public PixelColor set_colored_pixel(int x, int y, PixelColor pc) {
    if( x >= cols ) return set_row( y, pc);
    if( y >= rows)  return set_col( x, pc);
    if( this.get_pixel(x,y).equal(pc) ) {
      this.frame = null;
      this.thumb = null;
//scl      return this.get_pixel(x,y).next_color();
      return this.get_pixel(x,y);
    }
    return set_pixel(x, y, pc);
  }

  public PixelColor set_pixel(int x, int y, PixelColor pc) {
    if( pc == null ) pc = new PixelColor();
    if(this.get_pixel(x,y) != null ) {
      this.get_pixel(x,y).set_color(pc);
    }
    else {
      pixs[pos(x,y)] = pc.clone();
    }
    this.frame = null;
    this.thumb = null;
    return this.get_pixel(x,y).clone();
  }

  public PixelColor update(int x, int y, PixelColor pc, boolean ignore_last) {
    if(!ignore_last && x == last_x && y == last_y) return null;
    last_x = x;
    last_y = y;
    if(color_mode) return set_pixel(x, y, pc);
    return set_colored_pixel(x, y, pc);    
  }

  public Frame clone() {
    Frame f = new Frame(this.cols, this.rows);
    f.set_pixels(pixs);
    return f;
  }

  public void shift_left() {
    for( int y = 0; y < this.rows; y++ ) {
      for( int x = 0; x < this.cols; x++ ) {
        set_pixel(x, y, ( x < this.cols - 1) ? get_pixel(x+1,y) : null );
      }
    }
  }

  public void shift_right() {
    for( int y = 0; y < this.rows; y++ ) {
      for( int x = this.cols - 1; x >= 0; x-- ) {
        set_pixel(x, y, ( x > 0) ? get_pixel(x-1,y) : null );
      }
    }
  }

  public void shift_up() {
    for( int y = 0; y < this.rows; y++ ) {
      for( int x = 0; x < this.cols; x++ ) {
        set_pixel(x, y, ( y < this.rows - 1) ? get_pixel(x,y+1) : null );
      }
    }
  }

  public void shift_down() {
    for( int y = this.rows - 1; y >= 0; y-- ) {
      for( int x = 0; x < this.cols; x++ ) {
        set_pixel(x, y, ( y > 0) ? get_pixel(x,y-1) : null );
      }
    }
  }

public void meteor() {
  shift_down();
  int x,y;
  for (x=0;x < this.cols;x++) {
      if ((random(20) <= 1) && ((get_pixel(x,0).r == 0) || (get_pixel(x,0).g == 0) || (get_pixel(x,0).b == 0))) {
      get_pixel(x,0).r = 255;
      get_pixel(x,0).g = 255;
      get_pixel(x,0).b = 255;
    }
  }

  for (y=this.rows-1;y > 0;y--) {
    for (x=0;x < this.cols;x++) {

      if (get_pixel(x,y).r > 4){
      get_pixel(x,y-1).r = get_pixel(x,y).r - get_pixel(x,y).r/2;
      get_pixel(x,y-1).g = get_pixel(x,y).g - get_pixel(x,y).g/2;
      get_pixel(x,y-1).b = get_pixel(x,y).b - get_pixel(x,y).b/2;
//      pc.r /= 2;
 //     pc.g  /= 2;
  //    pc.b  /= 2;
      }
    }
  }

}

public void snow() {
  shift_down();
  for (int x=0;x < this.cols;x++) {
    if (random(20) <= 1) {
      set_pixel(x,0,new PixelColor(255,255,255));
    }
  }
}

  private int pos(int x, int y ) {
    return (y * this.cols) + x;
  }

  private PixelColor set_letter(char letter, PFont font, PixelColor pc) {
    return set_letter(letter, font, pc, 50);
  }

  private PixelColor set_letter(char letter, PFont font, PixelColor pc, int trashhold) {
    PixelColor new_pc = null;
    int offset = 0;
    this.pg.beginDraw();
    this.pg.fill(0xffFF0000);  //red
    this.pg.background(0);
    this.pg.textFont(font,10*letter_scale);
    this.pg.text(letter,0,8*letter_scale);
    this.pg.endDraw();
    this.pg.loadPixels();
    for(int row = 0; row < 10; row++) {
      int sum = 0;
      for(int col = 0; col < 8; col++) {
        if( this.get_pixs(row, col) > trashhold) {
          new_pc = this.set_colored_pixel(col, row-offset, pc);          
          sum++;
        }
      }
      if(sum == 0 && offset < 2) offset++;
      if(row-offset == 7) return new_pc; //exit earlier in case we dont have lower parts
    }
    return new_pc;
  }

  private int get_pixs(int x, int y) {
    int trashhold = 0;
    x *= this.letter_scale;
    y *= this.letter_scale;
    for(int k = 0; k < this.letter_scale; k++) {
      for(int k2 = 0; k2 < this.letter_scale; k2++) {
        trashhold += red(this.pg.pixels[(x+k)*8*this.letter_scale+y+k2]);
      }
    }
    return trashhold / (this.letter_scale * this.letter_scale);
  }
}
class FrameChooser extends RectButton {

  int frame_width;
  int show_frames;
  
  FrameChooser(int ix, int iy, int iwidth, int ishow_frames) {
    super(ix, iy, iwidth * ishow_frames, iwidth, 0xff111111, 0xffFFFF00);
    this.frame_width = iwidth;
    this.show_frames = ishow_frames;
    this.enable();
  }

  public boolean display() {
    if(this.hidden) return false;
    int frame_nr;
    for(int nr = 0; nr < this.show_frames; nr++) {
      frame_nr = this.get_frame_nr(nr);      
      display_frame(this.x + this.frame_width * nr, this.y, frame_nr);
    }
    return true;
  }
  
  private void display_frame(int frame_x, int frame_y, int frame_nr) {
      noFill();
      stroke( (frame_nr == matrix.current_frame_nr) ? this.highlightcolor : this.basecolor );      
      rect(frame_x, this.y, this.frame_width - 10, this.frame_width - 10);    
      if( frame_nr < 0 || frame_nr >= matrix.num_frames()) return;
      image(matrix.frame(frame_nr).draw_thumb(6, 4), frame_x + 1, frame_y + 1);
      fill(255); //white
      noStroke();
      textFont(fontA, 15);
      text(frame_nr + 1, frame_x + 20, frame_y + 62);    
  }
   
  public boolean clicked() {
    if(!super.clicked()) return false;
    int frame_nr =  this.get_frame_nr( (mouseX - this.x) / this.frame_width );
    if( frame_nr >= matrix.num_frames()) return false;
    matrix.current_frame_nr = frame_nr;
    return true;      
  }
  
  public boolean key_pressed(int key_code, boolean mac, boolean crtl, boolean alt) {
     if(this.disabled) return false;
     if(mac || crtl || alt) return false;
     if(key_code == 37) matrix.previous_frame(); // arrow left  //use perform_ation here??
     if(key_code == 39) matrix.next_frame();     // arrow right
     return (key_code == 37 || key_code == 39);
  }

  private int get_frame_nr(int nr) {
    return ( matrix.current_frame_nr > (this.show_frames - 1) ) ? (matrix.current_frame_nr - (this.show_frames - 1) + nr) : nr;
  }
}



class GuiElement {
  int x, y;
  int basecolor;
  boolean hidden = false;
  boolean disabled = false;
  private boolean old_disabled = false;
  
  GuiElement(int ix, int iy, int icolor) {
    this.x = ix;
    this.y = iy;
    this.basecolor = icolor;
  }

  public void disable() {
    old_disabled = disabled;    
    disabled = true;
  }
  
  public void enable() {
    old_disabled = disabled;
    disabled = false;
  }

  public void toggle() {
    disabled = old_disabled;
    hidden = false;
  }

  public void hide() {
    hidden = true;
    disable();
  }

  public void show() {
    hidden = false;
    enable();
  }

  public boolean display() {
    if(this.hidden) return false;
    stroke(30);
    fill(current_color());
    return true;
  }
  
  /* ************************************************************************** */
  protected int current_color() {
    return this.basecolor;
  } 

}

class TextElement extends TextButton {
  
  TextElement( String itext, int ix, int iy ) {
    super(itext, ix, iy, 0, 0, 0xff000000, 0xff000000);
    this.disable();
  }

  protected int current_text_color() {
    return 0xffFFFFFF;
  }

  
  protected void update_offset() {
    x_offset = x;
    y_offset = y + 25;
  }      
}

class Matrix {

  ArrayList frames  = new ArrayList();

  public int rad = 70;
  int border = 10;

  public int rows = 0;
  public int cols = 0;
  public PixelColor current_color;

  Frame copy_frame;

  int SCALE = 1;

  int current_frame_nr;

  Matrix(int cols, int rows ) {
    this.cols = cols; //X
    this.rows = rows; //Y
    this.current_color = new PixelColor();
    add_frame();
  }

  public int width() {
    return cols * rad;
  }

  public int height() {
    return rows * rad;
  }

  public PGraphics current_frame_image() {
    return this.current_frame_image(rad, border);
  }

  public PGraphics current_frame_image(int draw_rad, int draw_border) {
    return this.current_frame().draw_full(draw_rad, draw_border);
  }

  public boolean click(int x, int y, boolean dragged) {
    if( x < 0 || y < 0) return false; //25 pixel right and bottom for row editing
    if( x > this.width() + 25 || y > this.height() + 25) return false; //25 pixel right and bottom for row editing
    PixelColor pc = this.current_frame().update(x / rad, y / rad, current_color, !dragged);
    if( pc == null ) return false;
    current_color = pc.clone();
    return true;
  }

  public int num_frames() {
    return frames.size();
  }

  public Frame next_frame() {
    current_frame_nr = (current_frame_nr + 1 ) % num_frames();
    return current_frame();
  }

  public Frame previous_frame() {
    current_frame_nr = ( current_frame_nr == 0 ) ? num_frames() - 1 : current_frame_nr - 1;
    return current_frame();
  }

  public Frame first_frame() {
    current_frame_nr = 0;
    return current_frame();
  }


  public Frame meteor() {
    current_frame().meteor();
    return current_frame();
  }

  public Frame snow() {
    current_frame().snow();
    return current_frame();
  }



  /* +++++++++++++++ DATA STRUCTURE +++++++++++++++ */
  public Frame current_frame() {
    return frame(current_frame_nr);
  }

  public Frame frame(int f) {
    try {
      return (Frame) frames.get(f);
    }
    catch(Exception e ) {
      return (Frame) frames.get(0);
    }
  }

  public void set_pixel(int f, int x, int y, PixelColor pc) {
    frame(f).set_colored_pixel(x, y, pc);
  }

  /* +++++++++++++++ FRAME +++++++++++++++ */
  public Frame copy_frame() {
    copy_frame = current_frame().clone();
    return current_frame();
  }

  /* +++++++++++++++ FRAME +++++++++++++++ */
  public Frame paste_frame() {
    if( copy_frame != null) frames.set(current_frame_nr, copy_frame.clone()); // better use set_pixel here!?
    return current_frame();
  }

  public Frame add_frame() {
    if(!frames.isEmpty()) current_frame_nr++;
    frames.add(current_frame_nr, new Frame(this.cols, this.rows)); //init first frame
    return current_frame();
  }

  public Frame insert_first_frame() {
    current_frame_nr = 0;
    frames.add(current_frame_nr, new Frame(this.cols, this.rows)); //init first frame
    return current_frame();
  }

  public Frame delete_first_frame() {
//    matrix.copy_frame();
    if(this.num_frames() > 0) {
      current_frame_nr = 0;
      frames.remove(current_frame_nr);
    }
    return current_frame();
  }

  public Frame delete_frame() {
    matrix.copy_frame();
    if(this.num_frames() > 1) {
      frames.remove(current_frame_nr);
      current_frame_nr = current_frame_nr % num_frames();
    }
    return current_frame();
  }

  /* +++++++++++++++ FILE +++++++++++++++ */
    //scl
    public void save_bmp(String savePath) {
	int height = (int) Math.sqrt(this.num_frames());
	while( this.num_frames() % height != 0) {
	    height--;
	}
	int width =  this.num_frames() / height;
	
	PImage output = createImage( width * this.cols, height * this.rows, RGB);
	
	for(int h = 0; h < height; h++) {
	    for(int w = 0; w < width; w++) {
		Frame frame = this.frame(h*width + w);
		for(int y = 0; y < frame.rows; y++) {
		    for(int x = 0; x < frame.cols; x++) {
			output.set(x + frame.cols * w, y + frame.rows * h, frame.get_pixel(x,y).get_color() );
		    }
		}
	    }
	}
	output.save(savePath); //TODO add scaling??
    }

    public void save_mtx(String savePath) {
	PrintWriter output = createWriter(savePath);
	output.println(this.num_frames()+","+rows+","+cols+","+current_speed); //scl
	for(int f = 0; f < this.num_frames(); f++) {
	    Frame frame = this.frame(f);
	    for(int y=0; y<frame.rows; y++) {
		//scl
		for (int x=0;x < frame.cols;x++) {
		    PixelColor pc = frame.get_pixel(x,y);
		    output.print(pc.r + "," + pc.g + "," + pc.b + ",");
		}
		//scl
	    }
	    output.println();
	}
	
	output.flush(); // Writes the remaining data to the file
	output.close(); // Finishes the file
    }
    //scl

  public void save_to_file() {
    String savePath = selectOutput();  // Opens file chooser
    if(savePath == null) {
      println("No output file was selected...");
      return;
    }
    //scl
    if (match(savePath,".mtx") != null) {
	save_mtx(savePath);
    }
    else {
	if( match(savePath, ".bmp") == null )  savePath += ".bmp";
	save_bmp(savePath);
    }

    println("SAVED to " + savePath);
  }

  public Matrix load_from_file() {
    String loadPath = selectInput("Choose a Matrix File to load");  // Opens file chooser
    if(loadPath == null) {
      println("No file was selected...");
      return this;
    }
    if( match(loadPath, ".mtx") != null ) return load_mtx(loadPath); 
    return load_bmp(loadPath);
  }

  public Matrix load_mtx(String loadPath) {
//scl    PixelColor pc;
    Matrix matrix = new Matrix(this.cols, this.rows); //actually we have to read values from File!
    Frame frame = matrix.current_frame();
    BufferedReader reader = createReader(loadPath);
    String line = "";
//scl	
    int nframes,nrows,ncols;
    try {
    line = reader.readLine();
    }
          catch (IOException e) {
        e.printStackTrace();
        return matrix;
      }
    String[] str = line.split(",");
    nframes = Integer.parseInt(str[0]);
    nrows = Integer.parseInt(str[1]);
    ncols = Integer.parseInt(str[2]);
    current_speed = Integer.parseInt(str[3]);
    // need to do sanity checking of above
    line = "";
//scl
    while( line != null ) {
      try {
        line = reader.readLine();
        if( line != null && line.length() > 0) {
//scl
          str = line.split(",");
	  int i=0;
          PixelColor pc = new PixelColor();
          for(int y = 0; y < frame.rows; y++) {
	        for (int x = 0;x < frame.cols;x++) {
		      pc.r = Integer.parseInt(str[i++]);
		      pc.g = Integer.parseInt(str[i++]);
		      pc.b = Integer.parseInt(str[i++]);
		      frame.set_pixel(x,y,pc);
	        }
//scl
          }
          frame = matrix.add_frame();
        }
      }
      catch (IOException e) {
        e.printStackTrace();
        return matrix;
      }
    }
    matrix.delete_frame();
    return matrix;
  }

  public Matrix load_bmp(String loadPath) {
    Matrix matrix = new Matrix(this.cols, this.rows);

    PImage input = loadImage(loadPath);
    input.loadPixels();

    int width = input.width / this.cols / SCALE;
    int height = input.height / this.rows / SCALE;
    Frame frame = matrix.current_frame();
    for(int h = 0; h < height; h++) {
      for(int w = 0; w < width; w++) {
        for(int y = 0; y < frame.rows; y++) {
          for(int x = 0; x < frame.cols; x++) {
            int off = h * width * frame.cols * frame.rows + w * frame.cols + y * (frame.cols * width) + x ;
            int c = input.pixels[off * SCALE];          
            frame.get_pixel(x, y).set_color(c);
          } 
        }
        frame = matrix.add_frame();
      }
    }
    matrix.delete_frame();
    return matrix;
  }
}

static class PixelColorScheme {
  public static int[] R = {};
  public static int[] G = {};
  public static int[] B = {};
}

class PixelColor {
  public int r;
  public int g;
  public int b;

  PixelColor() {
    this(0,0,0);
  }

  PixelColor(int r, int g, int b) {
    set_color(r,g,b);
  }

  public PixelColor next_color() {
    this.set_color_index(this.to_int()+1);
    return this;
  }

  public PixelColor previous_color() {
    this.set_color_index(this.to_int()-1);
    return this;
  }
  
  public int numColors() {
      //scl
	if (Config.rgbmode) {
	    return 256*256*256;
        }
	else {
	    return PixelColorScheme.R.length * PixelColorScheme.G.length * PixelColorScheme.B.length;
	}
  }


  public boolean equal(PixelColor pc) {
    if(pc == null) return true;
    return this.r == pc.r && this.g == pc.g && this.b == pc.b;
  }

  public void set_color(PixelColor pc) {
    if(pc == null) return;
    this.r = pc.r;
    this.g = pc.g;
    this.b = pc.b;
  }

  public void set_color(int _r, int _g, int _b) {
    this.r = _r;
    this.g = _g;
    this.b = _b;
  }

  public void set_color_index(int i) {
      if (!Config.rgbmode) {
	  i = i % numColors();
	  if(i < 0) i += numColors();
	  this.r = i / (PixelColorScheme.G.length * PixelColorScheme.B.length);
	  this.g = (i / PixelColorScheme.B.length) - (this.r * PixelColorScheme.G.length) ;
	  this.b = (i - (this.r * PixelColorScheme.G.length + this.g) * PixelColorScheme.B.length);
      }
  }

  public void set_color(int c) {
      //scl
      if (Config.rgbmode) {
	  this.r = PApplet.parseInt(red(c));
	  this.g = PApplet.parseInt(green(c));
	  this.b = PApplet.parseInt(blue(c));
      }
      else {
	  int l_index = PixelColorScheme.R.length - 1;
	  this.r = PApplet.parseInt(red(c)   / PixelColorScheme.R[l_index] * l_index);
	  
	  l_index = PixelColorScheme.G.length - 1;
	  this.g = PApplet.parseInt(green(c) / PixelColorScheme.G[l_index] * l_index);
	  
	  l_index = PixelColorScheme.B.length - 1;
	  this.b = PApplet.parseInt(blue(c)  / PixelColorScheme.B[l_index] * l_index);
      }
  }

  public PixelColor clone() {
    return new PixelColor(r,g,b);
  }

  public int get_color() {
      if (Config.rgbmode) {
	  return color(this.r,this.g,this.b);
      }
      else {
	  return color(PixelColorScheme.R[this.r], PixelColorScheme.G[this.g], PixelColorScheme.B[this.b]);
      }
  } 

  public int to_int() {
    return (this.r*(PixelColorScheme.G.length) + this.g)*(PixelColorScheme.B.length) + b; 
  }
}



  static public void main(String args[]) {
    PApplet.main(new String[] { "--bgcolor=#F0F0F0", "RGBmtx" });
  }
}
