

//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
//_/_/
//_/_/  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 Observations.h
    @brief Typed "observation" records extracted from DataMessages: location, dimension, integer, float, time and string observations.

    An observation is a small POD payload (Obs* struct) plus an ObsHeader with
    identity, provenance and confidence, wrapped by GenericObservation<T>.
    Field-name mappings let heterogeneous sensor messages populate the same
    observation type: the Extract*FromMessage() helpers first look the logical
    field name up in the mapping table, then read the mapped field from the
    message. Range queries are supported via extractRangeFrom()/extractRangeTo()
    and isBetween(). Template bodies live in Observations.tpl.h. */
#if !defined(_OBSERVATIONS_H_)
#define _OBSERVATIONS_H_

#include "DataMessage.h"

namespace cmlabs {

/*! @brief Declarative description of one observation source: name, observation type, trigger expression and field-name mappings. */
struct ObsEntry {
	std::string name;
	std::string type;
	std::string trigger;
	std::map<std::string, std::string> mappings;
};

/*! @brief Comparator matching ObsEntry records by name only (used for de-duplication in containers). */
struct cmpObsEntry {
	bool operator()(const ObsEntry& a, const ObsEntry& b) const {
		return (a.name == b.name);
	}
};

/*! @brief Read an integer field from a message, translating @p name through @p mappings first.
    @param name Logical field name. @param msg Source message. @param mappings Logical→actual field-name map.
    @param[out] ok Set false when the field is missing/not convertible. @return Field value (0 when !ok). */
int64 ExtractIntFromMessage(const char* name, DataMessage* msg, std::map<std::string, std::string>& mappings, bool &ok);
/*! @brief Float variant of ExtractIntFromMessage(); same mapping semantics. */
float64 ExtractFloatFromMessage(const char* name, DataMessage* msg, std::map<std::string, std::string>& mappings, bool &ok);
/*! @brief Timestamp variant of ExtractIntFromMessage(); same mapping semantics. */
uint64 ExtractTimeFromMessage(const char* name, DataMessage* msg, std::map<std::string, std::string>& mappings, bool &ok);
/*! @brief String variant of ExtractIntFromMessage(); same mapping semantics. */
std::string ExtractStringFromMessage(const char* name, DataMessage* msg, std::map<std::string, std::string>& mappings, bool &ok);

/*! @brief Fixed-size header preceding every observation payload: sizes/ids, provenance (creator, object, topic), type tag, name and confidence/consensus scores. */
struct ObsHeader {
	uint32 cid;
	uint32 size;
	uint64 timestamp;
	uint64 id;
	uint64 creatorID;
	uint64 objectID;
	uint64 topicID;
	uint32 type;
	char name[MAXVALUENAMELEN + 1];
	float64 confidence;
	float64 consensus;
};

#define OBSID_LOC 1
/*! @brief 6-DOF pose observation: position (x,y,z), orientation (pitch,roll,yaw), size and owning map id.
    print()/toJSON() render it; extractRangeFrom()/extractRangeTo()/isBetween() implement 2D rectangular range queries;
    extractDataFromMessage() populates it from a DataMessage via field mappings. */
struct ObsLocation {
	float64 x, y, z, pitch, roll, yaw, size;
	uint64 mapID;
	static uint32 getType() { return OBSID_LOC; }
	static std::string getTypeName() { return "Location";  }
	void init() { memset(this, 0, sizeof(*this)); }
	std::string print() {
		std::string loc;
		if (!z)
			loc = utils::StringFormat("(%.3f,%.3f)", x, y);
		else
			loc = utils::StringFormat("(%.3f,%.3f,%.3f)", x, y, z);
		if (yaw)
			loc = utils::StringFormat("%s {%.3f}", loc.c_str(), yaw);
		if (size)
			loc = utils::StringFormat("%s [%.3f]", loc.c_str(), size);
		return loc;
	}
	std::string toJSON() { return utils::StringFormat(" \"x\": %.3f, \"y\": %.3f, \"z\": %.3f, \"pitch\": %.3f, \"roll\": %.3f, \"yaw\": %.3f, \"size\": %.2f", x, y, z, pitch, roll, yaw, size); }
	bool extractRangeFrom(DataMessage* msg) {
		bool ok;
		x = msg->getFloat("FromX", ok); if (!ok) return false;
		y = msg->getFloat("FromY", ok);
		return ok;
	}
	bool extractRangeTo(DataMessage* msg) {
		bool ok;
		x = msg->getFloat("ToX", ok);
		y = msg->getFloat("ToY", ok);
		return ok;
	}
	bool isBetween(ObsLocation& a, ObsLocation& b) {
		if (a.x <= b.x) {
			if ((x < a.x) || (x > b.x))
				return false;
			if (a.y <= b.y) {
				if ((y < a.y) || (y > b.y))
					return false;
			}
			else {
				if ((y < b.y) || (y > a.y))
					return false;
			}
		}
		else {
			if ((x < b.x) || (x > a.x))
				return false;
			if (a.y <= b.y) {
				if ((y < a.y) || (y > b.y))
					return false;
			}
			else {
				if ((y < b.y) || (y > a.y))
					return false;
			}
		}
		return true;
	}
	bool extractDataFromMessage(DataMessage* msg, std::map<std::string, std::string>& mappings) {
		bool ok;
		x = ExtractFloatFromMessage("x", msg, mappings, ok);
		if (!ok) return false;
		y = ExtractFloatFromMessage("y", msg, mappings, ok);
		if (!ok) return false;
		z = ExtractFloatFromMessage("z", msg, mappings, ok);
		pitch = ExtractFloatFromMessage("pitch", msg, mappings, ok);
		roll = ExtractFloatFromMessage("roll", msg, mappings, ok);
		yaw = ExtractFloatFromMessage("yaw", msg, mappings, ok);
		size = ExtractFloatFromMessage("size", msg, mappings, ok);
		mapID = (uint64)ExtractIntFromMessage("mapid", msg, mappings, ok);
		return true;
	}
};


#define OBSID_DIM 2
/*! @brief Extent observation (w,h,d). isBetween() compares by area/volume rather than per-component. */
struct ObsDimension {
	float64 w, h, d;
	uint32 getType() { return OBSID_DIM; }
	static std::string getTypeName() { return "Dimension"; }
	void init() { memset(this, 0, sizeof(*this)); }
	std::string print() { return utils::StringFormat("(%.3f,%.3f,%.3f)", w, h, d); }
	std::string toJSON() { return utils::StringFormat(" \"w\": %.3f, \"h\": %.3f, \"d\": %.3f ", w, h, d); }
	bool extractRangeFrom(DataMessage* msg) {
		bool ok;
		w = msg->getFloat("FromW", ok); if (!ok) return false;
		h = msg->getFloat("FromH", ok); if (!ok) return false;
		d = msg->getFloat("FromD");
		return true;
	}
	bool extractRangeTo(DataMessage* msg) {
		bool ok;
		w = msg->getFloat("ToW", ok); if (!ok) return false;
		h = msg->getFloat("ToH", ok); if (!ok) return false;
		d = msg->getFloat("ToD");
		return ok;
	}
	bool isBetween(ObsDimension& a, ObsDimension& b) {
		float64 vol, volA, volB;
		if (this->d) {
			// consider depth
			vol = w * h * d;
			volA = a.w * a.h * a.d;
			volB = b.w * b.h * b.d;
		}
		else {
			// ignore depth
			vol = w * h;
			volA = a.w * a.h;
			volB = b.w * b.h;
		}
		if (volA <= volB) {
			if ((vol < volA) || (vol > volB))
				return false;
		}
		else {
			if ((vol < volB) || (vol > volA))
				return false;
		}
		return true;
	}
	bool extractDataFromMessage(DataMessage* msg, std::map<std::string, std::string>& mappings) {
		bool ok;
		w = ExtractFloatFromMessage("w", msg, mappings, ok);
		if (!ok) return false;
		h = ExtractFloatFromMessage("h", msg, mappings, ok);
		if (!ok) return false;
		d = ExtractFloatFromMessage("d", msg, mappings, ok);
		return true;
	}
};


#define OBSID_INT 3
/*! @brief Scalar integer observation. @note isBetween() returns true when the value is OUTSIDE [a,b] — inverted relative to ObsLocation/ObsDimension (see code smell notes). */
struct ObsInteger {
	int64 val;
	uint32 getType() { return OBSID_INT; }
	static std::string getTypeName() { return "Integer"; }
	void init() { memset(this, 0, sizeof(*this)); }
	std::string print() { return utils::StringFormat("%lld", val); }
	std::string toJSON() { return utils::StringFormat(" \"value\": %lld ", val); }
	bool extractRangeFrom(DataMessage* msg) {
		bool ok;
		val = msg->getInt("FromVal", ok);
		return ok;
	}
	bool extractRangeTo(DataMessage* msg) {
		bool ok;
		val = msg->getInt("ToVal", ok);
		return ok;
	}
	bool isBetween(ObsInteger& a, ObsInteger& b) {
		if (a.val <= b.val)
			return ((val < a.val) || (val > b.val));
		else
			return ((val < b.val) || (val > a.val));
	}
	bool extractDataFromMessage(DataMessage* msg, std::map<std::string, std::string>& mappings) {
		bool ok;
		val = ExtractIntFromMessage("val", msg, mappings, ok);
		return ok;
	}
};

#define OBSID_FLOAT 4
/*! @brief Scalar float observation. @note isBetween() has the same inverted semantics as ObsInteger. */
struct ObsFloat {
	float64 val;
	uint32 getType() { return OBSID_FLOAT; }
	static std::string getTypeName() { return "Float"; }
	void init() { memset(this, 0, sizeof(*this)); }
	std::string print() { return utils::StringFormat("%.3f", val); }
	std::string toJSON() { return utils::StringFormat(" \"value\": %.3f ", val); }
	bool extractRangeFrom(DataMessage* msg) {
		bool ok;
		val = msg->getFloat("FromVal", ok);
		return ok;
	}
	bool extractRangeTo(DataMessage* msg) {
		bool ok;
		val = msg->getFloat("ToVal", ok);
		return ok;
	}
	bool isBetween(ObsFloat& a, ObsFloat& b) {
		if (a.val <= b.val)
			return ((val < a.val) || (val > b.val));
		else
			return ((val < b.val) || (val > a.val));
	}
	bool extractDataFromMessage(DataMessage* msg, std::map<std::string, std::string>& mappings) {
		bool ok;
		val = ExtractFloatFromMessage("val", msg, mappings, ok);
		return ok;
	}
};

#define OBSID_TIME 5
/*! @brief Timestamp observation (ms epoch). @note isBetween() has the same inverted semantics as ObsInteger. */
struct ObsTime {
	uint64 val;
	uint32 getType() { return OBSID_TIME; }
	static std::string getTypeName() { return "Time"; }
	void init() { memset(this, 0, sizeof(*this)); }
	std::string print() { return utils::StringFormat("%s", PrintTime(val)); }
	std::string toJSON() { return utils::StringFormat(" \"time\": %llu, \"timetext\": \"%s\" ", val, PrintTimeString(val).c_str()); }
	bool extractRangeFrom(DataMessage* msg) {
		bool ok;
		val = msg->getTime("FromVal", ok);
		return ok;
	}
	bool extractRangeTo(DataMessage* msg) {
		bool ok;
		val = msg->getTime("ToVal", ok);
		return ok;
	}
	bool isBetween(ObsTime& a, ObsTime& b) {
		if (a.val <= b.val)
			return ((val < a.val) || (val > b.val));
		else
			return ((val < b.val) || (val > a.val));
	}
	bool extractDataFromMessage(DataMessage* msg, std::map<std::string, std::string>& mappings) {
		bool ok;
		val = ExtractTimeFromMessage("val", msg, mappings, ok);
		return ok;
	}
};

#define OBSID_STRING 6
/*! @brief Fixed-buffer (1023 chars + NUL) string observation; comparisons are case-insensitive lexicographic. */
struct ObsString {
	char val[1024];
	uint32 getType() { return OBSID_STRING; }
	static std::string getTypeName() { return "String"; }
	void init() { memset(this, 0, sizeof(*this)); }
	std::string print() { return utils::StringFormat("%s", val); }
	std::string toJSON() { return utils::StringFormat(" \"value\": \"%s\" ", val); }
	bool extractRangeFrom(DataMessage* msg) {
		bool ok;
		const char* str = msg->getString("FromVal", ok);
		if (!ok) return false;
		utils::strcpyavail(val, str, 1023, true);
		return true;
	}
	bool extractRangeTo(DataMessage* msg) {
		bool ok;
		const char* str = msg->getString("ToVal", ok);
		if (!ok) return false;
		utils::strcpyavail(val, str, 1023, true);
		return true;
	}

