//
// Programmer:    Craig Stuart Sapp <craig@ccrma.stanford.edu>
// Creation Date: Thu Apr 11 11:43:12 PDT 2002
// Last Modified: Fri Jun 12 22:58:34 PDT 2009 renamed SigCollection class
// Last Modified: Mon Feb 21 08:29:14 PST 2011 added --match
// Last Modified: Sun Feb 27 15:09:29 PST 2011 added fixed vocal colors
// Filename:      ...sig/examples/all/proll.cpp
// Web Address:   http://sig.sapp.org/examples/museinfo/humdrum/proll.cpp
// Syntax:        C++; museinfo
//
// Description:   Generate piano roll plots.
//

#include "humdrum.h"
#include "PerlRegularExpression.h"

#include <string.h>
#include <ctype.h>

typedef SigCollection<PixelColor> PixelRow;


// function declarations
void   checkOptions(Options& opts, int argc, char* argv[]);
void   example(void);
void   usage(const char* command);
void   generateBackground(HumdrumFile& infile, int rfactor, 
                                 Array<PixelRow>& picturedata, 
                                 Array<PixelRow>& background);
int    generatePicture(HumdrumFile& infile, Array<PixelRow>& picture,
                                 int style);
void   printPicture(Array<PixelRow>& picturedata, 
                                 Array<PixelRow>& background, int rfactor, 
                                 int cfactor, int minp, int maxp, 
                                 HumdrumFile& infile);
void   placeNote(Array<PixelRow>& picture, int pitch, 
                                 double start, double duration, int min, 
                                 PixelColor& color, double factor, int match);
PixelColor makeColor(HumdrumFile& infile, int line, int spine, 
                                 int style, Array<int>& rhylev, int track);
void   getMarkChars(Array<char>& marks, HumdrumFile& infile);
int    isMatch(Array<char>& marks, const char* buffer);
const char* getInstrument(HumdrumFile& infile, int spine);

// global variables
Options   options;                   // database for command-line arguments
int       debugQ    = 0;             // used with --debug option
int       markQ     = 0;             // used with --mark option
int       maxwidth  = 3000;          // used with -w option
int       maxheight = 400;           // used with -h option
int       rfactor   = 1;
int       cfactor   = 1;
int       gminpitch = 0;
int       gmaxpitch = 127;
int       maxfactor = 5;
int       measureQ  = 1;              // used with the -M option
int       keyboardQ = 1;              // used with the -K option
int       style     = 'H';            // used with the -s option
const char* keyboardcolor = "151515"; // used with the -k option

///////////////////////////////////////////////////////////////////////////

int main(int argc, char* argv[]) {
   checkOptions(options, argc, argv);

   Array<PixelRow> picturedata;
   Array<PixelRow> background;
   HumdrumFile infile;

   int numinputs = options.getArgCount();
   if (numinputs > 0) {
      char* filenameIn  = options.getArg(1);
      infile.read(filenameIn);
   } else {
      infile.read(cin);
   }

   int rfactor = generatePicture(infile, picturedata, style);
   generateBackground(infile, rfactor, picturedata, background);
   printPicture(picturedata, background, rfactor, cfactor, 
         gminpitch, gmaxpitch, infile);

   return 0;
}


///////////////////////////////////////////////////////////////////////////


//////////////////////////////
//
// printPicture --
//

void printPicture(Array<PixelRow>& picturedata, Array<PixelRow>& background,
      int rfactor, int cfactor, int minp, int maxp, HumdrumFile& infile) {


   if (minp > 0) {
      minp--;
   }
   if (maxp < 127) {
      maxp++;
   }
   int i, j;
   int m;
   int width = picturedata[0].getSize();
   int height = (maxp - minp + 1);
   cfactor = (int)(maxheight / height);
   if (cfactor <= 0) {
      cfactor = 1;
   }
   if (cfactor > maxfactor) {
      cfactor = maxfactor;
   }
   PixelColor temp;
   PixelColor black(0,0,0);
   height = cfactor * height;
   cout << "P6\n" << width << " " << height << "\n255\n";
   for (i=maxp; i>=minp; i--) {
      for (m=0; m<cfactor; m++) {
         for (j=0; j<picturedata[i].getSize(); j++) {
            if (picturedata[i][j] == black) {
               background[i][j].writePpm6(cout);
            } else {
               if ((i > 0) && (cfactor > 1) && (m == cfactor-1) && 
                     (picturedata[i-1][j] == picturedata[i][j])) {
                  temp = picturedata[i][j] * 0.667;
                  temp.writePpm6(cout);
               } else {
                  picturedata[i][j].writePpm6(cout);
               }
            }
         }
      }
   }
}



