/*
 * 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 "src/datovka_shared/json/basic.h"
#include "src/datovka_shared/log/log.h"
#include "src/io/tag_client.h"
#include "src/io/tag_container.h"
#include "src/io/tag_db.h"

TagContainer::TagContainer(enum Backend backend)
    : m_backend(backend),
    m_tagDb(new (::std::nothrow) TagDb("tagDb", false)),
    m_tagClient(new (::std::nothrow) TagClient)
{
	if (Q_UNLIKELY(Q_NULLPTR == m_tagDb)) {
		logErrorNL("%s", "Cannot allocate tag db.");
	}
	if (Q_UNLIKELY(Q_NULLPTR == m_tagClient)) {
		logErrorNL("%s", "Cannot allocate tag client.");
	}

#if defined(HAVE_WEBSOCKETS)
#else /* !defined(HAVE_WEBSOCKETS) */
	/* Fall back to local database if tag client requested. */
	if (Q_UNLIKELY(m_backend == BACK_CLIENT)) {
		logErrorNL("%s", "Cannot use tag client.");
		m_backend = BACK_DB;
	}
#endif /* defined(HAVE_WEBSOCKETS) */

	selectBackend(m_backend);
}

TagContainer::~TagContainer(void)
{
	delete m_tagDb;
	delete m_tagClient;
}

enum TagContainer::Backend TagContainer::backend(void) const
{
	return m_backend;
}

void TagContainer::selectBackend(enum Backend backend)
{
	if (m_tagDb != Q_NULLPTR) {
		m_tagDb->disconnect(Q_NULLPTR, this, Q_NULLPTR);
	}
	if (m_tagClient != Q_NULLPTR) {
		m_tagClient->disconnect(Q_NULLPTR, this, Q_NULLPTR);
	}

#if defined(HAVE_WEBSOCKETS)
#else /* !defined(HAVE_WEBSOCKETS) */
	/* Fall back to local database if tag client requested. */
	if (Q_UNLIKELY(backend == BACK_CLIENT)) {
		logErrorNL("%s", "Cannot use tag client.");
		backend = BACK_DB;
	}
#endif /* defined(HAVE_WEBSOCKETS) */

	/* Force reset on backend change. */
	const bool emitReset = (m_backend != backend);

	switch (backend) {
	case BACK_DB:
		if (m_tagDb != Q_NULLPTR) {
			connect(m_tagDb, SIGNAL(tagsInserted(Json::TagEntryList)),
			    this, SLOT(watchTagsInserted(Json::TagEntryList)));
			connect(m_tagDb, SIGNAL(tagsUpdated(Json::TagEntryList)),
			    this, SLOT(watchTagsUpdated(Json::TagEntryList)));
			connect(m_tagDb, SIGNAL(tagsDeleted(Json::Int64StringList)),
			    this, SLOT(watchTagsDeleted(Json::Int64StringList)));
			connect(m_tagDb, SIGNAL(tagAssignmentChanged(Json::TagAssignmentList)),
			    this, SLOT(watchTagAssignmentChanged(Json::TagAssignmentList)));
			m_backend = backend;
		} else {
			logErrorNL("%s", "Uninitialised tag database backend.");
			Q_ASSERT(0);
		}
		break;
	case BACK_CLIENT:
		if (m_tagClient != Q_NULLPTR) {
			connect(m_tagClient, SIGNAL(connected()),
			    this, SLOT(watchConnected()));
			connect(m_tagClient, SIGNAL(disconnected()),
			    this, SLOT(watchDisconnected()));
			connect(m_tagClient, SIGNAL(reset()),
			    this, SLOT(watchReset()));
			connect(m_tagClient, SIGNAL(tagsInserted(Json::TagEntryList)),
			    this, SLOT(watchTagsInserted(Json::TagEntryList)));
			connect(m_tagClient, SIGNAL(tagsUpdated(Json::TagEntryList)),
			    this, SLOT(watchTagsUpdated(Json::TagEntryList)));
			connect(m_tagClient, SIGNAL(tagsDeleted(Json::Int64StringList)),
			    this, SLOT(watchTagsDeleted(Json::Int64StringList)));
			connect(m_tagClient, SIGNAL(tagAssignmentChanged(Json::TagAssignmentList)),
			    this, SLOT(watchTagAssignmentChanged(Json::TagAssignmentList)));
			m_backend = backend;
		} else {
			logErrorNL("%s", "Uninitialised tag client backend.");
			Q_ASSERT(0);
		}
		break;
	default:
		logErrorNL("Unknown backend type value '%d'.", m_backend);
		Q_ASSERT(0);
		break;
	}

	if (emitReset) {
		emit reset();
	}
}

TagDb *TagContainer::backendDb(void)
{
	return m_tagDb;
}

TagClient *TagContainer::backendClient(void)
{
	return m_tagClient;
}

bool TagContainer::isResponding(void)
{
	return ((m_backend == BACK_DB) && (m_tagDb != Q_NULLPTR)) ||
	   ((m_backend == BACK_CLIENT) && (m_tagClient != Q_NULLPTR) && (m_tagClient->isConnected()));
}

