/*
 * 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 <cinttypes> /* PRId64 */
#include <cstring>
#include <limits>
#include <QByteArray>
#include <QStringBuilder>
#include <QSystemSemaphore>
#include <QtEndian>
#include <QTimer>

#include "src/datovka_shared/log/log.h"
#include "src/global.h"
#include "src/single/single_instance.h"

#define UNIQUE_SEM_ID "CZ.NIC_Datovka_(e-gov_client)_semaphore"
#define UNIQUE_MEM_ID "CZ.NIC_Datovka_(e-gov_client)_shared_mem"
#define MEM_SIZE (1024 * 1024)
#define MAX_MSG_SIZE (8 * 1024)

static const QString msgRaiseMainWindow("RaiseMainWindow");
static const QString msgCompose("Compose");

static const QChar msgTypeSep('>'); /*!< Separator used to divide message type name and the value. */

SingleInstance::SingleInstance(const QString &shMemKey, QObject *parent)
    : QObject(parent),
    m_memoryExisted(false),
    m_shMem()
{
	/*
	 * On UNIX the semaphore is not removed on crash.
	 * For now let's assume that the code does not crash here.
	 */
	QSystemSemaphore sema(UNIQUE_SEM_ID, 1);
	logInfoNL("Trying to acquire semaphore '%s'.", UNIQUE_SEM_ID);
	sema.acquire();
	logInfoNL("Semaphore '%s' acquired.", UNIQUE_SEM_ID);

	QString memKey(UNIQUE_MEM_ID);
	if (!shMemKey.isEmpty()) {
		memKey += "[" + shMemKey + "]";
	}
	logInfoNL("Shared memory key is '%s'.", memKey.toUtf8().constData());

#if !defined(Q_OS_WIN)
	/*
	 * On UNIX systems the memory is not freed upon crash.
	 * If there is any previous instance, clean it.
	 */
	{
		QSharedMemory shMem(memKey);
		if (shMem.attach()) {
			shMem.detach();
		}
	}
#endif /* !defined(Q_OS_WIN) */

	m_shMem.setKey(memKey);

	if (m_shMem.create(MEM_SIZE)) {
		m_shMem.lock();
		::std::memset(m_shMem.data(), 0, MEM_SIZE);
		m_shMem.unlock();

		/* Start reading messages. */
		QTimer *timer = new (::std::nothrow) QTimer(this);
		connect(timer, SIGNAL(timeout()), this, SLOT(checkMessage()));
		timer->start(200);

		logInfoNL("Application owns shared memory under key '%s'.",
		    memKey.toUtf8().constData());
	} else if (m_shMem.attach()) {
		m_memoryExisted = true;

		logInfoNL(
		    "Another application owns shared memory under key '%s'.",
		    memKey.toUtf8().constData());
	} else {
		logErrorNL("Cannot access shared memory under key '%s'.",
		    memKey.toUtf8().constData());
	}

	sema.release();
	logInfoNL("Semaphore '%s' released.", UNIQUE_SEM_ID);
}

bool SingleInstance::existsInSystem(void) const
{
	return m_memoryExisted;
}

/*!
 * @brief Converts message type to string description.
 *
 * @param[in] msgType Message type identifier.
 * @return String description.
 */
static
const QString &msgTypeToStr(enum SingleInstance::MsgType msgType)
{
	switch (msgType) {
	case SingleInstance::MTYPE_RAISE_MAIN_WIN:
		return msgRaiseMainWindow;
		break;
	case SingleInstance::MTYPE_COMPOSE:
		return msgCompose;
		break;
	default:
		/* Default to raise main window. */
		Q_ASSERT(0);
		return msgRaiseMainWindow;
		break;
	}
}

/*!
 * @brief Composes a message.
 *
 * @param[in] msgType Message type identifier.
 * @param[in] msgVal Message value (serialised data).
 * @return Message string.
 */
static
QString composeMessage(enum SingleInstance::MsgType msgType,
    const QString &msgVal)
{
	return msgTypeToStr(msgType) % msgTypeSep % msgVal;
}

/*!
 * @brief Reads message length from the point in memory.
 *
 * @param[in] loc Memory location.
 * @return Message length.
 */
static
qint32 readMsgLen(const char *loc)
{
	if (Q_UNLIKELY(loc == Q_NULLPTR)) {
		Q_ASSERT(0);
		return 0;
	}

	/*
	 * Cannot use
	 * sizeToRead = qFromBigEndian(*(qint32 *)loc);
	 * because of memory alignment issues.
	 */
	qint32 bi32;
	::std::memcpy(&bi32, loc, sizeof(bi32));
	return qFromBigEndian(bi32);
}

bool SingleInstance::sendMessage(enum MsgType msgType, const QString &msgVal)
{
#if 0
	/* Master process cannot send messages. */
	if (!m_memoryExisted){
		return false;
	}
#endif

	QByteArray byteArray;
	{
		const QString message(composeMessage(msgType, msgVal));
		byteArray.append(message.toUtf8());
		byteArray.append('\0');
	}

	/* Only short messages can be sent. */
	if (Q_UNLIKELY(byteArray.size() > MAX_MSG_SIZE)) {
		logErrorNL(
		    "Cannot write %" PRId64 " bytes into shared memory buffer because it exceeds the limit of %d bytes.",
		    UGLY_QINT64_CAST byteArray.size(), MAX_MSG_SIZE);
		return false;
	}

	m_shMem.lock();

	/* Find the end of the buffer. */
	char *begin = (char *) m_shMem.data();
	char *to = begin;
	while ((to - begin) < MEM_SIZE) {
		qint32 sizeToRead = readMsgLen(to);
		if (sizeToRead == 0) {
			break;
		}
		to += sizeToRead + sizeof(sizeToRead);
	}

	qint32 beSize = qToBigEndian((qint32)byteArray.size());

	/* Compute the remaining space. */
	if ((MEM_SIZE - (to - begin)) < (byteArray.size() + (int)sizeof(beSize))) {
		m_shMem.unlock();

		logErrorNL(
		    "Not enough space in shared memory to write %d bytes into.",
		    beSize);
		return false;
	}

	::std::memcpy(to, &beSize, sizeof(beSize));
	to += sizeof(beSize);
	::std::memcpy(to, byteArray.constData(), byteArray.size());

	m_shMem.unlock();

	return true;
}

