//##########################################################################
//#                                                                        #
//#            CLOUDCOMPARE PLUGIN: ColorimetricSegmenter                  #
//#                                                                        #
//#  This program is free software; you can redistribute it and/or modify  #
//#  it under the terms of the GNU General Public License as published by  #
//#  the Free Software Foundation; version 2 of the License.               #
//#                                                                        #
//#  This program is distributed in the hope that it will be useful,       #
//#  but WITHOUT ANY WARRANTY; without even the implied warranty of        #
//#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         #
//#  GNU General Public License for more details.                          #
//#                                                                        #
//#    COPYRIGHT:	Tri-Thien TRUONG, Ronan COLLIER, Mathieu LETRONE       #
//#                                                                        #
//##########################################################################

//Local
#include "qColorimetricSegmenter.h"
#include "HSV.h"
#include "RgbDialog.h"
#include "HSVDialog.h"
#include "ScalarDialog.h"
#include "QuantiDialog.h"
#include "KmeansDlg.h"

//CloudCompare
#include <ccLog.h>
#include <ccPointCloud.h>

//CCCoreLib
#include <DistanceComputationTools.h>

//System
#include <algorithm>
#include <map>

//Qt
#include <QMainWindow>

static void ShowDurationNow(const std::chrono::high_resolution_clock::time_point& startTime)
{
	auto stopTime = std::chrono::high_resolution_clock::now();
	auto duration_ms = std::chrono::duration_cast<std::chrono::milliseconds>(stopTime - startTime).count();

	//Print duration of execution
	ccLog::Print("Time to execute: " + QString::number(duration_ms) + " milliseconds");
}

static inline bool Inside(ColorCompType lower, ColorCompType value, ColorCompType upper)
{
	Q_ASSERT(lower <= upper);
	return (value >= lower && value <= upper);
}

ColorimetricSegmenter::ColorimetricSegmenter(QObject* parent)
	: QObject(parent)
	, ccStdPluginInterface(":/CC/plugin/ColorimetricSegmenter/info.json")
	, m_action_filterRgb(nullptr)
	/*, m_action_filterRgbWithSegmentation(nullptr)*/
	, m_action_filterHSV(nullptr)
	, m_action_filterScalar(nullptr)
	, m_action_histogramClustering(nullptr)
	, m_action_kMeansClustering(nullptr)
	, m_addPointError(false)
{
}

void ColorimetricSegmenter::onNewSelection(const ccHObject::Container& selectedEntities)
{
	// Only enable our action if something is selected.
	bool activateColorFilters = false;
	bool activateScalarFilter = false;
	for (ccHObject* entity : selectedEntities)
	{
		if (entity->isKindOf(CC_TYPES::POINT_CLOUD))
		{
			if (entity->hasColors())
			{
				activateColorFilters = true;
			}
			else if (entity->hasDisplayedScalarField())
			{
				activateScalarFilter = true;
			}
		}
	}

	//Activate only if only one of them is activated
	if (activateColorFilters && activateScalarFilter)
	{
		activateColorFilters = activateScalarFilter = false;
	}

	if (m_action_filterRgb)
		m_action_filterRgb->setEnabled(activateColorFilters);
	if (m_action_filterHSV)
		m_action_filterHSV->setEnabled(activateColorFilters);
	//if (m_action_filterRgbWithSegmentation)
	//	m_action_filterRgbWithSegmentation->setEnabled(activateColorFilters);
	if (m_action_filterScalar)
		m_action_filterScalar->setEnabled(activateScalarFilter);
	if (m_action_histogramClustering)
		m_action_histogramClustering->setEnabled(activateColorFilters);
	if (m_action_kMeansClustering)
		m_action_kMeansClustering->setEnabled(activateColorFilters);
}

QList<QAction*> ColorimetricSegmenter::getActions()
{
	// RGB Filter
	if (!m_action_filterRgb)
	{
		m_action_filterRgb = new QAction("Filter RGB", this);
		m_action_filterRgb->setToolTip("Filter the points of the selected cloud by RGB color");
        m_action_filterRgb->setIcon(QIcon(":/CC/plugin/ColorimetricSegmenter/images/icon_rgb.png"));

		// Connect appropriate signal
		connect(m_action_filterRgb, &QAction::triggered, this, &ColorimetricSegmenter::filterRgb);
	}

	/*if (!m_action_filterRgbWithSegmentation)
	{
		// Here we use the default plugin name, description, and icon,
		// but each action should have its own.
		m_action_filterRgbWithSegmentation = new QAction("Filter RGB using segmentation", this);
		m_action_filterRgbWithSegmentation->setToolTip("Filter the points on the selected cloud by RGB color using segmentation");
		m_action_filterRgbWithSegmentation->setIcon(getIcon());

		// Connect appropriate signal
		connect(m_action_filterRgbWithSegmentation, &QAction::triggered, this, &ColorimetricSegmenter::filterRgbWithSegmentation);
	}*/

	// HSV Filter
	if (!m_action_filterHSV)
	{
		m_action_filterHSV = new QAction("Filter HSV", this);
		m_action_filterHSV->setToolTip("Filter the points of the selected cloud by HSV color");
        m_action_filterHSV->setIcon(QIcon(":/CC/plugin/ColorimetricSegmenter/images/icon_hsv.png"));

		// Connect appropriate signal
		connect(m_action_filterHSV, &QAction::triggered, this, &ColorimetricSegmenter::filterHSV);
	}

	// Scalar filter
	if (!m_action_filterScalar)
	{
		m_action_filterScalar = new QAction("Filter scalar", this);
		m_action_filterScalar->setToolTip("Filter the points of the selected cloud using scalar field");
        m_action_filterScalar->setIcon(QIcon(":/CC/plugin/ColorimetricSegmenter/images/icon_scalar.png"));

		// Connect appropriate signal
		connect(m_action_filterScalar, &QAction::triggered, this, &ColorimetricSegmenter::filterScalar);
	}
	
	if (!m_action_histogramClustering)
	{
		m_action_histogramClustering = new QAction("Histogram Clustering", this);
        m_action_histogramClustering->setToolTip("Quantify the number of colors using Histogram Clustering");
        m_action_histogramClustering->setIcon(QIcon(":/CC/plugin/ColorimetricSegmenter/images/icon_quantif_h.png"));

		// Connect appropriate signal
		connect(m_action_histogramClustering, &QAction::triggered, this, &ColorimetricSegmenter::HistogramClustering);
	}
	
	if (!m_action_kMeansClustering)
	{
		// Here we use the default plugin name, description, and icon,
		// but each action should have its own.
		m_action_kMeansClustering = new QAction("Kmeans Clustering", this);
        m_action_kMeansClustering->setToolTip("Quantify the number of colors using Kmeans Clustering");
        m_action_kMeansClustering->setIcon(QIcon(":/CC/plugin/ColorimetricSegmenter/images/icon_quantif_k.png"));

		// Connect appropriate signal
		connect(m_action_kMeansClustering, &QAction::triggered, this, &ColorimetricSegmenter::KmeansClustering);
	}

	return {	m_action_filterRgb,
				m_action_filterHSV,
				//m_action_filterRgbWithSegmentation,
				m_action_filterScalar,
				m_action_histogramClustering,
				m_action_kMeansClustering
	};
}

