#include "mapview.h"

#include "mapview_tile_cache_p.h"
#include "mapview_tile_downloader_p.h"
#include "osm_math_p.h"
#include "qad_types.h"

#include <QGraphicsPixmapItem>
#include <QGraphicsScene>
#include <QMetaMethod>
#include <QPainter>
#include <QPixmap>
#include <QProgressDialog>
#include <QScrollBar>
#include <QWheelEvent>

const int tileSize = 256;


MapView::MapView(QWidget * parent): QWidget(parent) {
	downloader = new MapViewTileDownloader(this);
	cache      = new MapViewTileCache(this);
	setMouseTracking(true);
	connect(cache, &MapViewTileCache::tileReady, this, [this]() {
		if (!is_downloading) drawBackground();
	});
	int size = 16;
	QPixmap px(QSize(size, size) * 2);
	QPainter p(&px);
	for (int i = 0; i < 2; ++i) {
		for (int j = 0; j < 2; ++j) {
			p.fillRect(i * size, j * size, size, size, (((i + j) % 2) == 0) ? Qt::white : Qt::lightGray);
		}
	}
	p.end();
	brush_tr.setTexture(px);
	zoomTo(0);
}


MapView::~MapView() {
	for (auto * i: items_)
		i->parent = nullptr;
	qDeleteAll(items_);
	delete cache;
	delete downloader;
}


void MapView::addItem(MapItemBase * item) {
	if (items_.contains(item)) return;
	items_ << item;
	item->parent = this;
	item->updatePosition();
	update();
}


void MapView::removeItem(MapItemBase * item) {
	if (!items_.contains(item)) return;
	items_.removeOne(item);
	if (hover == item) hover = nullptr;
}


QPointF MapView::center() const {
	return OSM::xy2geo(center_);
}


void MapView::setCenter(QPointF c) {
	center_ = OSM::geo2xy(c);
	checkCenter();
	updateViewRect();
	drawBackground();
}


void MapView::setZoom(double z) {
	zoomTo(z);
}


void MapView::setContrast(double value) {
	contrast = piClampd(value, 0., 100.);
	update();
}


QString MapView::cachePath() const {
	return cache->cacheRoot();
}


void MapView::setCachePath(const QString & p) {
	cache->setCacheRoot(p);
	drawBackground();
}


void MapView::setOfflineMode(bool yes) {
	is_offline = yes;
	cache->setOfflineMode(yes);
}


void MapView::setTileObsoleteTime(int secs) {
	cache->setTileObsoleteTime(secs);
}


void MapView::setTileProvider(MapViewTileProviderBase * p) {
	if (p) max_level = p->maximumLevel();
	cache->clearCache();
	downloader->setTileProvider(p);
	drawBackground();
}


MapViewTileProviderBase * MapView::tileProvider() const {
	return downloader->tileProvider();
}


void collectDownloadIndeces(MapViewTileCache * cache,
                            QQueue<MapViewTypes::TileIndex> & indeces,
                            MapViewTypes::TileIndex index,
                            int target_zoom_level,
                            bool only_new) {
	if (only_new) {
		if (!cache->isTileFileExists(index)) indeces << index;
	} else
		indeces << index;
	if (index.z >= target_zoom_level) return;
	int z = index.z + 1;
	int x = index.x * 2;
	int y = index.y * 2;
	for (int i = 0; i < 2; ++i) {
		for (int j = 0; j < 2; ++j) {
			collectDownloadIndeces(cache, indeces, (MapViewTypes::TileIndex){z, x + i, y + j}, target_zoom_level, only_new);
		}
	}
}


void MapView::downloadCurrentView(int target_zoom_level, bool only_new, int parallel) {
	int sx            = qMax(0, (int)floor(view_rect.left() * tiles_side));
	int sy            = qMax(0, (int)floor(view_rect.top() * tiles_side));
	int ex            = qMin(tiles_side, (int)ceil(view_rect.right() * tiles_side));
	int ey            = qMin(tiles_side, (int)ceil(view_rect.bottom() * tiles_side));
	target_zoom_level = qMin(target_zoom_level, max_level);
	QQueue<MapViewTypes::TileIndex> indeces;
	for (int ix = sx; ix < ex; ++ix) {
		for (int iy = sy; iy < ey; ++iy) {
			collectDownloadIndeces(cache, indeces, (MapViewTypes::TileIndex){zoom_level, ix, iy}, target_zoom_level, only_new);
		}
	}
	if (indeces.size() < 4) return;
	QProgressDialog dialog;
	dialog.setWindowTitle(tr("MapView"));
	dialog.setLabelText(tr("Downloading ..."));
	dialog.setCancelButtonText(tr("Cancel"));
	dialog.setMaximum(indeces.size());
	dialog.setValue(0);
	downloader->clearQueue();
	std::atomic_int value(0);
	auto conn = connect(
		downloader,
		&MapViewTileDownloader::tileDone,
		this,
		[this, &dialog, &indeces, &value]() {
			// qDebug() << "done";
			value++;
			int cv = value;
			dialog.setValue(cv);
			if (indeces.isEmpty())
				dialog.cancel();
			else {
				downloader->queueTile(indeces.dequeue(), true);
			}
		},
		Qt::DirectConnection);
	connect(&dialog, &QProgressDialog::canceled, this, [this, &dialog]() {
		dialog.cancel();
		downloader->clearQueue();
	});
	is_downloading = true;
	for (int i = 0; i < parallel; ++i) {
		if (indeces.isEmpty()) break;
		downloader->queueTile(indeces.dequeue());
	}
	// dialog.exec();
	dialog.setModal(true);
	dialog.show();
	while (!dialog.isHidden()) {
		QThread::msleep(1);
		QApplication::processEvents();
	}
	disconnect(conn);
	is_downloading = false;
	drawBackground();
}


