/*
	File                 : DatasetMetadataManagerWidget.cpp
	Project              : LabPlot
	Description          : widget for managing a metadata file of a dataset
	--------------------------------------------------------------------
	SPDX-FileCopyrightText: 2019 Ferencz Kovacs <kferike98@gmail.com>
	SPDX-License-Identifier: GPL-2.0-or-later
*/

#include "kdefrontend/datasources/DatasetMetadataManagerWidget.h"
#include "backend/datasources/filters/AsciiFilter.h"
#include "backend/lib/macros.h"
#include "kdefrontend/DatasetModel.h"

#include <KConfigGroup>
#include <KSharedConfig>
#include <QDir>
#include <QFile>
#include <QHBoxLayout>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonValue>
#include <QMap>
#include <QMapIterator>
#include <QRegularExpression>
#include <QStringList>
#include <QTcpSocket>
#include <QUrl>

/*!
	\class DatasetMetadataManagerWidget
	\brief Widget for adding a new dataset to LabPlot's current collection.

	\ingroup kdefrontend
 */
DatasetMetadataManagerWidget::DatasetMetadataManagerWidget(QWidget* parent, const QMap<QString, QMap<QString, QMap<QString, QVector<QString>>>>& datasetMap)
	: QWidget(parent) {
	ui.setupUi(this);
	m_datasetModel = new DatasetModel(datasetMap);

	m_baseColor = DARKMODE ? QStringLiteral("#5f5f5f") : QStringLiteral("#ffffff");
	m_textColor = DARKMODE ? QStringLiteral("#ffffff") : QStringLiteral("#000000");

	ui.cbCollection->addItems(m_datasetModel->collections());
	ui.cbCategory->addItems(m_datasetModel->categories(ui.cbCollection->currentText()));
	ui.cbSubcategory->addItems(m_datasetModel->subcategories(ui.cbCollection->currentText(), ui.cbCategory->currentText()));

	ui.cbSeparatingCharacter->addItems(AsciiFilter::separatorCharacters());
	ui.cbCommentCharacter->addItems(AsciiFilter::commentCharacters());
	ui.cbNumberFormat->addItems(AbstractFileFilter::numberFormats());
	ui.cbDateTimeFormat->addItems(AbstractColumn::dateTimeFormats());

	connect(ui.leDatasetName, &QLineEdit::textChanged, [this] {
		Q_EMIT checkOk();
	});
	connect(ui.leDownloadURL, &QLineEdit::textChanged, [this] {
		Q_EMIT checkOk();
	});
	connect(ui.teDescription, &QTextEdit::textChanged, [this] {
		Q_EMIT checkOk();
	});
	connect(ui.leFileName, &QLineEdit::textChanged, [this] {
		Q_EMIT checkOk();
	});

	connect(ui.cbSubcategory, &QComboBox::currentTextChanged, [this] {
		Q_EMIT checkOk();
	});

	connect(ui.cbCollection, &QComboBox::currentTextChanged, this, &DatasetMetadataManagerWidget::updateCategories);
	connect(ui.cbCategory, &QComboBox::currentTextChanged, this, &DatasetMetadataManagerWidget::updateSubcategories);
	connect(ui.bNewColumn, &QPushButton::clicked, this, &DatasetMetadataManagerWidget::addColumnDescription);
	connect(ui.bDelete, &QPushButton::clicked, this, &DatasetMetadataManagerWidget::removeColumnDescription);

	loadSettings();
}

DatasetMetadataManagerWidget::~DatasetMetadataManagerWidget() {
	KConfigGroup conf(KSharedConfig::openConfig(), "DatasetMetadataManagerWidget");

	// filter settings
	conf.writeEntry("separator", ui.cbSeparatingCharacter->currentText());
	conf.writeEntry("commentChar", ui.cbCommentCharacter->currentText());
	conf.writeEntry("numberFormat", ui.cbNumberFormat->currentIndex());
	conf.writeEntry("dateTimeFormat", ui.cbDateTimeFormat->currentText());
	conf.writeEntry("createIndexColumn", ui.chbCreateIndex->isChecked());
	conf.writeEntry("skipEmptyParts", ui.chbSkipEmptyParts->isChecked());
	conf.writeEntry("simplifyWhitespaces", ui.chbSimplifyWhitespaces->isChecked());
	conf.writeEntry("removeQuotes", ui.chbRemoveQuotes->isChecked());
	conf.writeEntry("useFirstRowForVectorName", ui.chbHeader->isChecked());
	conf.writeEntry("collection", ui.cbCollection->currentText());
	conf.writeEntry("category", ui.cbCategory->currentText());
	conf.writeEntry("subcategory", ui.cbSubcategory->currentText());
}

