/* +------------------------------------------------------------------------+
   |                     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          |
   +------------------------------------------------------------------------+ */
#pragma once

#include <mrpt/config/CLoadableOptions.h>
#include <mrpt/slam/CMetricMapsAlignmentAlgorithm.h>
#include <mrpt/typemeta/TEnumType.h>

namespace mrpt::slam
{
/** The ICP algorithm selection, used in mrpt::slam::CICP::options  \ingroup
 * mrpt_slam_grp  */
enum TICPAlgorithm
{
	icpClassic = 0,
	icpLevenbergMarquardt
};

/** ICP covariance estimation methods, used in mrpt::slam::CICP::options
 * \ingroup mrpt_slam_grp  */
enum TICPCovarianceMethod
{
	/** Use the covariance of the optimal registration, disregarding uncertainty
	   in data association */
	icpCovLinealMSE = 0,
	/** Covariance of a least-squares optimizer (includes data association
	   uncertainty) */
	icpCovFiniteDifferences
};

/** Several implementations of ICP (Iterative closest point) algorithms for
 * aligning two point maps or a point map wrt a grid map.
 *
 *  CICP::AlignPDF() or CICP::Align() are the two main entry points of the
 * algorithm.
 *
 *  To choose among existing ICP algorithms or customizing their parameters, see
 * CICP::TConfigParams and the member \a options.
 *
 *  There exists an extension of the original ICP algorithm that provides
 * multihypotheses-support for the correspondences, and which generates a
 * Sum-of-Gaussians (SOG)
 *    PDF as output. See mrpt::tfest::se2_l2_robust()
 *
 * For further details on the implemented methods, check the web:
 *   https://www.mrpt.org/Iterative_Closest_Point_(ICP)_and_other_matching_algorithms
 *
 *  For a paper explaining some of the basic equations, see for example:
 *   J. Martinez, J. Gonzalez, J. Morales, A. Mandow, A. Garcia-Cerezo,
 *   "Mobile robot motion estimation by 2D scan matching with genetic and
 * iterative closest point algorithms",
 *    Journal of Field Robotics, vol. 23, no. 1, pp. 21-34, 2006. (
 * http://babel.isa.uma.es/~jlblanco/papers/martinez2006gil.pdf )
 *
 * \sa CMetricMapsAlignmentAlgorithm
 * \ingroup mrpt_slam_grp
 */
class CICP : public mrpt::slam::CMetricMapsAlignmentAlgorithm
{
   public:
	/** The ICP algorithm configuration data
	 */
	class TConfigParams : public mrpt::config::CLoadableOptions
	{
	   public:
		void loadFromConfigFile(
			const mrpt::config::CConfigFileBase& source,
			const std::string& section) override;  // See base docs
		void saveToConfigFile(
			mrpt::config::CConfigFileBase& c,
			const std::string& s) const override;

		/** @name Algorithms selection
			@{ */
		/** The algorithm to use (default: icpClassic). See
		 * https://www.mrpt.org/tutorials/programming/scan-matching-and-icp/ for
		 * details */
		TICPAlgorithm ICP_algorithm{icpClassic};
		/** The method to use for covariance estimation (Default:
		 * icpCovFiniteDifferences) */
		TICPCovarianceMethod ICP_covariance_method{icpCovFiniteDifferences};
		/** @} */

		/** @name Correspondence-finding criteria
			@{ */
		bool onlyUniqueRobust{false};
		//! if this option is enabled only the closest
		//! correspondence for each reference point will
		//! be kept (default=false).
		/** @} */

		/** @name Termination criteria
			@{ */
		/** Maximum number of iterations to run. */
		unsigned int maxIterations{40};
		/** If the correction in all translation coordinates (X,Y,Z) is below
		 * this threshold (in meters), iterations are terminated (Default:1e-6)
		 */
		double minAbsStep_trans{1e-6};
		/** If the correction in all rotation coordinates (yaw,pitch,roll) is
		 * below this threshold (in radians), iterations are terminated
		 * (Default:1e-6) */
		double minAbsStep_rot{1e-6};
		/** @} */

		/** Initial threshold distance for two points to become a
		 * correspondence. */
		double thresholdDist{0.75}, thresholdAng{0.15 * M_PI / 180.0};
		/** The scale factor for thresholds every time convergence is achieved.
		 */
		double ALFA{0.5};
		/** The size for threshold such that iterations will stop, since it is
		 * considered precise enough. */
		double smallestThresholdDist{0.1};

		/** This is the normalization constant \f$ \sigma^2_p \f$ that is used
		 * to scale the whole 3x3 covariance.
		 *  This has a default value of \f$ (0.02)^2 \f$, that is, a 2cm sigma.
		 *  See paper: J.L. Blanco, J. Gonzalez-Jimenez, J.A.
		 * Fernandez-Madrigal, "A Robust, Multi-Hypothesis Approach to Matching
		 * Occupancy Grid Maps", Robotica, vol. 31, no. 5, pp. 687-701, 2013.
		 */
		double covariance_varPoints{0.02 * 0.02};

		/** Perform a RANSAC step, mrpt::tfest::se2_l2_robust(), after the ICP
		 * convergence, to obtain a better estimation of the pose PDF. */
		bool doRANSAC{false};

