/***************************** LICENSE START ***********************************

 Copyright 2014 ECMWF and INPE. This software is distributed under the terms
 of the Apache License version 2.0. In applying this license, ECMWF does not
 waive the privileges and immunities granted to it by virtue of its status as
 an Intergovernmental Organization or submit itself to any jurisdiction.

 ***************************** LICENSE END *************************************/

//#include <QtWidgets>
#include <QtPrintSupport/QPrinter>
#include <QtPrintSupport/QPrintDialog>

#include <QtGlobal>
#include <QActionGroup>
#include <QClipboard>
#include <QDebug>
#include <QFileInfo>
#include <QFileDialog>
#include <QFontDialog>
#include <QLocale>
#include <QMessageBox>
#include <QSettings>
#include <QToolButton>
#include <QVBoxLayout>

#include "MacroEdit.h"
#include "EditorTheme.h"
#include "FindTextDialog.h"
#include "GotoLineDialog.h"
#include "TabsDialog.h"
#include "RunDialog.h"
#include "CodeTemplateDialog.h"
#include "MvQAbout.h"
#include "MvQMethods.h"
#include "MacroToPythonConvert.h"
#include "MvMiscellaneous.h"

#include "ConfigLoader.h"
#include "MvQColourWidget.h"

#include "mvplaintextedit.h"
#include "Path.h"
#include "Metview.h"
#include "MvPath.hpp"
#include "MvLog.h"
#include <csignal>
#include <cassert>

//#define MACROEDIT_USE_LOGTIMER
#define MACROEDIT_DEBUG__

static bool initFinished = false;


// ---------------------------------------------------------------------------
// MacroRunTask::MacroRunTask
// Its purpose is to start the macro service and 'listen' for messages from it
// (it is a ReplyObsever). Shell and Python is run through QProcess.
// ---------------------------------------------------------------------------

MacroRunTask::MacroRunTask(MacroEdit* editor, QString macro, MacroRunOptions options) :
    ReplyObserver(editor),
    editor_(editor),
    macro_(macro),
    options_(options),
    proc_(nullptr)
{
    // to enable communication to the editor, we need to use
    // signals and slots.

    connect(this, SIGNAL(sendMessageToLog(const std::string&)),
            editor_, SLOT(logMessage(const std::string&)));

    connect(this, SIGNAL(sendMacroFinishedMsg(int)),
            editor_, SLOT(macroFinished(int)));

    connect(this, SIGNAL(sendCurrentLineNumber(int)),
            editor_, SLOT(stepLineNumber(int)));

    connect(this, SIGNAL(stopAttempFailed(int)),
            editor_, SLOT(stopAttemptFailed(int)));
}

// ---------------------------------------------------------------------------
// MacroRunTask::~MacroRunTask
// Destructor.
// ---------------------------------------------------------------------------

MacroRunTask::~MacroRunTask()
{
    if (proc_) {
        proc_->terminate();
        proc_->deleteLater();
    }
}


// ---------------------------------------------------------------------------
// MacroRunTask::start
// Calls the macro service with a path to the macro to be run.
// ---------------------------------------------------------------------------

void MacroRunTask::start()
{
    // first we must reset thist
    macroPid_ = -1;

    MvRequest r(editor_->languageHelper()->className().toUtf8().constData());  // is this the right string to use?

    r("PATH") = macro_.toUtf8().constData();                                        // path to macro
    r("_NAME") = macro_.toUtf8().constData();                                       // path to macro
    r("_CLASS") = editor_->languageHelper()->className().toUtf8().constData();      // class name
    r("_ACTION") = options_.runMode_.c_str();                                       // e.g. "prepare" or "syntax"
    r("_SERVICE") = editor_->languageHelper()->serviceName().toUtf8().constData();  // service name
    r("_TRACE") = options_.trace_;
    r("_WAITMODE") = options_.waitMode_;
    r("_SENDLINES") = options_.sendLines_;
    r("_PAUSE") = options_.pause_;

    r.print();


    // do we want to run this as a standard Metview service?

    if (editor_->languageHelper()->runAsService()) {
        callService(editor_->languageHelper()->serviceName().toUtf8().constData(), r);
    }

    // otherwise, use QProcess to run the command asynchronously
    else {
        // we always start afresh with a new QProcess object
        if (proc_) {
            proc_->kill();
            proc_->deleteLater();
            proc_ = nullptr;
        }

        Q_ASSERT(proc_ == nullptr);

        if (!proc_) {
            proc_ = new QProcess(this);

            connect(proc_, SIGNAL(finished(int, QProcess::ExitStatus)),
                    this, SLOT(slotProcFinished(int, QProcess::ExitStatus)));

            connect(proc_, SIGNAL(readyReadStandardOutput()),
                    this, SLOT(slotProcStdOutput()));
        }

        Q_ASSERT(proc_->state() == QProcess::NotRunning);

        // the stdout and stderr will be merged -> same as "2>&1"
        proc_->setProcessChannelMode(QProcess::MergedChannels);

        // we directly call python
        if (editor_->languageHelper()->name() == "Python") {
            QFileInfo fInfo(macro_);
            proc_->setWorkingDirectory(fInfo.dir().path());
            proc_->start("python3", QStringList() << macro_);
        }
        else {
            QString command = editor_->languageHelper()->serviceName();
            command.replace("%s", macro_);
            proc_->start("/bin/sh", QStringList() << "-c" << command);
        }
    }
}

void MacroRunTask::stop()
{
    // qDebug() << "stop" << "macroPid=" << macroPid_;
    if (macroPid_ > 0) {
        Q_ASSERT(proc_ == nullptr);
        if (kill(macroPid_, 9) == 0) {
            macroPid_ = -1;
            sendMacroFinishedMsg(1);
        }
        else {
            emit stopAttempFailed(macroPid_);
        }
        return;
    }
    else if (proc_) {
        // qDebug() << "procId" << proc_->processId();
        proc_->terminate();
    }
}


// ---------------------------------------------------------------------------
// MacroRunTask::reply
// Called automatically when the macro service sends a REPLY message.
// Here, we simply want to tell
// the editor that the macro has finished.
// ---------------------------------------------------------------------------

void MacroRunTask::reply(const MvRequest&, int err)
{
    emit sendMacroFinishedMsg(err);
}


// ---------------------------------------------------------------------------
// MacroRunTask::progress
// Called automatically when the macro service sends a PROGRESS message.
// Not sure if this ever happens, or what the contents of such a message are.
// ---------------------------------------------------------------------------

void MacroRunTask::progress(const MvRequest&)
{
}


// ---------------------------------------------------------------------------
// MacroRunTask::message
// Called automatically when the macro service sends a MESSAGE message.
// Here, we just pass the given message string to the editor's message log.
// ---------------------------------------------------------------------------

void MacroRunTask::message(const std::string& msg)
{
    // Macro can encode special messages which only the Macro Editor will interpret
    // They start with 'MACROEDITOR:' and are followed by a command and possibly
    // followed by a space-separated parameters.
    // Currently, we support
    //  MACROEDITOR:LINE x
    //    where x is the current line number being executed


    char command[64] = "";
    char param[8] = "";

    if (sscanf(msg.c_str(), "MACROEDITOR:%63s %7s", command, param) == 2) {
        // special message for the macro editor - which message is it?
        if (!strcmp(command, "LINE"))  // line number
        {
            int line = atoi(param);
            emit sendCurrentLineNumber(line);
        }
    }
    else {
        // standard message - send it to the log area
        emit sendMessageToLog(msg);
    }
}

void MacroRunTask::slotProcStdOutput()
{
    if (!proc_)
        return;

    auto s = proc_->readAllStandardOutput();
    //    qDebug() << "stdout" << s;
    message(s.toStdString());
}

void MacroRunTask::slotProcFinished(int exitCode, QProcess::ExitStatus exitStatus)
{
    if (!proc_)
        return;
#ifdef MACROEDIT_DEBUG__
    qDebug() << "slotProcFinished"
             << "exitCode=" << exitCode << "exitStatus" << exitStatus;
#endif
    // get remaining stdout (it is merged with stderr)
    QString stdOutTxt(proc_->readAllStandardOutput());
    if (!stdOutTxt.isEmpty()) {
        message(stdOutTxt.toStdString());
    }

#ifdef MACROEDIT_DEBUG__
    qDebug() << "program" << proc_->program();
    qDebug() << "args" << proc_->arguments();
    qDebug() << "stderr" << stdOutTxt;
#endif

    // we do not need the QProcess any more
    proc_->deleteLater();
    proc_ = nullptr;

    if (exitCode == 0 && exitStatus == QProcess::NormalExit) {
        emit sendMacroFinishedMsg(0);
        return;
    }

    emit sendMacroFinishedMsg((exitCode > 0) ? exitCode : 1);
}

void MacroRunTask::print(std::ostream&) const
{
    /*     s << "MacroRunTask["
      << action_.name()
      << ","
      << action_.mode()
      << ","
      << *object_
      << "]";
*/
}


/////////////////////////////////////////////////////////////////////////////////


//=================================================================
class ReplyHandler : public MvReply
{
    void callback(MvRequest&) override;
};

void ReplyHandler::callback(MvRequest& in)
{
    auto* s = (ReplyObserverH*)getReference();
    if (s == 0)
        return;

    const char* p = nullptr;
    int i = 0;
    while ((p = getMessage(i++)))
        (*s)->message(p);

    (*s)->reply(in, getError());

    delete s;
}

//=================================================================

class ProgressHandler : public MvProgress
{
    void callback(MvRequest& r) override;
};

void ProgressHandler::callback(MvRequest& in)
{
    auto* s = (ReplyObserverH*)getReference();
    if (s == nullptr)
        return;

    // get the pid of the macro we run
    if (const char* chv = in.getVerb()) {
        if (strcmp(chv, "REGISTER_MACRO_PID") == 0) {
            int v = -1;
            in.getValue(v, "PID");
            assert((*s)->macroPid() == -1);
            if (v > 0) {
                (*s)->setMacroPid(v);
            }
        }
        return;
    }

    const char* p = nullptr;
    int i = 0;
    while ((p = getMessage(i++)))
        (*s)->message(p);

    (*s)->progress(in);
}

//=================================================================

ReplyObserver::ReplyObserver(QObject* parent) :
    QObject(parent),
    macroPid_(-1)
{
    // This will install the handler on creation of the first instance
    static ProgressHandler progressHandler;
    static ReplyHandler replyHandler;
}

ReplyObserver::~ReplyObserver() = default;

void ReplyObserver::callService(const std::string& service, const MvRequest& r)
{
    MvApplication::callService(service.c_str(), r, new ReplyObserverH(this));
}


/////////////////////////////////// class MacroEdit ////////////////////////////////

QString MacroEdit::emptyString_;  // used as a default argument to findString()


