#include <QPainter>
#include <QFileInfo>
#include <QMap>
#include <QDir>
#include <QBuffer>
#include <QImageReader>
#include <QRegularExpression>
#include "common/coordinates.h"
#include "common/rectc.h"
#include "tar.h"
#include "ozf.h"
#include "image.h"
#include "mapfile.h"
#include "gmifile.h"
#include "rectd.h"
#include "tilecache.h"
#include "ozimap.h"


static QString tarFile(const QString &path)
{
	QDir dir(path);
	QFileInfoList files = dir.entryInfoList(QDir::Files);

	for (int i = 0; i < files.size(); i++) {
		const QFileInfo &fi = files.at(i);

		if (fi.suffix().toLower() == "tar")
			return fi.absoluteFilePath();
	}

	return QString();
}

QString OziMap::calibrationFile(const QStringList &files, const QString path,
  CalibrationType &type)
{
	for (int i = 0; i < files.size(); i++) {
		QFileInfo fi(files.at(i));
		QString suffix(fi.suffix().toLower());

		if (path.endsWith(fi.path())) {
			if (suffix == "map") {
				type = MAP;
				return files.at(i);
			} else if (suffix == "gmi") {
				type = GMI;
				return files.at(i);
			}
		}
	}

	type = Unknown;
	return QString();
}

OziMap::OziMap(const QString &fileName, CalibrationType type,
  const Projection &proj, QObject *parent) : Map(fileName, parent), _img(0),
  _tar(0), _ozf(0), _zoom(0), _mapRatio(1.0), _valid(false)
{
	// TAR maps
	if (type == Unknown) {
		_tar = new Tar(fileName);
		if (!_tar->open()) {
			_errorString = _tar->errorString();
			return;
		}
		QStringList files(_tar->files());
		QString cf(calibrationFile(files, ".", type));

		if (type == GMI) {
			QByteArray ba(_tar->file(cf));
			QBuffer buffer(&ba);
			GmiFile gmi(buffer);
			if (!gmi.isValid()) {
				_errorString = gmi.errorString();
				return;
			} else {
				_name = Util::file2name(fileName);
				_map.size = gmi.size();
				_map.path = gmi.image();
				_calibrationPoints = gmi.calibrationPoints();
				_projection = proj;
				computeTransform();
			}
		} else if (type == MAP) {
			QByteArray ba(_tar->file(cf));
			QBuffer buffer(&ba);
			MapFile mf(buffer);
			if (!mf.isValid()) {
				_errorString = mf.errorString();
				return;
			} else {
				_name = mf.name();
				_map.size = mf.size();
				_map.path = mf.image();
				_projection = mf.projection();
				_transform = mf.transform();
			}
		} else {
			_errorString = "No calibration file found";
			return;
		}

		if (!setTileInfo(files))
			return;

		_tar->close();

	// regular MAP or GMI maps
	} else {
		QFile file(fileName);

		if (type == MAP) {
			MapFile mf(file);
			if (!mf.isValid()) {
				_errorString = mf.errorString();
				return;
			} else {
				_name = mf.name();
				_map.size = mf.size();
				_map.path = mf.image();
				_projection = mf.projection();
				_transform = mf.transform();
			}
		} else if (type == GMI) {
			GmiFile gmi(file);
			if (!gmi.isValid()) {
				_errorString = gmi.errorString();
				return;
			} else {
				_name = Util::file2name(fileName);
				_map.size = gmi.size();
				_map.path = gmi.image();
				_calibrationPoints = gmi.calibrationPoints();
				_projection = proj;
				computeTransform();
			}
		}

		QFileInfo fi(fileName);
		QDir set(fi.absolutePath() + "/" + "set");
		if (set.exists()) {
			if (!setTileInfo(set.entryList(), set.absolutePath()))
				return;
		} else {
			if (!setImageInfo(fi.absolutePath()))
				return;
		}
	}

	_valid = true;
}

