/*
	File                 : CartesianCoordinateSystem.cpp
	Project              : LabPlot
	Description          : Cartesian coordinate system for plots.
	--------------------------------------------------------------------
	SPDX-FileCopyrightText: 2012-2025 Alexander Semke <alexander.semke@web.de>

	SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "backend/worksheet/plots/cartesian/CartesianCoordinateSystem.h"
#include "backend/lib/macros.h"
#include "backend/worksheet/plots/cartesian/CartesianCoordinateSystemPrivate.h"
#include "backend/worksheet/plots/cartesian/CartesianPlot.h"
#include <KLocalizedString>

#include "backend/nsl/nsl_math.h"

/* ============================================================================ */
/* ========================= coordinate system ================================ */
/* ============================================================================ */
/**
 * \class CartesianCoordinateSystem
 * \brief Cartesian coordinate system for plots.
 */
CartesianCoordinateSystem::CartesianCoordinateSystem(CartesianPlot* plot)
	: AbstractCoordinateSystem(plot)
	, d(new CartesianCoordinateSystemPrivate(this)) {
	d->plot = plot;
	// TODO: set some standard scales
}

CartesianCoordinateSystem::~CartesianCoordinateSystem() {
	delete d;
}

QString CartesianCoordinateSystem::dimensionToString(Dimension dim) {
	switch (dim) {
	case Dimension::X:
		return QLatin1String("x");
	case Dimension::Y:
		return QLatin1String("y");
	}
	return {};
}

QString CartesianCoordinateSystem::info() const {
	DEBUG(Q_FUNC_INFO)
	if (!name().isEmpty())
		return name();

	if (d->plot)
		return QString(QLatin1String("x = ") + d->plot->range(Dimension::X, d->xIndex).toString() + QLatin1String(", y = ")
					   + d->plot->range(Dimension::Y, d->yIndex).toString());

	return i18n("no info available");
}

// ##############################################################################
// ######################### logical to scene mappers ###########################
// ##############################################################################
Points CartesianCoordinateSystem::mapLogicalToScene(const Points& points, MappingFlags flags) const {
	// DEBUG(Q_FUNC_INFO << ", (points with flags)")
	const QRectF pageRect = d->plot->dataRect();
	const bool noPageClipping = pageRect.isNull() || (flags & MappingFlag::SuppressPageClipping);
	const bool noPageClippingY = flags & MappingFlag::SuppressPageClippingY;
	const bool limit = flags & MappingFlag::Limit;
	const double xPage = pageRect.x(), yPage = pageRect.y();
	const double w = pageRect.width(), h = pageRect.height();

	// DEBUG(Q_FUNC_INFO << ", xScales/YScales size: " << d->xScales.size() << '/' << d->yScales.size())

	Points result;
	result.reserve(points.size());
	for (const auto* xScale : d->xScales) {
		if (!xScale)
			continue;

		for (const auto* yScale : d->yScales) {
			if (!yScale)
				continue;

			for (const auto& point : points) {
				double x = point.x(), y = point.y();

				if (!xScale->contains(x) || !yScale->contains(y))
					continue;
				if (!xScale->map(&x) || !yScale->map(&y))
					continue;

				if (limit) {
					// set to max/min if passed over
					x = qBound(xPage, x, xPage + w);
					y = qBound(yPage, y, yPage + h);
				}

				if (noPageClippingY)
					y = yPage + h / 2.;

				const QPointF mappedPoint(x, y);
				if (noPageClipping || limit || rectContainsPoint(pageRect, mappedPoint))
					result.append(mappedPoint);
			}
		}
	}
	result.squeeze();

	return result;
}

/*!
	Maps the points in logical coordinates from @p points and fills the @p visiblePoints with the points in logical coordinates restricted to the current
   intervals.
	@param logicalPoints List of points in logical coordinates
	@param scenePoints List for the points in scene coordinates
	@param visiblePoints List for the logical coordinates restricted to the current region of the coordinate system
	@param flags
 */
