//
// Programmer:    Craig Stuart Sapp <craig@ccrma.stanford.edu>
// Creation Date: Thu Oct 20 20:52:45 PDT 2011
// Last Modified: Thu Oct 20 20:52:48 PDT 2011
// Filename:      ...sig/examples/all/staff.cpp
// Web Address:   http://sig.sapp.org/examples/museinfo/humdrum/staff.cpp
// Syntax:        C++; museinfo
//
// Description:   Extact music by staff indications.
//

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

#ifndef OLDCPP
   using namespace std;
#endif

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

// function declarations
void      checkOptions(Options& opts, int argc, char* argv[]);
void      example(void);
void      usage(const char* command);
void      processFile(HumdrumFile& infile, Array<int>& field);
void      getStaves(Array<Array<Array<double> > >& staves, 
                              HumdrumFile& infile);
void      setStaffAssignment(Array<double>& states, const char* string);
void      printStaves(Array<Array<Array<double> > >& staves);
void      fillFieldData(Array<int>& field, Array<int>& subfield, 
                              Array<int>& model, const char* fieldstring, 
                              HumdrumFile& infile);
void      processFieldEntry(Array<int>& field, Array<int>& subfield, 
                              Array<int>& model, const char* string, 
                              HumdrumFile& infile);
void      removeDollarsFromString(Array<char>& buffer, int maxtrack);
int       isMatch(int ss, Array<double>& list);
void      printLineDuration(HumdrumFile& infile, int line);
void      getMeasureDurations(Array<RationalNumber>& measuredur, 
                               HumdrumFile& infile);
void      addStaffEntry(Array<double> & states, double value);
void      getMeasureStates(Array<Array<double> >& measurestates, 
                               Array<Array<Array<double> > >& staves, 
                               Array<RationalNumber>& mdur,
                               HumdrumFile& infile);
void      printDuration(const RationalNumber& duration);

// global variables
Options   options;             // database for command-line arguments
const char*  fieldstring = ""; // used with -s option
int       debugQ         = 0;  // used with --debug option


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

int main(int argc, char* argv[]) {
   HumdrumFile infile;

   // process the command-line options
   checkOptions(options, argc, argv);

   const char* filename;
   infile.clear();
   // if no command-line arguments read data file from standard input
   int numinputs = options.getArgCount();
   if (numinputs < 1) {
      infile.read(cin);
   } else {
      filename = options.getArg(1);
      infile.read(options.getArg(1));
   }

   infile.analyzeRhythm("4");
   Array<int> model; 
   Array<int> field;
   Array<int> subfield;
   fillFieldData(field, subfield, model, fieldstring, infile);

   processFile(infile, field);

}

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


//////////////////////////////
//
// processFile --
//

void processFile(HumdrumFile& infile, Array& field) {

   Array<Array<Array<double> > > staves;
   getStaves(staves, infile);
   if (debugQ) {
      printStaves(staves);
   }

   Array<RationalNumber> measuredur;
   getMeasureDurations(measuredur, infile);

   Array<Array<double> > measurestates;
   getMeasureStates(measurestates, staves, measuredur, infile);
   
   PerlRegularExpression pre;
   int i, j, s, ss;
   int first = 0;
   int found = 0;
   int active;
   for (i=0; i<infile.getNumLines(); i++) {
      if (infile[i].isBibliographic()) {
         cout << infile[i] << endl;
         continue;
      }
      if (infile[i].isGlobalComment()) {
         cout << infile[i] << endl;
         continue;
      }
      if (infile[i].isEmpty()) {
         cout << infile[i] << endl;
         continue;
      }

      first = 0;
      found = 0;
      for (s=0; s<field.getSize(); s++) {
         ss = field[s];
         found = 0;
         for (j=0; j<infile[i].getFieldCount(); j++) {
            if (isMatch(ss, staves[i][j])) {
               if (first == 0) {
                  first = 1;
               } else {
                  cout << "\t";
               }
               cout << infile[i][j];
               found = 1;
            }
         }
         if (found) {
            continue;
         }
         if (first == 0) {
            first = 1;
         } else {
            cout << "\t";
         }
         if (infile[i].isMeasure()) {
            cout << infile[i][0];
         } else if (infile[i].isInterpretation()) {
            if (strcmp(infile[i][0], "*-") == 0) {
               cout << "*-";
            } else if (pre.search(infile[i][0], "^\\*M\\d+/\\d+")) {
               // time signature 
               cout << infile[i][0];
            } else if (pre.search(infile[i][0], "^\\*met\\(")) {
               // metrical symbol
               cout << infile[i][0];
            } else if (pre.search(infile[i][0], "^\\*MM\\d")) {
               // tempo marker
               cout << infile[i][0];
            } else {
               cout << "*";
            }
         } else if (infile[i].isLocalComment()) {
            cout << "!";
         } else {
            active = isMatch(ss, measurestates[i]);
            if (active) {
               if (infile[i].getDuration() == 0.0) {
                  cout << ".";
               } else {
                  printLineDuration(infile, i);
                  cout << "r";
               }
            } else {
               // staff is not active in this measure, so add a
               // full measure of rests.
               if (measuredur[i] > 0) {
                  // this is the first data element in the measure
                  // place the rest here.
                  printDuration(measuredur[i]);
                  cout << "r";
               } else {
                  // not the first data element in measure so
                  // just place a dot.
                  cout << ".";
               }
            }
         }
      }
      cout << endl;
   }  
}



