//
// Programmer:    Craig Stuart Sapp <craig@ccrma.stanford.edu>
// Creation Date: Mon Apr 15 20:05:01 PDT 2002
// Last Modified: Mon Apr 15 20:05:04 PDT 2002
// Last Modified: Sun Mar 20 23:37:41 PST 2005 Added *rit and *accel
// Last Modified: Sat Oct  8 22:14:11 PDT 2005 Added time interpolation
// Last Modified: Wed Mar 29 20:19:00 PST 2006 Fixed various errors for kglee
// Last Modified: Tue Apr  9 08:07:21 PDT 2013 Enabled multiple segment input
// Filename:      ...sig/examples/all/gettime.cpp
// Web Address:   http://sig.sapp.org/examples/museinfo/humdrum/gettime.cpp
// Syntax:        C++; museinfo
//
// Description:   Add a spine indicating the time of performance for
//                a given line of music.
// 

#include "humdrum.h"
#include <string.h>
#include <stdio.h>
#include <math.h>

// function declarations
void   checkOptions(Options& opts, int argc, char* argv[]);
void   example(void);
void   printAnalysis(HumdrumFile& infile, Array<double>& timings,
                                 Array<double>& tempo);
void   analyzeTiming(HumdrumFile& infile, Array<double>& timings,
                                 Array<double>& tempo);
void   usage(const char* command);
void   interpolateGeometric(HumdrumFile& infile, Array<double>& tempo, 
                                 int startindex, int stopindex);
int    getNextTempoIndex(HumdrumFile& infile, int startindex);
void   printtime(const char* filename, double totaldur);
void   doLinearInterpolation(HumdrumFile& infile);
void   interpolateTimings(Array<double>& timings, HumdrumFile& infile,
                                 int startindex, int endindex);
void   fixendingtimes(Array<double>& timings, HumdrumFile& infile);

// global variables
Options      options;            // database for command-line arguments
int          appendQ    = 0;     // used with -a option
int          prependQ   = 0;     // used with -p option
int          debugQ     = 0;     // used with --debug option
double       offset     = 0.0;   // used with -o option
int          style      = 's';   // used with -m option
double       dtempo     = 60.0;  // used with -d option
int          tempoQ     = 0;     // used with -t option
int          changeQ    = 1;     // used with -C option
int          roundQ     = 1;     // used with -R option
int          totalQ     = 0;     // used with --total option
int          interpQ    = 0;     // used with -i option

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

int main(int argc, char* argv[]) {
   HumdrumFileSet infiles;
   Array<double>  timings;
   Array<double>  tempo;

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

   // figure out the number of input files to process
   int numinputs = options.getArgCount();

   double totaldur = 0.0;

   int i;
   if (numinputs < 1) {
      infiles.read(cin);
   } else {
      for (i=0; i<numinputs; i++) {
         infiles.readAppend(options.getArg(i+1));
      }
   }

   for (i=0; i<infiles.getCount(); i++) {
      if (interpQ) {
         doLinearInterpolation(infiles[i]);
      } else {
         analyzeTiming(infiles[i], timings, tempo);
         if (totalQ) {
            totaldur += timings[timings.getSize()-1];
            if (numinputs > 1) {
               printtime(infiles[i].getFilename(), 
                     timings[timings.getSize()-1]);
            }
         } else {
            printAnalysis(infiles[i], timings, tempo);
         }
      }
   }

   if (totalQ) {
      printtime("", totaldur);
   }

   return 0;
}


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



///////////////////////////////
//
// doLinearInterpolation -- interpolate the timings values in the
//     score based on timing values which are currently present.
//
//

#define INVALIDTIME -100000
#define UNKNOWNTIME -1
// #define UNKNOWNTIME 0

