/*
 * Copyright (C) 2014-2024 CZ.NIC
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * In addition, as a special exception, the copyright holders give
 * permission to link the code of portions of this program with the
 * OpenSSL library under certain conditions as described in each
 * individual source file, and distribute linked combinations including
 * the two.
 */

#include <QMutexLocker>
#include <QSqlError>
#include <QSqlQuery>

#include "src/datovka_shared/compat/compiler.h" /* macroStdMove */
#include "src/datovka_shared/compat_qt/variant.h" /* nullVariantWhenIsNull */
#include "src/datovka_shared/graphics/colour.h"
#include "src/datovka_shared/io/prefs_db.h"
#include "src/datovka_shared/io/prefs_db_tables.h"
#include "src/datovka_shared/log/log.h"

/* Convenience values. */
static const QString falseStr("false");
static const QString trueStr("true");

bool PrefsDb::contains(const QString &name) const
{
	QMutexLocker locker(&m_lock);
	QSqlQuery query(m_db);
	QString queryStr(
	    "SELECT COUNT(*) AS nrNames FROM preferences WHERE name = :name");
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		return false;
	}
	query.bindValue(":name", name);
	if (Q_UNLIKELY(!(query.exec() && query.isActive()))) {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		return false;
	}
	if (query.first()) {
		if (query.isValid()) {
			if (Q_UNLIKELY(query.value(0).toInt() > 1)) {
				Q_ASSERT(0);
			}
			return query.value(0).toInt() != 0;
		} else {
			logWarningNL("Cannot read SQL data for entry '%s': %s.",
			    name.toUtf8().constData(),
			    query.lastError().text().toUtf8().constData());
			return false;
		}
	} else {
		logDebugLv0NL("No SQL data for entry '%s': %s.",
		    name.toUtf8().constData(),
		    query.lastError().text().toUtf8().constData());
		return false;
	}
}

bool PrefsDb::erase(const QString &name)
{
	QMutexLocker locker(&m_lock);
	QSqlQuery query(m_db);
	QString queryStr("DELETE FROM preferences WHERE name = :name");
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		return false;
	}
	query.bindValue(":name", name);
	if (query.exec()) {
		return true;
	} else {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		return false;
	}
}

QStringList PrefsDb::names(void) const
{
	QMutexLocker locker(&m_lock);
	QSqlQuery query(m_db);
	QString queryStr("SELECT name FROM preferences");
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		return QStringList();
	}
	if (query.exec() && query.isActive()) {
		QStringList nameList;
		query.first();
		while (query.isValid()) {
			QString name(query.value(0).toString());
			if (!name.isEmpty()) {
				nameList.append(name);
			} else {
				logErrorNL("%s", "Ignoring empty preference name.");
			}
			query.next();
		}
		return nameList;
	} else {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		return QStringList();
	}
}

/*!
 * @brief Converts boolean to string.
 *
 * @return String representation of a boolean value.
 */
static inline
const QString &boolToString(bool val)
{
	return val ? trueStr : falseStr;
}

/*!
 * @brief Converts string to boolean.
 *
 * @param[in]  val String containing a boolean value.
 * @param[out] ok Set to true f string could be successfully converted.
 * @returns Converted boolean value or false on error.
 */
static
bool stringToBool(const QString &val, bool *ok = Q_NULLPTR)
{
	if (val == falseStr) {
		if (ok != Q_NULLPTR) {
			*ok = true;
		}
		return false;
	} else if (val == trueStr) {
		if (ok != Q_NULLPTR) {
			*ok = true;
		}
		return true;
	} else {
		if (ok != Q_NULLPTR) {
			*ok = false;
		}
		return false;
	}
}

/*!
 * @brief Convert an integer value to the value enumeration type.
 *
 * @param[in]  i Integer type to be converted.
 * @param[out] ok Set to false if conversion fails, true else.
 * @return Target enumeration type if conversion succeeds.
 */
