/*
 * 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 <QColor>

#include "src/datovka_shared/compat_qt/misc.h" /* Q_FALLTHROUGH */
#include "src/datovka_shared/isds/type_conversion.h"
#include "src/isds/to_text_conversion.h"
#include "src/isds/type_description.h"
#include "src/models/helper.h"
#include "src/models/data_box_contacts_model.h"

BoxContactsModel::BoxContactsModel(QObject *parent)
    : TblModel(parent),
    m_colouring(false),
    m_senderEffectiveOVM(false)
{
	/* Fixed column count. */
	m_columnCount = MAX_COL;
}

/*!
 * @brief Convert NilBool value into user understandable string.
 *
 * @param[in] val NilBool value.
 * @return String that can be shown to the user.
 */
static
QString nilBoolString(enum Isds::Type::NilBool val)
{
	switch (val) {
	case Isds::Type::BOOL_NULL:
		return BoxContactsModel::tr("unknown");
		break;
	case Isds::Type::BOOL_FALSE:
		return BoxContactsModel::tr("no");
		break;
	case Isds::Type::BOOL_TRUE:
		return BoxContactsModel::tr("yes");
		break;
	default:
		return QString();
		break;
	}
}

QVariant BoxContactsModel::data(const QModelIndex &index, int role) const
{
	switch (role) {
	case Qt::DisplayRole:
		switch (index.column()) {
		case CHECKBOX_COL:
			return QVariant();
			break;
		case BOX_TYPE_COL:
			{
				QVariant entry(_data(index, role));
				if (!entry.isNull()) {
					return Isds::dbType2Str(Isds::intVariant2DbType(entry));
				} else {
					return entry;
				}
			}
			break;
		case PUBLIC_COL:
			Q_FALLTHROUGH();
		case PDZ_COL:
			switch (Isds::variant2NilBool(_data(index, role))) {
			case Isds::Type::BOOL_NULL:
				return QString();
				break;
			case Isds::Type::BOOL_FALSE:
				return tr("no");
				break;
			case Isds::Type::BOOL_TRUE:
				return tr("yes");
				break;
			default:
				return QString();
				break;
			}
			break;
		case PAYMENT_COL:
			{
				QList<QVariant> entries =
				    _data(index.sibling(index.row(), PAYMENTS_COL), Qt::DisplayRole).toList();
				if (entries.size() > 1) {
					/*
					 * Display permanent editor in the view rather than the value.
					 * The value must be hidden when permanent editor is visible
					 * because of some broken styles on macOS.
					 * */
					return QString();
				}
			}
			{
				QVariant entry(_data(index, role));
				if (!entry.isNull()) {
					return descrDmType(entry.toInt());
				} else {
					return QString();
				}
			}
			break;
		default:
			return _data(index, role);
			break;
		}
		break;
	case Qt::EditRole:
		if (index.column() == PAYMENT_COL) {
			return _data(index, Qt::DisplayRole);
		} else {
			return QVariant();
		}
		break;
	case Qt::ToolTipRole:
		switch (index.column()) {
		case BOX_ID_COL:
			if (m_colouring && (index.column() == BOX_ID_COL)) {
				enum Isds::Type::NilBool publ = Isds::variant2NilBool(
				    _data(index.sibling(index.row(), PUBLIC_COL), Qt::DisplayRole));
				enum Isds::Type::NilBool pdz = Isds::variant2NilBool(
				    _data(index.sibling(index.row(), PDZ_COL), Qt::DisplayRole));
				QString toolTipStr;
				if (!m_senderEffectiveOVM) {
					if ((publ == Isds::Type::BOOL_NULL) && (pdz == Isds::Type::BOOL_NULL)) {
						toolTipStr = tr("It's unknown whether public or commercial messages can be sent to this recipient.") +
						    QStringLiteral("\n");
					} else if ((publ == Isds::Type::BOOL_NULL) && (pdz == Isds::Type::BOOL_FALSE)) {
						toolTipStr = tr("It's unknown whether a public messages can be sent to this recipient.") +
						    QStringLiteral("\n");
					} else if ((publ == Isds::Type::BOOL_FALSE) && (pdz == Isds::Type::BOOL_NULL)) {
						toolTipStr = tr("It's unknown whether a commercial messages can be sent to this recipient.") +
						    QStringLiteral("\n");
					} else if ((publ == Isds::Type::BOOL_FALSE) && (pdz == Isds::Type::BOOL_FALSE)) {
						toolTipStr = tr("Receiving of PDZs has been disabled in the recipient data box or there are no available payment methods for PDZs.") +
						    QStringLiteral("\n");
					}

					toolTipStr += headerData(PUBLIC_COL, Qt::Horizontal, Qt::DisplayRole).toString() +
					    QStringLiteral(": ") + nilBoolString(publ) + QStringLiteral("\n");
					toolTipStr += headerData(PDZ_COL, Qt::Horizontal, Qt::DisplayRole).toString() +
					    QStringLiteral(": ") + nilBoolString(pdz);

					return toolTipStr;
				} else {
					return tr("Sender has the effective OVM flag set.");
				}
			}
			return QVariant();
			break;
		case BOX_TYPE_COL:
			{
				QVariant entry(_data(index, Qt::DisplayRole));
				if (!entry.isNull()) {
					return Isds::Description::descrDbType(
					    Isds::intVariant2DbType(entry));
				} else {
					return entry;
				}
			}
			break;
		case PUBLIC_COL:
			switch (Isds::variant2NilBool(_data(index, Qt::DisplayRole))) {
			case Isds::Type::BOOL_NULL:
				return QString("Cannot determine whether a public message can be sent.");
				break;
			case Isds::Type::BOOL_FALSE:
				return tr("Cannot send a public data message.");
				break;
			case Isds::Type::BOOL_TRUE:
				return tr("Can send a public data message.");
				break;
			default:
				return QString();
				break;
			}
			break;
		case PDZ_COL:
			switch (Isds::variant2NilBool(_data(index, Qt::DisplayRole))) {
			case Isds::Type::BOOL_NULL:
				return QString("Cannot determine whether a commercial message can be sent.");
				break;
			case Isds::Type::BOOL_FALSE:
				return tr("Cannot send a commercial data message.");
				break;
			case Isds::Type::BOOL_TRUE:
				return tr("Can send a commercial data message.");
				break;
			default:
				return QString();
				break;
			}
			break;
		case PAYMENT_COL:
			{
				QVariant entry(_data(index, Qt::DisplayRole));
				if (!entry.isNull()) {
					return dmTypeToolTipDescr(entry.toInt());
				} else {
					return QVariant();
				}
			}
			break;
		default:
			return QVariant();
			break;
		}
		break;
	case Qt::ForegroundRole:
		if (m_colouring && (index.column() == BOX_ID_COL)) {
			enum Isds::Type::NilBool publ = Isds::variant2NilBool(
			    _data(index.sibling(index.row(), PUBLIC_COL), Qt::DisplayRole));
			enum Isds::Type::NilBool pdz = Isds::variant2NilBool(
			    _data(index.sibling(index.row(), PDZ_COL), Qt::DisplayRole));
			if (!m_senderEffectiveOVM) {
				if ((publ == Isds::Type::BOOL_NULL) && (pdz == Isds::Type::BOOL_NULL)) {
					return QColor(Qt::darkGray);
				} else if ((publ == Isds::Type::BOOL_NULL) && (pdz == Isds::Type::BOOL_FALSE)) {
					return QColor(Qt::darkGray);
				} else if ((publ == Isds::Type::BOOL_FALSE) && (pdz == Isds::Type::BOOL_NULL)) {
					return QColor(Qt::darkGray);
				} else if ((publ == Isds::Type::BOOL_FALSE) && (pdz == Isds::Type::BOOL_FALSE)) {
					return QColor(Qt::darkRed);
				}
			}
			return QVariant();
		}
		return QVariant();
		break;
	case Qt::CheckStateRole:
		if (index.column() == CHECKBOX_COL) {
			return _data(index, Qt::DisplayRole).toBool() ?
			    Qt::Checked : Qt::Unchecked;
		} else {
			return QVariant();
		}
		break;
	case Qt::AccessibleTextRole:
		/*
		 * Most of the accessibility data are constructed using
		 * the header data followed by the cell content.
		 */
		switch (index.column()) {
		case CHECKBOX_COL:
			return _data(index, Qt::DisplayRole).toBool() ?
			    tr("selected") : tr("not selected");
			break;
		case BOX_ID_COL:
			return tr("box identifier") + QLatin1String(" ") +
			    data(index).toString();
			break;
		case BOX_TYPE_COL:
			return headerData(index.column(), Qt::Horizontal).toString() +
			    QLatin1String(" ") +
			    data(index, Qt::ToolTipRole).toString();
			break;
		case BOX_NAME_COL:
		case ADDRESS_COL:
		case POST_CODE_COL:
		case PUBLIC_COL:
		case PDZ_COL:
		case PAYMENT_COL:
			return headerData(index.column(), Qt::Horizontal).toString() +
			    QLatin1String(" ") +
			    data(index).toString();
			break;
		default:
			return QVariant();
			break;
		}
		break;
	case ROLE_PROXYSORT:
		switch (index.column()) {
		case CHECKBOX_COL:
		case PUBLIC_COL:
		case PDZ_COL:
			{
				QString rankKey("2");
				switch (Isds::variant2NilBool(_data(index, Qt::DisplayRole))) {
				case Isds::Type::BOOL_NULL:
					rankKey = "0";
					break;
				case Isds::Type::BOOL_FALSE:
					rankKey = "1";
					break;
				case Isds::Type::BOOL_TRUE:
					rankKey = "2";
					break;
				default:
					break;
				}
				return ModelHelper::sortRank(rankKey, getBoxId(index));
			}
			break;
		case BOX_TYPE_COL:
			return ModelHelper::sortRank(
			    data(index, Qt::DisplayRole).toString(),
			    getBoxId(index));
			break;
		default:
			return _data(index, Qt::DisplayRole);
			break;
		}
		break;
	default:
		return _data(index, role);
		break;
	}
}