// Get all point clouds that are selected in CC
// return a vector with ccPointCloud objects
std::vector<ccPointCloud*> ColorimetricSegmenter::getSelectedPointClouds()
{
	if (m_app == nullptr)
	{
		// m_app should have already been initialized by CC when plugin is loaded
		Q_ASSERT(false);
		return std::vector<ccPointCloud*>{};
	}

	ccHObject::Container selectedEntities = m_app->getSelectedEntities();
	std::vector<ccPointCloud*> clouds;
	for (size_t i = 0; i < selectedEntities.size(); ++i)
	{
		if (selectedEntities[i]->isKindOf(CC_TYPES::POINT_CLOUD))
		{
			clouds.push_back(static_cast<ccPointCloud*>(selectedEntities[i]));
		}
	}

	return clouds;
}

// Algorithm for the RGB filter
// It uses a color range with RGB values, and keeps the points with a color within that range.
void ColorimetricSegmenter::filterRgb()
{
	if (m_app == nullptr)
	{
		// m_app should have already been initialized by CC when plugin is loaded
		Q_ASSERT(false);

		return;
	}

	//check valid window
	if (!m_app->getActiveGLWindow())
	{
		m_app->dispToConsole("[ColorimetricSegmenter] No active 3D view", ccMainAppInterface::ERR_CONSOLE_MESSAGE);
		return;
	}

	std::vector<ccPointCloud*> clouds = getSelectedPointClouds();
	if (clouds.empty())
	{
		Q_ASSERT(false);
		return;
	}

	// Retrieve parameters from dialog
	RgbDialog rgbDlg(m_app->pickingHub(), m_app->getMainWindow());
	
	rgbDlg.show(); //necessary for setModal to be retained
	
	if (!rgbDlg.exec())
		return;

	// Start timer
	auto startTime = std::chrono::high_resolution_clock::now();

	// Get all values to make the color range with RGB values
	int redInf   = std::min( rgbDlg.red_first->value(),   rgbDlg.red_second->value()   );
	int redSup   = std::max( rgbDlg.red_first->value(),   rgbDlg.red_second->value()   );
	int greenInf = std::min( rgbDlg.green_first->value(), rgbDlg.green_second->value() );
	int greenSup = std::max( rgbDlg.green_first->value(), rgbDlg.green_second->value() );
	int blueInf  = std::min( rgbDlg.blue_first->value(),  rgbDlg.blue_second->value()  );
	int blueSup  = std::max( rgbDlg.blue_first->value(),  rgbDlg.blue_second->value()  );

	if (rgbDlg.margin->value() > 0)
	{
		// error margin
		int errorMargin = static_cast<int>(rgbDlg.margin->value() * 2.56); //256 / 100%

		redInf   -= errorMargin;
		redSup   += errorMargin;
		greenInf -= errorMargin;
		greenSup += errorMargin;
		blueInf  -= errorMargin;
		blueSup  += errorMargin;
	}

	// Set to min or max value (0-255)
	{
		redInf   = std::max(redInf,   0);
		greenInf = std::max(greenInf, 0);
		blueInf  = std::max(blueInf,  0);

		redSup   = std::min(redSup,   255);
		greenSup = std::min(greenSup, 255);
		blueSup  = std::min(blueSup,  255);
	}

	for (ccPointCloud* cloud : clouds)
	{
		if (cloud && cloud->hasColors())
		{
			// Use only references for speed reasons
			CCCoreLib::ReferenceCloud filteredCloudInside(cloud);
			CCCoreLib::ReferenceCloud filteredCloudOutside(cloud);

			for (unsigned j = 0; j < cloud->size(); ++j)
			{
				const ccColor::Rgba& rgb = cloud->getPointColor(j);
				if (	Inside(redInf,   rgb.r, redSup)
					&&	Inside(greenInf, rgb.g, greenSup)
					&&	Inside(blueInf,  rgb.b, blueSup)
					)
				{
					addPoint(filteredCloudInside, j);
				}
				else
				{
					addPoint(filteredCloudOutside, j);
				}

				if (m_addPointError)
				{
					return;
				}
			}
			QString name = "Rmin:" + QString::number(redInf) + "/Gmin:" + QString::number(greenInf) + "/Bmin:" + QString::number(blueInf) +
				"/Rmax:" + QString::number(redSup) + "/Gmax:" + QString::number(greenSup) + "/Bmax:" + QString::number(blueSup);

			createClouds<const RgbDialog&>(rgbDlg, cloud, filteredCloudInside, filteredCloudOutside, name);

			m_app->dispToConsole("[ColorimetricSegmenter] Cloud successfully filtered ! ", ccMainAppInterface::STD_CONSOLE_MESSAGE);
		}
	}

	ShowDurationNow(startTime);
}

