//
// Programmer:    Craig Stuart Sapp <craig@ccrma.stanford.edu>
// Creation Date: Tue Nov 14 09:18:07 PST 2000
// Last Modified: Fri Nov 17 12:02:24 PST 2000
// Filename:      ...sig/examples/all/diametric.cpp
// Web Address:   http://sig.sapp.org/examples/museinfo/humdrum/diametric.cpp
// Syntax:        C++; museinfo
//
// Description:   Analyzes **kern sonorities for diatonic compactness
//                in various orderings of seconds, thirds, fifths
//                and their inversions.
//

#include "humdrum.h"

#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <stdio.h>
#include <math.h>

#ifndef OLDCPP
   #include <iostream>
#else
   #include <iostream.h>
#endif

// function declarations
void   checkOptions(Options& opts, int argc, char* argv[]);
int    countAttacks(HumdrumRecord& record);
void   displayAccentedBeatMetrics(Array<double>& metposition, 
                            Array<double>& ascores, 
                            const char* metermarker, int start, int stop);
int    doublecompare(const void* a, const void* b);
void   example(void);
void   generateAnalysis(HumdrumFile& infile, Array<int>&analysis, 
                                  int metric, Array<int>& attacks);
void   generateSummary(HumdrumFile& infile, Array<int>& metricana);
void   generateSubSummary(HumdrumFile& infile, Array<int>& metricana, 
                                 const char* metricmarker, int start, int stop);
void   getPitches(Array<int>& pitches, HumdrumFile& infile, 
                                  int line);
double makeBeatScore(int totalbeats, double width, 
                                  Array<double>& metposition, 
                                  Array<double>&ascores);
int    makeMeasurement(Array<int>&pitches, int metric);
void   printAnalysis(HumdrumFile& infile, Array<int>&metric);
void   usage(const char* command);


// global variables
Options      options;            // database for command-line arguments
int          debugQ     = 0;     // used with the --debug option
int          metric     = 1;     // diatonic interleaving (1 = by thirds)
int          appendQ    = 0;     // used with the -a option
int          histogramQ = 0;     // used with the -c option
int          summaryQ   = 0;     // used with the -c option
int          beatQ      = 0;     // used with the -b option


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


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

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

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

   for (int 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);
      } else {
         infile.read(options.getArg(i+1));
      }

      generateAnalysis(infile, analysis, metric, attacks);
      printAnalysis(infile, analysis);
   }

   return 0;
}


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


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

void checkOptions(Options& opts, int argc, char* argv[]) {
   opts.define("m|metric=i:3",      "diatonic space: 3=thirds, 2=2nds, etc.");
   opts.define("a|append=b",        "append analysis to data in output");   
   opts.define("c|count|histogram=b", "diatonic histogram of sonority notes");
   opts.define("s|sum|summary=b",   "average value for each metrical position");
   opts.define("b|beat=b",          "display beat size");

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

   // metric == 0 --> ordered by seconds, sevenths
   // metric == 1 --> ordered by thirds, sixths
   // metric == 2 --> ordered by fourths, fifths
   metric = opts.getInteger("metric") - 2;
   if (metric < 0) {
      metric = 1;
   } else if (metric == 3) {
      metric = 2;
   } else if (metric == 4) {
      metric = 1;
   } else if (metric == 5) {
      metric = 0;
   } else if (metric > 5) {
      metric = 1;
   }

   debugQ = opts.getBoolean("debug");
   appendQ = opts.getBoolean("append");
   histogramQ = opts.getBoolean("histogram");
   if (opts.getBoolean("summary")) {
      summaryQ = 1;
      histogramQ = 0;
      appendQ = 0;
   } else {
      summaryQ = 0;
   }

   if (opts.getBoolean("beat")) {
      summaryQ = 1;
      beatQ = 1;
      histogramQ = 0;
      appendQ = 0;
   } 

}



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

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



