/*
 * Copyright (C) 2014-2026 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 <QMessageBox>
#include <QHash>
#include <QString>

#include "src/datovka_shared/log/log.h"
#include "src/dimensions/dimensions.h"
#include "src/global.h"
#include "src/gui/dlg_note.h"
#include "src/gui/dlg_msg_box_detail.h"
#include "src/io/tag_container.h"
#include "src/json/message_id.h"
#include "src/json/note_assignment_hash.h"
#include "src/settings/prefs_specific.h"
#include "ui_dlg_note.h"

static const QString dlgName("note");

/* Exclusive access to hash of opened note dialogues. */
static QMutex displayMutex;
static QHash<Json::MsgId1, DlgNote *> openedDialogues;

/*!
 * @brief Create Json::MsgId1.
 */
static inline
Json::MsgId1 _jsonMsgId1(const AcntId &acntId, const qint64 dmId)
{
	return Json::MsgId1(
	    acntId.testing() ? Isds::Type::BOOL_TRUE : Isds::Type::BOOL_FALSE,
	    dmId);
}

/*!
 * @brief Find out if message note exists in database.
 *
 * @param[in] acntId Account identifier.
 * @param[in] tagCont Pointer to tag container.
 * @param[in] dmId Message id
 * @param[out] entry Note entry.
 * @return True when message has its note.
 */
static
bool getMessageNoteData(const AcntId &acntId, TagContainer *tagCont,
    const qint64 dmId, Json::NoteEntry &entry)
{
	/* Notes are saved in tag db. */

	Json::MsgId1List jsonMsgIdList;
	jsonMsgIdList.append(_jsonMsgId1(acntId, dmId));

	Json::NoteAssignmentHash assignments;

	if (tagCont->getMessageNotes(jsonMsgIdList, assignments)) {
		if (1 == assignments.size()) {
			const Json::MsgId1 msgId = assignments.keys().first();
			const Json::NoteEntryList entries = assignments[msgId];
			if (1 == entries.size()) {
				entry = entries.first();
				return true;
			}
		}
	}

	return false;
}

DlgNote::DlgNote(const AcntId &acntId, TagContainer *tagCont, qint64 dmId,
    QWidget *parent)
    : QDialog(parent),
    m_ui(new (::std::nothrow) Ui::DlgNote),
    m_acntId(acntId),
    m_dmId(dmId),
    m_tagContPtr(tagCont),
    m_editedNoteEntry(),
    m_interruptClosing(true),
    m_dfltSize()
{
	m_ui->setupUi(this);

	m_ui->editNoteLabel->setText(
	    tr("Note assigned to message %1:").arg(m_dmId));

	connect(m_ui->notePlainTextEdit, SIGNAL(textChanged()),
	    this, SLOT(onTextChanged()));

	connect(m_ui->assignNoteButton, SIGNAL(clicked()),
	    this, SLOT(assignNoteAndClose()));
	connect(m_ui->deleteNoteButton, SIGNAL(clicked()),
	    this, SLOT(deleteNoteAndClose()));
	connect(m_ui->closeButton, SIGNAL(clicked()),
	    this, SLOT(close()));

	setButtons(getMessageNoteData(m_acntId, m_tagContPtr, m_dmId, m_editedNoteEntry));
	m_ui->notePlainTextEdit->setPlainText(m_editedNoteEntry.content());

	onTextChanged();

	if (Q_NULLPTR != m_tagContPtr) {
		databaseConnectActions();
	}

	m_dfltSize = this->size();
	{
		const QSize newSize = Dimensions::dialogueSize(this,
		    PrefsSpecific::dlgSize(*GlobInstcs::prefsPtr, dlgName),
		    m_dfltSize);
		if (newSize.isValid()) {
			this->resize(newSize);
		}
	}
}

DlgNote::~DlgNote(void)
{
	PrefsSpecific::setDlgSize(*GlobInstcs::prefsPtr, dlgName,
	    this->size(), m_dfltSize);

	{
		displayMutex.lock();
		openedDialogues.remove(_jsonMsgId1(m_acntId, m_dmId));
		displayMutex.unlock();
	}

	delete m_ui;
}