static
enum PrefsDb::ValueType intToValueType(int i, bool *ok = Q_NULLPTR)
{
	switch (i) {
	case PrefsDb::VAL_BOOLEAN:
	case PrefsDb::VAL_INTEGER:
	case PrefsDb::VAL_FLOAT:
	case PrefsDb::VAL_STRING:
	case PrefsDb::VAL_COLOUR:
	case PrefsDb::VAL_DATETIME:
	case PrefsDb::VAL_DATE:
		if (ok != Q_NULLPTR) {
			*ok = true;
		}
		return (enum PrefsDb::ValueType)i;
		break;
	default:
		Q_ASSERT(0);
		if (ok != Q_NULLPTR) {
			*ok = false;
		}
		return PrefsDb::VAL_BOOLEAN;
		break;
	}
}

/*!
 * @brief Converts string data from database to a suitable variant type.
 *
 * @param[in]  type Target type.
 * @param[in]  val Value to by converted.
 * @param[out] ok Set to false if conversion fails, true else.
 * @return Variant with desired value, null variant on error.
 */
static
QVariant valueToVariant(enum PrefsDb::ValueType type, const QString &val,
    bool *ok = Q_NULLPTR)
{
	bool iOk = false;
	QVariant ret;

	switch (type) {
	case PrefsDb::VAL_BOOLEAN:
		ret = stringToBool(val, &iOk);
		break;
	case PrefsDb::VAL_INTEGER:
		ret = val.toLongLong(&iOk);
		break;
	case PrefsDb::VAL_FLOAT:
		ret = val.toDouble(&iOk);
		break;
	case PrefsDb::VAL_STRING:
		iOk = true;
		ret = val;
		break;
	case PrefsDb::VAL_COLOUR:
		iOk = Colour::isValidColourStr(val);
		ret = val;
		break;
	case PrefsDb::VAL_DATETIME:
		{
			QDateTime dt(PrefsDb::stringToDateTime(val));
			iOk = dt.isValid();
			ret = dt;
		}
		break;
	case PrefsDb::VAL_DATE:
		{
			QDate d(QDate::fromString(val, Qt::ISODate));
			iOk = d.isValid();
			ret = d;
		}
		break;
	default:
		Q_ASSERT(0);
		break;
	}

	if (ok != Q_NULLPTR) {
		*ok = iOk;
	}
	return ret;
}

bool PrefsDb::type(const QString &name, enum ValueType &type,
    QVariant *val) const
{
	QMutexLocker locker(&m_lock);
	QSqlQuery query(m_db);
	QString queryStr(
	    "SELECT type, value FROM preferences WHERE name = :name");
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		return false;
	}
	query.bindValue(":name", name);
	if (Q_UNLIKELY(!(query.exec() && query.isActive()))) {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		return false;
	}
	if (query.first()) {
		if (query.isValid()) {
			bool iOk = false;
			enum ValueType tmpType = intToValueType(query.value(0).toInt(), &iOk);
			if (Q_UNLIKELY(!iOk)) {
				logErrorNL("Unknown type '%d' for given name '%s'.",
				    query.value(0).toInt(), name.toUtf8().constData());
				return false;
			}
			if (val != Q_NULLPTR) {
				iOk = false;
				const QString value(query.value(1).toString());
				QVariant tmpVal = valueToVariant(tmpType, value, &iOk);
				if (Q_UNLIKELY(!iOk)) {
					logErrorNL("Bad value '%s' of type '%d' in database.",
					    value.toUtf8().constData(), VAL_BOOLEAN);
					return false;
				}
				*val = macroStdMove(tmpVal);
			}
			type = tmpType;
			return true;
		} else {
			logWarningNL("Cannot read SQL data for entry '%s': %s.",
			    name.toUtf8().constData(),
			    query.lastError().text().toUtf8().constData());
			return false;
		}
	} else {
		logDebugLv0NL("No SQL data for entry '%s': %s.",
		    name.toUtf8().constData(),
		    query.lastError().text().toUtf8().constData());
		return false;
	}
}

