//SPDX-License-Identifier: GPL-3.0-or-later
/*****************************************************************************/
/*  Copyright 2021 WSL Institute for Snow and Avalanche Research  SLF-DAVOS  */
/*****************************************************************************/
/* This file is part of INIshell.
   INIshell 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.

   INIshell 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 INIshell.  If not, see <http://www.gnu.org/licenses/>.
*/

#include <src/gui/RememberDialog.h>
#include <src/gui/SectionTab.h>
#include <src/gui/SectionButton.h>
#include <src/main/colors.h>
#include <src/main/inishell.h>
#include <src/main/os.h>
#include <src/main/settings.h>
#include <src/main/XMLReader.h>
#include <src/panels/Group.h>


/**
 * @class ScrollPanel
 * @brief The main scroll panel controlling all dynamically built panels.
 * @param[in] section The section this panel corresponds to. It could be used for styling but is
 * unused otherwise.
 * @param[in] parent The parent widget (the main tab bar).
 */
ScrollPanel::ScrollPanel(const QString &tab_color, QWidget *parent)
	: QWidget(parent)
{
	/* create a scroll area and put a Group in it */
	main_area_ = new QScrollArea;
	main_area_->setWidgetResizable(true); //best to let the manager do its job - troubles ahead if we don't
	main_area_->setStyleSheet("QScrollArea {border: none}"); //we have one from the tabs already
	main_group_ = new Group(QString(), QString(), false, false, false, false, QString(), QString(), tab_color);
	main_area_->setWidget(main_group_);

	/* main layout */
	auto *layout( new QVBoxLayout );
	layout->addWidget(main_area_);
	this->setLayout(layout);
}

/**
 * @brief Retrieve the main grouping element of a scroll panel (there is one per tab).
 * @return The main Group of this scroll panel holding all widgets.
 */
Group * ScrollPanel::getGroup() const
{
	return this->main_group_;
}

/**
 * @class SectionTab
 * @brief A subclassed tab bar that the main GUI building recursion can be started on.
 * @param[in] parent The parent widget.
 */
SectionTab::SectionTab(QWidget *parent) : QTabWidget(parent)
{
	//do nothing
}

/**
 * @brief Retrieve the ScrollPanel of a section.
 * @details Sections are grouped in tab bar elements. For this, they are put in that specific
 * tab panel's ScrollPanel which can be retrieved by this function (to build top level panels into).
 * If the section/tab does not exist yet it is created.
 * @param[in] section The INI section for which to get the corresponding ScrollPanel.
 * @param[in] background_color Background color to give to a whole section that is being created.
 * @param[in] color Font color to give a new section.
 * @param[in] no_create Set to true when parsing an INI to "force" INI sections matching the XML.
 * @param[in] dynamic Is this a dynamic section that is being created?
 * @return The found or newly created ScrollPanel.
 */
ScrollPanel * SectionTab::getSectionScrollArea(const QString &section, const QString &background_color,
	const QString &color, const bool&no_create, const int &insert_idx)
{ //find or create section tab

	for (int ii = 0; ii < this->count(); ++ii) {
		if (QString::compare(os::cleanKDETabStr(this->tabBar()->tabText(ii)), section, Qt::CaseInsensitive) == 0)
			return qobject_cast<ScrollPanel *>(this->widget(ii)); //cast to access members
	}
	if (!no_create) {
		int new_index = insert_idx;
		if (insert_idx == -1)
			new_index = this->count();
		auto *new_scroll( new ScrollPanel(background_color) );
		this->insertTab(new_index, new_scroll, section);
		this->tabBar()->setTabTextColor(new_index, colors::getQColor(color));

		if (insert_idx != -1) {
			auto button = new SectionButton(SectionButton::minus, section);
			this->tabBar()->setTabButton(new_index, QTabBar::RightSide, button);
		} else if (isDynamicParent(section)) {
			auto button = new SectionButton(SectionButton::plus, section);
			this->tabBar()->setTabButton(new_index, QTabBar::RightSide, button);
		}
		return new_scroll;
	} else { //don't add sections for keys found in ini files
		return nullptr;
	}
}

/**
 * @brief Retrieve a section's ScrollPanel with known section tab index.
 * @param[in] index The section's index in the tab bar.
 * @return ScrollPanel for the section.
 */
ScrollPanel * SectionTab::getSectionScrollArea(const int &index)
{
	return qobject_cast<ScrollPanel *>(this->widget(index));
}

