

//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
//_/_/
//_/_/  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 Subscriptions.h
	@brief Subscription, trigger, retrieval, query, post and signal specifications — the declarative wiring of a Psyclone system.

	A Psyclone system is configured by declaring, per component, WHAT it reacts
	to and what it then does. Those declarations are held in the flat POD spec
	structs in this file: a TriggerSpec (what type/context/filters fire a
	crank), with attached FilterSpec, RetrieveSpec, QuerySpec, PostSpec and
	SignalSpec records laid out back-to-back in the same memory block so a
	complete trigger definition can be copied through shared memory verbatim.
	Each spec can be serialised to/from the system XML specification
	(toXML()/fromXML()).

	@note Time fields such as @c maxage are stored in MICROSECONDS internally;
	the XML system specification expresses them in milliseconds and the parser
	multiplies by 1000 when filling in these structs.
	@warning All spec structs are fixed-layout binary contracts shared across
	processes — do not change field order, sizes or the MAX...LEN buffer sizes.
	@ingroup Messaging
*/

#if !defined(_SUBSCRIPTIONS_H_)
#define _SUBSCRIPTIONS_H_

#include "PsyTime.h"
#include "Utils.h"
#include "HTML.h"
#include "ObjectIDs.h"
#include "xml_parser.h"
#include "DataMessage.h"

/*! @name Component types
	Kinds of components a Psyclone system can host: whiteboards (message
	stores), catalogs (keyed data stores), streams/feeds (continuous data),
	services, and modules (user processing code, internal or external).
	Note COMP_INTERNAL deliberately aliases COMP_MODULE.
@{ */
#define COMP_WHITEBOARD		1	//!< Whiteboard: publish/subscribe message store with retention.
#define COMP_CATALOG		2	//!< Catalog: keyed/queryable data store.
#define COMP_STREAM			3	//!< Stream: continuous binary/media data channel.
#define COMP_FEED			4	//!< Feed: external data feed.
#define COMP_SERVICE		5	//!< Service: request/response provider.
#define COMP_MODULE			6	//!< Module: user processing component.
#define COMP_INTERNAL		6	//!< Internal module (same id as COMP_MODULE).
#define COMP_EXTERNAL		7	//!< External module running outside the node process.
/*! @} */

/*! @name Migration types
	Whether a component may be migrated to another node at runtime.
@{ */
#define COMP_MIGRATE_NO		0	//!< Component must stay on its assigned node.
#define COMP_MIGRATE_ALLOW	1	//!< Component may be migrated between nodes.
/*! @} */


