/*
 * Copyright (C) 2014-2022 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 <QReadLocker>
#include <QRegularExpression>
#include <QSet>
#include <QWriteLocker>

#include "src/datovka_shared/graphics/colour.h"
#include "src/datovka_shared/log/log.h"
#include "src/datovka_shared/settings/prefs.h"

Prefs::Prefs(void)
    : m_lock(),
    m_defaults(),
    m_prefsDb(Q_NULLPTR)
{
}

bool Prefs::setDefault(const Entry &entry)
{
	if (Q_UNLIKELY(entry.m_name.isEmpty())) {
		Q_ASSERT(0);
		return false;
	}
	if (Q_UNLIKELY(entry.m_content.m_modified)) {
		/* Cannot insert value which declares to be already modified. */
		Q_ASSERT(0);
		return false;
	}
	switch (entry.m_content.m_type) {
	case PrefsDb::VAL_BOOLEAN:
	case PrefsDb::VAL_INTEGER:
	case PrefsDb::VAL_FLOAT:
		if (Q_UNLIKELY(entry.m_content.m_defaultValue.isNull())) {
			Q_ASSERT(0);
			return false;
		}
		break;
	default:
		break;
	}

	QWriteLocker locker(&m_lock);

	QMap<QString, EntryContent>::iterator it =
	    m_defaults.insert(entry.m_name, entry.m_content);

	/* Adjust modified value. */
	if ((m_prefsDb != Q_NULLPTR) && (m_prefsDb->contains(entry.m_name))) {
		enum PrefsDb::ValueType type = PrefsDb::VAL_BOOLEAN;
		QVariant value;
		if (Q_UNLIKELY((!m_prefsDb->type(entry.m_name, type, &value)) ||
		               (type != entry.m_content.m_type))) {
			logWarningNL(
			    "Deleting entry '%s' from database because of type mismatch.",
			    entry.m_name.toUtf8().constData());
			m_prefsDb->erase(entry.m_name);
		} else if (value != entry.m_content.m_defaultValue) {
			it->m_modified = true;
		} else {
			/* Default matches database value. */
			m_prefsDb->erase(entry.m_name);
			it->m_modified = false;
		}
	}

	return true;
}

bool Prefs::setDatabase(PrefsDb *prefsDb)
{
	QWriteLocker locker(&m_lock);

	if (m_prefsDb != Q_NULLPTR) {
		/* Set defaults as unmodified and stop database from being used. */
		resetDefaults();
		/* Just stop using this database. */
		m_prefsDb = Q_NULLPTR;
	}

	if (prefsDb != Q_NULLPTR) {
		const QStringList nameList(prefsDb->names());
		foreach (const QString &name, nameList) {
			enum PrefsDb::ValueType type = PrefsDb::VAL_BOOLEAN;
			QVariant value;
			if (Q_UNLIKELY(!prefsDb->type(name, type, &value))) {
				/* Ignore faulty values. */
				continue;
			}
			QMap<QString, EntryContent>::iterator it = m_defaults.find(name);
			if (it != m_defaults.end()) {
				/* There is a default entry. */
				if (Q_UNLIKELY(type != it->m_type)) {
					logWarningNL(
					    "Deleting entry '%s' from database because of type mismatch.",
					    name.toUtf8().constData());
					prefsDb->erase(name);
				} else if (value != it->m_defaultValue) {
					it->m_modified = true;
				} else {
					/* Default matches database value. */
					prefsDb->erase(name);
					it->m_modified = false;
				}
			}
		}
	}

	m_prefsDb = prefsDb;
	return true;
}

QStringList Prefs::names(const QRegularExpression &re) const
{
	QReadLocker locker(&m_lock);

#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
	QList<QString> keys = m_defaults.keys();
	QSet<QString> names(keys.begin(), keys.end());
	if (m_prefsDb != Q_NULLPTR) {
		QStringList dbNames = m_prefsDb->names();
		names += QSet<QString>(dbNames.begin(), dbNames.end());
	}
#else /* < Qt-5.14.0 */
	QSet<QString> names = m_defaults.keys().toSet();
	if (m_prefsDb != Q_NULLPTR) {
		names += m_prefsDb->names().toSet();
	}
#endif /* >= Qt-5.14.0 */

	QStringList matchingNames;
	foreach (const QString &name, names) {
		if (re.match(name).hasMatch()) {
			matchingNames.append(name);
		}
	}
	return matchingNames;
}