void ColorimetricSegmenter::filterScalar()
{
	if (m_app == nullptr)
	{
		// m_app should have already been initialized by CC when plugin is loaded
		Q_ASSERT(false);

		return;
	}

	//check valid window
	if (!m_app->getActiveGLWindow())
	{
		m_app->dispToConsole("[ColorimetricSegmenter] No active 3D view", ccMainAppInterface::ERR_CONSOLE_MESSAGE);
		return;
	}

	// Retrieve parameters from dialog
	ScalarDialog scalarDlg(m_app->pickingHub(), m_app->getMainWindow());

	scalarDlg.show(); //necessary for setModal to be retained

	if (!scalarDlg.exec())
		return;

	// Start timer
	auto startTime = std::chrono::high_resolution_clock::now();

	double errorMargin = scalarDlg.margin->value() / 100.0;
	ScalarType minVal = std::min(scalarDlg.first->value(), scalarDlg.second->value());
	ScalarType maxVal = std::max(scalarDlg.first->value(), scalarDlg.second->value());
	//DGM: this way of applying the error margin is a bit strange
	minVal -= (errorMargin * minVal);
	maxVal += (errorMargin * maxVal);

	std::vector<ccPointCloud*> clouds = getSelectedPointClouds();
	for (ccPointCloud* cloud : clouds)
	{
		// Use only references for speed reasons
		CCCoreLib::ReferenceCloud filteredCloudInside(cloud);
		CCCoreLib::ReferenceCloud filteredCloudOutside(cloud);

		for (unsigned j = 0; j < cloud->size(); ++j)
		{
			const ScalarType val = cloud->getPointScalarValue(j);
			addPoint(val >= minVal && val <= maxVal ? filteredCloudInside : filteredCloudOutside, j);

			if (m_addPointError)
			{
				return;
			}
		}
		QString name = "min:" + QString::number(minVal) + "/max:" + QString::number(maxVal);

		createClouds<const ScalarDialog&>(scalarDlg, cloud, filteredCloudInside, filteredCloudOutside, name);

		m_app->dispToConsole("[ColorimetricSegmenter] Cloud successfully filtered ! ", ccMainAppInterface::STD_CONSOLE_MESSAGE);
	}

	ShowDurationNow(startTime);
}

typedef QSharedPointer<CCCoreLib::ReferenceCloud> _Region;
typedef std::vector<_Region> _RegionSet;
typedef std::vector<_RegionSet> SetOfRegionSet;

/**
 * @brief KNNRegions Determines the neighboring regions of a region.
 * @param basePointCloud The base cloud containing the points.
 * @param regions The list containing all the regions in the base cloud point.
 * @param region The region to compare with others.
 * @param k Max number of neighbours to find.
 * @param neighbours The resulting nearest regions.
 * @param thresholdDistance The maximum distance to search for neighbors.
 */
static bool KNNRegions(	ccPointCloud* basePointCloud,
						const _RegionSet& regions,
						const _Region& region,
						unsigned k,
						_RegionSet& neighbourRegions,
						unsigned thresholdDistance)
{
	QScopedPointer<ccPointCloud> regionCloud(basePointCloud->partialClone(region.data()));
	if (!regionCloud)
	{
		//not enough memory
		return false;
	}

	// compute distances
	CCCoreLib::DistanceComputationTools::Cloud2CloudDistancesComputationParams params;
	{
		params.kNNForLocalModel = k;
		params.maxSearchDist = thresholdDistance;
	}

	std::vector<double> distancesToCentralRegion;
	distancesToCentralRegion.reserve(regions.size());
	
	for (const _Region& r : regions)
	{
		QScopedPointer<ccPointCloud> neighbourCloud(basePointCloud->partialClone(r.data()));
		if (!neighbourCloud)
		{
			//not enough memory
			return false;
		}
		//DGM: warning, the computeCloud2CloudDistances method doesn't return a distance value (but a status / error)
		//distances are stored in the active scalar field (one per point!)
		int result = CCCoreLib::DistanceComputationTools::computeCloud2CloudDistances(neighbourCloud.data(), regionCloud.data(), params);
		if (result >= 0)
		{
			double meanDistance = 0.0;
			for (unsigned i = 0; i < neighbourCloud->size(); ++i)
			{
				meanDistance += neighbourCloud->getPointScalarValue(i);
			}
			meanDistance /= neighbourCloud->size();
			
			distancesToCentralRegion.push_back(meanDistance);
		}
		else
		{
			//failed to compute the distances
			return false;
		}
	}
	regionCloud.reset(nullptr);

	// sort the regions by their distance
	std::vector<size_t> regionIndices(regions.size());
	for (size_t i = 0; i < regions.size(); ++i)
		regionIndices[i] = i;

	std::sort(regionIndices.begin(), regionIndices.end(), [&](size_t a, size_t b) { return distancesToCentralRegion[a] < distancesToCentralRegion[b]; });

	// then extract the 'k' nearest regions.
	for (size_t regionIndex : regionIndices)
	{
		if (neighbourRegions.size() < k)
		{
			neighbourRegions.push_back(regions[regionIndex]);
		}
		else
		{
			break;
		}
	}

	return true;
}

/**
 * @brief colorimetricalDifference Compute colorimetrical difference between two RGB color values.
 * @param c1 First color value.
 * @param c2 Second color value.
 * @return Colorimetrical difference.
 */
static double ColorimetricalDifference(ccColor::Rgb c1, ccColor::Rgb c2)
{
	int dr = static_cast<int>(c1.r) - c2.r;
	int dg = static_cast<int>(c1.g) - c2.g;
	int db = static_cast<int>(c1.b) - c2.b;
	return sqrt(static_cast<double>(dr*dr + dg*dg + db*db));
}

