/*
 * 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 <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonValue>
#include <utility> /* ::std::move */

#include "src/datovka_shared/json/helper.h"
#include "src/datovka_shared/log/log.h"
#include "src/datovka_shared/records_management/json/upload_hierarchy.h"

static
const QString keyName("name");
static
const QString keyId("id");
static
const QString keyMetadata("metadata");
static
const QString keyCaseId("case_id");
static
const QString keyCourtCaseId("court_case_id");
static
const QString keySub("sub");

RecMgmt::UploadHierarchyResp::NodeEntry::NodeEntry(void)
    : m_super(Q_NULLPTR),
    m_name(),
    m_id(),
    m_metadata(),
    m_caseId(),
    m_courtCaseId(),
    m_sub()
{
}

const RecMgmt::UploadHierarchyResp::NodeEntry *
    RecMgmt::UploadHierarchyResp::NodeEntry::super(void) const
{
	return m_super;
}

const QString &RecMgmt::UploadHierarchyResp::NodeEntry::name(void) const
{
	return m_name;
}

const QString &RecMgmt::UploadHierarchyResp::NodeEntry::id(void) const
{
	return m_id;
}

const QStringList &RecMgmt::UploadHierarchyResp::NodeEntry::metadata(void) const
{
	return m_metadata;
}

const QStringList &RecMgmt::UploadHierarchyResp::NodeEntry::caseId(void) const
{
	return m_caseId;
}

const QStringList &RecMgmt::UploadHierarchyResp::NodeEntry::courtCaseId(void) const
{
	return m_courtCaseId;
}

const QList<RecMgmt::UploadHierarchyResp::NodeEntry *> &
    RecMgmt::UploadHierarchyResp::NodeEntry::sub(void) const
{
	return m_sub;
}

RecMgmt::UploadHierarchyResp::NodeEntry *
    RecMgmt::UploadHierarchyResp::NodeEntry::copyRecursive(
        const NodeEntry *root)
{
	if (Q_UNLIKELY(root == Q_NULLPTR)) {
		Q_ASSERT(0);
		return Q_NULLPTR;
	}

	NodeEntry *newRoot = new (::std::nothrow) NodeEntry();
	if (Q_UNLIKELY(newRoot == Q_NULLPTR)) {
		return Q_NULLPTR;
	}

	newRoot->m_name = root->m_name;
	newRoot->m_id = root->m_id;
	newRoot->m_metadata = root->m_metadata;
	newRoot->m_caseId = root->m_caseId;
	newRoot->m_courtCaseId = root->m_courtCaseId;

	foreach (const NodeEntry *sub, root->m_sub) {
		if (Q_UNLIKELY(sub == Q_NULLPTR)) {
			Q_ASSERT(0);
			deleteRecursive(newRoot);
			return Q_NULLPTR;
		}
		NodeEntry *newSub = copyRecursive(sub);
		if (Q_UNLIKELY(newSub == Q_NULLPTR)) {
			deleteRecursive(newRoot);
			return Q_NULLPTR;
		}

		newSub->m_super = newRoot;
		newRoot->m_sub.append(newSub);
	}

	return newRoot;
}

void RecMgmt::UploadHierarchyResp::NodeEntry::deleteRecursive(NodeEntry *root)
{
	if (root == Q_NULLPTR) {
		return;
	}

	foreach (NodeEntry *entry, root->m_sub) {
		deleteRecursive(entry);
	}

	delete root;
}