bool Prefs::type(const QString &name, enum PrefsDb::ValueType &type) const
{
	QReadLocker locker(&m_lock);

	QMap<QString, EntryContent>::const_iterator it = m_defaults.constFind(name);
	if (it != m_defaults.constEnd()) {
		type = it->m_type;
		return true;
	}
	if (m_prefsDb != Q_NULLPTR) {
		return m_prefsDb->type(name, type, Q_NULLPTR);
	}
	/* No default, no database. */
	return false;
}

bool Prefs::status(const QString &name, enum Status &status) const
{
	QReadLocker locker(&m_lock);

	QMap<QString, EntryContent>::const_iterator it = m_defaults.constFind(name);
	if (it != m_defaults.constEnd()) {
		if (it->m_modified) {
			if (Q_UNLIKELY(m_prefsDb == Q_NULLPTR)) {
				Q_ASSERT(0);
				return false;
			}
			status = STAT_MODIFIED;
			return true;
		} else {
			status = STAT_DEFAULT;
			return true;
		}
	}
	if (m_prefsDb != Q_NULLPTR) {
		enum PrefsDb::ValueType type = PrefsDb::VAL_BOOLEAN;
		if (m_prefsDb->type(name, type)) {
			return true;
		}
		/* Name not in database. */
	}
	/* No default, no database. */
	return false;
}

bool Prefs::setBoolVal(const QString &name, bool val)
{
	QWriteLocker locker(&m_lock);

	QMap<QString, EntryContent>::iterator it = m_defaults.find(name);
	if (it != m_defaults.end()) {
		if (Q_UNLIKELY(it->m_type != PrefsDb::VAL_BOOLEAN)) {
			return false; /* Cannot override default type. */
		}
		if (it->m_defaultValue == QVariant(val)) {
			/* New value matches default value. */
			if (m_prefsDb != Q_NULLPTR) {
				m_prefsDb->erase(name);
			}
			it->m_modified = false;
			emit entrySet(PrefsDb::VAL_BOOLEAN, name, val);
			return true;
		}
	}
	if (m_prefsDb != Q_NULLPTR) {
		/* New value is different or default is not set. */
		if (Q_UNLIKELY(!m_prefsDb->setBoolVal(name, val))) {
			/* Cannot modify database. */
			return false;
		}
		if (it != m_defaults.end()) {
			/* Default has been changed. */
			it->m_modified = true;
		}
		emit entrySet(PrefsDb::VAL_BOOLEAN, name, val);
		return true;
	}
	/* No default, no database. */
	return false;
}

bool Prefs::boolVal(const QString &name, bool &val) const
{
	QReadLocker locker(&m_lock);

	QMap<QString, EntryContent>::const_iterator it = m_defaults.constFind(name);
	if (it != m_defaults.constEnd()) {
		if (it->m_modified) {
			if (Q_UNLIKELY(m_prefsDb == Q_NULLPTR)) {
				Q_ASSERT(0);
				return false;
			}
			return m_prefsDb->boolVal(name, val);
		} else {
			if (Q_UNLIKELY(it->m_type != PrefsDb::VAL_BOOLEAN)) {
				Q_ASSERT(0);
				return false;
			}
			if (Q_UNLIKELY(it->m_defaultValue.isNull())) {
				Q_ASSERT(0);
				return false;
			}
			val = it->m_defaultValue.toBool();
			return true;
		}
	}
	if (m_prefsDb != Q_NULLPTR) {
		return m_prefsDb->boolVal(name, val);
	}
	/* No defaults, no database. */
	return false;
}

bool Prefs::setIntVal(const QString &name, qint64 val)
{
	QWriteLocker locker(&m_lock);

	QMap<QString, EntryContent>::iterator it = m_defaults.find(name);
	if (it != m_defaults.end()) {
		if (Q_UNLIKELY(it->m_type != PrefsDb::VAL_INTEGER)) {
			return false; /* Cannot override default type. */
		}
		if (it->m_defaultValue == QVariant(val)) {
			/* New value matches default value. */
			if (m_prefsDb != Q_NULLPTR) {
				m_prefsDb->erase(name);
			}
			it->m_modified = false;
			emit entrySet(PrefsDb::VAL_INTEGER, name, val);
			return true;
		}
	}
	if (m_prefsDb != Q_NULLPTR) {
		/* New value is different or default is not set. */
		if (Q_UNLIKELY(!m_prefsDb->setIntVal(name, val))) {
			/* Cannot modify database. */
			return false;
		}
		if (it != m_defaults.end()) {
			/* Default has been changed. */
			it->m_modified = true;
		}
		emit entrySet(PrefsDb::VAL_INTEGER, name, val);
		return true;
	}
	/* No default, no database. */
	return false;
}

