/* +------------------------------------------------------------------------+
   |                     Mobile Robot Programming Toolkit (MRPT)            |
   |                          https://www.mrpt.org/                         |
   |                                                                        |
   | Copyright (c) 2005-2023, Individual contributors, see AUTHORS file     |
   | See: https://www.mrpt.org/Authors - All rights reserved.               |
   | Released under BSD License. See: https://www.mrpt.org/License          |
   +------------------------------------------------------------------------+ */

#include "maps-precomp.h"  // Precomp header
//
#include <mrpt/maps/CBeacon.h>
#include <mrpt/maps/CBeaconMap.h>
#include <mrpt/math/geometry.h>
#include <mrpt/math/ops_matrices.h>
#include <mrpt/obs/CObservation.h>
#include <mrpt/opengl/CEllipsoid3D.h>
#include <mrpt/opengl/CPointCloud.h>
#include <mrpt/opengl/CSetOfObjects.h>
#include <mrpt/opengl/CText.h>
#include <mrpt/serialization/CArchive.h>
#include <mrpt/system/os.h>

#include <Eigen/Dense>

using namespace mrpt;
using namespace mrpt::maps;
using namespace mrpt::obs;
using namespace mrpt::math;
using namespace mrpt::system;
using namespace mrpt::poses;
using namespace std;

IMPLEMENTS_SERIALIZABLE(CBeacon, CSerializable, mrpt::maps)

uint8_t CBeacon::serializeGetVersion() const { return 0; }
void CBeacon::serializeTo(mrpt::serialization::CArchive& out) const
{
	uint32_t i = m_ID;
	uint32_t j = m_typePDF;
	out << i << j << m_locationMC << m_locationGauss << m_locationSOG;
}

void CBeacon::serializeFrom(mrpt::serialization::CArchive& in, uint8_t version)
{
	switch (version)
	{
		case 0:
		{
			uint32_t i, j;
			in >> i >> j >> m_locationMC >> m_locationGauss >> m_locationSOG;
			m_ID = i;
			m_typePDF = static_cast<TTypePDF>(j);
		}
		break;
		default: MRPT_THROW_UNKNOWN_SERIALIZATION_VERSION(version);
	};
}

/*---------------------------------------------------------------
					getMean
  ---------------------------------------------------------------*/
void CBeacon::getMean(CPoint3D& p) const
{
	MRPT_START
	switch (m_typePDF)
	{
		case pdfMonteCarlo: m_locationMC.getMean(p); break;
		case pdfGauss: m_locationGauss.getMean(p); break;
		case pdfSOG: m_locationSOG.getMean(p); break;
		default: THROW_EXCEPTION("ERROR: Invalid 'm_typePDF' value");
	};
	MRPT_END
}

std::tuple<CMatrixDouble33, CPoint3D> CBeacon::getCovarianceAndMean() const
{
	MRPT_START
	switch (m_typePDF)
	{
		case pdfMonteCarlo: return m_locationMC.getCovarianceAndMean();
		case pdfGauss: return m_locationGauss.getCovarianceAndMean();
		case pdfSOG: return m_locationSOG.getCovarianceAndMean();
		default: THROW_EXCEPTION("ERROR: Invalid 'm_typePDF' value");
	};
	MRPT_END
}

/*---------------------------------------------------------------
					bayesianFusion
  ---------------------------------------------------------------*/
void CBeacon::bayesianFusion(
	const CPointPDF& p1, const CPointPDF& p2,
	const double minMahalanobisDistToDrop)
{
	MRPT_START
	switch (m_typePDF)
	{
		case pdfMonteCarlo:
			m_locationMC.bayesianFusion(p1, p2, minMahalanobisDistToDrop);
			break;
		case pdfGauss:
			m_locationGauss.bayesianFusion(p1, p2, minMahalanobisDistToDrop);
			break;
		case pdfSOG:
			m_locationSOG.bayesianFusion(p1, p2, minMahalanobisDistToDrop);
			break;
		default: THROW_EXCEPTION("ERROR: Invalid 'm_typePDF' value");
	};
	MRPT_END
}

