//SPDX-License-Identifier: GPL-3.0-or-later
/*****************************************************************************/
/*  Copyright 2019 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/panels/Textfield.h>
#include <src/panels/Label.h>
#include <src/main/constants.h>
#include <src/main/expressions.h>
#include <src/main/inishell.h>

#include <QDesktopServices>
#include <QFontMetrics>
#include <QHBoxLayout>

#ifdef DEBUG
	#include <iostream>
#endif //def DEBUG

// //regular expressions for validation and parsing of coordinates
const QString Textfield::regex_wgs84_decimal = R"(\Alatlon\s*\(([-\d\.]+)(?:,)\s*([-\d\.]+)(?:,\s*([-\d\.]+))?\))";
const QString Textfield::regex_wgs84_dms = R"(\Alatlon\s*\(([-\d\.]+d\s*[-\d\.]*'?\s*[-\d\.]*"?)(?:,)\s*([-\d\.]+d\s*[-\d\.']*'?\s*[-\d\.]*"?)(?:,\s*([-\d\.]+))?\))";
const QString Textfield::regex_xy = R"(\Axy\s*\(([-\d\.]+)(?:,)\s*([-\d\.]+)(?:,\s*([-\d\.]+))?\))";

/**
 * @class Textfield
 * @brief Default constructor for a Textfield.
 * @details A Textfield is used to enter plain text.
 * @param[in] section INI section the controlled value belongs to.
 * @param[in] key INI key corresponding to the value that is being controlled by this Textfield.
 * @param[in] options XML node responsible for this panel with all options and children.
 * @param[in] no_spacers Keep a tight layout for this panel.
 * @param[in] parent The parent widget.
 */
Textfield::Textfield(const QString &section, const QString &key, const QDomNode &options, const bool &no_spacers,
    QWidget *parent) : Atomic(section, key, parent)
{
	/* label and text box */
	auto *key_label( new Label(QString(), QString(), options, no_spacers, key_, this) );
	setEmphasisWidget(key_label->label_);
	textfield_ = new QLineEdit; //single line text box
	connect(textfield_, &QLineEdit::textEdited, this, &Textfield::checkValue);
	focus_filter_ = new FocusEventFilter;
	textfield_->installEventFilter(focus_filter_); //select all text on focus receive

	/* action button */
	validity_button_ = new QToolButton; //a button that can pop up if the text has a certain format
	validity_button_->setStyleSheet("* {border: none}");
	QSize sz_label;
	sz_label.setWidth(static_cast<int>(fontMetrics().boundingRect(Cst::u_globe).width() * 1.2));
	sz_label.setHeight(static_cast<int>(fontMetrics().boundingRect(Cst::u_globe).height()));
	validity_button_->setFixedSize(sz_label);
	validity_button_->setFocusPolicy(Qt::NoFocus); //unexpected tab stops when invisible otherwise
	connect(validity_button_, &QToolButton::clicked, this, &Textfield::onValidButtonClicked);

	/* layout of textbox plus button and the main layout */
	auto *validity_button_layout( new QHBoxLayout );
	validity_button_layout->addWidget(textfield_);
	validity_button_layout->addWidget(validity_button_);
	auto *textfield_layout( new QHBoxLayout );
	setLayoutMargins(textfield_layout);
	textfield_layout->addWidget(key_label, 0, Qt::AlignLeft);
	textfield_layout->addLayout(validity_button_layout);

	/* choose size of text box */
	const QString size( options.toElement().attribute("size") );
	if (size.toLower() == "small")
		textfield_->setMinimumWidth(Cst::tiny);
	else
		textfield_->setMinimumWidth(Cst::width_textbox_medium);
	if (!no_spacers && size.toLower() != "large") //"large": text box extends to window width
		textfield_layout->addSpacerItem(buildSpacer());

	addHelp(textfield_layout, options, no_spacers);
	this->setLayout(textfield_layout);
	setOptions(options);
}

/**
 * @brief Default destructor with minimal cleanup.
 */
Textfield::~Textfield()
{
	delete focus_filter_;
}

/**
 * @brief Parse options for a Textfield from XML.
 * @param[in] options XML node holding the Textfield.
 */
void Textfield::setOptions(const QDomNode &options)
{
	validation_regex_ = options.toElement().attribute("validate");
	if (options.toElement().attribute("lenient").toLower() == "true")
		needs_prefix_for_evaluation_ = false; //always evaluate arithmetic expressions
	if (!options.toElement().attribute("placeholder").isEmpty())
		textfield_->setPlaceholderText(options.toElement().attribute("placeholder"));

	//user-set substitutions in expressions to style custom keys correctly:
	substitutions_ = expr::parseSubstitutions(options);
}

/**
 * @brief Event listener for changed INI values.
 * @details The "ini_value" property is set when parsing default values and potentially again
 * when setting INI keys while parsing a file.
 */
void Textfield::onPropertySet()
{
	const QString text_to_set( this->property("ini_value").toString() );
	if (ini_value_ == text_to_set)
		return;
	textfield_->setText(text_to_set);
	checkValue(text_to_set);
}

/**
 * @brief Perform checks on the entered text.
 * @details This function checks if the entered text stands for a recognized format such
 * as expressions (like the Number panel does), and if yes, styles the text box.
 * @param[in] text The current text to check.
 */
