#include "variables_database.h"
#include <QDebug>
#include <QFile>
#include <QFileInfo>
#include <QDateTime>
#include <QDir>
#include <QSqlQuery>
#include <QSqlRecord>
#include <QSqlError>
#include "SH_server_base.h"
#include "script_engine.h"
#include "history_database_sqlite.h"
#include "history_database_influxdb.h"
#include <qad_locations.h>


SHSVariablesDatabase::SHSVariablesDatabase(QObject * parent): QThread(parent) {
	QDir().mkdir("projects");
	is_run = is_ready = false;
	QString confpath = QAD::userPath(QAD::ltConfig, "server");
	if (QFile::exists(confpath)) {
		QPIConfig conf(confpath, QIODevice::ReadOnly);
		QString dbs = conf.getValue("variables_database", "sqlite").toString();
		QString influx_addr = conf.getValue("influxdb_address", "127.0.0.1:8086").toString();
		createHistoryDatabaseFromString(dbs, influx_addr);
		QString bdbs = conf.getValue("variables_database_second", "").toString();
		if (dbs.trimmed() != bdbs.trimmed()) {
			createHistoryDatabaseFromString(bdbs, influx_addr);
		}
		for (auto history_db : history_dbs) qDebug() << history_db->objectName();
	} else {
		history_dbs << new SHSHistoryDatabaseSQLite();
	}
	if (!history_dbs.isEmpty()) {
		connect(history_dbs.first(), SIGNAL(readHistoryRequest()), this, SLOT(readVariablesHistory()), Qt::DirectConnection);
	}
	for (auto history_db: history_dbs) connect(history_db, SIGNAL(readHistoryProgress()), this, SLOT(resetStartTM()), Qt::DirectConnection);
}


SHSVariablesDatabase::~SHSVariablesDatabase() {
	stop();
	for (auto history_db : history_dbs) {
		delete history_db;
	}
}


void SHSVariablesDatabase::run() {
	db_vars = QSqlDatabase::addDatabase("QSQLITE", "VariablesDB");
	init();
	is_ready = true;
	//qDebug() << "SHSVariablesDatabase start" << (qulonglong)QThread::currentThreadId();
	while (is_run) {
		if (!db_vars.isOpen()) {
			piMSleep(1000);
			continue;
		}
		db_mutex.lock();
		QQueue<QPair<int, QVariant>> csq = db_script_queue;
		QMap<int, double> ccv = changed_vars;
		csq.detach();
		ccv.detach();
		db_script_queue.clear();
		changed_vars.clear();
		db_mutex.unlock();
		if (csq.isEmpty() && ccv.isEmpty()) {
			piMSleep(100);
			continue;
		}
		QSqlQuery q(db_vars);
		db_vars.transaction();
		while (!csq.isEmpty()) {
			QPair<int, QVariant> e = csq.dequeue();
			q.exec(QString("INSERT INTO script_variables (id) VALUES (%1)").arg(e.first));
			q.prepare(QString("UPDATE script_variables SET value=:v WHERE id=%1").arg(e.first));
			q.bindValue(":v", e.second);
			if (!q.exec()) {
				if (q.lastError().isValid())
					qDebug() << "save sv" << q.lastError();
			}
		}
		QMapIterator<int, double> it(ccv);
		while (it.hasNext()) {
			it.next();
			q.prepare(QString("UPDATE variables SET value=:v WHERE id=%1").arg(it.key()));
			q.bindValue(":v", it.value());
			if (!q.exec()) {
				if (q.lastError().isValid())
					qDebug() << "save cv" << q.lastError();
			}
		}
		db_vars.commit();

		piMSleep(100);
	}
	db_vars.close();
	db_vars = QSqlDatabase();
	QSqlDatabase::removeDatabase("VariablesDB");
}


void SHSVariablesDatabase::resetStartTM() {
	tm_mutex.lock();
	start_tm.reset();
	tm_mutex.unlock();
}


double SHSVariablesDatabase::elapsedStartTM() {
	tm_mutex.lock();
	double ret = start_tm.elapsed_s();
	tm_mutex.unlock();
	return ret;
}