MacroEdit::MacroEdit(QString appName, QWidget* parent, QString* fileToLoad) :
    QMainWindow(parent),
    appName_(appName),
    fontSize_(11),
    readOnly_(false),
    haveFocus_(false),
    autoInsertLicence_(false),
    runTask_(nullptr),
    findDialog_(nullptr),
    gotoLineDialog_(nullptr),
    tabsDialog_(nullptr),
    runDialog_(nullptr),
    codeTemplateDialog_(nullptr),
    rowColLabel_(nullptr),
    messageLabel_(nullptr),
    languageHelper_(nullptr),
    highlighter_(nullptr)
{
    setupUi(this);  // this sets up GUI// setupFileMenu();

    connect(actionStop, SIGNAL(triggered()), this, SLOT(stop()));
    connect(actionRun, SIGNAL(triggered()), this, SLOT(run()));
    connect(actionProcess_Monitor, SIGNAL(triggered()), this, SLOT(monitor()));
    connect(actionPrint, SIGNAL(triggered()), this, SLOT(print()));
    connect(actionOpen_in_External_editor, SIGNAL(triggered()), this, SLOT(external()));
    connect(actionCheckSyntax, SIGNAL(triggered()), this, SLOT(check_syntax()));
    connect(actionConvertToPython, SIGNAL(triggered()), this, SLOT(convertToPython()));

    MvQ::addShortCutToToolTip(this);
    MvQ::showShortcutInContextMenu(this);

    // This code causes a crash on MacOS Big Sur with Qt6.0.2 so it is commented out!
    //#ifdef __APPLE__
    //    // Set this to false if you want non-native menus on Mac OS X (i.e. menus are the same as under Linux)
    //    menuBar()->setNativeMenuBar(false);
    //#endif

    // setupHelpMenu();
    setupEditor();

    // read the user's settings from a file
    readSettings();

    // load the text file if specified
    if (fileToLoad != nullptr) {
        fileName_ = *fileToLoad;
        loadTextFile(fileName_);
    }

    // setCentralWidget(editor);
    setWindowTitle(tr("Metview Code Editor [*]"));

    initFinished = true;
}

MacroEdit::~MacroEdit()
{
    if (languageHelper_)
        delete languageHelper_;
}

void MacroEdit::setupEditor()
{
    // --- Save ---

    // if the document is modifed, then enable the 'save' action
    connect(textEditor->document(), SIGNAL(modificationChanged(bool)), actionSave, SLOT(setEnabled(bool)));

    // if the document is modifed, then tell this window about it
    connect(textEditor->document(), SIGNAL(modificationChanged(bool)), this, SLOT(setWindowModified(bool)));


    actionSave->setShortcut(QKeySequence::Save);
    connect(actionSave, SIGNAL(triggered()), this, SLOT(save()));
    connect(actionSave_As, SIGNAL(triggered()), this, SLOT(saveAs()));
    connect(action_Reload_from_file, SIGNAL(triggered()), this, SLOT(reload()));


    connect(textEditor->document(), SIGNAL(contentsChanged()), this, SLOT(documentWasModified()));


    // was the cursor position changed?
    connect(textEditor, SIGNAL(cursorPositionChanged()), this, SLOT(cursorPositionWasChanged()));


    // have we regained or lost focus?
    connect(textEditor, SIGNAL(focusRegained()), this, SLOT(onFocusRegained()));
    connect(textEditor, SIGNAL(focusLost()), this, SLOT(onFocusLost()));


    // --- Exit ---
    connect(actionExit, SIGNAL(triggered()), this, SLOT(close()));


    // --- Undo, redo ---
    connect(textEditor->document(), SIGNAL(undoAvailable(bool)),
            actionUndo, SLOT(setEnabled(bool)));
    connect(textEditor->document(), SIGNAL(redoAvailable(bool)),
            actionRedo, SLOT(setEnabled(bool)));

    setWindowModified(textEditor->document()->isModified());

    actionSave->setEnabled(textEditor->document()->isModified());
    actionUndo->setEnabled(textEditor->document()->isUndoAvailable());
    actionRedo->setEnabled(textEditor->document()->isRedoAvailable());

    connect(actionUndo, SIGNAL(triggered()), textEditor, SLOT(undo()));
    connect(actionRedo, SIGNAL(triggered()), textEditor, SLOT(redo()));

    actionUndo->setShortcut(QKeySequence::Undo);
    actionRedo->setShortcut(QKeySequence::Redo);


    // --- Cut, copy, paste ---
    actionCut->setEnabled(false);
    actionCopy->setEnabled(false);
    actionPaste->setEnabled(!QApplication::clipboard()->text().isEmpty());

    connect(actionCut, SIGNAL(triggered()), textEditor, SLOT(cut()));
    connect(actionCopy, SIGNAL(triggered()), textEditor, SLOT(copy()));
    connect(actionPaste, SIGNAL(triggered()), textEditor, SLOT(paste()));

    connect(textEditor, SIGNAL(copyAvailable(bool)), actionCut, SLOT(setEnabled(bool)));
    connect(textEditor, SIGNAL(copyAvailable(bool)), actionCopy, SLOT(setEnabled(bool)));


    // --- Clipboard ---
    connect(QApplication::clipboard(), SIGNAL(dataChanged()), this, SLOT(clipboardDataChanged()));


    actionCut->setShortcut(QKeySequence::Cut);
    actionCopy->setShortcut(QKeySequence::Copy);
    actionPaste->setShortcut(QKeySequence::Paste);

    // --- Select ---
    connect(actionSelect_All, SIGNAL(triggered()), textEditor, SLOT(selectAll()));
    actionSelect_All->setShortcut(QKeySequence::SelectAll);


    // --- Find, replace ---
    connect(actionFind, SIGNAL(triggered()), this, SLOT(findDialog()));
    connect(actionFind_Next, SIGNAL(triggered()), this, SLOT(findNext()));
    connect(actionFind_Selection, SIGNAL(triggered()), this, SLOT(findSelection()));
    connect(actionReplace, SIGNAL(triggered()), this, SLOT(findReplaceDialog()));


    // for the 'find next' functionality, although Qt (at the time of writing) uses
    // both F3 and CTRL-G for most platforms, this is not true for Linux. Therefore,
    // we have to add CTRL-G ourselves.

    QKeySequence ctrlg(tr("Ctrl+G"));
    QKeySequence f3(tr("F3"));
    QList<QKeySequence> qks;
    qks.push_back(ctrlg);
    // qks.push_back (QKeySequence::FindNext);
    qks.push_back(f3);  // for when FindNext is not F3
    actionFind_Next->setShortcuts(qks);
    // actionFind_Next->setShortcutContext(Qt::WidgetShortcut);

    actionFind->setShortcut(QKeySequence::Find);
    actionFind_Previous->setShortcut(QKeySequence::FindPrevious);
    actionReplace->setShortcut(QKeySequence("Ctrl+R"));


    //  --- Go to line ---
    connect(actionGo_To_Line, SIGNAL(triggered()), this, SLOT(gotoLine()));


    // --- Indent lines ---
    connect(actionIndent_Lines, SIGNAL(triggered()), this, SLOT(onIndentLines()));

    // --- Comment lines ---
    connect(actionComment_Lines, SIGNAL(triggered()), this, SLOT(onCommentLines()));

    // --- Settings menu ---
    connect(actionTabs, SIGNAL(triggered()), this, SLOT(tabsDialog()));


    // --- Run menu ---
    connect(actionRun_Options, SIGNAL(triggered()), this, SLOT(runDialog()));

    // --- Insert menu ---
    connect(actionCode_Template, SIGNAL(triggered()), this, SLOT(codeTemplateDialog()));
    connect(action_Licence_Text, SIGNAL(triggered()), this, SLOT(insertLicenceText()));
    connect(actionAutoInsertLicenceOn, SIGNAL(triggered()), this, SLOT(setAutoInsertLicenceOn()));
    connect(actionAutoInsertLicenceOff, SIGNAL(triggered()), this, SLOT(setAutoInsertLicenceOff()));

    connect(actionFunctionList, SIGNAL(triggered()),
            this, SLOT(slotFunctionList()));

    connect(actionCodeHelper, SIGNAL(triggered()),
            this, SLOT(slotCodeHelper()));

    connect(actionColourHelper, SIGNAL(triggered()),
            this, SLOT(slotColourHelper()));

    autoInsertLicenceGroup_ = new QActionGroup(this);                // group the 'auto insert licence' actions
    autoInsertLicenceGroup_->addAction(actionAutoInsertLicenceOn);   // group the 'auto insert licence' actions
    autoInsertLicenceGroup_->addAction(actionAutoInsertLicenceOff);  // group the 'auto insert licence' actions


    // --- Font size ---
    // actionEnlarge_Font->setShortcutContext(Qt::WidgetShortcut);
    // actionShrink_Font->setShortcutContext(Qt::WidgetShortcut);

    connect(actionEnlarge_Font, SIGNAL(triggered()), this, SLOT(enlargeFont()));
    connect(actionShrink_Font, SIGNAL(triggered()), this, SLOT(shrinkFont()));
    connect(actionNormal_Font, SIGNAL(triggered()), this, SLOT(normalFont()));
    connect(actionLarge_Font, SIGNAL(triggered()), this, SLOT(largeFont()));


    QKeySequence ctrleq(tr("Ctrl+="));  // for UK keyboards, QKeySequence::ZoomIn only works with the numeric keypad
    QList<QKeySequence> qkszoom;
    qkszoom.push_back(ctrleq);
    qkszoom.push_back(QKeySequence::ZoomIn);

    actionEnlarge_Font->setShortcuts(qkszoom);
    actionShrink_Font->setShortcut(QKeySequence::ZoomOut);

    changeFontSize(0);                  // set the font: '0' means 'change the size by 0'
    QCoreApplication::processEvents();  // we need to do this in order for the font settings to be properly
                                        // registered before the line numbering is rendered


    textEditor->setLineWrapMode(QPlainTextEdit::NoWrap);  // we don't allow line wrapping


    // --- show line numbers ---
    connect(actionShow_Line_Numbers, SIGNAL(toggled(bool)), this, SLOT(showLineNumbers(bool)));
    actionShow_Line_Numbers->setCheckable(true);
    actionShow_Line_Numbers->setChecked(true);


    // --- the Language menu ---

    // get a list of all the available programming languages

    std::vector<LanguageHelper*> languageHelpers = languageHelperFactory_.languageHelpers();

    for (auto& languageHelper : languageHelpers) {
        auto* actionLang = new QAction(this);
        actionLang->setObjectName("action_" + languageHelper->name());
        actionLang->setText(languageHelper->name());
        actionLang->setCheckable(true);
        menu_Language->addAction(actionLang);
        languageActions_.push_back(actionLang);
    }


    // every action in the language menu will share a common signal/slot, with a pointer
    // to the particular chosen action being sent in order to identify which was chosen

    connect(menu_Language, SIGNAL(triggered(QAction*)), this, SLOT(languageChosen(QAction*)));


    // the Help menu
    connect(actionAbout, SIGNAL(triggered()),
            this, SLOT(slotShowAboutBox()));

    actionFunctionList->setEnabled(false);
    actionCodeHelper->setEnabled(false);
    actionColourHelper->setEnabled(false);

    // some defaults for the toolbars - these will be over-ridden by the user's preferences
    fileToolBar->setVisible(true);
    printToolBar->setVisible(false);
    findToolBar->setVisible(false);
    editToolBar->setVisible(true);
    viewToolBar->setVisible(true);
    programToolBar->setVisible(true);
    insertItemToolBar->setVisible(false);
    helpToolBar->setVisible(true);

    // actionStop->setVisible(false);

    // This message label will show the main messages in the status bar.
    // We ensure that the user can select & copy the text in case they
    // need to send it to us.
    messageLabel_ = new QLabel("", this);
    messageLabel_->setTextInteractionFlags(Qt::TextSelectableByMouse);
    // messageLabel_->setMinimumSize(messageLabel_->sizeHint());
    statusBar()->addPermanentWidget(messageLabel_, 1);  // '1' means 'please stretch me when resized'


    // row/col indicator on the status bar
    // - ensure that it is updated when the cursor is moved
    rowColLabel_ = new QLabel("L: 9999, C: 999", this);
    rowColLabel_->setMinimumSize(rowColLabel_->sizeHint());
    statusBar()->addPermanentWidget(rowColLabel_);
    connect(textEditor, SIGNAL(cursorPositionChanged()), this, SLOT(updateRowColLabel()));


    // ensure that the user cannot write to the message log
    logMessages->setReadOnly(true);

    // we do not want a line to be highlighted initially
    highlightedLine_ = -1;

    // Colour scheme menu
    themeAg_ = new QActionGroup(this);
    QMapIterator<QString, EditorTheme*> it(EditorTheme::items());
    while (it.hasNext()) {
        it.next();
        QAction* a = menuTheme->addAction(it.value()->label());
        a->setCheckable(true);
        a->setData(it.key());
        themeAg_->addAction(a);
    }

    connect(themeAg_, SIGNAL(triggered(QAction*)),
            this, SLOT(slotSelectTheme(QAction*)));

    // Initialise with the default theme
    setTheme("metview");

    //------------------------------------
    // Log panel
    //------------------------------------

    logMessages->show();
    actionLogPanel->setChecked(true);

    connect(actionLogPanel, SIGNAL(toggled(bool)),
            logMessages, SLOT(setVisible(bool)));

    //------------------------------------
    // Sidebar
    //------------------------------------

    codeHelperW->hide();
    actionSideBar->setChecked(false);
    actionSideBar->setEnabled(false);

    connect(actionSideBar, SIGNAL(toggled(bool)),
            codeHelperW, SLOT(setVisible(bool)));

    connect(codeHelperW, SIGNAL(showRequested(bool)),
            actionSideBar, SLOT(setChecked(bool)));

    connect(codeHelperW, SIGNAL(insertRequested(QString)),
            this, SLOT(insertText(QString)));
}