bool TagContainer::insertTags(const Json::TagEntryList &entries)
{
	switch (m_backend) {
	case BACK_DB:
		if (m_tagDb != Q_NULLPTR) {
			return m_tagDb->insertTags(entries);
		} else {
			logErrorNL("%s", "Uninitialised tag database backend.");
			Q_ASSERT(0);
			return false;
		}
		break;
	case BACK_CLIENT:
		if (m_tagClient != Q_NULLPTR) {
			return m_tagClient->insertTags(entries);
		} else {
			logErrorNL("%s", "Uninitialised tag client backend.");
			Q_ASSERT(0);
			return false;
		}
		break;
	default:
		logErrorNL("Unknown backend type value '%d'.", m_backend);
		Q_ASSERT(0);
		return false;
		break;
	}
}

bool TagContainer::updateTags(const Json::TagEntryList &entries)
{
	switch (m_backend) {
	case BACK_DB:
		if (m_tagDb != Q_NULLPTR) {
			return m_tagDb->updateTags(entries);
		} else {
			logErrorNL("%s", "Uninitialised tag database backend.");
			Q_ASSERT(0);
			return false;
		}
		break;
	case BACK_CLIENT:
		if (m_tagClient != Q_NULLPTR) {
			return m_tagClient->updateTags(entries);
		} else {
			logErrorNL("%s", "Uninitialised tag client backend.");
			Q_ASSERT(0);
			return false;
		}
		break;
	default:
		logErrorNL("Unknown backend type value '%d'.", m_backend);
		Q_ASSERT(0);
		return false;
		break;
	}
}

bool TagContainer::deleteTags(const Json::Int64StringList &ids)
{
	switch (m_backend) {
	case BACK_DB:
		if (m_tagDb != Q_NULLPTR) {
			return m_tagDb->deleteTags(ids);
		} else {
			logErrorNL("%s", "Uninitialised tag database backend.");
			Q_ASSERT(0);
			return false;
		}
		break;
	case BACK_CLIENT:
		if (m_tagClient != Q_NULLPTR) {
			return m_tagClient->deleteTags(ids);
		} else {
			logErrorNL("%s", "Uninitialised tag client backend.");
			Q_ASSERT(0);
			return false;
		}
		break;
	default:
		logErrorNL("Unknown backend type value '%d'.", m_backend);
		Q_ASSERT(0);
		return false;
		break;
	}
}

bool TagContainer::getTagData(const Json::Int64StringList &ids,
    Json::TagEntryList &entries)
{
	switch (m_backend) {
	case BACK_DB:
		if (m_tagDb != Q_NULLPTR) {
			return m_tagDb->getTagData(ids, entries);
		} else {
			logErrorNL("%s", "Uninitialised tag database backend.");
			Q_ASSERT(0);
			return false;
		}
		break;
	case BACK_CLIENT:
		if (m_tagClient != Q_NULLPTR) {
			return m_tagClient->getTagData(ids, entries);
		} else {
			logErrorNL("%s", "Uninitialised tag client backend.");
			Q_ASSERT(0);
			return false;
		}
		break;
	default:
		logErrorNL("Unknown backend type value '%d'.", m_backend);
		Q_ASSERT(0);
		return false;
		break;
	}
}

bool TagContainer::getAllTags(Json::TagEntryList &entries)
{
	switch (m_backend) {
	case BACK_DB:
		if (m_tagDb != Q_NULLPTR) {
			return m_tagDb->getAllTags(entries);
		} else {
			logErrorNL("%s", "Uninitialised tag database backend.");
			Q_ASSERT(0);
			return false;
		}
		break;
	case BACK_CLIENT:
		if (m_tagClient != Q_NULLPTR) {
			return m_tagClient->getAllTags(entries);
		} else {
			logErrorNL("%s", "Uninitialised tag client backend.");
			Q_ASSERT(0);
			return false;
		}
		break;
	default:
		logErrorNL("Unknown backend type value '%d'.", m_backend);
		Q_ASSERT(0);
		return false;
		break;
	}
}

bool TagContainer::getTagAssignmentCounts(const Json::Int64StringList &ids,
    Json::TagIdToAssignmentCountHash &assignmentCounts)
{
	switch (m_backend) {
	case BACK_DB:
		if (m_tagDb != Q_NULLPTR) {
			return m_tagDb->getTagAssignmentCounts(ids, assignmentCounts);
		} else {
			logErrorNL("%s", "Uninitialised tag database backend.");
			Q_ASSERT(0);
			return false;
		}
		break;
	case BACK_CLIENT:
		if (m_tagClient != Q_NULLPTR) {
			return m_tagClient->getTagAssignmentCounts(ids, assignmentCounts);
		} else {
			logErrorNL("%s", "Uninitialised tag client backend.");
			Q_ASSERT(0);
			return false;
		}
		break;
	default:
		logErrorNL("Unknown backend type value '%d'.", m_backend);
		Q_ASSERT(0);
		return false;
		break;
	}
}

