/*
 * Stellarium
 *
 * Copyright (C) 2025 Moritz Rätz
 *
 * 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 2
 * 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/>.
 */

#include "SkyCultureMapGraphicsView.hpp"
#include "SkyCulturePolygonItem.hpp"
#include "StelLocaleMgr.hpp"
#include "StelFileMgr.hpp"
#include "StelSkyCultureMgr.hpp"
#include <qjsonarray.h>
#include <qscrollbar.h>

#include <QGraphicsSvgItem>
#include <QJsonObject>
#include <QJsonDocument>
#include <QWheelEvent>
#include <QMouseEvent>
#include <QFile>

SkyCultureMapGraphicsView::SkyCultureMapGraphicsView(QWidget *parent)
	: QGraphicsView(parent)
	, viewScrolling(false)
	, mapMoved(false)
	, firstShow(true)
	, isRotated(false)
	, currentYear(0)
	, mouseLastXY(0, 0)
	, oldSkyCulture("")
{
	QGraphicsScene *scene = new QGraphicsScene(this);
	setScene(scene);
	setInteractive(true);

	setRenderHint(QPainter::Antialiasing); // maybe unnecessary for this project
	setTransformationAnchor(QGraphicsView::AnchorUnderMouse);

	setCursor(Qt::ArrowCursor);
	setMouseTracking(true);

	setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
	setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);

	// set up QTimelines for smooth zoom on culture selection (through public 'selectCulture' slot)
	zoomToDefaultTimer.setDuration(2000);
	zoomOnTargetTimer.setDuration(3000);
	zoomToDefaultTimer.setUpdateInterval(20);
	zoomOnTargetTimer.setUpdateInterval(20);

	connect(&zoomToDefaultTimer, &QTimeLine::valueChanged, this, &SkyCultureMapGraphicsView::zoomToDefault);
	connect(&zoomToDefaultTimer, &QTimeLine::finished, &zoomOnTargetTimer, &QTimeLine::start);
	connect(&zoomOnTargetTimer, &QTimeLine::valueChanged, this, &SkyCultureMapGraphicsView::zoomOnTarget);
	connect(&zoomOnTargetTimer, &QTimeLine::finished, this, [this] () {
		setSceneRect(- defaultRect.width() * 0.375, - defaultRect.height() * 0.125, defaultRect.width() * 1.75, defaultRect.height() * 1.25);
	});

	// draw basemap and culture polygons
	drawMapContent();
}

void SkyCultureMapGraphicsView::drawMapContent()
{
	// delete all items
	scene()->clear();

	QGraphicsSvgItem *baseMap = new QGraphicsSvgItem(":/graphicGui/skyCultureWorldMap.svgz");

	scene()->addItem(baseMap);
	scene()->setSceneRect(- baseMap->boundingRect().width() * 0.375, - baseMap->boundingRect().height() * 0.125, baseMap->boundingRect().width() * 1.75, baseMap->boundingRect().height() * 1.25);
	this->defaultRect = baseMap->boundingRect();

	loadCulturePolygons();
}

