//
// Programmer:    Craig Stuart Sapp <craig@ccrma.stanford.edu>
// Creation Date: Wed Jan  9 14:29:54 PST 2002
// Last Modified: Thu Apr  3 10:23:41 PST 2008 (added Humdrum output)
// Filename:      tools/henonfile.cpp
// URL:           https://github.com/craigsapp/midifile/blob/master/tools/henonfile.cpp
// Syntax:        C++11
// vim:           ts=3
//
// Description:   Creates a fractal melodic line based on the
//                Henon Map.  Output can be either a MIDI file, Guido
//                Music Notation (GMN), Humdrum, or plain text.
//

#include "MidiFile.h"
#include "Convert.h"
#include "Options.h"
#include "CircularBuffer.h"

#include <stdio.h>
#include <string.h>

using namespace std;
using namespace smf;


void        checkOptions        (Options& opts, int argc, char** argv);
void        example             (void);
void        usage               (const char* command);
void        createHenon         (double alpha, double beta, double x0,
                                 double y0, int maxcount, MidiFile& midifile);
int         checkTermination    (int key);
void        storeInMidiFile     (MidiFile& midifile, int key);
void        printGuidoNotation  (void);
void        printHumdrumNotation(void);
char*       convertMidiToGuido  (char* buffer, Array<char> notelist, int index);
void        printLeftHandGuido  (void);
void        printRightHandGuido (void);

// User interface variables:
Options     options;
int         maxcount  = 10000;  // used with the -n option
double      alpha = -1.56693;   // used with the -a option
double      beta  = -0.011811;  // used with the -b option
double      x0 = 0.0;           // x-axis starting point
double      y0e = 0.0;          // y-axis starting point
int         textQ = 0;          // used with the --text option
int         guidoQ = 0;         // used with the -g option
int         humdrumQ = 0;       // used with the -u option
int         repeatQ = 0;        // used with the -r option
const char* filename = "test.mid"; // filename to write MIDI file
int         tpq = 96;           // ticks per quarter note in MIDI file
int         divisions = 4;      // number of notes per quarter note
int         instrument = 0;     // used with the -i option
double      tempo = 120.0;      // used with the -t option
Array<char> notelist;           // for printing in Guido Music Notation
int         minNote = 30;       // minimum note to play
int         maxNote = 100;      // maximum note to play


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

int main(int argc, char** argv) {
	checkOptions(options, argc, argv); // process the command-line options

	MidiFile midifile;
	midifile.setTicksPerQuarterNote(tpq);
	midifile.allocateEvents(0, 2 * maxcount + 500);  // pre allocate space for
	                                                 // max expected MIDI events
	notelist.setSize(maxcount+10);
	notelist.setSize(0);
	notelist.allowGrowth();

	midifile.absoluteTime();

	Array<uchar> mididata(2);
	mididata[0] = 0xc0;       // patch change on MIDI channel 1
	mididata[1] = (uchar) instrument;  // user input instrument
	midifile.addEvent(0, 0, mididata);

	// write the tempo to the midifile
	mididata.setSize(6);
	mididata[0] = 0xff;      // meta message
	mididata[1] = 0x51;      // tempo change
	mididata[2] = 0x03;      // three bytes to follow
	int microseconds = (int)(60.0 / tempo * 1000000.0 + 0.5);
	mididata[3] = (microseconds >> 16) & 0xff;
	mididata[4] = (microseconds >> 8)  & 0xff;
	mididata[5] = (microseconds >> 0)  & 0xff;
	midifile.addEvent(0, 0, mididata);

	createHenon(alpha, beta, x0, y0e, maxcount, midifile);
	if (guidoQ) {
		printGuidoNotation();
	} else if (humdrumQ) {
		printHumdrumNotation();
	} else {
		midifile.write(filename);
	}

	return 0;
}

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



//////////////////////////////
//
// createHenon --
//

void createHenon(double alpha, double beta, double x0, double y0e,
	int maxcount, MidiFile& midifile) {

	double x = x0;
	double y = y0e;
	int termination = 0;

	for (int i=0; i<maxcount; i++) {
		double newx = 1 + alpha * x * x + beta * y;
		double newy = x;
		x = newx;
		y = newy;

		int key = (int)((x + 1.0)/2.0 * 127.0 + 0.5);
		if (key < minNote) {
			key = 0;
		}
		if (key > maxNote) {
			key = 0;
		}
		if (repeatQ) {
			termination = 0;
		} else {
			termination = checkTermination(key);
		}
		if (textQ) {
			cout << key << "\n";
			if (termination != 0) {
				cout << "REPEAT" << termination << endl;
				exit(0);
			}
		} else {
			storeInMidiFile(midifile, key);
			if (termination != 0) {
				midifile.write(filename);
				exit(0);
			}
		}
	}

}



