/*
 * Copyright (C) 2014-2021 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 <QString>
#include <QWriteLocker>
#include <utility> /* ::std::swap */

#include "src/datovka_shared/identifiers/account_id.h"
#include "src/gui/mw_status.h"

static const AcntId nullAcntId;
static const QString nullString;
static const QSet<MsgId> nullMsgIdSet;

/*!
 * @brief PIMPL MWStatus class.
 */
class MWStatusPrivate {
	//Q_DISABLE_COPY(MWStatusPrivate)
public:
	MWStatusPrivate(void)
	    : m_acntId(), m_acntNodeType(AccountModel::nodeUnknown), m_year(),
	    m_msgIds(), m_flagBlockIsds(false), m_flagBlockImports(false)
	{ }

	MWStatusPrivate &operator=(const MWStatusPrivate &other) Q_DECL_NOTHROW
	{
		m_acntId = other.m_acntId;
		m_acntNodeType = other.m_acntNodeType;
		m_year = other.m_year;
		m_msgIds = other.m_msgIds;
		m_flagBlockIsds = other.m_flagBlockIsds;
		m_flagBlockImports = other.m_flagBlockImports;

		return *this;
	}

	bool operator==(const MWStatusPrivate &other) const
	{
		return (m_acntId == other.m_acntId) &&
		    (m_acntNodeType == other.m_acntNodeType) &&
		    (m_year == other.m_year) &&
		    (m_msgIds == other.m_msgIds) &&
		    (m_flagBlockIsds == other.m_flagBlockIsds) &&
		    (m_flagBlockImports == other.m_flagBlockImports);
	}

	AcntId m_acntId;
	enum AccountModel::NodeType m_acntNodeType;
	QString m_year;
	QSet<MsgId> m_msgIds;

	bool m_flagBlockIsds;
	bool m_flagBlockImports;
};

MWStatus::MWStatus(void)
    : d_ptr(Q_NULLPTR)
{
}

MWStatus::MWStatus(const MWStatus &other)
    : d_ptr((other.d_func() != Q_NULLPTR) ? (new (::std::nothrow) MWStatusPrivate) : Q_NULLPTR)
{
	Q_D(MWStatus);
	if (d == Q_NULLPTR) {
		return;
	}

	*d = *other.d_func();
}

#ifdef Q_COMPILER_RVALUE_REFS
MWStatus::MWStatus(MWStatus &&other) Q_DECL_NOEXCEPT
    : d_ptr(other.d_ptr.take()) //d_ptr(::std::move(other.d_ptr))
{
}
#endif /* Q_COMPILER_RVALUE_REFS */

MWStatus::~MWStatus(void)
{
}

/*!
 * @brief Ensures private status presence.
 *
 * @note Returns if private status could not be allocated.
 */
#define ensureMWStatusPrivate(_x_) \
	do { \
		if (Q_UNLIKELY(d_ptr == Q_NULLPTR)) { \
			MWStatusPrivate *p = new (::std::nothrow) MWStatusPrivate; \
			if (Q_UNLIKELY(p == Q_NULLPTR)) { \
				Q_ASSERT(0); \
				return _x_; \
			} \
			d_ptr.reset(p); \
		} \
	} while (0)

MWStatus &MWStatus::operator=(const MWStatus &other) Q_DECL_NOTHROW
{
	if (other.d_func() == Q_NULLPTR) {
		d_ptr.reset(Q_NULLPTR);
		return *this;
	}
	ensureMWStatusPrivate(*this);
	Q_D(MWStatus);

	*d = *other.d_func();

	return *this;
}

#ifdef Q_COMPILER_RVALUE_REFS
MWStatus &MWStatus::operator=(MWStatus &&other) Q_DECL_NOTHROW
{
	swap(*this, other);
	return *this;
}
#endif /* Q_COMPILER_RVALUE_REFS */

bool MWStatus::operator==(const MWStatus &other) const
{
	Q_D(const MWStatus);
	if ((d == Q_NULLPTR) && ((other.d_func() == Q_NULLPTR))) {
		return true;
	} else if ((d == Q_NULLPTR) || ((other.d_func() == Q_NULLPTR))) {
		return false;
	}

	return *d == *other.d_func();
}