/*---------------------------------------------------------------
					drawSingleSample
  ---------------------------------------------------------------*/
void CBeacon::drawSingleSample(CPoint3D& outSample) const
{
	MRPT_START
	switch (m_typePDF)
	{
		case pdfMonteCarlo: m_locationMC.drawSingleSample(outSample); break;
		case pdfGauss: m_locationGauss.drawSingleSample(outSample); break;
		case pdfSOG: m_locationSOG.drawSingleSample(outSample); break;
		default: THROW_EXCEPTION("ERROR: Invalid 'm_typePDF' value");
	};
	MRPT_END
}

/*---------------------------------------------------------------
					copyFrom
  ---------------------------------------------------------------*/
void CBeacon::copyFrom(const CPointPDF& o)
{
	MRPT_START
	switch (m_typePDF)
	{
		case pdfMonteCarlo: m_locationMC.copyFrom(o); break;
		case pdfGauss: m_locationGauss.copyFrom(o); break;
		case pdfSOG: m_locationSOG.copyFrom(o); break;
		default: THROW_EXCEPTION("ERROR: Invalid 'm_typePDF' value");
	};
	MRPT_END
}

/*---------------------------------------------------------------
					saveToTextFile
  ---------------------------------------------------------------*/
bool CBeacon::saveToTextFile(const std::string& file) const
{
	MRPT_START
	switch (m_typePDF)
	{
		case pdfMonteCarlo: return m_locationMC.saveToTextFile(file); break;
		case pdfGauss: return m_locationGauss.saveToTextFile(file); break;
		case pdfSOG: return m_locationSOG.saveToTextFile(file); break;
		default: THROW_EXCEPTION("ERROR: Invalid 'm_typePDF' value");
	};
	MRPT_END
}

/*---------------------------------------------------------------
					changeCoordinatesReference
  ---------------------------------------------------------------*/
void CBeacon::changeCoordinatesReference(const CPose3D& newReferenceBase)
{
	MRPT_START
	switch (m_typePDF)
	{
		case pdfMonteCarlo:
			m_locationMC.changeCoordinatesReference(newReferenceBase);
			break;
		case pdfGauss:
			m_locationGauss.changeCoordinatesReference(newReferenceBase);
			break;
		case pdfSOG:
			m_locationSOG.changeCoordinatesReference(newReferenceBase);
			break;
		default: THROW_EXCEPTION("ERROR: Invalid 'm_typePDF' value");
	};
	MRPT_END
}

void CBeacon::getVisualizationInto(mrpt::opengl::CSetOfObjects& o) const
{
	MRPT_START

	switch (m_typePDF)
	{
		case pdfMonteCarlo:
		{
			opengl::CPointCloud::Ptr obj =
				std::make_shared<opengl::CPointCloud>();
			obj->setColor(1, 0, 0);

			obj->setPointSize(2.5);

			const size_t N = m_locationMC.m_particles.size();
			obj->resize(N);

			for (size_t i = 0; i < N; i++)
				obj->setPoint(
					i, m_locationMC.m_particles[i].d->x,
					m_locationMC.m_particles[i].d->y,
					m_locationMC.m_particles[i].d->z);

			o.insert(obj);
		}
		break;
		case pdfGauss:
		{
			opengl::CEllipsoid3D::Ptr obj =
				std::make_shared<opengl::CEllipsoid3D>();

			obj->setPose(m_locationGauss.mean);
			obj->setLineWidth(3);

			CMatrixDouble C = CMatrixDouble(m_locationGauss.cov);
			obj->setCovMatrix(C);

			obj->setQuantiles(3);
			obj->enableDrawSolid3D(false);

			obj->setColor(1, 0, 0, 0.85f);
			o.insert(obj);
		}
		break;
		case pdfSOG:
		{
			auto auxObjs = mrpt::opengl::CSetOfObjects::Create();
			m_locationSOG.getAs3DObject(auxObjs);
			o.insert(auxObjs);
		}
		break;
		default: THROW_EXCEPTION("ERROR: Invalid 'm_typePDF' value");
	};

	opengl::CText::Ptr obj2 = std::make_shared<opengl::CText>();
	obj2->setString(format("#%d", static_cast<int>(m_ID)));

	CPoint3D meanP;
	this->getMean(meanP);
	obj2->setLocation(meanP.x() + 0.10, meanP.y() + 0.10, meanP.z());
	o.insert(obj2);

	MRPT_END
}

