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

 Copyright 2019 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 <QDebug>

#include "LanguageHelper.h"
#include "PythonHelper.h"
#include "Highlighter.h"
#include "EditorTheme.h"

#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
#include <QtCore5Compat/QRegExp>
#else
#include <QRegExp>
#endif

#include <QRegularExpression>
#include <QRegularExpressionMatch>

#define PYTHONHELPER_DEBUG_

// -----------------------------------------------------------------------
// ------------------- Syntax highlighter code ---------------------------
//        this class is used by the PythonHelper object, defined below
// -----------------------------------------------------------------------


class PythonHighlighter : public Highlighter
{
public:
    PythonHighlighter(QTextDocument* parent = nullptr);
    ~PythonHighlighter() override = default;

protected:
    QTextCharFormat keywordFormat;
    QTextCharFormat classFormat;
    QTextCharFormat singleLineCommentFormat;
    QTextCharFormat quotationFormat;
    QTextCharFormat functionFormat;
    QTextCharFormat numberFormat;
    QTextCharFormat builtinFunctionFormat;
    QTextCharFormat deprecatedFunctionFormat;
};


PythonHighlighter::PythonHighlighter(QTextDocument* parent) :
    Highlighter(parent)
{
    HighlightingRule rule;

    // Note that the order in which these highlighting rules are given
    // DOES matter! The rules seem to be applied in the order in which
    // they are given.

    EditorTheme* theme = EditorTheme::current();
    if (!theme)
        return;

    // Functions - user defined
    QString pattern = "\\b[A-Za-z0-9_]+(?=\\()";
#if QT_VERSION >= QT_VERSION_CHECK(5, 5, 0)
    rule.pattern = QRegularExpression(pattern);
#else
    rule.pattern = QRegExp(pattern);
#endif
    theme->setFormat("function", functionFormat);
    rule.format = functionFormat;
    highlightingRules.append(rule);


    // Numerics
    pattern = R"(\b((0(x|X)[0-9a-fA-F]*)|(([0-9]+\.?[0-9]*)|(\.[0-9]+))((e|E)(\+|-)?[0-9]+)?)(L|l|UL|ul|u|U|F|f)?\b)";
#if QT_VERSION >= QT_VERSION_CHECK(5, 5, 0)
    rule.pattern = QRegularExpression(pattern);
#else
    rule.pattern = QRegExp("#[^\n]*");
#endif
    theme->setFormat("number", numberFormat);
    rule.format = numberFormat;
    highlightingRules.append(rule);

    // Built in functions
    theme->setFormat("builtin_function", builtinFunctionFormat);

    // the list of builtins was generated from Python as follows:
    // import builtins
    // print(dir(builtins))
    // we added "assert" manually
    QStringList builtinFunctionLst;
    builtinFunctionLst << "assert";
    builtinFunctionLst << "abs"
                       << "absolute_import"
                       << "all"
                       << "any"
                       << "apply"
                       << "ascii"
                       << "basestring"
                       << "bin"
                       << "bool"
                       << "buffer"
                       << "bytearray"
                       << "bytes"
                       << "callable"
                       << "chr"
                       << "classmethod"
                       << "cmp"
                       << "coerce"
                       << "compile"
                       << "complex"
                       << "copyright"
                       << "credits"
                       << "delattr"
                       << "dict"
                       << "dir"
                       << "divmod"
                       << "enumerate"
                       << "eval"
                       << "execfile"
                       << "exit"
                       << "file"
                       << "filter"
                       << "float"
                       << "format"
                       << "frozenset"
                       << "getattr"
                       << "globals"
                       << "hasattr"
                       << "hash"
                       << "help"
                       << "hex"
                       << "id"
                       << "input"
                       << "int"
                       << "intern"
                       << "isinstance"
                       << "issubclass"
                       << "iter"
                       << "len"
                       << "license"
                       << "list"
                       << "locals"
                       << "long"
                       << "map"
                       << "max"
                       << "memoryview"
                       << "min"
                       << "next"
                       << "object"
                       << "oct"
                       << "open"
                       << "ord"
                       << "pow"
                       << "print"
                       << "property"
                       << "quit"
                       << "range"
                       << "raw_input"
                       << "reduce"
                       << "reload"
                       << "repr"
                       << "reversed"
                       << "round"
                       << "set"
                       << "setattr"
                       << "slice"
                       << "sorted"
                       << "staticmethod"
                       << "str"
                       << "sum"
                       << "super"
                       << "sys"
                       << "tuple"
                       << "type"
                       << "unichr"
                       << "unicode"
                       << "vars"
                       << "xrange"
                       << "zip";

    foreach (QString word, builtinFunctionLst) {
        pattern = "\\b" + word + "\\b";
#if QT_VERSION >= QT_VERSION_CHECK(5, 5, 0)
        rule.pattern = QRegularExpression(pattern);
#else
        rule.pattern = QRegExp(pattern);
#endif
        rule.format = builtinFunctionFormat;
        highlightingRules.append(rule);
    }

    // Deprecated functions - these are stored in a text file, so we read it in and parse it
    // - the font will be a bit darker than that for 'normal' functions, and also underlined
    // TODO: implement it!!!
    deprecatedFunctionFormat = builtinFunctionFormat;
    deprecatedFunctionFormat.setToolTip(tr("Deprecated function"));  // the tool top seems not to work...
    theme->setFormat("deprec_function", deprecatedFunctionFormat);


    // Keywords
    theme->setFormat("keyword", keywordFormat);

    // the list of keywords was generated from Python as follows:
    // import keyword
    // print(keyword.kwlist)
    // we: removed "print" and "assert"!!!

    QStringList keywords;

    keywords << "and"
             << "as"
             << "break"
             << "class"
             << "continue"
             << "def"
             << "del"
             << "elif"
             << "else"
             << "except"
             << "exec"
             << "finally"
             << "for"
             << "from"
             << "global"
             << "if"
             << "import"
             << "in"
             << "is"
             << "lambda"
             << "not"
             << "or"
             << "pass"
             << "raise"
             << "return"
             << "try"
             << "while"
             << "with"
             << "yield"
             << "ArithmeticError"
             << "AssertionError"
             << "AttributeError"
             << "BaseException"
             << "BufferError"
             << "BytesWarning"
             << "DeprecationWarning"
             << "EOFError"
             << "Ellipsis"
             << "EnvironmentError"
             << "Exception"
             << "False"
             << "FloatingPointError"
             << "FutureWarning"
             << "GeneratorExit"
             << "IOError"
             << "ImportError"
             << "ImportWarning"
             << "IndentationError"
             << "IndexError"
             << "KeyError"
             << "KeyboardInterrupt"
             << "LookupError"
             << "MemoryError"
             << "NameError"
             << "None"
             << "NotImplemented"
             << "NotImplementedError"
             << "OSError"
             << "OverflowError"
             << "PendingDeprecationWarning"
             << "ReferenceError"
             << "RuntimeError"
             << "RuntimeWarning"
             << "StandardError"
             << "StopIteration"
             << "SyntaxError"
             << "SyntaxWarning"
             << "SystemError"
             << "SystemExit"
             << "TabError"
             << "True"
             << "TypeError"
             << "UnboundLocalError"
             << "UnicodeDecodeError"
             << "UnicodeEncodeError"
             << "UnicodeError"
             << "UnicodeTranslateError"
             << "UnicodeWarning"
             << "UserWarning"
             << "ValueError"
             << "Warning"
             << "ZeroDivisionError"
             << "__builtins__"
             << "__doc__"
             << "__file__"
             << "__future_module__"
             << "__name__"
             << "__package__"
             << "__path__";

    foreach (QString word, keywords) {
        pattern = "\\b" + word + "\\b";
#if QT_VERSION >= QT_VERSION_CHECK(5, 5, 0)
        rule.pattern = QRegularExpression(pattern);
#else
        rule.pattern = QRegExp(pattern);
#endif
        rule.format = keywordFormat;
        highlightingRules.append(rule);
    }

    // strings
    // quotationFormat.setForeground(Qt::darkRed);
    pattern = R"(("[^"]*")|('[^']*'))";
    theme->setFormat("string", quotationFormat);
#if QT_VERSION >= QT_VERSION_CHECK(5, 5, 0)
    rule.pattern = QRegularExpression(pattern);
#else
    rule.pattern = QRegExp(pattern);
#endif
    rule.format = quotationFormat;
    highlightingRules.append(rule);


    // Comments
    pattern = "#[^\n]*";
#if QT_VERSION >= QT_VERSION_CHECK(5, 5, 0)
    rule.pattern = QRegularExpression(pattern);
#else
    rule.pattern = QRegExp(pattern);
#endif
    theme->setFormat("comment", singleLineCommentFormat);
    rule.format = singleLineCommentFormat;
    highlightingRules.append(rule);
}