Qt::ItemFlags BoxContactsModel::flags(const QModelIndex &index) const
{
	Qt::ItemFlags defaultFlags = TblModel::flags(index);

	switch (index.column()) {
	case CHECKBOX_COL:
		defaultFlags |= Qt::ItemIsUserCheckable;
		break;
	case PAYMENT_COL:
		{
			/* Set editable only when there are several choices. */
			QList<QVariant> entries =
			    _data(index.sibling(index.row(), PAYMENTS_COL), Qt::DisplayRole).toList();
			if (entries.size() > 1) {
				defaultFlags |= Qt::ItemIsEditable;
			}
		}
		break;
	default:
		break;
	}

	return defaultFlags;
}

bool BoxContactsModel::setData(const QModelIndex &index, const QVariant &value,
    int role)
{
	if (Q_UNLIKELY(!index.isValid())) {
		return false;
	}

	int row = index.row();
	if (Q_UNLIKELY((row < 0) || (row >= m_rowCount))) {
		return false;
	}

	if ((index.column() == CHECKBOX_COL) && (role == Qt::CheckStateRole)) {
		bool newVal = (value == Qt::Checked);
		if (newVal != _data(index, Qt::DisplayRole).toBool()) {
			m_data[row][CHECKBOX_COL] = newVal;
			emit dataChanged(index, index);
			return true;
		}
	} else if ((index.column() == PAYMENT_COL) && (role == Qt::EditRole)) {
		if (value != _data(index, Qt::DisplayRole).toInt()) {
			m_data[row][PAYMENT_COL] = value;
			emit dataChanged(index, index);
			return true;
		}
	} else {
		return TblModel::setData(index, value, role);
	}

	return false;
}

