/*
 * Copyright 2009- 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 "CommandDesignerWidget.hpp"

#include <QMessageBox>
#include <boost/enable_shared_from_this.hpp>

#include "CustomCommandHandler.hpp"
#include "NodeExpression.hpp"
#include "NodeQueryResult.hpp"
#include "ecflow/core/Child.hpp"
#include "ecflow/core/Str.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();

    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"));

    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;
}
*/