//////////////////////////////
//
// generateAnalysis -- 
//

void generateAnalysis(HumdrumFile& infile, Array<int>& analysis, int metric,
      Array<int>& attacks) {
   Array<int> pitches;
   pitches.setSize(100);
   pitches.setSize(0);
   pitches.allowGrowth(1);
   analysis.setSize(infile.getNumLines());
   attacks.setSize(infile.getNumLines());

   for (int i=0; i<infile.getNumLines(); i++) {
      if (options.getBoolean("debug")) {
         cout << "processing line " << (i+1) << " of input ..." << endl;
      }
      if (infile[i].getType() != E_humrec_data) {
         analysis[i] = -1;
         attacks[i] = -1;
         continue;
      }

      getPitches(pitches, infile, i);

      analysis[i] = makeMeasurement(pitches, metric);
      attacks[i] = countAttacks(infile[i]);
   }


}



//////////////////////////////
//
// countAttacks -- count the number of notes that are struck
//

int countAttacks(HumdrumRecord& record) {
   int i, j;
   int count = 0;
   char buffer[128] = {0};
   for (i=0; i<record.getFieldCount(); i++) {
      if (record.getExInterpNum(i) != E_KERN_EXINT) {
         continue;
      }
      if (strcmp(record[i], ".") == 0) {
         continue;
      }
      int tcount = record.getTokenCount(i);
      for (j=0; j<tcount; j++) {
         record.getToken(buffer, i, j);         
         if (strcmp(buffer, ".") == 0) {
            continue;
         }

         if (strchr(buffer, '_') != NULL) {
            continue;
         }
         if (strchr(buffer, ']') != NULL) {
            continue;
         }

         count++;
      }
   }

   return count;
}



//////////////////////////////
//
// getPitches -- get the diatonic pitches found on the current line.
//

void getPitches(Array& pitches, HumdrumFile& infile, int line) {
   pitches.setSize(0);
   int diatonic;
   int j, k;
   int tokencount;
   char buffer[128] = {0};

   for (j=0; j<infile[line].getFieldCount(); j++) {
      if (infile[line].getExInterpNum(j) != E_KERN_EXINT) {
         continue;
      }
      tokencount = infile[line].getTokenCount(j);

      for (k=0; k<tokencount; k++) {
         if (strcmp(infile[line][j], ".") == 0) {
            tokencount = infile[infile[line].getDotLine(j)].getTokenCount(
                            infile[line].getDotSpine(j));
            infile[infile[line].getDotLine(j)].getToken(buffer, 
               infile[line].getDotSpine(j), k);   
         } else {
            infile[line].getToken(buffer, j, k);   
         }
         diatonic = Convert::kernToDiatonicPitchClass(buffer);
         pitches.append(diatonic);
      }
   }
}



//////////////////////////////
//
// makeMeasurement -- get the diatonic pitches found on the current line.
//

int makeMeasurement(Array&pitches, int metric) {
   int compare[3][14] = 
   {{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'a', 'b', 'c', 'd', 'e', 'f', 'g'},
   {'a', 'c', 'e', 'g', 'b', 'd', 'f', 'a', 'c', 'e', 'g', 'b', 'd', 'f'},
   {'a', 'e', 'b', 'f', 'c', 'g', 'd', 'a', 'e', 'b', 'f', 'c', 'g', 'd'}};


   int data[14] = {0};
   int i;

//cout << "Comparing to : ";
//for (i=0; i<7; i++) {
//   cout << (char)compare[metric][i] << " ";
//}
//cout << "   ... : " << endl;


   int j;
   for (i=0; i<pitches.getSize(); i++) {
      j = 0;
      while (j<7) {
         if (compare[metric][j] == pitches[i]) {
            data[j]++;
            data[j+7]++;
            break;
         }
         j++;
      }
   }

   int max = 0;
   int count = 0;
   for (i=0; i<14; i++) {
      if (data[i] != 0) {
        if (count > max) {
           max = count;
        }
        count = 0;
      } else {
        count++;
      } 

   }
   if (count > max) {
      max = count;
   }

//for (i=0; i<pitches.getSize(); i++) {
//cout << (char)pitches[i] << " ";
//}
//cout << "\t";
//for (i=0; i<7; i++) {
//cout << data[i];
//}
//cout << " final result: " << 7 - max << endl;

   int result = 7 - max;
   if (result < 0) {
      return 0;
   } else {
      return result;
   }
}