void BoxContactsModel::setHeader(void)
{
	setHeaderData(BOX_ID_COL, Qt::Horizontal, tr("ID"), Qt::DisplayRole);
	setHeaderData(BOX_TYPE_COL, Qt::Horizontal, tr("Type"),
	    Qt::DisplayRole);
	setHeaderData(BOX_NAME_COL, Qt::Horizontal, tr("Name"),
	    Qt::DisplayRole);
	setHeaderData(ADDRESS_COL, Qt::Horizontal, tr("Address"),
	    Qt::DisplayRole);
	setHeaderData(POST_CODE_COL, Qt::Horizontal, tr("Postal Code"),
	    Qt::DisplayRole);
	setHeaderData(PUBLIC_COL, Qt::Horizontal, tr("Public Message"),
	    Qt::DisplayRole);
	setHeaderData(PDZ_COL, Qt::Horizontal, tr("PDZ"), Qt::DisplayRole);
	setHeaderData(PAYMENT_COL, Qt::Horizontal, tr("PDZ Payment"),
	    Qt::DisplayRole);
	setHeaderData(PAYMENT_COL, Qt::Horizontal,
	    tr("Determines the type of the commercial message and the means of payment."),
	    Qt::ToolTipRole);
}

void BoxContactsModel::appendData(
    const QList<Isds::DbOwnerInfo> &entryList)
{
	if (Q_UNLIKELY(entryList.isEmpty())) {
		return;
	}

	beginInsertRows(QModelIndex(), rowCount(),
	    rowCount() + entryList.size() - 1);

	foreach (const Isds::DbOwnerInfo &entry, entryList) {

		reserveSpace();

		QVector<QVariant> row(m_columnCount);

		row[CHECKBOX_COL] = false;
		row[BOX_ID_COL] = entry.dbID();
		row[BOX_TYPE_COL] = Isds::dbType2IntVariant(entry.dbType());
		row[BOX_NAME_COL] = Isds::textOwnerName(entry);
		row[ADDRESS_COL] = Isds::textAddressWithoutIc(entry.address());
		row[POST_CODE_COL] = entry.address().zipCode();
		row[PUBLIC_COL] = Isds::nilBool2Variant(entry.dbEffectiveOVM());
		row[PDZ_COL] = Isds::nilBool2Variant(
		    (entry.dbEffectiveOVM() == Isds::Type::BOOL_NULL) ? Isds::Type::BOOL_NULL :
		        (entry.dbEffectiveOVM() == Isds::Type::BOOL_TRUE ? Isds::Type::BOOL_FALSE : Isds::Type::BOOL_TRUE));
		//row[PAYMENT_COL];
		//row[PAYMENTS_COL];

		m_data[m_rowCount++] = row;
	}

	endInsertRows();
}