bool PrefsDb::setBoolVal(const QString &name, bool val)
{
	if (Q_UNLIKELY(name.isEmpty())) {
		return false;
	}

	QMutexLocker locker(&m_lock);
	QSqlQuery query(m_db);
	QString queryStr("INSERT OR REPLACE INTO preferences "
	    "(name, type, value) VALUES (:name, :type, :value)");
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		return false;
	}
	query.bindValue(":name", name);
	query.bindValue(":type", VAL_BOOLEAN);
	query.bindValue(":value", boolToString(val));
	if (query.exec()) {
		return true;
	} else {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		return false;
	}
}

bool PrefsDb::boolVal(const QString &name, bool &val) const
{
	QMutexLocker locker(&m_lock);
	QSqlQuery query(m_db);
	QString queryStr(
	    "SELECT type, value FROM preferences WHERE name = :name");
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		return false;
	}
	query.bindValue(":name", name);
	if (Q_UNLIKELY(!(query.exec() && query.isActive()))) {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		return false;
	}
	if (query.first()) {
		if (query.isValid()) {
			int type = query.value(0).toInt();
			const QString value(query.value(1).toString());
			if (Q_UNLIKELY(type != VAL_BOOLEAN)) {
				logErrorNL(
				    "Expected type '%d' but database contains '%d' for given name '%s'.",
				    VAL_BOOLEAN, type, name.toUtf8().constData());
				return false;
			}
			bool iOk = false;
			bool tmp = stringToBool(value, &iOk);
			if (Q_UNLIKELY(!iOk)) {
				logErrorNL("Bad value '%s' of type '%d' in database.",
				    value.toUtf8().constData(), VAL_BOOLEAN);
				return false;
			}
			val = tmp;
			return true;
		} else {
			logWarningNL("Cannot read SQL data for entry '%s': %s.",
			    name.toUtf8().constData(),
			    query.lastError().text().toUtf8().constData());
			return false;
		}
	} else {
		logDebugLv0NL("No SQL data for entry '%s': %s.",
		    name.toUtf8().constData(),
		    query.lastError().text().toUtf8().constData());
		return false;
	}
}

bool PrefsDb::setIntVal(const QString &name, qint64 val)
{
	if (Q_UNLIKELY(name.isEmpty())) {
		return false;
	}

	QMutexLocker locker(&m_lock);
	QSqlQuery query(m_db);
	QString queryStr("INSERT OR REPLACE INTO preferences "
	    "(name, type, value) VALUES (:name, :type, :value)");
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		return false;
	}
	query.bindValue(":name", name);
	query.bindValue(":type", VAL_INTEGER);
	query.bindValue(":value", QString::number(val));
	if (query.exec()) {
		return true;
	} else {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		return false;
	}
}

bool PrefsDb::intVal(const QString &name, qint64 &val) const
{
	QMutexLocker locker(&m_lock);
	QSqlQuery query(m_db);
	QString queryStr(
	    "SELECT type, value FROM preferences WHERE name = :name");
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		return false;
	}
	query.bindValue(":name", name);
	if (Q_UNLIKELY(!(query.exec() && query.isActive()))) {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		return false;
	}
	if (query.first()) {
		if (query.isValid()) {
			int type = query.value(0).toInt();
			const QString value(query.value(1).toString());
			if (Q_UNLIKELY(type != VAL_INTEGER)) {
				logErrorNL(
				    "Expected type '%d' but database contains '%d' for given name '%s'.",
				    VAL_INTEGER, type, name.toUtf8().constData());
				return false;
			}
			bool iOk = false;
			qint64 tmp = value.toLongLong(&iOk);
			if (Q_UNLIKELY(!iOk)) {
				logErrorNL("Bad value '%s' of type '%d' in database.",
				    value.toUtf8().constData(), VAL_INTEGER);
				return false;
			}
			val = tmp;
			return true;
		} else {
			logWarningNL("Cannot read SQL data for entry '%s': %s.",
			    name.toUtf8().constData(),
			    query.lastError().text().toUtf8().constData());
			return false;
		}
	} else {
		logDebugLv0NL("No SQL data for entry '%s': %s.",
		    name.toUtf8().constData(),
		    query.lastError().text().toUtf8().constData());
		return false;
	}
}