void CartesianCoordinateSystem::mapLogicalToScene(const Points& logicalPoints,
												  Points& scenePoints,
												  std::vector<bool>& visiblePoints,
												  MappingFlags flags) const {
	// DEBUG(Q_FUNC_INFO << ", (curve with all points)")
	const QRectF pageRect = d->plot->dataRect();
	const bool noPageClipping = pageRect.isNull() || (flags & MappingFlag::SuppressPageClipping);
	const bool noPageClippingY = flags & MappingFlag::SuppressPageClippingY;
	const bool limit = flags & MappingFlag::Limit;
	const double xPage = pageRect.x(), yPage = pageRect.y();
	const double w = pageRect.width(), h = pageRect.height();

	// DEBUG(Q_FUNC_INFO << ", xScales/YScales size: " << d->xScales.size() << '/' << d->yScales.size())

	for (const auto* xScale : d->xScales) {
		if (!xScale)
			continue;

		for (const auto* yScale : d->yScales) {
			if (!yScale)
				continue;

			int i = 0;
			for (const auto& point : logicalPoints) {
				double x = point.x(), y = point.y();
				if (!xScale->contains(x) || !yScale->contains(y))
					continue;
				if (!xScale->map(&x) || !yScale->map(&y))
					continue;

				if (limit) {
					// set to max/min if passed over
					x = qBound(xPage, x, xPage + w);
					y = qBound(yPage, y, yPage + h);
				}

				if (noPageClippingY)
					y = yPage + h / 2.;

				const QPointF mappedPoint(x, y);
				if (noPageClipping || limit || rectContainsPoint(pageRect, mappedPoint)) {
					scenePoints.append(mappedPoint);
					visiblePoints[i] = true;
				} else
					visiblePoints[i] = false;

				i++;
			}
		}
	}
}

/*!
	Maps the points in logical coordinates from \c points and fills the \c visiblePoints with the points in logical coordinates restricted to the current
   intervals. If there are points, that lie on another one they will not be added a second time.
	@param logicalPoints List of points in logical coordinates
	@param scenePoints List for the points in scene coordinates
	@param visiblePoints List for the logical coordinates restricted to the current region of the coordinate system
 */
void CartesianCoordinateSystem::mapLogicalToScene(int startIndex,
												  int endIndex,
												  const Points& logicalPoints,
												  Points& scenePoints,
												  std::vector<bool>& visiblePoints,
												  MappingFlags flags) const {
	// DEBUG(Q_FUNC_INFO << ", (curve points)")
	const QRectF pageRect = d->plot->dataRect();
	const bool noPageClipping = pageRect.isNull() || (flags & MappingFlag::SuppressPageClipping);
	const bool noPageClippingY = flags & MappingFlag::SuppressPageClippingY;
	const bool limit = flags & MappingFlag::Limit;
	const double xPage = pageRect.x(), yPage = pageRect.y();
	const double w = pageRect.width(), h = pageRect.height();

	const int numberOfPixelX = std::ceil(pageRect.width());
	const int numberOfPixelY = std::ceil(pageRect.height());

	if (numberOfPixelX <= 0 || numberOfPixelY <= 0)
		return;

	// eliminate multiple scene points (size (numberOfPixelX + 1) * (numberOfPixelY + 1))
	QVector<QVector<bool>> scenePointsUsed(numberOfPixelX + 1);
	for (auto& col : scenePointsUsed)
		col.resize(numberOfPixelY + 1);

	// DEBUG(Q_FUNC_INFO << ", xScales/YScales size: " << d->xScales.size() << '/' << d->yScales.size())

	for (const auto* xScale : d->xScales) {
		if (!xScale)
			continue;

		for (const auto* yScale : d->yScales) {
			if (!yScale)
				continue;

			for (int i = startIndex; i <= endIndex; i++) {
				const QPointF& point = logicalPoints.at(i);

				double x = point.x(), y = point.y();
				if (!xScale->contains(x) || !yScale->contains(y))
					continue;
				if (!xScale->map(&x) || !yScale->map(&y))
					continue;

				if (limit) {
					// set to max/min if passed over
					x = qBound(xPage, x, xPage + w);
					y = qBound(yPage, y, yPage + h);
				}

				if (noPageClippingY)
					y = yPage + h / 2.;

				const QPointF mappedPoint(x, y);
				// DEBUG(mappedPoint.x() << ' ' << mappedPoint.y())
				if (noPageClipping || limit || rectContainsPoint(pageRect, mappedPoint)) {
					// TODO: check
					const int indexX = std::round(x - xPage);
					const int indexY = std::round(y - yPage);
					if (scenePointsUsed.at(indexX).at(indexY))
						continue;

					scenePointsUsed[indexX][indexY] = true;
					scenePoints.append(mappedPoint);
					// DEBUG(mappedPoint.x() << ' ' << mappedPoint.y())
					visiblePoints[i] = true;
				} else
					visiblePoints[i] = false;
			}
		}
	}
}