//////////////////////////////
//
// generateSummary -- generate the average measurement value for
//     each metrical position in the meter.
//

void generateSummary(HumdrumFile& infile, Array& metricana) {
   int start = 0;
   int stop = 0;
   int i=0;
   int m=0;
   int datatrigger = 0;
   int metertrigger = 0;
   int summarytrigger = 0;
   int length;
   const char* metricmarker = "";

   if (!beatQ) {
      cout << "**takt\t**avg\t**fract\t**ascore\n";
   }

   while (i<infile.getNumLines()) {
      if (infile[i].getType() == E_humrec_data) {
         datatrigger = 1;
         if (start == 0) {
            start = i;
         }
      }

      if (infile[i].getType() == E_humrec_interpretation) {
         for (m=0; m<infile[i].getFieldCount(); m++) {
            if ((infile[i].getExInterpNum(m) == E_KERN_EXINT) && 
                  (strncmp(infile[i][m], "*M", 2) == 0)) {
               length = strlen(infile[i][m]);
               if ((length > 2) && isdigit(infile[i][m][2]) && 
                     (strchr(infile[i][m], '/') != NULL)) {
                  metertrigger = 1;
                  metricmarker = infile[i][m];
                  if (datatrigger) {
                     summarytrigger = 1;
                     stop = i-1;
                  } else {
                     start = i;
                  }
                  break;
               } else if (strncmp(infile[i][m], "*MM", 3) == 0) {
                  if (!beatQ) {
                     cout << infile[i][m] << "\t";
                     cout << infile[i][m] << "\t";
                     cout << infile[i][m] << "\t";
                     cout << infile[i][m] << "\n";
                     break;
                  }
               }
            } 
         }
      }
      if (strcmp(infile[i][0], "*-") == 0) {
         summarytrigger = 1;
         stop = i;
      }

      if (datatrigger == 1 && metertrigger == 0) {
         generateSubSummary(infile, metricana, "*MX", 0, infile.getNumLines());
         break;
      }

      if (summarytrigger) {
         generateSubSummary(infile, metricana, metricmarker, start, stop);
         summarytrigger = 0;
         datatrigger = 0;
         metricmarker = "";
         start = stop + 1;
      }
      i++;

   }
   if (!beatQ) {
      cout << "*-\t*-\t*-\t*-\n";
   }

}



//////////////////////////////
//
// generateSubSummary --
//