void MapView::mousePressEvent(QMouseEvent * e) {
	is_pan           = false;
	press_point      = e->pos();
	zoom_anchor      = press_point;
	last_click_coord = OSM::xy2geo(mapToNorm(press_point));
}


void MapView::mouseReleaseEvent(QMouseEvent * e) {
	if (!is_pan) {
		if (hover)
			emit itemClicked(hover);
		else
			emit mapClicked(last_click_coord);
	}
	is_pan = false;
}


void MapView::mouseDoubleClickEvent(QMouseEvent * e) {}


void MapView::mouseMoveEvent(QMouseEvent * e) {
	// QPointF geo = OSM::xy2geo(mapToNorm(e->pos()));
	// qDebug() << QString("%1 , %2").arg(geo.x(), 0, 'f', 5).arg(geo.y(), 0, 'f', 5);
	if (e->buttons() == Qt::LeftButton) {
		if (is_pan) {
			double mins = qMin(width(), height());
			center_ -= QPointF(e->pos() - press_point) / scale_ / mins;
			checkCenter();
			press_point = e->pos();
			updateViewRect();
			drawBackground();
		} else {
			if ((e->pos() - press_point).manhattanLength() >= QApplication::startDragDistance()) is_pan = true;
		}
	}
	if (e->buttons() == Qt::RightButton) {
		auto dp     = e->pos() - press_point;
		press_point = e->pos();
		zoom(1. - dp.y() / 50., zoom_anchor);
	}
	updateMouse(e->pos());
}


void MapView::wheelEvent(QWheelEvent * e) {
	double scl = e->angleDelta().x() == 0 ? e->angleDelta().y() : e->angleDelta().x();
	scl        = 1. + (scl / 400.);
	QPoint mp;
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
	mp = e->pos();
#else
	mp = e->position().toPoint();
#endif
	zoom(scl, mp);
}


void MapView::paintEvent(QPaintEvent *) {
	QPainter p(this);
	p.drawPixmap(0, 0, background);
	if (contrast < 100.) {
		auto col = palette().color(QPalette::Window);
		col.setAlphaF(1. - contrast / 100.);
		p.fillRect(rect(), col);
	}
	drawItems(p);
	updateMouse(mapFromGlobal(QCursor::pos()));
}


void MapView::resizeEvent(QResizeEvent *) {
	cache->setMinimumCacheSize((width() + tileSize) * (height() + tileSize) * 4 * 3);
	checkZoom();
	updateZoomLevel();
	updateViewRect();
	drawBackground();
}


void MapView::drawBackground() {
	if (size() != background.size()) background = QPixmap(size());
	background.fill(palette().window().color());
	QPainter p(&background);
	double mins    = qMin(width(), height());
	int sx         = qMax(0, (int)floor(view_rect.left() * tiles_side));
	int sy         = qMax(0, (int)floor(view_rect.top() * tiles_side));
	int ex         = qMin(tiles_side, (int)ceil(view_rect.right() * tiles_side));
	int ey         = qMin(tiles_side, (int)ceil(view_rect.bottom() * tiles_side));
	double ts      = 1. / tiles_side / qMax(view_rect.width(), view_rect.height()) * qMax(width(), height());
	QPointF offset = view_rect.topLeft() * mins * scale_;
	p.setRenderHint(QPainter::SmoothPixmapTransform);
	p.setPen(Qt::white);
	QSizeF tile_size(tileSize, tileSize);
	if (downloader->tileProvider()) tile_size = downloader->tileProvider()->tileSize();
	for (int ix = sx; ix < ex; ++ix) {
		for (int iy = sy; iy < ey; ++iy) {
			QRectF r((ix)*ts, (iy)*ts, ts, ts);
			r.translate(-offset);
			QRectF px_rct(QPointF(), tile_size /*QSizeF(tileSize, tileSize)*/);
			auto tile = cache->getTile((MapViewTypes::TileIndex){zoom_level, ix, iy}, px_rct);
			if (!tile.isEmpty()) {
				p.drawPixmap(r, tile.pixmap, px_rct);
			} else {
				p.fillRect(r, brush_tr);
			}
			// p.setPen(Qt::white);
			// p.drawText(r, Qt::AlignCenter, QString("%1x%2").arg(ix).arg(iy));
		}
	}
	// qDebug() << sx << sy << ex << ey << ts;
	update();
}