void DlgNote::editNote(const AcntId &acntId, TagContainer *tagCont,
    qint64 dmId, QWidget *parent)
{
	if (Q_UNLIKELY((!acntId.isValid())
	        || (tagCont == Q_NULLPTR) || (dmId < 0))) {
		Q_ASSERT(0);
		return;
	}

	DlgNote dlg(acntId, tagCont, dmId, parent);

	dlg.exec();
}

void DlgNote::editNoteNonModal(const AcntId &acntId, TagContainer *tagCont,
    qint64 dmId, QWidget *parent)
{
	if (Q_UNLIKELY((!acntId.isValid())
	        || (tagCont == Q_NULLPTR) || (dmId < 0))) {
		Q_ASSERT(0);
		return;
	}

	DlgNote *dlg = Q_NULLPTR;
	/*
	 * Find opened dialogue for given message. Create and remember if not
	 * found.
	 */
	{
		displayMutex.lock();
		const Json::MsgId1 msgId1 = _jsonMsgId1(acntId, dmId);
		dlg = openedDialogues.value(msgId1, Q_NULLPTR);

		if (Q_NULLPTR == dlg) {
			dlg = new (::std::nothrow) DlgNote(acntId, tagCont, dmId, parent);
			if (Q_NULLPTR != dlg) {
				openedDialogues[msgId1] = dlg;
			}
		}
		displayMutex.unlock();
	}

	if (Q_UNLIKELY(dlg == Q_NULLPTR)) {
		return;
	}

	dlg->setAttribute(Qt::WA_DeleteOnClose, true);
	dlg->show();
	dlg->raise();
}

void DlgNote::done(int resultCode)
{
	/* Don't ask if global halt requested. */
	if ((!GlobInstcs::req_halt) && m_interruptClosing) {
		/* Ask if new text written of when note text modified. */
		const QString editedText = m_ui->notePlainTextEdit->toPlainText();
		const bool haveNewText = m_editedNoteEntry.content().isEmpty()
		    && (!editedText.isEmpty());
		const bool modifiedText = (!m_editedNoteEntry.content().isEmpty())
		    && (m_editedNoteEntry.content() != editedText);
		if (haveNewText || modifiedText) {
			int ret = DlgMsgBoxDetail::message(this, QMessageBox::Question,
			    tr("Save Changes?"),
			    tr("Save the changes and close the dialogue?"),
			    QString(), QString(),
			    QMessageBox::Save | QMessageBox::Discard | QMessageBox::Abort, QMessageBox::Save);
			if (ret == QMessageBox::Abort) {
				/* Don't close the dialogue. */
				return;
			}

			if (ret == QMessageBox::Save) {
				bool ret = false;
				if (!editedText.isEmpty()) {
					ret = assignNote();
				} else {
					ret = deleteNote();
				}
				if (Q_UNLIKELY(!ret)) {
					/*
					 * Don't close the dialogue if saving
					 * fails.
					 */
					return;
				}
			}
		}
	}

	/* Close the dialogue. */
	QDialog::done(resultCode);
}

void DlgNote::assignNoteAndClose(void)
{
	if (assignNote()) {
		m_interruptClosing = false;
		close();
	}
}

void DlgNote::deleteNoteAndClose(void)
{
	if (Q_UNLIKELY(m_editedNoteEntry.id() < 0)) {
		Q_ASSERT(0);
		return;
	}

	QMessageBox::StandardButton reply = QMessageBox::question(this,
	    tr("Delete Assigned Note?"),
	    tr("Are you sure you want to delete the note assigned to the selected message?"),
	    QMessageBox::Yes | QMessageBox::No, QMessageBox::No);

	if (reply == QMessageBox::Yes) {
		if (deleteNote()) {
			m_interruptClosing = false;
			close();
		}
	}
}

void DlgNote::onTextChanged(void)
{
	bool hasText = !m_ui->notePlainTextEdit->toPlainText().isEmpty();
	m_ui->assignNoteButton->setEnabled(hasText);
}

void DlgNote::watchNotesInserted(const Json::NoteEntryList &entries)
{
	Q_UNUSED(entries);
}

void DlgNote::watchNotesUpdated(const Json::NoteEntryList &entries)
{
	for (const Json::NoteEntry &entry : entries) {
		if (entry.id() == m_editedNoteEntry.id()) {
			/* Update original entry to the database content.*/
			m_editedNoteEntry = entry;
		}
	}
}