namespace cmlabs {

/*! @name Trigger filter operations
	Operation codes for FilterSpec::filter — how a candidate message's user
	entry is tested before a trigger fires.
@{ */
#define TRIGGERFILTER_HASKEY			1	//!< Fire only if the message has the named key (any entry type).
#define TRIGGERFILTER_EQUALS			2	//!< String equality between entry value and filter value.
#define TRIGGERFILTER_NOTEQUALS			3	//!< String inequality.
#define TRIGGERFILTER_EQUALSNUMERIC		4	//!< Numeric equality (values parsed as float64, epsilon compare).
#define TRIGGERFILTER_NOTEQUALSNUMERIC	5	//!< Numeric inequality.
#define TRIGGERFILTER_GREATERTHAN		6	//!< Numeric greater-than (values equal within epsilon also pass, i.e. >=).
#define TRIGGERFILTER_LESSTHAN			7	//!< Numeric less-than (values equal within epsilon also pass, i.e. <=).
#define TRIGGERFILTER_MAXAGE			8	//!< Message must be younger than FilterSpec::time (milliseconds).
/*! @} */

/*! @name Index key types
	Value interpretations for indexed user entries (see MessageIndex::addIndexKey
	and RetrieveSpec::keytype).
@{ */
#define INDEX_TIME		1	//!< Index the entry as a time (uint64 microseconds).
#define INDEX_STRING	2	//!< Index the entry as a string.
#define INDEX_INTEGER	3	//!< Index the entry as an int64.
#define INDEX_FLOAT		4	//!< Index the entry as a float64.
/*! @} */

/*! @brief One filter condition attached to a TriggerSpec: a test on a user entry of the candidate message.

	A trigger only fires when ALL of its filters pass. Stored as fixed-size POD
	directly after the TriggerSpec in the same memory block.
	@ingroup Messaging
*/
struct FilterSpec {
	uint8 filter;					//!< Filter operation, one of the TRIGGERFILTER_... codes.
	uint64 time;					//!< Time operand in milliseconds (used by TRIGGERFILTER_MAXAGE).
	char key[MAXKEYNAMELEN+1];		//!< Name of the user entry to test.
	char value[MAXVALUENAMELEN+1];	//!< Comparison operand as text (parsed numerically for the numeric operations).
	//! Serialise as a <filter .../> XML element.
	std::string toXML() {
		return utils::StringFormat("\t<filter filter=\"%u\" time=\"%llu\" key=\"%s\" value=\"%s\" />\n",
			filter, time, key, value);
	}
	//! Parse from an XML string containing a <filter .../> element. @param xml XML text. @return true if all attributes were present and valid.
	bool fromXML(const char* xml) {
		if (!xml) return false;
		XMLResults xmlResults;
		XMLNode node = XMLNode::parseString(xml, "filter", &xmlResults);
		if (xmlResults.error != eXMLErrorNone)
			return false;
		if (node.isEmpty()) return false;
		return fromXML(node);
	}
	//! Parse from an already-parsed &lt;filter&gt; XML node. @param node the XML node. @return true if all attributes were present and valid.
	bool fromXML(XMLNode &node) {
		const char* str;
		if (str = node.getAttribute("filter")) filter = (uint8)utils::Ascii2Uint32(str); else return false;
		if (str = node.getAttribute("time")) time = utils::Ascii2Uint64(str); else return false;
		if (str = node.getAttribute("key")) utils::strcpyavail(key, str, MAXKEYNAMELEN, true); else return false;
		if (str = node.getAttribute("value")) utils::strcpyavail(value, str, MAXVALUENAMELEN, true); else return false;
		return true;
	}
};

/*! @brief Specification of a query against a catalog/service/feed, attached to a TriggerSpec.

	When its trigger fires, the runtime executes this query against the named
	source component (or an external host/port) and delivers the results to
	the crank alongside the trigger message. Fixed-size POD stored back-to-back
	inside the TriggerSpec block.
	@ingroup Messaging
*/
struct QuerySpec {
	uint32 source;						//!< Component id of the source to query.
	uint32 origin;						//!< Restrict to data originating from this component id (0 = any).
	uint64 maxage;						//!< Maximum age of results in MICROSECONDS (XML gives ms; parser multiplies by 1000).
	uint32 maxcount;					//!< Maximum number of results to return (0 = unlimited).
	PsyContext context;					//!< Restrict to data matching this context.
	bool binary;						//!< Request results in binary form rather than text.
	uint32 ipHost;						//!< External source: IPv4 host (as uint32), 0 if not used.
	uint16 ipPort;						//!< External source: TCP/UDP port, 0 if not used.
	char sourceName[MAXKEYNAMELEN+1];	//!< Name of the source component (alternative to numeric id).
	char hostName[MAXKEYNAMELEN + 1];	//!< External source: host name (alternative to ipHost).
	char name[MAXKEYNAMELEN+1];			//!< Name of this query, used to reference its results in the crank.
	char key[MAXKEYNAMELEN+1];			//!< Key to query on.
	char value[MAXVALUENAMELEN+1];		//!< Value to match.
	char type[MAXKEYNAMELEN+1];			//!< Requested data/content type.
	char subdir[MAXKEYNAMELEN+1];		//!< Sub-directory/collection within the source.
	char ext[MAXKEYNAMELEN+1];			//!< File extension / data format selector.
	char operation[MAXKEYNAMELEN+1];	//!< Operation to perform (source-specific verb).
	//! Serialise as a <query .../> XML element.
	std::string toXML() {
		return utils::StringFormat("\t<query source=\"%u\" origin=\"%u\" maxage=\"%llu\" maxcount=\"%u\" binary=\"%s\" name=\"%s\" key=\"%s\" value=\"%s\" type=\"%s\" subdir=\"%s\" ext=\"%s\" operation=\"%s\" sourcename=\"%s\" hostname=\"%s\" iphost=\"%u\" port=\"%u\" />\n",
			source, origin, maxage, maxcount, (binary ? "yes" : "no"), name, key, value, type, subdir, ext, operation,
			sourceName, hostName, ipHost, ipPort);
	}
	//! Parse from an XML string containing a <query .../> element. @param xml XML text. @return true if all attributes were present and valid.
	bool fromXML(const char* xml) {
		if (!xml) return false;
		XMLResults xmlResults;
		XMLNode node = XMLNode::parseString(xml, "query", &xmlResults);
		if (xmlResults.error != eXMLErrorNone)
			return false;
		if (node.isEmpty()) return false;
		return fromXML(node);
	}
	//! Parse from an already-parsed &lt;query&gt; XML node. @param node the XML node. @return true if all attributes were present and valid.
	bool fromXML(XMLNode &node) {
		const char* str;
		if (str = node.getAttribute("source")) source = utils::Ascii2Uint32(str); else return false;
		if (str = node.getAttribute("origin")) origin = utils::Ascii2Uint32(str); else return false;
		if (str = node.getAttribute("maxage")) maxage = utils::Ascii2Uint64(str); else return false;
		if (str = node.getAttribute("maxcount")) maxcount = utils::Ascii2Uint32(str); else return false;
		if (str = node.getAttribute("context")) { if (!context.fromString(str)) return false; } else return false;
		if (str = node.getAttribute("binary")) binary = (stricmp(str, "yes") == 0); else return false;
		if (str = node.getAttribute("name")) utils::strcpyavail(name, str, MAXKEYNAMELEN, true); else return false;
		if (str = node.getAttribute("key")) utils::strcpyavail(key, str, MAXKEYNAMELEN, true); else return false;
		if (str = node.getAttribute("value")) utils::strcpyavail(value, str, MAXVALUENAMELEN, true); else return false;
		if (str = node.getAttribute("type")) utils::strcpyavail(type, str, MAXKEYNAMELEN, true); else return false;
		if (str = node.getAttribute("subdir")) utils::strcpyavail(subdir, str, MAXKEYNAMELEN, true); else return false;
		if (str = node.getAttribute("ext")) utils::strcpyavail(ext, str, MAXKEYNAMELEN, true); else return false;
		if (str = node.getAttribute("operation")) utils::strcpyavail(operation, str, MAXKEYNAMELEN, true); else return false;
		if (str = node.getAttribute("sourcename")) utils::strcpyavail(sourceName, str, MAXKEYNAMELEN, true); else return false;
		if (str = node.getAttribute("hostname")) utils::strcpyavail(hostName, str, MAXKEYNAMELEN, true); else return false;
		if (str = node.getAttribute("iphost")) ipHost = utils::Ascii2Uint32(str); else return false;
		if (str = node.getAttribute("ipport")) ipPort = utils::Ascii2Uint32(str); else return false;
		return true;
	}
};

/*! @brief Specification of a whiteboard retrieval attached to a TriggerSpec.

	When its trigger fires, the runtime retrieves matching messages from the
	whiteboard's MessageIndex — by type, context, tag, sender/addressee, age,
	and optionally a value/range condition on an indexed user entry (see
	keytype and the start/end pairs) — and hands them to the crank together
	with the trigger message. Fixed-size POD stored inside the TriggerSpec block.
	@ingroup Messaging
*/
struct RetrieveSpec {
	uint32 source;						//!< Component id of the whiteboard to retrieve from.
	uint32 origin;						//!< Restrict to messages originating from this component id (0 = any).
	uint64 maxage;						//!< Maximum message age in MICROSECONDS (XML gives ms; parser multiplies by 1000).
	uint32 maxcount;					//!< Maximum number of messages to return (0 = unlimited).
	PsyType type;						//!< Message type to match (wildcards allowed).
	PsyContext context;					//!< Context to match.
	uint32 tag;							//!< Tag to match (0 = any).
	char name[MAXKEYNAMELEN+1];			//!< Name of this retrieve, used to reference its results in the crank.
	char key[MAXKEYNAMELEN+1];			//!< Indexed user entry to range-query on (empty = time/type retrieval only).
	uint8 keytype;						//!< Interpretation of @c key: INDEX_TIME, INDEX_STRING, INDEX_INTEGER or INDEX_FLOAT.
	uint32 from;						//!< Restrict to messages sent by this component id (0 = any).
	uint32 to;							//!< Restrict to messages addressed to this component id (0 = any).
	uint64 startTime;					//!< Range start when keytype == INDEX_TIME (microseconds).
	uint64 endTime;						//!< Range end when keytype == INDEX_TIME (microseconds).
	int64 startInt;						//!< Range start when keytype == INDEX_INTEGER.
	int64 endInt;						//!< Range end when keytype == INDEX_INTEGER.
	float64 startFloat;					//!< Range start when keytype == INDEX_FLOAT.
	float64 endFloat;					//!< Range end when keytype == INDEX_FLOAT.
	char startString[MAXVALUENAMELEN+1];//!< Range start when keytype == INDEX_STRING.
	char endString[MAXVALUENAMELEN+1];	//!< Range end when keytype == INDEX_STRING.
	//! Serialise as a <retrieve .../> XML element.
	std::string toXML() {
		return utils::StringFormat("\t<retrieve source=\"%u\" origin=\"%u\" maxage=\"%llu\" maxcount=\"%u\" type=\"%s\" context=\"%s\" tag=\"%u\" name=\"%s\" key=\"%s\" keytype=\"%u\" from=\"%u\" to=\"%u\" starttime=\"%llu\" endtime=\"%llu\" startint=\"%lld\" endint=\"%lld\" startfloat=\"%f\" endfloat=\"%f\" startstring=\"%s\" endstring=\"%s\" />\n",
			source, origin, maxage, maxcount, type.toString().c_str(), context.toString().c_str(),
			tag, name, key, keytype, from, to,
			startTime, endTime, startInt, endInt, startFloat, endFloat,	startString, endString);
	}
	//! Parse from an XML string containing a <retrieve .../> element. @param xml XML text. @return true if all attributes were present and valid.
	bool fromXML(const char* xml) {
		if (!xml) return false;
		XMLResults xmlResults;
		XMLNode node = XMLNode::parseString(xml, "retrieve", &xmlResults);
		if (xmlResults.error != eXMLErrorNone)
			return false;
		if (node.isEmpty()) return false;
		return fromXML(node);
	}
	//! Parse from an already-parsed &lt;retrieve&gt; XML node. @param node the XML node. @return true if all attributes were present and valid.
	bool fromXML(XMLNode &node) {
		const char* str;
		if (str = node.getAttribute("source")) source = utils::Ascii2Uint32(str); else return false;
		if (str = node.getAttribute("origin")) origin = utils::Ascii2Uint32(str); else return false;
		if (str = node.getAttribute("maxage")) maxage = utils::Ascii2Uint64(str); else return false;
		if (str = node.getAttribute("maxcount")) maxcount = utils::Ascii2Uint32(str); else return false;
		if (str = node.getAttribute("type")) { if (!type.fromString(str)) return false; } else return false;
		if (str = node.getAttribute("context")) { if (!context.fromString(str)) return false; } else return false;
		if (str = node.getAttribute("tag")) tag = utils::Ascii2Uint32(str); else return false;
		if (str = node.getAttribute("name")) utils::strcpyavail(name, str, MAXKEYNAMELEN, true); else return false;
		if (str = node.getAttribute("key")) utils::strcpyavail(key, str, MAXKEYNAMELEN, true); else return false;
		if (str = node.getAttribute("keytype")) keytype = (uint8)utils::Ascii2Uint32(str); else return false;
		if (str = node.getAttribute("from")) from = utils::Ascii2Uint32(str); else return false;
		if (str = node.getAttribute("to")) to = utils::Ascii2Uint32(str); else return false;
		if (str = node.getAttribute("starttime")) startTime = utils::Ascii2Uint64(str); else return false;
		if (str = node.getAttribute("endtime")) endTime = utils::Ascii2Uint64(str); else return false;
		if (str = node.getAttribute("startint")) startInt = utils::Ascii2Int64(str); else return false;
		if (str = node.getAttribute("endint")) endInt = utils::Ascii2Int64(str); else return false;
		if (str = node.getAttribute("startfloat")) startFloat = utils::Ascii2Float64(str); else return false;
		if (str = node.getAttribute("endfloat")) endFloat = utils::Ascii2Float64(str); else return false;
		if (str = node.getAttribute("startstring")) utils::strcpyavail(startString, str, MAXVALUENAMELEN, true); else return false;
		if (str = node.getAttribute("endstring")) utils::strcpyavail(endString, str, MAXVALUENAMELEN, true); else return false;
		return true;
	}
};

//! Delivery policy flag: message delivery is not guaranteed (may be dropped under load).
#define MESSAGE_NON_GUARANTEED	1

/*! @brief Specification of a message to post when a trigger fires (or a crank completes).

	Describes the outgoing message: addressee, type, contexts, tag, ttl,
	policy, plus optional inline content entries. Content is packed into the
	fixed @c content buffer as a "||T||key||value" token list (T = S/I/F for
	string/integer/float) so the whole spec stays a flat fixed-size POD;
	addContentToMsg() unpacks it into a real DataMessage at post time.
	@ingroup Messaging
*/
struct PostSpec {
	uint32 to;						//!< Addressee component id (0 = publish to subscribers).
	PsyType type;					//!< Type of the posted message.
	PsyContext context;				//!< Context the post applies in.
	PsyContext contextchange;		//!< Context switch the posted message announces (NOCONTEXT if none).
	uint32 tag;						//!< Tag for the posted message.
	uint64 ttl;						//!< Time-to-live of the posted message in microseconds.
	uint8 policy;					//!< Delivery policy flags (e.g. MESSAGE_NON_GUARANTEED).
	uint32 contentCount;			//!< Number of inline content entries packed into @c content.
	char name[MAXKEYNAMELEN+1];		//!< Name of this post spec.
//	char contentKey[MAXKEYNAMELEN + 1];
	char content[MAXCONTENTLEN + 1];//!< Packed content entries: "||T||key||value" repeated (T = S, I or F).
	//! Serialise as a &lt;post ...&gt;...&lt;/post&gt; XML element including content children.
	std::string toXML() {
		if (contentCount) {
			// ||S||key||val||I||key||val||S||key||val
			const char* contentType, *key, *value;
			std::string strContent;
			std::vector<std::string> entries = utils::TextListSplit(content, "||", false, true);
			uint32 slotCount = 3 * contentCount;
			uint32 n = 0;
			if (entries.size() == slotCount) {
				while (n < slotCount) {
					contentType = entries[n++].c_str();
					key = entries[n++].c_str();
					value = entries[n++].c_str();
					if (strcmp(contentType, "S") == 0) {
						strContent += utils::StringFormat("\t\t<content type=\"string\" key=\"%s\" value=\"%s\" />\n",
							html::EncodeHTML(key).c_str(), html::EncodeHTML(value).c_str());
					}
					else if (strcmp(contentType, "I") == 0) {
						strContent += utils::StringFormat("\t\t<content type=\"integer\" key=\"%s\" value=\"%s\" />\n",
							html::EncodeHTML(key).c_str(), html::EncodeHTML(value).c_str());
					}
					else if (strcmp(contentType, "F") == 0) {
						strContent += utils::StringFormat("\t\t<content type=\"float\" key=\"%s\" value=\"%s\" />\n",
							html::EncodeHTML(key).c_str(), html::EncodeHTML(value).c_str());
					}
				}
			}
			return utils::StringFormat("\t<post to=\"%u\" type=\"%s\" context=\"%s\" contextchange=\"%s\" tag=\"%u\" ttl=\"%llu\" policy=\"%u\" name=\"%s\">\n%s</post>\n",
				to, type.toString().c_str(), context.toString().c_str(), contextchange.toString().c_str(), tag, ttl, policy, name, strContent.c_str());
		}
		else {
			return utils::StringFormat("\t<post to=\"%u\" type=\"%s\" context=\"%s\" contextchange=\"%s\" tag=\"%u\" ttl=\"%llu\" policy=\"%u\" name=\"%s\" />\n",
				to, type.toString().c_str(), context.toString().c_str(), contextchange.toString().c_str(), tag, ttl, policy, name);
		}
	}
	//! Parse from an XML string containing a <post .../> element. @param xml XML text. @return true if all attributes were present and valid.
	bool fromXML(const char* xml) {
		if (!xml) return false;
		XMLResults xmlResults;
		XMLNode node = XMLNode::parseString(xml, "post", &xmlResults);
		if (xmlResults.error != eXMLErrorNone)
			return false;
		if (node.isEmpty()) return false;
		return fromXML(node);
	}
	//! Parse from an already-parsed &lt;post&gt; XML node, including its &lt;content&gt; children. @param node the XML node. @return true if all attributes were present and valid.
	bool fromXML(XMLNode &node) {
		const char* str;
		if (str = node.getAttribute("to")) to = utils::Ascii2Uint32(str); else return false;
		if (str = node.getAttribute("type")) { if (!type.fromString(str)) return false; }
		else return false;
		if (str = node.getAttribute("context")) { if (!context.fromString(str)) return false; }
		else return false;
		if (str = node.getAttribute("contextchange")) { if (!contextchange.fromString(str)) return false; }
		else return false;
		if (str = node.getAttribute("tag")) tag = utils::Ascii2Uint32(str); else return false;
		if (str = node.getAttribute("ttl")) ttl = utils::Ascii2Uint64(str); else return false;
		if (str = node.getAttribute("policy")) policy = (uint8)utils::Ascii2Uint32(str); else return false;
		if (str = node.getAttribute("name")) utils::strcpyavail(name, str, MAXKEYNAMELEN, true); else return false;
		contentFromXML(node);
		return true;
	}
	//! Collect inline &lt;content&gt; entries (and contentkey/content attributes) from the XML node into the packed @c content buffer. @param node the XML node. @return true.
	bool contentFromXML(XMLNode &node) {
		std::string strContent;
		const char* str, *key, *value;
		if ((key = node.getAttribute("contentkey")) && (value = node.getAttribute("content"))) {
			strContent = utils::StringFormat("%s||S||%s||%s", content, html::DecodeHTML(key).c_str(), html::DecodeHTML(value).c_str());
			utils::strcpyavail(content, strContent.c_str(), MAXCONTENTLEN, false);
			contentCount++;
		}

		XMLNode subNode;
		uint32 i, count = node.nChildNode("content");
		for (i = 0; i < count; i++) {
			if (!(subNode = node.getChildNode("content", i)).isEmpty()) {
				if (!(key = subNode.getAttribute("key")) || !strlen(key))
					continue;
				if ( (!(value = subNode.getAttribute("content")) || !strlen(value)) &&
					(!(value = subNode.getAttribute("value")) || !strlen(value)) )
					continue;
				str = subNode.getAttribute("type");
				if (str && stricmp(str, "integer") == 0)
					strContent = utils::StringFormat("%s||I||%s||%s", content, html::DecodeHTML(key).c_str(), html::DecodeHTML(value).c_str());
				else if (str && ((stricmp(str, "double") == 0) || (stricmp(str, "float") == 0)))
					strContent = utils::StringFormat("%s||F||%s||%s", content, html::DecodeHTML(key).c_str(), html::DecodeHTML(value).c_str());
				else
					strContent = utils::StringFormat("%s||S||%s||%s", content, html::DecodeHTML(key).c_str(), html::DecodeHTML(value).c_str());
				utils::strcpyavail(content, strContent.c_str(), MAXCONTENTLEN, false);
				contentCount++;
			}
		}
		return true;
	}
	/*! Unpack the inline content entries into @p msg as typed user entries.
	@param msg the message being posted (entries are set on it)
	@return number of entries added */
	uint32 addContentToMsg(DataMessage* msg) {
		if (!contentCount)
			return 0;
		uint32 count = 0;
		const char* contentType, *key, *value;
		std::string strContent;
		std::vector<std::string> entries = utils::TextListSplit(content, "||", false, true);
		uint32 slotCount = 3 * contentCount, n = 0;
		if (entries.size() == slotCount) {
			while (n < slotCount) {
				contentType = entries[n++].c_str();
				key = entries[n++].c_str();
				value = entries[n++].c_str();
				if (strcmp(contentType, "S") == 0)
					msg->setString(key, value);
				else if (strcmp(contentType, "I") == 0)
					msg->setInt(key, utils::Ascii2Int64(value));
				else if (strcmp(contentType, "F") == 0)
					msg->setFloat(key, utils::Ascii2Float64(value));
				count++;
			}
		}
		return count;
	}
};

/*! @brief Specification of a signal (a typed notification without payload) emitted when a trigger fires.
	@ingroup Messaging
*/
struct SignalSpec {
	PsyType type;				//!< Type of the signal to emit.
	char name[MAXKEYNAMELEN+1];	//!< Name of this signal spec.
	//! Serialise as a <signal .../> XML element.
	std::string toXML() {
		return utils::StringFormat("\t<signal type=\"%s\" name=\"%s\" />\n", type.toString().c_str(), name);
	}
	//! Parse from an XML string containing a <signal .../> element. @param xml XML text. @return true if valid.
	bool fromXML(const char* xml) {
		if (!xml) return false;
		XMLResults xmlResults;
		XMLNode node = XMLNode::parseString(xml, "signal", &xmlResults);
		if (xmlResults.error != eXMLErrorNone)
			return false;
		if (node.isEmpty()) return false;
		return fromXML(node);
	}
	bool fromXML(XMLNode &node) {
		const char* str;
		if (str = node.getAttribute("type")) { if (!type.fromString(str)) return false; } else return false;
		if (str = node.getAttribute("name")) utils::strcpyavail(name, str, MAXKEYNAMELEN, true); else return false;
		return true;
	}
};

/*! @brief A complete trigger definition: what fires a component's crank and what happens then.

	TriggerSpec is a variable-length flat binary object: the fixed header below
	is followed in the SAME allocation by filterCount FilterSpecs, then
	retrieveCount RetrieveSpecs, then queryCount QuerySpecs, then postCount
	PostSpecs, then signalCount SignalSpecs (see the get...Spec(n) accessors,
	which compute offsets into this tail). This layout lets a full trigger
	definition travel through shared memory as one block.
	@warning Binary contract — do not change field order/sizes, and keep the
	tail ordering (filters, retrieves, queries, posts, signals) in sync with
	the accessor offset arithmetic.
	@ingroup Messaging
*/
struct TriggerSpec {
	/*! Zero the header and stamp size/cid.
	@param s total block size to record (0 = just sizeof(TriggerSpec))
	@return true */
	bool reset(uint32 s = 0) {
		memset(this, 0, sizeof(TriggerSpec));
		size = (s) ? s : sizeof(TriggerSpec);
		cid = TRIGGERDATA;
		return true;
	}
	//! @param n filter index. @return pointer to the n'th FilterSpec in the tail, or NULL if out of range.
	FilterSpec* getFilterSpec(uint32 n) {
		if (n >= filterCount) return NULL;
		return (FilterSpec*)
			(((char*)this) + sizeof(TriggerSpec) + (n*sizeof(FilterSpec)));
	}
	//! @param n retrieve index. @return pointer to the n'th RetrieveSpec in the tail, or NULL if out of range.
	RetrieveSpec* getRetrieveSpec(uint32 n) {
		if (n >= retrieveCount) return NULL;
		return (RetrieveSpec*)
			(((char*)this) + sizeof(TriggerSpec) + (filterCount*sizeof(FilterSpec)) + (n*sizeof(RetrieveSpec)));
	}
	//! @param name retrieve name. @return the RetrieveSpec with this name, or NULL.
	RetrieveSpec* getRetrieveSpec(const char* name);
	//! @param n query index. @return pointer to the n'th QuerySpec in the tail, or NULL if out of range.
	QuerySpec* getQuerySpec(uint32 n) {
		if (n >= queryCount) return NULL;
		return (QuerySpec*)
			(((char*)this) + sizeof(TriggerSpec) + (filterCount*sizeof(FilterSpec)) +
			(retrieveCount*sizeof(RetrieveSpec)) + (n*sizeof(QuerySpec)));
	}
	//! @param name query name. @return the QuerySpec with this name, or NULL.
	QuerySpec* getQuerySpec(const char* name);
	//! @param name post name. @return new list of matching PostSpecs (caller deletes the list, NOT the specs), or NULL.
	std::list<PostSpec*>* getPostSpecs(const char* name);
	//! @param type post type. @return new list of matching PostSpecs (caller deletes the list, NOT the specs), or NULL.
	std::list<PostSpec*>* getPostSpecs(PsyType type);
	//! @param name post name. @return the first PostSpec with this name, or NULL.
	PostSpec* getPostSpec(const char* name);
	//! @param type post type. @return the first PostSpec with this type, or NULL.
	PostSpec* getPostSpec(PsyType type);
	//! @param n post index. @return pointer to the n'th PostSpec in the tail, or NULL if out of range.
	PostSpec* getPostSpec(uint32 n) {
		if (n >= postCount) return NULL;
		return (PostSpec*)
			(((char*)this) + sizeof(TriggerSpec) + (filterCount*sizeof(FilterSpec)) +
			(retrieveCount*sizeof(RetrieveSpec)) + (queryCount*sizeof(QuerySpec)) + 
			(n * sizeof(PostSpec)));
	}
	//! @param name signal name. @return the SignalSpec with this name, or NULL.
	SignalSpec* getSignalSpec(const char* name);
	//! @param type signal type. @return the SignalSpec with this type, or NULL.
	SignalSpec* getSignalSpec(PsyType type);
	//! @param n signal index. @return pointer to the n'th SignalSpec in the tail, or NULL if out of range.
	SignalSpec* getSignalSpec(uint32 n) {
		if (n >= signalCount) return NULL;
		return (SignalSpec*)
			(((char*)this) + sizeof(TriggerSpec) + (filterCount*sizeof(FilterSpec)) +
			(retrieveCount*sizeof(RetrieveSpec)) + (queryCount*sizeof(QuerySpec)) +
			(postCount*sizeof(PostSpec)) + (n * sizeof(SignalSpec)));
	}
	//! Serialise the whole trigger (with all attached specs) as XML.
	std::string toXML();