//////////////////////////////
//
// generatePicture -- create the picture.  Returns the number of
//    pixel repetitions for each row's pixel.
//

int generatePicture(HumdrumFile& infile, Array<PixelRow>& picture, int
      style) {

   Array<char> marks;
   getMarkChars(marks, infile);
   PixelColor matchcolor(255,255,255);

   infile.analyzeRhythm("4");
   int min = infile.getMinTimeBase();
   double totaldur = infile.getTotalDuration();
   
   int columns = (int)(totaldur * min / 4.0 + 0.5) + 5;
   if (columns > 50000) {
      cout << "Error: picture will be too big to generate" << endl;
      exit(1);
   }
   int factor = (int)(maxwidth / columns);
   if (factor <= 0) {
      factor = 1;
   }
   if (factor > maxfactor) {
      factor = maxfactor;
   }

   // set picture to black first.  Black regions will be filled in
   // with the background later.
   picture.setSize(128);
   int i, j, k;
   for (i=0; i<picture.getSize(); i++) {
      picture[i].setSize(columns * factor);
      for (j=0; j<picture[i].getSize(); j++) {
         picture[i][j].setRed(0);
         picture[i][j].setGreen(0);
         picture[i][j].setBlue(0);
      }
   }

   // examine metric levels for metric coloration
   Array<int>rhylev;
   infile.analyzeMetricLevel(rhylev);
   for (i=0; i<rhylev.getSize(); i++) {
      // reverse sign so that long notes are positive.
      rhylev[i] = -rhylev[i];
   }
   
   PixelColor color;
   int minpitch = 128;
   int maxpitch = -1;
   int pitch = 0;
   double duration = 0;
   double start = 0;
   char buffer[1024] = {0};
   for (i=0; i<infile.getNumLines(); i++) {
      if (debugQ) {
         cout << "Processing input line " << i + 1 << '\t' << infile[i] << endl;
      }
      if (infile[i].isData()) {
         start = infile[i].getAbsBeat();
         for (j=0; j<infile[i].getFieldCount(); j++) {
            if (strcmp(infile[i].getExInterp(j), "**kern") != 0) {
               continue;
            }
            // duration = Convert::kernToDuration(infile[i][j]);
            duration = infile.getTiedDuration(i, j);
            color = makeColor(infile, i, j, style, rhylev, 
                  infile[i].getPrimaryTrack(j));
            for (k=0; k<infile[i].getTokenCount(j); k++) {
               infile[i].getToken(buffer, j, k);
               if (strchr(buffer, '_') != NULL) {
                  continue;
               }
               if (strchr(buffer, ']') != NULL) {
                  continue;
               }

               pitch = Convert::kernToMidiNoteNumber(buffer);
               if (pitch < 0) {
                  // ignore rests
                  continue;
               }
               if (pitch < minpitch) {
                  minpitch = pitch;
               }
               if (pitch > maxpitch) {
                  maxpitch = pitch;
               }
               if (isMatch(marks, buffer)) {
                  placeNote(picture, pitch, start, duration, min, 
                        color, factor, 1);
               } else {
                  placeNote(picture, pitch, start, duration, min, 
                        color, factor, 0);
               }
            }
         }
      }

   }

   gmaxpitch = maxpitch;
   gminpitch = minpitch;
   return factor;
}



//////////////////////////////
//
// isMatch -- returns true if the string has a match character in it
//

int isMatch(Array& marks, const char* buffer) {
   int i;
   for (i=0; i<marks.getSize(); i++) {
      if (strchr(buffer, marks[i]) != NULL) {
         return 1;
      }
   }
   return 0;
}



//////////////////////////////
//
// makeColor --
//

