

//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
//_/_/
//_/_/  This file is part of CMSDK version 2.1.1.260726 (26 July 2026)
//_/_/  
//_/_/  developed by
//_/_/  
//_/_/  Communicative Machines Ltd. 
//_/_/    http://cmlabs.com/
//_/_/  Copyright (c) 2026
//_/_/  
//_/_/  in collaboration with 
//_/_/  
//_/_/  Icelandic Institute for Intelligent Machines ses
//_/_/    Menntavegur 1
//_/_/    101 Reykjavik
//_/_/    ICELAND
//_/_/    http://www.iiim.is/
//_/_/
//_/_/  and is released under the following
//_/_/  
//_/_/  EXTENDED BSD License
//_/_/
//_/_/  Redistribution and use in source and binary forms, with or without
//_/_/  modification, is permitted provided that the following conditions 
//_/_/  are met:
//_/_/
//_/_/  - Redistributions of source code must retain the above copyright 
//_/_/    and collaboration notice, this list of conditions and the following 
//_/_/    disclaimer.
//_/_/
//_/_/  - Redistributions in binary form must reproduce the above copyright 
//_/_/    notice, this list of conditions and the following disclaimer in the 
//_/_/    documentation and/or other materials provided with the distribution.
//_/_/
//_/_/  - Neither the name of its copyright holders nor the names of its 
//_/_/    contributors may be used to endorse or promote products derived from 
//_/_/    this software without specific prior written permission.
//_/_/
//_/_/  - CADIA Clause: The license granted in and to the software under this 
//_/_/    agreement is a limited-use license. The software may not be used in 
//_/_/    furtherance of: (i) intentionally causing bodily injury or severe 
//_/_/    emotional harm to any person; (ii) invading the personal privacy or 
//_/_/    violating the human rights of any person; or (iii) committing or 
//_/_/    preparing for any act of war. 
//_/_/
//_/_/  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 
//_/_/  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 
//_/_/  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 
//_/_/  PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER 
//_/_/  OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 
//_/_/  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 
//_/_/  PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
//_/_/  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 
//_/_/  LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 
//_/_/  NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 
//_/_/  SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//_/_/
//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
/*! @file PsyInternal.cpp
	@brief Implementation of the built-in internal cranks (see PsyInternal.h):
	ping/pong tests, shutdown, printing, message-script playback, stats logging and
	other system-service/diagnostic components dispatched by PsySpace.
*/
#include "PsyInternal.h"

