//
// Programmer:    Craig Stuart Sapp <craig@ccrma.stanford.edu>
// Creation Date: Wed Nov 19 15:25:16 PST 2003
// Last Modified: Thu Nov 20 10:10:42 PST 2003
// Filename:      ...sig/examples/all/lo5.cpp
// Web Address:   http://sig.sapp.org/examples/museinfo/humdrum/lofcog.cpp
// Syntax:        C++; museinfo
//
// Description:   Line of 5ths median (center of gravity) calculations.
//
// Reference:     David Temperley, "The Cognition of Basic Musical Structures"
//		  MIT Press; 2001, Chapter 5: Pitch Spelling and the 
//                Tonal-Pitch-Class, pp. 114-136.
// 

#include "humdrum.h"
#include "CircularBuffer.h"
#include "PlotFigure.h"

#include <math.h>

// function declarations
void   checkOptions(Options& opts, int argc, char* argv[]);
void   example(void);
void   usage(const char* command);
void   analyzeFile(HumdrumFile& infile, Array<double>& fifthmean);
void   printAnalysis(HumdrumFile& infile, Array<double>& fifthmeean);
int    lo5ToBase40(double lineval);
int    base40ToLo5(int base40);
double getFifthMean(HumdrumFile& infile, int line, double beats, 
                          CircularBuffer<int>& notes, 
                          CircularBuffer<double>& absbeat, double& meansum,
                          int measure);
void   printXfig(HumdrumFile& infile, Array<double>& fifthmean);
int    chooseLineNumber(int base12, double average);
void   convertBase12ToBase40(int base12, int& x, int& y, int& z);
void   getDeviation(HumdrumFile& infile, Array<double>& cog,
                          Array<Array<double> >& deviation);
double getAverage(HumdrumFile& infile, 
                          Array<Array<double> >& pastdist, double abeat,
                          int index);

// global variables
Options      options;            // database for command-line arguments
int          debugQ     = 0;     // used with --debug option
int          errorQ     = 0;     // used with -e option
int          appendQ    = 0;     // used with -a option
double       beats      = 8.0;   // used with -b option
int          basemode   = 0;     // used with -m option
int          pitchQ     = 0;     // used with -p option
int          xfigQ      = 0;     // used with -x option
int          startbranch= 0;     // used with -f and -s options
int          keysig     = 0;     // used with -k option
int          keysigQ    = 0;     // used with -k option
int          deviationQ = 0;     // used with -d option
int          averageQ   = 0;     // used with -g option
double       avgbeat    = 4.0;   // used with -g option

const char* CurrentFile = ".";



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

int main(int argc, char* argv[]) {
   HumdrumFile infile;
   Array<double> fifthmean;

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

   // build up the chord table from the input file(s):
   int i;
   for (i=1; i<=options.getArgCount() || options.getArgCount()==0; i++) {
      infile.clear();

      // if no command-line arguments read data file from standard input
      if (options.getArgCount() < 1) {
         infile.read(cin);
      } else {
         infile.read(options.getArg(i));
         CurrentFile = options.getArg(i);
         if (strrchr(options.getArg(i), '/') != NULL) {
            CurrentFile = strrchr(options.getArg(i), '/') + 1;
         }
      }
     
      infile.analyzeRhythm();
      analyzeFile(infile, fifthmean);
      if (xfigQ) {
         printXfig(infile, fifthmean);
      } else {
         printAnalysis(infile, fifthmean);
      }
   }

   return 0;
}


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



//////////////////////////////
//
// printXfig -- print data in xfig format
//

