#include <QFile>
#include <QXmlStreamReader>
#include "onlinemap.h"
#include "wmtsmap.h"
#include "wmsmap.h"
#include "osm.h"
#include "invalidmap.h"
#include "mapsource.h"

MapSource::Config::Config() : type(OSM), zooms(OSM::ZOOMS), bounds(OSM::BOUNDS),
  format("image/png"), rest(false) {}

static CoordinateSystem coordinateSystem(QXmlStreamReader &reader)
{
	QXmlStreamAttributes attr = reader.attributes();
	if (attr.value("axis") == QLatin1String("yx"))
		return CoordinateSystem::YX;
	else if (attr.value("axis") == QLatin1String("xy"))
		return CoordinateSystem::XY;
	else
		return CoordinateSystem::Unknown;
}

Range MapSource::zooms(QXmlStreamReader &reader)
{
	const QXmlStreamAttributes &attr = reader.attributes();
	int min, max;
	bool res;

	if (attr.hasAttribute("min")) {
		min = attr.value("min").toString().toInt(&res);
		if (!res || min < 0) {
			reader.raiseError("Invalid minimal zoom level");
			return Range();
		}
	} else
		min = OSM::ZOOMS.min();

	if (attr.hasAttribute("max")) {
		max = attr.value("max").toString().toInt(&res);
		if (!res || max < min) {
			reader.raiseError("Invalid maximal zoom level");
			return Range();
		}
	} else
		max = OSM::ZOOMS.max();

	return Range(min, max);
}

RectC MapSource::bounds(QXmlStreamReader &reader)
{
	const QXmlStreamAttributes &attr = reader.attributes();
	double top, left, bottom, right;
	bool res;

	if (attr.hasAttribute("top")) {
		top = attr.value("top").toString().toDouble(&res);
		if (!res || (top < OSM::BOUNDS.bottom() || top > OSM::BOUNDS.top())) {
			reader.raiseError("Invalid bounds top value");
			return RectC();
		}
	} else
		top = OSM::BOUNDS.top();

	if (attr.hasAttribute("bottom")) {
		bottom = attr.value("bottom").toString().toDouble(&res);
		if (!res || (bottom < OSM::BOUNDS.bottom()
		  || bottom > OSM::BOUNDS.top())) {
			reader.raiseError("Invalid bounds bottom value");
			return RectC();
		}
	} else
		bottom = OSM::BOUNDS.bottom();

	if (attr.hasAttribute("left")) {
		left = attr.value("left").toString().toDouble(&res);
		if (!res || (left < OSM::BOUNDS.left() || left > OSM::BOUNDS.right())) {
			reader.raiseError("Invalid bounds left value");
			return RectC();
		}
	} else
		left = OSM::BOUNDS.left();

	if (attr.hasAttribute("right")) {
		right = attr.value("right").toString().toDouble(&res);
		if (!res || (right < OSM::BOUNDS.left()
		  || right > OSM::BOUNDS.right())) {
			reader.raiseError("Invalid bounds right value");
			return RectC();
		}
	} else
		right = OSM::BOUNDS.right();

	if (bottom >= top) {
		reader.raiseError("Invalid bottom/top bounds combination");
		return RectC();
	}
	if (left >= right) {
		reader.raiseError("Invalid left/right bounds combination");
		return RectC();
	}

	return RectC(Coordinates(left, top), Coordinates(right, bottom));
}

void MapSource::tile(QXmlStreamReader &reader, Config &config, int layer)
{
	QXmlStreamAttributes attr = reader.attributes();
	bool ok;

	if (layer >= config.urls.size()) {
		reader.raiseError("url/tile count mismatch");
		return;
	}
	Tile &t = config.tiles[layer];

	if (attr.hasAttribute("size")) {
		int size = attr.value("size").toString().toInt(&ok);
		if (!ok || size < 0) {
			reader.raiseError("Invalid tile size");
			return;
		} else
			t.size = size;
	}
	if (attr.hasAttribute("type")) {
		if (attr.value("type") == QLatin1String("raster"))
			t.mvt = false;
		else if (attr.value("type") == QLatin1String("vector"))
			t.mvt = true;
		else {
			reader.raiseError("Invalid tile type");
			return;
		}
	}
	if (attr.hasAttribute("pixelRatio")) {
		qreal ratio = attr.value("pixelRatio").toString().toDouble(&ok);
		if (!ok || ratio < 0) {
			reader.raiseError("Invalid tile pixelRatio");
			return;
		} else
			t.ratio = ratio;
	}
	if (attr.hasAttribute("vectorLayers")) {
		QStringList layers(attr.value("vectorLayers").toString().split(','));
		t.vectorLayers = layers;
	}
}