//////////////////////////////
//
// getMeasureStates -- return a list of staves which contain
//    data within the current measure.
//

void getMeasureStates(Array<Array<double> >& measurestates, 
      Array<Array<Array<double> > >& staves, Array<RationalNumber>& mdur,
      HumdrumFile& infile) {
   int i, ii, j, k;
   measurestates.setSize(mdur.getSize());
   for (i=0; i<measurestates.getSize(); i++) {
      measurestates[i].setSize(100);
      measurestates[i].setSize(0);
   }

   for (i=0; i<mdur.getSize(); i++) {
      if (mdur[i] == 0) {
         continue;
      }
      for (ii=i; ii<mdur.getSize(); ii++) {
         if (infile[ii].isMeasure()) {
            i = ii;
            break;
         }
         for (j=0; j<staves[ii].getSize(); j++) {
            for (k=0; k<staves[ii][j].getSize(); k++) {
               addStaffEntry(measurestates[i], staves[ii][j][k]);
            }
         }
      }
   }

   for (i=1; i<measurestates.getSize(); i++) {
      if (measurestates[i].getSize() > 0) {
         continue;
      }
      if (measurestates[i-1].getSize() > 0) {
         measurestates[i] = measurestates[i-1];
      }
   }

   if (debugQ) {
      cout << "STAVES BY MEASURE" << endl;
      for (i=0; i<mdur.getSize(); i++) {
         for (j=0; j<measurestates[i].getSize(); j++) {
            cout << measurestates[i][j] << " ";
         }
         cout << endl;
      }
      cout << "========================" << endl;
   }

}



//////////////////////////////
//
// addStaffEntry --
//

void addStaffEntry(Array & states, double value) {
   int i;
   for (i=0; i<states.getSize(); i++) {
      if (states[i] == value) {
         return;
      }
   }
   states.append(value);
}



//////////////////////////////
//
// getMeasureDurations --
//

void getMeasureDurations(Array<RationalNumber>& measuredur, 
      HumdrumFile& infile) {

   measuredur.setSize(infile.getNumLines());
   measuredur.setAll(0);
   RationalNumber rn;

   int i;
   int lastmi     = -1;
   int mi         = -1;
   int lastdatai  = -1;
   int datai      = -1;
   int datamark = 0;
   for (i=0; i<infile.getNumLines(); i++) {
      if (infile[i].isMeasure()) {
         lastmi = mi;
         mi = i;
         datamark = 1;
         continue;
      } else if (infile[i].isData() && (infile[i].getDuration() != 0.0)) {
         if (datamark) {
            datai = i;
            datamark = 0;
            if ((mi > 0) && (lastmi > 0)) {
               rn = infile[mi].getAbsBeatR() - infile[lastmi].getAbsBeatR();  
               if (lastdatai > 0) {
                  measuredur[lastdatai] = rn;
               }
            }
            lastdatai = datai;
         }
      }
   }

   // process the last measure
   if (datamark) {
      datai = i;
      datamark = 0;
      if ((mi > 0) && (lastmi > 0)) {
         rn = infile[mi].getAbsBeatR() - infile[lastmi].getAbsBeatR();  
         if (lastdatai > 0) {
            measuredur[lastdatai] = rn;
         }
      }
      lastdatai = datai;
   }

   if (debugQ) {
      cout << "MEASURE DURATIONS" << endl;
      for (i=0; i<infile.getNumLines(); i++) {
         cout << measuredur[i] << endl;
      }
      cout << "=======================" << endl;
   }
}