/**
 * @brief Run through panels and query their user-set values.
 * @details This function finds all panel classes, gets their set values, and modifies the
 * corresponding INI setting. It also performs checks for missing values.
 * Note: Originally INI values were updated live in the panels on input, eliminating the need
 * for this loop. However, more and more intermediate loops needed to be introduced (for example
 * to deactivate panels that are hidden by a parent element like a Dropdown or Choice).
 * Furthermore, dealing with missing mandatory values and keys that are present multiple times
 * (e. g. needing to check if a second panel manipulates the same key before deleting it in a
 * Selector) convoluted the program so heavily that this solution is much cleaner and more robust.
 * @param[in] ini The INIParser for which to set the values (the one that will be output).
 * @param[in] insert_missing Set to true if the populated INIParser should contain empty values also.
 * In this case they are set to "#N/A".
 * @return A comma-separated list of missing mandatory INI keys.
 */
QString SectionTab::setIniValuesFromGui(INIParser *ini, const bool &insert_missing)
{
	//TODO: For some combination of properties (optional, will be defaulted by the
	//software, ...) we may be able to skip output of some keys to avoid
	//information redundancy. Parameter checks:
	//const QString default_value( panel->property("default_value").toString() ); //default value
	//( !ini->get(section, key).isNull()) ) //key present in input INI file
	//( is_mandatory ) //key is set non-optional

	QString missing;
	for (int ii = 0; ii < this->count(); ++ii) { //run through sections
		const QWidget *section_tab( this->widget(ii) );
		const QList<Atomic *> panel_list( section_tab->findChildren<Atomic *>() );
		for (auto &panel : panel_list) { //run through panels in section
			if (!panel->isVisibleTo(section_tab)) //visible if section_tab was visible?
				continue;
			QString section, key;
			QString value( panel->getIniValue(section, key) );
			if (panel->property("no_ini").toBool())
				continue;
			if (key.isNull()) //GUI element that is not an interactive panel
				continue;

			const bool is_mandatory = panel->property("is_mandatory").toBool();
			if (is_mandatory && value.isEmpty()) { //collect missing non-optional keys
				bool is_dynamic_widget = false;
				bool dyn_is_empty = false;

				auto *rep = qobject_cast<Replicator *>(panel);
				if (rep) {
					is_dynamic_widget = true;
					dyn_is_empty = rep->isEmpty();
				} else {
					auto *sel = qobject_cast<Selector *>(panel);
					if (sel) {
						is_dynamic_widget = true;
						dyn_is_empty = sel->isEmpty();
					}
				}
				if (!is_dynamic_widget || dyn_is_empty) //a dynamic panel is only missing if it's empty
					missing += section + "::" + key + ",\n";
			}
			if (insert_missing) { //this is used by the Preview Editor to insert missing values / get a full list
				if (value.isEmpty())
					value = "#N/A";
			}
			if (!value.isEmpty())
				ini->set(section, key, value, is_mandatory);
		} //endfor panel_list
	} //endfor ii
	missing.chop(2); //trailing ", "
	return missing;
}

/**
 * @brief Mark and remember a certain section to be dynamic.
 * @param[in] section The ini section to mark as dynamic.
 * @param[in] node The section's complete XML node. Note that this only works
 * in dedicated <section> tags!
 */
void SectionTab::setSectionDynamic(const QString &section, const QDomNode &node)
{
	dynamic_sections_.push_back(section);
	dynamic_nodes_.push_back(node);
	dynamic_running_indices_.push_back(1); //1st of its kind
}

/**
 * @brief Check if a section is marked as dynamic and can therefore
 * spawn child sections.
 * @param[in] section The ini section name to check.
 * @return True if this is a parent section.
 */
bool SectionTab::isDynamicParent(const QString &section) const
{
	const int dyn_idx = dynamic_sections_.indexOf(QRegExp(section, Qt::CaseInsensitive));
	return (dyn_idx != -1);
}

/**
 * @brief Check if a section was derived from any dynamic parent section.
 * @details This function checks for the syntax string + number.
 * @param[in] section The ini section name to check.
 * @return True if this is a child section that was spawned by a parent section.
 */
int SectionTab::isDynamicChild(const QString &section) const
{
	for (int ii = 0; ii < dynamic_sections_.length(); ++ii) {
		if (isDynamicChildOf(section, dynamic_sections_.at(ii)))
		if (section.startsWith(dynamic_sections_.at(ii), Qt::CaseInsensitive))
			return true;
		//else: the correct one may still come later
	} //endfor ii
	return false;
}

