#include <QPainter>
#include <QPixmapCache>
#include "common/range.h"
#include "common/wgs84.h"
#include "ENC/mapdata.h"
#include "ENC/style.h"
#include "rectd.h"
#include "pcs.h"
#include "encjob.h"
#include "encmap.h"


using namespace ENC;

#define EPSILON   1e-6
#define TILE_SIZE 512

constexpr quint32 SG2D = ISO8211::TAG("SG2D");
constexpr quint32 SG3D = ISO8211::TAG("SG3D");
constexpr quint32 VRID = ISO8211::TAG("VRID");
constexpr quint32 DSID = ISO8211::TAG("DSID");
constexpr quint32 DSPM = ISO8211::TAG("DSPM");

static Range zooms(const RectC &bounds)
{
	double size = qMin(bounds.width(), bounds.height());

	if (size > 180)
		return Range(0, 10);
	else if (size > 90)
		return Range(1, 11);
	else if (size > 45)
		return Range(2, 12);
	else if (size > 22.5)
		return Range(3, 13);
	else if (size > 11.25)
		return Range(4, 14);
	else if (size > 5.625)
		return Range(5, 15);
	else if (size > 2.813)
		return Range(6, 16);
	else if (size > 1.406)
		return Range(7, 17);
	else if (size > 0.703)
		return Range(8, 18);
	else if (size > 0.352)
		return Range(9, 19);
	else if (size > 0.176)
		return Range(10, 20);
	else if (size > 0.088)
		return Range(11, 20);
	else if (size > 0.044)
		return Range(12, 20);
	else if (size > 0.022)
		return Range(13, 20);
	else if (size > 0.011)
		return Range(14, 20);
	else
		return Range(15, 20);
}

static const ISO8211::Field *SGXD(const ISO8211::Record &r)
{
	const ISO8211::Field *f;

	if ((f = r.field(SG2D)))
		return f;
	else if ((f = r.field(SG3D)))
		return f;
	else
		return 0;
}

bool ENCMap::bounds(const ISO8211::Record &record, Rect &rect)
{
	bool xok, yok;
	// edge geometries can be empty!
	const ISO8211::Field *f = SGXD(record);
	if (!f)
		return true;

	for (int i = 0; i < f->data().size(); i++) {
		const QVector<QVariant> &c = f->data().at(i);
		rect.unite(c.at(1).toInt(&xok), c.at(0).toInt(&yok));
		if (!(xok && yok))
			return false;
	}

	return true;
}

bool ENCMap::bounds(const QVector<ISO8211::Record> &gv, Rect &b)
{
	Rect r;

	for (int i = 0; i < gv.size(); i++) {
		if (!bounds(gv.at(i), r))
			return false;
		b |= r;
	}

	return true;
}

bool ENCMap::processRecord(const ISO8211::Record &record,
  QVector<ISO8211::Record> &rv, uint &comf, QByteArray &dsnm)
{
	if (record.size() < 2)
		return false;

	const ISO8211::Field &f = record.at(1);
	quint32 tag = f.tag();

	if (tag == VRID) {
		rv.append(record);
	} else if (tag == DSID) {
		if (f.data().at(0).size() < 5)
			return false;
		dsnm = f.data().at(0).at(4).toByteArray();
	} else if (tag == DSPM) {
		bool ok;
		if (f.data().at(0).size() < 11)
			return false;
		comf = f.data().at(0).at(10).toUInt(&ok);
		if (!ok)
			return false;
	}

	return true;
}

ENCMap::ENCMap(const QString &fileName, QObject *parent)
  : Map(fileName, parent), _data(0), _style(0), _projection(PCS::pcs(3857)),
  _tileRatio(1.0), _valid(false)
{
	QVector<ISO8211::Record> gv;
	ISO8211 ddf(fileName);
	ISO8211::Record record;
	uint comf = 1;
	QByteArray dsnm;

	if (!ddf.readDDR()) {
		_errorString = ddf.errorString();
		return;
	}
	while (!ddf.atEnd()) {
		if (!ddf.readRecord(record)) {
			_errorString = ddf.errorString();
			return;
		}
		if (!processRecord(record, gv, comf, dsnm)) {
			_errorString = "Invalid S-57 record";
			return;
		}
	}

	_name = dsnm;

	Rect b;
	if (!bounds(gv, b)) {
		_errorString = "Error fetching geometries bounds";
		return;
	}
	Coordinates tl(b.minX() / (double)comf, b.maxY() / (double)comf);
	Coordinates br(b.maxX() / (double)comf, b.minY() / (double)comf);
	_llBounds = RectC(tl, br);
	if (!_llBounds.isValid()) {
		_errorString = "Invalid geometries bounds";
		return;
	}

	_zooms = zooms(_llBounds);
	_zoom = _zooms.min();
	updateTransform();

	_valid = true;
}

ENCMap::~ENCMap()
{
	delete _data;
	delete _style;
}

void ENCMap::load(const Projection &in, const Projection &out,
  qreal deviceRatio, bool hidpi)
{
	Q_UNUSED(in);
	Q_UNUSED(hidpi);

	_tileRatio = deviceRatio;
	_projection = out;
	Q_ASSERT(!_data);
	_data = new MapData(path());
	Q_ASSERT(!_style);
	_style = new Style(deviceRatio);

	QPixmapCache::clear();
}

