//
// Programmer:    Craig Stuart Sapp <craig@ccrma.stanford.edu>
// Creation Date: Sat Jun  9 14:48:41 PDT 2001
// Last Modified: Mon Apr 18 08:58:17 PDT 2011
// Filename:      ...museinfo/examples/all/parallel.cpp
// Web Address:   http://sig.sapp.org/examples/museinfo/humdrum/parallel.cpp
// Syntax:        C++; museinfo
//
// Description:   calculates the minimum timebase which is the least common
//                multiple of all rhythms in the file.
//
// Todo: some things to clean up in rhythmic representation due to conversions
// from ints to RationalNumber class for rhythms/durations...

#include "humdrum.h"
#include <stdlib.h>
#include "PerlRegularExpression.h"

class NoteNode {
   public:
      int b40;      // base-40 pitch number or 0 if a rest, negative if tied0.
      int line;     // line number in original score of note
      int spine;    // spine number in original score of note
      int lastint;  // interval to last note (or 0 if no last or last is rest)
      int nextint;  // interval to next note (or 0 if no next or last is rest)
      int serial;   // notes serial number
      int measure;  // measure number of note
      NoteNode(void) { clear(); }
      void clear(void) { serial = b40 = 0; line = spine = -1; 
                         lastint = nextint = 0; }
};

#define REST 0
#define RESTINT -1000000

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

// function declarations
void      checkOptions(Options& opts, int argc, char* argv[]);
void      example(void);
void      usage(const char* command);
void      processFile(HumdrumFile& infile, const char* filename);
void      getKernTracks(Array<int>& ktracks, HumdrumFile& infile);
void      calculateMelodicIntervals(Array<Array<NoteNode> >& notes);
void      markParallels(Array<Array<NoteNode> >& notes, 
                                HumdrumFile& infile, const char* filename, 
                                Array<Array<char> >& names, 
                                Array<int> reverselookup);
void      addMark(HumdrumFile& infile, int line, int spine, 
                                char marker);
void      markParallel(HumdrumFile& infile, 
                                Array<Array<NoteNode> >& notes, 
                                int hint, int i, int j, int k);
int       validateInterval(Array<Array<NoteNode> >& notes, 
                                int i, int j, int k);

// global variables
Options   options;             // database for command-line arguments
int       debugQ = 0;          // used with --debug
int       rawQ   = 0;          // used with -r option
int       locQ   = 0;          // used with -l option
int       raw2Q  = 0;          // used with --r2 option
int       fileQ  = 0;          // used with -f option



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


int main(int argc, char** argv) {
   checkOptions(options, argc, argv);
   HumdrumFile infile;

   int i;
   // figure out the number of input files to process
   int numinputs = options.getArgCount();
   const char* filename = "";

   PerlRegularExpression pre;

   if (locQ) {
      // print start of data
      if (fileQ) {
         cout << "**file\t";
      }
      cout << "**bar\t";
      cout << "**description\t";
      cout << "**voice1\t";
      cout << "**voice2";
      cout << "\n";
   }

   for (i=0; i<numinputs || i==0; i++) {
      infile.clear();

      // if no command-line arguments read data file from standard input
      if (numinputs < 1) {
         infile.read(cin);
         filename = "STDIN";
      } else {
         filename = options.getArg(i+1);
         infile.read(filename);
         pre.search(filename, "([^/.]+)(\\.[^\\.]+)$", "");
         filename = pre.getSubmatch(1);
      }
      processFile(infile, filename);
   }
 
   if (locQ) {
      if (fileQ) {
         cout << "*-\t";
      }
      cout << "*-\t*-\t*-\t*-\n";
   }



}



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



//////////////////////////////
//
// getNames -- get the names of each column if they have one.
//

void getNames(Array<Array<char> >& names, Array<int>& reverselookup, 
      HumdrumFile& infile) {

   names.setSize(reverselookup.getSize()-1);
   names.allowGrowth(0);
   char buffer[1024] = {0};
   int value;
   PerlRegularExpression pre;
   int i;
   int j;
   int track;
 
   for (i=0; i<names.getSize(); i++) {
      value = reverselookup.getSize() - i;
      sprintf(buffer, "%d", value);
      names[i].setSize(strlen(buffer)+1);
      strcpy(names[i].getBase(), buffer);
   }

   for (i=0; i<infile.getNumLines(); i++) {
      if (infile[i].isData()) {
         // stop looking for instrument name after the first data line
         break;
      }
      if (!infile[i].isInterpretation()) {
         continue;
      }
      for (j=0; j<infile[i].getFieldCount(); j++) {
         if (!infile[i].isExInterp(j, "**kern")) {
            continue;
         }
         if (pre.search(infile[i][j], "^\\*I\"(.*)", "")) {
            track = infile[i].getPrimaryTrack(j);
            strcpy(buffer, pre.getSubmatch(1));
            names[reverselookup[track]].setSize(strlen(buffer)+1);
            strcpy(names[reverselookup[track]].getBase(), buffer);
         }
      }
   }

   if (debugQ) {
      for (i=0; i<names.getSize(); i++) {
         cout << i << ":\t" <<  names[i] << endl;
      }
   }

}



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

