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

import arb.soundcipher.*; 
import arb.soundcipher.constants.*; 

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




SCScore score = new SCScore();

ArrayList<Note>[] notes = (ArrayList<Note>[])new ArrayList[128];

ArrayList<Point> highlightedNote = new ArrayList<Point>();

int curInstrument = 0;

int playStartMil = 0;

public void settings() {
  size(1920, 1024, P2D);
  
  PJOGL.setIcon("MusicComp.png");
}

public void setup() {
  colorMode(HSB);
  frameRate(999);
  
  //PImage titlebaricon = loadImage("MusicComp.png");
  //surface.setIcon(titlebaricon);
  
  frame.setResizable(true);
  surface.setTitle("Music Composer ( Made By Alex Shen )"); 
  
  calculateScales();

  for (int i=0; i<notes.length; i++) {
    notes[i] = new ArrayList<Note>();
  }
}

public void draw() {

  background(0);

  pushMatrix();
  moveCamera();

  int sPitch = max(0, gY(0));
  int ePitch = min(notes.length-1, gY(max(camY,0)+height));
  int sIndex = max(0, gX(0));
  int eIndex = min(gX(max(camX,0)+width), gX(width));

  stroke(50);
  strokeWeight(1);
  textSize(9);
  for (int j=sPitch; j<ePitch; j++) {
    if (isInScale(127-j)) {
      fill(70);
    } else { 
      fill(30);
    }
    rect(0, j*8, gX(max(camX,0)+width)*8, 8);
    fill(255);
    text(notesNames[127-j]+((127-j)/12), -25, j*8+7);
  }

  for (int j=sIndex-sIndex%4; j<eIndex-eIndex%4; j+=4) {
    if (j%16==0) {
      stroke(70);
    } else { 
      stroke(30);
    }
    line(j*8, 0, j*8, gY(max(camY,0)+height)*8);
  }

  stroke(150, 100, 100);
  strokeWeight(1);
  for (int i=sPitch; i<ePitch; i++) {
    for (int j=0; j<notes[i].size(); j++) {
      Note curNote = notes[i].get(j);
      if (curNote.markToBeRemoved) {
        notes[i].remove(j);
        j--;
        continue;
      }
      fill(150, 200, 255);
      rect(curNote.start*8, i*8, 8*curNote.len, 8, 3);
    }
  }
  stroke(250);
  strokeWeight(1);
  for (int i=0; i<highlightedNote.size(); i++) {
    Note curSelNote = notes[highlightedNote.get(i).pitch].get(highlightedNote.get(i).index);
    if (curSelNote.len!=0) {
      noFill();
      rect(curSelNote.start*8, highlightedNote.get(i).pitch*8, 8*curSelNote.len, 8, 3);
    }

    curSelNote.len = constrain(curSelNote.len, 0, 32);

    if (curSelNote.len==0) {
      curSelNote.markToBeRemoved = true;
      highlightedNote.remove(i);
      i--;
      continue;
    }
  }

  if (score.playing) {
    fill(0, 200, 255);
    stroke(0, 100, 255);
    float curPlayX = (millis() - playStartMil)*32.0f/500;
    line(curPlayX, 0, curPlayX, gY(max(camY,0)+height)*8);
  }

  popMatrix();

  updateTransformations();

  if (isSelecting) {
    fill(200, 50);
    stroke(255, 200);
    if(mousePressed) {
      rect(startDrag.x, startDrag.y, endDrag.x-startDrag.x, endDrag.y-startDrag.y);
    } else {
      line(mouseX, 0, mouseX, height);
      line(0, mouseY, width, mouseY);
    }
  } else {
    //line(0, gY(mouseY)*8, width, gY(mouseY)*8);
    //line(0, gY(mouseY)*8+8, width, gY(mouseY)*8+8);
  }
}