/**
 * @brief Loads the settings of the widget.
 */
void DatasetMetadataManagerWidget::loadSettings() {
	KConfigGroup conf(KSharedConfig::openConfig(), "DatasetMetadataManagerWidget");
	ui.cbCommentCharacter->setCurrentItem(conf.readEntry("commentChar", "#"));
	ui.cbSeparatingCharacter->setCurrentItem(conf.readEntry("separator", "auto"));
	// TODO: use general setting for decimal separator?
	ui.cbNumberFormat->setCurrentIndex(conf.readEntry("numberFormat", static_cast<int>(QLocale::AnyLanguage)));
	ui.cbDateTimeFormat->setCurrentItem(conf.readEntry("dateTimeFormat", "yyyy-MM-dd hh:mm:ss.zzz"));
	ui.chbCreateIndex->setChecked(conf.readEntry("createIndexColumn", false));
	ui.chbSimplifyWhitespaces->setChecked(conf.readEntry("simplifyWhitespaces", true));
	ui.chbRemoveQuotes->setChecked(conf.readEntry("removeQuotes", false));
	ui.chbSkipEmptyParts->setChecked(conf.readEntry("skipEmptyParts", false));
	ui.chbHeader->setChecked(conf.readEntry("useFirstRowForVectorName", true));

	QString lastCollection = conf.readEntry("collection", "");
	if (m_datasetModel->collections().contains(lastCollection))
		ui.cbCollection->setCurrentText(lastCollection);

	QString lastCategory = conf.readEntry("category", "");
	if (m_datasetModel->categories(ui.cbCollection->currentText()).contains(lastCategory))
		ui.cbCategory->setCurrentText(lastCategory);

	QString lastSubcategory = conf.readEntry("subcategory", "");
	if (m_datasetModel->subcategories(ui.cbCollection->currentText(), ui.cbCategory->currentText()).contains(lastSubcategory))
		ui.cbSubcategory->setCurrentText(lastSubcategory);
}

/**
 * @brief Checks whether leFileName contains a valid file name.
 */
bool DatasetMetadataManagerWidget::checkFileName() {
	const QString fileName = ui.leFileName->text();

	// check whether it contains only digits, letters, -, _ or not
	const QRegularExpression re(QLatin1String("^[\\w\\d-]+$"));
	const QRegularExpressionMatch match = re.match(fileName);
	bool hasMatch = match.hasMatch();

	if (!hasMatch || fileName.isEmpty()) {
		DEBUG("File name invalid");
		QPalette palette;
		palette.setColor(QPalette::Base, Qt::red);
		palette.setColor(QPalette::Text, Qt::black);
		ui.leFileName->setPalette(palette);
		ui.leFileName->setToolTip(QStringLiteral("Invalid name for a file (it can contain:digits, letters, -, _)"));
	} else {
		QPalette palette;
		palette.setColor(QPalette::Base, m_baseColor);
		palette.setColor(QPalette::Text, m_textColor);
		ui.leFileName->setPalette(palette);
		ui.leFileName->setToolTip(QString());
	}

	// check whether there already is a file named like this or not.
	bool found = false;

	if (m_datasetModel->allDatasetsList().toStringList().contains(fileName)) {
		DEBUG("There already is a metadata file with this name");
		QPalette palette;
		palette.setColor(QPalette::Base, Qt::red);
		palette.setColor(QPalette::Text, Qt::black);
		ui.leFileName->setPalette(palette);
		ui.leFileName->setToolTip(QStringLiteral("There already is a dataset metadata file with this name!"));
		found = true;
	} else {
		if (hasMatch) {
			QPalette palette;
			palette.setColor(QPalette::Base, m_baseColor);
			palette.setColor(QPalette::Text, m_textColor);
			ui.leFileName->setPalette(palette);
			ui.leFileName->setToolTip(QString());
		}
	}

	return hasMatch && !found;
}