	bool isBetween(ObsString& a, ObsString& b) {
		int64 comp = stricmp(a.val, b.val);
		if (comp <= 0) {
			if ((stricmp(val, a.val) < 0) || (stricmp(val, b.val) > 0))
				return false;
		}
		else {
			if ((stricmp(val, b.val) < 0) || (stricmp(val, a.val) > 0))
				return false;
		}
		return true;
	}
	bool extractDataFromMessage(DataMessage* msg, std::map<std::string, std::string>& mappings) {
		bool ok;
		std::string str = ExtractStringFromMessage("val", msg, mappings, ok);
		if (!ok) return false;
		utils::strcpyavail(val, str.c_str(), 1023, true);
		return true;
	}
};

/*! @brief Number of defined observation type ids (OBSID_LOC..OBSID_STRING). */
#define OBSID_COUNT 6

/*! @brief Inspect a raw observation blob and return its OBSID_* type tag from the embedded ObsHeader. @param data Raw blob. @param size Blob size. @return Type id or 0 when invalid. */
uint32 GetObsTypeFromRawData(const char* data, uint32 size);

/*! @brief Header + typed payload wrapper for one observation, serializable to/from DataMessage and raw memory.
    @tparam T One of the Obs* payload structs (ObsLocation, ObsDimension, ObsInteger, ObsFloat, ObsTime, ObsString).
    Implementations are in Observations.tpl.h. */
template <typename T>
class GenericObservation {
public:
	/*! @brief Deserialize every observation of type T carried by @p msg. @return Heap-allocated observations (caller owns). */
	static std::list<GenericObservation<T>*> ReadAllFromMessage(DataMessage* msg);
	/*! @brief Serialize all observations in @p list into @p msg. @return true on success. */
	static bool WriteAllToMessage(DataMessage* msg, std::list<GenericObservation<T>*>& list);