bool Prefs::intVal(const QString &name, qint64 &val) const
{
	QReadLocker locker(&m_lock);

	QMap<QString, EntryContent>::const_iterator it = m_defaults.constFind(name);
	if (it != m_defaults.constEnd()) {
		if (it->m_modified) {
			if (Q_UNLIKELY(m_prefsDb == Q_NULLPTR)) {
				Q_ASSERT(0);
				return false;
			}
			return m_prefsDb->intVal(name, val);
		} else {
			if (Q_UNLIKELY(it->m_type != PrefsDb::VAL_INTEGER)) {
				Q_ASSERT(0);
				return false;
			}
			if (Q_UNLIKELY(it->m_defaultValue.isNull())) {
				Q_ASSERT(0);
				return false;
			}
			val = it->m_defaultValue.toLongLong();
			return true;
		}
	}
	if (m_prefsDb != Q_NULLPTR) {
		return m_prefsDb->intVal(name, val);
	}
	/* No defaults, no database. */
	return false;
}

bool Prefs::setFloatVal(const QString &name, double val)
{
	QWriteLocker locker(&m_lock);

	QMap<QString, EntryContent>::iterator it = m_defaults.find(name);
	if (it != m_defaults.end()) {
		if (Q_UNLIKELY(it->m_type != PrefsDb::VAL_FLOAT)) {
			return false; /* Cannot override default type. */
		}
		if (it->m_defaultValue == QVariant(val)) {
			/* New value matches default value. */
			if (m_prefsDb != Q_NULLPTR) {
				m_prefsDb->erase(name);
			}
			it->m_modified = false;
			emit entrySet(PrefsDb::VAL_FLOAT, name, val);
			return true;
		}
	}
	if (m_prefsDb != Q_NULLPTR) {
		/* New value is different or default is not set. */
		if (Q_UNLIKELY(!m_prefsDb->setFloatVal(name, val))) {
			/* Cannot modify database. */
			return false;
		}
		if (it != m_defaults.end()) {
			/* Default has been changed. */
			it->m_modified = true;
		}
		emit entrySet(PrefsDb::VAL_FLOAT, name, val);
		return true;
	}
	/* No default, no database. */
	return false;
}

bool Prefs::floatVal(const QString &name, double &val) const
{
	QReadLocker locker(&m_lock);

	QMap<QString, EntryContent>::const_iterator it = m_defaults.constFind(name);
	if (it != m_defaults.constEnd()) {
		if (it->m_modified) {
			if (Q_UNLIKELY(m_prefsDb == Q_NULLPTR)) {
				Q_ASSERT(0);
				return false;
			}
			return m_prefsDb->floatVal(name, val);
		} else {
			if (Q_UNLIKELY(it->m_type != PrefsDb::VAL_FLOAT)) {
				Q_ASSERT(0);
				return false;
			}
			if (Q_UNLIKELY(it->m_defaultValue.isNull())) {
				Q_ASSERT(0);
				return false;
			}
			val = it->m_defaultValue.toDouble();
			return true;
		}
	}
	if (m_prefsDb != Q_NULLPTR) {
		return m_prefsDb->floatVal(name, val);
	}
	/* No defaults, no database. */
	return false;
}

