(
var formatMusicData, spellingDict, lyNoteNameStr, lyOctStr, lyFinalizeMusic, lyMeasureDef,
lyRelMark, lyRelMarkNote, lyHBracket, lyStaffDef, lyTie,
lyNoteName, lyCentDev, lyFreqRatio, lyDur, lyNote, lyBeamOpen, lyBeamClosed,
consolidateNotes, consolidateRests;

// formats the data for the transcriber
formatMusicData = {arg rawMusicData;
	var maxSize, musicData;
	maxSize = 0;
	musicData = rawMusicData.collect({arg partData, p;
		var res;
		res = partData.collect({arg item, i;
			var freq, dur, amp, mult, insRef, sus, note, rest;
			# freq, dur, amp, mult, insRef = item;
			sus = dur * sign(amp);
			note = sus.collect({[freq, mult, insRef, i]});
			rest = if(p < rawMusicData.size, {(dur - sus).collect({[-1, -1, -1, i]})}, {[]});
			note ++ rest
		}).flatten;
		if(res.size > maxSize, {maxSize = res.size});
		res
	});

	//make them all the same length
	maxSize = maxSize.trunc(64) + 64;
	musicData = musicData.collect({arg partData, p; partData.extend(maxSize, partData.last)});
	musicData
};

// constants (spelling dictionary note names and octaves)
spellingDict = Dictionary.with(*
	[
		\major -> Dictionary.with(*
			[0, 7, 2, 9, 4, 11].collect({arg pc; pc->\sharps}) ++
			[5, 10, 3, 8, 1, 6].collect({arg pc; pc->\flats})
		),
		\minor -> Dictionary.with(*
			[9, 4, 11, 6, 1, 8].collect({arg pc; pc->\sharps}) ++
			[2, 7, 0, 5, 10, 3].collect({arg pc; pc->\flats})
		)
	]
);

lyNoteNameStr = Dictionary.with(*
	[
		\sharps -> ["c", "cis", "d", "dis","e", "f", "fis", "g", "gis", "a", "ais", "b"],
		\flats -> ["c", "des", "d", "ees","e", "f", "ges", "g", "aes", "a", "bes", "b"],
	]
);

lyOctStr = [",,", ",", "", "'", "''", "'''", "''''"];

//define staff
lyStaffDef = {arg name, nameShort, nameMidi;
	"\\new Staff = \"" ++ name ++ "\" \\with { \n" ++
	"instrumentName = \"" ++ name ++ "\" \n" ++
	"shortInstrumentName = \"" ++ nameShort ++ "\" \n" ++
	"midiInstrument = #\"" ++ nameMidi ++ "\" \n" ++
	"\n}\n"
};

// add music preamble
lyFinalizeMusic = {arg lyStr, part, name, nameShort, nameMidi, clef;
	"\\new StaffGroup  \\with {\\remove \"System_start_delimiter_engraver\"}\n<<\n" ++
	lyStaffDef.value(name, nameShort, nameMidi)  ++
	"<<\n\n{ " +
	"\n\\set Score.markFormatter = #format-mark-box-numbers " +
	"\\tempo 2 = 60\n" +
	"\\numericTimeSignature \\time 2/2\n" +
	"\\clef " ++ clef ++ "\n" ++ lyStr + "\\fermata" +
	" }>> \\bar \"|.\" \n} \n\n>>" ++
	"\n>>"
};

lyRelMarkNote = {arg root, lastRoot, part, clef;
	if(root[part][2] != [[1], [1]], {
		"\\stopStaff s8. \\startStaff \\clef" + clef + "s16 \n" ++
		"\\once \\override TextScript.color = #(rgb-color 0.6 0.6 0.6) \n " ++
		"\\tweak Accidental.color #(rgb-color 0.6 0.6 0.6) \n " ++
		"\\tweak NoteHead.color #(rgb-color 0.6 0.6 0.6) \n " ++
		lyNote.value(lastRoot[part][1], 1, lastRoot[part][0], nil, \sharps, true, true, false) +
		"\\hide c" ++ [nil, "", "'", "''"][part] ++ "8 \n "
	}, {
		"\\stopStaff s4. \\startStaff \\clef" + clef + "s16 \n"
	}) ++
	lyNote.value(root[part][3], 1, root[part][2], nil, \sharps, true, false, true)
};

lyHBracket = {arg fr, yOffset, sPair1, sPair2, edgeH1, edgeH2;
	"-\\tweak HorizontalBracket.Y-offset #" ++ yOffset ++ "\n " ++
	"-\\tweak HorizontalBracket.shorten-pair #'(" ++ sPair1 + "." + sPair2 ++") \n " ++
	"-\\tweak HorizontalBracket.edge-height #'("  ++ edgeH1 + "." + edgeH2 ++ ") \n " ++
	"-\\tweak HorizontalBracketText.text" + fr + "\\startGroup \n "
};

lyRelMark = {arg root, lastRoot, section, subsection;
	var sectionMark;
	sectionMark = "\\mark \\markup { \\bold \\override #'(box-padding . 0.5) \\box " ++ section ++ "." ++ subsection ++ " } \n";
	if((section == 1) && (subsection > 1),
		{
			"\\once \\override Score.RehearsalMark.self-alignment-X = #0 \n " ++
			"\\once \\override Score.RehearsalMark.Y-offset = #5 \n " ++
			"\\once \\override Score.RehearsalMark.X-offset = #1 \n " ++
			sectionMark
		}, {
			"\\mark \\markup { \n" ++
			"\\halign #-1 \n " ++
			"\\relMark ##{ { \n " ++
			"\\time 15/8 \n " ++
			"\\once \\override Staff.Clef #'stencil = ##f \n " ++
			sectionMark ++

			lyRelMarkNote.value(root, lastRoot, 1, "bass") ++ "^\\markup{\\large \\raise #2 \"III\"}" ++

			lyHBracket.value(lyFreqRatio.value(root[1][4][2], nil, true, 0, false), 8.5, 1, 2, 1, 1) ++
			lyHBracket.value(lyFreqRatio.value(root[2][4][1], nil, true, 0, false), 5.5, 3, 3, 0, 0) ++

			"\\hide c16 \n " ++

			lyRelMarkNote.value(root, lastRoot, 2, "alto") ++ "^\\markup{\\large \\raise #2 \"II\"}" +
			"\\stopGroup \\hide c'16 \n " ++

			lyHBracket.value(lyFreqRatio.value(root[2][4][2], nil, true, 0, false), 5.5, 1, 3, 0, 0) ++

			lyRelMarkNote.value(root, lastRoot, 3, "treble") ++ "^\\markup{\\large \\raise #2 \"I\"}" +
			"\\stopGroup \\stopGroup \n " ++
			"\\hide c''16 \n " ++
			"}#}}"
	});
};

// barline and ossia definition
lyMeasureDef = {arg sectionData, insName, part, beat;
	var ossia = "", barline = "|", break = "";
	if(sectionData != nil, {
		var root, lastRoot, section, subsection;
		# root, lastRoot, section, subsection = sectionData;
		ossia = lyRelMark.value(root, lastRoot, section, subsection);
		barline = "\\bar \"||\"";
		if(sectionData[4], {barline = "\\bar \"|.|\""});
		if(sectionData[5], {barline = "\\bar \".|\""});
	});
	if((beat % 16) == 0, {break = "\\break \\noPageBreak"});
	if((beat % (16 * 3)) == 0, {break = "\\pageBreak"});
	if(beat != 0, {"}\n>>\n" + barline + break}, {""}) + "\n<<\n" ++ ossia + "{"
};

// add tie
lyTie = {"~"};

lyNoteName = {arg freq, spellingPref = \sharps;
	if(freq != -1, {
		lyNoteNameStr[spellingPref][((freq.cpsmidi).round(1) % 12)] ++
		lyOctStr[(((freq).cpsmidi).round(1) / 12).asInteger - 2];
	},{"r"});
};

lyCentDev = {arg freq, padding = true;
	var centDev;
	centDev = ((freq.cpsmidi - (freq.cpsmidi).round(1)) * 100).round(1).asInteger;
	"^\\markup { " ++ if(padding, {"\\pad-markup #0.2 \""}, {"\""}) ++
	if(centDev >= 0, {"+"}, {""}) ++ centDev.asString ++ "\"}"
};

lyFreqRatio = {arg freqRatioMult, ref, padding = true, lower = 3, attachedToNote = true;
	var res, ratio;
	res = "\\markup {" + if(attachedToNote, {""}, {"\\normalsize"}) +
	"\\lower #" ++ lower + if(padding, {"\\pad-markup #0.2 "}, {" "});
	ratio = "\"" ++ freqRatioMult[0].product.asInteger ++ "/" ++ freqRatioMult[1].product.asInteger ++ "\" }";
	res = if(ref != nil,
		{
			res ++ "\\concat{ \"" ++ [nil, "III", "II", "I"][ref] ++ "\"\\normal-size-super " ++ ratio ++ "}"
		}, {
			res ++ ratio
		}
	);
	if(attachedToNote, {"_" ++ res}, {res})
};


lyNote = {arg freq, noteLength, freqRatioMult, ref, spellingPref = \sharps, addMarkup = true, frHide = false, padding = true;
	lyNoteName.value(freq, spellingPref) ++
	lyDur.value(noteLength) ++
	if(addMarkup, {
		"<MARKUP" ++
		lyCentDev.value(freq, padding) ++
		if(frHide, {""}, {lyFreqRatio.value(freqRatioMult, ref, padding)}) ++
		"MARKUP>"
	}, {""})
};

lyDur = {arg noteLength;
	switch(noteLength, 1, {"16"}, 2, {"8"}, 3, {"8."}, 4, {"4"});
};

lyBeamOpen = {"["};

lyBeamClosed = {"]"};

consolidateNotes = {arg lyStr, part;
	var noteRegex, markupRegex, fullNoteRegex, restRegex, fullRestRegex, res;
	noteRegex = "(?<n>[a-g](?:es|is)?(?:[,']*?)?4)";
	markupRegex = if(part != 0, {"(<MARKUP.{75,85}MARKUP>)?"}, {"(<MARKUP.{75,115}MARKUP>)?"});
	fullNoteRegex = noteRegex ++ markupRegex ++ "(?:\\h+~\\h+\\k<n>)";
	restRegex = "(?<r>r4)";
	fullRestRegex = "(?<r>r4)(?:(\\h+)\\k<r>)";
	res = lyStr;
	[6, 4, 3, 2].do({arg len;
		[fullNoteRegex, fullRestRegex].do({arg regex;
			res.findRegexp(regex ++ "{" ++ (len-1) ++ "}").clump(3).do({arg match;
				var word, note, markup, lyDur;
				word = match[0][1];
				note = match[1][1];
				markup = match[2][1];
				lyDur = switch(len, 6, {"1."}, 4, {"1"}, 3, {"2."}, 2, {"2"});
				res = res.replace(word, note.replace("4", lyDur) ++ markup)});
		});
	});
	res.replace("<MARKUP", "").replace("MARKUP>", "");
};

~transcribe = {arg rawMusicData, sectionData, seed;
	var basePath, scoreFile, musicData, insData, insNames, insNamesShort, insMidi, insClef;

	basePath = ~dir +/+ ".." +/+ "lilypond" +/+ "seed_" ++ seed;
	basePath.mkdir;
	(basePath +/+ "includes").mkdir;

	scoreFile = File(basePath +/+ "tkam_score.ly".standardizePath,"w");
	scoreFile.write(File.readAllString(basePath +/+ ".." +/+ "template" +/+ "tkam_score_template.ly").replace("seed: xxx", "seed: " ++ seed));
	scoreFile.close;

	musicData = formatMusicData.value(rawMusicData);

	insData = [
		["*", "*", "clarinet", "\"treble_8\""],
		["III", "III", "clarinet", "bass"],
		["II", "II", "clarinet", "alto"],
		["I", "I", "clarinet", "treble"]
	];

	insNames = insData.slice(nil, 0);
	insNamesShort = insData.slice(nil, 1);
	insMidi = insData.slice(nil, 2);
	insClef = insData.slice(nil, 3);

	musicData.do({arg part, p;
		var lyFile, lyStr, lastMusAtom, measureCount, spellingPref,
		tmpSectionData, pcRoot, partLookup, quality;

		//create file
		lyFile = File(basePath +/+ "includes" +/+ "part_" ++ ["star", "III", "II", "I"][p] ++ ".ly".standardizePath,"w");

		//start lypond directives
		lyStr = "";
		lastMusAtom = nil;
		measureCount = 0;
		spellingPref = \sharps;
		tmpSectionData = nil;
		part.clump(4).do({arg beat, i;
			var gSum;
			gSum = 0;
			beat.separate({arg a, b;
				((a[0] != -1) || (b[0] != -1)) && (a != b)}).do({arg group, g;
				var noteLength, curMusAtom, freq, freqRatioMult, ref, isSame, isRest, isFirst, isLast,
				isTied, isMeasureBound, isBeamStart, isBeamEnd;

				noteLength = group.size;
				gSum = gSum + noteLength;
				curMusAtom = group[0];
				freq =  curMusAtom[0];
				freqRatioMult = curMusAtom[1];
				ref = curMusAtom[2];
				# isSame, isRest, isFirst, isLast = [curMusAtom == lastMusAtom, freq == -1, g == 0, gSum == 4];
				# isTied, isMeasureBound = [isSame && isRest.not, isFirst && ((i % 4) == 0)];
				# isBeamStart, isBeamEnd = [(noteLength != 4) && isFirst, (noteLength != 4) && isLast];

				//add ties
				if(isTied, {lyStr = lyStr + lyTie.value});

				//add barline and ossia definition
				if(isMeasureBound, {lyStr = lyStr + lyMeasureDef.value(sectionData[i], insNames[p], p, i)});

				//add note data
				if(sectionData[i] != nil, {
					tmpSectionData = sectionData[i];
				});
				if(isTied.not, {
					partLookup = if((p != 0) || [1, 2, 3].includes(ref).not , {p}, {ref});
					pcRoot = ((tmpSectionData[0][partLookup][3].cpsmidi).round(1) % 12).asInteger;
					quality = if(tmpSectionData[0][partLookup][1][2] == [[ 1, 5 ], [ 1, 2, 2 ]], {\major}, {\minor});
					spellingPref = spellingDict[quality][pcRoot];
					if(p == 0, {[(i / 4).asInteger, partLookup, pcRoot, quality]});
				});

				lyStr = lyStr + lyNote.value(freq, noteLength, freqRatioMult, ref, spellingPref, isSame.not && isRest.not);

				//beam group
				if(isBeamStart, {lyStr = lyStr ++ lyBeamOpen.value});
				if(isBeamEnd, {lyStr = lyStr ++ lyBeamClosed.value});

				lastMusAtom = curMusAtom;
			});
		});

		//wrap music and add staff definitions
		lyStr = lyFinalizeMusic.value(lyStr, p, insNames[p], insNamesShort[p], insMidi[p], insClef[p]);

		//consolidate notes and rests
		lyStr = consolidateNotes.value(lyStr, p);

		//write file
		lyFile.write(lyStr);
		lyFile.close;
	});
};


//~~~~~~~~~~~~GENERATE SCORE DATA~~~~~~~~~~~~
~genScoreData = {arg ensData;
	var res;
	res = ensData.collect({arg partData;
		partData.flop.collect({arg data, d; if(d == 1, {data.differentiate ++ [10]}, {[0] ++ data})})
	});
	res.collect({arg part; part.flop})
};
)