// we shoud call it after a language is set
void MacroEdit::adjustSideBarForLanguage()
{
    codeHelperW->setLanguage(languageHelper_);

    bool st = codeHelperW->canSupportLanguage();
    actionSideBar->setEnabled(st);
    actionCodeHelper->setEnabled(st);
    actionColourHelper->setEnabled(st);
    actionFunctionList->setEnabled(st);

    if (!st)
        codeHelperW->setVisible(false);
}

// -------------------------------------------------------
// MacroEdit::loadTextFile
// Loads the given file into the editor and sets the title
// -------------------------------------------------------

void MacroEdit::loadTextFile(const QString& fileToLoad)
{
    QFile file(fileToLoad);  // set the file to be loaded

    if (file.open(QFile::ReadOnly | QFile::Text))  // try to open in read-only mode
    {
        QTextStream in(&file);
        QApplication::setOverrideCursor(Qt::WaitCursor);
        textEditor->setPlainText(in.readAll());
        QApplication::restoreOverrideCursor();

        textEditor->document()->setModified(false);
        setWindowModified(false);

        setCurrentFile(fileToLoad);
        statusMessage(tr("File loaded"), 2000);
    }

    else {
        QMessageBox::warning(this, appName_,
                             tr("Cannot read file %1:\n%2.")
                                 .arg(fileToLoad)
                                 .arg(file.errorString()));
        return;
    }
}


bool MacroEdit::saveTextFile(const QString& fileToSave)
{
    QFile file(fileToSave);

    if (!file.open(QFile::WriteOnly | QFile::Text)) {
        QMessageBox::warning(this, appName_,
                             tr("Cannot write file %1:\n%2.")
                                 .arg(fileToSave)
                                 .arg(file.errorString()));
        return false;
    }

    {
        // inside its own block to ensure that the text stream is flushed before we call setCurrentFile()
        QTextStream out(&file);
        QApplication::setOverrideCursor(Qt::WaitCursor);
        out << textEditor->toPlainText();
    }
    QApplication::restoreOverrideCursor();
    textEditor->document()->setModified(false);
    setWindowModified(false);
    setCurrentFile(fileToSave);
    statusMessage(tr("File saved"), 2000);
    return true;
}


// ---------------------------------------------------------------------------
// MacroEdit::setCurrentFile
// Tells the editor that it has a new file or its current file status
// has been changed (e.g. saved). Things like the window title and
// read-only status are updated.
// If lastSaveTimeOnly is true, then only the last save time will be updated.
// ---------------------------------------------------------------------------

void MacroEdit::setCurrentFile(const QString& fileName, bool lastSaveTimeOnly)
{
    QString title;
    QFileInfo fi(fileName);
    fileName_ = fileName;

    if (!lastSaveTimeOnly) {
        if (fileName_.isEmpty())
            title = "untitled.txt";
        else
            title = strippedFileName(fileName_) + "[*]";  // the '*' will only appear when the document is modified

        if (!fi.isWritable()) {
            readOnly_ = true;
            textEditor->setReadOnly(true);
            title += tr(" (Read only)");
            // onReadOnlyStatusChanged (true);
        }
        else {
            readOnly_ = false;
            textEditor->setReadOnly(false);
            // onReadOnlyStatusChanged (false);
        }

        title += " - " + fileName_;

        setWindowTitle(title);
    }

    lastSaveTime_ = fi.lastModified();  // store the time it was last modified/saved


    //    setWindowTitle(tr("%1[*] - %2").arg(shownName).arg(tr("Application")));
}


// ---------------------------------------------------------------------------
// MacroEdit::strippedFileName
// Takes a filename and returns just the filename with no path.
// ---------------------------------------------------------------------------

QString MacroEdit::strippedFileName(const QString& fullFileName)
{
    return QFileInfo(fullFileName).fileName();
}


void MacroEdit::closeEvent(QCloseEvent* event)
{
    if (AskToSave()) {
        writeSettings();
        initFinished = false;
        event->accept();
        eventLoop_.exit();
    }

    else {
        event->ignore();
    }
}


// ---------------------------------------------------------------------------
// MacroEdit::wordUnderCursor
// returns the word currently under the cursor. Specifically, this is used to
// get the name of the function currently under the cursor, and if the cursor
// is on an open bracket, then the word to the left of the cursor is returned.
// THIS IS QUITE LANGUAGE-SPECIFIC, AND MAY NEED TO BE REVISED FOR
// NON-MACRO LANGUAGES.
// ---------------------------------------------------------------------------

QString MacroEdit::wordUnderCursor()
{
    QTextCursor cursor = textEditor->textCursor();  // get the document's cursor
    cursor.select(QTextCursor::WordUnderCursor);    // select the current word
    QString currentWord = cursor.selectedText();


    // if the text is 'pcont(', and the user has selected the 'pcont', then the
    // 'word-under-cursor' will be '(' for some reason; therefore we will go back
    // two words and try again... This won't affect the editor, because we will
    // not send the cursor back to the editor.

    if (currentWord == "(" || currentWord == "()") {
        cursor.movePosition(QTextCursor::PreviousWord, QTextCursor::MoveAnchor, 2);  // move it left two words
        cursor.select(QTextCursor::WordUnderCursor);                                 // select the current word
        currentWord = cursor.selectedText();
    }

    return currentWord;
}


QString MacroEdit::currentFunctionUnderCursor()
{
#if 0
    QStringList t=textEditor->textBeforeCursor(200);

    QRegExp rx("(\\b\\w+)\\(");
    int v = rx.lastIndexIn(t);

    qDebug() << "full" << t;
    qDebug() << "  " << rx.capturedTexts();

    if (v >=1 )
        qDebug() << "  res:" << rx.capturedTexts()[1];
#endif
    return {};
}

// ---------------------------------------------------------------------------
// MacroEdit::AskToSave
// If the document has been modified, then ask the user whether to save it
// or not - if they say 'yes', then save it.
// ---------------------------------------------------------------------------

bool MacroEdit::AskToSave()
{
    if (textEditor->document()->isModified()) {
        auto ret = QMessageBox::warning(this, appName_,
                                        tr("The document has been modified.\n"
                                           "Do you want to save your changes?"),
                                        QMessageBox::Cancel | QMessageBox::No | QMessageBox::Yes,
                                        QMessageBox::Yes);
        if (ret == QMessageBox::Yes)
            return save();
        else if (ret == QMessageBox::Cancel)
            return false;
    }

    return true;
}


// ---------------------------------------------------------------------------
// MacroEdit::save
// triggered when the user asks to save the document. If it currently
// has an assigned filename, then we use that - otherwise as the user.
// - returns true if it happens, false if the user cancels the operation.
// ---------------------------------------------------------------------------

bool MacroEdit::save()
{
    if (fileName_.isEmpty()) {
        return saveAs();
    }
    else {
        return saveTextFile(fileName_);
    }
}


// ---------------------------------------------------------------------------
// MacroEdit::saveAs
// triggered when the user asks to save the document under a new name
// - returns true if it happens, false if the user cancels the operation.
// ---------------------------------------------------------------------------

bool MacroEdit::saveAs()
{
    QString fileName = QFileDialog::getSaveFileName(this);
    if (fileName.isEmpty())
        return false;

    saveTextFile(fileName);
    return true;
}


// ---------------------------------------------------------------------------
// MacroEdit::reload
// triggered when the user asks to reload the document
// - returns true if it happens ok.
// ---------------------------------------------------------------------------

bool MacroEdit::reload()
{
    QTextCursor cursor = textEditor->textCursor();  // note the current text cursor position
    int pos = cursor.position();                    // note the current text cursor position
    loadTextFile(fileName_);                        // reload the file
    cursor.setPosition(pos);                        // restore the cursor position
    textEditor->setTextCursor(cursor);              // send the cursor back to the document

    return true;
}


// ---------------------------------------------------------------------------
// MacroEdit::documentWasModified
// triggered when the document is modified by something (proably the user!)
// ---------------------------------------------------------------------------

void MacroEdit::documentWasModified()
{
    setWindowModified(textEditor->document()->isModified());
}


// ---------------------------------------------------------------------------
// MacroEdit::clipboardDataChanged
// triggered when the clipboard data has been changed
// ---------------------------------------------------------------------------

void MacroEdit::clipboardDataChanged()
{
    actionPaste->setEnabled(!QApplication::clipboard()->text().isEmpty());
}


// ---------------------------------------------------------------------------
// MacroEdit::cursorPositionWasChanged
// triggered when the cursor position is changed
// ---------------------------------------------------------------------------

void MacroEdit::cursorPositionWasChanged()
{
    if (languageHelper_) {
        QString currentWord = wordUnderCursor();

        // actionVerb_Function_Help->setEnabled(!readOnly_ && languageHelper_->isHelpAvailableForFunction(currentWord));
    }


    // remove a highlighted line if there is one

    if (highlightedLine_ != -1)
        highlightLine(-1);
}


// ---------------------------------------------------------------------------
// MacroEdit::findDialog
// triggered when the user tries to activate the Find dialog.
// ---------------------------------------------------------------------------

void MacroEdit::findDialog()
{
    showFindDialog(false);
}

// ---------------------------------------------------------------------------
// MacroEdit::findReplaceDialog
// triggered when the user tries to activate the Replace dialog.
// ---------------------------------------------------------------------------

void MacroEdit::findReplaceDialog()
{
    showFindDialog(true);
}


// ---------------------------------------------------------------------------
// MacroEdit::showFindDialog
// displays the Find/Replace dialog.
// ---------------------------------------------------------------------------