/*---------------------------------------------------------------
					getAsMatlabDrawCommands
  ---------------------------------------------------------------*/
void CBeacon::getAsMatlabDrawCommands(std::vector<std::string>& out_Str) const
{
	MRPT_START

	out_Str.clear();
	char auxStr[1000];

	switch (m_typePDF)
	{
		case pdfMonteCarlo:
		{
			// xs=[...];
			// ys=[...];
			// plot(xs,ys,'.','MarkerSize',4);
			size_t i, N = m_locationMC.m_particles.size();
			std::string sx, sy;

			sx = "xs=[";
			sy = "ys=[";
			for (i = 0; i < N; i++)
			{
				os::sprintf(
					auxStr, sizeof(auxStr), "%.3f%c",
					m_locationMC.m_particles[i].d->x, (i == N - 1) ? ' ' : ',');
				sx = sx + std::string(auxStr);
				os::sprintf(
					auxStr, sizeof(auxStr), "%.3f%c",
					m_locationMC.m_particles[i].d->y, (i == N - 1) ? ' ' : ',');
				sy = sy + std::string(auxStr);
			}
			sx = sx + "];";
			sy = sy + "];";
			out_Str.emplace_back(sx);
			out_Str.emplace_back(sy);
			out_Str.emplace_back("plot(xs,ys,'k.','MarkerSize',4);");
		}
		break;
		case pdfGauss:
		{
			// m=[x y];
			// C=[2x2]
			// error_ellipse(C,m,'conf',0.997,'style','k');

			os::sprintf(
				auxStr, sizeof(auxStr), "m=[%.3f %.3f];",
				m_locationGauss.mean.x(), m_locationGauss.mean.y());
			out_Str.emplace_back(auxStr);
			os::sprintf(
				auxStr, sizeof(auxStr), "C=[%e %e;%e %e];",
				m_locationGauss.cov(0, 0), m_locationGauss.cov(0, 1),
				m_locationGauss.cov(1, 0), m_locationGauss.cov(1, 1));
			out_Str.emplace_back(auxStr);

			out_Str.emplace_back(
				"error_ellipse(C,m,'conf',0.997,'style','k');");
		}
		break;
		case pdfSOG:
		{
			for (const auto& it : m_locationSOG)
			{
				os::sprintf(
					auxStr, sizeof(auxStr), "m=[%.3f %.3f];", it.val.mean.x(),
					it.val.mean.y());
				out_Str.emplace_back(auxStr);
				os::sprintf(
					auxStr, sizeof(auxStr), "C=[%e %e;%e %e];",
					it.val.cov(0, 0), it.val.cov(0, 1), it.val.cov(1, 0),
					it.val.cov(1, 1));
				out_Str.emplace_back(auxStr);
				out_Str.emplace_back(
					"error_ellipse(C,m,'conf',0.997,'style','k');");
			}
		}
		break;
		default: THROW_EXCEPTION("ERROR: Invalid 'm_typePDF' value");
	};

	// The text:
	CPoint3D meanP;
	getMean(meanP);

	os::sprintf(
		auxStr, sizeof(auxStr), "text(%f,%f,'#%i');", meanP.x(), meanP.y(),
		static_cast<int>(m_ID));
	out_Str.emplace_back(auxStr);

	MRPT_END
}

