#include "TestScenario.h"
#include <cstdio>

namespace
{
	struct EventSampleComparator
	{
		static int compareElements (const TestScenario::Event &a, const TestScenario::Event &b)
		{
			if (a.sample < b.sample) return -1;
			if (a.sample > b.sample) return 1;
			return 0;
		}
	};

	// var itself has no hasProperty() in this JUCE version - only the
	// DynamicObject it may wrap does.
	bool hasProperty (const var &v, const Identifier &name)
	{
		DynamicObject *obj = v.getDynamicObject();
		return obj != nullptr && obj->hasProperty (name);
	}
}

bool TestScenario::loadFromFile (const File &jsonFile)
{
	var root = JSON::parse (jsonFile);

	if (root.isVoid())
	{
		fprintf (stderr, "testhost: could not parse scenario JSON '%s'\n", jsonFile.getFullPathName().toRawUTF8());
		return false;
	}

	return parseTopLevel (root, jsonFile.getParentDirectory());
}

bool TestScenario::parseTopLevel (const var &root, const File &baseDir)
{
	sampleRate = (double) root.getProperty ("sampleRate", sampleRate);
	blockSize = (int) root.getProperty ("blockSize", blockSize);

	if (hasProperty (root, "durationSamples"))
		durationSamples = (int64) (int) root.getProperty ("durationSamples", 0);
	else if (hasProperty (root, "durationSeconds"))
		durationSamples = (int64) ((double) root.getProperty ("durationSeconds", 0.0) * sampleRate);
	else
	{
		fprintf (stderr, "testhost: scenario JSON needs either \"durationSamples\" or \"durationSeconds\"\n");
		return false;
	}

	if (hasProperty (root, "input") && ! parseInput (root.getProperty ("input", var())))
		return false;

	if (hasProperty (root, "loadPatch"))
		initialLoadPatchFile = baseDir.getChildFile (root.getProperty ("loadPatch", var()).toString()).getFullPathName();

	if (hasProperty (root, "loadBank"))
		initialLoadBankFile = baseDir.getChildFile (root.getProperty ("loadBank", var()).toString()).getFullPathName();

	if (hasProperty (root, "recordWav"))
		recordWavFile = baseDir.getChildFile (root.getProperty ("recordWav", var()).toString()).getFullPathName();

	if (hasProperty (root, "events") && ! parseEvents (root.getProperty ("events", var()), baseDir))
		return false;

	EventSampleComparator comparator;
	events.sort (comparator, true);

	return true;
}

bool TestScenario::parseInput (const var &inputVar)
{
	const String type = inputVar.getProperty ("type", "silence").toString();
	inputAmplitude = (float) (double) inputVar.getProperty ("amplitude", 0.5);

	if (type == "silence")
		inputType = AudioInputSource::silence;
	else if (type == "sine")
	{
		inputType = AudioInputSource::sine;
		inputFrequencyHz = (double) inputVar.getProperty ("frequencyHz", 440.0);
	}
	else if (type == "noise")
	{
		const String kind = inputVar.getProperty ("kind", "white").toString();
		inputType = (kind == "pink") ? AudioInputSource::pinkNoise : AudioInputSource::whiteNoise;
	}
	else if (type == "impulse")
		inputType = AudioInputSource::impulse;
	else
	{
		fprintf (stderr, "testhost: unknown scenario input type '%s'\n", type.toRawUTF8());
		return false;
	}

	return true;
}

bool TestScenario::parseEventType (const String &typeName, Event::Type &outType)
{
	if (typeName == "noteOn")        { outType = Event::noteOn;    return true; }
	if (typeName == "noteOff")       { outType = Event::noteOff;   return true; }
	if (typeName == "controller")    { outType = Event::controller; return true; }
	if (typeName == "param")         { outType = Event::param;     return true; }
	if (typeName == "loadPatch")     { outType = Event::loadPatch; return true; }
	if (typeName == "loadBank")      { outType = Event::loadBank;  return true; }
	if (typeName == "savePatch")     { outType = Event::savePatch; return true; }
	if (typeName == "saveBank")      { outType = Event::saveBank;  return true; }
	return false;
}

bool TestScenario::parseEvents (const var &eventsVar, const File &baseDir)
{
	if (! eventsVar.isArray())
	{
		fprintf (stderr, "testhost: scenario \"events\" must be an array\n");
		return false;
	}

	for (int i = 0; i < eventsVar.size(); ++i)
	{
		const var &e = eventsVar[i];
		const String typeName = e.getProperty ("type", var()).toString();

		Event event;
		if (! parseEventType (typeName, event.type))
		{
			fprintf (stderr, "testhost: unknown event type '%s' (event #%d)\n", typeName.toRawUTF8(), i);
			return false;
		}

		event.sample = (int64) (int) e.getProperty ("sample", 0);

		switch (event.type)
		{
			case Event::noteOn:
				event.channel = (int) e.getProperty ("channel", 1);
				event.note = (int) e.getProperty ("note", 60);
				event.velocity = (int) e.getProperty ("velocity", 100);
				break;

			case Event::noteOff:
				event.channel = (int) e.getProperty ("channel", 1);
				event.note = (int) e.getProperty ("note", 60);
				break;

			case Event::controller:
				event.channel = (int) e.getProperty ("channel", 1);
				event.controllerNumber = (int) e.getProperty ("controllerNumber", 0);
				event.controllerValue = (int) e.getProperty ("controllerValue", 0);
				break;

			case Event::param:
				event.paramIndex = (int) e.getProperty ("index", 0);
				event.paramValue = (float) (double) e.getProperty ("value", 0.0);
				break;

			case Event::loadPatch:
			case Event::loadBank:
			case Event::savePatch:
			case Event::saveBank:
				event.file = baseDir.getChildFile (e.getProperty ("file", var()).toString()).getFullPathName();
				if (event.file.isEmpty())
				{
					fprintf (stderr, "testhost: event #%d (%s) is missing \"file\"\n", i, typeName.toRawUTF8());
					return false;
				}
				break;
		}

		events.add (event);
	}

	return true;
}