bool Prefs::setStrVal(const QString &name, const QString &val)
{
	QWriteLocker locker(&m_lock);

	QMap<QString, EntryContent>::iterator it = m_defaults.find(name);
	if (it != m_defaults.end()) {
		if (Q_UNLIKELY(it->m_type != PrefsDb::VAL_STRING)) {
			return false; /* Cannot override default type. */
		}
		if (it->m_defaultValue == QVariant(val)) {
			/* New value matches default value. */
			if (m_prefsDb != Q_NULLPTR) {
				m_prefsDb->erase(name);
			}
			it->m_modified = false;
			emit entrySet(PrefsDb::VAL_STRING, name, val);
			return true;
		}
	}
	if (m_prefsDb != Q_NULLPTR) {
		/* New value is different or default is not set. */
		if (Q_UNLIKELY(!m_prefsDb->setStrVal(name, val))) {
			/* Cannot modify database. */
			return false;
		}
		if (it != m_defaults.end()) {
			/* Default has been changed. */
			it->m_modified = true;
		}
		emit entrySet(PrefsDb::VAL_STRING, name, val);
		return true;
	}
	/* No default, no database. */
	return false;
}

bool Prefs::strVal(const QString &name, QString &val) const
{
	QReadLocker locker(&m_lock);

	QMap<QString, EntryContent>::const_iterator it = m_defaults.constFind(name);
	if (it != m_defaults.constEnd()) {
		if (it->m_modified) {
			if (Q_UNLIKELY(m_prefsDb == Q_NULLPTR)) {
				Q_ASSERT(0);
				return false;
			}
			return m_prefsDb->strVal(name, val);
		} else {
			if (Q_UNLIKELY(it->m_type != PrefsDb::VAL_STRING)) {
				Q_ASSERT(0);
				return false;
			}
			/* Allow null values here. */
			val = it->m_defaultValue.toString();
			return true;
		}
	}
	if (m_prefsDb != Q_NULLPTR) {
		return m_prefsDb->strVal(name, val);
	}
	/* No defaults, no database. */
	return false;
}

bool Prefs::setColourVal(const QString &name, const QString &val)
{
	QWriteLocker locker(&m_lock);

	QMap<QString, EntryContent>::iterator it = m_defaults.find(name);
	if (it != m_defaults.end()) {
		if (Q_UNLIKELY(it->m_type != PrefsDb::VAL_COLOUR)) {
			return false; /* Cannot override default type. */
		}
		if (it->m_defaultValue == QVariant(val)) {
			/* New value matches default value. */
			if (m_prefsDb != Q_NULLPTR) {
				m_prefsDb->erase(name);
			}
			it->m_modified = false;
			emit entrySet(PrefsDb::VAL_COLOUR, name, val);
			return true;
		}
	}
	if (m_prefsDb != Q_NULLPTR) {
		/* New value is different or default is not set. */
		if (Q_UNLIKELY(!m_prefsDb->setColourVal(name, val))) {
			/* Cannot modify database. */
			return false;
		}
		if (it != m_defaults.end()) {
			/* Default has been changed. */
			it->m_modified = true;
		}
		emit entrySet(PrefsDb::VAL_COLOUR, name, val);
		return true;
	}
	/* No default, no database. */
	return false;
}

bool Prefs::colourVal(const QString &name, QString &val) const
{
	QReadLocker locker(&m_lock);

	QMap<QString, EntryContent>::const_iterator it = m_defaults.constFind(name);
	if (it != m_defaults.constEnd()) {
		if (it->m_modified) {
			if (Q_UNLIKELY(m_prefsDb == Q_NULLPTR)) {
				Q_ASSERT(0);
				return false;
			}
			return m_prefsDb->colourVal(name, val);
		} else {
			if (Q_UNLIKELY(it->m_type != PrefsDb::VAL_COLOUR)) {
				Q_ASSERT(0);
				return false;
			}
			/* Allow null values here. */
			QString value = it->m_defaultValue.toString();
			if (Q_UNLIKELY((!value.isNull()) &&
			        (!Colour::isValidColourStr(value)))) {
				logErrorNL("Bad default value '%s' of type '%d'.",
				    value.toUtf8().constData(), PrefsDb::VAL_COLOUR);
				Q_ASSERT(0);
				return false;
			}
			val = value;
			return true;
		}
	}
	if (m_prefsDb != Q_NULLPTR) {
		return m_prefsDb->colourVal(name, val);
	}
	/* No defaults, no database. */
	return false;
}