//////////////////////////////
//
// printLineDuration --
//

void printLineDuration(HumdrumFile& infile, int line) {
   printDuration(infile[line].getDurationR());
}



//////////////////////////////
//
// printDuration --
//

void printDuration(const RationalNumber& duration) {
   RationalNumber rn;
   rn = duration;
   rn /= 4;
   if (rn.getNumerator() == 1) {
      cout << rn.getDenominator();
   } else if (rn.getNumerator() == 3) {
      cout << rn.getDenominator()/2 << ".";
   } else {
      cout << rn.getNumerator() << "%" << rn.getDenominator();
   }
}



//////////////////////////////
//
// isMatch -- returns a match if ss is in staves list.
//

int isMatch(int ss, Array& list) {
   int i;
   for (i=0; i<list.getSize(); i++) {
      if (ss == (int)list[i]) {
         return 1;
      }
   }
   return 0;
}



//////////////////////////////
//
// printStaves --
//

void printStaves(Array > >& staves) {
   int i, j, k;
   for (i=0; i<staves.getSize(); i++) {
      for (j=0; j<staves[i].getSize(); j++) {
         if (staves[i][j].getSize() == 0) {
            cout << ".";
         } else {
            for (k=0; k<staves[i][j].getSize(); k++) {
               cout << staves[i][j][k];
               if (k < staves[i][j].getSize()-1) {
                  cout << " ";
               }
            }
         }
         if (j<staves[i].getSize()-1) {
            cout << "\t";
         }
      }
      cout << "\n";
   }
}



//////////////////////////////
//
// getStaves -- get a list of staff ownershipts for each spine cell.
//

void getStaves(Array > >& staves, HumdrumFile& infile) {
   staves.setSize(infile.getNumLines());
   int i, j;

   for (i=0; i<infile.getNumLines(); i++) {
      staves[i].setSize(infile[i].getFieldCount());
      staves[i].allowGrowth(0);
      for (j=0; j<staves[i].getSize(); j++) {
         staves[i][j].setSize(0);
      }
   }

   // keep track of the first line which contains a **staff marker in each spine
   Array<int> firststaff;
   firststaff.setSize(infile.getMaxTracks()+1);
   firststaff.setAll(-1);

   Array<Array<double> > states;
   states.setSize(infile.getMaxTracks()+1);

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

   int track;
   for (i=0; i<infile.getNumLines(); i++) {
      if (infile[i].isInterpretation()) {
         for (j=0; j<infile[i].getFieldCount(); j++) {
            track = infile[i].getPrimaryTrack(j);
            if (strncmp(infile[i][j], "*staff", strlen("*staff")) == 0) {
               if (firststaff[track] == -1) {
                  firststaff[track] = i;
               }
               setStaffAssignment(states[track], infile[i][j]);
            }
         }
      }
      if (infile[i].isBibliographic()) {
         continue;
      }
      if (infile[i].isGlobalComment()) {
         continue;
      }
      if (infile[i].isEmpty()) {
         continue;
      }
      for (j=0; j<infile[i].getFieldCount(); j++) {
         track = infile[i].getPrimaryTrack(j);
         staves[i][j] = states[track];
      }
   }

   // at this point the staff assignments should work backwards from the
   // first *staff marker upwards, so that the exclusive interpretations 
   // can be tied to a staff assignment.
   int ttrack = -1;
   int row = -1;
   int k;
   int line;
   for (track = 1; track < firststaff.getSize(); track++) {
      if (firststaff[track] < 0) {
         continue;
      }
      line = firststaff[track];
      // keep track of *staff entry
      ttrack = -1;
      row = -1;
      for (j=0; j<staves[line].getSize(); j++) {
         ttrack = infile[line].getPrimaryTrack(j);
         if (ttrack != track) {
            ttrack = -1;
            row = -1;
            continue;
         } else {
            row = j;
            break;
         }
      }
      if (ttrack < 0) {
         continue;
      }
      if (row < 0) {
         continue;
      }
      states[ttrack] = staves[line][row];
      for (i=line-1; i>=0; i--) {
         if (infile[i].isBibliographic()) {
            continue;
         }
         if (infile[i].isGlobalComment()) {
            continue;
         }
         if (infile[i].isEmpty()) {
            continue;
         }
         for (j=0; j<infile[i].getFieldCount(); j++) {
            k = infile[i].getPrimaryTrack(j);
            if (k != ttrack) {
               continue;
            }
            staves[i][j] = states[k];
         }
      }
   }

}