void MapView::drawItems(QPainter & p) {
	auto src_tr = p.transform();
	QPainter::RenderHints cur_hints;
	QPoint mouse        = mapFromGlobal(QCursor::pos());
	MapItemBase * hover = nullptr;
	for (auto * i: items_) {
		if (!i->isVisible()) continue;
		QPointF ipos = mapFromNorm(i->norm_pos);
		p.setTransform(src_tr);
		p.translate(ipos);
		p.translate(i->m_offset.x(), -i->m_offset.y());
		p.rotate(i->getRotation());
		if (!i->ignore_scale) p.scale(i->getScale().x(), i->getScale().y());
		if (cur_hints != i->renderHints()) {
			cur_hints = i->renderHints();
			p.setRenderHints(cur_hints);
		}
		i->draw(&p);
		QTransform mtr;
		mtr.rotate(-i->getRotation());
		mtr.translate(-ipos.x(), -ipos.y());
		QPoint m = mtr.map(mouse);
		if (i->m_bounding.contains(m)) {
			hover = i;
		}
	}
	if (hover)
		setCursor(hover->cursor());
	else
		unsetCursor();
}


void MapView::checkZoom() {
	static const double max = 20;
	if (zoom_ < 0.) zoom_ = 0.;
	if (zoom_ > max) zoom_ = max;
	double mins = qMin(width(), height()) / appScale(this);
	scale_      = pow(2., zoom_) * tileSize / mins;
	updateViewRect();
}


void MapView::checkCenter() {
	if (center_.x() < 0.) center_.setX(0.);
	if (center_.x() > 1.) center_.setX(1.);
	if (center_.y() < 0.) center_.setY(0.);
	if (center_.y() > 1.) center_.setY(1.);
}


void MapView::zoomLevelChanged(int new_level) {
	zoom_level = qBound(0, new_level, max_level);
	// qDebug() << "level changed" << new_level << zoom_level;
	tiles_side = 1 << zoom_level;
	// for (int x = 0; x < tiles_side; ++x)
	//	for (int y = 0; y < tiles_side; ++y)
	//		downloader->queueTile(MapViewTypes::TileIndex{zoom_level, x, y});
}


void MapView::updateZoomLevel() {
	int zl = qRound(zoom_);
	if (zl != zoom_level) zoomLevelChanged(zl);
}


void MapView::updateViewRect() {
	double mins = qMin(width(), height());
	if (mins <= 0) return;
	view_rect.setRect(0, 0, (width() / mins) / scale_, (height() / mins) / scale_);
	view_rect.moveCenter(center_);
	for (auto * i: items_)
		i->zoomChanged();
}


void MapView::updateMouse(QPoint mouse) {
	MapItemBase * new_hover = nullptr;
	for (auto * i: items_) {
		if (!i->isVisible() || !i->isInteracive()) continue;
		QPointF ipos = mapFromNorm(i->norm_pos);
		QTransform mtr;
		if (!i->ignore_scale) mtr.scale(1. / i->getScale().x(), 1. / i->getScale().y());
		mtr.rotate(-i->getRotation());
		mtr.translate(-i->m_offset.x(), i->m_offset.y());
		mtr.translate(-ipos.x(), -ipos.y());
		QPoint m = mtr.map(mouse);
		if (i->m_bounding.contains(m)) {
			new_hover = i;
		}
	}
	if (new_hover)
		setCursor(new_hover->cursor());
	else
		unsetCursor();
	if (new_hover != hover) {
		if (hover) emit itemLeaved(hover);
		if (new_hover) emit itemEntered(new_hover);
	}
	hover = new_hover;
}


double MapView::scalePx2M(QPointF norm) {
	double lat = OSM::y2lat(norm.y()) * M_PI / 180.;
	px2m       = qMax(view_rect.width(), view_rect.height()) / qMax(width(), height()) * tileSize;
	px2m *= 156543.03 * cos(lat);
	// qDebug() << px2m << lat;
	return px2m;
}


QPointF MapView::mapToNorm(QPoint screen) const {
	QPointF s(screen.x() / (double)width(), screen.y() / (double)height());
	return view_rect.topLeft() + QPointF(view_rect.width() * s.x(), view_rect.height() * s.y());
}


QPoint MapView::mapFromNorm(QPointF norm) const {
	QPointF s = norm - view_rect.topLeft();
	return QPoint(qRound((s.x() / view_rect.width()) * width()), qRound((s.y() / view_rect.height()) * height()));
}


void MapView::zoom(double factor) {
	zoom(factor, rect().center());
}


void MapView::zoom(double factor, QPoint anchor) {
	QPointF prev_center = mapToNorm(anchor);
	zoom_ += (factor - 1.);
	checkZoom();
	QPointF new_center = mapToNorm(anchor);
	center_ += prev_center - new_center;
	checkCenter();
	view_rect.moveCenter(center_);
	updateZoomLevel();
	drawBackground();
}


void MapView::zoomTo(double level) {
	zoom_ = level;
	checkZoom();
	updateZoomLevel();
	drawBackground();
}


void MapView::centerTo(QPointF coord) {
	center_ = OSM::geo2xy(coord);
	checkCenter();
	updateViewRect();
	drawBackground();
}