void MacroEdit::showFindDialog(bool showReplaceOptions)
{
    // create the dialog if it does not already exist

    if (!findDialog_) {
        findDialog_ = new FindTextDialog(this);

        connect(findDialog_, SIGNAL(find(const QString&, QTextDocument::FindFlags)),
                this, SLOT(findString(const QString&, QTextDocument::FindFlags)));

        connect(findDialog_, SIGNAL(replace(const QString&, const QString&, QTextDocument::FindFlags)),
                this, SLOT(replaceString(const QString&, const QString&, QTextDocument::FindFlags)));

        connect(findDialog_, SIGNAL(replaceInSelected(const QString&, const QString&, QTextDocument::FindFlags)),
                this, SLOT(replaceStringInSelectedText(const QString&, const QString&, QTextDocument::FindFlags)));

        connect(findDialog_, SIGNAL(replaceAll(const QString&, const QString&, QTextDocument::FindFlags)),
                this, SLOT(replaceStringInFile(const QString&, const QString&, QTextDocument::FindFlags)));

        connect(findDialog_, SIGNAL(rejected()), this, SLOT(findClosed()));

        connect(textEditor, SIGNAL(selectionChanged()), this, SLOT(onSelectionChanged()));

        connect(this, SIGNAL(selectionChanged(bool)), findDialog_, SLOT(onSelectionChanged(bool)));
    }


    // if created, set it up and display it

    if (findDialog_) {
        QString selected("");
        bool isSelection = false;

        // if there is user-selected text, then use that as the search string

        if (textEditor->textCursor().hasSelection()) {
            selected = textEditor->textCursor().selectedText();
            isSelection = true;
        }

        // - but not if it spans multiple lines

        if (selected.contains(QChar::ParagraphSeparator))
            selected = "";


        // set up the dialog box and display it

        findDialog_->setupUIBeforeShow(selected, showReplaceOptions, isSelection);
        findDialog_->show();
        findDialog_->raise();
        findDialog_->activateWindow();
        statusMessage("", 0);  // reset the status message
    }
}


// ---------------------------------------------------------------------------
// MacroEdit::findNext
// triggered when the user re-executes the previous Find command
// ---------------------------------------------------------------------------

void MacroEdit::findNext()
{
    if (!lastFindString_.isEmpty()) {
        findString(lastFindString_, lastFindFlags_);
    }
}


// ---------------------------------------------------------------------------
// MacroEdit::findSelection
// triggered when the user tries to 'find' the currently selected text
// ---------------------------------------------------------------------------

void MacroEdit::findSelection()
{
    // if there is user-selected text, then use that as the search string

    if (textEditor->textCursor().hasSelection()) {
        QString selected = textEditor->textCursor().selectedText();

        findString(selected,
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
                   QTextDocument::FindFlags());
#else
                   0);
#endif
    }
}


// ---------------------------------------------------------------------------
// MacroEdit::findString(...)
// 1) triggered from the Find dialog when the user wants to perform the search
// 2) called from the other 'find' functions. Actually performs the search.
// ---------------------------------------------------------------------------

void MacroEdit::findString(const QString& s, QTextDocument::FindFlags flags, bool replace, const QString& r)
{
    lastFindString_ = s;     // store for repeat searches
    lastFindFlags_ = flags;  // store for repeat searches
    bool found = false;

    if (textEditor->find(s, flags))  // find and select the string - were we successful?
    {
        statusMessage("", 0);
        found = true;
    }
    else  // did not find the string
    {
        if (1)  // 'wraparound' search - on by default, we can add a user option if it might be useful to turn it off
        {
            QTextCursor original_cursor = textEditor->textCursor();  // get the document's cursor
            QTextCursor cursor(original_cursor);

            if (flags & QTextDocument::FindBackward)  // move to the start or end of the document to continue the search
                cursor.movePosition(QTextCursor::End);
            else
                cursor.movePosition(QTextCursor::Start);

            textEditor->setTextCursor(cursor);  // send the cursor back to the document

            if (textEditor->find(s, flags))  // search again, from the new position
            {
                statusMessage("", 0);
                found = true;
            }
            else {
                textEditor->setTextCursor(original_cursor);  // not found - restore the cursor to its original position
            }
        }
    }


    if (found) {
        if (replace) {
            // perform the 'replace'
            textEditor->insertPlainText(r);

            // highlight the replaced text - the current text cursor will be
            // at the end of the replaced text, so we move it back to the start
            // (anchored so that the text is selected)
            QTextCursor cursor = textEditor->textCursor();  // get the document's cursor
            cursor.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor, r.length());
            textEditor->setTextCursor(cursor);  // send the cursor back to the document
        }
    }

    else {
        statusMessage(tr("Searched whole file, string not found"), 5000);
    }
}

// ---------------------------------------------------------------------------
// MacroEdit::replaceString(...)
// triggered from the Find dialog when the user wants to perform the s/r
//   's' is the string to search for, 'r' is the string to replace it with
// ---------------------------------------------------------------------------

void MacroEdit::replaceString(const QString& /*s*/, const QString& r, QTextDocument::FindFlags /*flags*/)
{
    // perform the 'replace'
    textEditor->insertPlainText(r);

    // highlight the replaced text - the current text cursor will be
    // at the end of the replaced text, so we move it back to the start
    // (anchored so that the text is selected)
    QTextCursor cursor = textEditor->textCursor();  // get the document's cursor
    cursor.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor, r.length());
    textEditor->setTextCursor(cursor);  // send the cursor back to the document
}


// ---------------------------------------------------------------------------
// MacroEdit::replaceStringInSelectedText(...)
// triggered from the Find dialog when the user wants to perform the s/r
// on all the selected text
//   's' is the string to search for, 'r' is the string to replace it with
// ---------------------------------------------------------------------------

void MacroEdit::replaceStringInSelectedText(const QString& s, const QString& r, QTextDocument::FindFlags flags)
{
    int numReplacements = 0;

    // create a new (temporary) text document to hold the selected text
    // - we will perform the search/replace on this document, then replace
    // our actual document's selected text with this.

    QPlainTextEdit tempEdit(textEditor->textCursor().selectedText());


    // do the search/replace in the temporary editor

    while (tempEdit.find(s, flags)) {
        // perform the 'replace'
        tempEdit.insertPlainText(r);
        numReplacements++;
    }


    // copy the result back to our actual document - insertText will overwrite
    // the currently selected text, which is what we want. Clever, eh?

    QString tempAsString(tempEdit.toPlainText());
    textEditor->insertPlainText(tempAsString);


    // highlight the replaced text - the current text cursor will be
    // at the end of the replaced text, so we move it back to the start
    // (anchored so that the text is selected)

    QTextCursor cursor = textEditor->textCursor();  // get the document's cursor
    cursor.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor, tempAsString.length());
    textEditor->setTextCursor(cursor);  // send the cursor back to the document


    // tell the user how many instances of the string we replaced

    QString msg = QString(tr("Replaced %1 instances")).arg(numReplacements);
    statusMessage(msg, 5000);
}


// ---------------------------------------------------------------------------
// MacroEdit::replaceStringInFile(...)
// triggered from the Find dialog when the user wants to perform the s/r
// on all the whole file
//   's' is the string to search for, 'r' is the string to replace it with
// ---------------------------------------------------------------------------

void MacroEdit::replaceStringInFile(const QString& s, const QString& r, QTextDocument::FindFlags flags)
{
    // select the whole document, then do a 's/r on selected text'

    QTextCursor cursor = textEditor->textCursor();  // get the document's cursor
    cursor.select(QTextCursor::Document);           // select the whole document
    textEditor->setTextCursor(cursor);              // send the cursor back to the document

    // perform the search/replace on the selected text

    replaceStringInSelectedText(s, r, flags);
}


// ---------------------------------------------------------------------------
// MacroEdit::findClosed
// triggered when the 'find' dialog is closed.
// ---------------------------------------------------------------------------

void MacroEdit::findClosed()
{
    // reset the status message if we put something there

    statusMessage("", 0);
}


// ---------------------------------------------------------------------------
// MacroEdit::gotoLine
// triggered when the user asks to bring up the 'go to line' dialog
// ---------------------------------------------------------------------------

void MacroEdit::gotoLine()
{
    // create the dialog if it does not already exist

    if (!gotoLineDialog_) {
        gotoLineDialog_ = new GotoLineDialog(this);

        connect(gotoLineDialog_, SIGNAL(gotoLine(int)), this, SLOT(gotoLine(int)));
    }


    // if created, set it up and display it

    if (gotoLineDialog_) {
        gotoLineDialog_->show();
        gotoLineDialog_->raise();
        gotoLineDialog_->activateWindow();
        gotoLineDialog_->setupUIBeforeShow();
    }
}

// ---------------------------------------------------------------------------
// MacroEdit::gotoLine
// triggered from the GotoLine dialog when the user wants to go to that line
// ---------------------------------------------------------------------------

void MacroEdit::gotoLine(int line)
{
    int bn = 0;
    QTextBlock b;

    if (line <= textEditor->document()->blockCount()) {
        for (b = textEditor->document()->begin(); b != textEditor->document()->end(); b = b.next()) {
            if (bn == line - 1) {
                QTextCursor cursor = textEditor->textCursor();  // get the document's cursor
                cursor.setPosition(b.position());               // set it to the right position
                cursor.select(QTextCursor::LineUnderCursor);    // select the whole line
                textEditor->setTextCursor(cursor);              // send the cursor back to the document
                break;
            }
            bn++;
        }
    }

    else {
        statusMessage(tr("Line not in document"), 0);
    }
}


// ---------------------------------------------------------------------------
// MacroEdit::onIndentLines
// triggered when the user asks to indent the selected lines of code
// if indentString is not empty, then
// ---------------------------------------------------------------------------

void MacroEdit::onIndentLines()
{
    QTextCursor cursor = textEditor->textCursor();  // get the document's cursor


    // if no line is currently selected, then first select the current line

    if (!textEditor->textCursor().hasSelection()) {
        cursor.select(QTextCursor::LineUnderCursor);
        textEditor->setTextCursor(cursor);  // send the cursor back to the document
    }


    // find the number of lines in the selection

    int numLines = textEditor->numLinesSelected();


    // if the user selected the text from bottom to top, then the cursor will be at the top
    // of the selection - for ease of coding, we will ensure that the cursor is at the bottom
    // of the selection.

    cursor.setPosition(cursor.selectionEnd());


    // if the cursor is at the very start of a line, then we assume that the user does not want
    // to indent this line, and so we move it up one line

    QTextBlock cb = cursor.block();
    int column = (cursor.position() - cb.position()) + 1;

    if (column == 1) {
        cursor.movePosition(QTextCursor::Up, QTextCursor::MoveAnchor);
        numLines--;
    }


    cursor.beginEditBlock();  // this should be seen as a single operation for 'undo' purposes

    for (int i = 0; i < numLines; i++) {
        cursor.movePosition(QTextCursor::StartOfLine, QTextCursor::MoveAnchor);  // move it to the start of the line
        textEditor->setTextCursor(cursor);                                       // send the cursor back to the document
        if (cursor.block().length() > 1)                                         // only indent lines which are not empty (cb.length includes the newline)
            textEditor->insertTextIndent(cursor, false);                         // indent the line (needs document cursor)
        cursor.movePosition(QTextCursor::Up, QTextCursor::MoveAnchor);           // move it to the start of the line
    }

    cursor.movePosition(QTextCursor::Down, QTextCursor::MoveAnchor);  // move it down a line

    cursor.endEditBlock();  // this should be seen as a single operation for 'undo' purposes


    // select the entire blocks again in case the user wants to indent again
    // - the cursor will be on the first line of the block now

    cursor.movePosition(QTextCursor::StartOfLine, QTextCursor::MoveAnchor);         // move the anchor to the start of the line
    cursor.movePosition(QTextCursor::Down, QTextCursor::KeepAnchor, numLines - 1);  // move the cursor to the last line
    cursor.movePosition(QTextCursor::EndOfLine, QTextCursor::KeepAnchor);           // move the cursor to the end of the last line
    textEditor->setTextCursor(cursor);                                              // send the cursor back to the document
}