void BoxContactsModel::appendData(const QList<Isds::FulltextResult> &entryList)
{
	if (Q_UNLIKELY(entryList.isEmpty())) {
		return;
	}

	beginInsertRows(QModelIndex(), rowCount(),
	    rowCount() + entryList.size() - 1);

	foreach (const Isds::FulltextResult &entry, entryList) {

		reserveSpace();

		QVector<QVariant> row(m_columnCount);

		row[CHECKBOX_COL] = false;
		row[BOX_ID_COL] = entry.dbID();
		row[BOX_TYPE_COL] = Isds::dbType2IntVariant(entry.dbType());
		row[BOX_NAME_COL] = entry.dbName();
		row[ADDRESS_COL] = entry.dbAddress();
		//row[POST_CODE_COL];
		row[PUBLIC_COL] = entry.publicSending();
		row[PDZ_COL] = entry.commercialSending();
		//row[PAYMENT_COL];
		//row[PAYMENTS_COL];

		m_data[m_rowCount++] = row;
	}

	endInsertRows();
}

void BoxContactsModel::appendData(
    const QList<MessageDb::ContactEntry> &entryList)
{
	if (Q_UNLIKELY(entryList.isEmpty())) {
		return;
	}

	beginInsertRows(QModelIndex(), rowCount(),
	    rowCount() + entryList.size() - 1);

	foreach (const MessageDb::ContactEntry &entry, entryList) {

		reserveSpace();

		QVector<QVariant> row(m_columnCount);

		row[CHECKBOX_COL] = false;
		row[BOX_ID_COL] = entry.boxId;
		//row[BOX_TYPE_COL];
		row[BOX_NAME_COL] = entry.name;
		row[ADDRESS_COL] = entry.address;
		//row[POST_CODE_COL];
		//row[PUBLIC_COL];
		//row[PDZ_COL];
		//row[PAYMENT_COL];
		//row[PAYMENTS_COL];

		m_data[m_rowCount++] = row;
	}

	endInsertRows();
}