public void updateSelection() {
  if(!keyPresses[SHIFT])
    highlightedNote = new ArrayList<Point>();
  int sPitch = max(0, gY(min(endDrag.y, startDrag.y)));
  int ePitch = min(notes.length-1, gY(max(endDrag.y, startDrag.y))+1);
  int sIndex = max(0, gX(min(endDrag.x, startDrag.x)));
  int eIndex = min(width, gX(max(endDrag.x, startDrag.x))+1);
  for (int i=sPitch; i<ePitch; i++) {
    for (int j=0; j<notes[i].size(); j++) {
      Note curNote = notes[i].get(j);
      if (sIndex<=curNote.start&&curNote.start<=eIndex) {
        highlightedNote.add(new Point(i, j));
      }
    }
  }
}

public void updateScore() {
  score.empty();
  score.tempo(120);

  for (int i=0; i<notes.length; i++) {
    for (int j=0; j<notes[i].size(); j++) {
      Note curNote = notes[i].get(j);
      score.addNote(curNote.start/4.0f, 0, curInstrument, 127-i, curNote.dynamic, curNote.len/4.0f, 1.6f, 64);
    }
  }
}


public void deleteHiLiNote() {
  for (int i=0; i<highlightedNote.size(); i++) {
    Note curSelNote = notes[highlightedNote.get(i).pitch].get(highlightedNote.get(i).index);
    curSelNote.len = 0;
  }
}
float camX = 100;
float camY = -1200;

float scaleX = 3.0f;
float scaleY = 3.0f;

public void moveCamera() {
  //fill(255,255,255);
  //rect(0,0,camX,camY);
  translate(camX, camY);
  scale(scaleX, scaleY);
}

public int gX(float x1) {
  int xe = PApplet.parseInt((x1-camX)/scaleX)/8;
  return xe;
}

public int gY(float y1) {
  int ye = PApplet.parseInt((y1-camY)/scaleY)/8;
  ye = constrain(ye,0,notes.length-1);
  return ye;
}

ArrayList<Chord> chordPattern = new ArrayList<Chord>();

ArrayList<MovableNote> chorus = new ArrayList<MovableNote>();

int songLength = 360;

int chdNoteLen = 4;

public void composeMusic() {
  for (int i=0; i<notes.length; i++) {
    notes[i] = new ArrayList<Note>();
  }
  composeChords();
  int curChd = 0;
  for (int i=0; i<songLength; i+=4*chdNoteLen) {
    Chord curChord = chordPattern.get(curChd);
    int pitchOffset = 127-48;
    notes[pitchOffset-curChord.chordNotes[0]+12].add(new Note(i,4*chdNoteLen, 77));
     notes[pitchOffset-curChord.chordNotes[1]+12].add(new Note(i+1,4*chdNoteLen-1, 77));
     notes[pitchOffset-curChord.chordNotes[2]+12].add(new Note(i+2,4*chdNoteLen-2, 77));

    notes[pitchOffset-curChord.chordNotes[0]].add(new Note(i+0*chdNoteLen, chdNoteLen+4, 97));
    notes[pitchOffset-curChord.chordNotes[1]].add(new Note(i+1*chdNoteLen, chdNoteLen+4, 97));
    notes[pitchOffset-curChord.chordNotes[2]].add(new Note(i+2*chdNoteLen, chdNoteLen+4, 97));
    notes[pitchOffset-curChord.chordNotes[1]].add(new Note(i+3*chdNoteLen, chdNoteLen+4, 97));
    if ((i/(4*chdNoteLen))%2==1)
      curChd++;
    curChd = curChd%chordPattern.size();
  }

  composeChorus();
  for (int i=0; i<chorus.size(); i++) {
    MovableNote curNote = chorus.get(i);
    float newStart = curNote.start;
    int newPitch = 127-curNote.pitch;
    newStart = constrain(newStart, 0, 9999);
    newPitch = constrain(newPitch, 0, notes.length-1);
    Note newNote = new Note(newStart, curNote.len);
    notes[newPitch].add(newNote);
  }
}