/*
 * Map a single point
 * */
QPointF CartesianCoordinateSystem::mapLogicalToScene(QPointF logicalPoint, bool& visible, MappingFlags flags) const {
	// DEBUG(Q_FUNC_INFO << ", (single point)")
	const QRectF pageRect = d->plot->dataRect();
	const bool noPageClipping = pageRect.isNull() || (flags & MappingFlag::SuppressPageClipping) || (flags & MappingFlag::SuppressPageClippingVisible);
	const bool noPageClippingY = flags & MappingFlag::SuppressPageClippingY;
	const bool limit = flags & MappingFlag::Limit;
	const bool visibleFlag = flags & MappingFlag::SuppressPageClippingVisible;

	double x = logicalPoint.x(), y = logicalPoint.y();
	const double xPage = pageRect.x(), yPage = pageRect.y();
	const double w = pageRect.width(), h = pageRect.height();

	// DEBUG(Q_FUNC_INFO << ", xScales/YScales size: " << d->xScales.size() << '/' << d->yScales.size())

	for (const auto* xScale : d->xScales) {
		if (!xScale)
			continue;

		for (const auto* yScale : d->yScales) {
			if (!yScale)
				continue;

			if (!xScale->contains(x) || !yScale->contains(y))
				continue;
			if (!xScale->map(&x) || !yScale->map(&y))
				continue;

			if (limit) {
				// set to max/min if passed over
				x = qBound(xPage, x, xPage + w);
				y = qBound(yPage, y, yPage + h);
			}

			if (noPageClippingY)
				y = pageRect.y() + h / 2.;

			QPointF mappedPoint(x, y);
			const bool containsPoint = rectContainsPoint(pageRect, mappedPoint);
			if (noPageClipping || limit || containsPoint) {
				if (visibleFlag)
					visible = containsPoint;
				else
					visible = true;
				return mappedPoint;
			}
		}
	}

	visible = false;
	return QPointF{};
}

