#include <QPainter>
#include <QPixmapCache>
#include "common/wgs84.h"
#include "GUI/format.h"
#include "rectd.h"
#include "pcs.h"
#include "encjob.h"
#include "encatlas.h"

using namespace ENC;

#define EPSILON   1e-6
#define TILE_SIZE 512

constexpr quint32 CATD = ISO8211::TAG("CATD");

Range ENCAtlas::zooms(IntendedUsage usage)
{
	switch (usage) {
		case Overview:
			return Range(6, 7);
		case General:
			return Range(8, 9);
		case Coastal:
			return Range(10, 11);
		case Approach:
			return Range(12, 13);
		case Harbour:
			return Range(14, 18);
		case Berthing:
			return Range(19, 19);

		case River:
			return Range(12, 17);
		case RiverHarbour:
			return Range(18, 18);
		case RiverBerthing:
			return Range(19, 19);
		default:
			return Range(0, 19);
	};
}

ENCAtlas::IntendedUsage ENCAtlas::usage(const QString &path)
{
	QFileInfo fi(path);
	QString basename(fi.baseName());

	if (basename.size() != 8)
		return Unknown;
	int iu = basename.at(2).digitValue();
	if (iu < 1 || iu > 9)
		return Unknown;

	return (IntendedUsage)iu;
}

bool ENCAtlas::processRecord(const ISO8211::Record &record, QByteArray &file,
  RectC &bounds)
{
	if (record.size() < 2)
		return false;

	const ENC::ISO8211::Field &field = record.at(1);

	if (field.tag() == CATD) {
		if (field.data().at(0).size() < 10)
			return false;
		QByteArray impl = field.data().at(0).at(5).toByteArray();
		file = field.data().at(0).at(2).toByteArray();

		if (impl == "BIN" && file.endsWith("000")) {
			QByteArray slat = field.data().at(0).at(6).toByteArray();
			QByteArray wlon = field.data().at(0).at(7).toByteArray();
			QByteArray nlat = field.data().at(0).at(8).toByteArray();
			QByteArray elon = field.data().at(0).at(9).toByteArray();

			bool ok1, ok2, ok3, ok4;
			bounds = RectC(Coordinates(wlon.toDouble(&ok1), nlat.toDouble(&ok2)),
			  Coordinates(elon.toDouble(&ok3), slat.toDouble(&ok4)));
			if (!(ok1 && ok2 && ok3 && ok4))
				return false;

			file.replace('\\', '/');

			return true;
		}
	}

	return false;
}

void ENCAtlas::addMap(const QDir &dir, const QByteArray &file,
  const RectC &bounds)
{
	QString path(dir.absoluteFilePath(file));
	if (!QFileInfo::exists(path)) {
		qWarning("%s: No such map file", qUtf8Printable(path));
		return;
	}
	if (!bounds.isValid()) {
		qWarning("%s: Invalid map bounds", qUtf8Printable(path));
		return;
	}

	IntendedUsage iu = usage(path);
	auto it = _data.find(iu);
	if (it == _data.end())
		it = _data.insert(iu, new AtlasData(_cache, _cacheLock));

	it.value()->addMap(bounds, path);

	_llBounds |= bounds;
}

ENCAtlas::ENCAtlas(const QString &fileName, QObject *parent)
  : Map(fileName, parent), _projection(PCS::pcs(3857)),  _tileRatio(1.0),
  _style(0), _zoom(0), _valid(false)
{
	QDir dir(QFileInfo(fileName).absoluteDir());
	ISO8211 ddf(fileName);
	ISO8211::Record record;
	QByteArray file;
	RectC bounds;

	if (!ddf.readDDR()) {
		_errorString = ddf.errorString();
		return;
	}
	while (!ddf.atEnd()) {
		if (!ddf.readRecord(record)) {
			_errorString = ddf.errorString();
			return;
		}
		if (processRecord(record, file, bounds))
			addMap(dir, file, bounds);
	}

	if (_data.isEmpty()) {
		_errorString = "No usable ENC map found";
		return;
	}

	_name = "ENC (" + Format::coordinates(_llBounds.center(), DecimalDegrees)
	  + ")";
	_usage = _data.firstKey();
	_zoom = zooms(_usage).min();
	updateTransform();

	_cache.setMaxCost(16);

	_valid = true;
}

ENCAtlas::~ENCAtlas()
{
	qDeleteAll(_data);
	delete _style;
}

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

	_tileRatio = deviceRatio;
	_projection = out;

	Q_ASSERT(!_style);
	_style = new Style(deviceRatio);

	QPixmapCache::clear();
}

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

	_cache.clear();

	delete _style;
	_style = 0;
}

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

		for (auto it = _data.cbegin(); it != _data.cend(); ++it) {
			Range z(zooms(it.key()));

			_usage = it.key();
			_zoom = z.min();

			for (int i = z.min() + 1; i <= z.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()) {
					updateTransform();
					return _zoom;
				}

				_zoom = i;
			}
		}
	} else {
		IntendedUsage usage(_data.lastKey());
		_usage = usage;
		_zoom = zooms(usage).max();
	}

	updateTransform();

	return _zoom;
}

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

	if (_zoom + 1 <= zooms(_usage).max())
		_zoom++;
	else {
		auto it = _data.find(_usage);
		if (++it != _data.end()) {
			_usage = it.key();
			_zoom = zooms(it.key()).min();
		}
	}

	updateTransform();

	return _zoom;
}

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

	if (_zoom - 1 >= zooms(_usage).min())
		_zoom--;
	else {
		auto it = _data.find(_usage);
		if (it != _data.begin()) {
			--it;
			_usage = it.key();
			_zoom = zooms(it.key()).max();
		}
	}

	updateTransform();

	return _zoom;
}

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

Transform ENCAtlas::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 ENCAtlas::updateTransform()
{
	_transform = transform(_zoom);

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

bool ENCAtlas::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 ENCAtlas::runJob(ENCJob *job)
{
	_jobs.append(job);

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

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

void ENCAtlas::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 ENCAtlas::cancelJobs(bool wait)
{
	for (int i = 0; i < _jobs.size(); i++)
		_jobs.at(i)->cancel(wait);
}

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

QList<Data*> ENCAtlas::levels() const
{
	QList<Data*> list;
	QMap<IntendedUsage, ENC::AtlasData*>::const_iterator it = _data.find(_usage);

	do {
		list.append(it.value());
	} while (it-- != _data.cbegin());

	return list;
}

void ENCAtlas::draw(QPainter *painter, const QRectF &rect, Flags flags)
{
	QList<Data*> data(levels());
	Range zr(zooms(_usage));
	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, zr, 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 *ENCAtlas::create(const QString &path, const Projection &proj, bool *isDir)
{
	Q_UNUSED(proj);

	if (isDir)
		*isDir = true;

	return new ENCAtlas(path);
}