// -----------------------------------------------------------------------
// -------------------------- PythonHelper code ---------------------------
// -----------------------------------------------------------------------


// ---------------------------------------------------------------------------
// PythonHelper::PythonHelper
// constructor
// ---------------------------------------------------------------------------

PythonHelper::PythonHelper()
{
    languageName_ = "Python";
    className_ = "PYTHON";
    serviceName_ = R"((x=`dirname "%s"`;cd "$x" ; python3 "%s"))";
    iconName_ = "PYTHON";


    QString etcDir = getenv("METVIEW_DIR_SHARE");

    if (!etcDir.isEmpty())
        etcDir += "/etc/";

    pathToMetviewFunctionList_ = etcDir + "FunctionList.txt";
}


// ---------------------------------------------------------------------------
// PythonHelper::~PythonHelper
// destructor
// ---------------------------------------------------------------------------

PythonHelper::~PythonHelper() = default;


// ---------------------------------------------------------------------------
// PythonHelper::~createHighlighter
// creates a new syntax highlighter object
// ---------------------------------------------------------------------------

Highlighter* PythonHelper::createHighlighter(QTextDocument* parent)
{
    return new PythonHighlighter(parent);
}


// ---------------------------------------------------------------------------
// PythonHelper::isHeaderLine
// decides whether the given line is a Python script header line
// ---------------------------------------------------------------------------