// ---------------------------------------------------------------------------
// MacroEdit::onCommentLines
// triggered when the user asks to comment out the selected lines of code
// ---------------------------------------------------------------------------

void MacroEdit::onCommentLines()
{
    QString toAddAtEnd = "";


    QTextCursor cursor = textEditor->textCursor();  // get the document's cursor

    cursor.beginEditBlock();  // this should be seen as a single operation for 'undo' purposes


    // if no line is currently selected, then first select the current line

    if (!cursor.hasSelection()) {
        cursor.select(QTextCursor::LineUnderCursor);
        textEditor->setTextCursor(cursor);  // send the cursor back to the document
    }


    // if the user selected the text from bottom to top, then the cursor will be at the top
    // of the selection - for ease of coding, we will ensure that the cursor is at the bottom
    // of the selection. For this, we simply swap the cursor and the anchor
    int anchorPos = cursor.anchor();
    int cursorPos = cursor.position();
    if (anchorPos > cursorPos) {
        cursor.setPosition(cursorPos, QTextCursor::MoveAnchor);
        cursor.setPosition(anchorPos, QTextCursor::KeepAnchor);
    }


    // if the cursor is at the very start of a line, then we assume that the user does not want
    // to indent this line, and so we move it up one line

    QTextBlock cb = cursor.block();
    int column = (cursor.position() - cb.position()) + 1;

    if (column == 1) {
        cursor.movePosition(QTextCursor::Up, QTextCursor::KeepAnchor);         // move the cursor to the end of the previous line
        cursor.movePosition(QTextCursor::EndOfLine, QTextCursor::KeepAnchor);  // move the cursor to the end of the previous line
        toAddAtEnd = "\n";
    }


    // replace the currently-selected text with a commented out version
    // note that cursor.selectedText gives line breaks as unicode paragraph breaks, so we
    // have to replace them

    QString textToReplace = cursor.selectedText().replace(QChar(QChar::ParagraphSeparator), QChar('\n'));
    textEditor->insertPlainText(languageHelper_->commentBlock(textToReplace));  // add the text at the current cursor position

    if (!toAddAtEnd.isEmpty())  // maybe have to add a newline at the end
        textEditor->insertPlainText(toAddAtEnd);

    cursor.endEditBlock();  // this should be seen as a single operation for 'undo' purposes
}


// ---------------------------------------------------------------------------
// MacroEdit::tabsDialog
// triggered when the user asks to bring up the 'tabs' dialog
// ---------------------------------------------------------------------------

void MacroEdit::tabsDialog()
{
    // create the dialog if it does not already exist

    if (!tabsDialog_) {
        tabsDialog_ = new TabsDialog(this);

        connect(tabsDialog_, SIGNAL(tabsSettingsChanged(TabsSettings)), this, SLOT(onTabsSettingsChanged(TabsSettings)));
    }


    // if created, set it up and display it

    if (tabsDialog_) {
        tabsDialog_->show();
        tabsDialog_->raise();
        tabsDialog_->activateWindow();
        tabsDialog_->setupUIBeforeShow(textEditor->tabsSettings());
    }
}

// ---------------------------------------------------------------------------
// MacroEdit::runDialog
// triggered when the user asks to bring up the 'run' dialog
// ---------------------------------------------------------------------------

void MacroEdit::runDialog()
{
    // create the dialog if it does not already exist

    if (!runDialog_) {
        runDialog_ = new RunDialog(this);

        connect(runDialog_, SIGNAL(runOptionsChanged(MacroRunOptions)), this, SLOT(onRunOptionsChanged(MacroRunOptions)));
    }


    // if created, set it up and display it

    if (runDialog_) {
        runDialog_->show();
        runDialog_->raise();
        runDialog_->activateWindow();
        runDialog_->setupUIBeforeShow(runOptions_);
    }
}


// ---------------------------------------------------------------------------
// MacroEdit::onTabsSettingsChanged
// triggered from the Tabs dialog when the user has changed the settings
// ---------------------------------------------------------------------------

void MacroEdit::onTabsSettingsChanged(TabsSettings settings)
{
    // display tabs and spaces?

    textEditor->setDisplayTabsAndSpaces(settings.displayTabsAndSpaces);


    // has the number of spaces per tab changed?

    bool changeFont = (textEditor->numSpacesInTab() != settings.numSpacesInTab);


    // store the new settings

    textEditor->setUseSpacesForTabs(settings.useSpacesForTabs);
    textEditor->setNumSpacesInTab(settings.numSpacesInTab);
    textEditor->setUseSpacesForDrops(settings.useSpacesForDrops);
    textEditor->setDisplayTabsAndSpaces(settings.displayTabsAndSpaces);
    textEditor->setAutoIndent(settings.autoIndent);


    // if the number of spaces per tab has changed, then we need to update the font

    if (changeFont) {
        changeFontSize(0);
    }


    // also, if the user has already instantiated the code template dialog, then
    // we should recreate it in order to reflect the new tab settings

    if (codeTemplateDialog_) {
        codeTemplateDialog_->reloadCodeTemplates(textEditor->tabsSettings());
    }
}


// ---------------------------------------------------------------------------
// MacroEdit::onRunOptionsChanged
// triggered from the Run Options dialog when the user has changed the settings
// ---------------------------------------------------------------------------

void MacroEdit::onRunOptionsChanged(MacroRunOptions options)
{
    // store the new settings

    runOptions_ = options;
}


// ---------------------------------------------------------------------------
// MacroEdit::onSelectionChanged
// triggered when the user changes the text selection
// ---------------------------------------------------------------------------

void MacroEdit::onSelectionChanged()
{
    // if the find/replace dialogue is open, it needs to know about the change

    if (findDialog_) {
        bool isSelection = textEditor->textCursor().hasSelection();
        emit selectionChanged(isSelection);
    }
}


// ---------------------------------------------------------------------------
// MacroEdit::onReadOnlyStatusChanged
// triggered when the read-only status of the current file changes
// (XXX NOTE: this is not yet detected, but this function will be used
// when it is; in the meantime, it can be used in the setup).
// ---------------------------------------------------------------------------
/*
void MacroEdit::onReadOnlyStatusChanged (bool readOnly)
{
    action_Built_in_Function->setEnabled(!readOnly);
    action_List_of_available_functions->setEnabled(!readOnly);
    actionVerb_Function_Help->setEnabled(!readOnly);
    actionCode_Template->setEnabled(!readOnly);
}
*/

// ---------------------------------------------------------------------------
// MacroEdit::codeTemplateDialog
// triggered when the user asks to bring up the 'code template' dialog
// ---------------------------------------------------------------------------

void MacroEdit::codeTemplateDialog()
{
    // no point in even trying if file is read-only

    if (textEditor->isReadOnly()) {
        QMessageBox::warning(this, appName_,
                             tr("File is read-only."));
        return;
    }


    // create the dialog if it does not already exist

    if (!codeTemplateDialog_) {
        // we will give the dialog the path to the file containing the function names

        QString path = languageHelper_->pathToTemplateList();
        codeTemplateDialog_ = new CodeTemplateDialog(path, this);

        connect(codeTemplateDialog_, SIGNAL(insertCodeTemplate(QString)), this, SLOT(onInsertCodeTemplate(QString)));
    }


    // if created, set it up and display it

    if (codeTemplateDialog_) {
        codeTemplateDialog_->reloadCodeTemplates(textEditor->tabsSettings());
        codeTemplateDialog_->show();
        codeTemplateDialog_->raise();
        codeTemplateDialog_->activateWindow();
    }
}


// ---------------------------------------------------------------------------
// MacroEdit::onInsertFunctionName
// triggered from the Function List dialog when the user has selected a function
// ---------------------------------------------------------------------------

void MacroEdit::onInsertFunctionName(QString function)
{
    QString text = function + "()";  // eg: stdev()
    textEditor->insertPlainText(text);

    // move the cursor to inside the brackets
    QTextCursor cursor = textEditor->textCursor();                       // get the document's cursor
    cursor.movePosition(QTextCursor::Left, QTextCursor::MoveAnchor, 1);  // move it left one space
    textEditor->setTextCursor(cursor);                                   // send the cursor back to the document
}


// ---------------------------------------------------------------------------
// MacroEdit::onInsertCodeTemplate
// triggered from the Code Template dialog when the user has selected a template
// ---------------------------------------------------------------------------

void MacroEdit::onInsertCodeTemplate(QString codeTemplate)
{
    textEditor->insertPlainText(codeTemplate);


    // move the cursor to just after the new code
    //    QTextCursor cursor = textEditor->textCursor();   // get the document's cursor
    //    cursor.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, codeTemplate.count());
    //    textEditor->setTextCursor(cursor);               // send the cursor back to the document
}

void MacroEdit::insertText(QString text)
{
    // insert at cursor
    QTextCursor tc = textEditor->textCursor();

    // int lastPosition = tc.position();

    // this should be seen as a single operation for 'undo' purposes
    tc.beginEditBlock();

    // int extra = text.length() - completer_->completionPrefix().length();
    // tc.movePosition(QTextCursor::Left);
    // tc.movePosition(QTextCursor::EndOfWord);
    tc.insertText(text);
    textEditor->setTextCursor(tc);

    // textEditor->insertPlainText(text + postString);
    tc.endEditBlock();

    // int pos = tc.position();
    // tc.setPosition(lastPosition);
    textEditor->setFocus();
}

// ---------------------------------------------------------------------------------
// MacroEdit::onInsertTextIntoFunctionCall
// triggered from dialogs when text is to be inserted into the current function call
// ---------------------------------------------------------------------------------

void MacroEdit::onInsertTextIntoFunctionCall(QString text)
{
    // here we ASSUME that function calls are of the form:
    //    func_name <optional whitespace> (<parameters>)
    // e.g. pcont(contour:'on')
    // but the user may not have yet typed the opening bracket...
    // so we have to search.
    // NOTE that this will have to change when we have this sort of help available
    // for MagML - more of this intelligence will have to go into the LanguageHelper classes.

    QString preString;
    QString postString;


    // first, ensure we are past the function name

    QTextCursor cursor = textEditor->textCursor();  // get the document's cursor

    cursor.beginEditBlock();  // this should be seen as a single operation for 'undo' purposes

    int lastPosition = cursor.position();                                     // store the cursor's current position, because we will want to restore it
    cursor.movePosition(QTextCursor::EndOfWord, QTextCursor::MoveAnchor, 2);  // move it to the end of the word
    textEditor->setTextCursor(cursor);                                        // send the cursor back to the document


    // qDebug() << "char: " << textEditor->characterBehindCursor();

    // if the cursor is now just ahead of an open bracket, then good; otherwise
    // we may be just ahead of a closed bracket e.g. pcont(), and we need to go
    // back a space; otherwise we should insert an open bracket.

    QChar charBehindCursor = textEditor->characterBehindCursor();

    if (charBehindCursor != QChar('(')) {
        if (charBehindCursor == QChar(')'))  // we are just in front of ')'
        {
            // move the cursor back one place
            cursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::MoveAnchor, 1);
            textEditor->setTextCursor(cursor);
        }
        else {
            // no function brackets - we must insert these ourself (and move
            // the cursor between them)
            textEditor->insertPlainText("()");
            cursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::MoveAnchor, 1);
            textEditor->setTextCursor(cursor);
        }
    }


    // check whether the next character is a closed bracket - if it is, then
    // we will not want to put an ending comma and new line

    cursor.movePosition(QTextCursor::EndOfWord, QTextCursor::MoveAnchor, 2);  // move it to the end of the word

    if (textEditor->characterBehindCursor(&cursor) != QChar(')')) {
        postString = ",\n";
    }


    textEditor->insertPlainText(text + postString);


    // restore the cursor's original position

    cursor.setPosition(lastPosition);
    textEditor->setTextCursor(cursor);  // send the cursor back to the document


    cursor.endEditBlock();  // this should be seen as a single operation for 'undo' purposes
}