bool TagContainer::getMessageTags(const Json::TagMsgIdList &msgIds,
    Json::TagAssignmentHash &assignments)
{
	switch (m_backend) {
	case BACK_DB:
		if (m_tagDb != Q_NULLPTR) {
			return m_tagDb->getMessageTags(msgIds, assignments);
		} else {
			logErrorNL("%s", "Uninitialised tag database backend.");
			Q_ASSERT(0);
			return false;
		}
		break;
	case BACK_CLIENT:
		if (m_tagClient != Q_NULLPTR) {
			return m_tagClient->getMessageTags(msgIds, assignments);
		} else {
			logErrorNL("%s", "Uninitialised tag client backend.");
			Q_ASSERT(0);
			return false;
		}
		break;
	default:
		logErrorNL("Unknown backend type value '%d'.", m_backend);
		Q_ASSERT(0);
		return false;
		break;
	}
}

bool TagContainer::removeAllTagsFromMsgs(const Json::TagMsgIdList &msgIds)
{
	switch (m_backend) {
	case BACK_DB:
		if (m_tagDb != Q_NULLPTR) {
			return m_tagDb->removeAllTagsFromMsgs(msgIds);
		} else {
			logErrorNL("%s", "Uninitialised tag database backend.");
			Q_ASSERT(0);
			return false;
		}
		break;
	case BACK_CLIENT:
		if (m_tagClient != Q_NULLPTR) {
			return m_tagClient->removeAllTagsFromMsgs(msgIds);
		} else {
			logErrorNL("%s", "Uninitialised tag client backend.");
			Q_ASSERT(0);
			return false;
		}
		break;
	default:
		logErrorNL("Unknown backend type value '%d'.", m_backend);
		Q_ASSERT(0);
		return false;
		break;
	}
}

bool TagContainer::assignTagsToMsgs(const Json::TagAssignmentCommand &assignments)
{
	switch (m_backend) {
	case BACK_DB:
		if (m_tagDb != Q_NULLPTR) {
			return m_tagDb->assignTagsToMsgs(assignments);
		} else {
			logErrorNL("%s", "Uninitialised tag database backend.");
			Q_ASSERT(0);
			return false;
		}
		break;
	case BACK_CLIENT:
		if (m_tagClient != Q_NULLPTR) {
			return m_tagClient->assignTagsToMsgs(assignments);
		} else {
			logErrorNL("%s", "Uninitialised tag client backend.");
			Q_ASSERT(0);
			return false;
		}
		break;
	default:
		logErrorNL("Unknown backend type value '%d'.", m_backend);
		Q_ASSERT(0);
		return false;
		break;
	}
}

bool TagContainer::removeTagsFromMsgs(const Json::TagAssignmentCommand &assignments)
{
	switch (m_backend) {
	case BACK_DB:
		if (m_tagDb != Q_NULLPTR) {
			return m_tagDb->removeTagsFromMsgs(assignments);
		} else {
			logErrorNL("%s", "Uninitialised tag database backend.");
			Q_ASSERT(0);
			return false;
		}
		break;
	case BACK_CLIENT:
		if (m_tagClient != Q_NULLPTR) {
			return m_tagClient->removeTagsFromMsgs(assignments);
		} else {
			logErrorNL("%s", "Uninitialised tag client backend.");
			Q_ASSERT(0);
			return false;
		}
		break;
	default:
		logErrorNL("Unknown backend type value '%d'.", m_backend);
		Q_ASSERT(0);
		return false;
		break;
	}
}

bool TagContainer::getMsgIdsContainSearchTagText(
    const Json::TagTextSearchRequestList &requests,
    Json::TagTextSearchRequestToIdenfifierHash &responses)
{
	switch (m_backend) {
	case BACK_DB:
		if (m_tagDb != Q_NULLPTR) {
			return m_tagDb->getMsgIdsContainSearchTagText(requests, responses);
		} else {
			logErrorNL("%s", "Uninitialised tag database backend.");
			Q_ASSERT(0);
			return false;
		}
		break;
	case BACK_CLIENT:
		if (m_tagClient != Q_NULLPTR) {
			return m_tagClient->getMsgIdsContainSearchTagText(requests, responses);
		} else {
			logErrorNL("%s", "Uninitialised tag client backend.");
			Q_ASSERT(0);
			return false;
		}
		break;
	default:
		logErrorNL("Unknown backend type value '%d'.", m_backend);
		Q_ASSERT(0);
		return false;
		break;
	}
}

void TagContainer::watchConnected(void)
{
	emit connected();
}

void TagContainer::watchDisconnected(void)
{
	emit disconnected();
}

void TagContainer::watchReset(void)
{
	emit reset();
}

void TagContainer::watchTagsInserted(const Json::TagEntryList &entries)
{
	emit tagsInserted(entries);
}

void TagContainer::watchTagsUpdated(const Json::TagEntryList &entries)
{
	emit tagsUpdated(entries);
}

void TagContainer::watchTagsDeleted(const Json::Int64StringList &ids)
{
	emit tagsDeleted(ids);
}

void TagContainer::watchTagAssignmentChanged(const Json::TagAssignmentList &assignments)
{
	emit tagAssignmentChanged(assignments);
}
