

//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
//_/_/
//_/_/  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 MessagePlayer.h
	@brief Record and replay of DataMessage streams to/from disk.

	MessagePlayer implements Psyclone's recording and playback facility: in
	write mode it appends messages (with the name of the trigger that produced
	them) to a rotating on-disk buffer with an index file; in read mode it
	replays a recording, reproducing the original inter-message timing so a
	system can be re-run offline against recorded data. Recordings can also be
	exported to CSV for analysis.
	@ingroup Messaging
*/

#if !defined(_MESSAGEPLAYER_H_)
#define _MESSAGEPLAYER_H_

#include "DataMessage.h"

namespace cmlabs {

/*! @brief Fixed-size on-disk index record describing one recorded message.

	The index file is an array of these entries, memory-mapped/loaded verbatim,
	so the layout is a binary contract with existing recordings.
	@note PLATFORM DIFFERENCE: on 32-bit builds (ARCH_32) a dummy pointer pads
	the struct so that its size matches the 64-bit layout, keeping recordings
	interchangeable between 32- and 64-bit builds. The @c msg pointer is only
	meaningful in memory, never on disk.
	@ingroup Messaging
*/
struct ReplayIndexEntry {
	uint32 cid;					//!< Object id, DATAMESSAGEINFOID when valid.
	uint64 serial;				//!< Sequence number of the message within the recording.
	uint64 msgTime;				//!< Original message time in microseconds (drives replay timing).
	uint32 size;				//!< Binary size of the recorded message block in bytes.
	char msgTriggerName[128];	//!< Name of the trigger that produced the message.
	DataMessage* msg;			//!< In-memory pointer to the loaded message; not valid on disk.
#if defined	ARCH_32
	DataMessage* dummy;			//!< Padding on 32-bit builds so the struct size matches 64-bit builds.
#endif
	//! Zero the entry and stamp it with DATAMESSAGEINFOID.
	bool reset() {
		memset(this, 0, sizeof(ReplayIndexEntry));
		cid = DATAMESSAGEINFOID;
		return true;
	}
	//! @return true if the entry carries the expected object id.
	bool isValid() {
		return (cid == DATAMESSAGEINFOID);
	}
};


/*! @brief Records DataMessage streams to disk and replays them with original timing.

	A MessagePlayer instance is used in either write mode (initWrite(), then
	addMessage() repeatedly, finally flushToDisk()) or read mode (initRead(),
	then waitForNextMessage() in a loop until it returns NULL).

	Ownership: addMessage() copies the message content — the caller keeps
	ownership of the message it passes in. Messages returned by
	waitForNextMessage() are owned by the caller and must be deleted.
	@warning Not thread-safe; use one instance per thread/stream.
	@ingroup Messaging
*/
class MessagePlayer {
public:
	//! Create an uninitialised player; call initRead() or initWrite() before use.
	MessagePlayer();
	//! Destructor; releases buffers (does not delete messages already handed out).
	~MessagePlayer();

	/*! Open a recording for replay with a fixed playback interval.
	@param root directory (or file base) of the recording
	@param interval fixed inter-message interval in ms, overriding recorded timing
	@param rotate if true, restart from the beginning when the end is reached
	@return true if the recording was found and the index loaded */
	bool initRead(const char* root, uint32 interval, bool rotate = false);
	/*! Open a recording for replay using the original recorded timing.
	@param root directory (or file base) of the recording
	@param rotate if true, restart from the beginning when the end is reached
	@return true if the recording was found and the index loaded */
	bool initRead(const char* root, bool rotate = false);
	/*! Open a recording for writing.
	@param root directory (or file base) to write the recording to
	@param maxCount maximum number of messages to retain (ring buffer), 0 = unlimited
	@param maxSize maximum total size in bytes to retain, 0 = unlimited
	@return true if the storage could be created */
	bool initWrite(const char* root, uint32 maxCount, uint64 maxSize);

