/*
 * 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 <QDate>
#include <QDateTime>
#include <QFont>
#include <QRegularExpression>
#include <QString>

#include "src/datovka_shared/io/prefs_db.h"
#include "src/datovka_shared/settings/prefs.h"
#include "src/models/prefs_model.h"

PrefsModel::PrefsModel(Prefs &prefs, QObject *parent)
    : QAbstractTableModel(parent),
    m_prefs(prefs),
    m_prefKeys(prefs.names(QRegularExpression(".*")))
{
	connect(&m_prefs, SIGNAL(entryCreated(int, QString, QVariant)),
	    this, SLOT(watchPrefsCreation(int, QString, QVariant)));
	connect(&m_prefs, SIGNAL(entrySet(int, QString, QVariant)),
	    this, SLOT(watchPrefsModification(int, QString, QVariant)));
	connect(&m_prefs, SIGNAL(entryRemoved(int, QString)),
	    this, SLOT(watchPrefsRemoval(int, QString)));
}

int PrefsModel::rowCount(const QModelIndex &parent) const
{
	if (parent.isValid()) {
		return 0;
	} else {
		return m_prefKeys.size();
	}
}

int PrefsModel::columnCount(const QModelIndex &parent) const
{
	if (parent.isValid()) {
		return 0;
	} else {
		return MAX_COLNUM;
	}
}

/*!
 * @brief Converts type into viewable string.
 *
 * @param[in] prefs Preferences container.
 * @param[in] name Preference name.
 * @return Type string.
 */
static
const QString &typeString(const Prefs &prefs, const QString &name)
{
	static const QString nullStr;
	static const QString booleanStr("boolean");
	static const QString intStr("integer");
	static const QString floatStr("float");
	static const QString strStr("string");
	static const QString colourStr("colour");
	static const QString dateTimeStr("datetime");
	static const QString dateStr("date");

	enum PrefsDb::ValueType type = PrefsDb::VAL_BOOLEAN;
	if (Q_UNLIKELY(!prefs.type(name, type))) {
		Q_ASSERT(0);
		return nullStr;
	}

	switch (type) {
	case PrefsDb::VAL_BOOLEAN:
		return booleanStr;
		break;
	case PrefsDb::VAL_INTEGER:
		return intStr;
		break;
	case PrefsDb::VAL_FLOAT:
		return floatStr;
		break;
	case PrefsDb::VAL_STRING:
		return strStr;
		break;
	case PrefsDb::VAL_COLOUR:
		return colourStr;
		break;
	case PrefsDb::VAL_DATETIME:
		return dateTimeStr;
		break;
	case PrefsDb::VAL_DATE:
		return dateStr;
		break;
	default:
		Q_ASSERT(0);
		return nullStr;
		break;
	}
}

/*!
 * @brief Converts status data into viewable string.
 *
 * @param[in] prefs Preferences container.
 * @param[in] name Preference name.
 * @return String containing status description.
 */
static
QString statusString(const Prefs &prefs, const QString &name)
{
	enum Prefs::Status status = Prefs::STAT_NO_DEFAULT;
	if (Q_UNLIKELY(!prefs.status(name, status))) {
		/* Status of a known name should be determinable. */
		Q_ASSERT(0);
		return QString();
	}

	switch (status) {
	case Prefs::STAT_NO_DEFAULT:
		return QString();
		break;
	case Prefs::STAT_DEFAULT:
		return PrefsModel::tr("default");
		break;
	case Prefs::STAT_MODIFIED:
		return PrefsModel::tr("modified");
		break;
	default:
		Q_ASSERT(0);
		return QString();
		break;
	}
}

/*!
 * @brief Checks whether value is modified.
 *
 * @param[in] prefs Preferences container.
 * @param[in] name Preference name.
 * @return True if the value is modified.
 */
static
bool statusModified(const Prefs &prefs, const QString &name)
{
	enum Prefs::Status status = Prefs::STAT_NO_DEFAULT;
	if (Q_UNLIKELY(!prefs.status(name, status))) {
		/* Status of a known name should be determinable. */
		Q_ASSERT(0);
		return false;
	}
	return status == Prefs::STAT_MODIFIED;
}