RecMgmt::UploadHierarchyResp::NodeEntry *
    RecMgmt::UploadHierarchyResp::NodeEntry::fromJsonRecursive(
        const QJsonObject *jsonObj, bool &ok, bool acceptNullName)
{
	if (Q_UNLIKELY(jsonObj == Q_NULLPTR)) {
		Q_ASSERT(0);
		ok = false;
		return Q_NULLPTR;
	}

	NodeEntry *newEntry = new (::std::nothrow) NodeEntry();
	if (Q_UNLIKELY(newEntry == Q_NULLPTR)) {
		ok = false;
		return Q_NULLPTR;
	}

	if (!Json::Helper::readString(*jsonObj, keyName, newEntry->m_name,
	        acceptNullName ? Json::Helper::ACCEPT_NULL : Json::Helper::ACCEPT_VALID)) {
		ok = false;
		deleteRecursive(newEntry);
		return Q_NULLPTR;
	}
	if (!Json::Helper::readString(*jsonObj, keyId, newEntry->m_id,
	        Json::Helper::ACCEPT_NULL)) {
		ok = false;
		deleteRecursive(newEntry);
		return Q_NULLPTR;
	}
	if (!Json::Helper::readStringList(*jsonObj, keyMetadata,
	        newEntry->m_metadata, Json::Helper::ACCEPT_VALID)) {
		ok = false;
		deleteRecursive(newEntry);
		return Q_NULLPTR;
	}
	if (!Json::Helper::readStringList(*jsonObj, keyCaseId,
	        newEntry->m_caseId, Json::Helper::ACCEPT_MISSING)) {
		ok = false;
		deleteRecursive(newEntry);
		return Q_NULLPTR;
	}
	if (!Json::Helper::readStringList(*jsonObj, keyCourtCaseId,
	        newEntry->m_courtCaseId, Json::Helper::ACCEPT_MISSING)) {
		ok = false;
		deleteRecursive(newEntry);
		return Q_NULLPTR;
	}

	{
		QJsonArray jsonArr;
		if (!Json::Helper::readArray(*jsonObj, keySub, jsonArr,
		        Json::Helper::ACCEPT_VALID)) {
			ok = false;
			deleteRecursive(newEntry);
			return Q_NULLPTR;
		}

		foreach (const QJsonValue &jsonVal, jsonArr) {
			if (!jsonVal.isObject()) {
				logErrorNL("%s",
				    "Sub-node array holds a non-object value.");
				ok = false;
				deleteRecursive(newEntry);
				return Q_NULLPTR;
			}

			QJsonObject jsonSubObj(jsonVal.toObject());
			NodeEntry *subNewEntry =
			    fromJsonRecursive(&jsonSubObj, ok, false);
			if (!ok) {
				/* ok = false; */
				deleteRecursive(newEntry);
				return Q_NULLPTR;
			}
			Q_ASSERT(subNewEntry != Q_NULLPTR);

			subNewEntry->m_super = newEntry;
			newEntry->m_sub.append(subNewEntry);
		}
	}

	ok = true;
	return newEntry;
}

/*!
 * @brief Append a JSON object into sub-nodes.
 *
 * @param[in,out] jsonNode Not to append a sub-node to.
 * @param[in]     jsnoSubNode Sub-node to be appended.
 * @return True on success, false else.
 */
static
bool jsonHierarchyAppendSub(QJsonObject &jsonNode,
    const QJsonObject &jsonSubNode)
{
	QJsonObject::iterator it(jsonNode.find(keySub));
	if (it == jsonNode.end()) {
		return false;
	}

	QJsonValueRef arrRef(it.value());
	if (!arrRef.isArray()) {
		return false;
	}

	/* Sub-node must have a name. */
	{
		QJsonObject::const_iterator sit(jsonSubNode.constFind(keyName));
		if ((sit == jsonSubNode.constEnd()) ||
		    !sit.value().isString() ||
		    sit.value().toString().isEmpty()) {
			return false;
		}
	}

#if 0
	/* QJsonArray::operator+() is not available in Qt 5.2. */
	arrRef = arrRef.toArray() + jsonSubNode;
#else
	QJsonArray arr(arrRef.toArray());
	arr.append(jsonSubNode);
	arrRef = arr;
#endif
	return true;
}

/*!
 * @brief Creates a JSON object representing an upload hierarchy node with
 *     empty sub-node list.
 *
 * @param[in] name Node name.
 * @param[in] id Node identifier.
 * @param[in] metadata List of metadata.
 * @param[in] caseId List of case identifiers.
 * @param[in] cousrtCaseId List of court case identifiers.
 * @return JSON object.
 */
static
QJsonObject jsonHierarchyNode(const QString &name, const QString &id,
    const QStringList &metadata, const QStringList &caseId,
    const QStringList &courtCaseId)
{
	QJsonObject jsonObj;
	/* Intentionally using isEmpty() instead of isNull(). */
	jsonObj.insert(keyName, !name.isEmpty() ? name : QJsonValue());
	jsonObj.insert(keyId, !id.isEmpty() ? id : QJsonValue());
	jsonObj.insert(keyMetadata, QJsonArray::fromStringList(metadata));
	if (!caseId.isEmpty()) {
		jsonObj.insert(keyCaseId, QJsonArray::fromStringList(caseId));
	}
	if (!courtCaseId.isEmpty()) {
		jsonObj.insert(keyCourtCaseId, QJsonArray::fromStringList(courtCaseId));
	}
	jsonObj.insert(keySub, QJsonArray());

	return jsonObj;
}

