/*
    Scan Tailor - Interactive post-processing tool for scanned pages.
    Copyright (C) 2015  Joseph Artsimovich <joseph.artsimovich@gmail.com>

    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, either version 3 of the License, or
    (at your option) any later version.

    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.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

#ifndef IMAGEPROC_RAST_LINE_FINDER_H_
#define IMAGEPROC_RAST_LINE_FINDER_H_

#include "imageproc_config.h"
#include "PriorityQueue.h"
#include <QPointF>
#include <QLineF>
#include <functional>
#include <vector>
#include <string>
#include <stddef.h>

namespace imageproc
{

class IMAGEPROC_EXPORT RastLineFinderParams
{
public:
	RastLineFinderParams();

	/**
	 * The algorithm operates in polar coordinates. One of those coordinates
	 * is a signed distance to the origin. By default the origin is at (0, 0),
	 * but you can set it explicitly with this call.
	 */
	void setOrigin(QPointF const& origin) { m_origin = origin; }

	/** \see setOrigin() */
	QPointF const& origin() const { return m_origin; }

	/**
	 * By default, all angles are considered. Keeping in mind that line direction
	 * doesn't matter, that gives us the range of [0, 180) degrees.
	 * This method allows you to provide a custom range to consider.
	 * Cases where min_angle_deg > max_angle_deg are valid. Consider the difference
	 * between [20, 200) and [200, 20). The latter one is equivalent to [200, 380).
	 * 
	 * \note This is not the angle between the line and the X axis!
	 *       Instead, you take your origin point (which is customizable)
	 *       and draw a perpendicular to your line. This vector,
	 *       from origin to line, is what defines the line angle.
	 *       In other words, after normalizing it to unit length, its
	 *       coordinates will correspond to cosine and sine of your angle.
	 */
	void setAngleRangeDeg(double min_angle_deg, double max_angle_deg) {
		m_minAngleDeg = min_angle_deg;
		m_maxAngleDeg = max_angle_deg;
	}

	/** \see setAngleRangeDeg() */
	double minAngleDeg() const { return m_minAngleDeg; }

	/** \see setAngleRangeDeg() */
	double maxAngleDeg() const { return m_maxAngleDeg; }

	/**
	 * Being a recursive subdivision algorithm, it has to stop refining the angle
	 * at some point. Angle tolerance is the maximum acceptable error (in degrees)
	 * for the lines returned. By default it's set to 0.1 degrees. Setting it to
	 * a higher value will improve performance.
	 */
	void setAngleToleranceDeg(double tolerance_deg) { m_angleToleranceDeg = tolerance_deg; }

	/** \see setAngleToleranceDeg() */
	double angleToleranceDeg() const { return m_angleToleranceDeg; }

	/**
	 * Sets the maximum distance the point is allowed to be from a line
	 * to still be considered a part of it. In reality, this value is
	 * a lower bound. The upper bound depends on angle tolerance and
	 * will tend to the lower bound as angle tolerance tends to zero.
	 *
	 * \see setAngleTolerance()
	 */
	void setMaxDistFromLine(double dist) { m_maxDistFromLine = dist; }

	/** \see setMaxDistFromLine() */
	double maxDistFromLine() const { return m_maxDistFromLine; }

	/**
	 * A support point is a point considered to be a part of a line.
	 * By default, lines consisting of 3 or more points are considered.
	 * The minimum allowed value is 2, while higher values improve performance.
	 *
	 * \see setMaxDistFromLine()
	 */
	void setMinSupportPoints(unsigned pts) { m_minSupportPoints = pts; }

	/**
	 * \see setMinSupportPoints()
	 */
	unsigned minSupportPoints() const { return m_minSupportPoints; }

	/**
	 * \brief Checks if parameters are valid, optionally providing an error string.
	 */
	bool validate(std::string* error = 0) const;
private:
	QPointF m_origin;
	double m_minAngleDeg;
	double m_maxAngleDeg;
	double m_angleToleranceDeg;
	double m_maxDistFromLine;
	unsigned m_minSupportPoints;
};


/**
 * \brief Finds lines in point clouds.
 *
 * \see Help -> About -> References -> [14]
 */
class IMAGEPROC_EXPORT RastLineFinder
{
private:
	class SearchSpace;

	friend void swap(SearchSpace& o1, SearchSpace& o2) {
		o1.swap(o2);
	}
public:
	/**
	 * The function to be called by the client subdivision function in order
	 * to inject a new search space into the system.
	 */
	typedef std::function<
		void(std::vector<unsigned> const&)
	> ClientSubdivisionEmitter;
	