/**
 * @brief Check if a section was derived from a specific parent section.
 * @param section The section to check.
 * @param parent_section Parent section to check.
 * @return True if this is child of the given parent section.
 */
bool SectionTab::isDynamicChildOf(const QString &section, const QString &parent_section) const
{
	if (!section.startsWith(parent_section, Qt::CaseInsensitive))
		return false;
	if (QString::compare(section, parent_section, Qt::CaseInsensitive) == 0)
		return false; //a section is not a child of itself

	const QString postfix( section.mid(parent_section.length()) ); //the potential number
	bool success;
	(void) postfix.toInt(&success);
	return success;
}

/**
 * @brief Count how many children a dynamic section currently has.
 * @param parent_section The parent dynamic section to check.
 * @return Number of currently opened child sections.
 */
int SectionTab::countChildren(const QString &parent_section) const
{
	int counter = 0;
	for (int ii = 0; ii < this->tabBar()->count(); ++ii) {
		if (isDynamicChildOf(os::cleanKDETabStr(this->tabBar()->tabText(ii)), parent_section))
			counter++;
	}
	return counter;
}

/**
 * @brief Get the parent section to a dynamic child section.
 * @param section The ini section to get parent section for.
 * @return The section's parent section.
 */
QString SectionTab::getParentOf(const QString &section)
{
	for (auto &parsec : dynamic_sections_) {
		if (isDynamicChildOf(section, parsec))
			return parsec;
	}
	return QString(); //not found
}

/**
 * @brief Take a parent section and clone it to a new one.
 * @param[in] section The section to clone.
 * @param[in] new_tab_name The name to give to the new tab. Leave empty to pick one.
 */
void SectionTab::spawnDynamicSection(const QString &section, const QString &new_tab_name)
{
	int index = getIndex(section, Qt::CaseInsensitive);
	const int dyn_idx = dynamic_sections_.indexOf(QRegExp(section, Qt::CaseInsensitive));

	bool found = false;
	for (int ii = index + 1; ii < this->count(); ++ii) {
		const QString postfix( os::cleanKDETabStr(
			this->tabBar()->tabText(ii)).mid(section.length()) ); //the potential number
		bool success = true;
		if (!postfix.isEmpty()) //section is the start of a tab name --> check if remainder is a number:
			(void) postfix.toInt(&success); //(so that e. g. "InputEditing" is not seen as child of "Input")
		if (!os::cleanKDETabStr(this->tabBar()->tabText(ii)).startsWith(section, Qt::CaseInsensitive) || !success) {
			index = ii - 1; //order after all dynamic sections of this kind
			found = true;
			break;
		}
	}
	if (!found) //parent panel is the last one --> append
		index = this->count() - 1;

	QString new_name( new_tab_name );
	if (new_name.isNull()) { //from a + button click (i. e. not from an INI file) --> pick new name
		new_name = (section + "%1").arg(dynamic_running_indices_.at(dyn_idx));
		dynamic_running_indices_[dyn_idx]++;
	}
	auto dyn_scroll = getSectionScrollArea(new_name, QString(), "normal", false, index + 1);
	recursiveBuild(dynamic_nodes_.at(dyn_idx), dyn_scroll->getGroup(), new_name);
	if (new_tab_name.isNull()) //do not focus tab from ini read-in
		this->setCurrentIndex(index + 1);
}

/**
 * @brief Try for a currently unknown section if it could be a child section,
 * and if so, create it.
 * @param[in] section The ini section to check
 * @return A new tab for the new section, or nullptr if this is not a dynamic child.
 */
ScrollPanel * SectionTab::tryDynamicSection(const QString &section)
{
	for (int ii = 0; ii < dynamic_sections_.length(); ++ii) {
		if (section.startsWith(dynamic_sections_.at(ii), Qt::CaseInsensitive)) {
			const QString postfix( section.mid(dynamic_sections_.at(ii).length()) );
			bool success;
			const int sec_no = postfix.toInt(&success);
			if (success) { //we use the 1st one that fits
				spawnDynamicSection(dynamic_sections_.at(ii), section);
				if (sec_no > dynamic_running_indices_.at(ii))
					dynamic_running_indices_[ii] = sec_no + 1; //start at highest index on "+" click
				return getSectionScrollArea(section);
			}
		}
	} //endfor ii
	return nullptr;
}

/**
 * @brief Remove a dynamic child section.
 * @param[in] section The ini section to remove.
 */