//////////////////////////////
//
// setStaffAssignment --
//

void setStaffAssignment(Array& states, const char* string) {
   PerlRegularExpression pre;
   Array<Array<char> > pieces;
   pre.getTokens(pieces, "[^\\dX.]+", string);
   
   if (pieces.getSize() == 0) {
      states.setSize(0);
      states.allowGrowth(0);
      return;
   }
   int i;
   if (strncmp(pieces[0].getBase(), "X", 1) == 0) {
      states.setSize(0);
      states.allowGrowth(0);
      return;
   }
   
   states.setSize(pieces.getSize());
   int number;
   for (i=0; i<pieces.getSize(); i++) {
      // ignoring decimal part for now
      number = atoi(pieces[i].getBase());
      states[i] = number;
   }
}



//////////////////////////////
//
// fillFieldData --
//

void fillFieldData(Array<int>& field, Array<int>& subfield, Array<int>& model,
      const char* fieldstring, HumdrumFile& infile) {

   int maxtrack = infile.getMaxTracks();

   field.setSize(maxtrack);
   field.setGrowth(maxtrack);
   field.setSize(0);
   subfield.setSize(maxtrack);
   subfield.setGrowth(maxtrack);
   subfield.setSize(0);
   model.setSize(maxtrack);
   model.setGrowth(maxtrack);
   model.setSize(0);

   PerlRegularExpression pre;
   Array<char> buffer;
   buffer.setSize(strlen(fieldstring)+1);
   strcpy(buffer.getBase(), fieldstring);
   pre.sar(buffer, "\\s", "", "gs");
   int start = 0;
   int value = 0;
   value = pre.search(buffer.getBase(), "^([^,]+,?)");
   while (value != 0) {
      start += value - 1;
      start += strlen(pre.getSubmatch(1));
      processFieldEntry(field, subfield, model, pre.getSubmatch(), infile);
      value = pre.search(buffer.getBase() + start, "^([^,]+,?)");
   }
}



//////////////////////////////
//
// processFieldEntry -- 
//   3-6 expands to 3 4 5 6
//   $   expands to maximum spine track
//   $-1 expands to maximum spine track minus 1, etc.
//

