/* +------------------------------------------------------------------------+
   |                     Mobile Robot Programming Toolkit (MRPT)            |
   |                          https://www.mrpt.org/                         |
   |                                                                        |
   | Copyright (c) 2005-2022, 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/core/exceptions.h>
#include <mrpt/math/TPoint3D.h>
#include <mrpt/serialization/serialization_frwds.h>
#include <mrpt/typemeta/TTypeName.h>

#include <algorithm>  // max()
#include <optional>

namespace mrpt::math
{
/** \addtogroup  geometry_grp
 * @{ */

/** A bounding box defined by the 3D points at each minimum-maximum corner.
 */
template <typename T>
struct TBoundingBox_
{
	enum class CTOR_FLAGS
	{
		None = 0,
		AllowUnordered
	};

	TBoundingBox_() = default;

	/** Ctor from min-max corners.
	 * A bounding box may have a zero volume if max==min.
	 * It is ilegal for a coordinate of the `max` vector to be smaller than its
	 * `min` counterpart, in which case an exception will be thrown, except if
	 * the flag CTOR_FLAGS::AllowUnordered is passed.
	 */
	TBoundingBox_(
		const mrpt::math::TPoint3D_<T>& Min,
		const mrpt::math::TPoint3D_<T>& Max,
		const CTOR_FLAGS f = CTOR_FLAGS::None)
		: min(Min), max(Max)
	{
		if (f != CTOR_FLAGS::AllowUnordered)
			ASSERT_(max.x >= min.x && max.y >= min.y && max.z >= min.z);
	}

	/** The corners of the bounding box */
	mrpt::math::TPoint3D_<T> min, max;

	/** Initialize with min=+Infinity, max=-Infinity. This is useful as an
	 * initial value before processing a list of points to keep their
	 * minimum/maximum. */
	static TBoundingBox_<T> PlusMinusInfinity()
	{
		const T i = std::numeric_limits<T>::max();
		return {{i, i, i}, {-i, -i, -i}, CTOR_FLAGS::AllowUnordered};
	}

	/** Returns the volume of the box */
	T volume() const
	{
		return (max.x - min.x) * (max.y - min.y) * (max.z - min.z);
	}

	/** Returns the intersection of this bounding box with "b", or std::nullopt
	 * if no intersection exists.
	 * Note that borders are enlarged by "epsilon" before to testing for
	 * intersection to handle numerical innacuracies, for example on planar
	 * bounding boxes with a fixed "z".
	 */
	std::optional<TBoundingBox_<T>> intersection(
		const TBoundingBox_<T>& b, const T epsilon = static_cast<T>(1e-4)) const
	{
		if (b.min.x - epsilon > max.x || b.min.y - epsilon > max.y ||
			b.min.z - epsilon > max.z || b.max.x + epsilon < min.x ||
			b.max.y + epsilon < min.y || b.max.z + epsilon < min.z)
			return {};

		return {TBoundingBox_<T>(
			{std::max(min.x, b.min.x), std::max(min.y, b.min.y),
			 std::max(min.z, b.min.z)},
			{std::min(max.x, b.max.x), std::min(max.y, b.max.y),
			 std::min(max.z, b.max.z)},
			CTOR_FLAGS::AllowUnordered)};
	}

	/** Returns the union of this bounding box with "b", i.e. a new bounding box
	 * comprising both `this` and `b` */
	TBoundingBox_<T> unionWith(const TBoundingBox_<T>& b) const
	{
		return {TBoundingBox_<T>(
			{std::min(min.x, b.min.x), std::min(min.y, b.min.y),
			 std::min(min.z, b.min.z)},
			{std::max(max.x, b.max.x), std::max(max.y, b.max.y),
			 std::max(max.z, b.max.z)})};
	}

	/** Expands the box limits to include the given point */
	void updateWithPoint(const mrpt::math::TPoint3D_<T>& p)
	{
		mrpt::keep_min(min.x, p.x);
		mrpt::keep_min(min.y, p.y);
		mrpt::keep_min(min.z, p.z);

		mrpt::keep_max(max.x, p.x);
		mrpt::keep_max(max.y, p.y);
		mrpt::keep_max(max.z, p.z);
	}

	/** Returns true if the point lies within the bounding box (including the
	 * exact border)
	 * \note (New in MRPT 2.3.3)
	 */
	bool containsPoint(const mrpt::math::TPoint3D_<T>& p) const
	{
		return p.x >= min.x && p.y >= min.y && p.z >= min.z && p.x <= max.x &&
			p.y <= max.y && p.z <= max.z;
	}

	/** Returns a new bounding box, transforming `this` from local coordinates
	 * to global coordinates, as if `this` was given with respect to `pose`, ie:
	 *
	 * \code
	 *  return.min = pose \oplus this->min
	 *  return.max = pose \oplus this->max
	 * \endcode
	 * \tparam POSE_T Can be mrpt::poses::CPose3D, or mrpt::math::TPose3D
	 *
	 * \note If a rotation exists, the output bounding box will no longer be an
	 * accurate representation of the actual 3D box.
	 */
	template <typename POSE_T>
	TBoundingBox_<T> compose(const POSE_T& pose) const
	{
		return {pose.composePoint(min), pose.composePoint(max)};
	}

	/** Returns a new bounding box, transforming `this` from global coordinates
	 * to local coordinates with respect to `pose`, ie:
	 *
	 * \code
	 *  return.min = this->min \ominus pose
	 *  return.max = this->max \ominus pose
	 * \endcode
	 * \tparam POSE_T Can be mrpt::poses::CPose3D, or mrpt::math::TPose3D
	 *
	 * \note If a rotation exists, the output bounding box will no longer be an
	 * accurate representation of the actual 3D box.
	 */
	template <typename POSE_T>
	TBoundingBox_<T> inverseCompose(const POSE_T& pose) const
	{
		return {pose.inverseComposePoint(min), pose.inverseComposePoint(max)};
	}

	/** Print bounding box as a string with format
	 * "(minx,miny,minz)-(maxx,maxy,maxz)"
	 *
	 * \note Do not inherit from mrpt::Stringifyable to avoid virtual class
	 * table and keeping the class trivially-copiable.
	 */
	std::string asString() const
	{
		std::string s = min.asString();
		s += "-";
		s += max.asString();
		return s;
	}
};

/** A bounding box defined by the 3D points at each minimum-maximum corner.
 * \sa mrpt::math::TPoint3D, mrpt::math::TPoint3Df
 */
using TBoundingBox = TBoundingBox_<double>;
using TBoundingBoxf = TBoundingBox_<float>;

mrpt::serialization::CArchive& operator>>(
	mrpt::serialization::CArchive& in, mrpt::math::TBoundingBoxf& bb);
mrpt::serialization::CArchive& operator<<(
	mrpt::serialization::CArchive& out, const mrpt::math::TBoundingBoxf& bb);

mrpt::serialization::CArchive& operator>>(
	mrpt::serialization::CArchive& in, mrpt::math::TBoundingBox& bb);
mrpt::serialization::CArchive& operator<<(
	mrpt::serialization::CArchive& out, const mrpt::math::TBoundingBox& bb);

/** @} */

}  // namespace mrpt::math

namespace mrpt::typemeta
{
// Specialization must occur in the same namespace
MRPT_DECLARE_TTYPENAME_NO_NAMESPACE(TBoundingBox, mrpt::math)
MRPT_DECLARE_TTYPENAME_NO_NAMESPACE(TBoundingBoxf, mrpt::math)
}  // namespace mrpt::typemeta