/**
Compute the average color (RGB)
@param cloud : cloud which contains the points
@param subset : subset of points
Returns average color (RGB)
*/
static ccColor::Rgba ComputeAverageColor(const ccPointCloud& cloud, CCCoreLib::ReferenceCloud* subset)
{
	if (!subset || subset->size() == 0)
	{
		Q_ASSERT(false);
		return ccColor::white;
	}

	size_t count = subset->size();
	if (count == 0)
	{
		return ccColor::white;
	}
	else if (count == 1)
	{
		return cloud.getPointColor(subset->getPointGlobalIndex(0));
	}

	//other formula to compute the average can be used
	size_t redSum = 0, greenSum = 0, blueSum = 0, alphaSum = 0;
	for (unsigned j = 0; j < subset->size(); ++j)
	{
		const ccColor::Rgba& rgba = cloud.getPointColor(subset->getPointGlobalIndex(j));
		redSum   += rgba.r;
		greenSum += rgba.g;
		blueSum  += rgba.b;
		alphaSum += rgba.a;
	}

	ccColor::Rgba res(	static_cast<ColorCompType>(std::min(redSum   / count, static_cast<size_t>(ccColor::MAX))),
						static_cast<ColorCompType>(std::min(greenSum / count, static_cast<size_t>(ccColor::MAX))),
						static_cast<ColorCompType>(std::min(blueSum  / count, static_cast<size_t>(ccColor::MAX))),
						static_cast<ColorCompType>(std::min(alphaSum / count, static_cast<size_t>(ccColor::MAX))));

	return res;
}

/**
 * @brief colorimetricalDifference compute mean colorimetrical difference between two reference clouds.
 * The points in both clouds must be represented in RGB value.
 * @param basePointCloud The base cloud on which the reference clouds are based.
 * @param c1 The first reference cloud.
 * @param c2 The second reference cloud.
 * @return Colorimetrical difference.
 */
double ColorimetricalDifference(const ccPointCloud& basePointCloud,
								CCCoreLib::ReferenceCloud* c1,
								CCCoreLib::ReferenceCloud* c2)
{
	ccColor::Rgb rgb1 = ComputeAverageColor(basePointCloud, c1);
	ccColor::Rgb rgb2 = ComputeAverageColor(basePointCloud, c2);

	return ColorimetricalDifference(rgb1, rgb2);
}

bool ColorimetricSegmenter::RegionGrowing(	RegionSet& regions,
											ccPointCloud* pointCloud,
											const unsigned TNN,
											const double TPP,
											const double TD)
{
	if (!pointCloud || pointCloud->size() == 0)
	{
		Q_ASSERT(false);
		return false;
	}
	size_t pointCount = pointCloud->size();

	try
	{
		std::vector<unsigned> unlabeledPoints;
		unlabeledPoints.resize(pointCount);
		for (unsigned j = 0; j < pointCount; ++j)
		{
			unlabeledPoints.push_back(j);
		}
	
		std::vector<unsigned> pointIndices;
	
		CCCoreLib::DgmOctree* octree = new CCCoreLib::DgmOctree(pointCloud); // used to search nearest neighbors
		octree->build();
	
		// while there is points in {P} that haven’t been labeled
		while (!unlabeledPoints.empty())
		{
			// push an unlabeled point into stack Points
			pointIndices.push_back(unlabeledPoints.back());
			unlabeledPoints.pop_back();
		
			// initialize a new region Rc and add current point to R
			Region rc(new CCCoreLib::ReferenceCloud(pointCloud));
			rc->addPointIndex(unlabeledPoints.back());
		
			// while stack Points is not empty
			while (!pointIndices.empty())
			{
				// pop Points’ top element Tpoint
				unsigned tPointIndex = pointIndices.back();
				pointIndices.pop_back();

				// for each point p in {KNNTNN(Tpoint)}
				CCCoreLib::DgmOctree::NearestNeighboursSearchStruct nNSS = CCCoreLib::DgmOctree::NearestNeighboursSearchStruct();
				{
					nNSS.level = 1;
					nNSS.queryPoint = *(pointCloud->getPoint(tPointIndex));
					octree->getCellPos(octree->getCellCode(tPointIndex), 1, nNSS.cellPos, false);
					octree->computeCellCenter(octree->getCellCode(tPointIndex), 1, nNSS.cellCenter);
					nNSS.maxSearchSquareDistd = TD;
					nNSS.minNumberOfNeighbors = TNN;
				}
				octree->findNearestNeighborsStartingFromCell(nNSS);
			
				for (int i = 0; i < nNSS.pointsInNeighbourhood.size(); i++)
				{
					unsigned p = nNSS.pointsInNeighbourhood[i].pointIndex;
					// if p is labelled
					if (std::find(unlabeledPoints.begin(), unlabeledPoints.end(), p) != unlabeledPoints.end())
					{
						continue;
					}

					if (ColorimetricalDifference(pointCloud->getPointColor(p), pointCloud->getPointColor(tPointIndex)) < TPP)
					{
						pointIndices.push_back(p);
						rc->addPointIndex(p);
					}
				}

			}
			regions.push_back(rc);
		}
	}	
	catch (const std::bad_alloc&)
	{
		//not enough memory
		return false;
	}

	return true;
}

/**
 * @brief findRegion Find a given region in a vector of reference clouds.
 * @param container Container containing all the regions.
 * @param region Region to search for in the vector.
 * @return The index of the region if found, -1 in the other case.
 */
static int FindRegion(	const std::vector<_RegionSet>& container,
						CCCoreLib::ReferenceCloud* region )
{
	for (size_t i = 0; i < container.size(); ++i)
	{
		const _RegionSet& l = container[i];
		if (std::find(l.begin(), l.end(), region) != l.end())
		{
			return static_cast<int>(i);
		}
	}
	return -1;
}