/**
 * @brief Checks whether leDownloadURL contains a valid URL.
 */
bool DatasetMetadataManagerWidget::urlExists() {
	// Check whether the given url is acceptable syntactically
	const QRegularExpression re(QLatin1String(R"(^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]+$)"));
	const QRegularExpressionMatch match = re.match(ui.leDownloadURL->text());
	bool hasMatch = match.hasMatch();
	const bool urlExists = hasMatch && !ui.leDownloadURL->text().isEmpty();

	if (!urlExists) {
		QPalette palette;
		palette.setColor(QPalette::Base, Qt::red);
		palette.setColor(QPalette::Text, Qt::black);
		ui.leDownloadURL->setPalette(palette);
		ui.leDownloadURL->setToolTip(QStringLiteral("The URL is invalid!"));
	} else {
		QPalette palette;
		palette.setColor(QPalette::Base, m_baseColor);
		palette.setColor(QPalette::Text, m_textColor);
		ui.leDownloadURL->setPalette(palette);
		ui.leDownloadURL->setToolTip(QString());
	}
	return urlExists;
}

/**
 * @brief Checks whether leDatasetName is empty or not.
 */
bool DatasetMetadataManagerWidget::checkDatasetName() {
	const bool longNameOk = !ui.leDatasetName->text().isEmpty();
	if (!longNameOk) {
		QPalette palette;
		palette.setColor(QPalette::Base, Qt::red);
		palette.setColor(QPalette::Text, Qt::black);
		ui.leDatasetName->setPalette(palette);
		ui.leDatasetName->setToolTip(QStringLiteral("Please fill this out!"));
	} else {
		QPalette palette;
		palette.setColor(QPalette::Base, m_baseColor);
		palette.setColor(QPalette::Text, m_textColor);
		ui.leDatasetName->setPalette(palette);
		ui.leDatasetName->setToolTip(QString());
	}

	return longNameOk;
}

/**
 * @brief Checks whether teDescription is empty or not.
 */
bool DatasetMetadataManagerWidget::checkDescription() {
	const bool descriptionOk = !ui.teDescription->toPlainText().isEmpty();
	if (!descriptionOk) {
		QPalette palette;
		palette.setColor(QPalette::Base, Qt::red);
		palette.setColor(QPalette::Text, Qt::black);
		ui.teDescription->setPalette(palette);
		ui.teDescription->setToolTip(QStringLiteral("Please fill this out!"));
	} else {
		QPalette palette;
		palette.setColor(QPalette::Base, m_baseColor);
		palette.setColor(QPalette::Text, m_textColor);
		ui.teDescription->setPalette(palette);
		ui.teDescription->setToolTip(QString());
	}

	return descriptionOk;
}

/**
 * @brief Checks whether the given QComboBox's current text is empty or not.
 */
bool DatasetMetadataManagerWidget::checkCategories(QComboBox* comboBox) {
	// Check whether it is a word or not (might contain digits)
	const QString fileName = comboBox->currentText();
	const QRegularExpression re(QLatin1String("^[\\w\\d]+$"));
	const QRegularExpressionMatch match = re.match(fileName);
	const bool hasMatch = match.hasMatch();

	if (!hasMatch || fileName.isEmpty()) {
		DEBUG("category/subcategory name invalid");
		QPalette palette;
		palette.setColor(QPalette::Base, Qt::red);
		palette.setColor(QPalette::Text, Qt::black);
		comboBox->setPalette(palette);
		comboBox->setToolTip(QStringLiteral("Invalid or empty name for a category/subcategory (only digits and letters)"));
	} else {
		QPalette palette;
		palette.setColor(QPalette::Base, m_baseColor);
		palette.setColor(QPalette::Text, m_textColor);
		comboBox->setPalette(palette);
		comboBox->setToolTip(QString());
	}

	return hasMatch;
}

/**
 * @brief Enables/disables the widget's components meant to configure the metadata file of the new dataset.
 */
