#include "database.h"

using namespace jop;

Database::Database() : db_(NULL), isClosed_(true) {}

Database::~Database() {
	if (!isClosed()) qWarning() << "Database::close() should be called explicitely";
}

void Database::initialize(const QString &path) {
	version_ = -1;
	transactionCount_ = 0;
	logQueries_ = true;

	// QFile::remove(path);

	db_ = new QSqlDatabase(QSqlDatabase::addDatabase("QSQLITE"));
	db_->setDatabaseName(path);

	if  (!db_->open()) {
		qFatal("Error: connection with database fail");
	} else {
		qInfo() << "Database: connection ok";
		isClosed_ = false;
	}

	upgrade();
}

// See https://bugreports.qt.io/browse/QTBUG-35977 for the reason why it's necessary
// to manually destroy the QSqlDatabase instance (i.e. it cannot be done in the
// Database::~Database).
void Database::close() {
	if (db_ && db_->open()) db_->close();
	delete db_;
	db_ = NULL;
	isClosed_ = true;
}

bool Database::isClosed() const {
	return isClosed_;
}

QSqlDatabase* Database::database() const {
	if (isClosed_) qFatal("Database::database: Database is closed");
	return db_;
}

QSqlQuery Database::buildSqlQuery(Database::QueryType type, const QString &tableName, const QStringList &fields, const VariantVector &values, const QString &whereCondition) {
	QString sql;

	if (type == Insert) {
		QString fieldString = "";
		QString valueString = "";
		for (int i = 0; i < fields.length(); i++) {
			QString f = fields[i];
			if (fieldString != "") fieldString += ", ";
			if (valueString != "") valueString += ", ";
			fieldString += QString("`%1`").arg(f);
			valueString += ":" + f;
		}

		sql = QString("INSERT INTO `%1` (%2) VALUES (%3)").arg(tableName).arg(fieldString).arg(valueString);
	} else if (type == Update) {
		QString fieldString = "";
		for (int i = 0; i < fields.length(); i++) {
			QString f = fields[i];
			if (fieldString != "") fieldString += ", ";
			fieldString += QString("`%1`=:%1").arg(f);
		}

		sql = QString("UPDATE `%1` SET %2").arg(tableName).arg(fieldString);
		if (whereCondition != "") sql += " WHERE " + whereCondition;
	}

	QSqlQuery query(*db_);
	bool ok = query.prepare(sql);
	if (!ok) {
		printError(query);
		return query;
	}

	for (int i = 0; i < values.size(); i++) {
		QVariant v = values[i];
		QString fieldName = ":" + fields[i];
		if (v.type() == QVariant::String) {
			query.bindValue(fieldName, v.toString());
		} else if (v.type() == QVariant::Int) {
			query.bindValue(fieldName, v.toInt());
		} else if (v.isNull()) {
			query.bindValue(fieldName, (int)NULL);
		} else if (v.type() == QVariant::Double) {
			query.bindValue(fieldName, v.toDouble());
		} else if (v.type() == (QVariant::Type)QMetaType::Float) {
			query.bindValue(fieldName, v.toFloat());
		} else if (v.type() == QVariant::LongLong) {
			query.bindValue(fieldName, v.toLongLong());
		} else if (v.type() == QVariant::UInt) {
			query.bindValue(fieldName, v.toUInt());
		} else if (v.type() == QVariant::Char) {
			query.bindValue(fieldName, v.toChar());
		} else {
			qWarning() << Q_FUNC_INFO << "Unsupported variant type:" << v.type();
		}
	}

	return query;
}

QSqlQuery Database::buildSqlQuery(Database::QueryType type, const QString &tableName, const QMap<QString, QVariant> &values, const QString &whereCondition) {
	QStringList fields;
	VariantVector fieldValues;
	for (QMap<QString, QVariant>::const_iterator it = values.begin(); it != values.end(); ++it) {
		fields.push_back(it.key());
		fieldValues.push_back(it.value());
	}
	return buildSqlQuery(type, tableName, fields, fieldValues, whereCondition);
}