bool ColorimetricSegmenter::RegionMergingAndRefinement(	RegionSet& mergedRegions,
														ccPointCloud* basePointCloud,
														const RegionSet& regions,
														const unsigned TNN,
														const double TRR,
														const double TD,
														const unsigned Min)
{
	std::vector<_RegionSet> homogeneous;

	// for each region Ri in {R}
	for (const Region& ri : regions)
	{
		// if Ri is not in {H}
		_RegionSet* riSet = nullptr;
		int riSetIndex = FindRegion(homogeneous, ri.data());
		if (riSetIndex == -1)
		{
			// create a new list to record Ri
			homogeneous.resize(homogeneous.size() + 1);
			riSet = &homogeneous.back();
			riSet->push_back(ri);
		}
		else
		{
			riSet = &(homogeneous[riSetIndex]);
		}

		// for each region Rj in {KNNTNN2,TD2(Ri)}
		RegionSet knnResult;
		if (!KNNRegions(basePointCloud, regions, ri, TNN, knnResult, TD))
		{
			//process failed
			return false;
		}

		for (const Region& rj : knnResult)
		{
			// if CD(Ri,Rj)<TRR
			if (ColorimetricalDifference(*basePointCloud, ri.data(), rj.data()) < TNN)
			{
				// if Rj is in {H}
				int regionIndex = FindRegion(homogeneous, rj.data());
				if (regionIndex < 0)
				{
					// add Rj to the list which contains Ri
					riSet->push_back(rj);
				}
			}
		}
	}

	// merge all the regions in the same list in {H} and get {R’}
	for (const _RegionSet& l : homogeneous)
	{
		Region merged(new CCCoreLib::ReferenceCloud(l[0]->getAssociatedCloud()));
		for (const Region& li : l)
		{
			if (li && !merged->add(*li))
			{
				//not enough memory
				return false;
			}
		}
		mergedRegions.push_back(merged);
	}
	
	//std::vector<CCCoreLib::ReferenceCloud*>* knnResult;
	// for each region Ri in {R’}
	/*for (CCCoreLib::ReferenceCloud* r : *mergedRegionsRef)
	{
		// if sizeof(Ri)<Min
		if(r->size() < Min)
		{
			// merge Ri to its nearest neighbors
			KNNRegions(basePointCloud, mergedRegionsRef, r, 1, knnResult, 0);
			mergedRegionsRef.
		}
	}*/

	//Return the merged and refined {R’}
	return true;
}

// filterRgbWithSegmentation parameters
static const unsigned TNN = 1;
static const double TPP = 2.0;
static const double TD = 2.0;
static const double TRR = 2.0;
static const unsigned Min = 2;

void ColorimetricSegmenter::filterRgbWithSegmentation()
{
	if (m_app == nullptr)
	{
		// m_app should have already been initialized by CC when plugin is loaded
		Q_ASSERT(false);

		return;
	}

	std::vector<ccPointCloud*> clouds = getSelectedPointClouds();
	if (clouds.empty())
	{
		Q_ASSERT(false);
		return;
	}

	// Retrieve parameters from dialog
	RgbDialog rgbDlg(m_app->pickingHub(), m_app->getMainWindow());

	rgbDlg.show(); //necessary for setModal to be retained

	if (!rgbDlg.exec())
		return;

	// Start timer
	auto startTime = std::chrono::high_resolution_clock::now();

	// Get margin value (percent)
	double errorMargin = rgbDlg.margin->value() / 100.0;

	// Get all values to make the color range with RGB values
	int redInf   = rgbDlg.red_first->value()    - static_cast<int>(errorMargin * rgbDlg.red_first->value());
	int redSup   = rgbDlg.red_second->value()   + static_cast<int>(errorMargin * rgbDlg.red_second->value());
	int greenInf = rgbDlg.green_first->value()  - static_cast<int>(errorMargin * rgbDlg.green_first->value());
	int greenSup = rgbDlg.green_second->value() + static_cast<int>(errorMargin * rgbDlg.green_second->value());
	int blueInf  = rgbDlg.blue_first->value()   - static_cast<int>(errorMargin * rgbDlg.blue_first->value());
	int blueSup  = rgbDlg.blue_second->value()  + static_cast<int>(errorMargin * rgbDlg.blue_second->value());

	redInf   = std::max(0, redInf);
	greenInf = std::max(0, greenInf);
	blueInf  = std::max(0, blueInf);
	redSup   = std::min(255, redSup);
	greenSup = std::min(255, greenSup);
	blueSup  = std::min(255, blueSup);

	for (ccPointCloud* cloud : clouds)
	{
		if (cloud->hasColors())
		{
			RegionSet regions;
			if (!RegionGrowing(regions, cloud, TNN, TPP, TD))
			{
				ccLog::Error("Process failed (not enough memory?)");
				return;
			}

			RegionSet mergedRegions;
			RegionMergingAndRefinement(mergedRegions, cloud, regions, TNN, TRR, TD, Min);
			//m_app->dispToConsole(QString("[ColorimetricSegmenter] regions %1").arg(regions->size()), ccMainAppInterface::STD_CONSOLE_MESSAGE);

			// retrieve the nearest region (in color range)
			for (Region& r : mergedRegions)
			{
				ccColor::Rgb mean = ComputeAverageColor(*cloud, r.data());
				if (	Inside(redInf,   mean.r, redSup)
					&&	Inside(greenInf, mean.g, greenSup)
					&&	Inside(blueInf,  mean.b, blueSup)
					)
				{
					ccPointCloud* newCloud = cloud->partialClone(r.data());
					if (newCloud)
					{
						cloud->setEnabled(false);
						if (cloud->getParent())
						{
							cloud->getParent()->addChild(newCloud);
						}

						m_app->addToDB(newCloud, false, true, false, false);

						m_app->dispToConsole("[ColorimetricSegmenter] Cloud successfully filtered with segmentation!", ccMainAppInterface::STD_CONSOLE_MESSAGE);
					}
					else
					{
						m_app->dispToConsole("Not enough memory", ccMainAppInterface::ERR_CONSOLE_MESSAGE);
						return;
					}
				}
			}
		}
	}

	ShowDurationNow(startTime);
}