		/** @name RANSAC-step options for mrpt::tfest::se2_l2_robust() if \a
		 * doRANSAC=true
		 * @{ */
		unsigned int ransac_minSetSize{3}, ransac_maxSetSize{20},
			ransac_nSimulations{100};
		double ransac_mahalanobisDistanceThreshold{3.0};
		/** RANSAC-step option: The standard deviation in X,Y of
		 * landmarks/points which are being matched (used to compute covariances
		 * in the SoG) */
		double normalizationStd{0.02};
		bool ransac_fuseByCorrsMatch{true};
		double ransac_fuseMaxDiffXY{0.01},
			ransac_fuseMaxDiffPhi{0.1 * M_PI / 180.0};
		/** @} */

		/** Cauchy kernel rho, for estimating the optimal transformation
		 * covariance, in meters (default = 0.07m) */
		double kernel_rho{0.07};
		/** Whether to use kernel_rho to smooth distances, or use distances
		 * directly (default=true) */
		bool use_kernel{true};
		/** [LM method only] The size of the perturbance in x & y used to
		 * estimate the Jacobians of the square error (default=0.05) */
		double Axy_aprox_derivatives{0.05};
		/** [LM method only] The initial value of the lambda parameter in the LM
		 * method (default=1e-4) */
		double LM_initial_lambda{1e-4};

		/** Skip the computation of the covariance (saves some time)
		 * (default=false) */
		bool skip_cov_calculation{false};
		/** Skip the (sometimes) expensive evaluation of the term 'quality' at
		 * ICP output (Default=true) */
		bool skip_quality_calculation{true};

		/** Decimation of the point cloud being registered against the reference
		 * one (default=5) - set to 1 to have the older (MRPT <0.9.5) behavior
		 *  of not approximating ICP by ignoring the correspondence of some
		 * points. The speed-up comes from a decimation of the number of KD-tree
		 * queries,
		 *  the most expensive step in ICP */
		uint32_t corresponding_points_decimation{5};
	};

	/** The options employed by the ICP align. */
	TConfigParams options;

	/** Constructor with the default options */
	CICP() : options() {}
	/** Constructor that directly set the ICP params from a given struct \sa
	 * options */
	CICP(const TConfigParams& icpParams) : options(icpParams) {}
	/** Destructor */
	~CICP() override = default;
	/** The ICP algorithm return information*/
	struct TReturnInfo : public TMetricMapAlignmentResult
	{
		TReturnInfo() = default;
		virtual ~TReturnInfo() override = default;

		/** The number of executed iterations until convergence */
		unsigned int nIterations = 0;

		/** A goodness measure for the alignment, it is a [0,1] range indicator
		 * of percentage of correspondences. */
		double goodness = 0;

		/** A measure of the 'quality' of the local minimum of the sqr. error
		 * found by the method. Higher values are better. Low values will be
		 * found in ill-conditioned situations (e.g. a corridor) */
		double quality = 0;
	};

	/** See base method docs.
	 * This class offers an implementation for the case of "m1" being a point
	 * map and "m2" either an occupancy grid or a point map.
	 *
	 *  \note This method can be configurated with "CICP::options"
	 *  \note The output PDF is a CPosePDFGaussian if "doRANSAC=false", or a
	 * CPosePDFSOG otherwise.
	 *
	 * \sa CMetricMapsAlignmentAlgorithm, CICP::options, CICP::TReturnInfo
	 */
	mrpt::poses::CPosePDF::Ptr AlignPDF(
		const mrpt::maps::CMetricMap* m1, const mrpt::maps::CMetricMap* m2,
		const mrpt::poses::CPosePDFGaussian& initialEstimationPDF,
		mrpt::optional_ref<TMetricMapAlignmentResult> outInfo =
			std::nullopt) override;

	// See base class for docs
	mrpt::poses::CPose3DPDF::Ptr Align3DPDF(
		const mrpt::maps::CMetricMap* m1, const mrpt::maps::CMetricMap* m2,
		const mrpt::poses::CPose3DPDFGaussian& initialEstimationPDF,
		mrpt::optional_ref<TMetricMapAlignmentResult> outInfo =
			std::nullopt) override;

   protected:
	/** Computes:
	 *  \f[ K(x^2) = \frac{x^2}{x^2+\rho^2}  \f]
	 *  or just return the input if options.useKernel = false.
	 */
	float kernel(float x2, float rho2);

	mrpt::poses::CPosePDF::Ptr ICP_Method_Classic(
		const mrpt::maps::CMetricMap* m1, const mrpt::maps::CMetricMap* m2,
		const mrpt::poses::CPosePDFGaussian& initialEstimationPDF,
		TReturnInfo& outInfo);
	mrpt::poses::CPosePDF::Ptr ICP_Method_LM(
		const mrpt::maps::CMetricMap* m1, const mrpt::maps::CMetricMap* m2,
		const mrpt::poses::CPosePDFGaussian& initialEstimationPDF,
		TReturnInfo& outInfo);
	mrpt::poses::CPose3DPDF::Ptr ICP3D_Method_Classic(
		const mrpt::maps::CMetricMap* m1, const mrpt::maps::CMetricMap* m2,
		const mrpt::poses::CPose3DPDFGaussian& initialEstimationPDF,
		TReturnInfo& outInfo);
};
}  // namespace mrpt::slam
MRPT_ENUM_TYPE_BEGIN(mrpt::slam::TICPAlgorithm)
using namespace mrpt::slam;
MRPT_FILL_ENUM(icpClassic);
MRPT_FILL_ENUM(icpLevenbergMarquardt);
MRPT_ENUM_TYPE_END()

MRPT_ENUM_TYPE_BEGIN(mrpt::slam::TICPCovarianceMethod)
using namespace mrpt::slam;
MRPT_FILL_ENUM(icpCovLinealMSE);
MRPT_FILL_ENUM(icpCovFiniteDifferences);
MRPT_ENUM_TYPE_END()