void DatasetMetadataManagerWidget::enableDatasetSettings(bool enable) {
	ui.leFileName->setEnabled(enable);
	ui.leFileName->setReadOnly(!enable);
	ui.leDatasetName->setEnabled(enable);
	ui.leDatasetName->setReadOnly(!enable);
	ui.leDownloadURL->setEnabled(enable);
	ui.leDownloadURL->setReadOnly(!enable);
	ui.teDescription->setEnabled(enable);
	ui.teDescription->setReadOnly(!enable);
	ui.gbColumnDescriptions->setEnabled(enable);
	ui.gbFilter->setEnabled(enable);
}

/**
 * @brief Checks whether the introduced data is valid or not. Used by DatasetMetadataManagerDialog.
 */
bool DatasetMetadataManagerWidget::checkDataValidity() {
	const bool fileNameOK = checkFileName();
	const bool urlOk = urlExists();
	const bool longNameOk = checkDatasetName();
	const bool descriptionOk = checkDescription();
	const bool categoryOk = checkCategories(ui.cbCategory);
	const bool subcategoryOk = checkCategories(ui.cbSubcategory);
	const bool collectionOk = checkCategories(ui.cbCollection);

	enableDatasetSettings(categoryOk && subcategoryOk && collectionOk);

	return fileNameOK && urlOk && longNameOk && descriptionOk && subcategoryOk && categoryOk && collectionOk;
}

/**
 * @brief Updates content of cbCategory based on current collection.
 */
void DatasetMetadataManagerWidget::updateCategories(const QString& collection) {
	ui.cbCategory->clear();
	if (m_datasetModel->collections().contains(collection))
		ui.cbCategory->addItems(m_datasetModel->categories(collection));

	Q_EMIT checkOk();
}

/**
 * @brief Updates content of cbSubcategory based on current category.
 */
void DatasetMetadataManagerWidget::updateSubcategories(const QString& category) {
	ui.cbSubcategory->clear();
	const QString collection = ui.cbCollection->currentText();
	if (m_datasetModel->categories(collection).contains(category))
		ui.cbSubcategory->addItems(m_datasetModel->subcategories(collection, category));

	Q_EMIT checkOk();
}

/**
 * @brief Updates the metadata file containing the categories, subcategories and datasets.
 * @param fileName the name of the metadata file (path)
 */