// Algorithm for the HSV filter
// It uses the Hue-Saturation-Value (HSV) color space to filter the point cloud
void ColorimetricSegmenter::filterHSV()
{
	if (m_app == nullptr)
	{
		// m_app should have already been initialized by CC when plugin is loaded
		Q_ASSERT(false);
		return;
	}

	// Check valid window
	if (!m_app->getActiveGLWindow())
	{
		m_app->dispToConsole("[ColorimetricSegmenter] No active 3D view", ccMainAppInterface::ERR_CONSOLE_MESSAGE);
		return;
	}

	std::vector<ccPointCloud*> clouds = getSelectedPointClouds();
	if (clouds.empty())
	{
		Q_ASSERT(false);
		return;
	}

	// Retrieve parameters from dialog
	HSVDialog hsvDlg(m_app->pickingHub(), m_app->getMainWindow());
	
	hsvDlg.show(); //necessary for setModal to be retained
	
	if (!hsvDlg.exec())
		return;

	// Start timer
	auto startTime = std::chrono::high_resolution_clock::now();

	// Get HSV values
	Hsv hsv_first;
	hsv_first.h = hsvDlg.hue->value();
	hsv_first.s = hsvDlg.sat->value();
	hsv_first.v = hsvDlg.val->value();

	// We use look-up tables for faster comparisons
	static const uint8_t LOW = 0;
	static const uint8_t MIDDLE = 1;
	static const uint8_t HIGH = 2;
	uint8_t level[101];
	{
		for (unsigned i = 0; i <= 25; ++i)
			level[i] = LOW;
		for (unsigned i = 26; i <= 60; ++i)
			level[i] = MIDDLE;
		for (unsigned i = 61; i <= 100; ++i)
			level[i] = HIGH;
	}

	static const uint8_t RED     = 0;
	static const uint8_t YELLOW  = 1;
	static const uint8_t GREEN   = 2;
	static const uint8_t CYAN    = 3;
	static const uint8_t BLUE    = 4;
	static const uint8_t MAGENTA = 5;
	uint8_t section[360];
	{
		for (unsigned i = 0; i <= 30; ++i)
			section[i] = RED;
		for (unsigned i = 31; i <= 90; ++i)
			section[i] = YELLOW;
		for (unsigned i = 91; i <= 150; ++i)
			section[i] = GREEN;
		for (unsigned i = 151; i <= 210; ++i)
			section[i] = CYAN;
		for (unsigned i = 211; i <= 270; ++i)
			section[i] = BLUE;
		for (unsigned i = 271; i <= 330; ++i)
			section[i] = MAGENTA;
		for (unsigned i = 331; i <= 359; ++i)
			section[i] = RED;
	}

	for (ccPointCloud* cloud : clouds)
	{
		if (cloud->hasColors())
		{
			// Use only references for speed reasons
			CCCoreLib::ReferenceCloud filteredCloudInside(cloud);
			CCCoreLib::ReferenceCloud filteredCloudOutside(cloud);

			// We manually add color ranges with HSV values
			for (unsigned j = 0; j < cloud->size(); ++j)
			{
				const ccColor::Rgb& rgb = cloud->getPointColor(j);
				Hsv hsv_current(rgb);
				assert(hsv_current.h <= 359);
				assert(hsv_current.s <= 100);
				assert(hsv_current.v <= 100);

				if (level[hsv_first.s] == LOW && level[hsv_current.s] == LOW) //low saturation
				{
					// If Saturation is too small, considering Hue is useless
					// We only check that Value is equivalent
					addPoint(level[hsv_first.v] == level[hsv_current.v] ? filteredCloudInside : filteredCloudOutside, j);
				}
				else if (level[hsv_first.s] != LOW && level[hsv_current.s] != LOW) //middle to high saturation
				{
					if (level[hsv_first.v] == LOW && level[hsv_current.v] == LOW) //dark
					{
						addPoint(filteredCloudInside, j);
					}
					else if (level[hsv_first.v] != LOW && level[hsv_current.v] != LOW) //non-dark
					{
						addPoint(section[hsv_first.h] == section[hsv_current.h] ? filteredCloudInside : filteredCloudOutside, j);
					}
					else
					{
						addPoint(filteredCloudOutside, j);
					}
				}
				else
				{
					addPoint(filteredCloudOutside, j);
				}

				if (m_addPointError)
				{
					return;
				}
			}

			QString name = "h:" + QString::number(hsv_first.h, 'f', 0) + "/s:" + QString::number(hsv_first.s, 'f', 0) + "/v:" + QString::number(hsv_first.v, 'f', 0);
			createClouds<const HSVDialog&>(hsvDlg, cloud, filteredCloudInside, filteredCloudOutside, name);

			m_app->dispToConsole("[ColorimetricSegmenter] Cloud successfully filtered ! ", ccMainAppInterface::STD_CONSOLE_MESSAGE);
		}
	}

	ShowDurationNow(startTime);
}

// Method to add point to a ReferenceCloud*
bool ColorimetricSegmenter::addPoint(CCCoreLib::ReferenceCloud& filteredCloud, unsigned int j)
{
	m_addPointError = !filteredCloud.addPointIndex(j);
	
	if (m_addPointError)
	{
		//not enough memory
		m_app->dispToConsole("[ColorimetricSegmenter] Error, filter canceled.");
	}

	return m_addPointError;
}