void processFile(HumdrumFile& infile, const char* filename) {
   int i, j;

   Array<Array<NoteNode> > notes;
   Array<Array<char> >     names;
   Array<int>              ktracks;
   Array<int>              reverselookup;

   int ii;
   int jj;

   getKernTracks(ktracks, infile);
   notes.setSize(ktracks.getSize());
   reverselookup.setSize(infile.getMaxTracks()+1);
   reverselookup.setAll(-1);


   for (i=0; i<ktracks.getSize(); i++) {
      reverselookup[ktracks[i]] = i;
      notes[i].setSize(infile.getNumLines());
      notes[i].setSize(0);
   }

   getNames(names, reverselookup, infile);


   PerlRegularExpression pre;

   Array<NoteNode> current;
   current.setSize(ktracks.getSize());
   int sign;
   int track;
   int index;

   int snum = 0;
   int measurenumber = 0;
   
   for (i=0; i<infile.getNumLines(); i++) {
      if (debugQ) {
         cout << "PROCESSING LINE: " << i << "\t" << infile[i] << endl;
      }
      if (infile[i].isMeasure()) {
         if (pre.search(infile[i][0], "(\\d+)", "")) {
            measurenumber = atoi(pre.getSubmatch(1));
         }
      }
      for (j=0; j<current.getSize(); j++) {
         current[j].clear();
         current[j].measure = measurenumber;
      }

      if (infile[i].isMeasure() && (strstr(infile[i][0], "||") != NULL)) {
         // double barline (terminal for Josquin project), so add a row
         // of rests to prevent parallel interval identification between
         // adjacent notes in different sections.
         for (j=0; j<notes.getSize(); j++) {
            notes[j].append(current[j]);
         }
      }
      if (!infile[i].isData()) {
         continue;
      }
      for (j=0; j<infile[i].getFieldCount(); j++) {
         sign = 1;
         if (!infile[i].isExInterp(j, "**kern")) {
            continue;
         }
         track = infile[i].getPrimaryTrack(j);
         index = reverselookup[track];
         current[index].line  = i;
         current[index].spine = j;
         if (strcmp(infile[i][j], ".") == 0) {
            sign = -1;
            ii = infile[i].getDotLine(j);
            jj = infile[i].getDotSpine(j);
         } else {
            ii = i;
            jj = j;
         }
         if (strchr(infile[ii][jj], 'r') != NULL) {
            current[index].b40 = 0;
            current[index].serial = ++snum;
            continue;
         }
         if (strcmp(infile[ii][jj], ".") == 0) {
            current[index].b40 = 0;
            current[index].serial = snum;
         }
         current[index].b40 = Convert::kernToBase40(infile[ii][jj]);
         if (strchr(infile[ii][jj], '_') != NULL) {
            sign = -1;
            current[index].serial = snum;
         }
         if (strchr(infile[ii][jj], ']') != NULL) {
            sign = -1;
            current[index].serial = snum;
         }
         current[index].b40 *= sign;
         if (sign > 0) {
            current[index].serial = ++snum;
         }
      }
      for (j=0; j<notes.getSize(); j++) {
         notes[j].append(current[j]);
      }
   }

   if (rawQ) {
      for (i=0; i<notes[0].getSize(); i++) {
         for (j=0; j<notes.getSize(); j++) {
            cout << notes[j][i].b40;
            if (j < notes.getSize()-1) {
               cout << "\t";
            }
         }
         cout << endl;
      }
      exit(0);
   }

   calculateMelodicIntervals(notes);

   if (raw2Q) {
      for (i=0; i<notes[0].getSize(); i++) {
         for (j=0; j<notes.getSize(); j++) {
            cout << notes[j][i].b40;
            cout << "(";
            cout << notes[j][i].lastint;
            cout << ";";
            cout << notes[j][i].nextint;
            cout << ")";
            if (j < notes.getSize()-1) {
               cout << "\t";
            }
         }
         cout << endl;
      }
      exit(0);
   }

   markParallels(notes, infile, filename, names, reverselookup);

}