namespace cmlabs {

int8 Internal_Simple(PsyAPI* api) {
	//printf("************ Simple start [%p] %s *************\n", api, api->getModuleName().c_str()); fflush(stdout);
	bool print = false;
	if (api->hasParameter("Print"))
		print = (api->getParameterString("Print").compare("yes") == 0);

	int32 c = 0;
	DataMessage* inMsg, *msg;
	const char* triggerName;
	if (api->shouldContinue()) {
		if (inMsg = api->waitForNewMessage(0, triggerName)) {
			//printf("[%u]", c++); fflush(stdout);
			msg = new DataMessage(*inMsg);
			//std::string pIn = inMsg->getUserEntriesAsString();
			//std::string pOut = msg->getUserEntriesAsString();
			//utils::AppendToAFile("d:/out.txt", pIn.c_str(), pIn.length() + 1);
			//utils::AppendToAFile("d:/out.txt", pOut.c_str(), pOut.length() + 1);



			//delete(msg);
			////char* data = (char*)malloc(inMsg->data->size);
			////memcpy(data, inMsg->data, inMsg->data->size);
			////msg = new DataMessage(data);
			//msg = new DataMessage();

			//uint32 size = 100000;
			//char* data = new char[size];
			////char* out = new char[2048];
			//memset(data, 0, size);
			////DataMessage* testMsg, *draft;
			////int64 serial = 0;

			////for (int nnn = 0; nnn < 10000000; nnn++) {
			//msg = new DataMessage();
			//// testMsg->setSerial(++serial);
			//msg->setData("AudioDataFrame", data, size);
			//msg->setInt("AudioChannels", 1);
			//msg->setInt("AudioRate", 16000);
			//msg->setInt("AudioSamples", 1);
			//msg->setInt("AudioBitsPerSample", 2);
			//msg->setInt("TimeMS", 0);
			//msg->setDouble("FilePosition", 0);
			//msg->setDouble("VideoFPS", 0);

			c = api->postOutputMessage(NULL, msg);

			//delete[] data;

			if (c > 1) {
				if (print)
					api->logPrint(1, "Posted %d output messages", c);
			}
			else if (c == 1) {
				if (print)
					api->logPrint(1, "Posted one output message", c);
			}
			else if (c < 0)
				api->logPrint(1, "Posted no messages (%d)", c);
			else
				api->logPrint(1, "Posted no messages");
		}
	}
	//printf("************ Simple end [%p] %s *************\n", api, api->getModuleName().c_str()); fflush(stdout);
	return 0;
}

int8 Internal_Ping(PsyAPI* api) {

	api->logPrint(1,"Started running (internal)...");

	DataMessage* inMsg, *outMsg;
	int64 counter = 0, start, end;
	int64 t = 0;
	const char* triggerName;
	bool print = false;
	int64 count = 10000;
	int64 cycles = 10;
	int64 modules = 2;
	double current, last = 0;

	int64 val;
	if (api->hasParameter("Cycles"))
		if (api->getParameter("Cycles", val))
			cycles = val;
	if (api->hasParameter("Count"))
		if (api->getParameter("Count", val))
			count = val;
	if (api->hasParameter("Modules"))
		if (api->getParameter("Modules", val))
			modules = val;

	int64 totalMessageCount = cycles*count;

	while (api->shouldContinue()) {
		if ( inMsg = api->waitForNewMessage(100, triggerName)) {
			outMsg = new DataMessage();
			if ( (stricmp(triggerName, "Ready") == 0) || (stricmp(triggerName, "Start") == 0) ) {
				api->logPrint(1,"Starting %u cycles test...", cycles);
				start = GetTimeNow();
				print = true;
				t = 0;
				counter = 0;
			}
			else if (stricmp(triggerName, "Ball") == 0) {
				t += GetTimeAge(inMsg->getCreatedTime());
				// api->logPrint(2,"[%s] got ball message type '%s'...", api->typeToText(inMsg->getType()).c_str());
				if (!counter++)
					start = GetTimeNow();
				else if (counter % totalMessageCount == 0) {
					end = GetTimeNow();
					current = ((double)(end-start))/(modules*count);
					if (!last)
						api->logPrint(1,"Got msg %llu, avg msg time: %.1fus, avg msg age: %.1fus...",
							counter, current, ((double)t/count));
					else if (last > current)
						api->logPrint(1,"Got msg %llu, avg msg time: %.1fus, avg msg age: %.1fus (%.1fus faster)...",
							counter, current, ((double)t/count), last-current);
					else
						api->logPrint(1,"Got msg %llu, avg msg time: %.1fus, avg msg age: %.1fus (%.1fus slower)...",
							counter, current, ((double)t/count), current-last);
					api->postOutputMessage("Done", outMsg);
					outMsg = NULL;
				}
				else if (counter % count == 0) {
					end = GetTimeNow();
					current = ((double)(end-start))/(modules*count);
					if (print) {
						if (!last)
							api->logPrint(1,"Got msg %llu, avg msg time: %.1fus, avg msg age: %.1fus...",
								counter, current, ((double)t/count));
						else if (last > current)
							api->logPrint(1,"Got msg %llu, avg msg time: %.1fus, avg msg age: %.1fus (%.1fus faster)...",
								counter, current, ((double)t/count), last-current);
						else
							api->logPrint(1,"Got msg %llu, avg msg time: %.1fus, avg msg age: %.1fus (%.1fus slower)...",
								counter, current, ((double)t/count), current-last);
					}
					last = current;
					t = 0;
					start = GetTimeNow();
				}
			}
			else {
				api->logPrint(1,"Got other message type '%s'...", api->typeToText(inMsg->getType()).c_str());
				delete(outMsg);
				outMsg = NULL;
			}
			if (outMsg) {
				api->postOutputMessage("Ball", outMsg);
				// utils::Sleep(5);
			}
		}
	}

	return 0;
}

int8 Internal_Pong(PsyAPI* api) {

	api->logPrint(1,"Started running...");

	const char* triggerName;
	DataMessage* inMsg;
	while (api->shouldContinue()) {
		if ( inMsg = api->waitForNewMessage(100, triggerName))
			api->postOutputMessage("Ball");
	}

	return 0;
}

int8 Internal_Shutdown(PsyAPI* api) {

	api->logPrint(1,"Started running...");

	DataMessage* inMsg, *outMsg;
	const char* triggerName;

	while (api->shouldContinue()) {
		if ( inMsg = api->waitForNewMessage(100, triggerName)) {
			outMsg = new DataMessage();
			api->postOutputMessage("Done", outMsg);
		}
	}

	return 0;
}

int8 Internal_Print(PsyAPI* api) {

	api->logPrint(1,"Started running...");

	DataMessage* inMsg;
	const char* triggerName;
	std::string userdata;
	PsyContext context;

	uint32 count = 0;
	uint64 lastTime = 0;
	while (api->shouldContinue()) {
		if ( inMsg = api->waitForNewMessage(100, triggerName)) {
			userdata = inMsg->getUserEntriesAsString();

			context = inMsg->getContextChange();
			if (context != NOCONTEXT) {
				if (lastTime)
					api->logPrint(1, "Trigger '%s' new context '%s' from '%s' context '%s' [%u] (%s):\n%s", triggerName, api->contextToText(context).c_str(), api->getOtherModuleName(inMsg->getFrom()).c_str(), api->contextToText(api->getCurrentTriggerContext()).c_str(), count, PrintTimeDifString(GetTimeAge(lastTime)).c_str(), userdata.c_str());
				else
					api->logPrint(1, "Trigger '%s' new context '%s' from '%s' context '%s' [%u]:\n%s\n", triggerName, api->contextToText(context).c_str(), api->getOtherModuleName(inMsg->getFrom()).c_str(), api->contextToText(api->getCurrentTriggerContext()).c_str(), count, userdata.c_str());
			}
			else if (userdata.length()) {
				if (lastTime)
					api->logPrint(1, "Trigger '%s' type '%s' from '%s' context '%s' [%u] (%s):\n%s", triggerName, api->typeToText(inMsg->getType()).c_str(), api->getOtherModuleName(inMsg->getFrom()).c_str(), api->contextToText(api->getCurrentTriggerContext()).c_str(), count, PrintTimeDifString(GetTimeAge(lastTime)).c_str(), userdata.c_str());
				else
					api->logPrint(1, "Trigger '%s' type '%s' from '%s' context '%s' [%u]:\n%s\n", triggerName, api->typeToText(inMsg->getType()).c_str(), api->getOtherModuleName(inMsg->getFrom()).c_str(), api->contextToText(api->getCurrentTriggerContext()).c_str(), count, userdata.c_str());
			}
			else {
				if (lastTime)
					api->logPrint(1, "Trigger '%s' type '%s' from '%s' context '%s' [%u] (%s)", triggerName, api->typeToText(inMsg->getType()).c_str(), api->getOtherModuleName(inMsg->getFrom()).c_str(), api->contextToText(api->getCurrentTriggerContext()).c_str(), count, PrintTimeDifString(GetTimeAge(lastTime)).c_str());
				else
					api->logPrint(1, "Trigger '%s' type '%s' from '%s' context '%s' [%u]", triggerName, api->typeToText(inMsg->getType()).c_str(), api->getOtherModuleName(inMsg->getFrom()).c_str(), api->contextToText(api->getCurrentTriggerContext()).c_str(), count);
			}
			lastTime = inMsg->getCreatedTime();
			if (api->getCurrentPostNames().size())
				api->postOutputMessage();
			count++;
			//if (++count == 1) {
			//	api->postOutputMessage();
			//	count = 0;
			//}
		}
	}

	return 0;
}

int8 Internal_Time(PsyAPI* api) {
	
	uint64 t1, t2;
	int64 t;
	uint32 errorCount = 0;
	double var = 0;
	
	DataMessage* inMsg;
	const char* triggerName;
	while (api->shouldContinue()) {
		if ( inMsg = api->waitForNewMessage(100, triggerName))
			api->postOutputMessage();

		for (uint32 n=0; n<9; n++) {
			t1 = GetTimeNow();
			for (uint32 m=0; m<1000; m++) {
				var += sqrt((double)m*m+1);
			}
			t2 = GetTimeNow();
			t = t2 - t1;
			if ( (t < 0) || (t > 50) ) {
				errorCount++;
				api->logPrint(1,"[%u] Timing error - %lld", errorCount, t, var);
			}
		}
	}
	return 0;
}

int8 Internal_SignalPing(PsyAPI* api) {
	DataMessage* inMsg;
	const char* triggerName;

	api->logPrint(1,"Started running...");

	int64 steps = 10;
	int64 cycles;
	if (api->hasParameter("Cycles"))
		if (api->getParameter("Cycles", cycles))
			steps = cycles;

	uint64 lastTime = GetTimeNow() - 1000000;
	uint64 totalTime = 0;
	uint32 count = 0;
	while (api->shouldContinue()) {
		if ( inMsg = api->waitForNewMessage(0, triggerName)) {
			if (stricmp(triggerName, "Start") == 0) {
				api->logPrint(1,"Emitting signal [%u]...", count);
				api->emitSignal("Output");
				utils::Sleep(10);
			}
		}
		else if (inMsg = api->waitForSignal("Input", 100, lastTime)) {
			totalTime += GetTimeAge( lastTime = inMsg->getCreatedTime() );
			count++;
			// api->logPrint(1,"[%s] got signal, %u [%u]...", totalTime, count);
			if (count % 10 == 0)
				api->logPrint(1,"Got signal, average %.3fus [%u]...", ((double)totalTime/count), count);
			utils::Sleep(10);
			if (count < steps)
				api->emitSignal("Output");
			else
				api->postOutputMessage();
		}
	}
	return 0;
}

int8 Internal_SignalPong(PsyAPI* api) {
	DataMessage* inMsg;
	const char* triggerName;

	api->logPrint(1,"Started running...");

	utils::Sleep(100);

	uint64 lastTime = GetTimeNow() - 1000000;
	while (api->shouldContinue()) {
		if ( inMsg = api->waitForNewMessage(0, triggerName)) {
		}
		else if (inMsg = api->waitForSignal("Input", 100, lastTime)) {
			lastTime = inMsg->getCreatedTime();
			// api->postOutputMessage();
			api->emitSignal("Output");
			// utils::Sleep(10);
		}
	}
	return 0;
}

int8 Internal_RetrieveTest(PsyAPI* api) {

	int64 steps = 10;
	int64 cycles;
	if (api->hasParameter("Cycles"))
		if (api->getParameter("Cycles", cycles))
			steps = cycles;

	api->logPrint(1,"Started running, %u steps...", steps);

	int64 val;
	DataMessage* inMsg, *outMsg;
	const char* triggerName;
	uint64 start = GetTimeNow();

	std::list<DataMessage*> retrievedMsgs;
	std::list<DataMessage*>::iterator i, e;

	while (api->shouldContinue()) {
		if ( inMsg = api->waitForNewMessage(100, triggerName)) {
			if ( (stricmp(triggerName, "Ready") == 0) || (stricmp(triggerName, "Start") == 0) ) {
				api->logPrint(1,"Got start message type '%s'...", api->typeToText(inMsg->getType()).c_str());
				for (val = 0; val < 10; val++) {
					outMsg = new DataMessage();
					outMsg->setInt("count", val);
					// api->logPrint(1,"[%s] Posting msg %llu   %s...", val, PrintTimeString(outMsg->getCreatedTime()).c_str());
					api->postOutputMessage("Ball", outMsg);
				}
				start = GetTimeNow();
			}
		}

		uint8 status;
		if (GetTimeAgeMS(start) > 1000) {
			for (uint32 nn=0; nn<steps; nn++) {
				status = api->retrieve(retrievedMsgs, "r1");
				if (status == QUERY_SUCCESS)
					api->logPrint(1,"Successfully retrieved %u messages...", retrievedMsgs.size());
				else if (status == QUERY_TIMEOUT)
					api->logPrint(1,"Retrieve 'r1' timed out...");
				else
					api->logPrint(1,"Retrieve 'r1' failed (%u)...", status);

				for (i = retrievedMsgs.begin(), e = retrievedMsgs.end(); i != e; i++) {
					//if ((*i)->getInt("count", val))
					//	api->logPrint(1,"Count: %u", val);
					delete(*i);
				}
				retrievedMsgs.clear();

				status = api->retrieveIntegerParam(retrievedMsgs, "r2", 2, 8, 4);
				if (status == QUERY_SUCCESS)
					api->logPrint(1,"Successfully retrieved %u messages...", retrievedMsgs.size());
				else if (status == QUERY_TIMEOUT)
					api->logPrint(1,"Retrieve 'r2' timed out...");
				else
					api->logPrint(1,"Retrieve 'r2' failed (%u)...", status);

				for (i = retrievedMsgs.begin(), e = retrievedMsgs.end(); i != e; i++) {
					//if ((*i)->getInt("count", val))
					//	api->logPrint(1,"Count: %u", val);
					delete(*i);
				}
				retrievedMsgs.clear();
				utils::Sleep(100);

				status = api->retrieveIntegerParam(retrievedMsgs, "r3", 0, 100, 100);
				if (status == QUERY_SUCCESS)
					api->logPrint(1,"Successfully retrieved %u messages...", retrievedMsgs.size());
				else if (status == QUERY_TIMEOUT)
					api->logPrint(1,"Retrieve 'r3' timed out...");
				else
					api->logPrint(1,"Retrieve 'r3' failed (%u)...", status);

				for (i = retrievedMsgs.begin(), e = retrievedMsgs.end(); i != e; i++) {
					//if ((*i)->getInt("count", val))
					//	api->logPrint(1,"Count: %u", val);
					delete(*i);
				}
				retrievedMsgs.clear();

				uint32 datasize = 0;
				char* data = NULL;
				char* result = NULL;
				status = api->queryCatalog(&result, datasize, "MyFiles", "test", "read");
				if (status == QUERY_SUCCESS)
					api->logPrint(1,"Successfully retrieved %u bytes from file catalog...", datasize);
				else if (status == QUERY_TIMEOUT)
					api->logPrint(1,"Retrieve 'read' timed out for file catalog...");
				else
					api->logPrint(1,"Retrieve 'read' failed (%u) for file catalog...", status);
				delete [] result;

				data = utils::StringFormat(datasize, "Hello1 World");
				status = api->queryCatalog(&result, datasize, "MyFiles", "test", "write", data, datasize);
				if (status == QUERY_SUCCESS)
					api->logPrint(1,"Successfully wrote %u bytes to file catalog...", datasize);
				else if (status == QUERY_TIMEOUT)
					api->logPrint(1,"Retrieve 'write' timed out for file catalog...");
				else
					api->logPrint(1,"Retrieve 'write' failed (%u) for file catalog...", status);
				delete [] data;
				delete [] result;

				status = api->queryCatalog(&result, datasize, "MyData", "test", "read");
				if (status == QUERY_SUCCESS)
					api->logPrint(1,"Successfully read %u bytes from data catalog...", datasize);
				else if (status == QUERY_TIMEOUT)
					api->logPrint(1,"Retrieve 'read' timed out from data catalog...");
				else
					api->logPrint(1,"Retrieve 'read' failed (%u) from data catalog, ok if starting with a fresh catalog ...", status);
				delete [] result;

				data = utils::StringFormat(datasize, "Hello2 World");
				status = api->queryCatalog(&result, datasize, "MyData", "test", "write", data, datasize);
				if (status == QUERY_SUCCESS)
					api->logPrint(1,"Successfully wrote %u bytes to data catalog...", datasize);
				else if (status == QUERY_TIMEOUT)
					api->logPrint(1,"Retrieve 'write' timed out to data catalog...");
				else
					api->logPrint(1,"Retrieve 'write' failed to data catalog (%u)...", status);
				delete [] data;
				delete [] result;

				status = api->queryCatalog(&result, datasize, "MyData", "test", "read");
				if (status == QUERY_SUCCESS)
					api->logPrint(1,"Successfully read %u bytes from data catalog...", datasize);
				else if (status == QUERY_TIMEOUT)
					api->logPrint(1,"Retrieve 'read' timed out from data catalog...");
				else
					api->logPrint(1,"Retrieve 'read' failed (%u) from data catalog...", status);
				delete [] result;
			}

			api->postOutputMessage("Done");

			start = 0;
		}
	}

	return 0;
}


bool TrackPlayerMessage::addEntry(const char* key, const char* value, const char* keytype) {
	if (entryMap.find(key) != entryMap.end())
		return false;
	TrackPlayerMessageEntry entry;
	entry.key = key;
	entry.value = value;
	if (!keytype || (stricmp("String", keytype) == 0) || (stricmp("Text", keytype) == 0))
		entry.entryType = TrackPlayerMessageEntry::TEXT;
	else if ((stricmp("Integer", keytype) == 0) || (stricmp("Int", keytype) == 0))
		entry.entryType = TrackPlayerMessageEntry::INT;
	else if ((stricmp("Float", keytype) == 0) || (stricmp("Double", keytype) == 0))
		entry.entryType = TrackPlayerMessageEntry::FLOAT;
	else
		entry.entryType = TrackPlayerMessageEntry::TEXT;
	entryMap[key] = entry;
	return true;
}

int8 Internal_MessageScript(PsyAPI* api) {
	bool print = false;
	if (api->hasParameter("Print"))
		print = api->getParameterAsBool("Print");

	XMLResults xmlResults;
	std::string xml = api->getParameterString("componentsetup");
	XMLNode entryNode, node, trackNode, mainNode = XMLNode::parseString(xml.c_str(), "setup", &xmlResults);

	// display error message (if any)
	if (xmlResults.error != eXMLErrorNone)
	{
		if (xmlResults.error == eXMLErrorFirstTagNotFound) {
			api->logPrint(0, "Module setup error, main tag '<setup>' not found");
		}
		else {
			api->logPrint(0, "Module setup parsing error at line %i, column %i:\n   %s\n",
				xmlResults.nLine, xmlResults.nColumn, XMLNode::getError(xmlResults.error));
		}
	}

	uint64 startTime = GetTimeNow();
	uint64 lastTriggerTime = 0;
	uint32 nextDelayEntry = 0;
	uint32 globalIntervalMS = 0;
	uint32 trackIntervalMS = 0;
	std::string autoTrack;

	std::map<std::string, std::vector<TrackPlayerMessage> > trackMap;
	std::vector<TrackPlayerMessage>* trackEntries;
	TrackPlayerMessage::TriggerType triggerType = TrackPlayerMessage::NONE;

	TrackPlayerMessage entry;
	uint32 lastOffset = 0;
	int n = 0, t = 0, m = 0;
	const char* str, *postName, *key, *value, *keytype, *name, *output;
	uint32 timeMS, intervalMS, delayMS;

	if (str = mainNode.getAttribute("intervalms")) {
		if (utils::TextStartsWith(str, "+")) {
			lastOffset = 0;
			// interval needs calculating...
			globalIntervalMS = utils::Ascii2Uint32(str + 1);
			timeMS = 0;
			// only from the __default__ track, i.e. not from posts inside a track tag
			while (!(node = mainNode.getChildNode("post", n++)).isEmpty()) {
				if (str = node.getAttribute("timems")) {
					if (utils::TextStartsWith(str, "+")) {
						timeMS = utils::Ascii2Uint32(str + 1);
						timeMS += lastOffset;
					}
					else
						timeMS = utils::Ascii2Uint32(str);
					lastOffset = timeMS;
				}
			}
			globalIntervalMS += timeMS;
			api->logPrint(1, "Global track interval calculated: %ums", globalIntervalMS);
		}
		else {
			globalIntervalMS = utils::Ascii2Uint32(str);
			api->logPrint(1, "Global track interval: %ums", globalIntervalMS);
		}
	}

	bool usingMainNode = false;
	if ((trackNode = mainNode.getChildNode("track", t++)).isEmpty()) {
		trackNode = mainNode;
		usingMainNode = true;
	}

	std::string trackName;
	do { // process track node
		trackIntervalMS = 0;
		name = trackNode.getAttribute("name");
		if (!(name = trackNode.getAttribute("name")) || !strlen(name)) {
			if (!usingMainNode)
				continue;
			trackName = "__default__";
			api->logPrint(1, "Adding single global track (__default__)");
		}
		else {
			trackName = name;
			api->logPrint(1, "Adding track '%s'", trackName.c_str());
		}
		
		trackEntries = &trackMap[trackName];

		if (str = trackNode.getAttribute("autostart"))
			autoTrack = trackName;

		if (!usingMainNode && (str = trackNode.getAttribute("intervalms"))) {
			if (utils::TextStartsWith(str, "+")) {
				lastOffset = 0;
				// interval needs calculating...
				trackIntervalMS = utils::Ascii2Uint32(str + 1);
				timeMS = 0;
				while (!(node = trackNode.getChildNode("post", n++)).isEmpty()) {
					if (str = node.getAttribute("timems")) {
						if (utils::TextStartsWith(str, "+")) {
							timeMS = utils::Ascii2Uint32(str + 1);
							timeMS += lastOffset;
						}
						else
							timeMS = utils::Ascii2Uint32(str);
						lastOffset = timeMS;
					}
				}
				trackIntervalMS += timeMS;
				api->logPrint(1, "Track '%s' interval calculated: %ums", trackName.c_str(), trackIntervalMS);
			}
			else {
				trackIntervalMS = utils::Ascii2Uint32(str);
				api->logPrint(1, "Track '%s' interval: %ums", trackName.c_str(), trackIntervalMS);
			}
		}

		lastOffset = 0;
		n = 0;
		while (!(node = trackNode.getChildNode("post", n++)).isEmpty()) {
			timeMS = intervalMS = delayMS = 0;
			postName = node.getAttribute("name");
			key = node.getAttribute("key");
			value = node.getAttribute("value");
			keytype = node.getAttribute("keytype");
			output = node.getAttribute("output");

			if (str = node.getAttribute("timems")) {
				if (utils::TextStartsWith(str, "+")) {
					timeMS = utils::Ascii2Uint32(str + 1);
					timeMS += lastOffset;
				}
				else
					timeMS = utils::Ascii2Uint32(str);
				lastOffset = timeMS;
			}
			if (str = node.getAttribute("intervalms"))
				intervalMS = utils::Ascii2Uint32(str);
			else
				intervalMS = 0;
			if (str = node.getAttribute("delayms"))
				delayMS = utils::Ascii2Uint32(str);
			else
				delayMS = 0;
			if (!intervalMS && trackIntervalMS)
				intervalMS = trackIntervalMS;
			if (!intervalMS && globalIntervalMS)
				intervalMS = globalIntervalMS;
			if (!timeMS && intervalMS)
				timeMS = intervalMS;
			//if (!timeMS && !intervalMS && !delayMS)
			//	delayMS = 1000;

			entry.delayMS = delayMS;
			entry.timeMS = timeMS;
			entry.nextTime = timeMS ? startTime + (timeMS * 1000) : 0;
			entry.intervalMS = intervalMS;
			entry.postName = postName ? postName : "";
			entry.triggerType = entry.timeMS ? TrackPlayerMessage::TIME : TrackPlayerMessage::TRIGGER;

			if (output && (stricmp("Signal", output) == 0)) {
				if (!postName || !strlen(postName)) {
					api->logPrint(1, "Signal posts must specify a signal name, ignoring post entry");
					continue;
				}
				entry.postType = TrackPlayerMessage::SIGNAL;
			}
			else
				entry.postType = TrackPlayerMessage::MESSAGE;

			if (triggerType == TrackPlayerMessage::NONE)
				triggerType = entry.triggerType;
			if (triggerType != entry.triggerType) {
				if (triggerType == TrackPlayerMessage::TRIGGER)
					api->logPrint(1, "Trigger track '%s' cannot use time posts, ignoring time post entry", trackName.c_str());
				else
					api->logPrint(1, "Time track '%s' cannot use trigger posts, ignoring trigger post entry", trackName.c_str());
				continue;
			}

			entry.entryMap.clear();
			if (key && value && strlen(key) && strlen(value))
				entry.addEntry(key, value, keytype);
			m = 0;
			while (!(entryNode = node.getChildNode("user", m++)).isEmpty()) {
				key = entryNode.getAttribute("key");
				value = entryNode.getAttribute("value");
				keytype = entryNode.getAttribute("keytype");
				if (!keytype)
					keytype = node.getAttribute("type");
				if (key && value && strlen(key) && strlen(value))
					entry.addEntry(key, value, keytype);
			}
			trackEntries->push_back(entry);
		}
		api->logPrint(1, "Added %u posts to track '%s'", trackEntries->size(), trackName.c_str());
		trackEntries = NULL;
		trackNode = mainNode.getChildNode("track", t++);
	} while (!trackNode.isEmpty());

	api->logPrint(1, "Added %u tracks in total", trackMap.size());

	std::vector<TrackPlayerMessage>::iterator i, e;
	std::map<std::string, TrackPlayerMessageEntry>::iterator iEntry, eEntry;

	triggerType = TrackPlayerMessage::NONE;
	trackName = "";

	int32 c = 0;
	uint64 now;
	uint32 waitTime = 50;
	DataMessage* inMsg, *msg;
	const char* triggerName;
	while (api->shouldContinue()) {
		inMsg = NULL;
		if (inMsg = api->waitForNewMessage(waitTime, triggerName)) {
			//if (print)
			//	api->logPrint(1, "New message...");
			if (stricmp("Start", triggerName) == 0) {
				if (autoTrack.length()) {
					trackEntries = &trackMap[autoTrack];
					if (trackEntries) {
						api->logPrint(1, "Auto starting track '%s'", autoTrack.c_str());
						trackName = autoTrack;
					}
				}
				lastTriggerTime = 0;
				nextDelayEntry = 0;
				triggerType = TrackPlayerMessage::NONE;
			}
			else if (stricmp("Restart", triggerName) == 0) {
				lastTriggerTime = 0;
				nextDelayEntry = 0;
				if (trackEntries) {
					i = trackEntries->begin();
					e = trackEntries->end();
					while (i != e) {
						if ((*i).triggerType == TrackPlayerMessage::TIME) {
							if ((*i).timeMS)
								(*i).nextTime = GetTimeNow() + ((*i).timeMS * 1000);
						}
						i++;
					}
					if (trackEntries)
						api->logPrint(1, "Auto starting track '%s'", autoTrack.c_str());
				}
				triggerType = TrackPlayerMessage::NONE;
			}
			else if (trackMap.find(triggerName) != trackMap.end()) {
				trackEntries = &trackMap[triggerName];
				trackName = triggerName;
				//api->logPrint(1, "Changing track to: %s", triggerName);
				lastTriggerTime = 0;
				nextDelayEntry = 0;
				i = trackEntries->begin();
				e = trackEntries->end();
				while (i != e) {
					if ((*i).triggerType == TrackPlayerMessage::TIME) {
						if ((*i).timeMS)
							(*i).nextTime = GetTimeNow() + ((*i).timeMS * 1000);
					}
					i++;
				}
				triggerType = TrackPlayerMessage::NONE;
			}
			else {
				if (!lastTriggerTime)
					lastTriggerTime = GetTimeNow();
			}
		}

		now = GetTimeNow();

		if (trackEntries && trackEntries->size()) {
			if (triggerType == TrackPlayerMessage::NONE) {
				i = trackEntries->begin();
				if (i != trackEntries->end()) {
					triggerType = (*i).triggerType;
					api->logPrint(1, "New track '%s' starting, track type: %s", trackName.c_str(), (triggerType == TrackPlayerMessage::TRIGGER) ? "Trigger" : "Time" );
				}
			}
			if (triggerType == TrackPlayerMessage::TRIGGER) {
				entry = trackEntries->at(nextDelayEntry);
				if (lastTriggerTime && (entry.delayMS < (uint32)GetTimeAgeMS(lastTriggerTime))) {
					if (!nextDelayEntry) {
						msg = new DataMessage();
						msg->setString("Track", trackName.c_str());
						api->postOutputMessage("TrackStart", msg);
					}
					msg = new DataMessage();
					iEntry = entry.entryMap.begin();
					eEntry = entry.entryMap.end();
					while (iEntry != eEntry) {
						if ((*iEntry).second.key.length() && (*iEntry).second.value.length()) {
							if ((*iEntry).second.entryType == TrackPlayerMessageEntry::INT)
								msg->setInt((*iEntry).second.key.c_str(), utils::Ascii2Int64((*iEntry).second.value.c_str()));
							else if ((*iEntry).second.entryType == TrackPlayerMessageEntry::FLOAT)
								msg->setFloat((*iEntry).second.key.c_str(), utils::Ascii2Float64((*iEntry).second.value.c_str()));
							else
								msg->setString((*iEntry).second.key.c_str(), (*iEntry).second.value.c_str());
						}
						iEntry++;
					}
					if (entry.postName.length()) {
						if (entry.postType == TrackPlayerMessage::SIGNAL) {
							api->emitSignal(entry.postName.c_str(), msg);
							if (print)
								api->logPrint(1, "Triggered signal '%s' after delay of %ums", entry.postName.c_str(), entry.delayMS);
						}
						else {
							api->postOutputMessage(entry.postName.c_str(), msg);
							if (print)
								api->logPrint(1, "Triggered post '%s' after delay of %ums", entry.postName.c_str(), entry.delayMS);
						}
					}
					else {
						if (entry.postType == TrackPlayerMessage::MESSAGE) {
							api->postOutputMessage(NULL, msg);
							if (print)
								api->logPrint(1, "Triggered all posts after delay of %ums", entry.delayMS);
						}
					}
					lastTriggerTime = 0;
					nextDelayEntry++;
					if (nextDelayEntry >= trackEntries->size()) {
						msg = new DataMessage();
						msg->setString("Track", trackName.c_str());
						api->postOutputMessage("TrackEnd", msg);
						nextDelayEntry = 0;
					}
				}
			}
			else { // TrackPlayerMessage::TIME
				uint32 n = 0;
				i = trackEntries->begin();
				e = trackEntries->end();
				while (i != e) {
					n++;
					if ((*i).nextTime && ((*i).nextTime < now)) {
						if (n == 1) {
							msg = new DataMessage();
							msg->setString("Track", trackName.c_str());
							api->postOutputMessage("TrackStart", msg);
						}
						msg = new DataMessage();
						iEntry = (*i).entryMap.begin();
						eEntry = (*i).entryMap.end();
						while (iEntry != eEntry) {
							if ((*iEntry).second.key.length() && (*iEntry).second.value.length()) {
								if ((*iEntry).second.entryType == TrackPlayerMessageEntry::INT)
									msg->setInt((*iEntry).second.key.c_str(), utils::Ascii2Int64((*iEntry).second.value.c_str()));
								else if ((*iEntry).second.entryType == TrackPlayerMessageEntry::FLOAT)
									msg->setFloat((*iEntry).second.key.c_str(), utils::Ascii2Float64((*iEntry).second.value.c_str()));
								else
									msg->setString((*iEntry).second.key.c_str(), (*iEntry).second.value.c_str());
							}
							iEntry++;
						}

						if ((*i).postName.length()) {
							if ((*i).postType == TrackPlayerMessage::SIGNAL) {
								api->emitSignal((*i).postName.c_str(), msg);
								if (print) {
									if (!(*i).intervalMS)
										api->logPrint(1, "Triggered timed signal '%s' once at %s", (*i).postName.c_str(), PrintTimeDifString(GetTimeAge(startTime)).c_str());
									else
										api->logPrint(1, "Triggered timed signal '%s' at %s, next post in %s", (*i).postName.c_str(), PrintTimeDifString(GetTimeAge(startTime)).c_str(), PrintTimeDifString((*i).intervalMS * 1000).c_str());
								}
							}
							else {
								api->postOutputMessage((*i).postName.c_str(), msg);
								if (print) {
									if (!(*i).intervalMS)
										api->logPrint(1, "Triggered timed post '%s' once at %s", (*i).postName.c_str(), PrintTimeDifString(GetTimeAge(startTime)).c_str());
									else
										api->logPrint(1, "Triggered timed post '%s' at %s, next post in %s", (*i).postName.c_str(), PrintTimeDifString(GetTimeAge(startTime)).c_str(), PrintTimeDifString((*i).intervalMS * 1000).c_str());
								}
							}
						}
						else {
							if ((*i).postType == TrackPlayerMessage::MESSAGE) {
								api->postOutputMessage(NULL, msg);
								if (print) {
									if (!(*i).intervalMS)
										api->logPrint(1, "Triggered all timed posts once at %s", (*i).postName.c_str(), PrintTimeDifString(GetTimeAge(startTime)).c_str());
									else
										api->logPrint(1, "Triggered all timed posts at %s, next post in %s", (*i).postName.c_str(), PrintTimeDifString(GetTimeAge(startTime)).c_str(), PrintTimeDifString((*i).intervalMS*1000).c_str());
								}
							}
						}

						if (!(*i).intervalMS)
							(*i).nextTime = 0;
						else
							(*i).nextTime += ((*i).intervalMS * 1000);

						if (n == trackEntries->size()) {
							msg = new DataMessage();
							msg->setString("Track", trackName.c_str());
							api->postOutputMessage("TrackEnd", msg);
						}
					}
					i++;
				}
			}
		}
	}
	return 0;
}

int8 Internal_QueryTest(PsyAPI* api) {

	DataMessage* inMsg, *msg, *resultMsg;
	const char* triggerName;
	uint64 start = GetTimeNow();

	std::list<DataMessage*> retrievedMsgs;
	std::list<DataMessage*>::iterator i, e;

	while (api->shouldContinue()) {
		if (inMsg = api->waitForNewMessage(100, triggerName)) {
			start = GetTimeNow();
		}

		uint8 status;
		if (GetTimeAgeMS(start) > 1000) {
			msg = new DataMessage();
			msg->setTag(10);
			status = api->queryCatalog(&resultMsg, "Test", msg, 5000);
			if (status == QUERY_SUCCESS) {
				if (resultMsg)
					api->logPrint(1, "Successfully retrieved reply msg from catalog...");
				else
					api->logPrint(1, "Successfully got reply from catalog...");
			}
			else if (status == QUERY_TIMEOUT)
				api->logPrint(1, "Query timed out for catalog...");
			else
				api->logPrint(1, "Query failed (%u) for catalog...", status);
			delete resultMsg;

			start = GetTimeNow();
		}
	}
	return 0;
}


int8 Internal_StatsLog(PsyAPI* api) {

	api->logPrint(1, "Started running...");

	bool shouldReplace = api->getParameterAsBool("Replace");
	bool shouldRotate = api->getParameterAsBool("Rotate");

	uint64 start = GetTimeNow();

	std::string outFile = api->getParameterString("OutputFile");
	if (!outFile.length())
		outFile = utils::StringFormat("%s.stats", PrintDateStringSortableDelimiter(start, "").c_str());

	if (utils::DoesAFileExist(outFile.c_str())) {
		if (shouldRotate) {
			uint32 n = 1;
			while (utils::DoesAFileExist(utils::StringFormat("%s.%u", outFile.c_str(), n).c_str()))
				n++;
			std::string newOutFile = utils::StringFormat("%s.%u", outFile.c_str(), n);
			utils::MoveAFile(outFile.c_str(), newOutFile.c_str(), true);
		}
		else if (shouldReplace) {
			utils::DeleteAFile(outFile.c_str(), true);
		}
	}

	uint64 createdTime, sendTime, recvTime;
	uint32 count = 0;
	uint64 lastWrite = GetTimeNow();
	std::string buffer;
	const char* triggerName;
	DataMessage* inMsg;
	while (api->shouldContinue()) {
		if (inMsg = api->waitForNewMessage(100, triggerName)) {
			createdTime = inMsg->getCreatedTime();
			sendTime = inMsg->getSendTime();
			recvTime = inMsg->getRecvTime();
			// add log output line
			buffer += utils::StringFormat("%s,%s,%llu,%llu,%llu,%u,%u,%u,%u\n",
				triggerName, PrintTimeSortableMicrosecString(recvTime).c_str(), recvTime,
				recvTime - createdTime, recvTime - sendTime,
				inMsg->data->cyclecputime, inMsg->data->cyclewalltime,
				inMsg->data->chaincputime, inMsg->data->chainwalltime
				);
		}
		count++;

		if (count > 20) {
			if (buffer.length())
				utils::AppendToAFile(outFile.c_str(), buffer.c_str(), (uint32)buffer.length());
			buffer.clear();
			count = 0;
		}

	}

	return 0;
}

int8 Internal_BitmapPoster(PsyAPI* api) {

	api->logPrint(1, "Started running...");

	bool shouldRotate = api->getParameterAsBool("Rotate");
	std::string sourceDir = api->getParameterString("SourceDir");
	int64 intervalMS = api->getParameterInt("Interval");
	if (!intervalMS)
		intervalMS = 1000;
	int64 updateIntervalMS = api->getParameterInt("Interval");
	if (!updateIntervalMS)
		updateIntervalMS = 10000;

	uint32 fileCount;
	char* files = utils::GetFileList(sourceDir.c_str(), "bmp", fileCount, true);

	std::vector<std::string> fileNames;
	const char* name;
	uint32 n;
	for (n = 0; n<fileCount; n++) {
		name = files + (n*(MAXFILENAMELEN + 1));
		if (utils::DoesAFileExist(name))
			fileNames.push_back(name);
	}
	delete[] files;

	DataMessage* msg;
	Bitmap* bitmap;
	uint32 count, postCount = 0, currentNamePos = 0;
	uint64 lastPost = GetTimeNow();
	uint64 lastUpdate = GetTimeNow();
	const char* triggerName;
	DataMessage* inMsg;
	while (api->shouldContinue()) {
		if (inMsg = api->waitForNewMessage(100, triggerName)) {
		}
		if ((currentNamePos < fileNames.size()) && (GetTimeAgeMS(lastPost) > intervalMS)) {
			name = fileNames[currentNamePos].c_str();
			if (utils::DoesAFileExist(name)) {
				bitmap = new Bitmap(name);
				if (bitmap->width > 0) {
					msg = new DataMessage();
					msg->setSerial(postCount++);
					msg->setData("VideoDataFrame", bitmap->data, bitmap->size);
					msg->setInt("Width", bitmap->width);
					msg->setInt("Height", bitmap->height);
					msg->setString("Filename", name);
					api->postOutputMessage("Image", msg);
				}
				else {
					api->logPrint(1, "File '%s' isn't a valid BMP file", name);
				}
				delete bitmap;
			}
			else {
				api->logPrint(1, "File '%s' doesn't exist", name);
			}
			currentNamePos++;
			if ((currentNamePos >= fileNames.size()) && shouldRotate)
				currentNamePos = 0;
			lastPost = GetTimeNow();
		}

		if (GetTimeAgeMS(lastUpdate) > updateIntervalMS) {
			files = utils::GetFileList(sourceDir.c_str(), "bmp", count, true);
			if (count != fileCount) {
				api->logPrint(1, "File changes detected in '%s', updating list of files and starting over", sourceDir.c_str());
				fileNames.clear();
				for (n = 0; n<count; n++) {
					name = files + (n*(MAXFILENAMELEN + 1));
					if (utils::DoesAFileExist(name))
						fileNames.push_back(name);
				}
				fileCount = count;
				delete[] files;
				currentNamePos = 0;
			}
			lastUpdate = GetTimeNow();
		}
	}

	return 0;
}

int8 Internal_MessageToggler(PsyAPI* api) {

	std::string overrideState = api->getParameterString("OverrideState");
	std::string defaultState = api->getParameterString("DefaultState");
	if (!defaultState.length())
		defaultState = overrideState;
	std::string currentState = defaultState;
	std::string switchKey = api->getParameterString("SwitchKey");
	if (!switchKey.length()) {
		api->logPrint(1, "No SwitchKey parameter provided, cannot proceed without it");
		return -1;
	}

	DataMessage* inMsg;
	const char* triggerName, *newState;
	std::string postName;
	while (api->shouldContinue()) {
		if (inMsg = api->waitForNewMessage(100, triggerName)) {

			if (stricmp(triggerName, "Ready") == 0) {
				if (defaultState.length()) {
					if (api->hasCurrentPostName(defaultState.c_str()))
						api->postOutputMessage(defaultState.c_str());
				}
				else {
					if (api->hasCurrentPostName("NoState"))
						api->postOutputMessage("NoState");
				}
			}
			else if (stricmp(triggerName, "SwitchMessage") == 0) {
				if ((newState = inMsg->getString(switchKey.c_str())) != 0) {
					if (!strlen(newState)) {
						currentState.clear();
						if (api->hasCurrentPostName("NoState"))
							api->postOutputMessage("NoState", inMsg->copy());
					}
					else if (stricmp(currentState.c_str(), newState) != 0) {
						currentState = newState;
						if (api->hasCurrentPostName(newState))
							api->postOutputMessage(newState, inMsg->copy());
					}
				}
			}
			else {
				postName = utils::StringFormat("%s_%s", triggerName, currentState.c_str());
				if (api->hasCurrentPostName(postName.c_str()))
					api->postOutputMessage(postName.c_str(), inMsg->copy());
				else {
					if (overrideState.length()) {
						if (currentState.length())
							api->logPrint(1, "No post (%s) found for current state '%s', using override state instead (%s)",
								postName.c_str(), currentState.c_str(), overrideState.c_str());
						else
							api->logPrint(1, "No post (%s) found for current empty state, using override state instead (%s)",
								postName.c_str(), overrideState.c_str());
						postName = utils::StringFormat("%s_%s", triggerName, overrideState.c_str());
						if (api->hasCurrentPostName(postName.c_str()))
							api->postOutputMessage(postName.c_str(), inMsg->copy());
						else {
							api->logPrint(1, "No post (%s) found for override state '%s'",
								postName.c_str(), overrideState.c_str());
						}
					}
					else {
						if (currentState.length())
							api->logPrint(1, "No post (%s) found for current state '%s'", postName.c_str(), currentState.c_str());
						else
							api->logPrint(1, "No post (%s) found for current empty state", postName.c_str());
					}
				}
			}
		}
	}
	return 0;
}

int8 Internal_MessageTypeConverter(PsyAPI* api) {
	DataMessage* inMsg;
	const char* triggerName;
	if (api->shouldContinue()) {
		if (inMsg = api->waitForNewMessage(0, triggerName)) {
			if (api->hasCurrentPostName(triggerName))
				api->postOutputMessage(triggerName, inMsg->copy());
		}
	}
	return 0;
}

} // namespace cmlabs