/*!
 * @brief Converts preferences data into viewable string.
 *
 * @param[in] prefs Preferences container.
 * @param[in] name Preference name.
 * @return String containing value data.
 */
static
QString valueString(const Prefs &prefs, const QString &name)
{
	enum PrefsDb::ValueType type = PrefsDb::VAL_BOOLEAN;
	if (Q_UNLIKELY(!prefs.type(name, type))) {
		Q_ASSERT(0);
		return QString();
	}

	switch (type) {
	case PrefsDb::VAL_BOOLEAN:
		{
			bool val = false;
			if (prefs.boolVal(name, val)) {
				return val ? QStringLiteral("true") : QStringLiteral("false");
			} else {
				Q_ASSERT(0);
				return QString();
			}
		}
		break;
	case PrefsDb::VAL_INTEGER:
		{
			qint64 val = 0;
			if (prefs.intVal(name, val)) {
				return QString::number(val);
			} else {
				Q_ASSERT(0);
				return QString();
			}
		}
		break;
	case PrefsDb::VAL_FLOAT:
		{
			double val = 0;
			if (prefs.floatVal(name, val)) {
				return QString::number(val);
			} else {
				Q_ASSERT(0);
				return QString();
			}
		}
		break;
	case PrefsDb::VAL_STRING:
		{
			QString val;
			if (Q_UNLIKELY(!prefs.strVal(name, val))) {
				Q_ASSERT(0);
			}
			return val;
		}
		break;
	case PrefsDb::VAL_COLOUR:
		{
			QString val;
			if (Q_UNLIKELY(!prefs.colourVal(name, val))) {
				Q_ASSERT(0);
			}
			return val;
		}
		break;
	case PrefsDb::VAL_DATETIME:
		{
			QDateTime val;
			if (prefs.dateTimeVal(name, val)) {
				return PrefsDb::dateTimeToString(val);
			} else {
				Q_ASSERT(0);
				return QString();
			}
		}
		break;
	case PrefsDb::VAL_DATE:
		{
			QDate val;
			if (prefs.dateVal(name, val)) {
				return val.toString(Qt::ISODate);
			} else {
				Q_ASSERT(0);
				return QString();
			}
		}
		break;
	default:
		Q_ASSERT(0);
		return QString();
		break;
	}
}

QVariant PrefsModel::data(const QModelIndex &index, int role) const
{
	const int row = index.row();
	const int col = index.column();
	if (Q_UNLIKELY((row < 0) || (row >= rowCount()))) {
		Q_ASSERT(0);
		return QVariant();
	}
	if (Q_UNLIKELY((col < 0) || (col >= columnCount()))) {
		Q_ASSERT(0);
		return QVariant();
	}

	switch (role) {
	case Qt::DisplayRole:
		switch (col) {
		case COL_NAME:
			return m_prefKeys[row];
			break;
		case COL_STATUS:
			return statusString(m_prefs, m_prefKeys[row]);
			break;
		case COL_TYPE:
			return typeString(m_prefs, m_prefKeys[row]);
			break;
		case COL_VALUE:
			return valueString(m_prefs, m_prefKeys[row]);
			break;
		default:
			Q_ASSERT(0);
			return QVariant();
			break;
		}
		break;
	case Qt::FontRole:
		if (statusModified(m_prefs, m_prefKeys[row])) {
			QFont boldFont;
			boldFont.setBold(true);
			return boldFont;
		}
		return QVariant();
		break;
	case Qt::AccessibleTextRole:
		switch (col) {
		case COL_NAME:
		case COL_STATUS:
		case COL_TYPE:
		case COL_VALUE:
			{
				QString dataStr(data(index).toString());
				if (Q_UNLIKELY(dataStr.isEmpty())) {
					dataStr = tr("unspecified");
				}
				return headerData(col, Qt::Horizontal).toString() +
				    QStringLiteral(" ") + dataStr;
			}
			break;
		default:
			return QVariant();
			break;
		}
		break;
	default:
		return QVariant();
		break;
	}
}