void MacroEdit::slotFunctionList()
{
    if (languageHelper_->hasVerbFunctionHelp()) {
        codeHelperW->showFunctionList();
    }
}

void MacroEdit::slotCodeHelper()
{
    if (languageHelper_->hasVerbFunctionHelp()) {
        QStringList lst;
        int cursorPos = 0;
        textEditor->textBeforeCursorLine(20, lst, cursorPos);
        codeHelperW->adjustContents(lst, cursorPos);
    }
}

void MacroEdit::slotColourHelper()
{
    if (languageHelper_->hasVerbFunctionHelp()) {
        QStringList lst;
        int cursorPos = 0;
        textEditor->textBeforeCursorLine(1, lst, cursorPos);
        codeHelperW->adjustColourHelper(lst.join(""), cursorPos);
    }
}

// ---------------------------------------------------------------------------
// MacroEdit::insertLicenceText
// triggered from the Insert Licence Text action
// ---------------------------------------------------------------------------

void MacroEdit::insertLicenceText()
{
    if (licenceText_.isEmpty()) {
        std::string licenceTextPath = MakeSystemEtcPath("licence_for_macros.txt");

        QFile file(licenceTextPath.c_str());
        if (file.open(QIODevice::ReadOnly)) {
            // read the file into a string, and replace the year placeholder with the real thing
            int year = QDateTime::currentDateTime().date().year();
            QString yearString = QString::number(year);
            QTextStream ts(&file);
            licenceText_ = ts.readAll();
            licenceText_.replace("[YEAR]", yearString);
        }
        else {
            QMessageBox::warning(this, appName_,
                                 tr("Cannot read file %1:\n%2.")
                                     .arg(licenceTextPath.c_str())
                                     .arg(file.errorString()));
            return;
        }
    }


    // we want to put the text at the top of the file, but if there is a Metview Macro
    // header, then we should put the licence text AFTER that.

    const int numLinesToLookForHeader = 3;
    int line = 0;
    QTextBlock b;
    QTextCursor cursor = textEditor->textCursor();  // get the document's cursor

    cursor.beginEditBlock();  // this should be seen as a single operation for 'undo' purposes

    // search the first few lines

    for (b = textEditor->document()->begin();
         (line < numLinesToLookForHeader) && (b != textEditor->document()->end());
         b = b.next(), line++) {
        cursor.setPosition(b.position());
        QString lineText = cursor.block().text();

        if (languageHelper_->isHeaderLine(lineText)) {
            // move the cursor onto the next line, then insert a newline

            cursor.movePosition(QTextCursor::Down, QTextCursor::MoveAnchor);
            textEditor->setTextCursor(cursor);  // send the cursor back to the document
            textEditor->insertPlainText("\n");  // add the newline
            break;                              // do not need to check the other lines
        }
    }


    // not found? then put the text at the start of the document

    if (line == numLinesToLookForHeader || b == textEditor->document()->end()) {
        cursor.movePosition(QTextCursor::Start);
        textEditor->setTextCursor(cursor);  // send the cursor back to the document

        QString requiredHeader = languageHelper_->requiredHeaderLine();
        if (!requiredHeader.isEmpty()) {
            textEditor->insertPlainText(requiredHeader);
        }
    }

    textEditor->insertPlainText(languageHelper_->commentBlock(licenceText_));  // add the text at the current cursor position
    textEditor->insertPlainText("\n");

    cursor.endEditBlock();  // this should be seen as a single operation for 'undo' purposes
}

// ---------------------------------------------------------------------------
// MacroEdit::run
// triggered when the user clicks 'Run'
// ---------------------------------------------------------------------------

void MacroEdit::run()
{
    runMacro(runOptions_);
}

/*
// ---------------------------------------------------------------------------
// MacroEdit::debug
// triggered when the user clicks 'Debug'
// ---------------------------------------------------------------------------

void MacroEdit::debug(void)
{
    MacroRunOptions options(runOptions_);
    options.trace_ = true;    // 'true' == generate debug output
    runMacro (options);
}
*/

void MacroEdit::stop()
{
    Q_ASSERT(actionRun->isEnabled() == false);

    if (runTask_) {
        runTask_->stop();
    }
}


// ---------------------------------------------------------------------------
// MacroEdit::monitor
// Called when the user clicks on the Process Monitor button
// ---------------------------------------------------------------------------


void MacroEdit::monitor()
{
    MvRequest r;
    MvApplication::callService("MvMonitor", r, nullptr);  // Qt-based monitor
}


// ---------------------------------------------------------------------------
// MacroEdit::external
// called when the user clicks to open the file in an external editor
// ---------------------------------------------------------------------------

void MacroEdit::external()
{
    QString command = getenv("METVIEW_EDITOR");
    QString quote = "\"";

    command += " " + quote + fileName_ + quote;

    QProcess runExternal;
    QString msg;

    if (runExternal.startDetached(command))
        msg = tr("Started ");
    else
        msg = tr("<font color=red>Could not start</font> ");

    statusMessage(msg + "'" + command + "'", 5000);
}

// ---------------------------------------------------------------------------
// MacroEdit::runMacro
// Runs the macro, optionally with debug output.
// The actual running of the macro is performed in a new object, which
// processes any Metview messages passed by the macro.
// ---------------------------------------------------------------------------

void MacroEdit::runMacro(MacroRunOptions options)
{
    // if the document has been modified, then save - currently, we do not
    // ask the user first - should we?

    if (textEditor->document()->isModified()) {
        save();
    }


    // reset the message log

    logMessages->clear();
    logMessages->show();

#ifdef MACROEDIT_USE_LOGTIMER
    // for a macro with lots of output messages, it can be quite inefficient to visually
    // update the message log widget for each and every line of output. Therefore, we
    // set a timer to update the widget at regular intervals. It is also updated when
    // the macro finishes, so for quick macros, the user will not see any difference.

    logMessages->setUpdatesEnabled(false);
    logTimer_.setInterval(500);
    connect(&logTimer_, SIGNAL(timeout()), this, SLOT(updateMessageLog()));
    logTimer_.start();  // start the interval timer we use to update the message log
#endif

    // tell the user that we are starting, and grey out the 'run' buttons
    // and anything else that should not be touched whilst the macro is running

    statusMessage(tr("Program running..."), 0);
    enableRunActions(false);

    time_.start();  // start the timer we use to determine how long the macro took

    // before creating the new task, delete the old one

    if (runTask_)
        delete runTask_;

    // start the macro running via a new task asynchronously

    runTask_ = new MacroRunTask(this, fileName_, options);
    runTask_->start();
}


// ---------------------------------------------------------------------------
// MacroEdit::macroFinished
// Called when the macro has finished
// ---------------------------------------------------------------------------

void MacroEdit::macroFinished(int errorCode)
{
    QString elapsedMsg;

    int ms = time_.elapsed();  // get the elapsed time since the macro was started
    QString finishedTime(tr(" [Finished at ") + QTime::currentTime().toString() + "]");

    double seconds = ms / 1000.0;
    int wholeSeconds = (int)seconds;

    if (wholeSeconds > 0) {
        double minutes = seconds / 60.0;
        int wholeMinutes = (int)minutes;

        if (wholeMinutes > 0) {
            int remainingSeconds = seconds - (wholeMinutes * 60);
            elapsedMsg = QString("%1 m, %2 s").arg(wholeMinutes).arg(remainingSeconds, 2);
        }
        else {
            elapsedMsg = QString("%1 s").arg(seconds, 0, 'g', 4);
        }
    }
    else {
        elapsedMsg = QString("%1 ms").arg(ms, 2);
    }


    if (errorCode) {
        statusMessage(tr("Program finished <font color=red>(FAILED)</font> : ") + elapsedMsg + finishedTime);
        highlightLine(-2);  // set a warning colour on the highlight line
        // editor_.logMessage("FAILED");
        // Task::failure();
    }
    else {
        statusMessage(tr("Program finished <font color=green>(OK)</font> : ") + elapsedMsg + finishedTime);
        highlightLine(-1);  // un-highlight the highlight line
        // editor_.logMessage("RUN OK");
        // Task::success(r);
    }

#ifdef MACROEDIT_USE_LOGTIMER
    updateMessageLog();
    logMessages->setUpdatesEnabled(true);
    logTimer_.stop();
#endif

    enableRunActions(true);

    // wait a little bit before enabling the 'run' buttons
    // - to give the user a little feedback if their macro has run very quickly

    buttonTimer_.setInterval(250);
    connect(&buttonTimer_, SIGNAL(timeout()), this, SLOT(taskFinished()));
    buttonTimer_.start();  // start the interval timer we use to update the message log
}


// ---------------------------------------------------------------------------
// MacroEdit::taskFinished
// Called when the task has finished
// ---------------------------------------------------------------------------

void MacroEdit::taskFinished()
{
    enableRunActions(true);
    buttonTimer_.stop();
}

void MacroEdit::stopAttemptFailed(int pid)
{
    QMessageBox::critical(this, "Code Editor", tr("Could not stop current Macro process (pid=") + QString::number(pid) + ")",
                          QMessageBox::Ok, QMessageBox::Ok);
}

// ---------------------------------------------------------------------------
// MacroEdit::logMessage
// Writes the string to the log area. The string is parsed in case it
// contains an error message and will be printed differently if it does.
// ---------------------------------------------------------------------------

void MacroEdit::logMessage(const std::string& s)
{
    static const QString cr("\n");

    // Check for errors
    if (checkForErrorMessage(s)) {
        // if true, then we'll have an HTML string to add to the log...
        // ... but the message string might have characters that are 'special' in
        // HTML, so we need to do some translation...
        // Note that for some reason we need to add an extra space after closing the <b>
        // directive, because otherwise it does not seem to register that it's been closed
        // and every subsequent string will be displayed in bold...
        QString qs(s.c_str());
        qs.replace("<", "&lt;");
        qs.replace(">", "&gt;");
        // qs =  "<b>" + qs + "</b> ";
        qs = "<font color=\'red\'>" + qs + "</font> ";
        logMessages->appendHtml(qs);
    }

    else {
        // otherwise, we just have plain text
        QString qs(s.c_str());
        logMessages->appendPlainText(qs);
    }
}


// ---------------------------------------------------------------------------
// MacroEdit::stepLineNumber
// Called when the macro steps onto a new line, supplied as the argument
// ---------------------------------------------------------------------------

void MacroEdit::stepLineNumber(const int line)
{
    highlightLine(line);
}


// ---------------------------------------------------------------------------
// MacroEdit::checkForErrorMessage
// Parses the given string to see whether it is an error message or not.
// ---------------------------------------------------------------------------