Lines CartesianCoordinateSystem::mapLogicalToScene(const Lines& lines, MappingFlags flags) const {
	QRectF pageRect = d->plot->dataRect();
	Lines result;
	const bool doPageClipping = !pageRect.isNull() && !(flags & MappingFlag::SuppressPageClipping);

	double xGapBefore;
	double xGapAfter = NAN;
	double yGapBefore;
	double yGapAfter = NAN;

	// DEBUG(Q_FUNC_INFO << ", xScales/yScales size: " << d->xScales.size() << '/' << d->yScales.size())

	QVectorIterator<CartesianScale*> xIterator(d->xScales);
	while (xIterator.hasNext()) {
		const CartesianScale* xScale = xIterator.next();
		if (!xScale)
			continue;

		xGapBefore = xGapAfter;
		if (xIterator.hasNext()) {
			const CartesianScale* nextXScale = xIterator.peekNext();
			if (!nextXScale)
				continue;
			Range<double> nextXRange;
			nextXScale->getProperties(&nextXRange);

			double x1 = xScale->end();
			double x2 = nextXScale->start();

			bool valid = xScale->map(&x1);
			if (valid)
				valid = nextXScale->map(&x2);
			if (valid)
				xGapAfter = x2 - x1;
			else
				xGapAfter = NAN;
		} else
			xGapAfter = NAN;

		QVectorIterator<CartesianScale*> yIterator(d->yScales);
		while (yIterator.hasNext()) {
			const CartesianScale* yScale = yIterator.next();
			if (!yScale)
				continue;

			yGapBefore = yGapAfter;
			if (yIterator.hasNext()) {
				const CartesianScale* nextYScale = yIterator.peekNext();
				if (!nextYScale)
					continue;

				double y1 = yScale->end();
				double y2 = nextYScale->start();

				bool valid = yScale->map(&y1);
				if (valid)
					valid = nextYScale->map(&y2);
				if (valid)
					yGapAfter = y2 - y1;
				else
					yGapAfter = NAN;
			} else
				yGapAfter = NAN;

			const QRectF scaleRect = QRectF(xScale->start(), yScale->start(), xScale->end() - xScale->start(), yScale->end() - yScale->start()).normalized();

			for (auto line : lines) {
				// QDEBUG(Q_FUNC_INFO << ", LINE " << line)
				LineClipResult clipResult;
				if (!AbstractCoordinateSystem::clipLineToRect(&line, scaleRect, &clipResult))
					continue;

				double x1 = line.x1();
				if (!xScale->map(&x1))
					continue;

				double x2 = line.x2();
				if (!xScale->map(&x2))
					continue;

				double y1 = line.y1();
				if (!yScale->map(&y1))
					continue;

				double y2 = line.y2();
				if (!yScale->map(&y2))
					continue;

				if (flags & MappingFlag::MarkGaps) {
					// mark the end of the gap
					if (!std::isnan(xGapBefore)) {
						if (clipResult.xClippedLeft[0]) {
							QLineF gapMarker(x1 + xGapBefore / 4., y1 - xGapBefore / 2., x1 - xGapBefore / 4., y1 + xGapBefore / 2.);
							// 							if (AbstractCoordinateSystem::clipLineToRect(&gapMarker, pageRect))
							result.append(gapMarker);
						}
						if (clipResult.xClippedLeft[1]) {
							QLineF gapMarker(x2 + xGapBefore / 4., y2 - xGapBefore / 2., x2 - xGapBefore / 4., y2 + xGapBefore / 2.);
							// 							if (AbstractCoordinateSystem::clipLineToRect(&gapMarker, pageRect))
							result.append(gapMarker);
						}
					}

					// mark the beginning of the gap
					if (!std::isnan(xGapAfter)) {
						if (clipResult.xClippedRight[0]) {
							QLineF gapMarker(x1 + xGapAfter / 4., y1 - xGapAfter / 2., x1 - xGapAfter / 4., y1 + xGapAfter / 2.);
							// 							if (AbstractCoordinateSystem::clipLineToRect(&gapMarker, pageRect))
							result.append(gapMarker);
						}
						if (clipResult.xClippedRight[1]) {
							QLineF gapMarker(x2 + xGapAfter / 4., y2 - xGapAfter / 2., x2 - xGapAfter / 4., y2 + xGapAfter / 2.);
							// 							if (AbstractCoordinateSystem::clipLineToRect(&gapMarker, pageRect))
							result.append(gapMarker);
						}
					}

					if (!std::isnan(yGapBefore)) {
						if (clipResult.yClippedTop[0]) {
							QLineF gapMarker(x1 + yGapBefore / 2., y1 - yGapBefore / 4., x1 - yGapBefore / 2., y1 + yGapBefore / 4.);
							// 							if (AbstractCoordinateSystem::clipLineToRect(&gapMarker, pageRect))
							result.append(gapMarker);
						}
						if (clipResult.yClippedTop[1]) {
							QLineF gapMarker(x2 + yGapBefore / 2., y2 - yGapBefore / 4., x2 - yGapBefore / 2., y2 + yGapBefore / 4.);
							// 							if (AbstractCoordinateSystem::clipLineToRect(&gapMarker, pageRect))
							result.append(gapMarker);
						}
					}

					if (!std::isnan(yGapAfter)) {
						if (clipResult.yClippedBottom[0]) {
							QLineF gapMarker(QPointF(x1 + yGapAfter / 2., y1 - yGapAfter / 4.), QPointF(x1 - yGapAfter / 2., y1 + yGapAfter / 4.));
							if (AbstractCoordinateSystem::clipLineToRect(&gapMarker, pageRect))
								result.append(gapMarker);
						}
						if (clipResult.yClippedBottom[1]) {
							QLineF gapMarker(QPointF(x2 + yGapAfter / 2., y2 - yGapAfter / 4.), QPointF(x2 - yGapAfter / 2., y2 + yGapAfter / 4.));
							if (AbstractCoordinateSystem::clipLineToRect(&gapMarker, pageRect))
								result.append(gapMarker);
						}
					}
				}

				QLineF mappedLine(QPointF(x1, y1), QPointF(x2, y2));
				if (doPageClipping) {
					if (!AbstractCoordinateSystem::clipLineToRect(&mappedLine, pageRect)) {
						// DEBUG(Q_FUNC_INFO << ", WARNING: OMIT mapped line!")
						continue;
					}
				}

				//				QDEBUG(Q_FUNC_INFO << ", append line " << mappedLine)
				result.append(mappedLine);
			}
		}
	}

	return result;
}