void printXfig(HumdrumFile& infile, Array& fifthmean) {
   int i;
   double xmin = 10000;
   double xmax = 0;
   double ymin = 100;
   double ymax = -100;
   PlotData   plotdata;
   PlotFigure plot;
   plot.allocatePoints(infile.getNumLines());
   for (i=0; i<infile.getNumLines(); i++) {
      if (fifthmean[i] < -100) {
         continue;
      }
      if (infile[i].getType() == E_humrec_data) {
         if (fifthmean[i] < ymin) {
            ymin = fifthmean[i];
         }
            if (fifthmean[i] > ymax) {
            ymax = fifthmean[i];
         }
         if (infile[i].getAbsBeat() < xmin) {
            xmin = infile[i].getAbsBeat();
         }
         if (infile[i].getAbsBeat() > xmax) {
            xmax = infile[i].getAbsBeat();
         }
      }
   }

   plot.setXRangeAutoOff();
   plot.setYRangeAutoOff();
   // plot.setXMin(xmin);
   // plot.setXMax(xmax);
   plot.setXMin(0.0);
   plot.setXMax(infile[infile.getNumLines()-1].getAbsBeat());
   plot.setYMin(ymin-0.5);
   plot.setYMax(ymax+0.5);
   int mnum;
   int count;
   char buffer[1024] = {0};
   for (i=0; i<infile.getNumLines(); i++) {
      if (fifthmean[i] < -100) {
         continue;
      }
      if (infile[i].getType() == E_humrec_data) {
         plotdata.addPoint(infile[i].getAbsBeat(), fifthmean[i]);
      } else if (infile[i].getType() == E_humrec_data_measure) {
         count = sscanf(infile[i][0], "=%d", &mnum);
         sprintf(buffer, "%d", mnum);
         plot.addVLine(infile[i].getAbsBeat(), "");
         if ((mnum > 0) && (mnum % 10 == 0)) {
            plot.addText(buffer, infile[i].getAbsBeat(), ymin-0.75, 14, 0, 0);
         }
      }
   }

   plot.addPlot(plotdata);
   plot.yTicksOn();
   plot.setYTicksDelta(1.0);
   plot.printXfig(cout);
}



//////////////////////////////
//
// analyzeFile -- analyze the line of fifth mean for each line in
//     the file.
//

void analyzeFile(HumdrumFile& infile, Array& fifthmean) {
   int i;
   int measure = 0;
   int linecount = infile.getNumLines();
   fifthmean.setSize(linecount);
   fifthmean.allowGrowth(0);
   fifthmean.setAll(0);

   CircularBuffer<int>    notes(10000);
   CircularBuffer<double> absbeat(10000);
   CircularBuffer<double> meanline(10000);

   notes.reset();
   absbeat.reset();
   meanline.reset();

   double meansum = 0.0;

   int length, ii, jj;
   for (i=1; i<linecount; i++) {
      switch (infile[i].getType()) {
         case E_humrec_none:
         case E_humrec_empty:
         case E_humrec_global_comment:
         case E_humrec_bibliography:
         case E_humrec_data_comment:
            fifthmean[i] = fifthmean[i-1];
            break;
         case E_humrec_interpretation:
            if (keysigQ) {
               for (ii=0; ii<infile[i].getFieldCount(); ii++) {
                  if (strcmp(infile[i].getExInterp(ii), "**kern") != 0) {
                     continue;
                  }
                  if (strncmp(infile[i][ii], "*k[", 3) == 0) {
                     keysig = 0;
                     length = strlen(infile[i][ii]);
                     for (jj=3; jj<length; jj++) {
                        if (infile[i][ii][jj] == '-') {
                           keysig--;
                        } else if (infile[i][ii][jj] == '#') {
                           keysig++;
                        }
                     }
                     if (keysig > 3) {
                        startbranch = 1;
                     } else if (keysig < -3) {
                        startbranch = -1;
                     } else {
                        startbranch = 0;
                     }
                  }
               }
            }
            fifthmean[i] = fifthmean[i-1];
            break;
         case E_humrec_data_kern_measure:
            sscanf(infile[i][0], "=%d", &measure);
            fifthmean[i] = fifthmean[i-1];
            break;
         case E_humrec_data:
            fifthmean[i] = 
               getFifthMean(infile, i, beats, notes, absbeat, meansum, measure);
            break;
      }
   }

}



//////////////////////////////
//
// getFifthMean -- process the notes in a line.
//