public void composeChorus() {
  chorus = new ArrayList<MovableNote>();
  int r1 = PApplet.parseInt(random(7,9))*8*chdNoteLen;
  int curNote = chordPattern.get(0).chordScale+60;
  int curLen = 4;
  boolean wasCT = false;
  for (int i=64; i<=r1+64; i+=curLen) {
    if(random(2)<1) {
    } else if(i%2==1) {
      curLen = PApplet.parseInt(random(0,4))*2+1;
    } else if(i%4==2&&random(2)<1) {
      curLen = 2;
    } else if(random(4)<4){
      curLen = PApplet.parseInt(pow(2,PApplet.parseInt(random(0,3))));
    } else {
      curLen = PApplet.parseInt(random(1,4));
    }
    
    if(i%4==2&&curLen%4==0) {
      curLen += 2;
    }
    
    Chord curChord = chordPattern.get((i/(4*chdNoteLen*2))%chordPattern.size());
    int rScale = curChord.chordScale;
    boolean newNoteExist = true;
    if (i + curLen > r1+64) {
      for(;curNote>12&&((curNote)%12!=curChord.chordScale%12);curNote--) {
      }
      curLen = 10;
      println("here");
    } else if ((i%2==1&&curLen!=1)||(wasCT&&random(5)<2)) {
      curLen = PApplet.parseInt(random(1,4))*2+i%2;
      wasCT = false;
      newNoteExist = false;
    } else if (!wasCT) {
      curNote = getCTfromNCT(curChord.chordNotes, curNote);
      wasCT = true;
    } else if (random(2)<1) {
      int [] ct1 = getCT(curChord.chordNotes, curNote);
      curNote = ct1[PApplet.parseInt(random(ct1.length))];
      wasCT = true;
    } else {
      int [] nct1 = getNCT(rScale, curChord.chordScaleType, curChord.chordNotes, curNote);
      curNote = nct1[PApplet.parseInt(random(nct1.length))];
      wasCT = false;
    }
    if (newNoteExist) {
      MovableNote newNote = new MovableNote(curNote, i, curLen);
      chorus.add(newNote);
    }
  }
}

public void composeChords() {
  chordPattern = new ArrayList<Chord>();
  int r1 = PApplet.parseInt(random(2, 5));
  int rScale = PApplet.parseInt(random(12));
  for (int i=0; i<r1; i++) {
    rScale = (rScale+PApplet.parseInt(random(-3, 3))*7+60)%12;
    Chord newChord = new Chord(rScale, 1);
    chordPattern.add(newChord);
  }
}

class Chord {
  int chordScale;
  int chordScaleType;
  int [] chordNotes;
  Chord(int sc1, int st1) {
    chordScale = sc1;
    chordScaleType = st1;
    chordNotes = new int[3];
    chordNotes[0] = scales[sc1][st1][0];
    chordNotes[1] = scales[sc1][st1][2];
    chordNotes[2] = scales[sc1][st1][4];
    println("s "+chordNotes[0]+" "+chordNotes[1]+" "+chordNotes[2]);

    if (chordNotes[1]>10) {
      int tmp = chordNotes[2]-12;
      chordNotes[2] = chordNotes[0];
      chordNotes[0] = chordNotes[1]-12;
      chordNotes[1] = tmp;
    } else if (chordNotes[2]>10) {
      int tmp = chordNotes[2]-12;
      chordNotes[2] = chordNotes[1];
      chordNotes[1] = chordNotes[0];
      chordNotes[0] = tmp;
    }
    println("e "+chordNotes[0]+" "+chordNotes[1]+" "+chordNotes[2]);
  }
}


boolean [] keyPresses = new boolean[256];

public void keyPressed() {
  if (keyCode == ESC) {
    isSelecting = false;
    cancelTransform();
    key = 0;
    keyCode = 0;
  }
  keyPresses[keyCode] = true;
}