	//! Run the built-in self test. @return true if all tests pass.
	static bool UnitTest();

	uint32 size;				//!< Total block size in bytes including all tail specs.
	uint32 cid;					//!< Object id, TRIGGERDATA when valid.
	uint32 id;					//!< Unique id of this trigger.
	uint32 componentID;			//!< Id of the component owning the trigger.
	uint32 interval;			//!< Periodic trigger interval in ms (0 = message-triggered only).
	uint32 from;				//!< Only messages sent by this component id fire the trigger (0 = any).
	uint32 to;					//!< Only messages addressed to this component id fire the trigger (0 = any).
	uint32 tag;					//!< Only messages with this tag fire the trigger (0 = any).
	uint32 delay;				//!< Delay in ms between trigger match and crank execution.
	uint8 grouppolicy;			//!< Policy for distributing triggers within a group of components.
	uint32 groupparam;			//!< Parameter for the group policy.
	uint16 crankID;				//!< Id of the crank (processing function) to run when fired.
	uint32 group;				//!< Group id this trigger belongs to.
	PsyType type;				//!< Message type that fires the trigger (wildcards allowed).
	PsyContext context;			//!< Context in which the trigger is active.
	PsyContext triggerContext;	//!< Context the triggering message must match.
	char name[MAXKEYNAMELEN+1];	//!< Trigger name.
	uint16 filterCount;			//!< Number of FilterSpecs in the tail.
	uint16 retrieveCount;		//!< Number of RetrieveSpecs in the tail.
	uint16 queryCount;			//!< Number of QuerySpecs in the tail.
	uint16 postCount;			//!< Number of PostSpecs in the tail.
	uint16 signalCount;			//!< Number of SignalSpecs in the tail.
	// Tail layout, in order:
	// filterCount x FilterSpec
	// retrieveCount x RetrieveSpec
	// queryCount x QuerySpec
	// postCount x PostSpec
	// signalCount x SignalSpec
};

// T1.6: declarative join operator (<triggergroup>). See Docs/T1.6-triggergroup-plan.md.
// A group joins several member triggers and activates the crank ONCE per completed set.
// Packed, size-prefixed struct with a tail of GroupMemberSpec (same discipline as TriggerSpec):
// member metadata is kept SEPARATE from TriggerSpec so the shared TriggerSpec binary
// contract is untouched; members are matched back to their TriggerSpec by name.

//! T1.6 group completion policy: fire when ANY N members present (count), or all named.
enum TriggerGroupCount {
	TRIGGERGROUP_COUNT_ALL = 0		//!< 0 = "all" (every named member required). >0 = any-N-of-M.
};
//! T1.6 duplicate-member handling before a set completes.
enum TriggerGroupDup {
	TRIGGERGROUP_DUP_LATEST = 0,		//!< default: latest member value wins.
	TRIGGERGROUP_DUP_KEEP = 1			//!< queue all duplicate member values.
};
//! T1.6 ordering constraint.
enum TriggerGroupOrder {
	TRIGGERGROUP_ORDER_NONE = 0,		//!< default: any arrival order.
	TRIGGERGROUP_ORDER_STRICT = 1		//!< only fire if members arrived in declared order.
};
//! T1.6 join mode.
enum TriggerGroupMode {
	TRIGGERGROUP_MODE_NORMAL = 0,		//!< default: accumulate + complete-check.
	TRIGGERGROUP_MODE_SNAPSHOT = 1		//!< fire on primary member; attach latest of others, no wait.
};
//! T1.6 action on window/maxage expiry with an incomplete set.
enum TriggerGroupOnTimeout {
	TRIGGERGROUP_ONTIMEOUT_FIRE = 0,	//!< default: partial-fire + L1 warn.
	TRIGGERGROUP_ONTIMEOUT_DISCARD = 1	//!< drop the partial set silently-ish.
};

/*! @brief One member of a <code>&lt;triggergroup&gt;</code>, referencing a member trigger by name.
	Packed POD carried in the TriggerGroupSpec tail (binary contract; no heap pointers).
	@ingroup Messaging
*/
struct GroupMemberSpec {
	char name[MAXKEYNAMELEN+1];	//!< Member trigger name (matches a registered TriggerSpec name).
	uint32 maxage;				//!< Per-member freshness bound in ms (0 = no per-member bound).
	uint8 optional;				//!< 1 = not required for completion; included if present.
	uint8 primary;				//!< 1 = primary trigger for mode="snapshot" (defaults to first member).
	uint8 pad[2];				//!< Explicit padding to keep the tail layout deterministic.
};

/*! @brief Specification of a <code>&lt;triggergroup&gt;</code> declarative join (T1.6).
	Packed, size-prefixed struct. Member GroupMemberSpecs live in the tail after
	sizeof(TriggerGroupSpec), reached by byte-offset via getMemberSpec(). Synced across
	processes/nodes via shared memory / CMSDK networking — do NOT add heap pointers or
	std:: containers to the on-wire layout.
	@ingroup Messaging
*/
struct TriggerGroupSpec {
	//! Zero the header and stamp size/cid. @param s total block size (0 = just sizeof). @return true
	bool reset(uint32 s = 0) {
		memset(this, 0, sizeof(TriggerGroupSpec));
		size = (s) ? s : sizeof(TriggerGroupSpec);
		cid = TRIGGERGROUPDATA;
		return true;
	}
	//! @param n member index. @return pointer to the n'th GroupMemberSpec in the tail, or NULL if out of range.
	GroupMemberSpec* getMemberSpec(uint32 n) {
		if (n >= memberCount) return NULL;
		return (GroupMemberSpec*)(((char*)this) + sizeof(TriggerGroupSpec) + (n*sizeof(GroupMemberSpec)));
	}
	//! @param name member name. @return the GroupMemberSpec with this name, or NULL.
	GroupMemberSpec* getMemberSpec(const char* name) {
		for (uint32 n = 0; n < memberCount; n++) {
			GroupMemberSpec* m = getMemberSpec(n);
			if (m && stricmp(m->name, name) == 0) return m;
		}
		return NULL;
	}