void SkyCultureMapGraphicsView::loadCulturePolygons()
{
	// loop over all skyCultures
	StelApp& app = StelApp::getInstance();
	QMap<QString, QString> cultureIdToTranslationMap = app.getSkyCultureMgr().getDirToI18Map();
	const QStringList cultureIds = cultureIdToTranslationMap.keys();

	for (const auto &currentCulture : cultureIds)
	{
		// find path of file
		const QString filePath = StelFileMgr::findFile("skycultures/" + currentCulture + "/territory.geojson");
		if (filePath.isEmpty())
		{
			// There is just no point to emit a warning in V26.1.
			if (StelUtils::getApplicationSeries().toDouble() > 26.2)
				qWarning() << "Failed to * find * [ " << currentCulture << " ] territory file in sky culture directory";
			continue;
		}
		// try to open file
		QFile file(filePath);
		if (!file.open(QFile::ReadOnly))
		{
			qWarning() << "Failed to * open * [ " << currentCulture << " ] territory file in sky culture directory";
			continue;
		}
		// try to read file
		const auto jsonText = file.readAll();
		if (jsonText.isEmpty())
		{
			qWarning() << "Failed to * read data * from [ " << currentCulture << " ] territory file in sky culture directory";
			continue;
		}
		// try to parse file as JsonDocument
		QJsonParseError error;
		const auto jsonDoc = QJsonDocument::fromJson(jsonText, &error);
		if (error.error != QJsonParseError::NoError)
		{
			qWarning().nospace() << "Failed to * parse * [ " << currentCulture << " ] territory file in sky culture directory: " << error.errorString();
			continue;
		}
		if (!jsonDoc.isObject())
		{
			qWarning() << "Failed to find the expected JSON structure in [ " << currentCulture << " ] territory file in sky culture directory";
			continue;
		}
		// try to access important information ---> create a new PolygonItem and add it to the scene
		const auto data = jsonDoc.object();
		if (data["features"].isArray())
		{
			const auto polygonArray = data["features"].toArray();
			for (const auto &currentPoly : polygonArray)
			{
				auto polygonObject = currentPoly.toObject();
				int beginTime = polygonObject["properties"].toObject().value("beginTime").toInt();
				int endTime = polygonObject["properties"].toObject().value("endTime").toInt();
				QPolygonF geometry;
				const auto coordinatesArray = polygonObject["geometry"].toObject()["coordinates"].toArray()[0].toArray();
				for (const auto &point : coordinatesArray)
				{
					auto pointArray = point.toArray();
					geometry << QPointF(pointArray[0].toDouble(), pointArray[1].toDouble());
				}

				SkyCulturePolygonItem *item = new SkyCulturePolygonItem(currentCulture, beginTime, endTime);
				item->setToolTip(cultureIdToTranslationMap.value(currentCulture));
				item->setPolygon(convertLatLonToMeter(geometry));
				scene()->addItem(item);
			}
		}
	}
}

void SkyCultureMapGraphicsView::calculateVBarValue(QPoint delta)
{
	QScrollBar *vBar = verticalScrollBar();
	int newVBarValue = vBar->value() - delta.y();
	QPointF mappedUpperLeftBorder = transform().map(QPointF(defaultRect.x(), defaultRect.y()));
	QPointF mappedlowerRightBorder = transform().map(QPointF(defaultRect.x() + defaultRect.width(), defaultRect.y() + defaultRect.height()));
	qreal upperBorderValue = mappedUpperLeftBorder.y() - (viewport()->height() / 2.0);
	qreal lowerBorderValue = mappedlowerRightBorder.y() - (viewport()->height() / 2.0);

	evaluateScrollBarValue(vBar->value(), newVBarValue, upperBorderValue, lowerBorderValue, vBar);
}

void SkyCultureMapGraphicsView::calculateHBarValue(QPoint delta)
{
	QScrollBar *hBar = horizontalScrollBar();
	int newHBarValue = hBar->value() + (isRightToLeft() ? delta.x() : -delta.x());
	QPointF mappedUpperLeftBorder = transform().map(QPointF(defaultRect.x(), defaultRect.y()));
	QPointF mappedlowerRightBorder = transform().map(QPointF(defaultRect.x() + defaultRect.width(), defaultRect.y() + defaultRect.height()));
	qreal leftBorderValue = mappedUpperLeftBorder.x() - (viewport()->width() / 2.0);
	qreal rightBorderValue = mappedlowerRightBorder.x() - (viewport()->width() / 2.0);

	evaluateScrollBarValue(hBar->value(), newHBarValue, leftBorderValue, rightBorderValue, hBar);
}

void SkyCultureMapGraphicsView::evaluateScrollBarValue(int oldValue, int newValue, qreal upperLeftBorder, qreal lowerRightBorder,  QScrollBar *bar)
{
	if (newValue < oldValue)
	{
		if (newValue < upperLeftBorder)
		{
			bar->setValue(upperLeftBorder);
		}
		else
		{
			bar->setValue(newValue);
		}
	}
	else if (newValue > oldValue)
	{
		if (newValue > lowerRightBorder)
		{
			bar->setValue(lowerRightBorder);
		}
		else
		{
			bar->setValue(newValue);
		}
	}
}

