(
var modelView, transportView, mixerView, guiLayout, playButton, randButton;

// model / transform
modelView = {var transformGrid, transformButtons, layouts, layoutGrid, layoutButtons,
	fullScreenButton, decoratorButton, transcribeButton, contPlayButton, exportButton, displayGrid, model;

	// sets transform
	transformGrid = GridLayout();
	transformButtons = Array.fill2D(7, 4, {arg r, c; Button().states_(
		[[c.asString, Color.grey, Color.white], [c.asString, Color.white, Color.grey]])});
	transformButtons.do({arg row, rIndex;
		row.do({arg button, cIndex;
			if((rIndex < 5) || (cIndex < 2), {
				transformGrid.add(button, rIndex, cIndex + 1);
				button.action = {transformButtons[rIndex].do(
					{arg b, i; if(button == b, {b.value = 1}, {b.value = 0})});
				~transform[rIndex] = cIndex; if(rIndex < 4, {~tileMap = ~mapAll.value(6, ~transform)}); ~tileWin.refresh}.inEnvir})});
		transformGrid.add(if(rIndex < 4,
			{StaticText().string_("child " ++ rIndex.asString).align_(\center)},
			{StaticText().string_(["rotation", "mirror", "invert"][rIndex - 4])
				.align_(\center)}), rIndex, 0)});
	~transform.do({arg v, rIndex; transformButtons[rIndex][v].value = 1});

	// sets layout
	layouts = ["depth level 6 only (max depth)", "depth level 5 only", "depth level 4 only", "hierarchical (embedded)",
		"", "", ""];
	layoutGrid = GridLayout();
	layoutButtons = layouts.collect({arg l, i; Button().states_(
		[[layouts[i], Color.grey, Color.white], [l, Color.white, Color.grey]])});
	layoutButtons.do({arg button, bIndex; if(bIndex < 4,
		{layoutGrid.add(button, bIndex, 0);
		button.action = {layoutButtons.do({arg b; if(button == b, {b.value = 1}, {b.value = 0});
				~layoutState = bIndex; ~tileWin.refresh})}.inEnvir},
		{layoutGrid.add(StaticText().string_(""), bIndex, 0)})});
	layoutButtons[0].value = 1;

	randButton = Button().states_([["random transform", Color.grey, Color.white]]).action_({
		~transform = Array.fill(7, {arg i; if(i < 5, {4.rand}, {2.rand})});
		~tileMap = ~mapAll.value(6, ~transform);
		~tileWin.refresh;
		transformButtons.do({arg row; row.do({arg button; button.value = 0})});
		~transform.do({arg v, rIndex; transformButtons[rIndex][v].value = 1})}.inEnvir);

	// sets display
	fullScreenButton = Button().states_([["fullscreen", Color.grey, Color.white],
		["fullscreen", Color.white, Color.grey]]).action_({arg button;
		if(button.value == 0, {~tileWin.endFullScreen}, {~tileWin.fullScreen})}.inEnvir);

	~tileWin.view.keyDownAction = {arg doc, char, mod, unicode, keycode, key;
		case
		// <ctrl + f> = enter full screen
		{mod == 262144 && key == 70} {~tileWin.fullScreen; fullScreenButton.value = 1}
		// <esc> = exit full screen
		{mod == 0 && key == 16777216} {~tileWin.endFullScreen; fullScreenButton.value = 0}}.inEnvir;

	decoratorButton = Button().states_([["window decorator", Color.grey, Color.white],
		["window decorator",Color.white, Color.grey]]).action_({arg button;
		~tileWin.close;
		if(button.value == 0, {~launchTileVisualizer.value(false, false)},
			{~launchTileVisualizer.value(true, false)})}.inEnvir);
	decoratorButton.value = 1;

	// set continuous play option (disables transform buttons when set)
	contPlayButton = Button().states_([["continuous play", Color.grey, Color.white],
		["continuous play", Color.white, Color.grey]]).action_({arg elem;
		if(elem.value == 0, {~continuousPlay = false;
			transformButtons.do({arg row; row.do({arg button; button.enabled = true})});
			randButton.enabled = true}, {
			transformButtons.do({arg row; row.do({arg button; button.enabled = false})});
			randButton.enabled = false;
			if(playButton.value == 0, {
				fork({
					~continuousPlay = true;
					randButton.valueAction = 0;
					1.wait;
					playButton.valueAction = 1}, AppClock)})})}.inEnvir);

	// transcribe
	transcribeButton = Button().states_([["transcribe", Color.grey, Color.white]]).action_({
		var matrixH4, matrixH5, matrixH6,dataH4, dataH5, dataH6;
		matrixH4 = ~matricize.value([~tileMap[4]], 4, 0, ~transform);
		matrixH5 = ~matricize.value([~tileMap[5]], 5, 0, ~transform);
		matrixH6 = ~matricize.value([~tileMap[6]], 6, 0, ~transform);
		dataH4 = ~genData.value(matrixH4, ~transform, 1, 0, 1);
		dataH5 = ~genData.value(matrixH5, ~transform, 1, 0, 4);
		dataH6 = ~genData.value(matrixH6, ~transform, 4, 0, 16);
		~transcribe.value(dataH4, 16, "hl_4");
		~transcribe.value(dataH5, 4, "hl_5");
		~transcribe.value(dataH6, 1, "hl_6", true)}.inEnvir);

	// export tile image
	exportButton = Button().states_([["export image", Color.grey, Color.white]]).action_({var img;
		img = if(~transform[4] % 2 == 0, {Image.new(6000, 3000)}, {Image.new(3000, 6000)});
		img.draw({~tileDrawFunc.value(img, ~tileMap, ~transform, ~layoutState)});
		FileDialog({ arg path; img.write(path[0])}, {}, 0, 1)}.inEnvir);

	displayGrid = GridLayout();
	displayGrid.add(fullScreenButton, 0, 0);
	displayGrid.add(decoratorButton, 1, 0);
	5.do({arg i; displayGrid.add(StaticText().string_(""), i + 2, 0)});

	model = GridLayout();
	model.add(StaticText().string_("transform code"), 0, 0);
	model.add(transformGrid, 1, 0); model.add(randButton, 2, 0);
	model.add(StaticText().string_(""), 3, 0); model.add(StaticText().string_(""), 4, 0);
	model.add(StaticText().string_(""), 0, 1);
	model.add(StaticText().string_("layout"), 0, 2);
	model.add(layoutGrid, 1, 2);
	model.add(StaticText().string_(""), 0, 3);
	model.add(StaticText().string_("display"), 0, 4);
	model.add(displayGrid, 1, 4); model.add(contPlayButton, 2, 4); model.add(transcribeButton, 3, 4); model.add(exportButton, 4, 4);
	model};

// transport
transportView = {
	arg hash; var clockStringFunc, metronomeStringFunc, metronomeColorFunc,
	transport, posSlider, startPos = 0, startPosText, pauseButton, clock, metronome;

	// update clock and metronome
	clockStringFunc = {
		arg curBeat; var measure, beat;
		beat = (((curBeat / 2) % 4) + 1).trunc.asInteger.asString;
		measure = ((curBeat / 8) + 1).trunc.asInteger.asString;
		if(measure.size == 1, {measure = "  " ++ measure});
		if(measure.size == 2, {measure = " " ++ measure});
		measure ++ ":" ++ beat};
	// [-30, -105, -104].collect(_.asAscii).as(String) and [-30, -105, -113].collect(_.asAscii).as(String) are unicode inverse bullet and normal bullet, respectively
	metronomeStringFunc = {arg curBeat; case {curBeat % 8 < 1} {[-30, -105, -104].collect(_.asAscii).as(String)}
		{curBeat % 2 < 1} {[-30, -105, -113].collect(_.asAscii).as(String)} {true} {" "}};
	metronomeColorFunc = {arg curBeat; case {curBeat % 32 < 1} {Color.red} {curBeat % 8 < 1} {Color.blue} {true} {Color.black}};

	// get current time and call update clock and metronome
	OSCFunc({arg msg, time; {
		if(msg[2] == hash,{
			if(msg[3] > 0, {clock.string = clockStringFunc.value(msg[3])});
			metronome.stringColor = metronomeColorFunc.value(msg[3]);
			metronome.string = metronomeStringFunc.value(msg[3]);
			// if continuous play, when finished, delete all autogenerated files, reselect a transform, and then start
			if(msg[3] == 1031, {
				playButton.valueAction = 0;
				fork({
					if(~continuousPlay, {var baseDir;
						baseDir = ~dir +/+ ".." +/+ "audio" +/+ "transform_" ++ ~transform.join ++ "_audio";
						File.delete(baseDir +/+ "hl_6_fundamental.wav");
						File.delete(baseDir +/+ "hl_6_harmonics.wav");
						File.delete(baseDir +/+ "hl_5_high_noise.wav");
						File.delete(baseDir +/+ "hl_4_low_noise.wav");
						File.delete(baseDir);
						randButton.valueAction = 0;
						posSlider.valueAction = 0;
						2.wait;
						playButton.valueAction = 1
					})}, AppClock)})})}.inEnvir.defer}.inEnvir,'/tr', s.addr);

	// transport gui items
	transport = HLayout(
		// play / stop button
		playButton = Button().states_([["play", Color.black], ["stop", Color.black, Color.grey]]).action_({arg elem;
			fork {var cond, baseDir;
				cond = Condition.new;
				baseDir = ~dir +/+ ".." +/+ "audio" +/+ "transform_" ++ ~transform.join ++ "_audio";
				// generate audio files if they do not exist
				if(File.exists(baseDir +/+ "hl_6_fundamental.wav").not ||
						File.exists(baseDir +/+ "hl_6_harmonics.wav").not ||
						File.exists(baseDir +/+ "hl_5_high_noise.wav").not ||
						File.exists(baseDir +/+ "hl_4_low_noise.wav").not ||
					    File.exists(baseDir +/+ "click.wav").not, {
							var matrixH4, matrixH5, matrixH6, dataH4, dataH5, dataH6,
							patternH4, patternH5, patternH6Fund, patternH6Harms, click;
							matrixH4 = ~matricize.value([~tileMap[4]], 4, 0, ~transform);
							matrixH5 = ~matricize.value([~tileMap[5]], 5, 0, ~transform);
							matrixH6 = ~matricize.value([~tileMap[6]], 6, 0, ~transform);
							dataH4 = ~genData.value(matrixH4, ~transform, 1, 0, 1, 0.5, 1);
							dataH5 = ~genData.value(matrixH5, ~transform, 1, 0, 4, 0.5, 1);
							dataH6 = ~genData.value(matrixH6, ~transform, 4, 0, 16);
							patternH4 = ~genPattern.value(dataH4, "hl_4_low_noise", 16, true, cond); cond.hang;
							patternH5 = ~genPattern.value(dataH5, "hl_5_high_noise", 4, true, cond); cond.hang;
							patternH6Fund = ~genPattern.value([dataH6[0]], "hl_6_fundamental", 1, true, cond); cond.hang;
							patternH6Harms = ~genPattern.value(dataH6[1..3], "hl_6_harmonics", 1, true, cond); cond.hang;
						    click = ~genPattern.value([(0..7).wrapExtend(8 * 129)], "click", 2, true, cond); cond.hang});

				// load buffers if the transform has changed
				if(~loadedTransform != ~transform, {var baseDir;
					baseDir = ~dir +/+ ".." +/+ "audio" +/+ "transform_" ++ ~transform.join ++ "_audio";
					~buf1 = Buffer.read(s, baseDir +/+ "hl_6_fundamental.wav", action: {cond.unhang}); cond.hang;
					~buf2 = Buffer.read(s, baseDir +/+ "hl_6_harmonics.wav", action: {cond.unhang}); cond.hang;
					~buf3 = Buffer.read(s, baseDir +/+ "hl_5_high_noise.wav", action: {cond.unhang}); cond.hang;
					~buf4 = Buffer.read(s, baseDir +/+ "hl_4_low_noise.wav", action: {cond.unhang}); cond.hang});
					~loadedTransform = ~transform.deepCopy;


				// play / stop functionality (create synth if it does not exist
				{if(~play == nil, {~play = Synth.new(\play, [\hash, hash, \playRate, 0, \startTrig, 0,
						\sineBuf1, ~buf1, \sineBuf2, ~buf2, \highNoiseBuf, ~buf3, \lowNoiseBuf, ~buf4, \tempo, ~tempo])});
					pauseButton.value = 0;
					if(elem.value == 0, {
						clock.string = clockStringFunc.value((startPos * 129).trunc * 8);
						~play.set(\playRate, 0, \startTrig, 0,
							\sineBuf1, ~buf1, \sineBuf2, ~buf2, \highNoiseBuf, ~buf3, \lowNoiseBuf, ~buf4);
						clock.string = clockStringFunc.value((startPos * 129).trunc * 8)},{
						~play.set(\startPos, startPos, \playRate, 1, \startTrig, 1,
							\sineBuf1, ~buf1, \sineBuf2, ~buf2, \highNoiseBuf, ~buf3, \lowNoiseBuf, ~buf4)})}.inEnvir.defer}}.inEnvir),

		// pause button
		pauseButton = Button().states_([["pause", Color.black], ["pause", Color.black, Color.grey]]).action_({arg elem;
			if(elem.value == 1, {~play.set(\playRate, 0)},{~play.set(\playRate, 1)})}.inEnvir),

		// start position slider
		StaticText().string_("    start measure:beat"),[
			posSlider = Slider(bounds: Rect(0, 0, 30, 5)).action_({arg pos; var min, sec;
			startPosText.string = clockStringFunc.value((pos.value * 129).trunc * 8);
			startPos = ((pos.value * 129).trunc * 8) / (129 * 8);
			clock.string = clockStringFunc.value((startPos * 129).trunc * 8)}.inEnvir), stretch: 1],
		startPosText = StaticText().string_("  1:1").font_(Font("Monaco", 15)), nil);

	HLayout([VLayout(HLayout(
		clock = StaticText().string_("  1:1").font_(Font("Monaco", 200)),
		StaticText().string_("|").font_(Font("Monaco", 200)),
		metronome = StaticText().string_([-30, -105, -104].collect(_.asAscii).as(String)).font_(Font("Monaco", 300)).stringColor_(Color.red)), nil, transport),
	alignment: \top])};

// mixer
mixerView = {arg hash; var masterIndicators, trackIndicators, master, tracks,
	eVol = [0.8, 0.8, 0.8, 0.8], eMute = [1, 1, 1, 1], ePan = [0, 0, 0, 0],
	eNames = ["hl 6 (fundamental)", "hl 6 (harmonics)", "hl 5 (high noise)", "hl 4 (low noise)"];

	// init indicators
	masterIndicators = {LevelIndicator()} ! 2;
	trackIndicators = {LevelIndicator()} ! 4;

	// get amp values for indicators
	OSCFunc.new({arg msg; {
		if(msg[2] == hash, {{arg i;
			masterIndicators[i].value = msg[3 + i].ampdb.linlin(-60, 0, 0, 1)} ! 2})}.defer},
	'/masterLevel', s.addr);
	OSCFunc.new({arg msg; {
		if(msg[2] == hash, {{arg i;
			trackIndicators[i].value = msg[3 + i].ampdb.linlin(-60, 0, 0, 1)} ! 4})}.defer},
	'/trackLevels', s.addr);

	// master faders
	master = HLayout(
		VLayout([HLayout(
			Slider().value_(0.8).action_({arg elem;
				~play.set(\masterVol, elem.value * 1.25)}.inEnvir),
			masterIndicators[0], masterIndicators[1]), stretch: 2],
		Button().states_([["mute", Color.black], ["mute", Color.black, Color.grey]]).action_({arg elem;
			~play.set(\masterMute, (1 - elem.value).abs)}.inEnvir),
		StaticText().string_("          master          ").align_(\center)), nil);
	// track faders
	tracks = {arg part;
		HLayout(VLayout(HLayout(
			Slider().value_(0.8).action_({arg elem;
				eVol[part] = elem.value * 1.25; ~play.set("eVol", eVol)}.inEnvir),
			trackIndicators[part]),
		Button().states_([["mute", Color.black], ["mute", Color.black, Color.grey]]).action_({arg elem;
			eMute[part] = (1 - elem.value).abs; ~play.set("eMute", eMute)}.inEnvir),
		StaticText().string_("pan").align_(\center),
		Knob().value_(0.5).action_({arg elem;
			ePan[part] = elem.value * 2 - 1; ~play.set("ePan", ePan)}.inEnvir),
		StaticText().string_(eNames[part]).align_(\center)), nil)} ! 4;
	HLayout(master, nil, *tracks)};

// all gui
guiLayout = {var tabButtonReset, modelViewButton, transportViewButton, mixerViewButton, hash, tabs;
	tabButtonReset = {modelViewButton.value = 1; transportViewButton.value = 1; mixerViewButton.value = 1};
	VLayout(HLayout(
		modelViewButton = Button().states_([["model", Color.white, Color.grey], ["model", Color.black]]).action_(
			{tabButtonReset.value; modelViewButton.value = 0; tabs.index = 0 }).value_(0),
		transportViewButton = Button().states_([["transport", Color.white, Color.grey], ["transport", Color.black]]).action_(
			{tabButtonReset.value; transportViewButton.value = 0; tabs.index = 1 }).value_(1),
		mixerViewButton = Button().states_([["mixer", Color.white, Color.grey], ["mixer", Color.black]]).action_(
			{tabButtonReset.value; mixerViewButton.value = 0; tabs.index = 2 }).value_(1)),
	hash = 10000000.rand;
	tabs = StackLayout();
	tabs.add(Window.new.layout_(mixerView.value(hash)));
	tabs.insert(Window.new.layout_(transportView.value(hash)));
	tabs.insert(Window.new.layout_(modelView.value(hash)));
	tabs.index = 0)};

// launch the gui
~launchGui = {var guiWin;
	guiWin = Window.new("a lot of tiles (trivial scan) - gui", Rect(200, 25, 1000, 450)).front;
	guiWin.layout = guiLayout.value;
	guiWin.front}
)