	uint32 size;				//!< Total block size in bytes including the member tail.
	uint32 cid;					//!< Object id, TRIGGERGROUPDATA when valid.
	uint32 id;					//!< Unique id of this group (also used as the group tag key).
	uint32 componentID;			//!< Id of the component owning the group.
	uint16 crankID;				//!< Crank to run when the group fires.
	char name[MAXKEYNAMELEN+1];	//!< Group name (unique within the module).

	uint32 maxage;				//!< REQUIRED. Whole partial set evicted if older than this (ms).
	uint32 count;				//!< Completion count: 0 = all (TRIGGERGROUP_COUNT_ALL), else any-N.
	uint32 minCount;			//!< min= : fire at N then re-fire as more of the tag-set arrive (0 = unset).
	uint32 within;				//!< within= : max spread of member POST times, ms (0 = unset).
	uint32 timeWindow;			//!< time= : max spread of member CONTENT times, ms (see hasTimeWindow).
	uint32 debounce;			//!< debounce= : fire at most once per N ms with the latest set (0 = unset).
	uint32 timeout;				//!< timeout= : incomplete-set expiry in ms (0 = unset, falls back to maxage).
	uint8 hasTimeWindow;		//!< 1 = time= was specified (so timeWindow==0 means "identical content time").
	uint8 mode;					//!< TriggerGroupMode (normal/snapshot).
	uint8 slide;				//!< 1 = sliding-window correlation: re-emit on each member update in-window.
	uint8 dup;					//!< TriggerGroupDup (latest/keep).
	uint8 order;				//!< TriggerGroupOrder (none/strict).
	uint8 ontimeout;			//!< TriggerGroupOnTimeout (fire/discard).
	uint16 memberCount;			//!< Number of GroupMemberSpecs in the tail.
	// Tail layout: memberCount x GroupMemberSpec
};

/*! @brief Complete static setup for one component, as parsed from the system specification.

	A single flat POD block (COMPONENTSETUPDATA) carrying everything needed to
	instantiate a component: identity, type, logging, migration, storage
	locations, record/playback limits, an external command line, and the raw
	setup XML (up to 1 MB) for component-specific parameters.
	@warning Binary contract shared across processes — do not change layout.
	Note the 1 MB @c setup tail makes this struct large; allocate on the heap.
	@ingroup Messaging
*/
struct ComponentSetup {
	//! Zero the struct and stamp size/cid. @return true
	bool reset() {
		memset(this, 0, sizeof(ComponentSetup));
		size = sizeof(ComponentSetup);
		cid = COMPONENTSETUPDATA;
		return true;
	}
	uint32 size;					//!< Total struct size in bytes.
	uint32 cid;						//!< Object id, COMPONENTSETUPDATA when valid.
	uint32 componentID;				//!< Unique component id.
	uint16 spaceID;					//!< Id of the PsySpace the component runs in.
	uint32 tag;						//!< Default tag for the component.
	uint8 type;						//!< Component kind, one of the COMP_... defines.
	char name[MAXKEYNAMELEN+1];		//!< Component name.
	char logfile[MAXKEYNAMELEN+1];	//!< Log file name/path.
	uint8 verbose;					//!< Verbosity level.
	uint8 debug;					//!< Debug level.
	uint8 migration;				//!< Migration policy, COMP_MIGRATE_NO or COMP_MIGRATE_ALLOW.
	uint8 priority;					//!< Scheduling priority of the component.
	char function[MAXKEYNAMELEN+1];	//!< Entry function name (for library-loaded modules).
	char root[MAXKEYNAMELEN+1];		//!< Root dir or URL for the component's data.
	bool rotate;					//!< Rotate storage/playback when limits are reached.
	uint8 consoleOutput;			//!< Console output mode.
	uint8 autoRestart;				//!< Restart the component automatically if it dies.