//////////////////////////////
//
// markParallels -- search for parts which are parallel fifth or octaves 
//    
// 0(REST;REST)		134(-5;REST)	162(11;-11)	174(6;28)
// 111(REST;-6)		-134(-5;REST)	-162(11;-11)	202(28;-5)
// -111(REST;-6)	-134(-5;REST)	151(-11;6)	-202(28;-5)
// 105(-6;6)		-134(-5;REST)	157(6;5)	197(-5;-6)
// 111(6;23)		-134(-5;REST)	162(5;-5)	191(-6;-6)
// 134(23;REST)		-134(-5;REST)	157(-5;REST)	185(-6;REST)
//

void markParallels(Array<Array<NoteNode> >& notes, HumdrumFile& infile,
      const char* filename, Array<Array<char> >& names, 
      Array<int> reverselookup) {
   int i;
   int j;
   int k;
   int hint;
   int ii;
   int jj;
   int track;

   int hasoctavemarks = 0;
   int has5thmarks    = 0;

   for (i=0; i<notes.getSize(); i++) {
      for (j=0; j<notes[i].getSize(); j++) {
         if (notes[i][j].b40 == REST) {
            // ignore rests
            continue;
         }
         if (notes[i][j].nextint == RESTINT) {
            // ignore rest intervals
            continue;
         }
         if (notes[i][j].nextint == 0) {
            // ignore repeated notes
            continue;
         }
         for (k=i+1; k<notes.getSize(); k++) {
            if (notes[i][k].nextint == RESTINT) {
               // ignore rest intervals
               continue;
            }
            if (notes[i][k].b40 == 0) {
               // ignore rests
               continue;
            }
            // disallow interval check from elided notes for now
            //if (notes[i][j].b40 < 0) {
            //   continue;
            //}
            //if (notes[k][j].b40 < 0) {
            //   continue;
            //}
            
            if ((notes[k][j].b40 < 0) && (notes[i][j].b40 < 0)) {
               continue;
            }
            
            if (abs(notes[k][j].b40) > abs(notes[i][j].b40)) {
               hint = (abs(notes[k][j].b40) - abs(notes[i][j].b40) + 400) % 40;
            } else {
               hint = (abs(notes[i][j].b40) - abs(notes[k][j].b40) + 400) % 40;
            }
            if (!((hint == 0) || (hint == 23))) {
               // not a fifth or an octave
               continue;
            }
            if (notes[k][j].nextint != notes[i][j].nextint) {
               // note are not creating a parallel motion
               continue;
            }

            //cout << "Parallel " << hint << endl;
            if (validateInterval(notes, i, j, k)) {

               if (locQ) {

                  if (hint == 0) {
                     if (fileQ) {
                        cout << filename;
                     }
                     cout << "\t" << notes[i][j].measure;
                     cout << "\t" << "parallel octave/unison";
                     cout << "\t";
                     ii = notes[i][j].line;
                     jj = notes[i][j].spine;
                     track = infile[ii].getPrimaryTrack(jj);
                     cout << names[reverselookup[track]];
                     cout << "\t";
                     ii = notes[k][j].line;
                     jj = notes[k][j].spine;
                     track = infile[ii].getPrimaryTrack(jj);
                     cout << names[reverselookup[track]];
                     cout << endl;

                  } else if (hint == 23) {
                     if (fileQ) {
                        cout << filename;
                     }
                     cout << "\t" << notes[i][j].measure;
                     cout << "\t" << "parallel fifth";
                     cout << "\t";
                     ii = notes[i][j].line;
                     jj = notes[i][j].spine;
                     track = infile[ii].getPrimaryTrack(jj);
                     cout << names[reverselookup[track]];
                     cout << "\t";
                     ii = notes[k][j].line;
                     jj = notes[k][j].spine;
                     track = infile[ii].getPrimaryTrack(jj);
                     cout << names[reverselookup[track]];
                     cout << endl;

                  }


               }

               markParallel(infile, notes, hint, i, j, k);
               if (hint == 0)  { hasoctavemarks = 1; }
               if (hint == 23) { has5thmarks    = 1; }
            }
         }
      }
   }

/*   // mark the ending notes in the parallel motion
   for (i=0; i<notes.getSize(); i++) {
      for (j=0; j<notes[i].getSize(); j++) {
         if (notes[i][j].b40 == REST) {
            // ignore rests
            continue;
         }
         if (notes[i][j].lastint == RESTINT) {
            // ignore rest intervals
            continue;
         }
         if (notes[i][j].lastint == 0) {
            // ignore repeated notes
            continue;
         }
         for (k=i+1; k<notes.getSize(); k++) {
            if (notes[i][k].lastint == RESTINT) {
               // ignore rest intervals
               continue;
            }
            if (notes[i][k].b40 == 0) {
               // ignore rests
               continue;
            }
            // disallow interval check from elided notes for now
            if (notes[i][j].b40 < 0) {
               continue;
            }
            if (notes[k][j].b40 < 0) {
               continue;
            }

            if (abs(notes[k][j].b40) > abs(notes[i][j].b40)) {
               hint = (abs(notes[k][j].b40) - abs(notes[i][j].b40) + 400) % 40;
            } else {
               hint = (abs(notes[i][j].b40) - abs(notes[k][j].b40) + 400) % 40;
            }

            if (!((hint == 0) || (hint == 23))) {
               // not a fifth or an octave
               continue;
            }
            if (notes[k][j].lastint != notes[i][j].lastint) {
               // notes are not creating a parallel motion
               continue;
            }
            // cout << "Parallel2 " << hint << endl;
            //if (validateInterval(notes, i, j, k)) {
               markParallel(infile, notes, hint, i, j, k);
               if (hint == 0)  { hasoctavemarks = 1; }
               if (hint == 23) { has5thmarks    = 1; }
            //}
         }
      }
   }
*/

   if (!locQ) {
      cout << infile;
      if (hasoctavemarks) {
         cout << "!!!RDF**kern: < = marked note, color=\"#00ff00\", ";
         cout << "parallel octave" << endl;
      }
      if (has5thmarks) {
         cout << "!!!RDF**kern: > = marked note, color=\"#ff0000\", ";
         cout << "parallel fifth" << endl;
      }
   }

}