void SkyCultureMapGraphicsView::wheelEvent(QWheelEvent *e)
{
	qreal zoomFactor = std::pow(2.0, e->angleDelta().y() / 240.0);
	if (e->modifiers() & Qt::ControlModifier)
	{
		//holding ctrl while wheel zooming results in a finer zoom
		zoomFactor = 1.0 + ( zoomFactor - 1.0 ) / 15.0;
	}
	scaleView(zoomFactor);

	// ensure that the view is in bounds after scale operation
	QRectF currentViewRect = mapToScene(viewport()->rect()).boundingRect();
	if (!defaultRect.contains(currentViewRect.center()))
	{
		qreal x = currentViewRect.center().x();
		qreal y = currentViewRect.center().y();

		if (currentViewRect.center().x() < defaultRect.x())
		{
			x = defaultRect.x();
		}
		else if (currentViewRect.center().x() > defaultRect.x() + defaultRect.width())
		{
			x = defaultRect.x() + defaultRect.width();
		}
		if (currentViewRect.center().y() < defaultRect.y())
		{
			y = defaultRect.y();
		}
		else if (currentViewRect.center().y() > defaultRect.y() + defaultRect.height())
		{
			y = defaultRect.y() + defaultRect.height();
		}

		centerOn(x, y);
	}
}

void SkyCultureMapGraphicsView::mouseMoveEvent(QMouseEvent *e)
{
	// reimplementation of default ScrollHandDrag in QGraphicsView
	if (viewScrolling) {
		if (!mapMoved)
		{
			QGuiApplication::setOverrideCursor(Qt::ClosedHandCursor);
		}

		// ensure the user is not able to pan too far away from the map
		QPoint delta = e->pos() - mouseLastXY;
		calculateVBarValue(delta);
		calculateHBarValue(delta);

		mapMoved = true;
	}
	mouseLastXY = e->pos();

	QGraphicsView::mouseMoveEvent(e);
}

void SkyCultureMapGraphicsView::mousePressEvent(QMouseEvent *e)
{
	if ( e->button() == Qt::LeftButton )
	{
		viewScrolling = true;
	}

	// if event is not accepted (mouse not over item) mouseReleaseEvent is not triggered
	e->setAccepted(true);
}

void SkyCultureMapGraphicsView::mouseReleaseEvent( QMouseEvent *e )
{
	setFocus();
	QGraphicsView::mouseReleaseEvent(e);

	if (!mapMoved)
	{
		QGraphicsItem *currentTopmostMouseGrabberItem = itemAt(e->pos());
		// the item is either SkyCulturePolygonItem or QGraphicsSvgItem (background) ---> try to cast it to SkyCulturePolygonItem
		SkyCulturePolygonItem *scPolyItem = qgraphicsitem_cast<SkyCulturePolygonItem *>(currentTopmostMouseGrabberItem);
		if (scPolyItem)
		{
			const QString currentSkyCulture = scPolyItem->getSkyCultureId();
			// determine if a new culture is being selected
			if (oldSkyCulture != currentSkyCulture)
			{
				// if so, select all polygons of the respective culture, emit the cultureSelected Signal and set the oldSkyCulture to currentSkyCulture
				selectAllCulturePolygon(currentSkyCulture);
				emit cultureSelected(scPolyItem->toolTip());
			}
		}
	}

	if(viewScrolling)
	{
		viewScrolling = false;
		if (mapMoved)
		{
			mapMoved = false;
			QGuiApplication::restoreOverrideCursor();
		}
	}
}

void SkyCultureMapGraphicsView::showEvent(QShowEvent *e)
{
	// fit the base map to the current view when the widget is first shown
	// (This cannot be done beforehand because the calculation is based on the current size of the viewPort.
	// The viewPort is smaller than it should be until the first show because the widget is located on the stackedWidget in ViewDialog.
	// The boolean firstShow is used so the map doesn't reset itself every time the user changes pages or closes the ViewDialog.)
	if (firstShow)
	{
		qreal ratio = calculateScaleRatio(defaultRect.width(), defaultRect.height());
		scale(ratio, ratio);
		centerOn(defaultRect.center());
		firstShow = false;
	}

	QGraphicsView::showEvent(e);
}