//////////////////////////////
//
// storeInMidiFile --
//

void storeInMidiFile(MidiFile& midifile, int key) {
	static int timer = tpq;   // start after one beat (for patch change)
	char note = (char)key;

	// don't store extreme notes -- this gives interesting rhythms sometimes.
	if (key < minNote || key > maxNote) {
		note = 0;
		notelist.append(note);
		timer += tpq/divisions;
		return;
	}
	notelist.append(note);       // store note for displaying Guido Notation
	Array<uchar> midinote(3);
	midinote[0] = 0x90;
	midinote[1] = key;
	midinote[2] = 64;
	midifile.addEvent(0, timer, midinote);
	midinote[0] = 0x80;
	timer += tpq/divisions;
	midifile.addEvent(0, timer, midinote);
}



//////////////////////////////
//
// checkTermination --
//

int checkTermination(int key) {
	static CircularBuffer<int> memory;
	static int init = 0;
	if (init == 0) {
		init = 1;
		memory.setSize(1000);
		memory.reset();
	}
	memory.insert(key);
	if (memory.getCount() < 40 + 10) {
		return 0;
	}

	for (int j=1; j<20; j++) {
		int cycleQ = 1;
		for (int i=0; i<40; i++) {
			if (memory[i] != memory[i+j]) {
				cycleQ = 0;
				break;
			}
		}
		if (cycleQ == 1) {
			return j;
		}
	}

	// no 1-9 period cycles detected
	return 0;

}




//////////////////////////////
//
// checkOptions --
//

void checkOptions(Options& opts, int argc, char* argv[]) {
	opts.define("n|max-number=i:10000", "Maximum number of notes to generate");
	opts.define("a|alpha=d:-1.56693",   "alpha factor");
	opts.define("b|beta=d:-0.011811",   "beta factor");
	opts.define("x|x0=d:0.0",           "initial x value");
	opts.define("y|y0=d:0.0",           "initial y value");
	opts.define("max|max-note=i:100",   "maximum note to play; higher is rest");
	opts.define("min|min-note=i:30",    "minimum note to play; lower is rest");
	opts.define("text=b",               "display output as text only");
	opts.define("g|guido=b",            "Guido Music Notation output");
	opts.define("u|humdrum=b",          "Humdrum data file output");
	opts.define("i|instrument=i:0",     "General MIDI instrument number");
	opts.define("t|tempo=d:120",        "Tempo");
	opts.define("d|divisions=i:4",      "Number of notes per quarter note");
	opts.define("r|allow-repeats=b",    "Do not stop at cyclical patterns");

	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, Jan 2002" << endl;
		exit(0);
	} else if (opts.getBoolean("version")) {
		cout << argv[0] << ", version: 3 Apr 2008" << endl;
		cout << "compiled: " << __DATE__ << endl;
		exit(0);
	} else if (opts.getBoolean("help")) {
		usage(opts.getCommand());
		exit(0);
	} else if (opts.getBoolean("example")) {
		example();
		exit(0);
	}

	maxcount   = opts.getInteger("max-number");
	alpha      = opts.getDouble("alpha");
	beta       = opts.getDouble("beta");
	x0         = opts.getDouble("x0");
	y0e        = opts.getDouble("y0");
	textQ      = opts.getBoolean("text");
	guidoQ     = opts.getBoolean("guido");
	humdrumQ   = opts.getBoolean("humdrum");
	if (textQ == 1) {
		guidoQ   = 0;
		humdrumQ = 0;
	}
	repeatQ    = opts.getBoolean("allow-repeats");
	tempo      = opts.getDouble("tempo");
	divisions  = opts.getInteger("divisions");
	instrument = opts.getInteger("instrument");

	maxNote = opts.getInteger("max-note");
	minNote = opts.getInteger("min-note");
	if (minNote < 0)   minNote = 0;
	if (minNote > 126) minNote = 126;
	if (maxNote < 0)   maxNote = 0;
	if (maxNote > 126) maxNote = 126;
	if (minNote > maxNote) {
		int temp = minNote;
		minNote = maxNote;
		maxNote = temp;
	}

	if (instrument < 0) {
		instrument = 0;
	} else if (instrument > 127) {
		instrument = 127;
	}

	if (tempo < 4) {
		tempo = 4;
	} else if (tempo > 1000) {
		tempo = 1000;
	}

	if (humdrumQ == 0 && guidoQ == 0 && textQ == 0 && opts.getArgCount() != 1) {
		usage(opts.getCommand());
		exit(1);
	}
	if (humdrumQ == 0 && textQ == 0 && guidoQ == 0) {
		filename = opts.getArg(1);
	}

}