void ENCMap::unload()
{
	cancelJobs(true);

	delete _data;
	_data = 0;
	delete _style;
	_style = 0;
}

int ENCMap::zoomFit(const QSize &size, const RectC &rect)
{
	if (rect.isValid()) {
		RectD pr(rect, _projection, 10);

		_zoom = _zooms.min();
		for (int i = _zooms.min() + 1; i <= _zooms.max(); i++) {
			Transform t(transform(i));
			QRectF r(t.proj2img(pr.topLeft()), t.proj2img(pr.bottomRight()));
			if (size.width() + EPSILON < r.width()
			  || size.height() + EPSILON < r.height())
				break;
			_zoom = i;
		}
	} else
		_zoom = _zooms.max();

	updateTransform();

	return _zoom;
}

int ENCMap::zoomIn()
{
	cancelJobs(false);

	_zoom = qMin(_zoom + 1, _zooms.max());
	updateTransform();
	return _zoom;
}

int ENCMap::zoomOut()
{
	cancelJobs(false);

	_zoom = qMax(_zoom - 1, _zooms.min());
	updateTransform();
	return _zoom;
}

void ENCMap::setZoom(int zoom)
{
	_zoom = zoom;
	updateTransform();
}

Transform ENCMap::transform(int zoom) const
{
	int z = zoom + Util::log2i(TILE_SIZE);

	double scale = _projection.isGeographic()
	  ? 360.0 / (1<<z) : (2.0 * M_PI * WGS84_RADIUS) / (1<<z);
	PointD topLeft(_projection.ll2xy(_llBounds.topLeft()));
	return Transform(ReferencePoint(PointD(0, 0), topLeft),
	  PointD(scale, scale));
}

void ENCMap::updateTransform()
{
	_transform = transform(_zoom);

	RectD prect(_llBounds, _projection);
	_bounds = QRectF(_transform.proj2img(prect.topLeft()),
	  _transform.proj2img(prect.bottomRight()));
}

bool ENCMap::isRunning(int zoom, const QPoint &xy) const
{
	for (int i = 0; i < _jobs.size(); i++) {
		const QList<ENC::RasterTile> &tiles = _jobs.at(i)->tiles();
		for (int j = 0; j < tiles.size(); j++) {
			const ENC::RasterTile &mt = tiles.at(j);
			if (mt.zoom() == zoom && mt.xy() == xy)
				return true;
		}
	}

	return false;
}

void ENCMap::runJob(ENCJob *job)
{
	_jobs.append(job);

	connect(job, &ENCJob::finished, this, &ENCMap::jobFinished);
	job->run();
}

void ENCMap::removeJob(ENCJob *job)
{
	_jobs.removeOne(job);
	job->deleteLater();
}

void ENCMap::jobFinished(ENCJob *job)
{
	const QList<ENC::RasterTile> &tiles = job->tiles();

	for (int i = 0; i < tiles.size(); i++) {
		const ENC::RasterTile &mt = tiles.at(i);
		if (!mt.pixmap().isNull())
			QPixmapCache::insert(key(mt.zoom(), mt.xy()), mt.pixmap());
	}

	removeJob(job);

	emit tilesLoaded();
}

void ENCMap::cancelJobs(bool wait)
{
	for (int i = 0; i < _jobs.size(); i++)
		_jobs.at(i)->cancel(wait);
}

QString ENCMap::key(int zoom, const QPoint &xy) const
{
	return path() + "-" + QString::number(zoom) + "_"
	  + QString::number(xy.x()) + "_" + QString::number(xy.y());
}

void ENCMap::draw(QPainter *painter, const QRectF &rect, Flags flags)
{
	Q_UNUSED(flags);
	QPointF tl(floor(rect.left() / TILE_SIZE) * TILE_SIZE,
	  floor(rect.top() / TILE_SIZE) * TILE_SIZE);
	QSizeF s(rect.right() - tl.x(), rect.bottom() - tl.y());
	int width = ceil(s.width() / TILE_SIZE);
	int height = ceil(s.height() / TILE_SIZE);

	QList<RasterTile> tiles;

	for (int i = 0; i < width; i++) {
		for (int j = 0; j < height; j++) {
			QPoint ttl(tl.x() + i * TILE_SIZE, tl.y() + j * TILE_SIZE);
			if (isRunning(_zoom, ttl))
				continue;

			QPixmap pm;
			if (QPixmapCache::find(key(_zoom, ttl), &pm))
				painter->drawPixmap(ttl, pm);
			else
				tiles.append(RasterTile(_projection, _transform, _style, _data,
				  _zoom, _zooms, QRect(ttl, QSize(TILE_SIZE, TILE_SIZE)),
				  _tileRatio));
		}
	}

	if (!tiles.isEmpty()) {
		if (flags & Map::Block) {
			QFuture<void> future = QtConcurrent::map(tiles, &RasterTile::render);
			future.waitForFinished();

			for (int i = 0; i < tiles.size(); i++) {
				const RasterTile &mt = tiles.at(i);
				const QPixmap &pm = mt.pixmap();
				painter->drawPixmap(mt.xy(), pm);
				QPixmapCache::insert(key(mt.zoom(), mt.xy()), pm);
			}
		} else
			runJob(new ENCJob(tiles));
	}
}

Map *ENCMap::create(const QString &path, const Projection &proj, bool *isMap)
{
	Q_UNUSED(proj);

	if (isMap)
		*isMap = false;

	return new ENCMap(path);
}