void SkyCultureMapGraphicsView::scaleView(double factor)
{
	// calculate requested zoom before executing the zoom operation to limit the min / max zoom level
	QRectF viewRect = viewport()->rect().adjusted(2, 2, - 2, - 2);
	QRectF sceneRect = transform().scale(factor, factor).mapRect(QRectF(2, 2, defaultRect.width() * 1.3, defaultRect.height() * 1.3));
	const double currentTransform = transform().mapRect(QRectF(0, 0, 1, 1)).width();
	const double scaledTransform = transform().scale(factor, factor).mapRect(QRectF(0, 0, 1, 1)).width();
	const double windowMapRatio = calculateScaleRatio(defaultRect.width() * 1.3, defaultRect.height() * 1.3);
	const double scaledWindowMapRatio = std::min(viewRect.width() / sceneRect.width(), viewRect.height() / sceneRect.height());

	if (factor < 1) // zoom out operation
	{
		if (scaledWindowMapRatio > 1.0)
		{
			factor = windowMapRatio;
		}
	}
	else // zoom in operation
	{
		if (scaledTransform > 500.0)
			factor = 500.0 / currentTransform;
	}

	scale(factor, factor);
}

QPolygonF SkyCultureMapGraphicsView::convertLatLonToMeter(const QPolygonF &latLonCoordinates)
{
	QPolygonF meterCoordinates;

	for(auto point : latLonCoordinates)
	{
		qreal xMeter = (point.x() * 20037508.3427892439067363739014) / 180.0;
		qreal yMeter = ((std::log(std::tan(((90.0 + point.y())* M_PI) / 360.0)) / (M_PI / 180.0)) * 20037508.3427892439067363739014) / 180.0;
		meterCoordinates << QPointF(xMeter, yMeter);
	}

	return convertMeterToView(meterCoordinates);
}

QPolygonF SkyCultureMapGraphicsView::convertMeterToView(const QPolygonF &meterCoordinates)
{
	QPolygonF viewCoordinates;

	// cropped map (EPSG: 3857) extent:
	// x / lon: -20037507.0671618431806564 | 20037507.0671618431806564 ---> sum(abs) = 40075014.1343236863613128
	// y / lat: -18856469.2953464388847351 | 18418386.3090785145759583 ---> sum(abs) = 37274855.60442495346069336
	for(auto point : meterCoordinates)
	{
		// used map is cropped ---> project points (in meter) from full extent to smaller / cropped extent
		qreal xView = ((point.x() + 20037507.0671618431806564) / 40075014.1343236863613128) * defaultRect.width();
		qreal yView = ((point.y() - 18418386.3090785145759583) / -37274855.60442495346069336) * defaultRect.height();
		viewCoordinates << QPointF(xView, yView);
	}

	return viewCoordinates;
}

void SkyCultureMapGraphicsView::selectAllCulturePolygon(const QString &skyCultureId)
{
	const auto itemList = scene()->items();
	for (const auto &item : itemList)
	{
		// make sure the current item is a SkyCulturePolygonItem
		SkyCulturePolygonItem *scPolyItem = qgraphicsitem_cast<SkyCulturePolygonItem *>(item);
		if (!scPolyItem)
		{
			continue;
		}

		// by default set selection to false
		scPolyItem->setSelectionState(false);

		if (skyCultureId == scPolyItem->getSkyCultureId())
		{
			// items can't be selected when hidden
			if (!scPolyItem->isVisible())
			{
				scPolyItem->setVisible(true);
				scPolyItem->setSelectionState(true);
				scPolyItem->setVisible(false);
			}
			else
			{
				scPolyItem->setSelectionState(true);
			}
		}
	}
	oldSkyCulture = skyCultureId;
}