OziMap::OziMap(const QString &dirName, Tar &tar, const Projection &proj,
  QObject *parent) : Map(dirName, parent), _img(0), _tar(0), _ozf(0), _zoom(0),
  _mapRatio(1.0), _valid(false)
{
	CalibrationType type;
	QString cf(calibrationFile(tar.files(), dirName, type));

	if (type == MAP) {
		QByteArray ba = tar.file(cf);
		QBuffer buffer(&ba);
		MapFile mf(buffer);
		if (!mf.isValid()) {
			_errorString = mf.errorString();
			return;
		}

		_name = mf.name();
		_map.size = mf.size();
		_projection = mf.projection();
		_transform = mf.transform();
	} else if (type == GMI) {
		QByteArray ba = tar.file(cf);
		QBuffer buffer(&ba);
		GmiFile gmi(buffer);
		if (!gmi.isValid()) {
			_errorString = gmi.errorString();
			return;
		}

		_name = Util::file2name(cf);
		_map.size = gmi.size();
		_calibrationPoints = gmi.calibrationPoints();
		_projection = proj;
		computeTransform();
	} else {
		_errorString = "No calibration file found";
		return;
	}

	QString tf(tarFile(dirName));
	if (tf.isNull()) {
		_errorString = "No map tar file found";
		return;
	}
	_tar = new Tar(tf);
	if (!_tar->open()) {
		_errorString = _tar->fileName() + ": " + _tar->errorString();
		return;
	}
	if (!setTileInfo(_tar->files())) {
		_errorString = _tar->fileName() + ": " + _errorString;
		return;
	}
	_tar->close();

	_valid = true;
}

OziMap::~OziMap()
{
	delete _img;
	delete _tar;
	delete _ozf;
}

bool OziMap::setImageInfo(const QString &path)
{
	QFileInfo ii(_map.path);

	if (ii.isRelative())
		ii.setFile(path + "/" + _map.path);

	if (!ii.exists()) {
		int last = _map.path.lastIndexOf('\\');
		if (last >= 0 && last < _map.path.length() - 1) {
			QString fn(_map.path.mid(last + 1, _map.path.length() - last - 1));
			ii.setFile(path + "/" + fn);
		}
	}

	if (ii.exists())
		_map.path = ii.absoluteFilePath();
	else {
		_errorString = QString("%1: No such image file").arg(_map.path);
		return false;
	}

	if (OZF::isOZF(_map.path)) {
		_ozf = new OZF(_map.path);
		if (!_ozf->open()) {
			_errorString = QString("%1: %2").arg(_map.path, _ozf->errorString());
			return false;
		}
		_scale = _ozf->scale(_zoom);
		_ozf->close();
	} else {
		QImageReader ir(_map.path);
		if (!ir.canRead()) {
			_errorString = QString("%1: Unsupported/invalid image file")
			  .arg(_map.path);
			return false;
		}
		_map.size = ir.size();
	}

	return true;
}

bool OziMap::setTileInfo(const QStringList &tiles, const QString &path)
{
	static const QRegularExpression rx("_[0-9]+_[0-9]+\\.");

	if (!_map.size.isValid()) {
		_errorString = "Missing total image size (IWH)";
		return false;
	}

	for (int i = 0; i < tiles.size(); i++) {
		const QString &tile = tiles.at(i);

		if (tile.contains(rx)) {
			QString pattern(QString(tile).replace(rx, "_%1_%2."));

			if (_tar) {
				QByteArray ba(_tar->file(tile));
				QBuffer buffer(&ba);
				_tile.path = pattern;
				_tile.size = QImageReader(&buffer).size();
			} else {
				_tile.path = path + "/" + pattern;
				_tile.size = QImageReader(path + "/" + tile).size();
			}

			if (_tile.size.isValid())
				return true;
			else
				qWarning("%s: error reading tile image", qUtf8Printable(tile));
		}
	}

	_errorString = "Invalid/missing tile set";
	return false;
}