/*---------------------------------------------------------------
					generateObservationModelDistribution
 Compute the observation model p(z_t|x_t) for a given observation (range value),
and return it as an approximate SOG.
*  Note that if the beacon is a SOG itself, the number of gaussian modes will be
square.
*  As a speed-up, if a "center point"+"maxDistanceFromCenter" is supplied
(maxDistanceFromCenter!=0), those modes farther than this sphere will be
discarded.
*  Parameters such as the stdSigma of the sensor are gathered from "myBeaconMap"
*  The result is one "ring" for each Gaussian mode that represent the beacon
position in this object.
*  The position of the sensor on the robot is used to shift the resulting
densities such as they represent the position of the robot, not the sensor.
*  \sa CBeaconMap::insertionOptions, generateRingSOG

  ---------------------------------------------------------------*/
void CBeacon::generateObservationModelDistribution(
	float sensedRange, CPointPDFSOG& outPDF, const CBeaconMap* myBeaconMap,
	const CPoint3D& sensorPntOnRobot, const CPoint3D& centerPoint,
	float maxDistanceFromCenter) const
{
	MRPT_START

	std::unique_ptr<CPointPDFSOG> beaconPos;
	const CPointPDFSOG* beaconPosToUse = nullptr;

	if (m_typePDF == pdfGauss)
	{
		// Copy the gaussian to the SOG:
		auto new_beaconPos = std::make_unique<CPointPDFSOG>(1);
		new_beaconPos->push_back(CPointPDFSOG::TGaussianMode());
		new_beaconPos->get(0).log_w = 0;
		new_beaconPos->get(0).val = m_locationGauss;
		beaconPos = std::move(new_beaconPos);
		beaconPosToUse = beaconPos.get();
	}
	else
	{
		ASSERT_(m_typePDF == pdfSOG);
		beaconPosToUse = static_cast<const CPointPDFSOG*>(&m_locationSOG);
	}

	outPDF.clear();

	for (const auto& beaconPo : *beaconPosToUse)
	{
		// The center of the ring to be generated
		CPoint3D ringCenter(
			beaconPo.val.mean.x() - sensorPntOnRobot.x(),
			beaconPo.val.mean.y() - sensorPntOnRobot.y(),
			beaconPo.val.mean.z() - sensorPntOnRobot.z());

		size_t startIdx = outPDF.size();

		CBeacon::generateRingSOG(
			sensedRange,  // Sensed range
			outPDF,	 // The ouput (Append all !!)
			myBeaconMap,  // For params
			ringCenter,	 // The center of the ring to be generated
			&beaconPo.val
				 .cov,	// The covariance to ADD to each mode, due to the
			// composition of uncertainty
			false,	// clearPreviousContentsOutPDF
			centerPoint,
			maxDistanceFromCenter  // Directly, do not create too far modes
		);

		// Adjust the weights to the one of "this" mode:
		for (size_t k = startIdx; k < outPDF.size(); k++)
			outPDF.get(k).log_w = beaconPo.log_w;
	}

	MRPT_END
}

/*---------------------------------------------------------------
					generateRingSOG
  ---------------------------------------------------------------*/