void doLinearInterpolation(HumdrumFile& infile) {
   infile.analyzeRhythm("4");

   // extract the timing data using -1 for undefined

   Array<double> timings;
   Array<double> absbeat;

   timings.setSize(infile.getNumLines());
   timings.setAll(INVALIDTIME);
   double atime;

   int i, j;
   for (i=0; i<infile.getNumLines(); i++) {
      if (infile[i].getType() != E_humrec_data) {
         continue;
      }
      for (j=0; j<infile[i].getFieldCount(); j++) {
         if (strcmp("**time", infile[i].getExInterp(j)) == 0) {
            if (strcmp(infile[i][j], ".") == 0) {
               timings[i] = UNKNOWNTIME;
            } else {
               sscanf(infile[i][j], "%lf", &atime);
               timings[i] = atime;
            }
            break;
         }

      }

   }

   int lasttimeindex = -1;
   for (i=0; i<timings.getSize(); i++) {
      if (timings[i] < 0) {
         continue;
      }

      if (lasttimeindex < 0) {
         lasttimeindex = i;
         continue;
      }

      interpolateTimings(timings, infile, lasttimeindex, i);

      lasttimeindex = i;

   }

   fixendingtimes(timings, infile);

   // now insert the interpolated timings back into the score.
   infile.printNonemptySegmentLabel(cout);
   int tfound = 0;
   for (i=0; i<infile.getNumLines(); i++) {
      if (infile[i].getType() == E_humrec_data) {
         tfound = 0;
         for (j=0; j<infile[i].getFieldCount(); j++) {
            if ((tfound == 0) && 
                (strcmp(infile[i].getExInterp(j), "**time") == 0)) {
               tfound = 1;
               if (timings[i] < 0) { 
                  cout << "0.0";
               } else {
                  cout << timings[i];
               }
            } else {
               cout << infile[i][j];
            }
            if (j < infile[i].getFieldCount() - 1) {
               cout << "\t";
            }
         }
         cout << "\n";
      } else {
         cout << infile[i] << "\n";
      }
   }

}



//////////////////////////////
//
// fixendingtimes --
//

void fixendingtimes(Array& timings, HumdrumFile& infile) {
   int i;
   int lastdatai = -1;
   int lastlastdatai = -1;
   for (i=timings.getSize()-1; i>=0; i--) {
      if ((lastdatai == -1) && (timings[i] >= 0)) {
         lastdatai = i;
      } else if ((lastlastdatai == -1) && (timings[i] >= 0)) {
         lastlastdatai = i;
         break;
      }
   }

   if ((lastdatai < 0) || (lastlastdatai < 0)) {
      return;
   }

   double beatincrement = (timings[lastdatai] - timings[lastlastdatai]) / 
         (infile[lastdatai].getAbsBeat() - infile[lastlastdatai].getAbsBeat());
   double beatdiff = 0.0;


   for (i=lastdatai+1; i<timings.getSize(); i++) {
      if (timings[i] == UNKNOWNTIME) {
         beatdiff = infile[i].getAbsBeat() - infile[lastdatai].getAbsBeat();
         timings[i] = timings[lastdatai] + beatdiff * beatincrement;
      }
   }

}




//////////////////////////////
//
// interpolateTimings -- do the actual interpolation work.
//

void interpolateTimings(Array<double>& timings, HumdrumFile& infile,
      int startindex, int endindex) {

   int i;
   double beatwidth = infile[endindex].getAbsBeat() - 
                            infile[startindex].getAbsBeat();
   double timewidth = timings[endindex] - timings[startindex];
   double lbeat = 0.0;

   for (i=startindex+1; i<endindex; i++) {
      if (timings[i] == UNKNOWNTIME) {
         lbeat = infile[i].getAbsBeat() - infile[startindex].getAbsBeat();
         timings[i] = timewidth / beatwidth * lbeat + timings[startindex];
         if (timings[i] < timings[i-1]) {
            cerr << "Error new timing is less than old time" << endl;
            cerr << "New Time: " << timings[i] << endl;
            cerr << "Old Time: " << timings[i-1] << endl;
            cerr << "Beat width = " << beatwidth << endl;
            cerr << "Time width = " << timewidth << endl;
            cerr << "Start index = " << startindex << endl;
            cerr << "End   index = " << endindex << endl;
            exit(1);
         }
         if (roundQ) {
            timings[i] = (int)(timings[i] + 0.5);
         }
      }
   }

}



//////////////////////////////
//
// printtime --
//

void printtime(const char* filename, double totaldur) {
   if (filename[0] != '\0') {
      cout << filename << ":\t";
   } else {
      cout << "Total time:\t";
   }

   if (options.getBoolean("simple")) {
      cout << totaldur << endl;
      return;
   }

   int hours = 0;
   int minutes = 0;
   if (totaldur >= 3600) {
      hours = (int)(totaldur / 3600);
      totaldur -= hours * 3600;
   }
   if (totaldur >= 60) {
      minutes = (int)(totaldur / 60);
      totaldur -= minutes * 60;
   }
   if (hours > 0) {
      cout << hours;
      cout << ":";
      if (minutes < 10) {
         cout << "0";
      }
      cout << minutes << ":";
      if (totaldur < 10) {
         cout << "0";
      }
      cout << totaldur;
      cout << " hours" << endl;
   } else if (minutes > 0) {
      cout << minutes << ":";
      if (totaldur < 10) {
         cout << "0";
      }
      cout << totaldur;
      cout << " minutes" << endl;
   } else {
      cout << totaldur << " seconds" << endl;
   }

}