void OziMap::load(const Projection &in, const Projection &out,
  qreal deviceRatio, bool hidpi, bool hillShading, int style, int layer)
{
	Q_UNUSED(out);
	Q_UNUSED(hillShading);
	Q_UNUSED(style);
	Q_UNUSED(layer);

	_mapRatio = hidpi ? deviceRatio : 1.0;
	if (!_calibrationPoints.isEmpty()) {
		_projection = in;
		computeTransform();
	}

	if (_tar) {
		Q_ASSERT(!_tar->isOpen());
		if (!_tar->open()) {
			qWarning("%s: %s", qUtf8Printable(_tar->fileName()),
			  qUtf8Printable(_tar->errorString()));
			return;
		}
	}
	if (_ozf) {
		Q_ASSERT(!_ozf->isOpen());
		if (!_ozf->open()) {
			qWarning("%s: %s", qUtf8Printable(_ozf->fileName()),
			  qUtf8Printable(_ozf->errorString()));
			return;
		}
	}
	if (!_tile.isValid() && !_ozf) {
		Q_ASSERT(!_img);
		_img = new Image(_map.path);
		_img->setDevicePixelRatio(_mapRatio);
	}
}

void OziMap::unload()
{
	delete _img;
	_img = 0;

	if (_tar && _tar->isOpen())
		_tar->close();

	if (_ozf && _ozf->isOpen())
		_ozf->close();
}

void OziMap::drawTiled(QPainter *painter, const QRectF &rect) const
{
	QSizeF ts(_tile.size.width() / _mapRatio, _tile.size.height() / _mapRatio);
	QPointF tl(floor(rect.left() / ts.width()) * ts.width(),
	  floor(rect.top() / ts.height()) * ts.height());

	QSizeF s(rect.right() - tl.x(), rect.bottom() - tl.y());
	for (int i = 0; i < ceil(s.width() / ts.width()); i++) {
		for (int j = 0; j < ceil(s.height() / ts.height()); j++) {
			QPoint xy(round(tl.x() * _mapRatio + i * _tile.size.width()),
			  round(tl.y() * _mapRatio + j * _tile.size.height()));
			QPointF tp(tl.x() + i * ts.width(), tl.y() + j * ts.height());
			QString tileName(_tile.path.arg(QString::number(xy.x()),
			  QString::number(xy.y())));
			QPixmap *pm;

			if (_tar) {
				TileCache::Key key(_tar, 0, xy);
				pm = TileCache::object(key);
				if (!pm) {
					QImage img(QImage::fromData(_tar->file(tileName)));
					if (!img.isNull()) {
						pm = new QPixmap(QPixmap::fromImage(img));
						drawTile(painter, pm, tp);
						TileCache::insert(key, pm);
					}
				} else
					drawTile(painter, pm, tp);
			} else {
				TileCache::Key key(this, 0, xy);
				pm = TileCache::object(key);
				if (!pm) {
					QImage img(tileName);
					if (!img.isNull()) {
						pm = new QPixmap(QPixmap::fromImage(img));
						drawTile(painter, pm, tp);
						TileCache::insert(key, pm);
					}
				} else
					drawTile(painter, pm, tp);
			}
		}
	}
}

void OziMap::drawOZF(QPainter *painter, const QRectF &rect) const
{
	QSizeF ts(_ozf->tileSize().width() / _mapRatio, _ozf->tileSize().height()
	  / _mapRatio);
	QPointF tl(floor(rect.left() / ts.width()) * ts.width(),
	  floor(rect.top() / ts.height()) * ts.height());

	QSizeF s(rect.right() - tl.x(), rect.bottom() - tl.y());
	for (int i = 0; i < ceil(s.width() / ts.width()); i++) {
		for (int j = 0; j < ceil(s.height() / ts.height()); j++) {
			QPoint xy(round(tl.x() * _mapRatio + i * _ozf->tileSize().width()),
			  round(tl.y() * _mapRatio + j * _ozf->tileSize().height()));
			QPointF tp(tl.x() + i * ts.width(), tl.y() + j * ts.height());
			TileCache::Key key(_ozf, _zoom, xy);

			QPixmap *pm = TileCache::object(key);
			if (!pm) {
				pm = _ozf->tile(_zoom, xy);
				if (pm) {
					drawTile(painter, pm, tp);
					TileCache::insert(key, pm);
				}
			} else
				drawTile(painter, pm, tp);
		}
	}
}