bool PrefsDb::setFloatVal(const QString &name, double val)
{
	if (Q_UNLIKELY(name.isEmpty())) {
		return false;
	}

	QMutexLocker locker(&m_lock);
	QSqlQuery query(m_db);
	QString queryStr("INSERT OR REPLACE INTO preferences "
	    "(name, type, value) VALUES (:name, :type, :value)");
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		return false;
	}
	query.bindValue(":name", name);
	query.bindValue(":type", VAL_FLOAT);
	query.bindValue(":value", QString::number(val));
	if (query.exec()) {
		return true;
	} else {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		return false;
	}
}

bool PrefsDb::floatVal(const QString &name, double &val) const
{
	QMutexLocker locker(&m_lock);
	QSqlQuery query(m_db);
	QString queryStr(
	    "SELECT type, value FROM preferences WHERE name = :name");
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		return false;
	}
	query.bindValue(":name", name);
	if (Q_UNLIKELY(!(query.exec() && query.isActive()))) {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		return false;
	}
	if (query.first()) {
		if (query.isValid()) {
			int type = query.value(0).toInt();
			const QString value(query.value(1).toString());
			if (Q_UNLIKELY(type != VAL_FLOAT)) {
				logErrorNL(
				    "Expected type '%d' but database contains '%d' for given name '%s'.",
				    VAL_FLOAT, type, name.toUtf8().constData());
				return false;
			}
			bool iOk = false;
			double tmp = value.toDouble(&iOk);
			if (Q_UNLIKELY(!iOk)) {
				logErrorNL("Bad value '%s' of type '%d' in database.",
				    value.toUtf8().constData(), VAL_FLOAT);
				return false;
			}
			val = tmp;
			return true;
		} else {
			logWarningNL("Cannot read SQL data for entry '%s': %s.",
			    name.toUtf8().constData(),
			    query.lastError().text().toUtf8().constData());
			return false;
		}
	} else {
		logDebugLv0NL("No SQL data for entry '%s': %s.",
		    name.toUtf8().constData(),
		    query.lastError().text().toUtf8().constData());
		return false;
	}
}

bool PrefsDb::setStrVal(const QString &name, const QString &val)
{
	if (Q_UNLIKELY(name.isEmpty())) {
		return false;
	}

	QMutexLocker locker(&m_lock);
	QSqlQuery query(m_db);
	QString queryStr("INSERT OR REPLACE INTO preferences "
	    "(name, type, value) VALUES (:name, :type, :value)");
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		return false;
	}
	query.bindValue(":name", name);
	query.bindValue(":type", VAL_STRING);
	query.bindValue(":value", nullVariantWhenIsNull(val));
	if (query.exec()) {
		return true;
	} else {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		return false;
	}
}

bool PrefsDb::strVal(const QString &name, QString &val) const
{
	QMutexLocker locker(&m_lock);
	QSqlQuery query(m_db);
	QString queryStr(
	    "SELECT type, value FROM preferences WHERE name = :name");
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		return false;
	}
	query.bindValue(":name", name);
	if (Q_UNLIKELY(!(query.exec() && query.isActive()))) {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		return false;
	}
	if (query.first()) {
		if (query.isValid()) {
			int type = query.value(0).toInt();
			QString value(query.value(1).toString());
			if (Q_UNLIKELY(type != VAL_STRING)) {
				logErrorNL(
				    "Expected type '%d' but database contains '%d' for given name '%s'.",
				    VAL_STRING, type, name.toUtf8().constData());
				return false;
			}
			val = macroStdMove(value);
			return true;
		} else {
			logWarningNL("Cannot read SQL data for entry '%s': %s.",
			    name.toUtf8().constData(),
			    query.lastError().text().toUtf8().constData());
			return false;
		}
	} else {
		logDebugLv0NL("No SQL data for entry '%s': %s.",
		    name.toUtf8().constData(),
		    query.lastError().text().toUtf8().constData());
		return false;
	}
}