void BoxContactsModel::appendData(const QString &id,
    enum Isds::Type::DbType type, const QString &name, const QString &addr,
    const QString &postCode, enum Isds::Type::NilBool publ,
    enum Isds::Type::NilBool pdz, const QList<enum Isds::Type::DmType> &dmTypes)
{
	/*
	 * We've already encountered some data boxes without any address.
	 * Don't know whether it's a bug in ISDS or just sloppiness.
	 * See issue #471.
	 */
	if (Q_UNLIKELY(id.isEmpty())) {
		return;
	}

	beginInsertRows(QModelIndex(), rowCount(), rowCount());

	{
		reserveSpace();

		QVector<QVariant> row(m_columnCount);

		row[CHECKBOX_COL] = false;
		row[BOX_ID_COL] = id;
		if (type != Isds::Type::BT_NULL) {
			row[BOX_TYPE_COL] = Isds::dbType2IntVariant(type);
		}
		row[BOX_NAME_COL] = name;
		row[ADDRESS_COL] = addr;
		row[POST_CODE_COL] = postCode;
		row[PUBLIC_COL] = Isds::nilBool2Variant(publ);
		row[PDZ_COL] = Isds::nilBool2Variant(pdz);

		QList<QVariant> dmTypeVariantList;
		switch (pdz) {
		case Isds::Type::BOOL_NULL: /* Undefined value. */
			row[PAYMENT_COL] = Isds::Type::MT_UNKNOWN;
			dmTypeVariantList.append(Isds::Type::MT_UNKNOWN);
			break;
		case Isds::Type::BOOL_FALSE:
			row[PAYMENT_COL] = Isds::Type::MT_V;
			dmTypeVariantList.append(Isds::Type::MT_V);
			break;
		case Isds::Type::BOOL_TRUE:
			foreach (enum Isds::Type::DmType dmType, dmTypes) {
				dmTypeVariantList.append(dmType);
			}
			if (dmTypeVariantList.count() > 0) {
				row[PAYMENT_COL] = dmTypeVariantList.at(0);
			} else {
				row[PAYMENT_COL] = Isds::Type::MT_UNKNOWN;
			}
			break;
		default:
			Q_ASSERT(0);
			break;
		}

		row[PAYMENTS_COL] = dmTypeVariantList;

		m_data[m_rowCount++] = row;
	}

	endInsertRows();
}

bool BoxContactsModel::somethingChecked(void) const
{
	for (int row = 0; row < m_rowCount; ++row) {
		if (m_data[row][CHECKBOX_COL].toBool()) {
			return true;
		}
	}

	return false;
}

bool BoxContactsModel::containsBoxId(const QString &boxId) const
{
	for (int row = 0; row < m_rowCount; ++row) {
		if (m_data[row][BOX_ID_COL].toString() == boxId) {
			return true;
		}
	}

	return false;
}

/*!
 * @brief Compares the check state according to required criteria.
 *
 * @param[in] checked Check state from the model.
 * @param[in] entryState User required check state.
 * @return True if state matches the criteria.
 */
static inline
bool checkStateMatch(bool checked, enum BoxContactsModel::EntryState entryState)
{
	return (!checked && (entryState == BoxContactsModel::UNCHECKED)) ||
	    (checked && (entryState == BoxContactsModel::CHECKED)) ||
	    (entryState == BoxContactsModel::ANY);
}

QStringList BoxContactsModel::boxIdentifiers(enum EntryState entryState) const
{
	QStringList ids;

	for (int row = 0; row < m_rowCount; ++row) {
		if (checkStateMatch(m_data[row][CHECKBOX_COL].toBool(),
		        entryState)) {
			ids.append(m_data[row][BOX_ID_COL].toString());
		}
	}

	return ids;
}

QList<BoxContactsModel::PartialEntry> BoxContactsModel::partialBoxEntries(
    enum EntryState entryState) const
{
	QList<PartialEntry> entries;