public void keyReleased() {

  if (keyCode==' ') {
    if (score.playing) {
      score.stop();
    } else {
      score.instrument(curInstrument);
      updateScore();
      score.update();
      println("The new instrument is " + curInstrument);
      score.play(0);
      playStartMil = millis();
    }
  } else if (keyCode=='X') {
    deleteHiLiNote();
  } else if (keyCode=='S') {
    if (transformationNow==0)
      startTransform(SCALE);
  } else if (keyCode=='G') {
    if (transformationNow==0)
      startTransform(TRANSLATE);
  } else if (keyCode=='E') {
    if (transformationNow==0)
      startTransform(EXTRUDE);
  } else if (keyCode=='D') {
    if (transformationNow==0)
      startTransform(DUPLICATE);
  } else if (keyCode=='B') {
    if (transformationNow==0) {
      isSelecting = true;
    }
  } else if (keyCode=='C') {
    composeMusic();
  } else if (keyCode=='I') {
    curInstrument = PApplet.parseInt(random(128));
  } else if (keyCode=='A') {
    if (highlightedNote.size()==0) {
      for (int i=0; i<notes.length; i++) {
        for (int j=0; j<notes[i].size(); j++) {
          highlightedNote.add(new Point(i, j));
        }
      }
    } else {
      highlightedNote = new ArrayList<Point>();
    }
  }

  keyPresses[keyCode] = false;
}

PVector startDrag = new PVector(0, 0);
PVector endDrag = new PVector(0, 0);

boolean isSelecting = false;

public void mouseWheel(MouseEvent event) {
  float e = -event.getCount();

  if (keyPresses[ALT]) {
    float pcamX = (mouseX-camX)/scaleX;
    float pcamY = (mouseY-camY)/scaleY;

    scaleX *= pow(1.01f, e);
    scaleY *= pow(1.01f, e);

    camX = mouseX-pcamX*scaleX;
    camY = mouseY-pcamY*scaleY;
  } else if (keyPresses[CONTROL]) {
    camX += 3.0f*e;
  } else {
    camY += 3.0f*e;
  }
}

public void mouseDragged() {
  endDrag = new PVector(mouseX, mouseY);
  if (keyPresses[SHIFT]) {
    camX += mouseX-pmouseX;
    camY += mouseY-pmouseY;
  } else if(isSelecting) {
    updateSelection();
  }
}

public void mouseReleased() {
  startDrag = new PVector(0, 0);
  endDrag = new PVector(0, 0);
  isSelecting = false;
  updateScore();
}

public void mousePressed() {

  startDrag = new PVector(mouseX, mouseY);
  endDrag = new PVector(mouseX, mouseY);

  confirmTransform();
  
  int i1 = -1;
  int j1 = gY(mouseY);

  boolean noteAtMouse = false;
  for (int i=0; i<notes[j1].size(); i++) {
    Note curNote = notes[j1].get(i);
    if (curNote.start<=gX(mouseX)&&gX(mouseX)<=(curNote.start+curNote.len)) {
      i1 = i;
      if (gX(mouseX)==curNote.start) {
        noteAtMouse = true;
      }
    }
  }

  if (mouseButton == RIGHT && keyPresses[CONTROL] && !noteAtMouse) {
    notes[j1].add(new Note(gX(mouseX), 2));
    i1 = notes[j1].size()-1;
  }

  updateSelection();

  if (keyPresses['X']) {
    deleteHiLiNote();
  }

  updateScore();
}

int scaleNow = 2;
int scaleTypeNow = 0;

int maxPitch = 100;
int minPitch = 72;

class Note {
  float start;
  float len;
  boolean markToBeRemoved;
  float dynamic;
  Note(float s1, float l1) {
    start = s1;
    len = l1;
    markToBeRemoved = false;
    dynamic = 127;
  }
  Note(float s1, float l1, float d1) {
    start = s1;
    len = l1;
    markToBeRemoved = false;
    dynamic = d1;
  }
}

class MovableNote extends Note {
  int pitch;
  MovableNote(int x1, float y1, float l1) {
    super(y1, l1);
    pitch = x1;
  }
}

public int getCTfromNCT(int [] chd1, int noteNow1) {
  for (int i=1; i<10; i++) {
    if (noteNow1-i>minPitch&&isCT(chd1, noteNow1-i)) {
      return noteNow1-i;
    }
  }
  for (int i=1; i<10; i++) {
    if (noteNow1+i<maxPitch&&isCT(chd1, noteNow1+i)) {
      return noteNow1+i;
    }
  }
  return noteNow1;
}

