#pragma once

#include <iostream>
#include <stdexcept>

// JSON-Schema includes
#include <utils/jsonschema/QJsonSchemaChecker.h>

#include <QFile>
#include <QString>
#include <QJsonObject>
#include <QJsonDocument>
#include <QRegularExpression>

class QJsonFactory
{
public:

	static int load(const QString& schema, const QString& config, QJsonObject& json)
	{
		// Load the schema and the config trees
		QJsonObject schemaTree = readSchema(schema);
		QJsonObject configTree = readConfig(config);

		// create the validator
		QJsonSchemaChecker schemaChecker;
		schemaChecker.setSchema(schemaTree);

		QStringList messages = schemaChecker.getMessages();

		if (!schemaChecker.validate(configTree).first)
		{
			for (int i = 0; i < messages.size(); ++i)
				std::cout << messages[i].toStdString() << std::endl;

			std::cerr << "Validation failed for configuration file: " << config.toStdString() << std::endl;
			return -3;
		}

		json = configTree;
		return 0;
	}

	static QJsonObject readConfig(const QString& path)
	{
		QFile file(path);
		QJsonParseError error;

		if (!file.open(QIODevice::ReadOnly))
		{
			throw std::runtime_error(QString("Configuration file not found: '" + path + "' ("  +  file.errorString() + ")").toStdString());
		}

		//Allow Comments in Config
		QString config = QString(file.readAll());
		config.remove(QRegularExpression("([^:]?\\/\\/.*)"));

		QJsonDocument doc = QJsonDocument::fromJson(config.toUtf8(), &error);
		file.close();

		if (error.error != QJsonParseError::NoError)
		{
			// report to the user the failure and their locations in the document.
			int errorLine(0), errorColumn(0);

			for( int i=0, count=qMin( error.offset,config.size()); i<count; ++i )
			{
				++errorColumn;
				if(config.at(i) == '\n' )
				{
					errorColumn = 0;
					++errorLine;
				}
			}

			throw std::runtime_error (
				QString("Failed to parse configuration: " + error.errorString() + " at Line: " + QString::number(errorLine) + ", Column: " + QString::number(errorColumn)).toStdString()
			);
		}

		return doc.object();
	}

	static QJsonObject readSchema(const QString& path)
	{
		QFile schemaData(path);
		QJsonParseError error;

		if (!schemaData.open(QIODevice::ReadOnly))
		{
			throw std::runtime_error(QString("Schema not found: '" + path + "' (" + schemaData.errorString() + ")").toStdString());
		}

		QByteArray schema = schemaData.readAll();
		QJsonDocument doc = QJsonDocument::fromJson(schema, &error);
		schemaData.close();

		if (error.error != QJsonParseError::NoError)
		{
			// report to the user the failure and their locations in the document.
			int errorLine(0), errorColumn(0);

			for( int i=0, count=qMin( error.offset,schema.size()); i<count; ++i )
			{
				++errorColumn;
				if(schema.at(i) == '\n' )
				{
					errorColumn = 0;
					++errorLine;
				}
			}

			throw std::runtime_error(QString("ERROR: Json schema wrong: " + error.errorString() +
											" at Line: " + QString::number(errorLine) +
											", Column: " + QString::number(errorColumn)).toStdString());
		}

		return resolveReferences(doc.object());
	}

	static QJsonObject resolveReferences(const QJsonObject& schema)
	{
		QJsonObject result;

		for (QJsonObject::const_iterator i = schema.begin(); i != schema.end(); ++i)
		{
			QString attribute = i.key();
			const QJsonValue & attributeValue = *i;

			if (attribute == "$ref" && attributeValue.isString())
			{
				try
				{
					result = readSchema(":/" + attributeValue.toString());
				}
				catch (std::runtime_error& error)
				{
					throw std::runtime_error(error.what());
				}
			}
			else if (attributeValue.isObject())
				result.insert(attribute, resolveReferences(attributeValue.toObject()));
			else
				result.insert(attribute, attributeValue);
		}

		return result;
	}

	static bool writeJson(const QString& filename, QJsonObject& jsonTree)
	{
		QJsonDocument doc;

		doc.setObject(jsonTree);
		QByteArray configData = doc.toJson(QJsonDocument::Indented);

		QFile configFile(filename);
		if (!configFile.open(QFile::WriteOnly | QFile::Truncate))
			return false;

		configFile.write(configData);

		QFile::FileError error = configFile.error();
		if (error != QFile::NoError)
			return false;

		configFile.close();

		return true;
	}
};