//////////////////////////////
//
// analyzeTiming -- determing the absolute time position of each
//     line in the file.
// 

void analyzeTiming(HumdrumFile& infile, Array<double>& timings,
      Array<double>& tempo) {
   infile.analyzeRhythm("4");

   timings.setSize(infile.getNumLines());
   timings.setAll(0.0);
   tempo.setSize(infile.getNumLines());
   tempo.setAll(dtempo);
   tempo[0] = dtempo;
   double currtempo = dtempo;
   double input = 0.0;
   int count;
   Array<double> tempoindicators;
   Array<int>    tempoindex;

   tempoindicators.setSize(1000);
   tempoindicators.setSize(0);
   tempoindicators.setGrowth(1000);
   tempoindex.setSize(1000);
   tempoindex.setSize(0);
   tempoindex.setGrowth(1000);

   double ritard = -1.0;
   double accel  = -2.0;

   int i;
   int j;
   for (i=1; i<infile.getNumLines(); i++) {
      // check for new tempo...
      if (infile[i].getType() == E_humrec_interpretation) {
         for (j=0; j<infile[i].getFieldCount(); j++) {
            if (strcmp(infile[i].getExInterp(j), "**kern") != 0) {
               continue;
            }
            if (strncmp(infile[i][j], "*MM", 3) == 0) {
               count = sscanf(infile[i][j], "*MM%lf", &input);
               if (count == 1) {
                  currtempo = input;
                  tempoindicators.append(currtempo);
                  tempoindex.append(i);
                  if (i > 0) {
                     tempo[i-1] = currtempo;
                  }
                  tempo[i] = currtempo;
               }
            } else if (strcmp(infile[i][j], "*accel") == 0) {
               tempoindicators.append(accel);
               tempoindex.append(i);
            } else if (strcmp(infile[i][j], "*rit") == 0) {
               tempoindicators.append(ritard);
               tempoindex.append(i);
            }
            break;
         }
      }

      tempo[i] = currtempo;
      timings[i] = timings[i-1] + (infile[i].getAbsBeat() - 
            infile[i-1].getAbsBeat()) * 60.0/currtempo;
   }

   if (!changeQ) {
      // do not adjust for *rit and *accel markers
      return;
   }

   // go back and geometrically interpolate the tempo markings
   // when there are *accel and *rit markers in the data.

   for (i=1; i<tempoindicators.getSize()-1; i++) {
      if ((tempoindicators[i] < 0) && (tempoindicators[i+1] > 0)) {
         interpolateGeometric(infile, tempo, tempoindex[i], tempoindex[i+1]);
      }
   }


   // the following code will have to be debugged (mostly for off-by-one
   // errors).

/*   // adjust the timing values
   double beatdiff;
   double increment = 1.0/32.0;
   double starttempo;
   double stoptempo;
   double timesum;
   double logtem1;
   double logtem2;
   double tfraction;
   double ftempo;
   double beatduration;
   int    ntindex;
   double startbeat;
   double stopbeat;

   for (i=1; i<infile.getNumLines(); i++) {
      starttempo   = tempo[i];
      ntindex      = getNextTempoIndex(infile, i);
      stoptempo    = tempo[ntindex];
      
      startbeat    = infile[i-1].getAbsBeat();
      stopbeat     = infile[i].getAbsBeat();
      beatduration = stopbeat - startbeat;
      beatdiff     = beatduration;

      if (starttempo == stoptempo) {
         timings[i] = timings[i-1] + beatduration * 60/tempo[i];
      } else {
         // calculate the time as the tempo changes
         logtem1      = log10(starttempo);
         logtem2      = log10(stoptempo);

         timesum = 0.0;
         while (beatdiff >= increment) {
            tfraction = ((beatduration - beatdiff) / beatduration);
            ftempo = pow(10.0, logtem1 + (logtem2 - logtem1) * tfraction);
            timesum += increment * 60/ftempo;
            beatdiff -= increment;
         }

         if (beatduration > 0.0) {
            tfraction = ((beatduration - beatdiff) / beatduration);
            ftempo = pow(10.0, logtem1 + (logtem2 - logtem1) * tfraction);
            timesum += beatdiff * 60/ftempo;
         } else {
            timesum = 0.0;
         }

         timings[i] = timings[i-1] + timesum;
      }
   }
*/
}



