/***************************** 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 <string>

#include <QDebug>
#include <QFile>
#include <QTextStream>

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

#include <QRegularExpression>
#include <QRegularExpressionMatch>

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

//#define MacroHelper_DEBUG_

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

class MacroHighlighter : public Highlighter
{
public:
    MacroHighlighter(QString& pathToFunctionList, QString& pathToMacroDeprecatedFunctionList, QTextDocument* parent = nullptr);
    ~MacroHighlighter() override = default;

private:
    void LoadKeywordsFromFile(QString& fileName, QTextCharFormat& format);

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


MacroHighlighter::MacroHighlighter(QString& pathToFunctionList, QString& pathToMacroDeprecatedFunctionList, QTextDocument* parent) :
    Highlighter(parent)
{
    HighlightingRule rule;

    /*
        From NEdit:

        InlineString:"string inline":"end inline"::MvString1::\n\
        Comment:"#":"$"::MvComment::\n\
        DoubleQuoteString:"""":""""::MvString1::\n\
        SingleQuoteString:"'":"'"::MvString1::\n\
        Keywords:"<(and|by|export|for|function|global|extern|import|in|include|inline|nil|not|object|of|on|or|otherwise|repeat|task|tell|to|until|when|return|if|then|else|switch|case|while|end|do|loop|extern)>":::MvKeyword::\n\
        BuiltinFunctions:"<(|&|\\*|\\+|-|^|abs|accumulate|acos|addmonths|annotation|any|append|area|arguments|ascii|asin|atan|atan2|attributes|average|average_ew|average_ns|averageview|bar|bitmap|budget|bufr_timeseries|carousel|cleanfile|color|colour|colour_polar_field|colour_vector_field|compute|convolution|corr_a|cos|coslat|count|covar|covar_a|create_geo|cross_sect|curve|curveview|datacov_data|datacoverage|datainfo|date|day|definition|describe|det|device|dictionary|dimensions|direction|distance|distribution|divrot|divwind|dow|drawing_priority|dumpgrib|duplicate|ecfs|edit_map|emptyview|eps_metgram_coach|epsmetgram|epsplus|eqpott_m|eqpott_p|exist|exp|fail|fetch|fieldmean_timeseries|fieldpoint_timeseries|file|filter|find|float|frequencies|generate|geo_to_grib|geo_to_matrix|geopoint_timeseries|geosort|getenv|getksec1|getksec2|getksec3|getksec4|getrsec2|getrsec3|getrsec4|gfind|graph|grib_get_double|grib_get_long|grib_get_string|grib_set_double|grib_set_long|grib_to_geo|grib_to_vis5d|gribsetbits|gridvals|hhmm|hhmmss|hour|hovmoeller_area|hovmoeller_expand|hovmoeller_height|hovmoeller_line|hovmoellerview|icon|importview|in|input|inputwindow|int|intbits|integer|integrate|interpolate|inverse|juldate|julday|latitude|legendentry|length|level|line|list|log|log10|longitude|lookup|lowercase|macro_edit|magml|map_definition|mapview|mask|matrix|max|maxvalue|mean|mean_ew|menu|merge|metgram|metgram0|metgram_coach|metplus|min|minute|minvalue|month|name|nearest_gridpoint|nearest_gridpoint_info|nice|nobitmap|now|obsfilter|odb|offset|option_menu|output|overlay_control|page|parse|paxis|pcoast|pcont|percentile|pgraph|pimage|pimport|plot_page|plot_subpage|plot_superpage|plotwindow|pm_bufr_tephi|pm_grib_tephi|pmimage|pobs|polar_field|polar_vector|pott_m|pott_p|pre1995gribtephi|precision|pressure|print|print_menu|printdef|psymb|ptephi|ptext|putenv|putksec1|putksec2|putksec3|putksec4|putrsec2|putrsec3|putrsec4|pwind|random|read|read_g2|reduce|relhum|remove_duplicates|remove_missing_values|repeat|reprojection|retrieve|retrieve_g2|rmask|rms|round|satelliteview|scores|search|second|seqpott_m|seqpott_p|set_date|set_latitude|set_level|set_longitude|set_time|set_value|set_value2|setcurrent|sgn|shell|sin|sinlat|sleep|slider|sort|sort_and_indices|sort_indices|spec_contour|spec_graph|sqrt|stage|stations|stdev|stdev_a|stop|stopwatch_laptime|stopwatch_reset|stopwatch_start|stopwatch_stop|store|streamfn|subpage|subpagedetails|subsample|substring|sum|superpage|tan|tephigramview|textview|thickness|timer|tmpfile|tmpname|togrib|track|trajectory|transpose|type|unipressure|unithickness|univertint|uppercase|value|value2|values|var|var_a|variables|vector|vector_field|velpot|vert_prof|vertint|vertprofview|vis5d|vis5d_object|vis5d_settings|vis5d_trajectory|vis5d_wind_object|vis5d_window|webaccess|write|xs_average|xsectview|xy_vector|year|yymmdd|yyyymmdd|class|dialog|plot|runmode|number|string|mod|neg|setoutput|getoutput|newpage)>":::MvBuiltinFuns::\n\
        Numeric:"<((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)?>":::MVNumericConst::\n\
        MacroLib:"<mml_\\w*>":::MvMacroLib::\n\
*/


    // 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);