bool PythonHelper::isHeaderLine(const QString& text)
{
    return (text.contains("#") &&
            text.contains("python"));
}

// ---------------------------------------------------------------------------
// PythonHelper::autoHeaderText
// returns the line(s) of text to add to an empty file when opened in editor
// ---------------------------------------------------------------------------

QString PythonHelper::autoHeaderText()
{
    return "import metview as mv\n\n";
}
#if 0
bool PythonHelper::isHelpAvailableForFunction(QString function)
{
    return VerbFunctionHelp::isHelpAvailableForFunction(function);
}
#endif

QString PythonHelper::pathToFunctionList()
{
    return pathToMetviewFunctionList_;
}

QChar PythonHelper::functionParameterAssignChar() const
{
    static QChar ch('=');
    return ch;
}

// determine what function and what parameter is the text editor cursor is
// Argument t contains the chunck of text preceding the cursor.
void PythonHelper::whatFunction(QStringList lines, int cursorPosInLine, QString& functionName, QString& paramName) const
{
    static QChar leftBracket('(');
    static QChar rightBracket(')');

    int sum = 0;

    // lines are in reverse order
    for (int i = 0; i < lines.count(); i++) {
        QString t = lines[i];

        // skip comment lines
        if (t.simplified().startsWith("#"))
            continue;

        // find function calls with regexp
        QStringList capText;
        QList<int> capPos;
        int pos = 0;
        QRegExp rx(R"((?:[=?|\:?|\s?])?(mv\.[A-z|0-9|_]+)\()");
        rx.setMinimal(true);
#ifdef PYTHONHELPER_DEBUG_
        qDebug() << "line:" << t;
#endif

        while ((pos = rx.indexIn(t, pos)) != -1) {
            capText << rx.cap(1);
            capPos << pos;
            pos += rx.matchedLength();
        }
#ifdef PYTHONHELPER_DEBUG_
        qDebug() << "  caps:" << capText;
#endif
        int endPos = t.count();

        // no match - check for '(' and ')' chars
        if (capText.count() == 0) {
            for (int k = 0; k < endPos && k < cursorPosInLine; k++) {
                if (t[k] == leftBracket)
                    sum++;
                else if (t[k] == rightBracket)
                    sum--;
            }
            // match
        }
        else {
            // check captured text in reverse order
            for (int j = capText.count() - 1; j >= 0; j--) {
#ifdef PYTHONHELPER_DEBUG_
                qDebug() << "  check:" << capText[j];
#endif
                // check for '(' and ')' chars after the function call
                // till the start of the previous function call

                // if the cursor is in the captured text
                if (i == 0 && cursorPosInLine >= capPos[j] &&
                    cursorPosInLine <= capPos[j] + capText[j].length()) {
                    functionName = capText[j].mid(3);
#ifdef PYTHONHELPER_DEBUG_
                    qDebug() << "    function" << functionName;
#endif
                    return;
                }

                if (i == 0) {
                    for (int k = capPos[j] + capText[j].length(); k < endPos && k < cursorPosInLine; k++) {
                        if (t[k] == leftBracket)
                            sum++;
                        else if (t[k] == rightBracket)
                            sum--;
                    }
                }
                else {
                    for (int k = capPos[j] + capText[j].length(); k < endPos; k++) {
                        if (t[k] == leftBracket)
                            sum++;
                        else if (t[k] == rightBracket)
                            sum--;
                    }
                }

                // we are within a function
                if (sum > 0) {
                    functionName = capText[j].mid(3);
                    paramName = whatParam(lines[0], cursorPosInLine);
#ifdef PYTHONHELPER_DEBUG_
                    qDebug() << "    function" << functionName;
                    qDebug() << "    param" << paramName;
#endif
                    return;
                }

                endPos = capPos[j];
            }
        }
    }
}