void OziMap::drawTile(QPainter *painter, const QPixmap *pixmap,
  const QPointF &tp) const
{
	if (_mapRatio != pixmap->devicePixelRatio()) {
		QPixmap pm(*pixmap);
		pm.setDevicePixelRatio(_mapRatio);
		painter->drawPixmap(tp, pm);
	} else
		painter->drawPixmap(tp, *pixmap);
}

void OziMap::draw(QPainter *painter, const QRectF &rect, Flags flags)
{
	Q_UNUSED(flags);

	if (_ozf)
		drawOZF(painter, rect);
	else if (_img)
		_img->draw(painter, rect, flags);
	else if (_tile.isValid())
		drawTiled(painter, rect);
}

QPointF OziMap::ll2xy(const Coordinates &c)
{
	QPointF p(_transform.proj2img(_projection.ll2xy(c)));
	return _ozf
	  ? QPointF(p.x() * _scale.x(), p.y() * _scale.y()) / _mapRatio
	  : p / _mapRatio;
}

Coordinates OziMap::xy2ll(const QPointF &p)
{
	return _ozf
	  ? _projection.xy2ll(_transform.img2proj(QPointF(p.x() / _scale.x(),
		p.y() / _scale.y()) * _mapRatio))
	  : _projection.xy2ll(_transform.img2proj(p * _mapRatio));
}

QRectF OziMap::bounds()
{
	return _ozf
	  ? QRectF(QPointF(0, 0), _ozf->size(_zoom) / _mapRatio)
	  : QRectF(QPointF(0, 0), _map.size / _mapRatio);
}

int OziMap::zoomFit(const QSize &size, const RectC &rect)
{
	if (!_ozf)
		return _zoom;

	if (!rect.isValid())
		rescale(0);
	else {
		RectD prect(rect, _projection);
		QRectF sbr(_transform.proj2img(prect.topLeft()),
		  _transform.proj2img(prect.bottomRight()));

		for (int i = 0; i < _ozf->zooms(); i++) {
			rescale(i);
			if (sbr.size().width() * _scale.x() <= size.width()
			  && sbr.size().height() * _scale.y() <= size.height())
				break;
		}
	}

	return _zoom;
}

int OziMap::zoomIn()
{
	if (_ozf)
		rescale(qMax(_zoom - 1, 0));

	return _zoom;
}

int OziMap::zoomOut()
{
	if (_ozf)
		rescale(qMin(_zoom + 1, _ozf->zooms() - 1));

	return _zoom;
}

void OziMap::rescale(int zoom)
{
	_zoom = zoom;
	_scale = _ozf->scale(zoom);
}

void OziMap::computeTransform()
{
	QList<ReferencePoint> rp;

	for (int i = 0; i < _calibrationPoints.size(); i++)
		rp.append(_calibrationPoints.at(i).rp(_projection));

	_transform = Transform(rp);
}

Map *OziMap::createTAR(const QString &path, const Projection &proj, bool *isDir)
{
	if (isDir)
		*isDir = false;

	return new OziMap(path, Unknown, proj);
}

Map *OziMap::createMAP(const QString &path, const Projection &proj, bool *isDir)
{
	if (isDir)
		*isDir = false;

	return new OziMap(path, MAP, proj);
}

Map *OziMap::createGMI(const QString &path, const Projection &proj, bool *isDir)
{
	if (isDir)
		*isDir = false;

	return new OziMap(path, GMI, proj);
}