public int [] getCT(int [] chd1, int noteNow1) {
  int [] ct1 = {noteNow1};
  for (int i=1; i<6; i++) {
    if (noteNow1+i<maxPitch&&isCT(chd1, noteNow1+i)) {
      ct1 = append(ct1, noteNow1+i);
    }
    if (noteNow1-i>minPitch&&isCT(chd1, noteNow1-i)) {
      ct1 = append(ct1, noteNow1-i);
    }
  }
  return ct1;
}

public int [] getNCT(int sId, int sTy, int [] chd1, int noteNow1) {
  int [] nct1 = {};
  for (int i=1; i<6; i++) {
    if (noteNow1+i<maxPitch&&isNCT(sId, sTy, chd1, noteNow1+i)) {
      nct1 = append(nct1, noteNow1+i);
    }
    if (noteNow1-i>minPitch&&isNCT(sId, sTy, chd1, noteNow1-i)) {
      nct1 = append(nct1, noteNow1-i);
    }
  }
  return nct1;
}

public boolean isCT(int [] chd1, int noteNow1) {
  for (int i=0; i<chd1.length; i++) {
    if ((noteNow1+60)%12==(chd1[i]+60)%12)
      return true;
  }
  return false;
}

public boolean isNCT(int sId, int sTy, int [] chd1, int noteNow1) {
  for (int i=0; i<chd1.length; i++) {
    if ((noteNow1+60)%12==(chd1[i]+60)%12)
      return false;
  }
  if (isInScale(sId, sTy, noteNow1))
    return true;
  else
    return false;
}

public boolean isInScale(int n1) {
  return isInScale(scaleNow, scaleTypeNow, n1);
}

public boolean isInScale(int sId, int sTy, int n1) {
  n1 = (n1+60)%12;
  for (int i=0; i<7; i++) {
    if (n1==(scales[sId][sTy][i]+60)%12)
      return true;
  }
  return false;
}

class Point {
  int pitch;
  int index;
  Point(int x1, int y1) {
    pitch = x1;
    index = y1;
  }
}

public void calculateScales() {
  for (int i=0; i<12; i++) {
    scales[i][0][0] = (i+0);
    scales[i][0][1] = (i+2);
    scales[i][0][2] = (i+4);
    scales[i][0][3] = (i+5);
    scales[i][0][4] = (i+7);
    scales[i][0][5] = (i+9);
    scales[i][0][6] = (i+11);

    scales[i][1][0] = (i+0);
    scales[i][1][1] = (i+2);
    scales[i][1][2] = (i+3);
    scales[i][1][3] = (i+5);
    scales[i][1][4] = (i+7);
    scales[i][1][5] = (i+8);
    scales[i][1][6] = (i+10);
  }
}

int [][][] scales = new int[12][2][7];

String [] notesNames = {
  "C", "C#", "D", "Eb", "E", "F", "F#", "G", "Ab", "A", "Bb", "B", 
  "C", "C#", "D", "Eb", "E", "F", "F#", "G", "Ab", "A", "Bb", "B", 
  "C", "C#", "D", "Eb", "E", "F", "F#", "G", "Ab", "A", "Bb", "B", 
  "C", "C#", "D", "Eb", "E", "F", "F#", "G", "Ab", "A", "Bb", "B", 
  "C", "C#", "D", "Eb", "E", "F", "F#", "G", "Ab", "A", "Bb", "B", 
  "C", "C#", "D", "Eb", "E", "F", "F#", "G", "Ab", "A", "Bb", "B", 
  "C", "C#", "D", "Eb", "E", "F", "F#", "G", "Ab", "A", "Bb", "B", 
  "C", "C#", "D", "Eb", "E", "F", "F#", "G", "Ab", "A", "Bb", "B", 
  "C", "C#", "D", "Eb", "E", "F", "F#", "G", "Ab", "A", "Bb", "B", 
  "C", "C#", "D", "Eb", "E", "F", "F#", "G", "Ab", "A", "Bb", "B", 
  "C", "C#", "D", "Eb", "E", "F", "F#", "G", "Ab", "A", "Bb", "B", };