bool MacroEdit::checkForErrorMessage(const std::string& s)
{
    int len = s.length();

    // Check for errors
    // Error messages are of the form:
    // "Line xx: blah blah"
    // or
    // "Line xx in 'file': blah blah"
    if (
        len > 6 &&
        s[0] == 'L' &&
        s[1] == 'i' &&
        s[2] == 'n' &&
        s[3] == 'e' &&
        s[4] == ' ') {
        int n = 5;
        int m = 0;
        while (n < len && isdigit(s[n])) {
            m *= 10;
            m += s[n] - '0';
            n++;
        }
        if (s[n] == ':' || (s[n++] == ' ' && s[n++] == 'i' && s[n++] == 'n')) {
            gotoLine(m);
            //            s = "<a href='silly'>" + s + "</a>";
            return true;  // we found an error message
        }
    }

    return false;  // no errors
}


// ---------------------------------------------------------------------------
// MacroEdit::updateRowColLabel
// Called when the cursor's position is changed in the editor. The row/col
// indicator is updated with the current coordinates.
// ---------------------------------------------------------------------------

void MacroEdit::updateRowColLabel()
{
    int line = 0, column = 0;

    textEditor->cursorRowCol(&line, &column);

    QString status = QString("L: %1, C: %2")
                         .arg(line)
                         .arg(column);
    rowColLabel_->setText(status);
}


// ---------------------------------------------------------------------------
// MacroEdit::updateMessageLog
// Although messages are (potentially) being added to the log all the time,
// we do not update its display as soon as we can, because this can
// degrade performance. Instead, we periodically update the display - this
// is that function.
// ---------------------------------------------------------------------------

void MacroEdit::updateMessageLog()
{
#ifdef MACROEDIT_USE_LOGTIMER
    //    static int i = 0;
    //    static bool locked = false;

    //    if (locked && !necessary) {logMessages->insertPlainText ("LOCKED!\n"); return;}

    //    locked = true;
    //    QString status = QString("%1").arg(i++);
    //    rowColLabel_->setText(status);
    logMessages->setUpdatesEnabled(true);
    //    logMessages->moveCursor(QTextCursor::End, QTextCursor::MoveAnchor);
    //    logMessages->ensureCursorVisible();
    logMessages->show();
    logMessages->setUpdatesEnabled(false);
//    locked = false;
#endif
}


// ---------------------------------------------------------------------------
// MacroEdit::statusMessage
// Sets the message on the status bar. In theory, we might like to only
// display a message for a set time, but this functionality is currently
// not operational because we can't use the standard function to update
// the status bar because we want to display html text on it and therefore
// have a custom label there instead which cannot have a 'timed' display.
// ---------------------------------------------------------------------------

void MacroEdit::statusMessage(const QString& msg, int)
{
    // second parameter is 'timeToDisplay', but that function is not
    // currently active, so we remove the name to avoid compiler warnings

    //    if (timeToDisplay != 0)
    //        statusBar()->showMessage (msg, timeToDisplay);
    //    else
    messageLabel_->setText(msg);
}


// ---------------------------------------------------------------------------
// MacroEdit::print
// Provides a print dialog and lets the user print the document.
// ---------------------------------------------------------------------------

void MacroEdit::print()
{
    // let the user select a printer

    QPrinter printer;
    auto* dialog = new QPrintDialog(&printer, this);

    dialog->setWindowTitle(tr("Print Document"));


    // give the option to only print the current selection?

    if (textEditor->textCursor().hasSelection())
        dialog->setOption(QAbstractPrintDialog::PrintSelection, true);

    if (dialog->exec() != QDialog::Accepted)
        return;


    // now do the actual printing

    textEditor->document()->print(&printer);
}


// ---------------------------------------------------------------------------
// MacroEdit::check_syntax
// Called when the user clicks the 'Check Syntax' button.
// Functionality is very similar to runMacro(), so see the comments there
// for more information.
// ---------------------------------------------------------------------------

void MacroEdit::check_syntax()
{
    // save the document first?

    if (textEditor->document()->isModified()) {
        save();
    }

    logMessages->clear();
    logMessages->show();

    statusMessage(tr("Checking syntax running..."), 0);
    enableRunActions(false);

    time_.start();  // start the timer we use to determine how long the macro took

    if (runTask_)
        delete runTask_;

    MacroRunOptions options;
    options.runMode_ = "syntax";
    runTask_ = new MacroRunTask(this, fileName_, options);  // start the macro running in a new task object
    runTask_->start();
}


// ---------------------------------------------------------------------------
// MacroEdit::enableRunActions
// Enables (true) or disables (false) the actions which should not be
// possible whilst the macro is running.
// ---------------------------------------------------------------------------

void MacroEdit::enableRunActions(bool enable)
{
    enableActionsForCurrentLanguage(enable);  // takes care of run, debug and syntax checking
}


// ---------------------------------------------------------------------------
// MacroEdit::onFocusRegained
// Called when we regain focus
// ---------------------------------------------------------------------------

void MacroEdit::onFocusRegained()
{
    if (!haveFocus_) {
        QFileInfo fi(fileName_);
        QDateTime dt = fi.lastModified();


        // if the read-only status has changed, then we WILL update the editor

        if (fi.isWritable() == readOnly_)  // ie, if the read-only status is different
        {
            setCurrentFile(fileName_);
        }


        // has the file been modified since we loaded it or saved it?

        if (dt > lastSaveTime_) {
            // yes - ask the user if they want to reload the document from disk
            auto ret = QMessageBox::warning(this, appName_,
                                            tr("The document has been externally modified (%1).\n"
                                               "Do you want to reload from disk?\n")
                                                .arg(dt.toString(QLocale::system().dateTimeFormat(QLocale::ShortFormat))),
                                            QMessageBox::No | QMessageBox::Yes, QMessageBox::Yes);

            if (ret == QMessageBox::Yes) {
                reload();  // reload the file
            }

            else  // user has selected not to reload the file from disk
            {
                setCurrentFile(fileName_, true);  // note the current save time so we don't
                                                  // issue this message until it's changed again


                // to force the editor to save the current macro; otherwise it will run
                // the exernally saved version, which is not the version in the editor
                // and this will cause all sorts of confusion...

                textEditor->document()->setModified(true);
                setWindowModified(true);
            }
        }

        haveFocus_ = true;
    }
}

// ---------------------------------------------------------------------------
// MacroEdit::onFocusRegained
// Called when we lose focus. Note that this might just mean that we are
// displaying a dialog box.
// ---------------------------------------------------------------------------

void MacroEdit::onFocusLost()
{
    // we've only *really* lost focus if none of our widgets have it

    if (QApplication::focusWidget() == nullptr)
        haveFocus_ = false;
}

void MacroEdit::slotChanegFont(bool)
{
    QFontDialog d;
    // d.setCurrentFont(editor_->);
    // d.setOptions(QFontDialog::MonospacedFonts);
}

// ---------------------------------------------------------------------------
// MacroEdit::setFontSize
// Sets the size of the font used in the editor
// ---------------------------------------------------------------------------

void MacroEdit::setFontSize(int newSize)
{
    if (newSize > 2 && newSize < 64) {
        QFont fe = textEditor->font();
        QFont f(fe.family(), newSize);
        f.setFixedPitch(true);
        textEditor->setFont(f);
        logMessages->setFont(f);

        QFontMetrics fm(f);
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
        textEditor->setTabStopDistance(MvQ::textWidth(fm, QLatin1Char(' ')) * textEditor->numSpacesInTab());   // ensure the tab stops (in pixels)
        logMessages->setTabStopDistance(MvQ::textWidth(fm, QLatin1Char(' ')) * textEditor->numSpacesInTab());  // match the equivalent number of spaces
#else
        textEditor->setTabStopWidth(MvQ::textWidth(fm, QLatin1Char(' ')) * textEditor->numSpacesInTab());
        logMessages->setTabStopWidth(MvQ::textWidth(fm, QLatin1Char(' ')) * textEditor->numSpacesInTab());
#endif
        fontSize_ = newSize;

        // Save the settings
        if (initFinished)
            writeSettings();
    }
}


// ---------------------------------------------------------------------------
// MacroEdit::changeFontSize
// Changes the size of the font used in the editor by the specified amount
// ---------------------------------------------------------------------------

void MacroEdit::changeFontSize(int amount)
{
    setFontSize(fontSize_ + amount);
}

// ---------------------------------------------------------------------------
// MacroEdit::enlargeFont
// Increases the size of the font used in the editor
// ---------------------------------------------------------------------------

void MacroEdit::enlargeFont()
{
    changeFontSize(1);
}


// ---------------------------------------------------------------------------
// MacroEdit::shrinkFont
// Reduces the size of the font used in the editor
// ---------------------------------------------------------------------------

void MacroEdit::shrinkFont()
{
    changeFontSize(-1);
}

// ---------------------------------------------------------------------------
// MacroEdit::normalFont
// Sets the size of the font used in the editor to the 'normal' size
// ---------------------------------------------------------------------------

void MacroEdit::normalFont()
{
    setFontSize(11);
}

// ---------------------------------------------------------------------------
// MacroEdit::largeFont
// Sets the size of the font used in the editor to the 'large' size
// (mainly for presentations)
// ---------------------------------------------------------------------------

void MacroEdit::largeFont()
{
    setFontSize(30);
}


// ---------------------------------------------------------------------------
// MacroEdit::showLineNumbers
// Toggles whether we show the line numbers or not
// ---------------------------------------------------------------------------

void MacroEdit::showLineNumbers(bool show)
{
    textEditor->setShowLineNumbers(show);

    // Save the settings
    if (initFinished)
        writeSettings();
}

// ---------------------------------------------------------------------------
// MacroEdit::highlightLine
// Highlights the given line. Supply -1 to remove the highlight.
// ---------------------------------------------------------------------------

void MacroEdit::highlightLine(int line)
{
    int bn = 0;
    QTextBlock b;
    EditorTheme* theme = textEditor->theme();
    Q_ASSERT(theme);

    auto highlightColour = theme->colour("highlight");

    // trivial case - do not re-highlight the same line again
    if (line == highlightedLine_)
        return;

    // special case: -1 means 'un-highlight' the line
    if (line == -1) {
        QList<QTextEdit::ExtraSelection> empty;
        textEditor->setExtraSelections(empty);
        highlightedLine_ = line;
        return;
    }


    // another special case: -2 means 're-highlight the current line in a warning colour'
    if (line == -2) {
        highlightColour = theme->colour("warning");
    }

    else {
        highlightedLine_ = line;
    }

    if (line <= textEditor->document()->blockCount()) {
        for (b = textEditor->document()->begin(); b != textEditor->document()->end(); b = b.next()) {
            if (bn == highlightedLine_ - 1) {
                QTextEdit::ExtraSelection highlight;
                highlight.cursor = textEditor->textCursor();
                highlight.cursor.setPosition(b.position());
                highlight.format.setProperty(QTextFormat::FullWidthSelection, true);
                highlight.format.setBackground(highlightColour);

                QList<QTextEdit::ExtraSelection> extras;
                extras << highlight;
                textEditor->setExtraSelections(extras);
                break;

                // QTextCursor cursor = textEditor->textCursor();   // get the document's cursor
                // cursor.setPosition (b.position());               // set it to the right position
                // cursor.select(QTextCursor::LineUnderCursor);     // select the whole line
                // textEditor->setTextCursor(cursor);               // send the cursor back to the document
                // break;
            }
            bn++;
        }
    }

    else {
        // statusMessage(tr("Line not in document"), 0);
    }
}


