

//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
//_/_/
//_/_/  This file is part of PSYCLONE version 2.0.0.170915_Beta4 (15 September 2017)
//_/_/  
//_/_/  developed by
//_/_/  
//_/_/  Communicative Machines Ltd. 
//_/_/    http://cmlabs.com/
//_/_/  Copyright (c) 2017
//_/_/  
//_/_/  in collaboration with 
//_/_/  
//_/_/  Icelandic Institute for Intelligent Machines ses
//_/_/    Menntavegur 1
//_/_/    101 Reykjavik
//_/_/    ICELAND
//_/_/    http://www.iiim.is/
//_/_/
//_/_/  PSYCLONE 2.0 is released under the GNU Lesser General Public License 
//_/_/  (LGPL) version 3.0, and is subject to the following
//_/_/  
//_/_/  - 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. 
//_/_/    
//_/_/  A commercial license for PSYCLONE 2.0 is also available to 
//_/_/  any interested party from the copyright holder.
//_/_/  
//_/_/  You can redistribute this copy of PSYCLONE 2.0 and/or modify it under 
//_/_/  the terms of the GNU General Public License as published by the Free 
//_/_/  Software Foundation, either version 3 of the License, or (at your 
//_/_/  option) any later version.
//_/_/  
//_/_/  You should have received a copy of the GNU General Public License
//_/_/  and the GNU Lesser General Public License along with PSYCLONE 2.0.  
//_/_/  If not, see <http://www.gnu.org/licenses/>.
//_/_/  
//_/_/  PSYCLONE 2.0 is distributed in the hope that it will be useful,
//_/_/  but WITHOUT ANY WARRANTY; without even the implied warranty of
//_/_/  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//_/_/  GNU General Public License for more details.
//_/_/
//_/_/  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.
//_/_/
//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
#include "TaskManagerCatalog.h"