void SkyCultureMapGraphicsView::selectCulture(const QString &skyCultureName, int beginTime)
{
	QString skyCultureId = "";
	QList<QGraphicsItem *> currentTimeItems = QList<QGraphicsItem *>();
	QList<QGraphicsItem *> beginTimeItems = QList<QGraphicsItem *>();

	const auto itemList = scene()->items();
	for(const auto &item : itemList)
	{
		// cast QGraphicsItem to SkyCulturePolygonItem --> if the item is not a SkyCulturePolygonItem: skip it to prevent errors
		SkyCulturePolygonItem *scPolyItem = qgraphicsitem_cast<SkyCulturePolygonItem *>(item);
		if(!scPolyItem)
		{
			continue;
		}

		if(skyCultureName == scPolyItem->toolTip())
		{
			skyCultureId = scPolyItem->getSkyCultureId();
			if (scPolyItem->existsAtPointInTime(currentYear))
			{
				currentTimeItems.append(item);
			}
			else if (scPolyItem->existsAtPointInTime(beginTime))
			{
				beginTimeItems.append(item);
			}
		}
	}
	// if no polygon with the specified skyCultureId exists, this function simply clears the selection
	selectAllCulturePolygon(skyCultureId);

	if (!currentTimeItems.empty())
	{
		smoothFitInView(calculateBoundingBox(currentTimeItems));
	}
	else if (!beginTimeItems.empty())
	{
		smoothFitInView(calculateBoundingBox(beginTimeItems));
		emit timeValueChanged(beginTime);
	}
}

void SkyCultureMapGraphicsView::translatePolygons()
{
	StelApp& app = StelApp::getInstance();
	QMap<QString, QString> cultureIdToTranslationMap = app.getSkyCultureMgr().getDirToI18Map();
	const auto itemList = scene()->items();
	for(const auto &item : itemList)
	{
		SkyCulturePolygonItem *scPolyItem = qgraphicsitem_cast<SkyCulturePolygonItem *>(item);
		if(!scPolyItem)
		{
			continue;
		}
		scPolyItem->setToolTip(cultureIdToTranslationMap.value(scPolyItem->getSkyCultureId()));
	}
}

void SkyCultureMapGraphicsView::updateTime(int year)
{
	currentYear = year;
	updateCultureVisibility();
}

void SkyCultureMapGraphicsView::rotateMap(bool applyRotation)
{
	if (applyRotation)
	{
		StelCore *core = StelApp::getInstance().getCore();
		if (core->getCurrentLocation().getLatitude() < 0)
		{
			if (!isRotated)
			{
				rotate(180.0);
				isRotated = true;
			}
		}
		else
		{
			if (isRotated)
			{
				rotate(-180.0);
				isRotated = false;
			}
		}
	}
	else
	{
		if (isRotated)
		{
			rotate(-180.0);
			isRotated = false;
		}
	}
}

void SkyCultureMapGraphicsView::reloadMap()
{
	drawMapContent();
	updateCultureVisibility();
	selectAllCulturePolygon(oldSkyCulture);
}

void SkyCultureMapGraphicsView::updateCultureVisibility()
{
	// iterate over all polygons --> if currentTime is between beginTime and endTime: show polygon (else hide it)
	const auto itemList = scene()->items();
	for(const auto &item : itemList) {
		// cast generic QGraphicsItem to subclass SkyCulturePolygonItem
		SkyCulturePolygonItem *scPolyItem = qgraphicsitem_cast<SkyCulturePolygonItem *>(item);
		// if cast was unsuccessful (item is not an SkyCulturePolygonItem) --> look at the next item
		if(!scPolyItem)
			continue;

		// if the current year is between the start and end time of the polygon --> show (otherwise hide the item)
		if(scPolyItem->existsAtPointInTime(currentYear))
		{
			scPolyItem->setVisible(true);
		}
		else
		{
			scPolyItem->setVisible(false);
		}
	}
}

void SkyCultureMapGraphicsView::smoothFitInView(QRectF targetRect)
{
	// zoomOnTargetTimer must be stopped if the user selects a different culture (starts a new zoom operation) while the old one is still in progress
	zoomOnTargetTimer.stop();

	// update global variables so that they can be accessed by the slots 'zoomToDefault' and 'zoomOnTarget'
	this->targetRect = targetRect;
	this->startingRect = mapToScene(viewport()->rect()).boundingRect();

	// make sure sceneRect is big enough to not interfere with zoom
	setSceneRect(- defaultRect.width() * 1.0, - defaultRect.height() * 0.75, defaultRect.width() * 3.0, defaultRect.height() * 2.5);

	qreal maxDuration = 2000;
	qreal threshold = 1200;
	QEasingCurve factor(QEasingCurve::OutQuad);

	zoomToDefaultTimer.start();
	qreal deviation = std::max((std::fabs(startingRect.center().x() - defaultRect.center().x()) + std::fabs(startingRect.center().y() - defaultRect.center().y())) / 2,
						   std::min(std::fabs(startingRect.width() - defaultRect.width()), std::fabs(startingRect.height() - defaultRect.height())));

	// if value > threshold --> result > 1.0
	// valueForProgress returns 1.0 for all values greater than 1.0 (which equals maxDuration)
	zoomToDefaultTimer.setDuration(maxDuration * factor.valueForProgress(deviation * (1 / threshold)));
}