bool PrefsDb::setColourVal(const QString &name, const QString &val)
{
	if (Q_UNLIKELY(name.isEmpty())) {
		return false;
	}
	if (Q_UNLIKELY((!val.isNull()) && (!Colour::isValidColourStr(val)))) {
		logErrorNL("Expected valid colour value but got '%s'.",
		    val.toUtf8().constData());
		return false;
	}

	QMutexLocker locker(&m_lock);
	QSqlQuery query(m_db);
	QString queryStr("INSERT OR REPLACE INTO preferences "
	    "(name, type, value) VALUES (:name, :type, :value)");
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		return false;
	}
	query.bindValue(":name", name);
	query.bindValue(":type", VAL_COLOUR);
	query.bindValue(":value", nullVariantWhenIsNull(val));
	if (query.exec()) {
		return true;
	} else {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		return false;
	}
}

bool PrefsDb::colourVal(const QString &name, QString &val) const
{
	QMutexLocker locker(&m_lock);
	QSqlQuery query(m_db);
	QString queryStr(
	    "SELECT type, value FROM preferences WHERE name = :name");
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		return false;
	}
	query.bindValue(":name", name);
	if (Q_UNLIKELY(!(query.exec() && query.isActive()))) {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		return false;
	}
	if (query.first()) {
		if (query.isValid()) {
			int type = query.value(0).toInt();
			QString value(query.value(1).toString());
			if (Q_UNLIKELY(type != VAL_COLOUR)) {
				logErrorNL(
				    "Expected type '%d' but database contains '%d' for given name '%s'.",
				    VAL_COLOUR, type, name.toUtf8().constData());
				return false;
			}
			if (!value.isNull()) {
				if (Q_UNLIKELY(!Colour::isValidColourStr(value))) {
					logErrorNL("Bad value '%s' of type '%d' in database.",
					    value.toUtf8().constData(), VAL_COLOUR);
					return false;
				}
			}
			val = macroStdMove(value);
			return true;
		} else {
			logWarningNL("Cannot read SQL data for entry '%s': %s.",
			    name.toUtf8().constData(),
			    query.lastError().text().toUtf8().constData());
			return false;
		}
	} else {
		logDebugLv0NL("No SQL data for entry '%s': %s.",
		    name.toUtf8().constData(),
		    query.lastError().text().toUtf8().constData());
		return false;
	}
}

bool PrefsDb::setDateTimeVal(const QString &name, const QDateTime &val)
{
	if (Q_UNLIKELY(name.isEmpty())) {
		return false;
	}

	QMutexLocker locker(&m_lock);
	QSqlQuery query(m_db);
	QString queryStr("INSERT OR REPLACE INTO preferences "
	    "(name, type, value) VALUES (:name, :type, :value)");
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		return false;
	}
	query.bindValue(":name", name);
	query.bindValue(":type", VAL_DATETIME);
	QString valStr; /* Use null string for null value. */
	if (val.isValid()) {
		valStr = dateTimeToString(val);
	}
	query.bindValue(":value", nullVariantWhenIsNull(valStr));
	if (query.exec()) {
		return true;
	} else {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		return false;
	}
}

bool PrefsDb::dateTimeVal(const QString &name, QDateTime &val) const
{
	QMutexLocker locker(&m_lock);
	QSqlQuery query(m_db);
	QString queryStr(
	    "SELECT type, value FROM preferences WHERE name = :name");
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		return false;
	}
	query.bindValue(":name", name);
	if (Q_UNLIKELY(!(query.exec() && query.isActive()))) {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		return false;
	}
	if (query.first()) {
		if (query.isValid()) {
			int type = query.value(0).toInt();
			const QString value(query.value(1).toString());
			if (Q_UNLIKELY(type != VAL_DATETIME)) {
				logErrorNL(
				    "Expected type '%d' but database contains '%d' for given name '%s'.",
				    VAL_DATETIME, type, name.toUtf8().constData());
				return false;
			}
			QDateTime tmp;
			if (!value.isNull()) {
				tmp = stringToDateTime(value);
				if (Q_UNLIKELY(!tmp.isValid())) {
					logErrorNL("Bad value '%s' of type '%d' in database.",
					    value.toUtf8().constData(), VAL_DATETIME);
					return false;
				}
			}
			val = macroStdMove(tmp);
			return true;
		} else {
			logWarningNL("Cannot read SQL data for entry '%s': %s.",
			    name.toUtf8().constData(),
			    query.lastError().text().toUtf8().constData());
			return false;
		}
	} else {
		logDebugLv0NL("No SQL data for entry '%s': %s.",
		    name.toUtf8().constData(),
		    query.lastError().text().toUtf8().constData());
		return false;
	}
}