double getFifthMean(HumdrumFile& infile, int line, double beats, 
      CircularBuffer<int>& notes, CircularBuffer<double>& absbeat, 
      double& meansum, int measure) {
   char buffer[1024] = {0};
   int tnote;
   int base12;
   double bias;
   double tbeat;
   int base40;
   int linenum;
   int tlinenum;
   int i, j;
   for (i=0; i<infile[line].getFieldCount(); i++) {
      if (strcmp(infile[line].getExInterp(i), "**kern") != 0) {
         // ignore non-kern spines
         continue;
      }
      if (strcmp(infile[line][i], ".") == 0) {
         // null token, so ignore
         continue;
      }
      for (j=0; j<infile[line].getTokenCount(i); j++) {
         infile[line].getToken(buffer, i, j);
         if (buffer[0] == '\0') {
            // invalid chord note
            continue;
         }
         base40 = Convert::kernToBase40(buffer);
         if (base40 < -10) {
            // ignore rests
            continue;
         }
         if (basemode == 0) {
            linenum = base40ToLo5(base40);
         } else {
            // forget the spelling and try to figure it out from the
            // line of fifths mean 
            base12 = Convert::base40ToMidiNoteNumber(base40) % 12;
            bias = 0.0;
            if (infile[line].getAbsBeat() <= beats) {
               bias = startbranch * 3;   // shift to sharps, flats or naturals
               // gradually remove effect of bias as more notes are added
               bias = bias - bias/(notes.getCount()+10);  
            }
            if (notes.getCount() > 0) {
               linenum = chooseLineNumber(base12, 
                     bias+meansum/notes.getCount());
            } else {
               linenum = chooseLineNumber(base12, bias);
            }
            tlinenum = base40ToLo5(base40);
            if (errorQ && (tlinenum != linenum)) {
               cout << "!! naming error";

               if (measure > 0) { 
                  cout << " in bar=" << measure 
                       << ", beat=" << infile[line].getBeat();
               } else {
                  cout << " on original line=" << line;
               }
               cout << ", spine=" << i+1;
               if (infile[line].getTokenCount(i) > 1) {
                  cout << " token=" << j+1;
               }
               cout << " (" << Convert::base40ToKern(buffer, 
                       lo5ToBase40(tlinenum)+4*40) 
                    << " is changed to ";

               cout << Convert::base40ToKern(buffer, lo5ToBase40(linenum)+4*40)
                    << ")";
               cout << " [" << tlinenum << " to " << linenum << "]";
               cout << endl;
            }
            
         }

         notes.insert(linenum);         
         absbeat.insert(infile[line].getAbsBeat());
         meansum += linenum;

         // remove any old notes which are outside of the analysis window
         while (infile[line].getAbsBeat() - absbeat[absbeat.getCount()-1] > 
               beats) {
            tnote = notes.extract();
            tbeat = absbeat.extract();
            meansum -= tnote;
         }
      }
   }

   if (notes.getCount() > 0) {
      return meansum/notes.getCount();
   } else {
      return -1000;
   }
}



//////////////////////////////
//
// chooseLineNumber --  Choose the pitch spelling which is closest to 
//   the average pitch value.
//

int chooseLineNumber(int base12, double average) {
   int x, y, z;
   convertBase12ToBase40(base12, x, y, z);
   x = base40ToLo5(x);
   y = base40ToLo5(y);
   z = base40ToLo5(z);

   double diffx = fabs(average-x);
   double diffy = fabs(average-y);
   double diffz = fabs(average-z);

   if ((diffx < diffy) && (diffx < diffz)) {
      return x;
   }
   if (diffy < diffz) {
      return y;
   }
   return z;
}



///////////////////////////////
//
// convertBase12ToBase40 -- give a list of the 

void convertBase12ToBase40(int base12, int& x, int& y, int& z) {
   switch (base12) {
      case 0:   x=38; y=2;  z=6;     break;  // B#/C/D--
      case 1:   x=39; y=3;  z=7;     break;  // B##/C#/D-
      case 2:   x= 4; y=8;  z=12;    break;  // C##/D/E--
      case 3:   x= 9; y=13; z=17;    break;  // D#/E-/F--
      case 4:   x=10; y=14; z=18;    break;  // D##/E/F-
      case 5:   x=15; y=19; z=23;    break;  // E#/F/G--
      case 6:   x=16; y=20; z=24;    break;  // E##/F#/G-
      case 7:   x=21; y=25; z=29;    break;  // F##/G/A--
      case 8:   x=26; y=30; z=-1000; break;  // G#/A-
      case 9:   x=27; y=31; z=35;    break;  // G##/A/B--
      case 10:  x=32; y=36; z=0;     break;  // A#/B-/C--
      case 11:  x=33; y=37; z=1;     break;  // A##/B/C-
      default:  x=-1000; y=-1000; z=-1000; break;  // unknown
   }
}



//////////////////////////////
//
// getDeviation -- calculate the deviation of notes from the
//    fifth mean.
//