void SkyCultureMapGraphicsView::zoomToDefault(qreal zoomFactor)
{
	// transform the scene (scaling) to fit the current timestep
	qreal width = startingRect.width() + (defaultRect.width() - startingRect.width()) * zoomFactor;
	qreal height = startingRect.height() + (defaultRect.height() - startingRect.height()) * zoomFactor;

	qreal ratio = calculateScaleRatio(width, height);
	scale(ratio, ratio);

	// slowly move the center of the view to the new location
	QEasingCurve centerEasing(QEasingCurve::Linear);
	centerOn(startingRect.center() - (startingRect.center() - defaultRect.center()) * centerEasing.valueForProgress(zoomFactor));
}

void SkyCultureMapGraphicsView::zoomOnTarget(qreal zoomFactor)
{
	// transform the scene (scaling) to fit the current timestep
	qreal width = defaultRect.width() - (defaultRect.width() - targetRect.width()) * zoomFactor;
	qreal height = defaultRect.height() - (defaultRect.height() - targetRect.height()) * zoomFactor;

	qreal ratio = calculateScaleRatio(width, height);
	scale(ratio, ratio);

	// slowly move the center of the view to the new location
	QEasingCurve centerEasing(QEasingCurve::InCubic);
	centerOn(defaultRect.center() - (defaultRect.center() - targetRect.center()) * centerEasing.valueForProgress(zoomFactor));
}

qreal SkyCultureMapGraphicsView::calculateScaleRatio(qreal width, qreal height)
{
	// Rect of the current view with a margin of 2
	QRectF viewRect = viewport()->rect().adjusted(2, 2, - 2, - 2);
	if (viewRect.isEmpty())
	{
		return 0;
	}

	// Rect of the current transformation in scene coordinates
	// values of x / y of sceneRect are not important since only the width / height are used for the calculation
	QRectF sceneRect = transform().mapRect(QRectF(2, 2, width, height));
	if (sceneRect.isEmpty())
	{
		return 0;
	}

	// calculate the x / y ratio for scaling
	qreal xratio = viewRect.width() / sceneRect.width();
	qreal yratio = viewRect.height() / sceneRect.height();

	// keep original aspect ratio
	return std::min(xratio, yratio);
}

QRectF SkyCultureMapGraphicsView::calculateBoundingBox(const QList<QGraphicsItem *> graphicsItemList)
{
	QRectF boundingBox = QRectF();

	// iterate over list of items to build the Bbox
	for (auto &item : graphicsItemList)
	{
		boundingBox = boundingBox.united(item->boundingRect());
	}

	// slightly enlarge the Bbox so that the items don't take up the entire screen
	qreal width = boundingBox.width();
	qreal height = boundingBox.height();
	// threshold that controls how small the smaller dim of the Bbox is allowed to be (value in view coordinates, not real world)
	int minViewValue = 80;

	if(std::max(width, height) * 1.25 < minViewValue)
	{
		if(std::max(width, height) == width)
		{
			// width is the smaller dim --> set width to minViewValue and scale height accordingly
			width = minViewValue;
			height = (boundingBox.height() / boundingBox.width()) * minViewValue;
		}
		else
		{
			// height is the smaller dim --> set height to minViewValue and scale width accordingly
			height = minViewValue;
			width = (boundingBox.width() / boundingBox.height()) * minViewValue;
		}
		return QRectF(boundingBox.center().x() - width / 2, boundingBox.center().y() - height / 2, width, height);
	}

	return QRectF(boundingBox.x() - width / 8, boundingBox.y() - height / 8, width * 1.25, height * 1.25);
}