//////////////////////////////
//
// getNextTempoIndex --
//

int getNextTempoIndex(HumdrumFile& infile, int startindex) {
   int i = startindex + 1;
   int stopindex = -1;
   while (i < infile.getNumLines()) {
      if (infile[i].getAbsBeat() > infile[startindex].getAbsBeat()) {
         stopindex = i;
         break;
      }
      i++;
   }

   if (stopindex < 0) {
      stopindex = infile.getNumLines() - 1;
   }

   return stopindex;
}



//////////////////////////////
//
// interpolateGeometric --
//

void interpolateGeometric(HumdrumFile& infile, Array<double>& tempo, 
      int startindex, int stopindex) {
   double duration   = 0.0;
   double newtempo   = 0.0;
   double beatstart  = infile[startindex].getAbsBeat();
   double beatstop   = infile[stopindex].getAbsBeat();
   double starttempo = tempo[startindex];
   double stoptempo  = tempo[stopindex];
   double logstart   = log10(starttempo);
   double logstop    = log10(stoptempo);
   double tfraction;
   duration = beatstop - beatstart;
   int i;

   for (i=startindex+1; i<stopindex; i++) {
      if (infile[i].getAbsBeat() == beatstart) {
         tempo[i] = tempo[startindex];
      } else if (infile[i].getAbsBeat() == beatstop) {
         tempo[i] = tempo[stopindex];
      } else {
         tfraction = (infile[i].getAbsBeat() - beatstart)/duration;
         newtempo = pow(10.0, (logstart + (logstop - logstart) * tfraction));
         tempo[i] = newtempo;
      }
   }

}



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

void checkOptions(Options& opts, int argc, char* argv[]) {
   opts.define("T|total=b",         "calculate total time duration of input.");
   opts.define("C|no-change=b",     "do not do ritard and accels.");
   opts.define("R|no-round=b",      "do not round millisecond values.");
   opts.define("a|append=b",        "append analysis to input data");
   opts.define("p|prepend=b",       "prepend analysis to input data");
   opts.define("i|interpolation=b", "interpolate times in prexisting spine");
   opts.define("o|offset=d:0.0",    "starting time offset");
   opts.define("s|seconds=b",       "use seconds as the time unit");
   opts.define("simple=b",          "print total time in pure seconds");
   opts.define("t|tempo=b",         "display tempo rather than time");
   opts.define("d|default-tempo=d:60.0",  "default tempo to use if none in file");   

   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("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 2002" << endl;
      exit(0);
   } else if (opts.getBoolean("version")) {
      cout << argv[0] << ", version: 15 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);
   }

   tempoQ  =  opts.getBoolean("tempo");
   changeQ = !opts.getBoolean("no-change");
   roundQ  = !opts.getBoolean("no-round");
roundQ = 0;
   interpQ =  opts.getBoolean("interpolation");
   debugQ  =  opts.getBoolean("debug");
   offset  =  opts.getDouble("offset");
   if (opts.getBoolean("seconds")) {
      style = 's';
   } else {
      style = 'm';
   }
   dtempo = opts.getDouble("default-tempo");

   // don't let people set the tempo to slow or negative
   if (dtempo < 1.0) {
      dtempo = 60.0;
   }

   totalQ   = opts.getBoolean("total");
   appendQ  = opts.getBoolean("append");
   prependQ = opts.getBoolean("prepend");
   if (appendQ) { // exclusive options
      prependQ = 0;
   }
}



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

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



//////////////////////////////
//
// printAnalysis -- 
//