// ##############################################################################
// ######################### scene to logical mappers ###########################
// ##############################################################################
Points CartesianCoordinateSystem::mapSceneToLogical(const Points& points, MappingFlags flags) const {
	QRectF pageRect = d->plot->dataRect();
	Points result;
	const bool noPageClipping = pageRect.isNull() || (flags & MappingFlag::SuppressPageClipping);
	const bool limit = flags & MappingFlag::Limit;
	const bool noPageClippingY = flags & MappingFlag::SuppressPageClippingY;
	const double xPage = pageRect.x();
	const double yPage = pageRect.y();
	const double w = pageRect.width();
	const double h = pageRect.height();

	// DEBUG(Q_FUNC_INFO << ", xScales/YScales size: " << d->xScales.size() << '/' << d->yScales.size())

	for (const auto& point : points) {
		double x = point.x();
		double y = point.y();
		if (limit) {
			// set to max/min if passed over
			x = qBound(xPage, x, xPage + w);
			y = qBound(yPage, y, yPage + h);
		}

		if (noPageClippingY)
			y = yPage + h / 2.;

		if (noPageClipping || limit || pageRect.contains(point)) {
			bool found = false;

			for (const auto* xScale : d->xScales) {
				if (found)
					break;
				if (!xScale)
					continue;

				for (const auto* yScale : d->yScales) {
					if (found)
						break;
					if (!yScale)
						continue;

					if (!xScale->inverseMap(&x)) {
						x = point.x();
						continue;
					}

					if (!yScale->inverseMap(&y)) {
						y = point.y();
						continue;
					}

					if (!xScale->contains(x)) {
						x = point.x();
						continue;
					}

					if (!yScale->contains(y)) {
						y = point.y();
						continue;
					}

					result.append(QPointF(x, y));
					found = true;
				}
			}
		}
	}

	return result;
}

QPointF CartesianCoordinateSystem::mapSceneToLogical(QPointF logicalPoint, MappingFlags flags) const {
	QRectF pageRect = d->plot->dataRect();
	QPointF result;
	bool noPageClipping = pageRect.isNull() || (flags & MappingFlag::SuppressPageClipping);
	bool limit = flags & MappingFlag::Limit;
	const bool noPageClippingY = flags & MappingFlag::SuppressPageClippingY;

	if (limit) {
		// set to max/min if passed over
		logicalPoint.setX(qBound(pageRect.x(), logicalPoint.x(), pageRect.x() + pageRect.width()));
		logicalPoint.setY(qBound(pageRect.y(), logicalPoint.y(), pageRect.y() + pageRect.height()));
	}

	if (noPageClippingY)
		logicalPoint.setY(pageRect.y() + pageRect.height() / 2.);

	// DEBUG(Q_FUNC_INFO << ", xScales/YScales size: " << d->xScales.size() << '/' << d->yScales.size())

	if (noPageClipping || limit || pageRect.contains(logicalPoint)) {
		double x = logicalPoint.x();
		double y = logicalPoint.y();
		// DEBUG(Q_FUNC_INFO << ", x/y = " << x << " " << y)

		for (const auto* xScale : d->xScales) {
			if (!xScale)
				continue;
			for (const auto* yScale : d->yScales) {
				if (!yScale)
					continue;

				if (!xScale->inverseMap(&x) || !yScale->inverseMap(&y))
					continue;

				if (!xScale->contains(x) || !yScale->contains(y))
					continue;

				result.setX(x);
				result.setY(y);
				return result;
			}
		}
	}

	return result;
}