void Database::printError(const QSqlQuery& query) const {
	if (query.lastError().isValid()) {
		qCritical().noquote() << "SQL error: " << query.lastError().text().trimmed() << ". Query was: " << query.lastQuery();
		QMapIterator<QString, QVariant> i(query.boundValues());
		while (i.hasNext()) {
			i.next();
			qCritical() << i.key() << "=" << i.value().toString();
		}
	}
}

bool Database::errorCheck(const QSqlQuery& query) {
	if (query.lastError().isValid()) {
		printError(query);
		return false;
	}
	return true;
}

bool Database::transaction() {
	transactionCount_++;
	if (transactionCount_ > 1) return true;
	return db_->transaction();
}

bool Database::commit() {
	transactionCount_--;

	if (transactionCount_ < 0) {
		transactionCount_ = 0;
		qCritical() << "Attempting commit on a database that is not in transaction mode";
		return false;
	}

	if (transactionCount_ <= 0) {
		return db_->commit();
	}

	return true;
}

bool Database::execQuery(QSqlQuery &query) {
	if (logQueries_) {
		QString sql = query.lastQuery();
		qDebug().noquote() << "SQL:" << sql;

		QMapIterator<QString, QVariant> i(query.boundValues());
		while (i.hasNext()) {
			i.next();
			qDebug().noquote() << "SQL:" << i.key() << "=" << i.value().toString();
		}
	}

	return query.exec();
}

bool Database::execQuery(const QString &sql) {
	QSqlQuery query(sql, *db_);
	return execQuery(query);
}

QSqlQuery Database::prepare(const QString &sql) {
	QSqlQuery query(*db_);
	query.prepare(sql);
	return query;
}

int Database::version() const {
	if (version_ >= 0) return version_;

	QSqlQuery query = db_->exec("SELECT * FROM version");
	bool result = query.next();
	if (!result) return 0;

	QSqlRecord r = query.record();
	int i_version = r.indexOf("version");

	version_ = query.value(i_version).toInt();
	return version_;
}

QStringList Database::sqlStringToLines(const QString& sql) {
	QStringList statements;
	QStringList lines = sql.split("\n");
	QString statement;
	foreach (QString line, lines) {
		line = line.trimmed();
		if (line == "") continue;
		if (line.left(2) == "--") continue;
		statement += line;
		if (line[line.length() - 1] == ';') {
			statements.append(statement);
			statement = "";
		}
	}
	return statements;
}

void Database::upgrade() {
	// INSTRUCTIONS TO UPGRADE THE DATABASE:
	//
	// 1. Add the new version number to the existingDatabaseVersions array
	// 2. Add the upgrade logic to the "switch (targetVersion)" statement below

	QList<int> existingVersions;
	existingVersions << 1;

	int versionIndex = existingVersions.indexOf(version());
	if (versionIndex == existingVersions.length() - 1) return;

	while (versionIndex < existingVersions.length() - 1) {
		int targetVersion = existingVersions[versionIndex + 1];

		qDebug() << "Upgrading database to version " << targetVersion;

		db_->transaction();

		switch (targetVersion) {

		    case 1:

			    QFile f(":/schema.sql");
				if  (!f.open(QFile::ReadOnly | QFile::Text)) {
					qFatal("Cannot open database schema file");
					return;
				}
				QTextStream in(&f);
				QString schemaSql = in.readAll();

				QStringList lines = sqlStringToLines(schemaSql);
				foreach (const QString& line, lines) {
					db_->exec(line);
				}

			break;

		}

		db_->exec(QString("UPDATE version SET version = %1").arg(targetVersion));
		db_->commit();

		versionIndex++;
	}
}

Database databaseInstance_;

Database& jop::db() {
	return databaseInstance_;
}