	GenericObservation(); //!< Empty observation (zeroed header and payload).
	GenericObservation(const char* data, uint32 size, uint64 id = 0); //!< Deserialize from a raw blob; @p id 0 means "first/any".
	~GenericObservation();

	ObsHeader header; //!< Identity/provenance/confidence header.
	T data;           //!< Typed payload.

	std::string print(); //!< One-line human-readable rendering (header + payload).
	std::string toJSON(); //!< JSON object rendering.

	/*! @brief Populate the payload from a message using logical→actual field mappings. @return true when all required fields were present. */
	bool extractDataFromMessage(DataMessage* msg, std::map<std::string, std::string>& mappings);

	bool writeToMessage(DataMessage* msg, uint64 id); //!< Append this observation to @p msg under entry id @p id. @return true on success.
	bool readFromMessage(DataMessage* msg, uint64 id = 0); //!< Read the observation with id @p id (0 = first) from @p msg. @return true on success.
	bool readFromData(const char* data, uint32 size, uint64 id = 0); //!< Parse from a raw blob. @return true on success.

	//bool buildFromQuery(uint8 queryType, std::string& query);
	bool isBetween(GenericObservation<T>* a, GenericObservation<T>* b); //!< Range test delegated to the payload's isBetween(); see per-type notes on semantics.
};

/*! @brief Unit test for the Observations module. @return true when all checks pass. */
bool Observations_UnitTest();
/*! @brief Register the Observations tests with the unit test framework. */
void Register_Observations_Tests();


} // namespace cmlabs

#include "Observations.tpl.h"

#endif //_OBSERVATIONS_H_