void SHSVariablesDatabase::init() {
	db_filename = "unnamed";
	QString pn = engine_->project.property("name").toString();
	if (!pn.isEmpty()) db_filename = pn;
	QDir::current().mkpath("projects");
	db_vars.setDatabaseName("projects/" + db_filename + ".dat");
	if (!db_vars.open())
		qDebug() << "Error while open variables database" << db_vars.databaseName();
	else
		qDebug() << "Open db vars" << db_vars.databaseName() << "OK";
	db_vars.transaction();
	QSqlQuery q(db_vars);
	if (!db_vars.tables().contains("variables")) {
		q.exec(QString("CREATE TABLE variables (id integer primary key, name varchar(256), desc varchar(1024), value real)"));
		if (q.lastError().isValid()) qDebug() << q.lastError();
	}
	if (!db_vars.tables().contains("script_variables")) {
		q.exec(QString("CREATE TABLE script_variables (id integer primary key, value blob)"));
		if (q.lastError().isValid()) qDebug() << q.lastError();
	}
	resetStartTM();
	QStringList new_vars;
	for (const auto & i: vars)
		new_vars << i.name;
	//qDebug() << vars_;
	vars_ids.clear();
	q.exec(QString("SELECT * FROM variables"));
	while (q.next()) {
		QSqlRecord r = q.record();
		QString vn = r.value(1).toString();
		if (new_vars.contains(vn)) {
			new_vars.removeOne(vn);
			setVarIdValue(vn, r.value(0).toInt(), r.value(3).toDouble());
		}
	}
	foreach (QString vn, new_vars) {
		q.exec(QString("INSERT INTO variables (name,desc,value) VALUES ('%1','%2',0)").arg(vn, ""));
		setVarIdValue(vn, q.lastInsertId().toInt(), 0.);
	}
	db_vars.commit();
	resetStartTM();
	QMap<int, SHSHistoryDatabaseBase::Variable> pvars = vars;
	vars.clear();
	for (const auto & i: pvars) {
		vars[i.id] = i;
		//qDebug() << i.id << i.name << i.duration << i.value;
	}

	script_vars.clear();
	q.exec(QString("SELECT * FROM script_variables"));
	while (q.next()) {
		QSqlRecord r = q.record();
		script_vars[r.value(0).toInt()] = r.value(1);
	}

	if (db_vars.tables().contains("history")) {
		is_migrating = true;
		qDebug() << "[SHSVariablesDatabase] Migrating from old history database ...";
		if (migrateOldHistory(db_vars)) {
			qDebug() << "[SHSVariablesDatabase] Migrate success";
		} else {
			qDebug() << "[SHSVariablesDatabase] Migrate failed";
		}
		is_migrating = false;
	}
	for (auto history_db : history_dbs) {
		resetStartTM();
		history_db->setVariables(vars.values());
		history_db->start(db_filename);
	}
}


void SHSVariablesDatabase::setVarIdValue(QString vn, int id, double value) {
	QMutableMapIterator<int, SHSHistoryDatabaseBase::Variable> it(vars);
	while (it.hasNext()) {
		it.next();
		if (it.value().name == vn) {
			vars_ids[vn] = id;
			it.value().id = id;
			it.value().value = value;
			break;
		}
	}
}


int SHSVariablesDatabase::variableID(const QString & name) const {
	return vars_ids.value(name, -1);
}


double SHSVariablesDatabase::variableValue(int id) const {
	if (id < 0) return 0.;
	QMutexLocker ml(&db_mutex);
	return vars[id].value;
}


void SHSVariablesDatabase::setVariableValue(int id, double value) {
	if (id < 0) return;
	//qDebug() << "setVariableValue" << id << value;
	QDateTime cdt = QDateTime::currentDateTime();
	qint64 t = cdt.toMSecsSinceEpoch();
	db_mutex.lock();
	SHSHistoryDatabaseBase::Variable & var(vars[id]);
	QPolygonF & hist(var.history);
	var.value = value;
	hist << QPointF(t, value);
	if (var.duration == 1) hist.clear();
	qint64 del_time = t - var.duration;
	for (int i = 0; i < hist.size(); ++i) {
		if (hist[i].x() >= del_time) {
			if (i > 0)
				hist.remove(0, i);
			break;
		}
	}
	changed_vars[id] = value;
	db_mutex.unlock();
	for (auto history_db : history_dbs) {
		history_db->add(id, t, value);
	}
	emit engine_->variableHistoryAdded(vars[id].name, QPair<QDateTime, double>(cdt, value));
	//qDebug() << "setVariableValue done" << id << value;
}


QVariant SHSVariablesDatabase::scriptVariableValue(int id) const {
	QMutexLocker ml(&db_mutex);
	return script_vars[id];
}


void SHSVariablesDatabase::setScriptVariableValue(int id, QVariant value) {
	QMutexLocker ml(&db_mutex);
	db_script_queue.enqueue(QPair<int, QVariant>(id, value));
	script_vars[id] = value;
}