// Method to interact with the component "Which points to keep"
template <typename T>
void ColorimetricSegmenter::createClouds(	T& dlg,
											ccPointCloud* cloud,
											const CCCoreLib::ReferenceCloud& filteredCloudInside,
											const CCCoreLib::ReferenceCloud& filteredCloudOutside,
											QString name )
{
	if (dlg.retain->isChecked())
	{
		createCloud(cloud, filteredCloudInside, name + ".inside");
	}
	else if (dlg.exclude->isChecked())
	{
		createCloud(cloud, filteredCloudOutside, name + ".outside");
	}
	else if (dlg.both->isChecked())
	{
		createCloud(cloud, filteredCloudInside, name + ".inside");
		createCloud(cloud, filteredCloudOutside, name + ".outside");
	}

}

// Method to create a new cloud
void ColorimetricSegmenter::createCloud(ccPointCloud* cloud,
										const CCCoreLib::ReferenceCloud& referenceCloud,
										QString name)
{
	if (!cloud)
	{
		Q_ASSERT(false);
		return;
	}
	
	ccPointCloud* newCloud = cloud->partialClone(&referenceCloud);
	if (!newCloud)
	{
		m_app->dispToConsole("Not enough memory");
		return;
	}
	
	newCloud->setName(name);
	cloud->setEnabled(false);
	if (cloud->getParent())
	{
		cloud->getParent()->addChild(newCloud);
	}

	m_app->addToDB(newCloud, false, true, false, false);
}

/**
Generate nxnxn clusters of points according to their color value (RGB)
@param cloud : the cloud which we work with
@param clusterPerDim : coefficient uses to split each RGB component
Returns a map of nxnxn keys, for each key a vector of the points index in the partition
*/
typedef std::map< size_t, std::vector<unsigned> > ClusterMap;
static bool GetKeyCluster(const ccPointCloud& cloud, size_t clusterPerDim, ClusterMap& clusterMap)
{
	Q_ASSERT(ccColor::MAX == 255);

	try
	{
		for (unsigned i = 0; i < cloud.size(); i++)
		{
			const ccColor::Rgb& rgb = cloud.getPointColor(i);

			size_t redCluster   = (static_cast<size_t>(rgb.r) * clusterPerDim) >> 8; // shift 8 bits (= division by 256)
			size_t greenCluster = (static_cast<size_t>(rgb.g) * clusterPerDim) >> 8; // shift 8 bits (= division by 256)
			size_t blueCluster  = (static_cast<size_t>(rgb.b) * clusterPerDim) >> 8; // shift 8 bits (= division by 256)

			size_t index = redCluster + (greenCluster + blueCluster * clusterPerDim) * clusterPerDim;

			//we add the point to the right container
			clusterMap[index].push_back(i);
		}
	}
	catch (const std::bad_alloc&)
	{
		return false;
	}

	return true;
}
/**
Compute the average color (RGB)
@param cloud : cloud which contains the points
@param bucket : vector of indexes of points
Returns average color (RGB)
*/
static ccColor::Rgba ComputeAverageColor(const ccPointCloud& cloud, const std::vector<unsigned>& bucket)
{
	size_t count = bucket.size();
	if (count == 0)
	{
		return ccColor::white;
	}
	else if (count == 1)
	{
		return cloud.getPointColor(bucket.front());
	}

	//other formula to compute the average can be used
	size_t redSum = 0, greenSum = 0, blueSum = 0, alphaSum = 0;
	for (unsigned pointIndex : bucket)
	{
		const ccColor::Rgba& rgba = cloud.getPointColor(pointIndex);
		redSum   += rgba.r;
		greenSum += rgba.g;
		blueSum  += rgba.b;
		alphaSum += rgba.a;
	}

	ccColor::Rgba res(	static_cast<ColorCompType>(std::min(redSum   / count, static_cast<size_t>(ccColor::MAX))),
						static_cast<ColorCompType>(std::min(greenSum / count, static_cast<size_t>(ccColor::MAX))),
						static_cast<ColorCompType>(std::min(blueSum  / count, static_cast<size_t>(ccColor::MAX))),
						static_cast<ColorCompType>(std::min(alphaSum / count, static_cast<size_t>(ccColor::MAX))));

	return res;
}

/**
Compute the distance between two colors
/!\ the formula can be modified, here it is simple to be as quick as possible
*/
static int ColorDistance(const ccColor::Rgb& c1, const ccColor::Rgb& c2)
{
	return (static_cast<int>(c1.r) - c2.r) + (static_cast<int>(c1.b) - c2.b) + (static_cast<int>(c1.g) - c2.g);
}

/**
Generate a pointcloud quantified using an histogram clustering
The purpose is to counter luminance variation due to the merge of different scans
*/
void ColorimetricSegmenter::HistogramClustering()
{
	if (m_app == nullptr)
	{
		// m_app should have already been initialized by CC when plugin is loaded
		Q_ASSERT(false);

		return;
	}

	std::vector<ccPointCloud*> clouds = ColorimetricSegmenter::getSelectedPointClouds();
	if (clouds.empty())
	{
		Q_ASSERT(false);
		return;
	}

	// creation of the window
	QuantiDialog quantiDlg(m_app->getMainWindow());
	if (!quantiDlg.exec())
		return;

	// Start timer
	auto startTime = std::chrono::high_resolution_clock::now();

	int nbClusterByComponent = quantiDlg.area_quanti->value();
	if (nbClusterByComponent < 0)
	{
		Q_ASSERT(false);
		return;
	}

	for (ccPointCloud* cloud : clouds)
	{
		if (cloud->hasColors())
		{
			ClusterMap clusterMap;
			if (!GetKeyCluster(*cloud, static_cast<size_t>(nbClusterByComponent), clusterMap))
			{
				m_app->dispToConsole("Not enough memory", ccMainAppInterface::ERR_CONSOLE_MESSAGE);
				break;
			}

			ccPointCloud* histCloud = cloud->cloneThis();
			if (!histCloud)
			{
				m_app->dispToConsole("Not enough memory", ccMainAppInterface::ERR_CONSOLE_MESSAGE);
				break;
			}

			histCloud->setName(QString("HistogramClustering: Indice Q = %1 // colors = %2").arg(nbClusterByComponent).arg(nbClusterByComponent * nbClusterByComponent * nbClusterByComponent));

			for (auto it = clusterMap.begin(); it != clusterMap.end(); it++)
			{
				ccColor::Rgba averageColor = ComputeAverageColor(*histCloud, it->second);

				for (unsigned pointIndex : it->second)
				{
					(*histCloud).setPointColor(pointIndex, averageColor);
				}
			}

			cloud->setEnabled(false);
			if (cloud->getParent())
			{
				cloud->getParent()->addChild(histCloud);
			}

			m_app->addToDB(histCloud, false, true, false, false);

			m_app->dispToConsole("[ColorimetricSegmenter] Cloud successfully clustered!", ccMainAppInterface::STD_CONSOLE_MESSAGE);

		}
	}

	ShowDurationNow(startTime);
}