bool MWStatus::operator!=(const MWStatus &other) const
{
	return !operator==(other);
}

bool MWStatus::isNull(void) const
{
	Q_D(const MWStatus);
	return d == Q_NULLPTR;
}

const AcntId &MWStatus::acntId(void) const
{
	Q_D(const MWStatus);
	if (Q_UNLIKELY(d == Q_NULLPTR)) {
		return nullAcntId;
	}

	return d->m_acntId;
}

void MWStatus::setAcntId(const AcntId &ai)
{
	ensureMWStatusPrivate();
	Q_D(MWStatus);
	d->m_acntId = ai;
}

#ifdef Q_COMPILER_RVALUE_REFS
void MWStatus::setAcntId(AcntId &&ai)
{
	ensureMWStatusPrivate();
	Q_D(MWStatus);
	d->m_acntId = ::std::move(ai);
}
#endif /* Q_COMPILER_RVALUE_REFS */

enum AccountModel::NodeType MWStatus::acntNodeType(void) const
{
	Q_D(const MWStatus);
	if (Q_UNLIKELY(d == Q_NULLPTR)) {
		return AccountModel::nodeUnknown;
	}

	return d->m_acntNodeType;
}

void MWStatus::setAcntNodeType(enum AccountModel::NodeType nt)
{
	ensureMWStatusPrivate();
	Q_D(MWStatus);
	d->m_acntNodeType = nt;
}

const QString &MWStatus::year(void) const
{
	Q_D(const MWStatus);
	if (Q_UNLIKELY(d == Q_NULLPTR)) {
		return nullString;
	}

	return d->m_year;
}

void MWStatus::setYear(const QString &y)
{
	ensureMWStatusPrivate();
	Q_D(MWStatus);
	d->m_year = y;
}

#ifdef Q_COMPILER_RVALUE_REFS
void MWStatus::setYear(QString &&y)
{
	ensureMWStatusPrivate();
	Q_D(MWStatus);
	d->m_year = ::std::move(y);
}
#endif /* Q_COMPILER_RVALUE_REFS */

const QSet<MsgId> &MWStatus::msgIds(void) const
{
	Q_D(const MWStatus);
	if (Q_UNLIKELY(d == Q_NULLPTR)) {
		return nullMsgIdSet;
	}

	return d->m_msgIds;
}

void MWStatus::setMsgIds(const QSet<MsgId> &ml)
{
	ensureMWStatusPrivate();
	Q_D(MWStatus);
	d->m_msgIds = ml;
}

#ifdef Q_COMPILER_RVALUE_REFS
void MWStatus::setMsgIds(QSet<MsgId> &&ml)
{
	ensureMWStatusPrivate();
	Q_D(MWStatus);
	d->m_msgIds = ::std::move(ml);
}
#endif /* Q_COMPILER_RVALUE_REFS */

bool MWStatus::flagBlockIsds(void) const
{
	Q_D(const MWStatus);
	if (Q_UNLIKELY(d == Q_NULLPTR)) {
		return false;
	}

	return d->m_flagBlockIsds;
}

void MWStatus::setFlagBlockIsds(bool fbi)
{
	ensureMWStatusPrivate();
	Q_D(MWStatus);
	d->m_flagBlockIsds = fbi;
}

bool MWStatus::flagBlockImports(void) const
{
	Q_D(const MWStatus);
	if (Q_UNLIKELY(d == Q_NULLPTR)) {
		return false;
	}

	return d->m_flagBlockImports;
}

void MWStatus::setFlagBlockImports(bool fbi)
{
	ensureMWStatusPrivate();
	Q_D(MWStatus);
	d->m_flagBlockImports = fbi;
}

void swap(MWStatus &first, MWStatus &second) Q_DECL_NOTHROW
{
	using ::std::swap;
	swap(first.d_ptr, second.d_ptr);
}

MWLockedStatus::MWLockedStatus(QObject *parent)
    : QObject(parent),
    m_lock(),
    m_mwStatus()
{
}

MWStatus MWLockedStatus::mwStatus(void) const
{
	QReadLocker locker(&m_lock);

	return m_mwStatus;
}