void generateSubSummary(HumdrumFile& infile, Array<int>& metricana, 
   const char* metricmarker, int start, int stop) {

   infile.analyzeRhythm();

   Array<double> metposition;
   metposition.setSize(0);
   metposition.allowGrowth();

   Array<int> count;
   count.setSize(0);
   count.allowGrowth();

   Array<int> data;
   data.setSize(0);
   data.allowGrowth();

   double current;
   int one = 1;

   const char* metermarker = "*";

   int sumcount = 0;
   int i;
   int j;
   int k;
   int sindex;
   for (i=start; i<stop; i++) {
      if (infile[i].getType() == E_humrec_interpretation) {
         for (j=0; j<infile[i].getFieldCount(); j++) {
            if (strncmp(infile[i][j], "*M", 2) == 0) {
               if (isdigit(infile[i][j][2]) && 
                  (strchr(infile[i][j], '/') != NULL)) {
                  metermarker = infile[i][j];
               }
            }
         }
      }
      if (infile[i].getType() != E_humrec_data) {
         continue;
      }
      sindex = -1;
      if (metposition.getSize() == 0) {
         current = infile[i].getBeat();
         metposition.append(current);
         count.append(one);
         data.append(metricana[i]);
         sumcount++;
      } else {
         k = 0;
         while (k < metposition.getSize()) {
            if (fabs(metposition[k] - infile[i].getBeat()) < 0.001) {
               sindex = k;
               break;
            } 
            k++;
         }
         if (sindex >= 0) {
            count[sindex]++;
            data[sindex] += metricana[i];
            sumcount++;            
         } else {
            current = infile[i].getBeat();
            metposition.append(current);
            count.append(one);
            data.append(metricana[i]);
            sumcount++;
         }
      }

   }

   Array<double> sortmet = metposition;

   qsort(sortmet.getBase(), metposition.getSize(), sizeof(double), 
      doublecompare);

   Array<double> ascores;
   Array<double> average;
   Array<double> fraction;
   ascores.setSize(metposition.getSize());
   average.setSize(metposition.getSize());
   fraction.setSize(metposition.getSize());

   double min = 99999999.0;
   for (i=0; i<ascores.getSize(); i++) {
      average[i] = (double)data[i]/count[i];
      fraction[i] = (double)count[i]/sumcount;
      ascores[i] = average[i]/fraction[i];
      if (i==0) {
         min = ascores[i];
      } else if (min > ascores[i]) {
         min = ascores[i];
      }
   }
   // normalize ascore values
   if (min > 0.0) {
      for (i=0; i<ascores.getSize(); i++) {
         ascores[i] /= min;
      }
   }

   if (!beatQ) {
      cout << metermarker << "\t*lines=" << start+1 << ":" << stop+1 
           << "\t*cnt=" << sumcount << "\t*norm=" << min << "\n";
      for (i=0; i<metposition.getSize(); i++) {
         k = 0;
         while (k < metposition.getSize()) {
            if (metposition[k] == sortmet[i]) {
               break;
            }
            k++;
         }
         if (k > metposition.getSize()) {
            continue;
         }
         cout << sortmet[i]  << "\t"
              << average[k]  << "\t" 
              << fraction[k] << "\t" 
              << 1.0/ascores[k]
              << "\n";
      }
   } else {
      displayAccentedBeatMetrics(metposition, ascores, metermarker, start, stop);
   }

}



//////////////////////////////
//
// diaplyAccentedBeatMetrics -- generate a score for each possible
//   cyclical meter/beat combination.
//

void displayAccentedBeatMetrics(Array<double>& metposition, 
      Array<double>& ascores, const char* metermarker,
      int start, int stop) {
   int beatcount = -1; 
   int beatvalue = -1;
   sscanf(metermarker, "*M%d/%d", &beatcount, &beatvalue);
   int i;

   cout << "Start = " << start << "\tStop = " << stop << endl;
   cout << "Meter: " << metermarker << endl;
   cout << "beats: " << beatcount << endl;
   cout << "base: " << beatvalue << endl;

   double difference;
   double beatscore;
   double width;

   Array<double> beatscores;
   beatscores.setSize(0);
   beatscores.allowGrowth();

   Array<double> widths;
   widths.setSize(0);
   widths.allowGrowth();

   int candidate;
   for (i=1; i<ascores.getSize(); i++) {
      width = metposition[i] - 1.0;
      if (fabs(width) < 0.001) {
         width = beatcount;
      }
      difference = beatcount/width;
      difference = difference - (int)difference;
      if (difference < 0.001 || difference > 0.999) {
         candidate = 1;
      } else {
         candidate = 0;
      }

      if (candidate) {
         beatscore = makeBeatScore(beatcount, width, metposition, ascores);
         beatscores.append(beatscore);
         widths.append(width);
         if (!summaryQ) {
            cout << "beat duration: " << metposition[i] - 1
                 << "\tscore: " << beatscore << endl;
         }
      }
   }

   if (summaryQ) {
      double max = beatscores[0];
      int index = 0;
      for (i=1; i<beatscores.getSize(); i++) {
         if (beatscores[i] > max) {
            max = beatscores[i];
            index = i;
         }
      }

      cout << "Maximum Beat: " << widths[index] 
           << "\tRating: " << beatscores[index] << endl;

   }
      
}