void MapSource::map(QXmlStreamReader &reader, Config &config)
{
	const QXmlStreamAttributes &attr = reader.attributes();
	QStringView type = attr.value("type");
	int layer = 0;

	if (type == QLatin1String("WMTS"))
		config.type = WMTS;
	else if (type == QLatin1String("WMS"))
		config.type = WMS;
	else if (type == QLatin1String("TMS"))
		config.type = TMS;
	else if (type == QLatin1String("QuadTiles"))
		config.type = QuadTiles;
	else if (type == QLatin1String("OSM") || type.isEmpty())
		config.type = OSM;
	else {
		reader.raiseError("Invalid map type");
		return;
	}

	while (reader.readNextStartElement()) {
		if (reader.name() == QLatin1String("name"))
			config.name = reader.readElementText();
		else if (reader.name() == QLatin1String("url")) {
			config.rest = (reader.attributes().value("type")
			  == QLatin1String("REST")) ? true : false;
			config.urls.append(reader.readElementText());
			config.tiles.append(Tile());
		} else if (reader.name() == QLatin1String("zoom")) {
			config.zooms = zooms(reader);
			reader.skipCurrentElement();
		} else if (reader.name() == QLatin1String("bounds")) {
			config.bounds = bounds(reader);
			reader.skipCurrentElement();
		} else if (reader.name() == QLatin1String("format"))
			config.format = reader.readElementText();
		else if (reader.name() == QLatin1String("layer"))
			config.layer = reader.readElementText();
		else if (reader.name() == QLatin1String("style"))
			config.style = reader.readElementText();
		else if (reader.name() == QLatin1String("set")) {
			config.coordinateSystem = coordinateSystem(reader);
			config.set = reader.readElementText();
		} else if (reader.name() == QLatin1String("dimension")) {
			QXmlStreamAttributes attr = reader.attributes();
			if (!attr.hasAttribute("id"))
				reader.raiseError("Missing dimension id");
			else
				config.dimensions.append(KV<QString, QString>
				  (attr.value("id").toString(), reader.readElementText()));
		} else if (reader.name() == QLatin1String("header")) {
			QXmlStreamAttributes attr = reader.attributes();
			if (!attr.hasAttribute("name"))
				reader.raiseError("Missing header name");
			else
				config.headers.append(HTTPHeader(
				  attr.value("name").toString().toLatin1(),
				  reader.readElementText().toLatin1()));
		} else if (reader.name() == QLatin1String("crs")) {
			config.coordinateSystem = coordinateSystem(reader);
			config.crs = reader.readElementText();
		} else if (reader.name() == QLatin1String("authorization")) {
			QXmlStreamAttributes attr = reader.attributes();
			Authorization auth(attr.value("username").toString(),
			  attr.value("password").toString());
			config.headers.append(auth.header());
			reader.skipCurrentElement();
		} else if (reader.name() == QLatin1String("tile")) {
			tile(reader, config, layer);
			reader.skipCurrentElement();
			layer++;
		} else
			reader.skipCurrentElement();
	}
}

Map *MapSource::create(const QString &path, const Projection &proj, bool *isDir)
{
	Q_UNUSED(proj);
	Config config;
	QFile file(path);

	if (isDir)
		*isDir = false;

	if (!file.open(QFile::ReadOnly | QFile::Text))
		return new InvalidMap(path, file.errorString());

	QXmlStreamReader reader(&file);
	if (reader.readNextStartElement()) {
		if (reader.name() == QLatin1String("map"))
			map(reader, config);
		else
			reader.raiseError("Not an online map source file");
	}
	if (reader.error())
		return new InvalidMap(path, QString("%1: %2").arg(reader.lineNumber())
		  .arg(reader.errorString()));

	if (config.name.isEmpty())
		return new InvalidMap(path, "Missing name definition");
	if (config.urls.isEmpty())
		return new InvalidMap(path, "Missing URL definition");
	if (config.type == WMTS || config.type == WMS) {
		if (config.urls.size() > 1)
			return new InvalidMap(path,
			  "Multiple URLs not allowed for WMS/WMTS maps");
		if (config.layer.isEmpty())
			return new InvalidMap(path, "Missing layer definition");
		if (config.format.isEmpty())
			return new InvalidMap(path, "Missing format definition");
	}
	if (config.type == WMTS) {
		if (config.set.isEmpty())
			return new InvalidMap(path, "Missing set definiton");
	}
	if (config.type == WMS) {
		if (config.crs.isEmpty())
			return new InvalidMap(path, "Missing CRS definiton");
	}

	int tileSize, rasterTileSize = 0, mvtTileSize = 0;
	qreal tileRatio = 0;
	QList<OnlineMap::TileType> tileTypes;
	QStringList vectorLayers;

	tileTypes.reserve(config.tiles.size());

	for (int i = 0; i < config.tiles.size(); i++) {
		const Tile &t = config.tiles.at(i);

		tileTypes.append(t.mvt ? OnlineMap::MVT : OnlineMap::Raster);
		vectorLayers += t.vectorLayers;

		if (t.mvt) {
			mvtTileSize = qMax(mvtTileSize, t.size);
		} else {
			if (rasterTileSize && t.size != rasterTileSize)
				return new InvalidMap(path, "Tile size mismatch");
			if (tileRatio && t.ratio != tileRatio)
				return new InvalidMap(path, "Tile ratio mismatch");
			rasterTileSize = t.size;
			tileRatio = t.ratio;
		}
	}

	if (!tileRatio)
		tileRatio = 1.0;
	tileSize = rasterTileSize ? rasterTileSize : mvtTileSize;

	switch (config.type) {
		case WMTS:
			return new WMTSMap(path, config.name, WMTS::Setup(config.urls.first(),
			  config.layer, config.set, config.style, config.format, config.rest,
			  config.coordinateSystem, config.dimensions, config.headers),
			  tileRatio);
		case WMS:
			return new WMSMap(path, config.name, WMS::Setup(config.urls.first(),
			  config.layer, config.style, config.format, config.crs,
			  config.coordinateSystem, config.dimensions, config.headers),
			  tileSize);
		case TMS:
			return new OnlineMap(path, config.name, config.urls, tileTypes,
			  tileSize, tileRatio, config.zooms, config.bounds, config.headers,
			  true, false, vectorLayers);
		case OSM:
			return new OnlineMap(path, config.name, config.urls, tileTypes,
			  tileSize, tileRatio, config.zooms, config.bounds, config.headers,
			  false, false, vectorLayers);
		case QuadTiles:
			return new OnlineMap(path, config.name, config.urls, tileTypes,
			  tileSize, tileRatio, config.zooms, config.bounds, config.headers,
			  false, true, vectorLayers);
		default:
			return new InvalidMap(path, "Invalid map type");
	}
}