//////////////////////////////
//
// validateInterval -- Currently only allow intervals where both notes
// change at the same time.
//

int validateInterval(Array >& notes, int i, int j, int k) {
   int nij = -1;
   int nkj = -1;

   int m;


   for (m=j+1; m<notes[i].getSize(); m++) {
      if (notes[i][m].b40 >= 0) {
         nij = m;
         break;
      }
   }

   for (m=j+1; m<notes[k].getSize(); m++) {
      if (notes[k][m].b40 >= 0) {
         nkj = m;
         break;
      }
   }

   if (nij == nkj) {
      return 1;
   }
   if (nij < 0) {   // strange case if it occurs, but just mark as good
      return 1;
   }
   if (nkj < 0) {   // strage case if it occurs, but just mark as good
      return 1;
   }

   return 0;
}




//////////////////////////////
//
// markParallel --
//

void markParallel(HumdrumFile& infile, Array<Array<NoteNode> >& notes, 
      int hint, int i, int j, int k) {

   char marker = '<';  // parallel octaves/unisons
   if (hint == 23) {
      marker = '>';    // parallel fifths
   }

   int ii;
   int jj;


   // mark first note in parallel motion
   ii = notes[i][j].line;
   jj = notes[i][j].spine;

   if (strcmp(infile[ii][jj], ".") == 0) {
      int ti;
      int tj;
      ti = infile[ii].getDotLine(jj);
      tj = infile[ii].getDotSpine(jj);
      ii = ti;
      jj = tj;
   }

   if (strcmp(infile[ii][jj], ".") != 0) {
      // if (strchr(infile[ii][jj], marker) == NULL) {
         addMark(infile, ii, jj, marker);
      // }
   }

   // mark second note in parallel motion
   ii = notes[k][j].line;
   jj = notes[k][j].spine;

   if (strcmp(infile[ii][jj], ".") == 0) {
      int ti;
      int tj;
      ti = infile[ii].getDotLine(jj);
      tj = infile[ii].getDotSpine(jj);
      ii = ti;
      jj = tj;
   }

   if (strcmp(infile[ii][jj], ".") != 0) {
      // if (strchr(infile[ii][jj], marker) == NULL) {
         addMark(infile, ii, jj, marker);
      // }
   }

   int m;

   // mark third note in parallel motion
   for (m=j+1; m<notes[i].getSize(); m++) {
      if (notes[i][m].b40 < 0) {
         continue;
      }
      ii = notes[i][m].line;
      jj = notes[i][m].spine;
      if (strcmp(infile[ii][jj], ".") == 0) {
         int ti;
         int tj;
         ti = infile[ii].getDotLine(jj);
         tj = infile[ii].getDotSpine(jj);
         ii = ti;
         jj = tj;
      }
      if (strcmp(infile[ii][jj], ".") != 0) {
         // if (strchr(infile[ii][jj], marker) == NULL) {
            addMark(infile, ii, jj, marker);
         // }
      }
      break;
   }

   // mark third note in parallel motion
   for (m=j+1; m<notes[k].getSize(); m++) {
      if (notes[i][m].b40 < 0) {
         continue;
      }
      ii = notes[k][m].line;
      jj = notes[k][m].spine;
      if (strcmp(infile[ii][jj], ".") == 0) {
         int ti;
         int tj;
         ti = infile[ii].getDotLine(jj);
         tj = infile[ii].getDotSpine(jj);
         ii = ti;
         jj = tj;
      }
      if (strcmp(infile[ii][jj], ".") != 0) {
         // if (strchr(infile[ii][jj], marker) == NULL) {
            addMark(infile, ii, jj, marker);
         // }
      }
      break;
   }

}