void DatasetMetadataManagerWidget::updateDocument(const QString& dirPath) {
	if (checkDataValidity()) {
		// Check whether the current collection already exists, if yes update it
		if (m_datasetModel->collections().contains(ui.cbCollection->currentText())) {
			QString fileName = dirPath + ui.cbCollection->currentText() + QStringLiteral(".json");
			qDebug() << "Updating: " << fileName;
			QFile file(fileName);
			if (file.open(QIODevice::ReadWrite)) {
				QJsonDocument document = QJsonDocument::fromJson(file.readAll());
				QJsonObject rootObject = document.object();
				QJsonValueRef categoryArrayRef = rootObject.find(QStringLiteral("categories")).value();
				QJsonArray categoryArray = categoryArrayRef.toArray();

				// Check whether the current category already exists
				bool foundCategory = false;
				for (int i = 0; i < categoryArray.size(); ++i) {
					QJsonValueRef categoryRef = categoryArray[i];
					QJsonObject currentCategory = categoryRef.toObject();
					QString categoryName = currentCategory.value(QStringLiteral("category_name")).toString();

					// If we find the category we have to update that QJsonObject
					if (categoryName.compare(ui.cbCategory->currentText()) == 0) {
						foundCategory = true;
						QJsonValueRef subcategoryArrayRef = currentCategory.find(QStringLiteral("subcategories")).value();
						QJsonArray subcategoryArray = subcategoryArrayRef.toArray();

						// Check whether the current subcategory already exists
						bool subcategoryFound = false;
						for (int j = 0; j < subcategoryArray.size(); ++j) {
							QJsonValueRef subcategoryRef = subcategoryArray[j];
							QJsonObject currentSubcategory = subcategoryRef.toObject();
							QString subcategoryName = currentSubcategory.value(QStringLiteral("subcategory_name")).toString();

							// If we find the subcategory we have to update that QJsonObject
							if (subcategoryName.compare(ui.cbSubcategory->currentText()) == 0) {
								subcategoryFound = true;
								QJsonValueRef datasetsRef = currentSubcategory.find(QStringLiteral("datasets")).value();
								QJsonArray datasets = datasetsRef.toArray();

								datasets.append(createDatasetObject());
								datasetsRef = datasets;

								subcategoryRef = currentSubcategory;
								subcategoryArrayRef = subcategoryArray;
								categoryRef = currentCategory;
								categoryArrayRef = categoryArray;
								document.setObject(rootObject);
								break;
							}
						}

						// If we didn't find the subcategory, we have to create it
						if (!subcategoryFound) {
							qDebug() << "Subcategory not found";
							QJsonObject newSubcategory;
							newSubcategory.insert(QStringLiteral("subcategory_name"), ui.cbSubcategory->currentText());

							QJsonArray datasets;
							datasets.append(createDatasetObject());

							newSubcategory.insert(QStringLiteral("datasets"), datasets);
							subcategoryArray.append(newSubcategory);

							subcategoryArrayRef = subcategoryArray;
							categoryRef = currentCategory;
							categoryArrayRef = categoryArray;
							document.setObject(rootObject);
						}
						break;
					}
				}

				// If we didn't find the category, we have to create it
				if (!foundCategory) {
					qDebug() << "Category not found";
					QJsonObject newCategory;
					newCategory.insert(QStringLiteral("category_name"), ui.cbCategory->currentText());

					QJsonArray subcategoryArray;

					QJsonObject newSubcategory;
					newSubcategory.insert(QStringLiteral("subcategory_name"), ui.cbSubcategory->currentText());

					QJsonArray datasets;
					datasets.append(createDatasetObject());
					newSubcategory.insert(QStringLiteral("datasets"), datasets);

					subcategoryArray.append(newSubcategory);
					newCategory.insert(QStringLiteral("subcategories"), subcategoryArray);

					categoryArray.append(newCategory);
					categoryArrayRef = categoryArray;
					document.setObject(rootObject);
				}
				file.close();
				bool rc = file.open(QIODevice::ReadWrite | QIODevice::Truncate);
				if (rc) {
					file.write(document.toJson());
					file.close();
				} else
					qDebug() << "Couldn't write file, because " << file.errorString();
			} else
				qDebug() << "Couldn't open dataset category file, because " << file.errorString();
		}
		// If the collection doesn't exist we have to create a new json file for it.
		else {
			QString fileName = dirPath + QStringLiteral("DatasetCollections.json");
			qDebug() << "creating: " << fileName;
			QFile file(fileName);
			if (file.open(QIODevice::ReadWrite | QIODevice::Truncate | QIODevice::Text)) {
				QJsonArray collectionArray;

				for (QString collection : m_datasetModel->collections())
					collectionArray.append(collection);

				collectionArray.append(ui.cbCollection->currentText());

				QJsonDocument newDocument;
				newDocument.setArray(collectionArray);
				file.write(newDocument.toJson());
				file.close();
			}

			QJsonObject rootObject;

			rootObject.insert(QStringLiteral("collection_name"), ui.cbCollection->currentText());

			QJsonArray categoryArray;
			QJsonObject newCategory;
			newCategory.insert(QStringLiteral("category_name"), ui.cbCategory->currentText());

			QJsonArray subcategoryArray;

			QJsonObject newSubcategory;
			newSubcategory.insert(QStringLiteral("subcategory_name"), ui.cbSubcategory->currentText());

			QJsonArray datasets;
			datasets.append(createDatasetObject());
			newSubcategory.insert(QStringLiteral("datasets"), datasets);

			subcategoryArray.append(newSubcategory);
			newCategory.insert(QStringLiteral("subcategories"), subcategoryArray);
			categoryArray.append(newCategory);
			rootObject.insert(QStringLiteral("categories"), categoryArray);

			QJsonDocument document;
			document.setObject(rootObject);

			QFile collectionFile(dirPath + ui.cbCollection->currentText() + QStringLiteral(".json"));
			if (collectionFile.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
				collectionFile.write(document.toJson());
				collectionFile.close();
			}
		}
	}
}

/**
 * @brief Creates and returns a QJsonObject based on the given settings of the widget, this will be part of the collection's metadata file
 */