void SectionTab::removeDynamicSection(const QString &section)
{
	if (getSetting("user::warnings::warn_unsaved_tab", "value") == "TRUE") {
		const QString msg( tr("INI settings will be lost") );
		const QString informative( tr("If you close this tab, all of its settings will be lost.") );
		const QString details( tr(
			"If you did not make any changes to the default values of this tab/section, you can safely ignore this message.") );
		const QFlags<QMessageBox::StandardButton> buttons( QMessageBox::Ok | QMessageBox::Cancel );
		RememberDialog msgNotSaved("user::warnings::warn_unsaved_tab", msg, informative, details, buttons, this);

		int clicked = msgNotSaved.exec();
		if (clicked == QMessageBox::Cancel)
			return;
	}

	const QString parent_section( getParentOf(section) );
	if (countChildren(parent_section) == 1) {
		const int dyn_idx = dynamic_sections_.indexOf(parent_section, Qt::CaseInsensitive);
		dynamic_running_indices_[dyn_idx] = 1;
	}
	for (int ii = 0; ii < this->tabBar()->count(); ++ii) {
		if (os::cleanKDETabStr(this->tabBar()->tabText(ii)) == section) {
			this->widget(ii)->deleteLater();
			break;
		}
	} //endfor ii
}

/**
 * @brief Select tab specified by its caption.
 * @param[in] tab_name The tab/section to select.
 * @return True if the tab could be selected.
 */
bool SectionTab::showTab(const QString &tab_name)
{
	const int idx = getIndex(tab_name);
	if (idx == -1) //not found
		return false;
	this->setCurrentIndex(idx);
	return true;
}

/**
 * @brief Highlight a certain frame within a tab.
 * @details For this to work the frame must have a "key" property to reference
 * in the XML.
 * @param[in] section The tab/section the frame is in.
 * @param[in] element_key The element's key to show and highlight.
 * @param[in] no_create Do not create the panel if it is not found (e. g. for help topics).
 * @return True if the panel could be shown.
 */
bool SectionTab::showPanel(const QString &section, const QString &element_key, const bool &no_create)
{
	const bool success = showTab(section);
#ifdef DEBUG
	if (!success)
		qDebug() << "Panel " << section << "::" << element_key << " does not exist";
#endif //DEBUG

	if (element_key.isEmpty())
		return success; //show frame with no specific element
	const auto parent = this->getSectionScrollArea(section, QString(), "normal", no_create);
	const QList<Atomic *> panel_list( parent->findChildren<Atomic *>(
		Atomic::getQtKey(section + Cst::sep + element_key)) ); //a frame can also have an (arbitrary) key
	for (auto &panel : panel_list) { //run through elements in section and highlight all matching ones
		parent->ensureWidgetVisible(panel->getEmphasisWidget()); //scroll the ScrollBar until visible
		panel->setHighlightedStyle(); //TODO: make it work for Selectors (their keys can not match)
	} //endfor panel_list
	this->raise();
	return !panel_list.isEmpty();
}

/**
 * @brief Get tab index from a section name.
 * @param[in] section Section name to retrieve the index for.
 * @param[in] sensitivity Case sensitive search?
 * @return Tab index of the section, or -1 if not found.
 */
int SectionTab::getIndex(const QString &section, const Qt::CaseSensitivity &sensitivity)
{
	for (int ii = 0; ii < this->count(); ++ii) {
		if (QString::compare(os::cleanKDETabStr(this->tabBar()->tabText(ii)), section, sensitivity) == 0)
			return ii;
	}
	return -1; //not found
}

/**
 * @brief Clear all tab pages.
 * @details Unlike the native QTabWidget::clear(), this here also deletes the pages.
 */
void SectionTab::clear()
{
	while (this->count() > 0) {
		this->widget(0)->deleteLater();
		this->removeTab(0);
	}
	dynamic_sections_.clear();
	dynamic_nodes_.clear();
	dynamic_running_indices_.clear();
}

/**
 * @brief Remove all dynamic child tabs.
 */
void SectionTab::clearDynamicTabs()
{
	for (int ii = this->count() - 1; ii > -1; --ii) {
		if (isDynamicChild(os::cleanKDETabStr(this->tabBar()->tabText(ii)))) {
			this->widget(ii)->deleteLater();
			this->removeTab(ii);
		}
	}
	for (int ii = 0; ii < dynamic_running_indices_.count(); ++ii)
		dynamic_running_indices_[ii] = 1;
}