PixelColor makeColor(HumdrumFile& infile, int line, int spine, int style,
      Array<int>& rhylev, int track) {
   PixelColor output;
   int trackCount;
   PerlRegularExpression pre;
   const char* instrument = "";

   PixelColor purple     (225, 121, 255);
   PixelColor yellowgreen(150, 200,   0);

   switch (toupper(style)) {
      case 'M':    // color by metric position
         if (rhylev[line] >= 2) {
            output.setColor("red");
         } else if (rhylev[line] == 1) {
            output.setColor("lightorange");
         } else if (rhylev[line] == 0) {
            output.setColor("yellow");
         } else if (rhylev[line] == -1) {
            output.setColor("green");
         } else if (rhylev[line] == -2) {
            output.setColor("blue");
         } else if (rhylev[line] <= -3) {
            output.setColor("violet");
         } else {
            output.setColor("silver");
         }
         break;

      case 'V':    // color spines by voice
         instrument = getInstrument(infile, track);
         if (pre.search(instrument, "Bassus", "i")) {
            output.setColor("red");
         } else if (pre.search(instrument, "Contra", "i")) {
            output.setColor("darkorange");
         } else if (pre.search(instrument, "Tenor", "i")) {
            output.setColor("blue");
         } else if (pre.search(instrument, "Altus", "i")) {
            output = purple;
         } else if (pre.search(instrument, "Superius", "i")) {
            output.setColor("limegreen");
         } else if (pre.search(instrument, "Cantus", "i")) {
            output.setColor("limegreen");
         } else if (pre.search(instrument, "Discantus", "i")) {
            output = yellowgreen;
         } else {
            output.setColor("black");
         }
         break;
         
      case 'H':    // color spines by hue
      default:
         trackCount = infile.getMaxTracks();
         output.setHue(((int)infile[line].getTrack(spine))/(double)trackCount);
   }

   return output;
}



//////////////////////////////
//
// getInstrument --
//

const char* getInstrument(HumdrumFile& infile, int track) {
   int i, j;
   for (i=0; i<infile.getNumLines(); i++) {
      if (!infile[i].isInterpretation()) {
         continue;
      }
      for (j=0; j<infile[i].getFieldCount(); j++) {
         if (track != infile[i].getPrimaryTrack(j)) {
            continue;
         }
         if (strncmp(infile[i][j], "*I\"", 3) == 0) {
            return &infile[i][j][3];
         }
      }
   }
   return "";
}



//////////////////////////////
//
// placeNote -- draw a note in the picture area
//

void placeNote(Array<PixelRow>& picture, int pitch, double start, 
      double duration, int min, PixelColor& color, double factor, int match) {
   int startindex = (int)(start * min / 4.0 * factor);
   int endindex = (int)((start + duration) * min / 4.0 * factor) - 1;

   PixelColor zcolor = color;
   if (match) {
      zcolor.Red   = (zcolor.Red   + 4*255)/5;
      zcolor.Green = (zcolor.Green + 4*255)/5;
      zcolor.Blue  = (zcolor.Blue  + 4*255)/5;
   }
   PixelColor black(0,0,0);
   if (startindex-1 >= 0) {
      if (picture[pitch][startindex-1] == color) {
         picture[pitch][startindex-1] *= 0.667;
      }
   }
   for (int i=startindex; i<=endindex; i++) {
      if (picture[pitch][i] == black) {
         picture[pitch][i] = zcolor;
      } else {
         if (match) {
            picture[pitch][i].Red   = (2*zcolor.Red 
                  + picture[pitch][i].Red)/3;
            picture[pitch][i].Green = (2*zcolor.Green 
                  + picture[pitch][i].Green)/3;
            picture[pitch][i].Blue  = (2*zcolor.Blue 
                  + picture[pitch][i].Blue)/3;
         } else {
            picture[pitch][i].Red   = (color.Red 
                  + picture[pitch][i].Red)/2;
            picture[pitch][i].Green = (color.Green 
                  + picture[pitch][i].Green)/2;
            picture[pitch][i].Blue  = (color.Blue 
                  + picture[pitch][i].Blue)/2;
         }
      }
   }
}



//////////////////////////////
//
// generateBackground -- create the picture.
//