/**
K-means algorithm
@param k : k clusters
@param it : limit of iterations before returns a result
Returns a cloud quantified
*/
static ccPointCloud* ComputeKmeansClustering(ccPointCloud* theCloud, unsigned K, int maxIterationCount)
{
	//valid parameters?
	if (!theCloud || K == 0)
	{
		Q_ASSERT(false);
		return nullptr;
	}

	unsigned pointCount = theCloud->size();
	if (pointCount == 0)
		return nullptr;

	if (K >= pointCount)
	{
		ccLog::Warning("Cloud %1 has less point than the expected number of classes.");
		return nullptr;
	}

	ccPointCloud* KCloud = nullptr;

	try
	{
		std::vector<ccColor::Rgba> clusterCenters;	//K clusters centers
		std::vector<int> clusterIndex;				//index of the cluster the point belongs to

		clusterIndex.resize(pointCount);
		clusterCenters.resize(K);

		//init (regularly sampled) classes centers
		double step = static_cast<double>(pointCount) / K;
		for (unsigned j = 0; j < K; ++j)
		{
			//TODO: this initialization is pretty biased... To be improved?
			clusterCenters[j] = theCloud->getPointColor(static_cast<unsigned>(std::ceil(step * j)));
		}

		//let's start
		int iteration = 0;
		for (; iteration < maxIterationCount; ++iteration)
		{
			bool meansHaveMoved = false;

			// assign each point (color) to the nearest cluster
			for (unsigned i = 0; i < pointCount; ++i)
			{
				const ccColor::Rgba& color = theCloud->getPointColor(i);

				int minK = 0;
				int minDistsToMean = std::abs(ColorDistance(color, clusterCenters[minK]));

				//we look for the nearest cluster center
				for (unsigned j = 1; j < K; ++j)
				{
					int distToMean = std::abs(ColorDistance(color, clusterCenters[j]));
					if (distToMean < minDistsToMean)
					{
						minDistsToMean = distToMean;
						minK = j;
					}
				}

				clusterIndex[i] = minK;
			}

			//update the clusters centers
			std::vector< std::vector<unsigned> > clusters;
			clusters.resize(K);
			for (unsigned i = 0; i < pointCount; ++i)
			{
				unsigned index = clusterIndex[i];
				clusters[index].push_back(i);
			}

			ccLog::Print("Iteration " + QString::number(iteration));
			for (unsigned j = 0; j < K; ++j)
			{
				const std::vector<unsigned>& cluster = clusters[j];
				if (cluster.empty())
				{
					continue;
				}
				
				ccColor::Rgba newMean = ComputeAverageColor(*theCloud, cluster);

				if (!meansHaveMoved && ColorDistance(clusterCenters[j], newMean) != 0)
				{
					meansHaveMoved = true;
				}

				clusterCenters[j] = newMean;
			}

			if (!meansHaveMoved)
			{
				break;
			}
		}

		KCloud = theCloud->cloneThis();
		if (!KCloud)
		{
			//not enough memory
			return nullptr;
		}
		KCloud->setName("Kmeans clustering: K = " + QString::number(K) + " / it = " + QString::number(iteration));

		//set color for each cluster
		for (unsigned i = 0; i < pointCount; i++)
		{
			KCloud->setPointColor(i, clusterCenters[clusterIndex[i]]);
		}

	}
	catch (const std::bad_alloc&)
	{
		//not enough memory
		return nullptr;
	}

	return KCloud;
}

/**
Algorithm based on k-means for clustering points cloud by its colors
*/
void ColorimetricSegmenter::KmeansClustering()
{
	std::vector<ccPointCloud*> clouds = ColorimetricSegmenter::getSelectedPointClouds();
	if (clouds.empty())
	{
		Q_ASSERT(false);
		return;
	}

	KmeansDlg kmeansDlg(m_app->getMainWindow());
	if (!kmeansDlg.exec())
		return;

	assert(kmeansDlg.spinBox_k->value() >= 0);
	unsigned K = static_cast<unsigned>(kmeansDlg.spinBox_k->value());
	int iterationCount = kmeansDlg.spinBox_it->value();

	// Start timer
	auto startTime = std::chrono::high_resolution_clock::now();

	for (ccPointCloud* cloud : clouds)
	{
		ccPointCloud* kcloud = ComputeKmeansClustering(cloud, K, iterationCount);
		if (!kcloud)
		{
			m_app->dispToConsole(QString("[ColorimetricSegmenter] Failed to cluster cloud %1").arg(cloud->getName()), ccMainAppInterface::WRN_CONSOLE_MESSAGE);
			continue;
		}

		cloud->setEnabled(false);
		if (cloud->getParent())
		{
			cloud->getParent()->addChild(kcloud);
		}

		m_app->addToDB(kcloud, false, true, false, false);
		m_app->dispToConsole("[ColorimetricSegmenter] Cloud successfully clustered!", ccMainAppInterface::STD_CONSOLE_MESSAGE);

	}

	ShowDurationNow(startTime);
}