int transformationNow = 0;

int SCALE = 1;
int TRANSLATE = 2;
int EXTRUDE = 3;
int DUPLICATE = 4;

PVector startTransform = new PVector(0, 0);

ArrayList<MovableNote> transformingNotes = new ArrayList<MovableNote>();

public void startTransform(int type1) {
  transformationNow = type1;
  startTransform = new PVector(mouseX, mouseY);

  transformingNotes = new ArrayList<MovableNote>();

  if (transformationNow==SCALE) {
  } else if (transformationNow==TRANSLATE) {
    for (int i=0; i<highlightedNote.size(); i++) {
      Note curSelNote = notes[highlightedNote.get(i).pitch].get(highlightedNote.get(i).index);
      transformingNotes.add(new MovableNote(highlightedNote.get(i).pitch, curSelNote.start, curSelNote.len));
      curSelNote.len = 0;
    }
  } else if (transformationNow==EXTRUDE) {
    transformingNotes.add(new MovableNote(gY(mouseY), gX(mouseX), 2));
  } else if (transformationNow==DUPLICATE) {
    for (int i=0; i<highlightedNote.size(); i++) {
      Note curSelNote = notes[highlightedNote.get(i).pitch].get(highlightedNote.get(i).index);
      transformingNotes.add(new MovableNote(highlightedNote.get(i).pitch, curSelNote.start, curSelNote.len));
    }
  }
}

public void confirmTransform() {
  if (transformationNow==SCALE) {
  } else if (transformationNow==TRANSLATE||transformationNow==EXTRUDE||transformationNow==DUPLICATE) {
    for (int i=0; i<transformingNotes.size(); i++) {
      MovableNote curNote = transformingNotes.get(i);
      float newStart = gX(curNote.start*8*scaleX+(mouseX-startTransform.x+camX)+8);
      int newPitch = gY(curNote.pitch*8*scaleY+(mouseY-startTransform.y+camY)+8);
      newStart = constrain(newStart, 0, 9999);
      newPitch = constrain(newPitch, 0, notes.length-1);
      Note newNote = new Note(newStart, curNote.len);
      notes[newPitch].add(newNote);
    }
  }

  transformingNotes = new ArrayList<MovableNote>();

  transformationNow = 0;
}

public void cancelTransform() {
  transformingNotes = new ArrayList<MovableNote>();
  transformationNow = 0;
}

public void updateTransformations() {
  if (transformationNow==SCALE) {
    for (int i=0; i<highlightedNote.size(); i++) {
      Note curSelNote = notes[highlightedNote.get(i).pitch].get(highlightedNote.get(i).index);
      if (curSelNote.len!=0) {
        curSelNote.len = ceil((mouseX-startTransform.x)/scaleX/8);
        curSelNote.len = constrain(curSelNote.len, 1, 32);
      }
    }
  } else if (transformationNow==TRANSLATE||transformationNow==EXTRUDE||transformationNow==DUPLICATE) {
    pushMatrix();
    translate((mouseX-startTransform.x)+camX, (mouseY-startTransform.y)+camY);
    scale(scaleX, scaleY);
    stroke(150, 100, 100);
    strokeWeight(1);
    fill(150, 200, 255);
    for (int i=0; i<transformingNotes.size(); i++) {
      MovableNote curNote = transformingNotes.get(i);
      rect(curNote.start*8, curNote.pitch*8, 8*curNote.len, 8, 3);
    }
    popMatrix();
  }
}
  static public void main(String[] passedArgs) {
    String[] appletArgs = new String[] { "MusicComposer" };
    if (passedArgs != null) {
      PApplet.main(concat(appletArgs, passedArgs));
    } else {
      PApplet.main(appletArgs);
    }
  }
}