void getDeviation(HumdrumFile& infile, Array<double>& cog,
      Array<Array<double> >& deviation) {

   Array<Array<int> > notes;

   deviation.setSize(infile.getNumLines());
   notes.setSize(infile.getNumLines());

   int i, j, k;
   for (i=0; i<deviation.getSize(); i++) {
      deviation[i].setSize(32);
      deviation[i].setGrowth(32);
      deviation[i].setSize(0);
      notes[i].setSize(32);
      notes[i].setGrowth(32);
      notes[i].setSize(0);
   }

   char buffer[1024] = {0};
   int tokencount;
   int base40;
   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(infile[i].getExInterp(j), "**kern") != 0) {
            continue;
         }
         if (strcmp(infile[i][j], ".") == 0) {
            continue;
         }
         
         tokencount = infile[i].getTokenCount(j);
         for (k=0; k<tokencount; k++) {
            infile[i].getToken(buffer, j, k);
            if (strchr(buffer, ']') != NULL) {  // ignore tied notes
               continue;
            }
            if (strchr(buffer, '_') != NULL) {  // ignore tied notes
               continue;
            }
            base40 = Convert::kernToBase40(buffer);
            if (base40 < 0) {
               continue;
            }
            // have an interesting note, so store it
            notes[i].append(base40);
         }
      }
   }

   // notes are extracted so calculate the deviations

   double dev;
   for (i=0; i<infile.getNumLines(); i++) {
      if (notes[i].getSize() < 1) {
         continue;
      }
      deviation[i].setSize(notes[i].getSize());
      for (j=0; j<notes[i].getSize(); j++) {
         dev = base40ToLo5(notes[i][j]) - cog[i];
         deviation[i][j] = dev;
      }
   }

}



//////////////////////////////
//
// printAnalysis -- print the analysis of the mean.
//

void printAnalysis(HumdrumFile& infile, Array& fifthmean) {
   int i;
   int linecount = infile.getNumLines();
   char buffer[1024] = {0};
   int printbeat = 0;
   int ii, kk;

   Array<Array<double> > deviation;
   if (deviationQ) {
      getDeviation(infile, fifthmean, deviation);
   }

   double tempval;
   for (i=0; i<linecount; i++) {
      switch (infile[i].getType()) {
         case E_humrec_none:
         case E_humrec_empty:
         case E_humrec_global_comment:
         case E_humrec_bibliography:
            cout << infile[i] << endl;
            break;
         case E_humrec_data_comment:
            if (appendQ) {
               cout << infile[i] << "\t" << "!" << endl;
            }
            break;
         case E_humrec_data_kern_measure:
            if (appendQ) {
               cout << infile[i] << "\t" << infile[i][0] << endl;
            } else {
               cout << infile[i][0] << endl;
            }
            break;
         case E_humrec_interpretation:
            if (appendQ) {
               cout << infile[i] << "\t";
            }
            if (strcmp(infile[i][0], "*-") == 0) {
               cout << "*-";
            } else if (strncmp(infile[i][0], "**", 2) == 0) {
               cout << "**lo5";
               if (appendQ == 0) {
                  cout << "\n*b=" << beats;
                  printbeat = 1;
               }
            } else {
               cout << "*";
               if (printbeat == 0) {
                  cout << "b=" << beats;
                  printbeat = 1;
               }
            }
            cout << endl;
            break;
         case E_humrec_data:
            if (appendQ) {
               if (printbeat == 0) {
                  for (ii=0; ii<infile[i].getFieldCount(); ii++) {
                     cout << "*\t";
                  }
                  cout << "*b=" << beats << endl;
                  printbeat = 1;
               } 
               cout << infile[i] << "\t";
            } 
            if (deviationQ) {
               if (lo5ToBase40(fifthmean[i]) < -100) {
                  cout << ".";
               } else {
                  if (averageQ) {
                     cout << getAverage(infile, deviation, avgbeat, i);
                  } else {
                     for (kk=0; kk<deviation[i].getSize(); kk++) {
                        tempval = deviation[i][kk];
                        tempval = (int)(100 * tempval + 0.5)/100.0;
                        cout << tempval;
                        if (kk < deviation[i].getSize() - 1) {
                           cout << " ";
                        }
                     }
                  }
               }
            } else if (pitchQ) {
               if (lo5ToBase40(fifthmean[i]) < -100) {
                  cout << ".";
               } else {
                  cout << Convert::base40ToKern(buffer, 
                           lo5ToBase40(fifthmean[i]) + 4 * 40);
               }
            } else {
               if (fifthmean[i] < -100) {
                  cout << ".";
               } else {
                  cout << fifthmean[i];
               }
            }
            cout << endl;
            break;
      
      }
   }
}



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

