text: //============================================================================
  // Copyright 2009-2020 ECMWF.
  // This software is licensed under the terms of the Apache Licence version 2.0
  // which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
  // In applying this licence, ECMWF does not waive the privileges and immunities
  // granted to it by virtue of its status as an intergovernmental organisation
  // nor does it submit to any jurisdiction.
  //============================================================================
  
  #include <boost/enable_shared_from_this.hpp>
  
  #include <QMessageBox>
  
  #include "CommandDesignerWidget.hpp"
  #include "CustomCommandHandler.hpp"
  #include "Child.hpp"
  #include "Str.hpp"
  #include "NodeQueryResult.hpp"
  #include "NodeExpression.hpp"
  
  using namespace boost;
  namespace po = boost::program_options;
  
  
  CommandDesignerWidget::CommandDesignerWidget(QWidget *parent) : QWidget(parent), menuItem_("")
  {
  	setupUi(this);
  
  
  	//at least for now, all commands will start with 'ecflow_client' and end with '<full_name>'
  	commandLineEdit_->setText("ecflow_client  <full_name>");
  	//commandLineEdit_->installEventFilter(this);
  	commandLineEdit_->setFocus();
  
  	haveSetUpDefaultCommandLine_ = false;
  	inCommandEditMode_           = false;
  	saveCommandsOnExit_          = false;
  
  	addToContextMenuCb_->setChecked(true);  // by default, suggest saving to context menu
  
  	// ensure the Save button is in the right state
  	on_commandLineEdit__textChanged();
  
  	saveNameLineEdit_->setPlaceholderText(tr("Unnamed"));
  
  	currentCommandSaved_ = false;
  	refreshSavedCommandList();
  
  
  	setSaveOptionsState(false, true);
  
  
  	// ensure we start on the command-builder tab
  	changeToTab(TAB_BUILD);
  	on_tabWidget__currentChanged(TAB_BUILD);  // trigger the callback to ensure the correct visibility of the save buttons
  
  	// set up and populate our list of options to the ecflow client
  	// - this would be more efficient if we did it only once, in a singleton, but
  	//   it seems pretty fast so we'll leave it like this for now
  	initialiseComponentListDetails();
  	clientOptionsDescriptions_ = new po::options_description("help" , po::options_description::m_default_line_length + 80);
  	cmdRegistry_.addAllOptions(*clientOptionsDescriptions_);
  	addClientCommandsToComponentList();
  
  
  #if QT_VERSION >= QT_VERSION_CHECK(5, 2, 0)
  	commandLineEdit_->setClearButtonEnabled(true);
  #endif
  
  
  	infoLabel_->setShowTypeTitle(false);
  	infoLabel_->showInfo(tr("Click command for help, double-click to insert"));
  
  	nodeSelectionView_->enableContextMenu(false);
  
  
  	nodeListLinkLabel_->setOpenExternalLinks(false);
  
  
  	connect(nodeSelectionView_, SIGNAL(selectionChanged()), this, SLOT(on_nodeSelectionChanged()));
  
  
  	setSavedCommandsButtonStatus();
  
  
  	// temporary
  	//saveCommandGroupBox_->setVisible(false);
  	//tabWidget_->setTabEnabled(2, false);
  	//savedCommandsGroupBox_->setVisible(false);
  
  }
  
  CommandDesignerWidget::~CommandDesignerWidget()
  {
  	delete clientOptionsDescriptions_;
  
  	if (saveCommandsOnExit_)
  		CustomSavedCommandHandler::instance()->writeSettings();
  
  	MenuHandler::refreshCustomMenuCommands();
  }
  
  
  void CommandDesignerWidget::initialiseComponentListDetails()
  {
  
  	// we don't want all the available commands to appear in the component list because they
  	// are not all relevant for one reason or another
  
  	componentBlacklist_.clear();
  
  	// The following rely on standard out:
  	componentBlacklist_.emplace_back("help");
  	componentBlacklist_.emplace_back("get");
  	componentBlacklist_.emplace_back("get_state");
  	componentBlacklist_.emplace_back("ch_suites");
  	componentBlacklist_.emplace_back("migrate");
  	componentBlacklist_.emplace_back("ping");
  	componentBlacklist_.emplace_back("server_version");
  	componentBlacklist_.emplace_back("stats");
  	componentBlacklist_.emplace_back("status");
  	componentBlacklist_.emplace_back("suites");
  	componentBlacklist_.emplace_back("version");
  	componentBlacklist_.emplace_back("zombie_get");
  
  	// The following only make sense for a persistent client. (like GUI, python):
  	componentBlacklist_.emplace_back("news");
  	componentBlacklist_.emplace_back("sync");
  	componentBlacklist_.emplace_back("sync_full");
  
  	// The following require a prompt:
  	componentBlacklist_.emplace_back("delete");
  	componentBlacklist_.emplace_back("terminate");
  	componentBlacklist_.emplace_back("halt");
  
  	// The following only make sense in a group:
  	componentBlacklist_.emplace_back("show");
  	componentBlacklist_.emplace_back("why");
  }
  
  
  void CommandDesignerWidget::initialiseCommandLine()
  {
  	if (!haveSetUpDefaultCommandLine_)
  	{
  		// put the cursor between the two pre-defined strings - this is where the command will go
  		commandLineEdit_->home(false);
  		commandLineEdit_->setCursorPosition(14);
  		commandLineEdit_->deselect();
  
  		haveSetUpDefaultCommandLine_ = true;
  	}
  }
  
  
  void CommandDesignerWidget::changeToTab(TabIndexes i)
  {
  	tabWidget_->setCurrentIndex(i);
  }
  
  
  void CommandDesignerWidget::setNodes(const std::vector<VInfo_ptr> &nodes)
  {
  	nodes_ = nodes;
  
  
  	// populate the list of nodes
  	nodeSelectionView_->setSourceModel(&nodeModel_);
  	nodeModel_.slotBeginReset();
  	nodeModel_.data()->add(nodes);
  	nodeModel_.slotEndReset();
  
  	// all should be selected at first
  	nodeSelectionView_->selectAll();
  
  
  	on_nodeSelectionChanged();  // get the number of selected nodes and act accordingly
  }
  
  
  
  // when the user clicks on the hyperlinked label which tells them how many nodes
  // will be acted on, we want to switch to the Nodes tab
  void CommandDesignerWidget::on_nodeListLinkLabel__linkActivated(const QString &link)
  {
  	if (link == "#nodes")
  	{
  		changeToTab(TAB_NODES);
  	}
  }
  
  
  void CommandDesignerWidget::setNodeNumberLinkText(int numNodes)
  {
  	QString s;
  
  	s = (numNodes == 1) ? "" : "s";
  
  	nodeListLinkLabel_->setText(tr("Command will be run on %1 node%2: <a href=\"#nodes\">click here for list</a>").arg(numNodes).arg(s));
  }
  
  // triggered when the user changes their node selection
  void CommandDesignerWidget::on_nodeSelectionChanged()
  {
  	setNodeNumberLinkText(selectedNodes().size());
  	on_commandLineEdit__textChanged();  // trigger the enabling/disabling of the Run button
  }
  
  
  const std::vector<VInfo_ptr>& CommandDesignerWidget::selectedNodes()
  {
  	nodeSelectionView_->getListOfSelectedNodes(nodes_);
  	return nodes_;
  }
  
  void CommandDesignerWidget::addClientCommandsToComponentList()
  {
  	// sort the commands into alphabetical order
  	std::vector< boost::shared_ptr<po::option_description> > options = clientOptionsDescriptions_->options();
  
     using opt_desc = boost::shared_ptr<po::option_description>;
     std::sort(options.begin(),options.end(),
               [](const opt_desc& a,const opt_desc& b) { return a->long_name() < b->long_name();}
     );
  
  	// loop through the sorted commands and add them to the list
  	size_t numOptions = options.size();
  
  	for(size_t i = 0; i < numOptions; i++)
  	{
  		std::string longName(options[i]->long_name());
  		if (!ecf::Child::valid_child_cmd(longName))  // do not show the 'child' options
  		{
  			if (std::find(componentBlacklist_.begin(), componentBlacklist_.end(), longName) == componentBlacklist_.end())  // not in blacklist?
  			{
  				componentsList_->addItem(QString("--") + QString::fromStdString(longName));
  				QString statusTip(QString::fromStdString(longName));
  				componentsList_->item(componentsList_->count()-1)->setStatusTip(statusTip);
  			}
  		}
  	}
  
  	// ensure the itemEntered slot is triggered
  	componentsList_->setMouseTracking(true);
  
  	// when the mouse hovers over an item, set the background colour of that item
  	componentsList_->setStyleSheet("QListWidget::item:hover {background-color:#FFFFDD;color:black}");
  }
  
  
  void CommandDesignerWidget::showCommandHelp(QListWidgetItem *item, bool showFullHelp)
  {
  	// get the command name
  	QString qCommand(item->text());
  	std::string command = qCommand.toStdString();
  	ecf::Str::replace_all(command, "--", "");  // remove the "--" from the start
  
  
  	// try to find it in our list of commands
  	const po::option_description* od = clientOptionsDescriptions_->find_nothrow(command,
  				false,  /*approx, will find nearest match*/
  				false, /*long_ignore_case = false*/
  				false  /*short_ignore_case = false*/
  				);
  
  	if (od)
  	{
  		// get the description, but only take the first line
  		std::vector< std::string > lines;
  		ecf::Str::split(od->description(),lines,"\n");
  		if (!lines.empty())
  		{
  			QString text = qCommand + QString(": ");
  			commandHelpLabel_->setText(text + QString::fromStdString(lines[0]));
  		}
  
  		if (showFullHelp)
  		{
  			commandManPage_->setText(qCommand + "\n\n" + QString::fromStdString(od->description()));
  		}
  	}
  	else
  	{
  		// not a command that we have help text for
  		commandHelpLabel_->setText("");
  	}
  }
  
  
  void CommandDesignerWidget::on_tabWidget__currentChanged(int index)
  {
  	//bool onSaveTab = (index == TAB_SAVE);
  	//saveCommandGroupBox_->setVisible(onSaveTab);
  	//saveOptionsButton_->setVisible(!onSaveTab);
  }
  
  
  
  // when the mouse moves over a command, display the help text for it
  void CommandDesignerWidget::on_componentsList__itemEntered(QListWidgetItem *item)
  {
  	showCommandHelp(item, false);
  	initialiseCommandLine();
  }
  
  // when the mouse is clicked on a command, display the help text for it
  void CommandDesignerWidget::on_componentsList__itemClicked(QListWidgetItem *item)
  {
  	showCommandHelp(item, true);
  	commandLineEdit_->setFocus(); // to keep the text cursor visible
  }
  
  // when the mouse is double-clicked on a command, insert it into the command line box
  void CommandDesignerWidget::on_componentsList__itemDoubleClicked(QListWidgetItem *item)
  {
  	insertComponent(item);
  	commandLineEdit_->setFocus(); // to keep the text cursor visible
  }
  
  
  void CommandDesignerWidget::insertComponent(QListWidgetItem *item)
  {
  	commandLineEdit_->insert(item->text() + " ");
  }
  
  
  
  void CommandDesignerWidget::on_commandLineEdit__textChanged()
  {
  	 // only allow to run a non-empty command, and on 1 or more nodes
  	runButton_->setEnabled((!commandLineEdit_->text().isEmpty()) && nodes_.size() > 0);
  
  	currentCommandSaved_ = false;
  	updateSaveButtonStatus();
  }
  
  void CommandDesignerWidget::on_saveNameLineEdit__textChanged()
  {
  	currentCommandSaved_ = false;
  	updateSaveButtonStatus();
  }
  
  void CommandDesignerWidget::on_addToContextMenuCb__stateChanged()
  {
  	currentCommandSaved_ = false;
  	updateSaveButtonStatus();
  }
  
  
  void CommandDesignerWidget::updateSaveButtonStatus()
  {
  	// inCommandEditMode_ means we're editing a command from the saved list
  	if (inCommandEditMode_)
  	{
  		overwriteButton_->setEnabled(true);
  		saveAsNewButton_->setEnabled(false);
  	}
  	else
  	{
  		overwriteButton_->setEnabled(false);
  		saveAsNewButton_->setEnabled(true);
  	}
  
  	// the cancel button is only available if we're in edit mode
  	cancelSaveButton_->setEnabled(inCommandEditMode_);
  }
  
  
  void CommandDesignerWidget::setSavedCommandsButtonStatus()
  {
  	int row = savedCommandsTable_->currentRow();
  	bool isRowSelected = (row != -1);
  	deleteCommandButton_   ->setEnabled(isRowSelected);
  	editCommandButton_     ->setEnabled(isRowSelected);
  	duplicateCommandButton_->setEnabled(isRowSelected);
  	useCommandButton_      ->setEnabled(isRowSelected);
  
  	upButton_  ->setEnabled(isRowSelected && row != 0);  // not the first row
  	downButton_->setEnabled(isRowSelected && row != savedCommandsTable_->rowCount()-1); // not the last row
  }
  
  
  bool CommandDesignerWidget::validSaveName(const std::string &name)
  {
  	// name empty?
  	if (name.empty())
  	{
  		QMessageBox::critical(nullptr,QObject::tr("Custom command"), tr("Please enter a name for the command"));
  		return false;
  	}
  
  
  	// is there already a command with this name?
  	int commandWithThisName = CustomSavedCommandHandler::instance()->findIndexFromName(name);
  	bool nameUnique;
  	if (inCommandEditMode_)
  		nameUnique =  (commandWithThisName == -1 || commandWithThisName == savedCommandsTable_->currentRow());
  	else
  		nameUnique =  (commandWithThisName == -1);
  
  	if (!nameUnique)
  	{
  		QMessageBox::critical(nullptr,QObject::tr("Custom command"), tr("A command with that name already exists - please choose another name"));
  		return false;
  	}
  	else
  	{
  		return true;
  	}
  }
  
  void CommandDesignerWidget::selectRow(int row)
  {
  	savedCommandsTable_->setCurrentCell(row, 0);
  }
  
  
  void CommandDesignerWidget::selectLastSavedCommand()
  {
  	int lastRow = savedCommandsTable_->rowCount()-1;
  	selectRow(lastRow);
  }
  
  // swap the commands in these two positions and select the one which will end up in the second position
  void CommandDesignerWidget::swapSavedCommands(int i1, int i2)
  {
  	CustomSavedCommandHandler::instance()->swapCommandsByIndex(i1, i2);
  	refreshSavedCommandList();
  	selectRow(i2);
  	setSavedCommandsButtonStatus();
  	saveCommandsOnExit_ = true;  // we won't save the commands yet, but mark for save on exit
  }
  
  
  void CommandDesignerWidget::on_saveAsNewButton__clicked()
  {
  	std::string name, command;
  	bool context;
  
  	name    = saveNameLineEdit_->text().toStdString();
  	command = commandLineEdit_->text().toStdString();
  	context = addToContextMenuCb_->isChecked();
  
  	if (validSaveName(name))
  	{
          CustomSavedCommandHandler::instance()->add(name, command, context, true);
  		refreshSavedCommandList();
  		currentCommandSaved_ = true;
  		updateSaveButtonStatus();
  		changeToTab(TAB_SAVE);
  		selectLastSavedCommand();
  		setSavedCommandsButtonStatus();
  		setSaveOptionsState(true, true);
  	}
  }
  
  void CommandDesignerWidget::on_overwriteButton__clicked()
  {
  	std::string name, command;
  	bool context;
  
  	name    = saveNameLineEdit_->text().toStdString();
  	command = commandLineEdit_->text().toStdString();
  	context = addToContextMenuCb_->isChecked();
  
  	if (validSaveName(name))
  	{
          CustomSavedCommandHandler::instance()->replace(savedCommandsTable_->currentRow(), name, command, context);
  		savedCommandsTable_->setEnabled(true);  // to show that we are no longer busy editing an entry
  		inCommandEditMode_ = false;
  		refreshSavedCommandList();
  		currentCommandSaved_ = true;
  		updateSaveButtonStatus();
  		setSaveOptionsState(true, true);
  	}
  }
  
  
  void CommandDesignerWidget::on_runButton__clicked()
  {
  	std::string command = commandLineEdit_->text().toStdString();
  
  	// save this in the command history
  	CustomCommandHistoryHandler::instance()->add(command, command, true, true);
  
  
  	// close the dialogue - the calling function will call the command() function
  	// to retrieve the user's command
  	//accept();
  }
  
  void CommandDesignerWidget::setSaveOptionsState(bool optionsVisible, bool saveOptionsButtonEnabled)
  {
  	// we just switch to the Saved Commands tab
  	//changeToTab(TAB_SAVE);
  
      saveCommandGroupBox_->setVisible(optionsVisible);
      QString buttonText = (optionsVisible) ? "Save Options <<" :  "Save Options >>";
      saveOptionsButton_->setText(buttonText);
      saveOptionsVisible_ = optionsVisible;
      saveOptionsButton_->setEnabled(saveOptionsButtonEnabled);
  }
  
  
  void CommandDesignerWidget::on_saveOptionsButton__clicked()
  {
  	setSaveOptionsState(!saveOptionsVisible_, true);
  
  	// we just switch to the Saved Commands tab
  	//changeToTab(TAB_SAVE);
  }
  
  
  void CommandDesignerWidget::on_useCommandButton__clicked()
  {
  	// just put the command into the command edit box
  	int row = savedCommandsTable_->currentRow();
  	QTableWidgetItem *commandItem = savedCommandsTable_->item(row, 2);
  	commandLineEdit_->setText(commandItem->text());
  }
  
  
  void CommandDesignerWidget::on_editCommandButton__clicked()
  {
  	int row = savedCommandsTable_->currentRow();
  
  	// get the details of this command from the table
  	QTableWidgetItem *nameItem    = savedCommandsTable_->item(row, 0);
  	QTableWidgetItem *contextItem = savedCommandsTable_->item(row, 1);
  	QTableWidgetItem *commandItem = savedCommandsTable_->item(row, 2);
  
  
  	inCommandEditMode_ = true;
  
  	// insert the details into the edit boxes
  	commandLineEdit_->setText(commandItem->text());
  	saveNameLineEdit_->setText(nameItem->text());
  	std::string context = contextItem->text().toStdString();
  	addToContextMenuCb_->setChecked(CustomSavedCommandHandler::instance()->stringToBool(context));
  
  	savedCommandsTable_    ->setEnabled(false);  // to show that we are busy editing an entry
  	deleteCommandButton_   ->setEnabled(false);  // to show that we are busy editing an entry
  	editCommandButton_     ->setEnabled(false);  // to show that we are busy editing an entry
  	duplicateCommandButton_->setEnabled(false);  // to show that we are busy editing an entry
  	useCommandButton_      ->setEnabled(false);  // to show that we are busy editing an entry
  	upButton_              ->setEnabled(false);  // to show that we are busy editing an entry
  	downButton_            ->setEnabled(false);  // to show that we are busy editing an entry
  
  	updateSaveButtonStatus();
  
  	// users should have the Save Options visible
  	setSaveOptionsState(true, false);
  }
  
  
  void CommandDesignerWidget::on_savedCommandsTable__cellDoubleClicked(int row, int column)
  {
  	on_editCommandButton__clicked();  // same as selecting a cell and clicking 'edit'
  }
  
  void CommandDesignerWidget::on_duplicateCommandButton__clicked()
  {
  	CustomSavedCommandHandler::instance()->duplicate(savedCommandsTable_->currentRow());
  	refreshSavedCommandList();
  }
  
  
  void CommandDesignerWidget::on_deleteCommandButton__clicked()
  {
  	CustomSavedCommandHandler::instance()->remove(savedCommandsTable_->currentRow());
  	refreshSavedCommandList();
  }
  
  void CommandDesignerWidget::on_upButton__clicked()
  {
  	int row = savedCommandsTable_->currentRow();
  	swapSavedCommands(row, row-1);
  }
  
  void CommandDesignerWidget::on_downButton__clicked()
  {
  	int row = savedCommandsTable_->currentRow();
  	swapSavedCommands(row, row+1);
  }
  
  
  void CommandDesignerWidget::on_cancelSaveButton__clicked()
  {
  	savedCommandsTable_->setEnabled(true);  // to show that we are no longer busy editing an entry
  	inCommandEditMode_ = false;
  	updateSaveButtonStatus();
  	refreshSavedCommandList();
  	setSaveOptionsState(true, true);
  }
  
  
  void CommandDesignerWidget::refreshSavedCommandList()
  {
  	int n = CustomSavedCommandHandler::instance()->numCommands();
  
  	savedCommandsTable_->clearContents();
  
  	for (int i = 0; i < n; i++)
  	{
  		CustomCommand *command = CustomSavedCommandHandler::instance()->commandFromIndex(i);
  		addCommandToSavedList(command, i);
  	}
  	savedCommandsTable_->setRowCount(n);
  	setSavedCommandsButtonStatus();
  }
  
  
  void CommandDesignerWidget::addCommandToSavedList(CustomCommand *command, int row)
  {
  	QTableWidgetItem *nameItem    = new QTableWidgetItem(QString::fromStdString(command->name()));
  	QTableWidgetItem *contextItem = new QTableWidgetItem(QString::fromStdString(command->contextString()));
  	QTableWidgetItem *commandItem = new QTableWidgetItem(QString::fromStdString(command->command()));
  
  	// if the command already exists (by name) then we will replaced it;
  	// otherwise add a new row to the table
  
  //	int thisRow = CustomCommandHandler::instance()->findIndex(command->name());
  //	if (thisRow == -1)
  //		thisRow = savedCommandsTable_->rowCount();
  
  	//contextItem->setCheckable();
  
  	int lastRow = savedCommandsTable_->rowCount()-1;
  
  	if (row > lastRow)
  		savedCommandsTable_->insertRow(row);
  
  	savedCommandsTable_->setItem(row, 0, nameItem);
  	savedCommandsTable_->setItem(row, 1, contextItem);
  	savedCommandsTable_->setItem(row, 2, commandItem);
  }
  
  
  void CommandDesignerWidget::on_savedCommandsTable__cellClicked(int row, int column)
  {
  	setSavedCommandsButtonStatus();
  }
  
  
  // the caller has asked for a 'fake' menu item from the dialogue, so we create it here
  MenuItem &CommandDesignerWidget::menuItem()
  {
  	// put the right information into our menu item and return a reference to it
  	menuItem_.setCommand(commandLineEdit_->text().toStdString());
  	menuItem_.setCustom(true);
  
  	if (menuItem_.visibleCondition() == nullptr)
  	{
  		BaseNodeCondition *trueCond  = new TrueNodeCondition();
  		menuItem_.setEnabledCondition(trueCond);
  		menuItem_.setVisibleCondition(trueCond);
  		menuItem_.setQuestionCondition(trueCond);
  	}
  	return menuItem_;
  }
  
  
  /*
  bool CommandDesignerWidget::eventFilter(QObject* object, QEvent* event)
  {
  	if(object == commandLineEdit_ && event->type() == QEvent::FocusIn)
  	{
  		initialiseCommandLine();
  		return false;
  	}
  	return false;
  }
  */
  