void CBeacon::generateRingSOG(
	float R, CPointPDFSOG& outPDF, const CBeaconMap* myBeaconMap,
	const CPoint3D& sensorPnt,
	const CMatrixDouble33* covarianceCompositionToAdd,
	bool clearPreviousContentsOutPDF, const CPoint3D& centerPoint,
	float maxDistanceFromCenter)
{
	MRPT_START

	ASSERT_(myBeaconMap);

	// Compute the number of Gaussians:
	const double minEl =
		DEG2RAD(myBeaconMap->insertionOptions.minElevation_deg);
	const double maxEl =
		DEG2RAD(myBeaconMap->insertionOptions.maxElevation_deg);
	ASSERT_(
		myBeaconMap->insertionOptions.minElevation_deg <=
		myBeaconMap->insertionOptions.maxElevation_deg);

	double el, th, A_ang;
	const float maxDistBetweenGaussians =
		myBeaconMap->insertionOptions.SOG_maxDistBetweenGaussians;	// Meters

	// B: Number of gaussians in each cut to the sphere (XY,XZ,...)
	size_t B = (size_t)(M_2PIf * R / maxDistBetweenGaussians) + 1;

	// Assure minimum B (maximum angular increment):
	B = max(B, (size_t)30);

	// B must be even:
	if (B % 2) B++;

	A_ang = M_2PI / B;	// Angular increments between modes:

	// The diagonal basic covariance matrix.
	//  (0,0) is the variance in the direction "in->out" of the sphere
	//  (1,1),(2,2) is the var. in the two directions tangent to the sphere
	CMatrixDouble33 S;
	S(0, 0) = square(myBeaconMap->likelihoodOptions.rangeStd);
	S(1, 1) = S(2, 2) = square(
		A_ang * R /
		myBeaconMap->insertionOptions
			.SOG_separationConstant);  // 4.0f * sensedRange * S(0,0);

	CPoint3D dir;

	// Create the SOG:
	size_t modeIdx;
	if (clearPreviousContentsOutPDF)
	{
		// Overwrite modes:
		modeIdx = 0;
		outPDF.resize(B * B);
	}
	else
	{
		// Append modes:
		modeIdx = outPDF.size();  // Start here
		outPDF.resize(outPDF.size() + B * B);
	}

	size_t idxEl, idxTh;  // idxEl:[0,B/2+1]

	for (idxEl = 0; idxEl <= (1 + B / 2); idxEl++)
	{
		el = minEl + idxEl * A_ang;
		if (el > (maxEl + 0.5 * A_ang)) continue;

		size_t nThSteps = B;
		// Except if we are in the top/bottom of the sphere:
		if (fabs(cos(el)) < 1e-4) { nThSteps = 1; }

		for (idxTh = 0; idxTh < nThSteps; idxTh++)
		{
			th = idxTh * A_ang;

			// Compute the mean of the new Gaussian:
			dir.x((sensorPnt.x() + R * cos(th) * cos(el)));
			dir.y((sensorPnt.y() + R * sin(th) * cos(el)));
			dir.z((sensorPnt.z() + R * sin(el)));

			// If we are provided a radius for not creating modes out of it,
			// check it:
			bool reallyCreateIt = true;
			if (maxDistanceFromCenter > 0)
				reallyCreateIt =
					dir.distanceTo(centerPoint) < maxDistanceFromCenter;

			if (reallyCreateIt)
			{
				// All have equal log-weights:
				outPDF.get(modeIdx).log_w = 0;

				// The mean:
				outPDF.get(modeIdx).val.mean = dir;

				// Compute the covariance:
				dir = dir - sensorPnt;
				// 3 perpendicular & normalized vectors.
				CMatrixDouble33 H = math::generateAxisBaseFromDirection(
					dir.x(), dir.y(), dir.z());

				// out = H * S * H';
				outPDF.get(modeIdx).val.cov = mrpt::math::multiply_HCHt(H, S);

				if (std::abs(minEl - maxEl) < 1e-6)
				{  // We are in 2D:
					// 3rd column/row = 0
					CMatrixDouble33& C33 = outPDF.get(modeIdx).val.cov;
					C33(0, 2) = C33(2, 0) = C33(1, 2) = C33(2, 1) = C33(2, 2) =
						0;
				}

				// Add covariance for uncertainty composition?
				if (covarianceCompositionToAdd)
					outPDF.get(modeIdx).val.cov += *covarianceCompositionToAdd;

				// One more mode is used:
				modeIdx++;

			}  // end if reallyCreateIt

		}  // end for idxTh
	}  // end for idxEl

	// resize to the number of really used modes:
	outPDF.resize(modeIdx);

	MRPT_END
}