bool Prefs::setDateTimeVal(const QString &name, const QDateTime &val)
{
	QWriteLocker locker(&m_lock);

	QMap<QString, EntryContent>::iterator it = m_defaults.find(name);
	if (it != m_defaults.end()) {
		if (Q_UNLIKELY(it->m_type != PrefsDb::VAL_DATETIME)) {
			return false; /* Cannot override default type. */
		}
		if (it->m_defaultValue == QVariant(val)) {
			/* New value matches default value. */
			if (m_prefsDb != Q_NULLPTR) {
				m_prefsDb->erase(name);
			}
			it->m_modified = false;
			emit entrySet(PrefsDb::VAL_DATETIME, name, val);
			return true;
		}
	}
	if (m_prefsDb != Q_NULLPTR) {
		/* New value is different or default is not set. */
		if (Q_UNLIKELY(!m_prefsDb->setDateTimeVal(name, val))) {
			/* Cannot modify database. */
			return false;
		}
		if (it != m_defaults.end()) {
			/* Default has been changed. */
			it->m_modified = true;
		}
		emit entrySet(PrefsDb::VAL_DATETIME, name, val);
		return true;
	}
	/* No default, no database. */
	return false;
}

bool Prefs::dateTimeVal(const QString &name, QDateTime &val) const
{
	QReadLocker locker(&m_lock);

	QMap<QString, EntryContent>::const_iterator it = m_defaults.constFind(name);
	if (it != m_defaults.constEnd()) {
		if (it->m_modified) {
			if (Q_UNLIKELY(m_prefsDb == Q_NULLPTR)) {
				Q_ASSERT(0);
				return false;
			}
			return m_prefsDb->dateTimeVal(name, val);
		} else {
			if (Q_UNLIKELY(it->m_type != PrefsDb::VAL_DATETIME)) {
				Q_ASSERT(0);
				return false;
			}
			/* Allow null values here. */
			val = it->m_defaultValue.toDateTime();
			return true;
		}
	}
	if (m_prefsDb != Q_NULLPTR) {
		return m_prefsDb->dateTimeVal(name, val);
	}
	/* No defaults, no database. */
	return false;
}

bool Prefs::setDateVal(const QString &name, const QDate &val)
{
	QWriteLocker locker(&m_lock);

	QMap<QString, EntryContent>::iterator it = m_defaults.find(name);
	if (it != m_defaults.end()) {
		if (Q_UNLIKELY(it->m_type != PrefsDb::VAL_DATE)) {
			return false; /* Cannot override default type. */
		}
		if (it->m_defaultValue == QVariant(val)) {
			/* New value matches default value. */
			if (m_prefsDb != Q_NULLPTR) {
				m_prefsDb->erase(name);
			}
			it->m_modified = false;
			emit entrySet(PrefsDb::VAL_DATE, name, val);
			return true;
		}
	}
	if (m_prefsDb != Q_NULLPTR) {
		/* New value is different or default is not set. */
		if (Q_UNLIKELY(!m_prefsDb->setDateVal(name, val))) {
			/* Cannot modify database. */
			return false;
		}
		if (it != m_defaults.end()) {
			/* Default has been changed. */
			it->m_modified = true;
		}
		emit entrySet(PrefsDb::VAL_DATE, name, val);
		return true;
	}
	/* No default, no database. */
	return false;
}

bool Prefs::dateVal(const QString &name, QDate &val) const
{
	QReadLocker locker(&m_lock);

	QMap<QString, EntryContent>::const_iterator it = m_defaults.constFind(name);
	if (it != m_defaults.constEnd()) {
		if (it->m_modified) {
			if (Q_UNLIKELY(m_prefsDb == Q_NULLPTR)) {
				Q_ASSERT(0);
				return false;
			}
			return m_prefsDb->dateVal(name, val);
		} else {
			if (Q_UNLIKELY(it->m_type != PrefsDb::VAL_DATE)) {
				Q_ASSERT(0);
				return false;
			}
			/* Allow null values here. */
			val = it->m_defaultValue.toDate();
			return true;
		}
	}
	if (m_prefsDb != Q_NULLPTR) {
		return m_prefsDb->dateVal(name, val);
	}
	/* No defaults, no database. */
	return false;
}

void Prefs::resetDefaults(void)
{
	/* No locker because not part of public interface. */

	QMap<QString, EntryContent>::iterator it = m_defaults.begin();
	while (it != m_defaults.end()) {
		if (it->hasDefault()) {
			it->m_modified = false;
			++it;
		} else {
			/* Completely erase content with no default values. */
			it = m_defaults.erase(it);
		}
	}
}