//    rule.pattern.setPatternOptions(QRegularExpression::CaseInsensitiveOption);
#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)";
    theme->setFormat("number", numberFormat);
#if QT_VERSION >= QT_VERSION_CHECK(5, 5, 0)
    rule.pattern = QRegularExpression(pattern);
#else
    rule.pattern = QRegExp(pattern);
#endif
    rule.format = numberFormat;
    highlightingRules.append(rule);

    // Built in functions - these are stored in a text file, so we read it in and parse it
    // builtinFunctionFormat.setFontWeight(QFont::Bold);
    theme->setFormat("builtin_function", builtinFunctionFormat);

    LoadKeywordsFromFile(pathToFunctionList, builtinFunctionFormat);


    // 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
    deprecatedFunctionFormat = builtinFunctionFormat;
    // deprecatedFunctionFormat.setForeground(builtinFunctionFormat.foreground().color().darker(115));
    // deprecatedFunctionFormat.setUnderlineStyle(QTextCharFormat::SpellCheckUnderline	);
    deprecatedFunctionFormat.setToolTip(tr("Deprecated function"));  // the tool top seems not to work...

    theme->setFormat("deprec_function", deprecatedFunctionFormat);

    LoadKeywordsFromFile(pathToMacroDeprecatedFunctionList, deprecatedFunctionFormat);


    // Keywords
    // keywordFormat.setForeground(Qt::darkBlue);
    // keywordFormat.setFontWeight(QFont::Bold);

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

    QStringList keywords;

    keywords << "and"
             << "by"
             << "export"
             << "for"
             << "function"
             << "global"
             << "extern"
             << "import"
             << "in"
             << "include"
             << "inline"
             << "nil"
             << "not"
             << "object"
             << "of"
             << "\\bon\\b"
             << "\\bor\\b"
             << "otherwise"
             << "repeat"
             << "task"
             << "tell"
             << "to"
             << "until"
             << "when"
             << "return"
             << "if"
             << "then"
             << "else"
             << "switch"
             << "case"
             << "while"
             << "end"
             << "do"
             << "loop"
             << "extern";

    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"(("[^"]*")|('[^']*'))";
#if QT_VERSION >= QT_VERSION_CHECK(5, 5, 0)
    rule.pattern = QRegularExpression(pattern);
#else
    rule.pattern = QRegExp(pattern);
#endif
    theme->setFormat("string", quotationFormat);
    rule.format = quotationFormat;
    highlightingRules.append(rule);

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

// ---------------------------------------------------------------------------
// MacroHighlighter::LoadKeywordsFromFile
// Loads a set of keywords from a given file, assuming one word per line.
// These are added to a new highlighting rule with the given char format.
// ---------------------------------------------------------------------------