// ---------------------------------------------------------------------------
// MacroEdit::languageChosen
// Called when a programming language is chosen. We call setLanguage()
// to use the language corresponding to the given action
// ---------------------------------------------------------------------------

void MacroEdit::languageChosen(QAction* action)
{
    QString language = action->iconText();

    setLanguage(language);
}


// ---------------------------------------------------------------------------
// MacroEdit::setLanguage
// Sets the programming language to specialise the editor for. We use the
// factory to create a new language helper corresponding to the given name
// ---------------------------------------------------------------------------

void MacroEdit::setLanguage(const QString& language)
{
    languageHelper_ = languageHelperFactory_.languageHelper(language);
    Q_ASSERT(languageHelper_);
    if (highlighter_)
        delete highlighter_;

    highlighter_ = languageHelper_->createHighlighter(textEditor->document());
    highlighter_->rehighlight();

    setLanguageMenuCheckedStatus();

    // update the menus for the new language
    enableActionsForCurrentLanguage(true);

    // does this language provide a list of built-in functions or code templates?
    actionCode_Template->setEnabled(!readOnly_ && !languageHelper_->pathToTemplateList().isEmpty());

    // action_List_of_available_functions->setEnabled(!readOnly_);

    // we must call when a language is set
    adjustSideBarForLanguage();

#if 0
    if (verbHelper_) {        
        verbHelper_->setLanguage(languageHelper_);
        if (EditorTheme* theme = EditorTheme::current()) {
            verbHelper_->setCodeEditorTheme(theme, textEditor->font());
        }

        verbHelper_->showFunction("mcont");
    }
#endif
    // tell the editor object what the language is - it needs to know so that it can correctly
    // process icon drops
    textEditor->setLanguage(languageHelper_->className());
    textEditor->setLanguageHelper(languageHelper_);

    // window title
    QString langName = languageHelper_->name();
    // setWindowTitle(tr("Metview ") + langName + tr("Macro Editor [*]"));

    // set the application icon
    QPixmap winPix(MvQ::iconPixmapPath(languageHelper_->iconName()));
    if (!winPix.isNull()) {
        QApplication::setWindowIcon(QIcon(winPix));
    }
    else {
        winPix = QPixmap(MvQ::iconPixmapPath("MACRO"));
        QApplication::setWindowIcon(QIcon(winPix));
    }
}

// ---------------------------------------------------------------------------
// MacroEdit::enableActionsForCurrentLanguage
// Enables the actions which are valid for the current language
// ---------------------------------------------------------------------------

void MacroEdit::enableActionsForCurrentLanguage(bool enable)
{
    actionRun->setEnabled(enable && languageHelper_->canRun());
    actionStop->setEnabled(!enable && languageHelper_->canRun());
    // actionDebug      ->setEnabled (enable && languageHelper_->canDebug());
    actionCheckSyntax->setEnabled(enable && languageHelper_->canCheckSyntax());
    actionRun_Options->setEnabled(enable && languageHelper_->canHaveRunOptions());

    // TODO: change it when the converter is ready for release
    //actionConvertToPython->setEnabled(enable && languageHelper_->isConvertableToPython());
    actionConvertToPython->setEnabled(false);
}


// ---------------------------------------------------------------------------
// MacroEdit::setLanguageMenuCheckedStatus
// Unchecks all the language menu items, then checks the currently selected one
// ---------------------------------------------------------------------------

void MacroEdit::setLanguageMenuCheckedStatus()
{
    // loop through the language menu items
    // - uncheck the ones that are not the current language
    // - check the one that is

    for (auto& languageAction : languageActions_) {
        QString language = languageAction->iconText();
        languageAction->setChecked(languageHelper_->isLanguageName(language));
    }
}


// ---------------------------------------------------------------------------
// MacroEdit::setAutoInsertLicence
// Sets the status of the flag to say whether we want to automatically insert
// the licence text when we open an empty file. Ensures that the menu options
// are correctly set.
// ---------------------------------------------------------------------------

void MacroEdit::setAutoInsertLicence(bool setting)
{
    autoInsertLicence_ = setting;

    if (autoInsertLicence_) {
        if (!actionAutoInsertLicenceOn->isChecked())
            actionAutoInsertLicenceOn->setChecked(true);
    }
    else {
        if (!actionAutoInsertLicenceOff->isChecked())
            actionAutoInsertLicenceOff->setChecked(true);
    }
}


// ---------------------------------------------------------------------------
// MacroEdit::autoInsertLicence
// If set, automatically insert the licence text if this is an empty file.
// ---------------------------------------------------------------------------

void MacroEdit::autoInsertLicence()
{
    if (autoInsertLicence_ && languageHelper_->isAutoLicenceTextAllowed()) {
        if (textEditor->document()->isEmpty()) {
            insertLicenceText();
            statusMessage(tr("Licence text automatically added. See the Settings menu to change this behaviour."), 2000);
        }
    }
}

// ---------------------------------------------------------------------------
// MacroEdit::autoInsertLanguageHeader
// If set, automatically insert a header for the current language.
// ---------------------------------------------------------------------------

void MacroEdit::autoInsertLanguageHeader()
{
    QString header(languageHelper_->autoHeaderText());
    if (!header.isEmpty()) {
        if (textEditor->document()->isEmpty()) {
            textEditor->insertPlainText(header);
        }
    }
}

void MacroEdit::slotSelectTheme(QAction* a)
{
    setTheme(a->data().toString());
}

void MacroEdit::setTheme(QString name)
{
    if (EditorTheme* theme = EditorTheme::setCurrent(name)) {
        textEditor->setTheme(theme);
        logMessages->setTheme(theme);
        codeHelperW->setCodeEditorTheme(theme, textEditor->font());

        QFont fe = textEditor->font();
        QFont f(fe.family(), fe.pointSize());
        f.setFixedPitch(true);
        logMessages->setFont(f);

        if (languageHelper_) {
            if (highlighter_)
                delete highlighter_;

            highlighter_ = languageHelper_->createHighlighter(textEditor->document());
            highlighter_->rehighlight();
        }

        // Adjust the menu
        foreach (QAction* a, themeAg_->actions()) {
            if (a->data().toString() == name) {
                a->setChecked(true);
                break;
            }
        }

        // Save the settings
        if (initFinished)
            writeSettings();
    }
}

void MacroEdit::slotShowAboutBox()
{
    MvQAbout about("CodeEditor", "", MvQAbout::MetviewVersion);
    about.exec();
}

void MacroEdit::convertToPython()
{
    // TODO: if we call waitservice to execute the conversion then while the
    // popup is shown the application hangs. To overcome this problem
    // we directly call the converter via system.
    std::string outFileName;

    MacroToPythonConvert converter;
    int ret = converter.convert(fileName_.toStdString(), false, outFileName);

    if (ret == 0) {
        std::string t = "Conversion successful! The geneated Python code was saved into file: " +
                outFileName;
        MvLog().popup().info() << t;
    } else {
        MvLog().popup().errNoExit() <<
            "Failed to generate Python script!" <<
            MvLog::detailsBegin() << "Error: " << converter.errText()  <<
                                "\n\nOutput:" << converter.outText();
    }
}

// ---------------------------------------------------------------------------
// MacroEdit::writeSettings
// Saves the editor's settings (such as font size, window size)
// ---------------------------------------------------------------------------

void MacroEdit::writeSettings()
{
    QSettings settings(MVQ_QSETTINGS_DEFAULT_ARGS, "MacroEditor");
    settings.clear();

    settings.beginGroup("mainWindow");
    settings.setValue("geometry", saveGeometry());
    settings.setValue("state", saveState());
    settings.setValue("sidebarSplitter_v2", sidebarSplitter->saveState());
    settings.setValue("splitter_v2", splitter->saveState());
    settings.setValue("fontSize", fontSize_);
    settings.setValue("useSpacesForTabs", textEditor->useSpacesForTabs());
    settings.setValue("numSpacesInTab", textEditor->numSpacesInTab());
    settings.setValue("useSpacesForDrops", textEditor->useSpacesForDrops());
    settings.setValue("displayTabsAndSpaces", textEditor->displayTabsAndSpaces());
    settings.setValue("autoIndent", textEditor->autoIndent());
    settings.setValue("showLineNumbers", textEditor->showLineNumbers());
    settings.setValue("autoInsertLicence", autoInsertLicence_);
    settings.setValue("logPanel", actionLogPanel->isChecked());

    if (EditorTheme* t = EditorTheme::current())
        settings.setValue("theme", t->name());

    settings.endGroup();

    codeHelperW->writeSettings();
}


// ---------------------------------------------------------------------------
// MacroEdit::readSettings
// Reads the editor's settings (such as font size, window size)
// ---------------------------------------------------------------------------

void MacroEdit::readSettings()
{
    QSettings settings(MVQ_QSETTINGS_DEFAULT_ARGS, "MacroEditor");

    int newFontSize, numSpacesInTab;
    bool useSpacesForTabs, useSpacesForDrops, displayTabsAndSpaces, autoIndent, visLineNumbers,
        autoInsertLicence;

    settings.beginGroup("mainWindow");
    restoreGeometry(settings.value("geometry").toByteArray());
    restoreState(settings.value("state").toByteArray());

    // Init sidebarSplitter
    if (settings.contains("sidebarSplitter_v2"))
        sidebarSplitter->restoreState(settings.value("sidebarSplitter_v2").toByteArray());
    else {
        Q_ASSERT(sidebarSplitter->count() == 2);
        if (sidebarSplitter->count() == 2) {
            int w = size().width();
            QList<int> sLst = sidebarSplitter->sizes();
            if (w > 500) {
                sLst[1] = 220;
                sLst[0] = w - 220;
            }
            else {
                sLst[1] = w / 3;
                sLst[0] = w - w / 3;
            }
            sidebarSplitter->setSizes(sLst);
        }
    }

    // Init splitter (it is within sidebarSplitter
    if (settings.contains("splitter_v2")) {
        splitter->restoreState(settings.value("splitter_v2").toByteArray());
    }

    newFontSize = settings.value("fontSize").toInt();
    useSpacesForTabs = settings.value("useSpacesForTabs", true).toBool();
    numSpacesInTab = settings.value("numSpacesInTab", 4).toInt();
    useSpacesForDrops = settings.value("useSpacesForDrops", true).toBool();
    displayTabsAndSpaces = settings.value("displayTabsAndSpaces", false).toBool();
    autoIndent = settings.value("autoIndent", true).toBool();
    visLineNumbers = settings.value("showLineNumbers", true).toBool();
    autoInsertLicence = settings.value("autoInsertLicence", false).toBool();

    actionLogPanel->setChecked(settings.value("logPanel", false).toBool());

    QString theme = settings.value("theme").toString();

    settings.endGroup();

    codeHelperW->readSettings();

    textEditor->setUseSpacesForTabs(useSpacesForTabs);
    textEditor->setNumSpacesInTab(numSpacesInTab);
    textEditor->setUseSpacesForDrops(useSpacesForDrops);
    textEditor->setDisplayTabsAndSpaces(displayTabsAndSpaces);
    textEditor->setAutoIndent(autoIndent);
    actionShow_Line_Numbers->setChecked(visLineNumbers);
    setAutoInsertLicence(autoInsertLicence);
    setFontSize(newFontSize);

    if (!theme.isEmpty())
        setTheme(theme);
    else
        setTheme("metview");
}