void processFieldEntry(Array<int>& field, Array<int>& subfield, 
      Array<int>& model, const char* string, HumdrumFile& infile) {

   int maxtrack = infile.getMaxTracks();
   int modletter;
   int subletter;

   PerlRegularExpression pre;
   Array<char> buffer;
   buffer.setSize(strlen(string)+1);
   strcpy(buffer.getBase(), string);

   // remove any comma left at end of input string (or anywhere else)
   pre.sar(buffer, ",", "", "g");

   // first remove $ symbols and replace with the correct values
   removeDollarsFromString(buffer, infile.getMaxTracks());

   int zero = 0;
   if (pre.search(buffer.getBase(), "^(\\d+)-(\\d+)$")) {
      int firstone = strtol(pre.getSubmatch(1), NULL, 10);
      int lastone  = strtol(pre.getSubmatch(2), NULL, 10);

      if ((firstone < 1) && (firstone != 0)) {
         cerr << "Error: range token: \"" << string << "\"" 
              << " contains too small a number at start: " << firstone << endl;
         cerr << "Minimum number allowed is " << 1 << endl;
         exit(1);
      }
      if ((lastone < 1) && (lastone != 0)) {
         cerr << "Error: range token: \"" << string << "\"" 
              << " contains too small a number at end: " << lastone << endl;
         cerr << "Minimum number allowed is " << 1 << endl;
         exit(1);
      }
      if (firstone > maxtrack) {
         cerr << "Error: range token: \"" << string << "\"" 
              << " contains number too large at start: " << firstone << endl;
         cerr << "Maximum number allowed is " << maxtrack << endl;
         exit(1);
      }
      if (lastone > maxtrack) {
         cerr << "Error: range token: \"" << string << "\"" 
              << " contains number too large at end: " << lastone << endl;
         cerr << "Maximum number allowed is " << maxtrack << endl;
         exit(1);
      }

      int i;
      if (firstone > lastone) {
         for (i=firstone; i>=lastone; i--) {
            field.append(i);
            subfield.append(zero);
            model.append(zero);
         }
      } else {
         for (i=firstone; i<=lastone; i++) {
            field.append(i);
            subfield.append(zero);
            model.append(zero);
         }
      }
   } else if (pre.search(buffer.getBase(), "^(\\d+)([a-z]*)")) {
      int value = strtol(pre.getSubmatch(1), NULL, 10);
      modletter = 0;
      subletter = 0;
      if (strchr(pre.getSubmatch(2), 'a') != NULL) {
         subletter = 'a';
      }
      if (strchr(pre.getSubmatch(), 'b') != NULL) {
         subletter = 'b';
      }
      if (strchr(pre.getSubmatch(), 'c') != NULL) {
         subletter = 'c';
      }
      if (strchr(pre.getSubmatch(), 'd') != NULL) {
         modletter = 'd';
      }
      if (strchr(pre.getSubmatch(), 'n') != NULL) {
         modletter = 'n';
      }
      if (strchr(pre.getSubmatch(), 'r') != NULL) {
         modletter = 'r';
      }

      if ((value < 1) && (value != 0)) {
         cerr << "Error: range token: \"" << string << "\"" 
              << " contains too small a number at end: " << value << endl;
         cerr << "Minimum number allowed is " << 1 << endl;
         exit(1);
      }
      if (value > maxtrack) {
         cerr << "Error: range token: \"" << string << "\"" 
              << " contains number too large at start: " << value << endl;
         cerr << "Maximum number allowed is " << maxtrack << endl;
         exit(1);
      }
      field.append(value);
      if (value == 0) {
         subfield.append(zero);
         model.append(zero);
      } else {
         subfield.append(subletter);
         model.append(modletter);
      }
   }
}



//////////////////////////////
//
// removeDollarsFromString -- substitute $ sign for maximum track count.
//

void removeDollarsFromString(Array& buffer, int maxtrack) {
   PerlRegularExpression pre;
   char buf2[128] = {0};
   int value2;

   if (pre.search(buffer.getBase(), "\\$$")) {
      sprintf(buf2, "%d", maxtrack);
      pre.sar(buffer, "\\$$", buf2);
   }

   if (pre.search(buffer.getBase(), "\\$(?![\\d-])")) {
      // don't know how this case could happen, however...
      sprintf(buf2, "%d", maxtrack);
      pre.sar(buffer, "\\$(?![\\d-])", buf2, "g");
   }

   if (pre.search(buffer.getBase(), "\\$0")) {
      // replace $0 with maxtrack (used for reverse orderings)
      sprintf(buf2, "%d", maxtrack);
      pre.sar(buffer, "\\$0", buf2, "g");
   }

   while (pre.search(buffer.getBase(), "\\$(-?\\d+)")) {
      value2 = maxtrack - (int)fabs(strtol(pre.getSubmatch(1), NULL, 10));
      sprintf(buf2, "%d", value2);
      pre.sar(buffer, "\\$-?\\d+", buf2);
   }

}



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

void checkOptions(Options& opts, int argc, char* argv[]) {
   opts.define("s|staves=s:", "List of staves to extract");
   opts.define("debug=b",     "Determine bad input line num");
   opts.define("author=b",    "Author of program"); 
   opts.define("version=b",   "Compilation info");
   opts.define("example=b",   "Example usages"); 
   opts.define("h|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, Oct 2011" << endl;
      exit(0);
   } else if (opts.getBoolean("version")) {
      cout << argv[0] << ", version: 20 Oct 2011" << 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);
   }


   if (!opts.getBoolean("staves")) {
      cerr << "Error: you must specify the -s option" << endl;
      exit(1);
   }
   fieldstring = opts.getString("staves");

   debugQ = opts.getBoolean("debug");

}



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

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



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

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



// md5sum: 93a34f9674e91ccff8aa12b5f975c818 staff.cpp [20111105]