void generateBackground(HumdrumFile& infile, int rfactor, 
      Array<PixelRow>& picturedata, Array<PixelRow>& background) {

   background.setSize(picturedata.getSize());
   int i, j;

   for (i=0; i<picturedata.getSize(); i++) {
      background[i].setSize(picturedata[i].getSize());
      for (j=0; j<picturedata[i].getSize(); j++) {
         background[i][j] = 0;
      }
   }

   PixelColor whitekeys(keyboardcolor);
   if (keyboardQ) {
      for (i=0; i<background.getSize(); i++) {
         switch (i % 12) {
            case 0: case 2: case 4: case 5: case 7: case 9: case 11:
               for (j=0; j<background[i].getSize(); j++) {
                  background[i][j] = whitekeys;
               }
               break;
         }
      }
   }


   int index;
   int min = infile.getMinTimeBase();
   PixelColor measureColor;
   measureColor.setColor(25, 25, 25);
   for (i=0; i<infile.getNumLines(); i++) {
      if (infile[i].isMeasure()) {
         index = (int)(infile[i].getAbsBeat() * min / 4.0 * rfactor);
         for (j=0; j<background.getSize(); j++) {
            background[j][index] = measureColor;
         }
      }
   }

}



//////////////////////////////
//
// checkOptions -- validate and process command-line options.
//

void checkOptions(Options& opts, int argc, char* argv[]) {
   opts.define("w|width=i:3000",      "maximum width allowable for image");   
   opts.define("h|height=i:400",      "maximum height allowable for image");   
   opts.define("M|no-measure=b",      "do not display measure lines on image");
   opts.define("K|no-keyboard=b",     "do not display keyboard in background");
   opts.define("k|keyboard=s:151515", "keyboard white keys color");
   opts.define("s|style=s:H",         "Coloring style");
   opts.define("mark=b",              "Highlight marked/matched notes");

   opts.define("debug=b",          "trace input parsing");   
   opts.define("author=b",         "author of the program");   
   opts.define("version=b",        "compilation information"); 
   opts.define("example=b",        "example usage"); 
   opts.define("help=b",           "short description"); 
   opts.process(argc, argv);
   
   // handle basic options:
   if (opts.getBoolean("author")) {
      cout << "Written by Craig Stuart Sapp, "
           << "craig@ccrma.stanford.edu, April 2002" << endl;
      exit(0);
   } else if (opts.getBoolean("version")) {
      cout << argv[0] << ", version: 8 April 2002" << endl;
      cout << "compiled: " << __DATE__ << endl;
      cout << MUSEINFO_VERSION << endl;
      exit(0);
   } else if (opts.getBoolean("help")) {
      usage(opts.getCommand());
      exit(0);
   } else if (opts.getBoolean("example")) {
      example();
      exit(0);
   }


   debugQ    =  opts.getBoolean("debug");
   markQ     =  opts.getBoolean("mark");
   maxwidth  =  opts.getInteger("width");
   maxheight =  opts.getInteger("height");
   measureQ  = !opts.getBoolean("no-measure");
   keyboardQ = !opts.getBoolean("no-keyboard");
   keyboardcolor = opts.getString("keyboard");
   style     = opts.getString("style")[0];
}



//////////////////////////////
//
// example -- example usage of the maxent program
//

void example(void) {
   cout <<
   "                                                                        \n"
   << endl;
}



//////////////////////////////
//
// usage -- gives the usage statement for the quality program
//

void usage(const char* command) {
   cout <<
   "                                                                        \n"
   << endl;
}



//////////////////////////////
//
// getMarkChars --
//

void getMarkChars(Array& marks, HumdrumFile& infile) {
   PerlRegularExpression pre;
   Array<char>& colorchar = marks;

   colorchar.setSize(0);
   char value;
   int i;
   for (i=0; i<infile.getNumLines(); i++) {
      if (!infile[i].isBibliographic()) {
         continue;
      }
      // !!!RDF**kern: N= mark color="#ff0000", root
      if (pre.search(infile[i].getLine(), 
            "^!!!RDF\\*\\*kern:\\s*([^\\s])\\s*=\\s*match", "i") ||
          pre.search(infile[i].getLine(), 
            "^!!!RDF\\*\\*kern:\\s*([^\\s])\\s*=\\s*mark", "i")
         ) {
         value = pre.getSubmatch(1)[0];
         colorchar.append(value);
      }
   }
}



// md5sum: 3d3af5f512f6fef7c9c890dd52011719 proll.cpp [20120805]