/*!
 * @brief Set new account state.
 *
 * @param[in] acntId Account identifier.
 * @param[in] acntNodeType Account node type.
 * @param[in] year Year string if node type points to a year entry.
 * @return True if state has been modified.
 */
static
bool updateAccount(MWStatus &mwStatus, const AcntId &acntId,
    enum AccountModel::NodeType acntNodeType, const QString &year)
{
	if ((mwStatus.acntId() == acntId) &&
	    (mwStatus.acntNodeType() == acntNodeType) &&
	    (mwStatus.year() == year)) {
		/* No change. */
		return false;
	}

	if (acntId.isValid()) {
		switch (acntNodeType) {
		case AccountModel::nodeUnknown:
		case AccountModel::nodeRoot:
			/* These nodes should be inaccessible. */
			Q_ASSERT(0);
			return false;
			break;
		case AccountModel::nodeReceivedYear:
		case AccountModel::nodeSentYear:
			/* Year must be set. */
			if (Q_UNLIKELY(year.isEmpty())) {
				Q_ASSERT(0);
				return false;
			}
			break;
		default:
			break;
		}
		mwStatus.setAcntId(acntId);
		mwStatus.setAcntNodeType(acntNodeType);
		mwStatus.setYear(year);
	} else {
		/* Other values are set to empty. */
		mwStatus.setAcntId(acntId);
		mwStatus.setAcntNodeType(AccountModel::nodeUnknown);
		mwStatus.setYear(nullString);
	}

	return true;
}

/*!
 * @brief Set message selection.
 *
 * @param[in] msgIds Message selection.
 * @return True if state has been modified.
 */
static
bool updateMsgIds(MWStatus &mwStatus, const QSet<MsgId> &msgIds)
{
	if (mwStatus.msgIds() == msgIds) {
		/* No change. */
		return false;
	}

	mwStatus.setMsgIds(msgIds);
	return true;
}

void MWLockedStatus::setAccount(const AcntId &acntId,
    enum AccountModel::NodeType acntNodeType, const QString &year)
{
	bool aChanged = false;
	bool mChanged = false;

	{
		QWriteLocker locker(&m_lock);

		aChanged = updateAccount(m_mwStatus, acntId, acntNodeType, year);
		if (aChanged) {
			/* Clear message selection on account state change. */
			mChanged = updateMsgIds(m_mwStatus, QSet<MsgId>());
		}
	}

	/*
	 * Signal must be emitted with released lock to prevent deadlocks when
	 * handling connected slots that may want to read the value.
	 */
	if (aChanged) {
		emit accountChanged();
	}
	if (mChanged) {
		emit messageChanged();
	}
	if (aChanged || mChanged) {
		emit changed();
	}
}

void MWLockedStatus::setMsgIds(const QSet<MsgId> &msgIds)
{
	bool mChanged = false;

	{
		QWriteLocker locker(&m_lock);

		mChanged = updateMsgIds(m_mwStatus, msgIds);
	}

	/*
	 * Signal must be emitted with released lock to prevent deadlocks when
	 * handling connected slots that may want to read the value.
	 */
	if (mChanged) {
		emit messageChanged();
		emit changed();
	}
}

void MWLockedStatus::setFlagBlockIsds(bool fbi)
{
	{
		QWriteLocker locker(&m_lock);

		if (m_mwStatus.flagBlockIsds() == fbi) {
			/* No change. */
			return;
		}

		m_mwStatus.setFlagBlockIsds(fbi);
	}

	/*
	 * Signal must be emitted with released lock to prevent deadlocks when
	 * handling connected slots that may want to read the value.
	 */
	emit changed();
}

void MWLockedStatus::setFlagBlockImports(bool fbi)
{
	{
		QWriteLocker locker(&m_lock);

		if (m_mwStatus.flagBlockImports() == fbi) {
			/* No change. */
			return;
		}

		m_mwStatus.setFlagBlockImports(fbi);
	}

	/*
	 * Signal must be emitted with released lock to prevent deadlocks when
	 * handling connected slots that may want to read the value.
	 */
	emit changed();
}