void printAnalysis(HumdrumFile& infile, Array<double>& timings,
      Array<double>& tempo) {
   int tempomark = 0;
   int i, j;
   int m;
   infile.printNonemptySegmentLabel(cout);
   for (i=0; i<infile.getNumLines(); i++) {
      switch (infile[i].getType()) {
      case E_humrec_global_comment:
      case E_humrec_bibliography:
      case E_humrec_none:
      case E_humrec_empty:
         cout << infile[i].getLine() << "\n";
         break;
      case E_humrec_data:

         if (tempomark == 0) {
            tempomark = 1;
            if (appendQ) {
               for (j=0; j<infile[i].getFieldCount(); j++) {
                  cout << "*MM" << tempo[i] << "\t";
               }
            }
            cout << "*MM" << tempo[i];
            if (prependQ) {
               for (j=0; j<infile[i].getFieldCount(); j++) {
                  cout << "\t" << "*MM" << tempo[i];
               }
            }
            cout << "\n";
         }

         if (appendQ) {
            cout << infile[i].getLine() << "\t";
         }

         if (tempoQ) {
            if (roundQ) {
               cout << (int)(tempo[i] + 0.5);
            } else {
               cout << tempo[i];
            }
         } else {
            if (style == 's') {
               cout << timings[i] + offset;
            } else {
               if (roundQ) {
                  cout << (int)((timings[i] + offset + 0.0005)* 1000);
               } else {
                  cout << (timings[i] + offset) * 1000.0;
               }
            }
         }

         if (prependQ) {
            cout << "\t" << infile[i].getLine();
         }

         cout << "\n";
         break;
      case E_humrec_data_comment:
         if (appendQ) {
            cout << infile[i].getLine() << "\t";
         }
         cout << "!";
         if (prependQ) {
            cout << "\t" << infile[i].getLine();
         }
         cout << "\n";
         break;
      case E_humrec_data_measure:
         if (appendQ) {
            cout << infile[i].getLine() << "\t";
         }
         if (infile[i].equalFieldsQ("**kern")) {
            cout << infile[i][0];
         } else {
            cout << "=";
         }
         if (prependQ) {
            cout << "\t" << infile[i].getLine();
         }
         cout << "\n";
         break;
      case E_humrec_data_interpretation:
         if (strncmp(infile[i][0], "**", 2) == 0) {
            if (appendQ) {
               cout << infile[i].getLine() << "\t";
            }
            if (tempoQ) {
               cout << "**tempo";
            } else {
               cout << "**time";
            }
            if (prependQ) {
               cout << "\t" << infile[i].getLine();
            }
            cout << "\n";

            // print units marker
            if (appendQ) {
               for (m=0; m<infile[i].getFieldCount(); m++) {
                  cout << "*";
                  cout << "\t";
               }
            }
            cout << "*u=";
            switch (style) {
               case 's': cout << "sec";  break;
               case 'm': cout << "msec"; break;
               default : cout << "msec"; break;
            }
            if (prependQ) {
               cout << "\t";
               for (m=0; m<infile[i].getFieldCount(); m++) {
                  cout << "*";
                  if (m < infile[i].getFieldCount() - 1) {
                     cout << "\t";
                  }
               }
            }
            cout << "\n";
         } else {
            if (prependQ) {
               if (strcmp(infile[i][0], "*-") == 0) {
                  cout << "*-";
               } else if (strncmp(infile[i][0], "*MM", 3) == 0) {
                  tempomark = 1;
                  cout << infile[i][0];
               } else if (infile[i].equalFieldsQ("**kern")) {
                  if ((strcmp(infile[i][0], "*^") != 0) &&
                      (strcmp(infile[i][0], "*+") != 0) &&
                      (strcmp(infile[i][0], "*x") != 0) &&
                      (strcmp(infile[i][0], "*v") != 0)) {
                     cout << infile[i][0];
                  } else {
                     cout << "*";
                  }
               } else {
                  cout << "*";
               }
               cout << "\t";
            }

            if (prependQ || appendQ) {
               cout << infile[i].getLine();
            }

            if (appendQ) {
               cout << "\t";
               if (strcmp(infile[i][0], "*-") == 0) {
                  cout << "*-";
               } else if (strncmp(infile[i][0], "*MM", 3) == 0) {
                  tempomark = 1;
                  cout << infile[i][0];
               } else if (infile[i].equalFieldsQ("**kern")) {
                  if ((strcmp(infile[i][0], "*^") != 0) &&
                      (strcmp(infile[i][0], "*+") != 0) &&
                      (strcmp(infile[i][0], "*x") != 0) &&
                      (strcmp(infile[i][0], "*v") != 0)) {
                     cout << infile[i][0];
                  } else {
                     cout << "*";
                  }
               } else {
                  cout << "*";
               }
            }

            cout << "\n";
         }
         break;
      }

   }
}



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

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


// md5sum: 8fc2dc7d1056f7cbbeb237ee2093192e gettime.cpp [20130420]