	// Whiteboard, Stream, Catalog, Service, Feed
	char subdir[MAXKEYNAMELEN+1];		//!< Storage sub-directory.
	char extension[MAXKEYNAMELEN+1];	//!< File extension / filetype for stored data.
	char base[MAXKEYNAMELEN+1];			//!< Filename base for stored data.
	char keys[MAXVALUENAMELEN+1];		//!< Index key definitions (component-specific format).
	char value[MAXVALUENAMELEN+1];		//!< Default value parameter.
	char operation[MAXKEYNAMELEN+1];	//!< Default operation verb.
	uint32 timeout;						//!< Operation timeout in ms.

	// Recording & Playback
	uint32 maxcount;	//!< Max number of messages to retain/record (0 = unlimited).
	uint64 maxsize;		//!< Max total bytes to retain/record (0 = unlimited).
	uint32 interval;	//!< Playback interval in ms (0 = use recorded timing).

	// Executable
	char cmdline[MAXKEYNAMELEN+1];	//!< Command line for external executable components.

	// Setup
	char setup[1024*1024];	//!< Raw component-specific setup XML, 1 MB max.
};

// Local only, not transmitted

/*! @brief A custom UI view registered for a component (local configuration; never transmitted).
	@ingroup Messaging */
struct CustomView {
	uint32 compID;						//!< Component the view belongs to.
	char name[MAXKEYNAMELEN+1];			//!< Display name of the view.
	char templateURL[MAXKEYNAMELEN+1];	//!< URL of the HTML template implementing the view.
};

/*! @brief In-memory aggregate of a component's setup, triggers and custom views.

	Convenience owner object used while parsing/holding a system specification:
	owns its ComponentSetup and all TriggerSpecs in @c triggers (deleted in the
	destructor). Local only — never transmitted as a binary block.
	@ingroup Messaging
*/
class ComponentSpec {
public:
	ComponentSpec() {
		setup = new ComponentSetup;
		setup->reset();
	};
	~ComponentSpec() {
		delete(setup);
		setup = NULL;
		while (triggers.size()) {
			delete(triggers.front());
			triggers.pop_front();
		}
		// T1.6: free triggergroup specs (malloc'd packed blocks).
		while (triggerGroups.size()) {
			free(triggerGroups.front());
			triggerGroups.pop_front();
		}
	};

	ComponentSetup* setup;				//!< The component's setup block; owned by this object.
	std::list<TriggerSpec*> triggers;	//!< The component's triggers; owned by this object (deleted in destructor).
	std::list<TriggerGroupSpec*> triggerGroups;	//!< T1.6: <code>&lt;triggergroup&gt;</code> join specs; owned here (freed in destructor).
	std::list<CustomView> customViews;	//!< Custom UI views registered for the component.
};

} // namespace cmlabs

#endif //_SUBSCRIPTIONS_H_