#if (QT_VERSION >= QT_VERSION_CHECK(5, 10, 0))
/*!
 * @brief Converts message description string to numerical value.
 *
 * @note The preferred way to QStringView parameters is to pass by value.
 *
 * @param[in] typeStr String containing operation name.
 * @return Message type.
 */
static
enum SingleInstance::MsgType strToMsgType(const QStringView typeStr)
{
	if (typeStr == msgRaiseMainWindow) {
		return SingleInstance::MTYPE_RAISE_MAIN_WIN;
	} else if (typeStr == msgCompose) {
		return SingleInstance::MTYPE_COMPOSE;
	} else {
		Q_ASSERT(0);
		return SingleInstance::MTYPE_UNKNOWN;
	}
}

/*!
 * @brief Decomposes a message.
 *
 * @param[in]  message Received message.
 * @param[out] msgType Message type.
 * @param[out] msgValView Message value (serialised data).
 * @return True if message type has been recognised.
 */
static
bool decomposeMessage(const QString &message,
    enum SingleInstance::MsgType &msgType, QStringView &msgValView)
{
	if (Q_UNLIKELY(message.isEmpty())) {
		Q_ASSERT(0);
		msgType = SingleInstance::MTYPE_UNKNOWN;
		msgValView = QStringView();
		return false;
	}

	int sepIdx = message.indexOf(msgTypeSep);
	if (Q_UNLIKELY(sepIdx < 0)) {
		msgType = SingleInstance::MTYPE_UNKNOWN;
		msgValView = QStringView();
		return false;
	}

	msgType = strToMsgType(QStringView{message}.left(sepIdx));
	if ((sepIdx + 1) < message.size()) {
		/* Separator is not the last character in the message. */
		msgValView = QStringView{message}.mid(sepIdx + 1);
	} else {
		msgValView = QStringView();
	}
	return msgType != SingleInstance::MTYPE_UNKNOWN;
}
#else /* < Qt-5.10 */
/*!
 * @brief Converts message description string to numerical value.
 *
 * @param[in] typeStr String containing operation name.
 * @return Message type.
 */
static
enum SingleInstance::MsgType strToMsgType(const QStringRef &typeStr)
{
	if (typeStr == msgRaiseMainWindow) {
		return SingleInstance::MTYPE_RAISE_MAIN_WIN;
	} else if (typeStr == msgCompose) {
		return SingleInstance::MTYPE_COMPOSE;
	} else {
		Q_ASSERT(0);
		return SingleInstance::MTYPE_UNKNOWN;
	}
}

/*!
 * @brief Decomposes a message.
 *
 * @param[in]  message Received message.
 * @param[out] msgType Message type.
 * @param[out] msgValRef Message value (serialised data).
 * @return True if message type has been recognised.
 */
static
bool decomposeMessage(const QString &message,
    enum SingleInstance::MsgType &msgType, QStringRef &msgValRef)
{
	if (Q_UNLIKELY(message.isEmpty())) {
		Q_ASSERT(0);
		msgType = SingleInstance::MTYPE_UNKNOWN;
		msgValRef.clear();
		return false;
	}

	int sepIdx = message.indexOf(msgTypeSep);
	if (Q_UNLIKELY(sepIdx < 0)) {
		msgType = SingleInstance::MTYPE_UNKNOWN;
		msgValRef.clear();
		return false;
	}

	msgType = strToMsgType(message.leftRef(sepIdx));
	if ((sepIdx + 1) < message.size()) {
		/* Separator is not the last character in the message. */
		msgValRef = message.midRef(sepIdx + 1);
	} else {
		msgValRef.clear();
	}
	return msgType != SingleInstance::MTYPE_UNKNOWN;
}
#endif /* >= Qt-5.10 */

void SingleInstance::checkMessage(void)
{
	m_shMem.lock();

	bool clearBuffer = false;
	char *begin = (char *) m_shMem.data();
	char *from = begin;

	while ((from - begin) < MEM_SIZE) {
		qint32 sizeToRead = readMsgLen(from);
		if (sizeToRead == 0) {
			break;
		}

		clearBuffer = true; /* Buffer should be cleared. */

		from += sizeof(sizeToRead);
		QByteArray byteArray(from, sizeToRead);
		from += sizeToRead;

		const QString message(
		    QString::fromUtf8((byteArray + '\0').constData()));

		enum MsgType msgType = MTYPE_UNKNOWN;
#if (QT_VERSION >= QT_VERSION_CHECK(5, 10, 0))
		QStringView msgVal;
#else /* < Qt-5.10 */
		QStringRef msgVal;
#endif /* >= Qt-5.10 */
		if (decomposeMessage(message, msgType, msgVal)) {
			emit GlobInstcs::snglInstEmitterPtr->messageReceived(
			    msgType, msgVal.toString());
		}
	}

	if (clearBuffer) {
		::std::memset(m_shMem.data(), 0, MEM_SIZE);
	}

	m_shMem.unlock();
}