//////////////////////////////
//
// addMark --
//

void addMark(HumdrumFile& infile, int line, int spine, char marker) {
   char buffer[1024]; 
   char mstring[2] = {0};
   mstring[0] = marker;
   strcpy(buffer, infile[line][spine]);
   strcat(buffer, mstring);
   infile[line].changeField(spine, buffer);
}



//////////////////////////////
//
// calculateMelodicIntervals --
//

void calculateMelodicIntervals(Array >& notes) {
   int i, j;
   int lastint = RESTINT;
   int lastnote = 0;

   // caluculate the intervals to previous notes:
   for (i=0; i<notes.getSize(); i++) {
      lastint = RESTINT;
      lastnote = 0;
      for (j=0; j<notes[i].getSize(); j++) {
         if (notes[i][j].b40 < 0) {
            // continuing note, just repeat previously calculate interval
            notes[i][j].lastint = lastint;
            continue;
         }
         if (notes[i][j].b40 == 0) {
            // current note is a rest, to make the interval to the last
            // note a RESTINT.
            lastnote = 0;
            notes[i][j].lastint = RESTINT;
            lastint = RESTINT;
            continue;
         }
         if (lastnote == 0) {
            // if the last note was a rest, then the interval to it is a RESTINT
            lastnote = abs(notes[i][j].b40); // update last note
            notes[i][j].lastint = RESTINT;
            lastint = RESTINT;
            continue;
         }
         // have a note attack which does note follow a rest
         lastint = notes[i][j].b40 - lastnote;
         lastnote = notes[i][j].b40;
         notes[i][j].lastint = lastint;
      }
   }


   // caluculate the intervales in the other direction
   //128(-6)	-174(0)		-185(5)		-197(-5)
   //122(-6)	-174(0)		-185(5)		191(-6)
   //128(6)	168(-6)		180(-5)		-191(-6)
   //-128(6)	162(-6)		-180(-5)	-191(-6)
   //105(-23)	157(-5)		168(-12)	185(-6)
   
   int nextint = RESTINT;
   int nextnote = 0;
   // caluculate the intervals to next notes:
   for (i=0; i<notes.getSize(); i++) {
      nextint = RESTINT;
      nextnote = 0;
      for (j=notes[i].getSize()-1; j>=0; j--) {
         notes[i][j].nextint = nextint;
         if (notes[i][j].b40 >= 0) {
            nextint = notes[i][j].lastint;
         }
      }
   }

}




//////////////////////////////
//
// getKernTracks -- return a list of track number for **kern spines.
//

void getKernTracks(Array& ktracks, HumdrumFile& infile) {
   int i, j;
   ktracks.setSize(infile.getMaxTracks());
   ktracks.setSize(0);
   int track;
   for (i=0; i<infile.getNumLines(); i++) {
      if (!infile[i].isInterpretation()) {
         continue;
      }
      for (j=0; j<infile[i].getFieldCount(); j++) {
         if (infile[i].isExInterp(j, "**kern")) {
            track = infile[i].getPrimaryTrack(j);
            ktracks.append(track);
         }
      }
      break;
   }
}



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

void checkOptions(Options& opts, int argc, char* argv[]) {
   opts.define("f|file=b", "display filenames with -l option");
   opts.define("l|location|loc=b", "print locations of parallels");
   opts.define("r|raw=b", "print raw data before analysis is done");
   opts.define("raw2|r2=b", "print raw data 2 before analysis is done");
   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, April 2011" << endl;
      exit(0);
   } else if (opts.getBoolean("version")) {
      cout << argv[0] << ", version: 22 April 2010" << 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");
   rawQ   = opts.getBoolean("raw");
   locQ   = opts.getBoolean("location");
   raw2Q  = opts.getBoolean("raw2");
   fileQ  = opts.getBoolean("file");
}



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

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



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

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


// md5sum: aa48d9a604d24a2651e6363a9799c018 parallel.cpp [20110427]