bool SHSVariablesDatabase::migrateOldHistory(QSqlDatabase & old_db) {
	QSqlDatabase new_db = QSqlDatabase::addDatabase("QSQLITE", "HistoryDB");
	new_db.setDatabaseName("projects/" + db_filename + "_history.dat");
	if (!new_db.open()) {
		qDebug() << "Error while open new database" << new_db.databaseName();
		return false;
	}

	QSqlQuery oq(old_db);
	QSqlQuery nq(new_db);
	new_db.transaction();
	QList<int> var_ids = vars.keys();
	for (int id: var_ids) {
		nq.exec(QString("CREATE TABLE var%1 (time integer(8) primary key, value real)").arg(id));
		if (nq.lastError().isValid()) qDebug() << nq.lastError();
		oq.exec(QString("SELECT time,value FROM history WHERE vind=%1").arg(id)); // (id integer primary key, vind integer, time integer(8), value real)"));
		if (oq.lastError().isValid()) qDebug() << oq.lastError();
		while (oq.next()) {
			nq.prepare(QString("INSERT INTO var%1 (time,value) VALUES (:t,:v)").arg(id));
			nq.bindValue(":t", oq.value(0));
			nq.bindValue(":v", oq.value(1));
			nq.exec();
		}
	}
	new_db.commit();
	oq.exec(QString("DROP TABLE history"));
	oq.exec(QString("VACUUM"));

	new_db.close();
	new_db = QSqlDatabase();
	QSqlDatabase::removeDatabase("HistoryDB");
	return true;
}


void SHSVariablesDatabase::readVariablesHistory() {
	if (history_dbs.isEmpty()) return;
	QMutableMapIterator<int, SHSHistoryDatabaseBase::Variable> it(vars);
	while (it.hasNext()) {
		it.next();
		it.value().history = history_dbs.first()->readHistory(it.key());
	}
}


void SHSVariablesDatabase::assignEngine(SHSScriptEngine * e, const QList<SHS::ScriptVariable> & in_vars) {
	engine_ = e;
	vars.clear();
	if (!e) {
		qDebug() << "[SHSVariablesDatabase] Error: Invalid engine!";
		return;
	}
	for (const auto & i: in_vars) {
		SHSHistoryDatabaseBase::Variable v;
		v.name = i.name;
		v.duration = i.store_msecs;
		vars[vars.size()] = v; // temporary map
	}
	resetStartTM();
	start();
	while (elapsedStartTM() < 60.) {
		if (is_ready)
			break;
		if (is_migrating)
			resetStartTM();
		piMSleep(10);
	}
	if (!is_ready) {
		qDebug() << "[SHSVariablesDatabase] Error: can`t init database!";
		stop();
		return;
	}
}


void SHSVariablesDatabase::start() {
	stop();
	is_run = true;
	is_ready = is_migrating = false;
	QThread::start();
}


void SHSVariablesDatabase::stop() {
	for (auto history_db : history_dbs) {
		history_db->stop();
	}
	if (!isRunning()) return;
	is_run = is_ready = is_migrating = false;
	if (!wait(5000))
		terminate();
}


bool SHSVariablesDatabase::isHistoryDBRunning() const {
	for (auto history_db : history_dbs) {
		if (!history_db->isRunning()) return false;
	}
	return true;
}


PIVector<PIPair<ullong, double>> SHSVariablesDatabase::getVariableHistory(const QString & vname) {
	PIVector<PIPair<ullong, double>> ret;
	int id = vars_ids.value(vname.trimmed(), -1);
	if (id < 0) return ret;
	QMutexLocker ml(&db_mutex);
	const QPolygonF & hist(vars[id].history);
	ret.reserve(hist.size());
	foreach (const QPointF & p, hist)
		ret << PIPair<ullong, double>(p.x(), p.y());
	return ret;
}


PIPair<ullong, double> SHSVariablesDatabase::getVariableValue(const QString & vname) {
	PIPair<ullong, double> ret(0, 0.);
	int id = vars_ids.value(vname.trimmed(), -1);
	if (id < 0) return ret;
	QMutexLocker ml(&db_mutex);
	const QPolygonF & hist(vars[id].history);
	if (hist.isEmpty()) return ret;
	ret.first = hist.back().x();
	ret.second = hist.back().y();
	return ret;
}


void SHSVariablesDatabase::createHistoryDatabaseFromString(QString db, QString influx_addr) {
	if (db.trimmed() == "sqlite") history_dbs << new SHSHistoryDatabaseSQLite();
	else if (db.trimmed() == "influxdb") history_dbs << new SHSHistoryDatabaseInfluxDB(influx_addr);
}