QJsonObject DatasetMetadataManagerWidget::createDatasetObject() {
	QJsonObject rootObject;
	rootObject.insert(QStringLiteral("filename"), ui.leFileName->text());
	rootObject.insert(QStringLiteral("name"), ui.leDatasetName->text());
	rootObject.insert(QStringLiteral("download"), ui.leDownloadURL->text());
	rootObject.insert(QStringLiteral("description"), ui.teDescription->toPlainText());
	rootObject.insert(QStringLiteral("separator"), ui.cbSeparatingCharacter->currentText());
	rootObject.insert(QStringLiteral("comment_character"), ui.cbCommentCharacter->currentText());
	rootObject.insert(QStringLiteral("DateTime_format"), ui.cbDateTimeFormat->currentText());
	rootObject.insert(QStringLiteral("number_format"), ui.cbNumberFormat->currentIndex());
	rootObject.insert(QStringLiteral("create_index_column"), ui.chbCreateIndex->isChecked());
	rootObject.insert(QStringLiteral("skip_empty_parts"), ui.chbSkipEmptyParts->isChecked());
	rootObject.insert(QStringLiteral("simplify_whitespaces"), ui.chbSimplifyWhitespaces->isChecked());
	rootObject.insert(QStringLiteral("remove_quotes"), ui.chbRemoveQuotes->isChecked());
	rootObject.insert(QStringLiteral("use_first_row_for_vectorname"), ui.chbHeader->isChecked());

	for (int i = 0; i < m_columnDescriptions.size(); ++i)
		rootObject.insert(i18n("column_description_%1", i), m_columnDescriptions[i]);

	return rootObject;
}

/**
 * @brief Adds a new QLineEdit so the user can set a new column description.
 */
void DatasetMetadataManagerWidget::addColumnDescription() {
	auto* label = new QLabel();
	label->setText(i18n("Description for column %1", m_columnDescriptions.size() + 1));
	auto* lineEdit = new QLineEdit();

	int layoutIndex = m_columnDescriptions.size() + 1;
	ui.columnLayout->addWidget(label, layoutIndex, 0);
	ui.columnLayout->addWidget(lineEdit, layoutIndex, 1, 1, -1);

	connect(lineEdit, &QLineEdit::textChanged, [this, layoutIndex](const QString& text) {
		m_columnDescriptions[layoutIndex - 1] = text;
	});

	m_columnDescriptions.append(QString());
}

/**
 * @brief Removes the lastly added QLineEdit (used to set a column description).
 */
void DatasetMetadataManagerWidget::removeColumnDescription() {
	const int index = ui.columnLayout->count() - 1;

	QLayoutItem* item;
	if ((item = ui.columnLayout->takeAt(index)) != nullptr) {
		delete item->widget();
		delete item;
	}

	if ((item = ui.columnLayout->takeAt(index - 1)) != nullptr) {
		delete item->widget();
		delete item;
	}

	m_columnDescriptions.removeLast();
}

/**
 * @brief returns the path to the new metadata file of the new dataset.
 */
QString DatasetMetadataManagerWidget::getMetadataFilePath() const {
	return m_metadataFilePath;
}

/**
 * @brief Sets the collection name.
 */
void DatasetMetadataManagerWidget::setCollection(const QString& collection) {
	ui.cbCollection->setCurrentText(collection);
}

/**
 * @brief Sets the category name.
 */
void DatasetMetadataManagerWidget::setCategory(const QString& category) {
	ui.cbCategory->setCurrentText(category);
}

/**
 * @brief Sets the subcategory name.
 */
void DatasetMetadataManagerWidget::setSubcategory(const QString& subcategory) {
	ui.cbSubcategory->setCurrentText(subcategory);
}

/**
 * @brief Sets the short name of the dataset.
 */
void DatasetMetadataManagerWidget::setShortName(const QString& name) {
	ui.leFileName->setText(name);
}

/**
 * @brief Sets the full name of the dataset.
 */
void DatasetMetadataManagerWidget::setFullName(const QString& name) {
	ui.leDatasetName->setText(name);
}

/**
 * @brief Sets the text of the description.
 */
void DatasetMetadataManagerWidget::setDescription(const QString& description) {
	ui.teDescription->setText(description);
}

/**
 * @brief Sets the download url.
 */
void DatasetMetadataManagerWidget::setURL(const QString& url) {
	ui.leDownloadURL->setText(url);
}