Qt::ItemFlags PrefsModel::flags(const QModelIndex &index) const
{
	Qt::ItemFlags defaultFlags = QAbstractTableModel::flags(index);
	return defaultFlags;
}

QVariant PrefsModel::headerData(int section, Qt::Orientation orientation,
    int role) const
{
	Q_UNUSED(orientation);

	switch (role) {
	case Qt::DisplayRole:
		switch (section) {
		case COL_NAME:
			return tr("Preference Name");
			break;
		case COL_STATUS:
			return tr("Status");
			break;
		case COL_TYPE:
			return tr("Type");
			break;
		case COL_VALUE:
			return tr("Value");
			break;
		default:
			Q_ASSERT(0);
			return QVariant();
			break;
		}
		break;

	default:
		return QVariant();
		break;
	}
}

int PrefsModel::findRow(const QString &name) const
{
	int foundRow = 0;
	while ((foundRow < m_prefKeys.size()) && (m_prefKeys.at(foundRow) != name)) {
		++foundRow;
	}
	if (Q_UNLIKELY(foundRow >= m_prefKeys.size())) {
		return -1;
	}

	return foundRow;
}

int PrefsModel::type(int row) const
{
	if (Q_UNLIKELY((row < 0) || (row >= rowCount()))) {
		Q_ASSERT(0);
		return -1;
	}

	enum PrefsDb::ValueType type = PrefsDb::VAL_BOOLEAN;
	if (Q_UNLIKELY(!m_prefs.type(m_prefKeys[row], type))) {
		Q_ASSERT(0);
		return -1;
	}
	return type;
}

void PrefsModel::alterBool(int row)
{
	if (Q_UNLIKELY((row < 0) || (row >= rowCount()))) {
		Q_ASSERT(0);
		return;
	}
	const QString &name(m_prefKeys[row]);
	enum PrefsDb::ValueType type = PrefsDb::VAL_BOOLEAN;
	if (Q_UNLIKELY(!m_prefs.type(name, type))) {
		Q_ASSERT(0);
		return;
	}
	if (Q_UNLIKELY(type != PrefsDb::VAL_BOOLEAN)) {
		Q_ASSERT(0);
		return;
	}
	bool val= false;
	if (Q_UNLIKELY(!m_prefs.boolVal(name, val))) {
		Q_ASSERT(0);
		return;
	}
	if (Q_UNLIKELY(!m_prefs.setBoolVal(name, !val))) {
		Q_ASSERT(0);
		return;
	}
}

void PrefsModel::modifyInt(int row, qint64 val)
{
	if (Q_UNLIKELY((row < 0) || (row >= rowCount()))) {
		Q_ASSERT(0);
		return;
	}
	const QString &name(m_prefKeys[row]);
	enum PrefsDb::ValueType type = PrefsDb::VAL_BOOLEAN;
	if (Q_UNLIKELY(!m_prefs.type(name, type))) {
		Q_ASSERT(0);
		return;
	}
	if (Q_UNLIKELY(type != PrefsDb::VAL_INTEGER)) {
		Q_ASSERT(0);
		return;
	}
	if (Q_UNLIKELY(!m_prefs.setIntVal(name, val))) {
		Q_ASSERT(0);
		return;
	}
}

void PrefsModel::modifyFloat(int row, double val)
{
	if (Q_UNLIKELY((row < 0) || (row >= rowCount()))) {
		Q_ASSERT(0);
		return;
	}
	const QString &name(m_prefKeys[row]);
	enum PrefsDb::ValueType type = PrefsDb::VAL_BOOLEAN;
	if (Q_UNLIKELY(!m_prefs.type(name, type))) {
		Q_ASSERT(0);
		return;
	}
	if (Q_UNLIKELY(type != PrefsDb::VAL_FLOAT)) {
		Q_ASSERT(0);
		return;
	}
	if (Q_UNLIKELY(!m_prefs.setFloatVal(name, val))) {
		Q_ASSERT(0);
		return;
	}
}