	//! Provide readable names for type level ids, used in metadata/CSV export. @param subtypes level id → name. @return true if stored.
	bool setSubTypeList(std::map<uint16, std::string>& subtypes);
	//! Provide readable names for context level ids, used in metadata/CSV export. @param subcontexts level id → name. @return true if stored.
	bool setSubContextList(std::map<uint16, std::string>& subcontexts);
	//! Provide readable names for component ids, used in metadata/CSV export. @param names component id → name. @return true if stored.
	bool setComponentNameList(std::map<uint32, std::string>& names);
	//! Write the metadata (name lists etc.) files alongside the recording. @return true if successful.
	bool writeMetadata();
	//! Load the metadata files of an opened recording. @return true if successful.
	bool readMetadata();
	//! Stamp system-identifying header fields onto @p msg for recording. @param msg message to update. @return true if successful.
	bool setSystemIDs(DataMessage* msg);

	/*! Export the whole recording to a CSV file.
	@param filename output file path
	@param separator separator string, defaults to comma
	@return true if successful */
	bool exportToCSVFile(const char* filename, const char* separator = NULL);
	/*! Export the whole recording as a CSV string.
	@param separator separator string, defaults to comma
	@return CSV text */
	std::string exportToCSV(const char* separator = NULL);

	/*! Append a message to the recording (write mode).
	The message content is copied; the caller keeps ownership of @p msg.
	@param name name of the trigger that produced the message
	@param msg the message to record
	@return true if successful */
	bool addMessage(const char* name, DataMessage* msg);
	/*! Wait for and return the next message in the recording (read mode),
	honouring the replay timing.
	@param ms maximum time to wait in milliseconds (blocks up to this long)
	@param triggerName receives a pointer to the recorded trigger name (owned by the player, valid until the next call)
	@return the next message (caller owns it, delete when done), or NULL if none became due within @p ms */
	DataMessage* waitForNextMessage(uint32 ms, const char* &triggerName);
	/*! As waitForNextMessage(uint32, const char*&) but also reports the delay to the following message.
	@param ms maximum time to wait in milliseconds
	@param msToNext receives the number of ms until the next message is due
	@return the next message (caller owns it), or NULL */
	DataMessage* waitForNextMessage(uint32 ms, uint32 &msToNext);
	/*! As above, returning both the trigger name and the delay to the following message.
	@param ms maximum time to wait in milliseconds
	@param triggerName receives the recorded trigger name (owned by the player)
	@param msToNext receives the number of ms until the next message is due
	@return the next message (caller owns it), or NULL */
	DataMessage* waitForNextMessage(uint32 ms, const char* &triggerName, uint32 &msToNext);
	/*! Wait for and return the next message without extra outputs.
	@param ms maximum time to wait in milliseconds
	@return the next message (caller owns it), or NULL */
	DataMessage* waitForNextMessage(uint32 ms);
	//! @return the trigger name of the most recently returned message.
	std::string getCurrentTriggerName();

	//! Force buffered messages and the index out to disk (write mode). @return true if successful.
	bool flushToDisk();
	/*! Render a human-readable dump of all recorded messages.
	@param format optional format selector (implementation-defined), NULL for default
	@return printable text */
	std::string printAllString(const char* format = NULL);

private:
	bool writeBuffer();
	bool readBuffer();

	uint32 interval;
	uint32 maxCount;
	uint64 maxSize;
	uint32 indexSize;
	bool rotate;
	std::string root;
	char* indexData;
	char currentTriggerName[MAXKEYNAMELEN + 1];
	std::string indexFile;
	uint32 indexFileSize;

	std::string metadataFile;
	std::string metadataTextFile;
	std::string csvFile;

	std::map<uint16, std::string> subtypeList;
	std::map<uint16, std::string> subcontextList;
	std::map<uint32, std::string> componentNameList;

	uint32 curStart;
	uint32 curWrite;
	uint32 curIndex;
	uint32 curCount;
	uint64 curSize;
	uint64 lastWriteTime;
	uint64 lastReceiveTime;
	uint64 lastReadTime;
	uint64 serial;
	uint64 nextMsgTime;
};

} // namespace cmlabs

#endif //_MESSAGEPLAYER_H_