	/**
	 * The idea behind the ClientSubdivisionFunction is to give the client code
	 * an opportunity to influence the subspace subdivision process. For instance,
	 * the client code may know about some constraints that don't allow certain
	 * groups of points to be a part of the same line.
	 * This function takes the following arguments:
	 * \li A line representing the middle of the current search space.
	 * \li The list of point indexes that fall into the current search space.
	 * \li A function to be called by the client code to inject a new search space into the system.
	 *
	 * If the client decides not to inject any subspaces, it has to return false. In this case,
	 * the standard search space subdivision process is invoked.
	 */
	typedef std::function<
		bool(QLineF const&, std::vector<unsigned> const&, ClientSubdivisionEmitter const&)
	> ClientSubdivisionFunction;

	/**
	 * Construct a line finder from a point cloud and a set of parameters.
	 *
	 * \throw std::invalid_argument if \p params are invalid.
	 * \see RastLineFinderParams::validate()
	 */
	RastLineFinder(std::vector<QPointF> const& points, RastLineFinderParams const& params);

	/** @see ClientSubdivisionFunction */
	void setClientSubdivisionFunction(ClientSubdivisionFunction const& func);

	/**
	 * Look for the next best line in terms of the number of support points.
	 * When a line is found, its support points are removed from the lists of
	 * support points of other candidate lines.
	 *
	 * \param[out] point_idxs If provided, it will be filled with indices of support
	 *             points for this line. The indices index the vector of points
	 *             that was passed to RastLineFinder constructor.
	 * \return If there are no more lines satisfying the search criteria,
	 *         a null (default constructed) QLineF is returned. Otherwise,
	 *         a line that goes near its support points is returned.
	 *         Such a line is not to be treated as a line segment, that is positions
	 *         of its endpoints should not be counted upon. In addition, the
	 *         line won't be properly fit to its support points, but merely be
	 *         close to an optimal line.
	 */
	QLineF findNext(std::vector<unsigned>* point_idxs = 0);
private:
	class Point
	{
	public:
		QPointF pt;
		bool available;

		Point(QPointF const& p) : pt(p), available(true) {}
	};

	class PointUnavailablePred
	{
	public:
		PointUnavailablePred(std::vector<Point> const* points) : m_pPoints(points) {}

		bool operator()(unsigned idx) const { return !(*m_pPoints)[idx].available; }
	private:
		std::vector<Point> const* m_pPoints;
	};

	class SearchSpace
	{
		// Member-wise copying is OK.
	public:
		SearchSpace();

		SearchSpace(RastLineFinder const& owner, float min_dist, float max_dist,
			float min_angle_rad, float max_angle_rad, std::vector<unsigned> const& candidate_idxs);

		/**
		 * Returns a line that corresponds to the center of this search space.
		 * The returned line should be treated as an unbounded line rather than
		 * line segment, meaning that exact positions of endpoints can't be
		 * counted on.
		 */
		QLineF representativeLine(RastLineFinder const& owner) const;

		bool subdivideDist(RastLineFinder const& owner, SearchSpace& subspace1, SearchSpace& subspace2) const;

		bool subdivideAngle(RastLineFinder const& owner, SearchSpace& subspace1, SearchSpace& subspace2) const;

		void pruneUnavailablePoints(PointUnavailablePred pred);

		std::vector<unsigned>& pointIdxs() { return m_pointIdxs; }

		std::vector<unsigned> const& pointIdxs() const { return m_pointIdxs; }

		void swap(SearchSpace& other);
	private:
		float m_minDist; //
		float m_maxDist; // These are already extended by max-dist-to-line.
		float m_minAngleRad;
		float m_maxAngleRad;
		std::vector<unsigned> m_pointIdxs; // Indexes into m_points of the parent object.
	};

	class OrderedSearchSpaces : public PriorityQueue<SearchSpace, OrderedSearchSpaces>
	{
		friend class PriorityQueue<SearchSpace, OrderedSearchSpaces>;
	private:
		void setIndex(SearchSpace& obj, size_t heap_idx) {}
		
		bool higherThan(SearchSpace const& lhs, SearchSpace const& rhs) const {
			return lhs.pointIdxs().size() > rhs.pointIdxs().size();
		}
	};

	bool tryClientSubdivision(SearchSpace& ssp, QLineF const& line);

	void pushIfGoodEnough(SearchSpace& ssp);

	void processClientReducedSubspace(
		SearchSpace const& ssp, std::vector<unsigned> const& point_idxs);

	void markPointsUnavailable(std::vector<unsigned> const& point_idxs);

	void pruneUnavailablePoints();

	ClientSubdivisionFunction m_clientSubdivisionFunc;
	QPointF m_origin;
	double m_angleToleranceRad;
	double m_maxDistFromLine;
	unsigned m_minSupportPoints;
	std::vector<Point> m_points;
	OrderedSearchSpaces m_orderedSearchSpaces;
	bool m_firstLine;
};

} // namespace imageproc

#endif