void Textfield::checkValue(const QString &text)
{
	setDefaultPanelStyles(text);
	static const int idx_full = 0; //regex index

	if (validation_regex_.isNull()) { //check for (arithmetic) expressions

		bool evaluation_success;
		const bool is_expression = expr::checkExpression(text, evaluation_success,
			substitutions_, needs_prefix_for_evaluation_);
		const bool is_invalid = is_expression && !evaluation_success;
		setInvalidStyle(is_invalid);
		//we use a fixed-width button for this because a Textfield can span the whole line
		//--> then those with and without icons are still aligned
		validity_button_->setText(is_invalid? Cst::u_warning : (is_expression? Cst::u_valid : ""));
		validity_button_->setCursor(is_expression? Qt::PointingHandCursor : Qt::ArrowCursor);
		validity_button_->setToolTip(is_expression?
			(is_invalid? tr("Wrong format for the expression") : tr("Valid expression")) : "");
		validity_button_->setProperty("invalid", is_invalid? "true" : "false");
		validity_button_->style()->unpolish(validity_button_);
		validity_button_->style()->polish(validity_button_);

	} else { //check against regex specified in XML

			const bool isCoords = (validation_regex_.compare("coordinates", Qt::CaseInsensitive)==0);
			static const QString regex_coordinates_ = regex_wgs84_decimal+"|"+regex_xy+"|"+regex_wgs84_dms;
			const QString regex = isCoords? regex_coordinates_ : validation_regex_;
			const QString u_valid = isCoords? Cst::u_globe : Cst::u_valid;
			const QString toolTipValid = isCoords? tr("Show online map") : tr("Valid expression");
			const QString toolTipInvalid = isCoords? tr("Wrong format for the provided coordinates") : tr("Wrong format for the provided value");
			
			const QRegularExpression rex_xml(regex);
			const QRegularExpressionMatch xml_match(rex_xml.match(text));
			const bool is_invalid = xml_match.captured(idx_full) != text && !text.isEmpty();
			
			setInvalidStyle(is_invalid);
			validity_button_->setText(is_invalid? Cst::u_warning : (text.isEmpty()? "" : u_valid));
			validity_button_->setCursor(text.isEmpty()? Qt::ArrowCursor : Qt::PointingHandCursor); //mark as link
			validity_button_->setToolTip(text.isEmpty()? "" : (is_invalid? toolTipInvalid : toolTipValid));
			
			//set "invalid" style on navigation button also:
			validity_button_->setProperty("invalid", is_invalid? "true" : "false");
			validity_button_->style()->unpolish(validity_button_);
			validity_button_->style()->polish(validity_button_);

	}
	setIniValue(text); //checks are just a hint - set anyway
}

/**
 * @brief Event listener for the action button.
 * @details The button is only shown for coordinates and will open the Browser
 * with a map link, or it acts to display info about valid/invalid expressions
 * (then leading to the help files).
 */
void Textfield::onValidButtonClicked() //for now this button only pops up for coordinates
{
	if (validity_button_->text().isEmpty())
		return;
	if (validity_button_->text() != Cst::u_globe) { //"valid" resp. "invalid" icons
		getMainWindow()->loadHelp("Input panels 1", "help-textfield");
		return;
	}

	//capture the lat and lon components to build the geohacks URL
	static const QString regex_coordinates_( regex_wgs84_decimal+"|"+regex_wgs84_dms );
	static const int idx_lat = 1, idx_lon = 2, idx_lat_dms = 4, idx_lon_dms = 5; //altitudes are at positions 3 and 6
	
	static const QRegularExpression rex_coord(regex_coordinates_);
	const QRegularExpressionMatch coord_match(rex_coord.match(textfield_->text()));
	QString url("https://tools.wmflabs.org/geohack/geohack.php?params=");
	
	if (!coord_match.captured(idx_lat).isEmpty() && !coord_match.captured(idx_lon).isEmpty()) {
		//decimal wgs84
		const QChar latchar = coord_match.captured(idx_lat).toDouble() < 0? 'S' : 'N';
		const QChar lonchar = coord_match.captured(idx_lon).toDouble() < 0? 'W' : 'E';
		url += coord_match.captured(idx_lat) + "_" + latchar + "_";
		url += coord_match.captured(idx_lon) + "_" + lonchar;
		QDesktopServices::openUrl(url);
	} else if(!coord_match.captured(idx_lat_dms).isEmpty() && !coord_match.captured(idx_lon_dms).isEmpty()) {
		//dms wgs84
		QString lat( coord_match.captured(idx_lat_dms) );
		QString lon( coord_match.captured(idx_lon_dms) );
		const QChar latchar = lat.section('d',1,1).toDouble() < 0? 'S' : 'N';
		const QChar lonchar = lon.section('d',1,1).toDouble() < 0? 'W' : 'E';
		lat.remove(QRegExp(" |\"")); lat.replace(QRegExp("d|'"), "_");
		lon.remove(QRegExp(" |\"")); lon.replace(QRegExp("d|'"), "_");
		
		url += lat + "_" + latchar + "_";
		url += lon + "_" + lonchar;
		QDesktopServices::openUrl(url);
	}
}