bool CartesianCoordinateSystem::isValid() const {
	if (d->xScales.isEmpty() || d->yScales.isEmpty())
		return false;

	for (const auto* scale : d->xScales) {
		if (!scale)
			return false;
	}

	for (const auto* scale : d->yScales) {
		if (!scale)
			return false;
	}
	return true;
}

/**************************************************************************************/

/**
 * \brief Determine the direction relative to the page in different directions
 *
 * This function is needed for untransformed lengths such as axis tick length.
 * \return 1 or -1
 */
int CartesianCoordinateSystem::direction(const Dimension dim) const {
	switch (dim) {
	case Dimension::X: {
		if (d->xScales.isEmpty() || !d->xScales.at(0)) {
			DEBUG(Q_FUNC_INFO << ", WARNING: no x scale!")
			return 1;
		}

		return d->xScales.at(0)->direction();
	}
	case Dimension::Y: {
		if (d->yScales.isEmpty() || !d->yScales.at(0)) {
			DEBUG(Q_FUNC_INFO << ", WARNING: no y scale!")
			return 1;
		}

		return d->yScales.at(0)->direction();
	}
	}
	return 1;
}

// TODO: design elegant, flexible and undo-aware API for changing scales
bool CartesianCoordinateSystem::setScales(const Dimension dim, const QVector<CartesianScale*>& scales) {
	DEBUG(Q_FUNC_INFO)
	switch (dim) {
	case Dimension::X: {
		while (!d->xScales.isEmpty())
			delete d->xScales.takeFirst();

		d->xScales = scales;
		return true; // TODO: check scales validity
	}
	case Dimension::Y: {
		while (!d->yScales.isEmpty())
			delete d->yScales.takeFirst();

		d->yScales = scales;
		return true; // TODO: check scales validity
	}
	}
	return 1;
}

QVector<CartesianScale*> CartesianCoordinateSystem::scales(const Dimension dim) const {
	DEBUG(Q_FUNC_INFO)
	switch (dim) {
	case Dimension::X:
		return d->xScales; // TODO: should rather return a copy of the scales here
	case Dimension::Y:
		return d->yScales; // TODO: should rather return a copy of the scales here
	}
	return QVector<CartesianScale*>();
}

int CartesianCoordinateSystem::index(const Dimension dim) const {
	switch (dim) {
	case Dimension::X:
		return d->xIndex;
	case Dimension::Y:
		return d->yIndex;
	}
	return 0;
}

void CartesianCoordinateSystem::setIndex(const Dimension dim, const int index) {
	switch (dim) {
	case Dimension::X:
		d->xIndex = index;
		d->xScales.clear();
		break;
	case Dimension::Y:
		d->yIndex = index;
		d->yScales.clear();
		break;
	}
}

/*!
 * Adjusted the function QRectF::contains(QPointF) from Qt 4.8.4 to handle the
 * comparison of float numbers correctly.
 * TODO: check whether the newer versions of Qt do the comparison correctly.
 */
bool CartesianCoordinateSystem::rectContainsPoint(const QRectF& rect, QPointF point) const {
	qreal l = rect.x();
	qreal r = rect.x();
	qreal w = rect.width();
	qreal h = rect.height();
	if (w < 0)
		l += w;
	else
		r += w;
	if (nsl_math_essentially_equal(l, r)) // null rect
		return false;

	if (nsl_math_definitely_less_than(point.x(), l) || nsl_math_definitely_greater_than(point.x(), r))
		return false;

	qreal t = rect.y();
	qreal b = rect.y();
	if (h < 0)
		t += h;
	else
		b += h;
	if (nsl_math_essentially_equal(t, b)) // null rect
		return false;

	if (nsl_math_definitely_less_than(point.y(), t) || nsl_math_definitely_greater_than(point.y(), b))
		return false;

	return true;
}

// ##############################################################################
// ######################### Private implementation #############################
// ##############################################################################
CartesianCoordinateSystemPrivate::CartesianCoordinateSystemPrivate(CartesianCoordinateSystem* owner)
	: q(owner) {
}

CartesianCoordinateSystemPrivate::~CartesianCoordinateSystemPrivate() {
	while (!xScales.isEmpty())
		delete xScales.takeFirst();

	while (!yScales.isEmpty())
		delete yScales.takeFirst();
}