	for (int row = 0; row < m_rowCount; ++row) {
		if (checkStateMatch(m_data[row][CHECKBOX_COL].toBool(),
		        entryState)) {
			PartialEntry entry;

			entry.id = m_data[row][BOX_ID_COL].toString();
			entry.name = m_data[row][BOX_NAME_COL].toString();
			entry.address = m_data[row][ADDRESS_COL].toString();
			entry.publ = Isds::variant2NilBool(m_data[row][PUBLIC_COL]);
			entry.pdz = Isds::variant2NilBool(m_data[row][PDZ_COL]);
			switch (entry.pdz) {
			case Isds::Type::BOOL_NULL: /* Undefined value. */
				break;
			case Isds::Type::BOOL_FALSE:
				entry.dmType = Isds::Type::MT_V;
				break;
			case Isds::Type::BOOL_TRUE:
				entry.dmType =
				    (enum Isds::Type::DmType)m_data[row][PAYMENT_COL].toInt();
				break;
			default:
				Q_ASSERT(0);
				break;
			}
			if (entry.pdz) {
			} else {
				entry.dmType = Isds::Type::MT_V;
			}
			entries.append(entry);
		}
	}

	return entries;
}

void BoxContactsModel::setColouring(bool colouring)
{
	if (m_colouring != colouring) {
		beginResetModel();
		m_colouring = colouring;
		endResetModel();
	}
}

void BoxContactsModel::setSenderEffectiveOVM(bool senderEffectiveOVM)
{
	const bool colouring = m_colouring;

	if (m_senderEffectiveOVM != senderEffectiveOVM) {
		/* Inly emit signals when colouring enabled. */
		if (colouring) {
			beginResetModel();
		}
		m_senderEffectiveOVM = senderEffectiveOVM;
		if (colouring) {
			endResetModel();
		}
	}
}

QString BoxContactsModel::descrDmType(int type)
{
	switch (type) {
	case Isds::Type::MT_UNKNOWN:
		return QString();
		break;
	case Isds::Type::MT_V:
		return tr("Public");
		break;
	case Isds::Type::MT_O:
		return tr("Response to initiatory");
		break;
	case Isds::Type::MT_G:
		return tr("Subsidised");
		break;
	case Isds::Type::MT_K:
		return tr("Contractual");
		break;
	case Isds::Type::MT_I:
		return tr("Initiatory");
		break;
	case Isds::Type::MT_E:
		return tr("Prepaid credit");
		break;
	default:
		return QString();
		break;
	}
}

QString BoxContactsModel::dmTypeToolTipDescr(int type)
{
	switch (type) {
	case Isds::Type::MT_UNKNOWN:
		return tr("Type of the message is not determined.");
		break;
	case Isds::Type::MT_V:
		return tr("A free-of-charge public data message will be sent to the recipient.");
		break;
	case Isds::Type::MT_O:
		/* The recipient offers pre-paid response commercial message to be sent to him. */
		return tr(
		    "The recipient offers a pre-paid commercial message response to be sent to him.\n"
		    "This setting allows you to use the offered payment.\n"
		    "The transfer charges are going to be paid by the recipient.");
		break;
	case Isds::Type::MT_G:
		return tr("Send a subsidised commercial data message.\n"
		    "Another data box is paying for the transfer charges.");
		break;
	case Isds::Type::MT_K:
		return tr("Send a contractual commercial data message.\n"
		    "The charges for sending this message are going to be billed by the Czech Post.\n"
		    "An active contract with the Czech Post is needed.");
		break;
	case Isds::Type::MT_I:
		return tr("Send an initiatory commercial data message.\n"
		    "This type of message enabled the recipient to send a response on behalf of your account.\n"
		    "A sender reference number must be filled in in order to send this type of message.\n"
		    "An active contract with the Czech Post or a subsidising data box is needed.");
		break;
	case Isds::Type::MT_E:
		return tr("Send a commercial data message.\n"
		    "Pay charges from a prepaid credit.");
		break;
	default:
		return QString();
		break;
	}
}

QString BoxContactsModel::getBoxId(const QModelIndex &index) const
{
	return _data(index.sibling(index.row(), BOX_ID_COL),
	    Qt::DisplayRole).toString();
}