//////////////////////////////
//
// makeBeatScore --
//

double makeBeatScore(int totalbeats, double width, Array<double>& metposition, 
      Array<double>&ascores) {

   int i;
   double product = 1.0;
   double difference;
   int count = 0;
   for (i=0; i<metposition.getSize(); i++) {
      difference = metposition[i]/width;
      difference = difference - (int)difference;
      if (difference < 0.001 || difference > 0.999) {
         count++;
         product *= 1.0/ascores[i];
      }
   }

   int realcount = (int)(totalbeats/width + 0.5);
   for (i=0; i<realcount-count; i++) {
      product *= 0.01;
   }

   double output = pow(realcount, 1.0/2.0) * pow(product, 3.1415968/realcount);

   return output;
}



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

void printAnalysis(HumdrumFile& infile, Array& metricana) {

   int i;

   if (summaryQ || beatQ) {
      generateSummary(infile, metricana);
      return;
   }

   if (appendQ) {
      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:
            cout << infile[i].getLine() << "\t"
                 << metricana[i] << "\n";
            break;
         case E_humrec_data_comment:
            if (infile[i].equalFieldsQ("**kern")) {
               cout << infile[i].getLine() << "\t"
                    << infile[i][0] << "\n";
            } else {
               cout << infile[i].getLine() << "\t!\n";
            }
            break;
         case E_humrec_data_measure:
            if (infile[i].equalFieldsQ("**kern")) {
               cout << infile[i].getLine() << "\t"
                    << infile[i][0] << "\n";
            } else {
               cout << infile[i].getLine() << "\t=\n";
            }
            break;
         case E_humrec_data_interpretation:
            if (strncmp(infile[i][0], "**", 2) == 0) {
               cout << infile[i].getLine() << "\t"
                    << "**diam" << "\n";
               int counter = infile[i].getFieldCount();
               for (int z=0; z<counter; z++) {
                  cout << "*\t";
               }
               cout << "*metric:" << metric+2 << "\n";
            } else if (infile[i].equalFieldsQ("**kern")) {
               cout << infile[i].getLine() << "\t"
                    << infile[i][0] << "\n";
            } else {
               cout << infile[i].getLine() << "\t*\n";
            }
            break;
         }
      }

   } else {

      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:
            cout << metricana[i] << "\n";
            break;
         case E_humrec_data_comment:
            if (infile[i].equalFieldsQ("**kern")) {
               cout << infile[i][0] << "\n";
            } else {
               // do nothing
            }
            break;
         case E_humrec_data_measure:
            if (infile[i].equalFieldsQ("**kern")) {
               cout << infile[i][0] << "\n";
            } else {
               cout << "\t=\n";
            }
            break;
         case E_humrec_data_interpretation:
            if (strncmp(infile[i][0], "**", 2) == 0) {
               cout << "**diam" << "\n";
               cout << "*metric:" << metric+2 << "\n";
            } else if (infile[i].equalFieldsQ("**kern")) {
               cout << infile[i][0] << "\n";
            } else {
               // do nothing
            }
            break;
         }
      }
   }
}



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

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



//////////////////////////////
//
// doublecompare -- compare two doubles for ordering
//

int doublecompare(const void* a, const void* b) {
   if (*((double*)a) < *((double*)b)) {
      return -1;
   } else if (*((double*)a) > *((double*)b)) {
      return 1;
   } else {
      return 0;
   }
}
              

// md5sum: e62a7791feeb3e4522b94907300b65bf diametric.cpp [20050403]