void checkOptions(Options& opts, int argc, char* argv[]) {
   opts.define("b|beats=d:8.0",   "number of quarter-notes in past to average");
   opts.define("a|append=b",      "append analysis to input file");
   opts.define("x|xfig=b",        "print output in xfig format");
   opts.define("p|pitch=b",       "print lo5 pitch name instead of number");
   opts.define("m|midi=b",        "simiulate MIDI pitch names");
   opts.define("e|error=b",       "print branch errors");
   opts.define("s|sharp=b",       "start with a sharp bias");
   opts.define("f|flat=b",        "start with a flat bias");
   opts.define("k|keysig=b",      "start with a bias related to key signature");
   opts.define("d|deviation=b",   "deviation of a note from the lof cog");
   opts.define("g|average=d:4.0", "average the deviations of individual 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("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, Nov 2003" << endl;
      exit(0);
   } else if (opts.getBoolean("version")) {
      cout << argv[0] << ", version: 19 Nov 2003" << 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");
   keysigQ      = opts.getBoolean("keysig");
   errorQ       = opts.getBoolean("error");
   xfigQ        = opts.getBoolean("xfig");
   appendQ      = opts.getBoolean("append");
   pitchQ       = opts.getBoolean("pitch");
   beats        = opts.getDouble("beats");
   basemode     = opts.getBoolean("midi");
   startbranch += opts.getBoolean("sharp");
   startbranch -= opts.getBoolean("flat");
   deviationQ   = (opts.getBoolean("deviation") || opts.getBoolean("average"));
   averageQ     = opts.getBoolean("average");
   avgbeat      = opts.getDouble("average");

}



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

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



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

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



//////////////////////////////
//
// lo5ToBase40 -- convert a line of fifth pitch-class into a
//      base-40 pitch class.
//

int lo5ToBase40(double lineval) {
   int lv = (int)(lineval+0.49);
   if (lv < -15)  {  // can't go lower than F--
      return -1000;
   }
   if (lv > 19)  {  // can't go higher than B##
      return -1000;
   }

   if (lv >= 0) {
      return ((lv * 23 + 2) + 4000) % 40;
   } else { 
      return ((-(lv-1) * 17 + 2) + 4000) % 40;
   }
}



///////////////////////////////
//
// base40ToLo5 -- convert base40 to line of fifths
//

int base40ToLo5(int base40) {
   base40 = (base40 + 4000) % 40; // remove any octave information;

   switch (base40) {
      case  0: return -14;   // C--
      case  1: return  -7;   // C-
      case  2: return   0;   // C
      case  3: return   7;   // C#
      case  4: return  14;   // C##
      case  5: return -1000; // unknown
      case  6: return -12;   // D--
      case  7: return  -5;   // D-
      case  8: return   2;   // D
      case  9: return   9;   // D#
      case 10: return  16;   // D##
      case 11: return -1000; // unknown
      case 12: return -10;   // E--
      case 13: return  -3;   // E-
      case 14: return   4;   // E
      case 15: return  11;   // E#
      case 16: return  18;   // E##
      case 17: return -15;   // F--
      case 18: return  -8;   // F-
      case 19: return  -1;   // F
      case 20: return   6;   // F#
      case 21: return  13;   // F##
      case 22: return -1000; // unknown
      case 23: return -13;   // G--
      case 24: return  -6;   // G-
      case 25: return   1;   // G
      case 26: return   8;   // G#
      case 27: return  15;   // G##
      case 28: return -1000; // unknown
      case 29: return -11;   // A--
      case 30: return  -4;   // A-
      case 31: return   3;   // A
      case 32: return  10;   // A#
      case 33: return  17;   // A##
      case 34: return -1000; // unknown
      case 35: return  -9;   // B--
      case 36: return  -2;   // B-
      case 37: return   5;   // B
      case 38: return  12;   // B#
      case 39: return  19;   // B##
   }
   return -1000;  // unknown or rest
}



//////////////////////////////
//
// getAverage --
//

double getAverage(HumdrumFile& infile, Array<Array<double> >& data,
      double abeat, int index) {
   int i, j;

   int count = 0;
   double sum = 0.0;
   double startdur = infile[index].getAbsBeat();

   for (i=index; i>=0; i--) {
      if (startdur - infile[i].getAbsBeat() > abeat) {
         break;
      }
      for (j=0; j<data[i].getSize(); j++) {
         if (data[i][j] == -1.0) {
            sum += infile[i].getAbsBeat();
         } else {
            sum += data[i][j];
         }
         count++;
      }
   }

   if (count > 0) {
      return sum/count;
   }
   return 0;
}



// md5sum: 5a02410e288a4f8168cc943cde1ace7c lofcog.cpp [20050403]