QString PythonHelper::whatParam(QString line, int cursorPosInLine) const
{
    // find param within an icon function with regexp
    QRegExp rx("([A-z|0-9|_]+)\\s*\\=");

    int prevPos = -1;
    QString prevText;

    // we use forward check
    int pos = 0;
    while ((pos = rx.indexIn(line, pos)) != -1) {
        QString text = rx.cap(1);

        // the cursor is in the captured text
        if (cursorPosInLine >= pos &&
            cursorPosInLine <= pos + text.length()) {
            return text;
        }

        // the captured text starts after the cursor postion
        if (pos > cursorPosInLine)
            break;

        prevPos = pos;
        prevText = text;
        pos += rx.matchedLength();
    }

    // the cursor after the previous captured text
    if (prevPos >= 0 && cursorPosInLine > prevPos + prevText.length()) {
        return prevText;
    }

    return {};
}

void PythonHelper::whatColour(QString t, int cursorPosInLine, QString& colName) const
{
    // skip comment lines
    if (t.simplified().startsWith("#"))
        return;

    QList<QRegExp> rxLst;
    rxLst << QRegExp(R"((rgb\(\d*\.?\d*,\d*\.?\d*,\d*\.?\d*\)))");
    rxLst << QRegExp(R"((rgba\(\d*\.?\d*,\d*\.?\d*,\d*\.?\d*\)))");

    foreach (QRegExp rx, rxLst) {
        rx.setCaseSensitivity(Qt::CaseInsensitive);
        rx.setMinimal(true);
        int pos = 0;

        while ((pos = rx.indexIn(t, pos)) != -1) {
            QString capText = rx.cap(1);
            int capPos = pos;
#ifdef PYTHONHELPER_DEBUG_
            qDebug() << " cap:";
#endif
            // the cursor must be inside the captured text
            if (cursorPosInLine >= capPos && cursorPosInLine <= capPos + capText.length()) {
                colName = capText;
                colName.remove('\'').remove('\"');
#ifdef PYTHONHELPER_DEBUG_
                qDebug() << "  --> col:" << colName;
#endif
                return;
            }

            pos += rx.matchedLength();
        }
    }
}