//////////////////////////////
//
// example --
//

void example(void) {
	// to add
}



//////////////////////////////
//
// usage --
//

void usage(const char* command) {
	cout << "Usage: " << command << " outputfile" << endl;
}



//////////////////////////////
//
// printGuidoNotation --
//

void printGuidoNotation(void) {
	cout << "{ ";
	printRightHandGuido();
	cout << ", ";
	printLeftHandGuido();
	cout << " }\n";
}



//////////////////////////////
//
// printHumdrumNotation --
//

void printHumdrumNotation(void) {
	char buffer[1024] = {0};
	cout << "!!!alpha:\t" << alpha << "\n";
	cout << "!!!beta:\t" << beta << "\n";
	cout << "!!!start:\t(" << x0 << ", " << y0e << ")\n";
	cout << "**kern\n";
	int i;
	for (i=0; i<notelist.getSize(); i++) {
		cout << "16";
		if (notelist[i] <= 0) {
			cout << "r";
		} else {
			cout << Convert::base12ToKern(buffer, notelist[i]);
		}
		cout << "\n";
		if ((i+1) % 32 == 0) {
			cout << "=" << (i / 32) + 2 << "\n";
		}

	}
	cout << "*-" << endl;
}



//////////////////////////////
//
// printRightHandGuido --
//

void printRightHandGuido(void) {
	char buffer[128] = {0};
	cout << "[";
	int i;
	cout << "\\meter<\"2/4\">\n";
	for (i=0; i<notelist.getSize(); i++) {
		if (notelist[i] >= 60) {
			cout << convertMidiToGuido(buffer, notelist, i) << " ";
		} else {
			cout << "_/16" << " ";
		}
		if ((i+1)%16 == 0) {
			cout << "\n\t";
		}
	}
	cout << "]\n";
}



//////////////////////////////
//
// printLeftHandGuido --
//

void printLeftHandGuido(void) {
	char buffer[128] = {0};
	cout << "[";
	int i;
	cout << "\\meter<\"2/4\">\n";
	for (i=0; i<notelist.getSize(); i++) {
		if (notelist[i] < 60) {
			cout << convertMidiToGuido(buffer, notelist, i) << " ";
		} else {
			cout << "_/16" << " ";
		}
		if ((i+1)%16 == 0) {
			cout << "\n\t";
		}
	}
	cout << "]\n";
}



//////////////////////////////
//
// convertMidiToGuido -- convert from MIDI note number to Guido Music
//     Notation note name.
//

char* convertMidiToGuido(char* buffer, Array<char> notelist, int index) {
	if (notelist[index] == 0) {
		strcpy(buffer, "_/16");
		return buffer;
	}
	int octave = (int)notelist[index] / 12 - 4;
	if (octave > 2) octave--;

	char octbuf[32] = {0};
	sprintf(octbuf, "%d", octave);

	int pc = (int)notelist[index] % 12;
	switch (pc) {
		case 0:  strcpy(buffer, "c");  break;
		case 1:  strcpy(buffer, "c#");  break;
		case 2:  strcpy(buffer, "d");   break;
		case 3:  strcpy(buffer, "e&");  break;
		case 4:  strcpy(buffer, "e");   break;
		case 5:  strcpy(buffer, "f");   break;
		case 6:  strcpy(buffer, "f#");  break;
		case 7:  strcpy(buffer, "g");   break;
		case 8:  strcpy(buffer, "a&");  break;
		case 9:  strcpy(buffer, "a");   break;
		case 10: strcpy(buffer, "b&");  break;
		case 11: strcpy(buffer, "b");   break;
	}
	strcat(buffer, octbuf);
	strcat(buffer, "/16");
	return buffer;
}