namespace	cmlabs {


// Built-in TaskManagerCatalog catalog 
int8 TaskManagerCatalog(PsyAPI* api) {
	std::string myName = api->getModuleName();
	api->logPrint(2, "Started running...");

	uint64 lastTaskID = 0;
	TaskEntry *masterTask = NULL;
	std::map<uint64, TaskQueueTrigger> timeQueue;
	std::map<uint64, TaskQueueTrigger>::iterator timeQueueIt, timeQueueEnd = timeQueue.end();
	std::list<TaskQueueTrigger> triggerList;
	std::list<TaskQueueTrigger>::iterator triggerListIt, triggerListEnd = triggerList.end();

	std::string jsonfile = api->getParameterString("TaskFile");
	if (!jsonfile.length()) {
		api->logPrint(0, "Parameter TaskFile not found, cannot continue");
		return -1;
	}
	std::string jsonString = utils::ReadAFileString(jsonfile);
	if (!jsonString.length()) {
		api->logPrint(0, "TaskFile cannot be read, cannot continue");
		return -1;
	}

	const char* json = jsonString.c_str();
	uint32 len = jsonString.length();

	jsmn_parser parser;
	jsmntok_t* tokens = new jsmntok_t[256];
	int tokenCount = 0;
	jsmn_init(&parser);

	if ((tokenCount = jsmn_parse(&parser, json, len, tokens, 256)) <= 0) {
		// No JSON found
		delete[] tokens;
		api->logPrint(0, "TaskFile contains no JSON, cannot continue");
		return -1;
	}

	std::list<std::string> allTriggers, allPosts;

	// First read JSON setup and initialise task hierarchy
	masterTask = new TaskEntry(api);
	if (!masterTask->fromJSON(json, tokens, tokenCount, 0, allTriggers, allPosts)) {
		delete[] tokens;
		api->logPrint(0, "TaskFile contains invalid JSON, cannot continue");
		return -1;
	}

	delete[] tokens;

	allTriggers.sort();
	allTriggers.unique();
	allPosts.sort();
	allPosts.unique();

	std::string subscription;
	// for each trigger - add trigger to spec if not already added
	std::list<std::string>::iterator allTriggersIt = allTriggers.begin(), allTriggersEnd = allTriggers.end();
	while (allTriggersIt != allTriggersEnd) {
		subscription += utils::StringFormat("<trigger name=\"%s\" type=\"%s\" />\n", (*allTriggersIt).c_str(), (*allTriggersIt).c_str());
		allTriggersIt++;
	}
	// for each post - add post to spec if not already added
	std::list<std::string>::iterator allPostsIt = allPosts.begin(), allPostsEnd = allPosts.end();
	while (allPostsIt != allPostsEnd) {
		if (std::find(allTriggers.begin(), allTriggersEnd, (*allPostsIt)) == allTriggersEnd)
			subscription += utils::StringFormat("<post name=\"%s\" type=\"%s\" />\n", (*allPostsIt).c_str(), (*allPostsIt).c_str());
		else {
			api->logPrint(0, "Post '%s' ignored as it would cause a self-triggering loop", (*allPostsIt).c_str());
		}
		allPostsIt++;
	}

	subscription = utils::StringFormat("<subscription name=\"%s\">\n%s</subscription>\n", myName.c_str(), subscription.c_str());
	// Register full spec with Psyclone system
	if (!api->addSubscription(subscription.c_str())) {
		api->logPrint(0, "Unable to register new subscriptions, cannot continue");
		return -1;
	}
	api->logPrint(1, "New subscriptions registered");

	// if autostart, put into timeQueue now

	api->addPsyProbeCustomView("TaskView", "elements/hierarchicaldiagram.html");

	std::string jsonView = masterTask->toJSONStatus();
	api->setPrivateData("JSON", jsonView.c_str(), jsonView.length(), "application/json");


	DataMessage* inMsg; // , *replyMsg;
	const char* triggerName;
	uint64 now, t;
	uint32 waitMS = 100;
	// std::list<std::string> msgTypes;
	std::list<std::string> posts;
	std::list<std::string>::iterator postsIt, postsEnd;
	std::list<TaskQueueTrigger> procList;
	std::list<TaskQueueTrigger>::iterator procListIt, procListEnd = procList.end();

	std::list<TaskQueueTrigger> newTriggers;
	std::list<TaskEntry*> oldTasks;
	std::list<TaskQueueTrigger>::iterator newTriggersIt, newTriggersEnd = newTriggers.end();
	std::list<TaskEntry*>::iterator oldTasksIt, oldTasksEnd = oldTasks.end();

	while (api->shouldContinue()) {
		procList.clear();
		// is it time for dealing with the next task in the timeQueue?
		if (((timeQueueIt = timeQueue.begin()) != timeQueueEnd) && ((t = timeQueueIt->first) <= (now = GetTimeNow()))) {
			// Yes, it's time for this task to run
			procList.push_back(timeQueueIt->second);
		}

		if (!procList.size()) {
			// No more timers to do for now, calculate how long till the next one
			if ((timeQueueIt != timeQueueEnd) && (timeQueueIt->first > now)) {
				// there is at least one left in the queue which is not yet ready to run
				// and we know that now has just been calculated
				waitMS = (uint32)(timeQueueIt->first - now);
			}
			else
				waitMS = 0;
		}

		if (inMsg = api->waitForNewMessage(waitMS, triggerName)) {
			std::string msgType = api->typeToText(inMsg->getType());
			// are we looking for this trigger?
			for (triggerListIt = triggerList.begin(); triggerListIt != triggerListEnd; triggerListIt++) {
				if ((*triggerListIt).msgType == msgType)
					procList.push_back(*triggerListIt);
			}
		}

		now = GetTimeNow();
		for (procListIt = procList.begin(); procListIt != procListEnd; procListIt++) {
			// Execute trigger on task at hand
			(*procListIt).task->handleTrigger((*procListIt).triggerType, posts, newTriggers, oldTasks);
			
			// deal with any oldTasks if any came back
			for (oldTasksIt = oldTasks.begin(); oldTasksIt != oldTasksEnd; oldTasksIt++) {
				removeTaskFromQueues(*oldTasksIt, triggerList, timeQueue);
			}
			oldTasks.clear();

			// deal with any newTriggers if any came back
			for (newTriggersIt = newTriggers.begin(); newTriggersIt != newTriggersEnd; newTriggersIt++) {
				if (!(*newTriggersIt).msgType.length()) {
					addToTimerQueue((*newTriggersIt).time ? (*newTriggersIt).time : now + (*newTriggersIt).delay, (*newTriggersIt), timeQueue);
				}
				else {
					triggerList.push_back(*newTriggersIt);
				}
			}
			newTriggers.clear();

			// deal with posts if any came back
			for (postsIt = posts.begin(), postsEnd = posts.end(); postsIt != postsEnd; postsIt++) {
				api->postOutputMessage((*postsIt).c_str());
			}
			posts.clear();
		}
	}
	return 0;
}


bool addToTimerQueue(uint64 t, TaskQueueTrigger trigger, std::map<uint64, TaskQueueTrigger> &timeQueue) {
	uint64 actualT = t;
	uint32 c = 0;
	while (timeQueue.find(actualT) != timeQueue.end()) {
		if (++c > 1000)
			return false;
		actualT++;
	}
	timeQueue[actualT] = trigger;
	return true;
}

bool removeTaskFromQueues(TaskEntry* oldTask, std::list<TaskQueueTrigger> &triggerQueue, std::map<uint64, TaskQueueTrigger> &timeQueue) {
	std::map<uint64, TaskQueueTrigger>::iterator timeQueueIt = timeQueue.begin(), timeQueueEnd = timeQueue.end();
	while (timeQueueIt != timeQueueEnd) {
		if (timeQueueIt->second.task == oldTask)
			timeQueue.erase(timeQueueIt);
		timeQueueIt++;
	}
	std::list<TaskQueueTrigger>::iterator triggerQueueIt = triggerQueue.begin(), triggerQueueEnd = triggerQueue.end();
	while (triggerQueueIt != triggerQueueEnd) {
		if ((*triggerQueueIt).task == oldTask)
			triggerQueue.erase(triggerQueueIt);
		triggerQueueIt++;
	}
	return true;
}


bool TaskEntry::handleTrigger(TaskEventType trigger, std::list<std::string> &posts,
	std::list<TaskQueueTrigger> &newTriggers, std::list<TaskEntry*> &oldTasks) {

	if (trigger == EVENT_START && isActive()) {
		log("Trigger Start ignored, task already running");
		return false;
	}
	else if (trigger != EVENT_START && !isActive()) {
		log(utils::StringFormat("Trigger %s ignored, task not running", TaskEventTypeNames[trigger].c_str()));
		return false;
	}

	TaskEntry* task;
	TaskQueueTrigger newTrigger;
	std::vector<TaskEntry*>::iterator subTasksIt, subTasksEnd = subTasks.end();
	std::list<std::string>::iterator msgTriggersIt, msgTriggersEnd;

	switch (trigger) {
	case EVENT_START:
		if (delayStartMS) {
			// call back but skip here to next case after delay
			newTrigger.setAsTimer(0, delayStartMS, EVENT_STARTNOW, this);
			newTriggers.push_back(newTrigger);
			return true;
		}
		// continue to next line
	case EVENT_STARTNOW:
		// Start current module
		currentTask = 0;
		// put me on the remove list to clear out all triggers
		oldTasks.push_back(this);
		// add own new triggers
		msgTriggersIt = msgTriggers[trigger].begin();
		msgTriggersEnd = msgTriggers[trigger].end();
		while (msgTriggersIt != msgTriggersEnd) {
			newTrigger.setAsMsgTrigger(*msgTriggersIt, trigger, this);
			newTriggers.push_back(newTrigger);
			msgTriggersIt++;
		}
		// add own posts
		posts.insert(posts.end(), msgPosts[trigger].begin(), msgPosts[trigger].end());
		// if subtasks
		if (( (subTasksIt = subTasks.begin()) != subTasksEnd) && (*subTasksIt) ) {
			// - send start trigger to first one (iterates down)
			(*subTasksIt)->handleTrigger(trigger, posts, newTriggers, oldTasks);
			currentTask = 1;
		}
		// done
		break;
	case EVENT_TIMEOUT:
		// stop current module
		currentTask = -1;
		// put me on the remove list to clear out all triggers
		oldTasks.push_back(this);
		// we are stopping so no new triggers to add
		// add own posts
		posts.insert(posts.end(), msgPosts[trigger].begin(), msgPosts[trigger].end());
		// if any subtasks are active, just stop it (iterates down)
		for (subTasksIt = subTasks.begin(); subTasksIt != subTasksEnd; subTasksIt++) {
			if ((*subTasksIt)->isActive())
				(*subTasksIt)->handleTrigger(EVENT_STOP, posts, newTriggers, oldTasks);
		}
		// if has parent
		// - send timeout trigger to this one (iterates up)
		if (parentTask && parentTask->isActive())
			parentTask->handleTrigger(EVENT_TIMEOUT, posts, newTriggers, oldTasks);
		// done
		break;
	case EVENT_STOP:
		// stop current module
		currentTask = -1;
		// we are stopping so no new triggers to add
		// put me on the remove list to clear out all triggers
		oldTasks.push_back(this);
		// don't add any posts, just stop
		// if any subtasks are active, just stop it (iterates down)
		for (subTasksIt = subTasks.begin(); subTasksIt != subTasksEnd; subTasksIt++) {
			if ((*subTasksIt)->isActive())
				(*subTasksIt)->handleTrigger(EVENT_STOP, posts, newTriggers, oldTasks);
		}
		// if has parent, do not send stop to this one
		// done
		break;

	case EVENT_SUCCESS:
		// stop current module
		currentTask = -1;
		// put me on the remove list to clear out all triggers
		oldTasks.push_back(this);
		// we are stopping so no new triggers to add
		// add own posts
		posts.insert(posts.end(), msgPosts[trigger].begin(), msgPosts[trigger].end());
		// if any subtasks are active, just stop it (iterates down)
		for (subTasksIt = subTasks.begin(); subTasksIt != subTasksEnd; subTasksIt++) {
			if ((*subTasksIt)->isActive())
				(*subTasksIt)->handleTrigger(EVENT_STOP, posts, newTriggers, oldTasks);
		}
		// if has parent
		// - send next trigger to this one (one level up only)
		if (parentTask && parentTask->isActive())
			parentTask->handleTrigger(EVENT_COMPLETE, posts, newTriggers, oldTasks);
		// done
		break;

	case EVENT_COMPLETE:
		// subtask [currentTask] has completed successfully
		// and we know it and all children have stopped now
		// add own posts
		posts.insert(posts.end(), msgPosts[trigger].begin(), msgPosts[trigger].end());

		if (!currentTask || (currentTask == subTasks.size() - 1)) {
			currentTask = -1;
			// this task is itself now also complete
			if (parentTask && parentTask->isActive())
				parentTask->handleTrigger(EVENT_COMPLETE, posts, newTriggers, oldTasks);
			return true;
		}

		// now we know there is a next task
		task = subTasks.at((uint32)currentTask);
		currentTask++;

		if (delayBetweenMS) {
			// call next subtask after delay
			newTrigger.setAsTimer(0, delayBetweenMS, EVENT_START, task);
			newTriggers.push_back(newTrigger);
			return true;
		}

		// else just start it right now
		task->handleTrigger(trigger, posts, newTriggers, oldTasks);

		// if has parent no need to tell it anything
		// done
		break;

	case EVENT_FAIL:
		// stop current module
		currentTask = -1;
		// put me on the remove list to clear out all triggers
		oldTasks.push_back(this);
		// we are stopping so no new triggers to add
		// add own posts
		posts.insert(posts.end(), msgPosts[trigger].begin(), msgPosts[trigger].end());
		// if any subtasks are active, just stop it (iterates down)
		for (subTasksIt = subTasks.begin(); subTasksIt != subTasksEnd; subTasksIt++) {
			if ((*subTasksIt)->isActive())
				(*subTasksIt)->handleTrigger(EVENT_STOP, posts, newTriggers, oldTasks);
		}
		// if has parent
		// - send fail trigger to this one (iterates up)
		if (parentTask && parentTask->isActive())
			parentTask->handleTrigger(EVENT_FAIL, posts, newTriggers, oldTasks);
		// done
		break;

	case EVENT_RESTART:
		currentTask = -1;
		// we are stopping so no new triggers to add
		// put me on the remove list to clear out all triggers
		oldTasks.push_back(this);
		// if any subtasks are active, just stop it (iterates down)
		for (subTasksIt = subTasks.begin(); subTasksIt != subTasksEnd; subTasksIt++) {
			if ((*subTasksIt)->isActive())
				(*subTasksIt)->handleTrigger(EVENT_STOP, posts, newTriggers, oldTasks);
		}

		// call back to normal START immediately
		newTrigger.setAsTimer(GetTimeNow(), 0, EVENT_START, this);
		newTriggers.push_back(newTrigger);
		// done
		break;
	case EVENT_NEXT:
		if (!currentTask || (currentTask == subTasks.size() - 1)) {
			currentTask = -1;
			// this task is itself now also complete
			if (parentTask && parentTask->isActive())
				parentTask->handleTrigger(EVENT_COMPLETE, posts, newTriggers, oldTasks);
			return true;
		}

		// now we know there is a next task
		task = subTasks.at((uint32)currentTask);
		currentTask++;

		if (delayBetweenMS) {
			// call next subtask after delay
			newTrigger.setAsTimer(0, delayBetweenMS, EVENT_START, task);
			newTriggers.push_back(newTrigger);
			return true;
		}

		// else just start it right now
		task->handleTrigger(trigger, posts, newTriggers, oldTasks);

		// if has parent no need to tell it anything
		// done
		break;
	case EVENT_PREVIOUS:
		if (!currentTask || (currentTask == 1)) {
			// there is no previous to go to, ignore
			return true;
		}

		// now we know there is a next task
		task = subTasks.at((uint32)currentTask - 2);
		currentTask--;

		// we do not add delayBetweenMS when going back
		// just start it right now
		task->handleTrigger(EVENT_START, posts, newTriggers, oldTasks);

		// if has parent no need to tell it anything
		// done
		break;

	default:
		break;
	}

	return true;
}




bool TaskEntry::log(std::string text) {
	if (!api) return false;
	return api->logPrint(1, "Task '%s': %s", name.c_str(), text.c_str());
}

bool TaskEntry::fromJSON(const char* json, jsmntok_t* tokens, int tokenCount, int parent, std::list<std::string> &allTriggers, std::list<std::string> &allPosts) {
	TaskEntry* task;
	std::vector<int> jsonArrayIdx;
	std::vector<int>::iterator i, e;
	int token;

	if (!parent) {
		// I am the top task, just add subtasks
		jsonArrayIdx = GetJSONArrayIndexes(tokens, tokenCount, json, "tasks", JSMN_UNDEFINED);
		i = jsonArrayIdx.begin();
		e = jsonArrayIdx.end();
		while (i != e) {
			task = new TaskEntry(api);
			task->parentTask = this;
			if (!task->fromJSON(json, tokens, tokenCount, *i, allTriggers, allPosts)) {
				delete task;
				return false;
			}
			subTasks.push_back(task);
			i++;
		}
		return (subTasks.size());
	}
	
	// I am not the top task, add other info
	name = GetJSONChildValueString(tokens, tokenCount, json, "name", parent);
	//printf("Got: %s\n", name.c_str());
	description = GetJSONChildValueString(tokens, tokenCount, json, "description", parent);
	timeoutMS = (uint32)GetJSONChildValueUint64(tokens, tokenCount, json, "timeout", parent);

	// read delays
	if ((token = GetJSONToken(tokens, tokenCount, json, "delays", parent)) >= 0) {
		delayStartMS = (uint32)GetJSONChildValueUint64(tokens, tokenCount, json, "start", token);
		delayBetweenMS = (uint32)GetJSONChildValueUint64(tokens, tokenCount, json, "between", token);
	}

	int n, c;
	std::string typeStr;
	// read triggers
	if ((token = GetJSONToken(tokens, tokenCount, json, "triggers", parent)) >= 0) {
		for (n = 1; n < EVENT_COUNT; n++) {
			c = 0;
			jsonArrayIdx = GetJSONChildArrayIndexes(tokens, tokenCount, json, TaskEventTypeNames[n].c_str(), token, JSMN_UNDEFINED);
			i = jsonArrayIdx.begin();
			e = jsonArrayIdx.end();
			while (i != e) {
				if ((typeStr = GetJSONValueString(tokens, tokenCount, json, *i)).length()) {
					msgTriggers[n].push_back(typeStr);
					allTriggers.push_back(typeStr);
				}
				c++;
				i++;
			}
			//if (c)
			//	log(utils::StringFormat("Found %d triggers for event '%s'", c, TaskEventTypeNames[n].c_str()).c_str());
		}
	}

	// read posts
	if ((token = GetJSONToken(tokens, tokenCount, json, "posts", parent)) >= 0) {
		for (n = 1; n < EVENT_COUNT; n++) {
			c = 0;
			jsonArrayIdx = GetJSONChildArrayIndexes(tokens, tokenCount, json, TaskEventTypeNames[n].c_str(), token, JSMN_UNDEFINED);
			i = jsonArrayIdx.begin();
			e = jsonArrayIdx.end();
			while (i != e) {
				if ((typeStr = GetJSONValueString(tokens, tokenCount, json, *i)).length()) {
					msgPosts[n].push_back(typeStr);
					allPosts.push_back(typeStr);
				}
				c++;
				i++;
			}
			//if (c)
			//	log(utils::StringFormat("Found %d posts for event '%s'", c, TaskEventTypeNames[n].c_str()).c_str());
		}
	}


	// and then add subtasks
	jsonArrayIdx = GetJSONChildArrayIndexes(tokens, tokenCount, json, "subtasks", parent, JSMN_UNDEFINED);
	i = jsonArrayIdx.begin();
	e = jsonArrayIdx.end();
	while (i != e) {
		task = new TaskEntry(api);
		task->parentTask = this;
		if (!task->fromJSON(json, tokens, tokenCount, *i, allTriggers, allPosts)) {
			delete task;
			return false;
		}
		subTasks.push_back(task);
		i++;
	}

	return true;
}

std::string TaskEntry::toJSON() {
	return "";
}

std::string TaskEntry::toJSONStatus() {

	std::list<std::string> statusList;
	std::vector<TaskEntry*>::iterator subTasksIt, subTasksEnd = subTasks.end();

	for (subTasksIt = subTasks.begin(); subTasksIt != subTasksEnd; subTasksIt++)
		statusList.push_back((*subTasksIt)->toJSONStatus());

	if (!parentTask) {
		return utils::StringFormat("{\"tasks\": [%s]}\n", utils::TextJoin(statusList, ",").c_str());
	}
	else {
		return utils::StringFormat("{\"title\": \"%s\", \"text\" : \"%s\", \"color\" : \"%s\", \"status\" : \"\", \"children\" : [%s]}\n",
			name.c_str(), description.c_str(),
			((currentTask < 0) ? "lightgrey" : ((currentTask > 0) ? "lightgreen" : "green")),
			utils::TextJoin(statusList, ",").c_str());
	}
}




} // namespace cmlabs