bool RecMgmt::UploadHierarchyResp::NodeEntry::toJsonRecursive(
    QJsonObject *jsonObj, const QList<NodeEntry *> &uhrList)
{
	if (Q_UNLIKELY(jsonObj == Q_NULLPTR)) {
		Q_ASSERT(0);
		return false;
	}

	foreach (const NodeEntry *entry, uhrList) {
		if (Q_UNLIKELY(entry == Q_NULLPTR)) {
			Q_ASSERT(0);
			return false;
		}
		QJsonObject jsonSubObj(jsonHierarchyNode(entry->m_name,
		    entry->m_id, entry->m_metadata, entry->m_caseId,
		    entry->m_courtCaseId));
		if (!toJsonRecursive(&jsonSubObj, entry->m_sub)) {
			return false;
		}
		if (!jsonHierarchyAppendSub(*jsonObj, jsonSubObj)) {
			return false;
		}
	}

	return true;
}

RecMgmt::UploadHierarchyResp::UploadHierarchyResp(void)
    : m_root(Q_NULLPTR)
{
}

RecMgmt::UploadHierarchyResp::UploadHierarchyResp(
    const UploadHierarchyResp &other)
    : m_root(Q_NULLPTR)
{
	if (other.m_root != Q_NULLPTR) {
		m_root = NodeEntry::copyRecursive(other.m_root);
	}
}

#ifdef Q_COMPILER_RVALUE_REFS
RecMgmt::UploadHierarchyResp::UploadHierarchyResp(
    UploadHierarchyResp &&other) Q_DECL_NOEXCEPT
    : m_root(::std::move(other.m_root))
{
}
#endif /* Q_COMPILER_RVALUE_REFS */

RecMgmt::UploadHierarchyResp::~UploadHierarchyResp(void)
{
	NodeEntry::deleteRecursive(m_root);
}

RecMgmt::UploadHierarchyResp &RecMgmt::UploadHierarchyResp::operator=(
    const UploadHierarchyResp &other) Q_DECL_NOTHROW
{
	NodeEntry *tmpRoot = Q_NULLPTR;

	if (other.m_root != Q_NULLPTR) {
		tmpRoot = NodeEntry::copyRecursive(other.m_root);
		if (Q_UNLIKELY(tmpRoot == Q_NULLPTR)) {
			/* Copying failed. */
			Q_ASSERT(0);
		}
	}

	NodeEntry::deleteRecursive(m_root);
	m_root = tmpRoot;

	return *this;
}

RecMgmt::UploadHierarchyResp &RecMgmt::UploadHierarchyResp::operator=(
    UploadHierarchyResp &&other) Q_DECL_NOTHROW
{
	::std::swap(m_root, other.m_root);
	return *this;
}

const RecMgmt::UploadHierarchyResp::NodeEntry *
    RecMgmt::UploadHierarchyResp::root(void) const
{
	return m_root;
}

bool RecMgmt::UploadHierarchyResp::isValid(void) const
{
	return m_root != Q_NULLPTR;
}

RecMgmt::UploadHierarchyResp RecMgmt::UploadHierarchyResp::fromJson(
    const QByteArray &json, bool *ok)
{
	QJsonObject jsonObj;
	if (!Json::Helper::readRootObject(json, jsonObj)) {
		if (ok != Q_NULLPTR) {
			*ok = false;
		}
		return UploadHierarchyResp();
	}

	bool intOk = false;
	UploadHierarchyResp uhr;

	uhr.m_root = NodeEntry::fromJsonRecursive(&jsonObj, intOk, true);
	if (ok != Q_NULLPTR) {
		*ok = intOk;
	}
	Q_ASSERT(uhr.m_root != Q_NULLPTR);
	return intOk ? uhr : UploadHierarchyResp();
}

QByteArray RecMgmt::UploadHierarchyResp::toJson(void) const
{
	if (Q_UNLIKELY(m_root == Q_NULLPTR)) {
		Q_ASSERT(0);
		return QJsonDocument(QJsonObject()).toJson(
		    QJsonDocument::Indented);
	}

	QJsonObject jsonObj(jsonHierarchyNode(m_root->m_name, m_root->m_id,
	    m_root->m_metadata, m_root->m_caseId, m_root->m_courtCaseId));

	if (!NodeEntry::toJsonRecursive(&jsonObj, m_root->m_sub)) {
		return QByteArray();
	}

	return QJsonDocument(jsonObj).toJson(QJsonDocument::Indented);
}