void MacroHighlighter::LoadKeywordsFromFile(QString& fileName, QTextCharFormat& format)
{
    QFile file(fileName);

    if (file.open(QFile::ReadOnly | QFile::Text))  // try to open in read-only mode
    {
        HighlightingRule rule;
        QTextStream in(&file);

        std::string s;
        while (!file.atEnd()) {
            QString s = file.readLine();
            s = "\\b" + s.trimmed() + "\\b";  // note: the Qt docs say that trimmed() should not be necessary, but it is!
#if QT_VERSION >= QT_VERSION_CHECK(5, 5, 0)
            rule.pattern = QRegularExpression(s);
#else
            rule.pattern = QRegExp(s);
#endif
            rule.format = format;
            highlightingRules.append(rule);
        }
    }

    else {
        /* QMessageBox::warning(this, tr("Application"),
                             tr("Cannot read file %1:\n%2.")
                             .arg(fileName)
                             .arg(file.errorString())); */
    }
}


// -----------------------------------------------------------------------
// -------------------------- MacroHelper code ---------------------------
// -----------------------------------------------------------------------


// ---------------------------------------------------------------------------
// MacroHelper::MacroHelper
// constructor
// ---------------------------------------------------------------------------

MacroHelper::MacroHelper()
{
    languageName_ = "Macro";
    className_ = "MACRO";
    serviceName_ = "macro";
    iconName_ = "MACRO";

    QString etcDir = getenv("METVIEW_DIR_SHARE");

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

    pathToMacroFunctionList_ = etcDir + "FunctionList.txt";
    pathToMacroDeprecatedFunctionList_ = etcDir + "macro_deprecated_functions.txt";
    pathToMacroTemplateList_ = etcDir + "macro_templates.txt";
}


// ---------------------------------------------------------------------------
// MacroHelper::~MacroHelper
// destructor
// ---------------------------------------------------------------------------

MacroHelper::~MacroHelper() = default;


// ---------------------------------------------------------------------------
// MacroHelper::createHighlighter
// creates a new syntax highlighter object
// ---------------------------------------------------------------------------

Highlighter* MacroHelper::createHighlighter(QTextDocument* parent)
{
    return new MacroHighlighter(pathToMacroFunctionList_, pathToMacroDeprecatedFunctionList_, parent);
}

#if 0
// ---------------------------------------------------------------------------
// MacroHelper::isHelpAvailableForFunction
// returns a bool depending on whether the given string is the name of a
// function for which we have some help available
// ---------------------------------------------------------------------------

bool MacroHelper::isHelpAvailableForFunction(QString function)
{
    return VerbFunctionHelp::isHelpAvailableForFunction(function);
}
#endif

// ---------------------------------------------------------------------------
// MacroHelper::pathToFunctionList
// returns the path to where our list of macro functions is. This is generated
// in the constructor.
// ---------------------------------------------------------------------------

QString MacroHelper::pathToFunctionList()
{
    return pathToMacroFunctionList_;
}


// ---------------------------------------------------------------------------
// MacroHelper::pathToTemplateList
// returns the path to where our list of macro templates is. This is generated
// in the constructor.
// ---------------------------------------------------------------------------

QString MacroHelper::pathToTemplateList()
{
    return pathToMacroTemplateList_;
}


// ---------------------------------------------------------------------------
// MacroHelper::isHeaderLine
// decides whether the given line is a Macro header line
// ---------------------------------------------------------------------------

bool MacroHelper::isHeaderLine(const QString& text)
{
    return (text.contains("#") &&
            text.contains("Metview") &&
            text.contains("Macro"));
}

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

// determine in what function and icon parameter the text editor cursor is.
// Argument "lines" contains the chunck of text preceding (and including) the cursor line.
void MacroHelper::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?])?([A-z|0-9|_]+)\()");
        rx.setMinimal(true);

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

#ifdef MacroHelper_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 MacroHelper_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];
#ifdef MacroHelper_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];
                    paramName = whatParam(lines[0], cursorPosInLine);
#ifdef MacroHelper_DEBUG_
                    qDebug() << "    function" << functionName;
                    qDebug() << "    param" << paramName;
#endif
                    return;
                }

                endPos = capPos[j];
            }
        }
    }
}


QString MacroHelper::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 MacroHelper::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 MacroHelper_DEBUG_
            qDebug() << " cap:" << std::endl;
#endif
            // the cursor must be inside the captured text
            if (cursorPosInLine >= capPos && cursorPosInLine <= capPos + capText.length()) {
                colName = capText;
                colName.remove('\'').remove('\"');
#ifdef MacroHelper_DEBUG_
                qDebug() << "  --> col:" << colName;
#endif
                return;
            }

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