void PrefsModel::modifyString(int row, const QString &val)
{
	if (Q_UNLIKELY((row < 0) || (row >= rowCount()))) {
		Q_ASSERT(0);
		return;
	}
	const QString &name(m_prefKeys[row]);
	enum PrefsDb::ValueType type = PrefsDb::VAL_BOOLEAN;
	if (Q_UNLIKELY(!m_prefs.type(name, type))) {
		Q_ASSERT(0);
		return;
	}
	if (Q_UNLIKELY(type != PrefsDb::VAL_STRING)) {
		Q_ASSERT(0);
		return;
	}
	if (Q_UNLIKELY(!m_prefs.setStrVal(name, val))) {
		Q_ASSERT(0);
		return;
	}
}

void PrefsModel::modifyColour(int row, const QString &val)
{
	if (Q_UNLIKELY((row < 0) || (row >= rowCount()))) {
		Q_ASSERT(0);
		return;
	}
	const QString &name(m_prefKeys[row]);
	enum PrefsDb::ValueType type = PrefsDb::VAL_BOOLEAN;
	if (Q_UNLIKELY(!m_prefs.type(name, type))) {
		Q_ASSERT(0);
		return;
	}
	if (Q_UNLIKELY(type != PrefsDb::VAL_COLOUR)) {
		Q_ASSERT(0);
		return;
	}
	if (Q_UNLIKELY(!m_prefs.setColourVal(name, val))) {
		Q_ASSERT(0);
		return;
	}
}

void PrefsModel::modifyDateTime(int row, const QDateTime &val)
{
	if (Q_UNLIKELY((row < 0) || (row >= rowCount()))) {
		Q_ASSERT(0);
		return;
	}
	const QString &name(m_prefKeys[row]);
	enum PrefsDb::ValueType type = PrefsDb::VAL_BOOLEAN;
	if (Q_UNLIKELY(!m_prefs.type(name, type))) {
		Q_ASSERT(0);
		return;
	}
	if (Q_UNLIKELY(type != PrefsDb::VAL_DATETIME)) {
		Q_ASSERT(0);
		return;
	}
	if (Q_UNLIKELY(!m_prefs.setDateTimeVal(name, val))) {
		Q_ASSERT(0);
		return;
	}
}

void PrefsModel::modifyDate(int row, const QDate &val)
{
	if (Q_UNLIKELY((row < 0) || (row >= rowCount()))) {
		Q_ASSERT(0);
		return;
	}
	const QString &name(m_prefKeys[row]);
	enum PrefsDb::ValueType type = PrefsDb::VAL_BOOLEAN;
	if (Q_UNLIKELY(!m_prefs.type(name, type))) {
		Q_ASSERT(0);
		return;
	}
	if (Q_UNLIKELY(type != PrefsDb::VAL_DATE)) {
		Q_ASSERT(0);
		return;
	}
	if (Q_UNLIKELY(!m_prefs.setDateVal(name, val))) {
		Q_ASSERT(0);
		return;
	}
}

void PrefsModel::resetEntry(int row)
{
	if (Q_UNLIKELY((row < 0) || (row >= rowCount()))) {
		Q_ASSERT(0);
		return;
	}

	m_prefs.resetVal(m_prefKeys.at(row));
}

void PrefsModel::watchPrefsCreation(int valueType, const QString &name,
    const QVariant &value)
{
	Q_UNUSED(valueType);
	Q_UNUSED(value);

	int row = findRow(name);
	if (Q_UNLIKELY(row >= 0)) {
		return;
	}

	row = rowCount();

	beginInsertRows(QModelIndex(), row, row);

	m_prefKeys.append(name);

	endInsertRows();
}

void PrefsModel::watchPrefsModification(int valueType, const QString &name,
    const QVariant &value)
{
	Q_UNUSED(valueType);
	Q_UNUSED(value);

	int row = findRow(name);
	if (Q_UNLIKELY(row < 0)) {
		return;
	}

	emit dataChanged(index(row, COL_NAME), index(row, COL_VALUE));
}

void PrefsModel::watchPrefsRemoval(int valueType, const QString &name)
{
	Q_UNUSED(valueType);

	int row = findRow(name);
	if (Q_UNLIKELY(row < 0)) {
		return;
	}

	beginRemoveRows(QModelIndex(), row, row);

	m_prefKeys.removeAt(row);

	endRemoveRows();
}