void DlgNote::watchNotesDeleted(const Json::Int64StringList &ids)
{
	for (const qint64 id : ids) {
		if (id == m_editedNoteEntry.id()) {
			/* Delete original entry and update buttons. */
			m_editedNoteEntry = Json::NoteEntry();
			setButtons(false);
		}
	}
}

void DlgNote::watchNoteAssignmentChanged(
    const Json::NoteAssignmentList &assignments)
{
	const Json::MsgId1 msgId1 = _jsonMsgId1(m_acntId, m_dmId);

	for (const Json::NoteAssignment &assignment : assignments) {
		if ((assignment.testEnv() == msgId1.testEnv())
		    && (assignment.dmId() == m_dmId)) {
			const Json::NoteEntryList &entries = assignment.entries();
			if (entries.size() == 1) {
				/* Update original entry and update buttons.*/
				m_editedNoteEntry = entries.at(0);
				setButtons(true);
			}
		}
	}
}

void DlgNote::watchTagContConnected(void)
{
	/* TODO */
}

void DlgNote::watchTagContDisconnected(void)
{
	/* TODO */
}

void DlgNote::watchTagContReset(void)
{
	/* TODO */
}

void DlgNote::setButtons(bool existentNote)
{
	if (existentNote) {
		m_ui->assignNoteButton->setText(tr("Update"));
		m_ui->deleteNoteButton->setVisible(true);
	} else {
		m_ui->assignNoteButton->setText(tr("Add"));
		m_ui->deleteNoteButton->setVisible(false);
	}
}

bool DlgNote::assignNote(void)
{
	bool ret = false;

	if (Q_UNLIKELY(Q_NULLPTR == m_tagContPtr)) {
		Q_ASSERT(0);
		return ret;
	}

	QString noteText = m_ui->notePlainTextEdit->toPlainText();
	if (Q_UNLIKELY(noteText.isEmpty())) {
		logErrorNL("%s", "Cannot assign empty string as a note to a message.");
		return ret;
	}

	/* Notes are saved in tags db. */
	const qint64 editedNoteId = m_editedNoteEntry.id();
	const Json::NoteEntry noteEntry(editedNoteId, noteText);
	const Json::MsgId1 msgId1 = _jsonMsgId1(m_acntId, m_dmId);
	if (editedNoteId < 0) {
		ret = m_tagContPtr->setNote(noteEntry, msgId1);
	} else {
		ret = m_tagContPtr->updateNotes({noteEntry});
	}

	if (Q_UNLIKELY(!ret)) {
		QMessageBox::critical(this, tr("Cannot Store Note"),
		    tr("Cannot save the note for message %1.").arg(m_dmId));
	}

	return ret;
}

bool DlgNote::deleteNote(void)
{
	/* Notes are saved in tags db. */
	bool ret = m_tagContPtr->deleteNotes({m_editedNoteEntry.id()});

	if (Q_UNLIKELY(!ret)) {
		QMessageBox::critical(this, tr("Cannot Delete Note"),
		    tr("Cannot delete the note of message %1.").arg(m_dmId));
	}

	return ret;
}

void DlgNote::databaseConnectActions(void)
{
	connect(m_tagContPtr, SIGNAL(notesInserted(Json::NoteEntryList)),
	    this, SLOT(watchNotesInserted(Json::NoteEntryList)));
	connect(m_tagContPtr, SIGNAL(notesUpdated(Json::NoteEntryList)),
	    this, SLOT(watchNotesUpdated(Json::NoteEntryList)));
	connect(m_tagContPtr, SIGNAL(notesDeleted(Json::Int64StringList)),
	    this, SLOT(watchNotesDeleted(Json::Int64StringList)));
	connect(m_tagContPtr, SIGNAL(noteAssignmentChanged(Json::NoteAssignmentList)),
	    this, SLOT(watchNoteAssignmentChanged(Json::NoteAssignmentList)));
	connect(m_tagContPtr, SIGNAL(connected()),
	    this, SLOT(watchTagContConnected()));
	connect(m_tagContPtr, SIGNAL(disconnected()),
	    this, SLOT(watchTagContDisconnected()));
	connect(m_tagContPtr, SIGNAL(reset()),
	    this, SLOT(watchTagContReset()));
}