bool PrefsDb::setDateVal(const QString &name, const QDate &val)
{
	if (Q_UNLIKELY(name.isEmpty())) {
		return false;
	}

	QMutexLocker locker(&m_lock);
	QSqlQuery query(m_db);
	QString queryStr("INSERT OR REPLACE INTO preferences "
	    "(name, type, value) VALUES (:name, :type, :value)");
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		return false;
	}
	query.bindValue(":name", name);
	query.bindValue(":type", VAL_DATE);
	QString valStr; /* Use null string for null value. */
	if (val.isValid()) {
		valStr = val.toString(Qt::ISODate);
	}
	query.bindValue(":value", nullVariantWhenIsNull(valStr));
	if (query.exec()) {
		return true;
	} else {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		return false;
	}
}

bool PrefsDb::dateVal(const QString &name, QDate &val) const
{
	QMutexLocker locker(&m_lock);
	QSqlQuery query(m_db);
	QString queryStr(
	    "SELECT type, value FROM preferences WHERE name = :name");
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		return false;
	}
	query.bindValue(":name", name);
	if (Q_UNLIKELY(!(query.exec() && query.isActive()))) {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		return false;
	}
	if (query.first()) {
		if (query.isValid()) {
			int type = query.value(0).toInt();
			const QString value(query.value(1).toString());
			if (Q_UNLIKELY(type != VAL_DATE)) {
				logErrorNL(
				    "Expected type '%d' but database contains '%d' for given name '%s'.",
				    VAL_DATE, type, name.toUtf8().constData());
				return false;
			}
			QDate tmp;
			if (!value.isNull()) {
				tmp = QDate::fromString(value, Qt::ISODate);
				if (Q_UNLIKELY(!tmp.isValid())) {
					logErrorNL("Bad value '%s' of type '%d' in database.",
					    value.toUtf8().constData(), VAL_DATE);
					return false;
				}
			}
			val = macroStdMove(tmp);
			return true;
		} else {
			logWarningNL("Cannot read SQL data for entry '%s': %s.",
			    name.toUtf8().constData(),
			    query.lastError().text().toUtf8().constData());
			return false;
		}
	} else {
		logDebugLv0NL("No SQL data for entry '%s': %s.",
		    name.toUtf8().constData(),
		    query.lastError().text().toUtf8().constData());
		return false;
	}
}

#define ISO_DATE_WITH_MS_FORMAT "yyyy-MM-ddTHH:mm:ss.zzzZ"

QString PrefsDb::dateTimeToString(const QDateTime &val)
{
#if (QT_VERSION >= QT_VERSION_CHECK(5, 8, 0))
	return val.toUTC().toString(Qt::ISODateWithMs);
#else /* < Qt-5.8 */
	return val.toUTC().toString(ISO_DATE_WITH_MS_FORMAT);
#endif /* >= Qt-5.8 */
}

QDateTime PrefsDb::stringToDateTime(const QString &str)
{
#if (QT_VERSION >= QT_VERSION_CHECK(5, 8, 0))
	return QDateTime::fromString(str, Qt::ISODateWithMs);
#else /* < Qt-5.8 */
	{
		QDateTime val = QDateTime::fromString(str, ISO_DATE_WITH_MS_FORMAT);
		val.setTimeSpec(Qt::UTC);
		return val;
	}
#endif /* >= Qt-5.8 */
}

QList<class SQLiteTbl *> PrefsDb::listOfTables(void) const
{
	QList<class SQLiteTbl *> tables;
	tables.append(&prfsTbl);
	return tables;
}
