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


/////////////////////////////////// class MvPlainTextEdit ////////////////////////////////

#include "mvplaintextedit.h"

#include <QAbstractItemView>
#include <QDebug>
#include <QFile>
#include <QFontDatabase>
#include <QMouseEvent>
#include <QPainter>
#include <QRegularExpression>
#include <QScrollBar>
#include <QTextBlock>
#include <QHBoxLayout>

#include "MvQDragDrop.h"
#include "MvQMethods.h"
#include "EditorTheme.h"
#include "LanguageHelper.h"
#include "Path.h"

MvPlainTextEdit::MvPlainTextEdit(QWidget* parent) :
    QPlainTextEdit(parent),
    languageHelper_(nullptr)
{
    setUseSpacesForTabs(false);
    setNumSpacesInTab(4);
    setUseSpacesForDrops(false);
    showLineNumbers_ = true;

    lineNumberArea = new LineNumberArea(this);

    connect(this, SIGNAL(blockCountChanged(int)), this,
            SLOT(updateLineNumberAreaWidth(int)));
    connect(this, SIGNAL(updateRequest(QRect, int)), this,
            SLOT(updateLineNumberArea(QRect, int)));
    connect(this, SIGNAL(cursorPositionChanged()),
            lineNumberArea, SLOT(update()));

    updateLineNumberAreaWidth(-1);

    setAcceptDrops(true);
}


MvPlainTextEdit::~MvPlainTextEdit() = default;


// ---------------------------------------------------------------------------
// MvPlainTextEdit::keyPressEvent
// triggered each time the user presses a key. We want to intercept
// certain keystrokes.
// ---------------------------------------------------------------------------

void MvPlainTextEdit::keyPressEvent(QKeyEvent* e)
{
    int key = e->key();

    if (key == Qt::Key_Tab) {
        QTextCursor cursor = textCursor();
        insertTextIndent(cursor, true);
        return;
    }

    else if ((key == Qt::Key_Return || key == Qt::Key_Enter) && autoIndent()) {
        autoIndentNewLine();
    }

    else {
        // we are not interested in other keypresses - they should be handled normally
        QPlainTextEdit::keyPressEvent(e);
    }
}

// ---------------------------------------------------------------------------
// MvPlainTextEdit::insertTextIndent
// inserts appropriate tab or spaces at the given cursor location
// if 'intelligentIndent' is true, then if spaces are used, only enough
// to supply padding will be inserted; otherwise, the full number will
// 'blindly' be added.
// ---------------------------------------------------------------------------

void MvPlainTextEdit::insertTextIndent(QTextCursor& cursor, bool /*intelligentIndent*/)
{
    if (useSpacesForTabs()) {
        // we want to insert enough spaces to bring us to the next 'tab stop'
        // e.g. if tab_spaces == 4 and we are at column 1, then we add 3 spaces

        int row = 0, col = 0;
        cursorRowCol(&row, &col);
        int spaces = numSpacesInTab() - ((col - 1) % numSpacesInTab());

        // create a string with 'n' spaces and insert it
        QString spacesString(spaces, ' ');
        cursor.insertText(spacesString);
    }
    else {
        insertPlainText("\t");  // just insert a tab
    }
}

// ---------------------------------------------------------------------------
// MvPlainTextEdit::cursorRowCol
// returns the row and column position of the cursor
//  - note that the first row and column are (1,1)
// ---------------------------------------------------------------------------

void MvPlainTextEdit::cursorRowCol(int* row, int* col)
{
    const QTextCursor cursor = textCursor();

    QTextBlock cb, b;
    int column = 0, line = 1;
    cb = cursor.block();
    column = (cursor.position() - cb.position()) + 1;

    // find the line number - is there a better way than this?

    for (b = document()->begin(); b != document()->end(); b = b.next()) {
        if (b == cb)
            break;
        line++;
    }

    *row = line;
    *col = column;
}

// ---------------------------------------------------------------------------
// MvPlainTextEdit::characterBehindCursor
// returns the character to the left of the text cursor
// ---------------------------------------------------------------------------

QChar MvPlainTextEdit::characterBehindCursor(QTextCursor* cursor)
{
    QTextCursor docTextCursor = textCursor();
    QTextCursor* theCursor = (cursor == nullptr) ? &docTextCursor : cursor;
    return document()->characterAt(theCursor->position() - 1);
}

// ---------------------------------------------------------------------------
// MvPlainTextEdit::numLinesSelected
// returns the number of lines in the current selection
// yes - all this code to do that!
// ---------------------------------------------------------------------------

int MvPlainTextEdit::numLinesSelected()
{
    QTextCursor cursor = textCursor();  // get the document's cursor
    int selStart = cursor.selectionStart();
    int selEnd = cursor.selectionEnd();
    QTextBlock bStart = document()->findBlock(selStart);
    QTextBlock bEnd = document()->findBlock(selEnd);
    int lineStart = bStart.firstLineNumber();
    int lineEnd = bEnd.firstLineNumber();
    int numLines = (lineEnd - lineStart) + 1;

    return numLines;
}

// ---------------------------------------------------------------------------
// MvPlainTextEdit::autoIndentNewLine
// when the user presses RETURN, we want to create a new line with an indent
// equal to that in the line above.
// ---------------------------------------------------------------------------

void MvPlainTextEdit::autoIndentNewLine()
{
    int i = 0;
    QTextCursor cursor = textCursor();
    QString indent("\n");  // start with a newline

    // get the current line as a string (we have not yet moved to a new line)

    QTextBlock block = cursor.block();
    QString lineText = block.text();
    int cursorColumn = (cursor.position() - block.position()) + 1;

    // keep moving to the right until we hit either the end of the line or a non-whitespace character
    // (or we get to the point where the cursor is)

    for (i = 0; i < lineText.length(); i++) {
        if (!lineText.at(i).isSpace() || lineText.at(i) == QChar('\n') || (i == cursorColumn - 1))
            break;

        indent += lineText.at(i);  // add this character to the indent string
    }


    // insert the text into the document

    cursor.insertText(indent);


    // ensure the cursor has not gone off the bottom of the screen

    ensureCursorVisible();
}

void MvPlainTextEdit::setDisplayTabsAndSpaces(bool show)
{
    QTextDocument* doc = document();
    QTextOption opt = doc->defaultTextOption();

    if (show)
        opt.setFlags(opt.flags() | QTextOption::ShowTabsAndSpaces);
    else
        opt.setFlags(opt.flags() & ~QTextOption::ShowTabsAndSpaces);

    doc->setDefaultTextOption(opt);
    tabsSettings_.displayTabsAndSpaces = show;
}

int MvPlainTextEdit::totalLineNumber() const
{
    return qMax(1, blockCount());
}

// ---------------------------------------------------------------------------
// MvPlainTextEdit::updateLineNumberAreaWidth
// called when the number of lines in the document changes. The argument is
// the new number of lines (blocks).
// ---------------------------------------------------------------------------

void MvPlainTextEdit::updateLineNumberAreaWidth(int)
{
    lineNumberArea->updateWidth();
    if (showLineNumbers_)
        setViewportMargins(lineNumberArea->width(), 0, 0, 0);
    else
        setViewportMargins(0, 0, 0, 0);
}

void MvPlainTextEdit::updateLineNumberArea(const QRect& rect, int dy)
{
    if (dy)
        lineNumberArea->scroll(0, dy);
    else
        lineNumberArea->update(0, rect.y(), lineNumberArea->width(), rect.height());

    if (rect.contains(viewport()->rect()))
        updateLineNumberAreaWidth(-1);

    // lineNumberArea_->update();
}

void MvPlainTextEdit::setShowLineNumbers(bool yes)
{
    showLineNumbers_ = yes;
    lineNumberArea->setVisible(yes);
    updateLineNumberAreaWidth(-1);
}


#if 0
// ---------------------------------------------------------------------------
// MvPlainTextEdit::updateLineNumberArea
// called when the editor is updated. We want to ensure that the line number
// widget stays in sync with it.
// ---------------------------------------------------------------------------

void MvPlainTextEdit::updateLineNumberArea(const QRect &rect, int dy)
{
    if (dy)
        lineNumberArea->scroll(0, dy);
    else
        lineNumberArea->update(0, rect.y(), lineNumberArea->width(), rect.height());

    if (rect.contains(viewport()->rect()))
        updateLineNumberAreaWidth(0);
}
#endif


// ---------------------------------------------------------------------------
// MvPlainTextEdit::resizeEvent
// called when a resize event is triggered. Reset the size of the line widget.
// ---------------------------------------------------------------------------

void MvPlainTextEdit::resizeEvent(QResizeEvent* e)
{
    QPlainTextEdit::resizeEvent(e);

    QRect cr = contentsRect();
    lineNumberArea->setGeometry(QRect(cr.left(), cr.top(), lineNumberArea->width(), cr.height()));
}

// ---------------------------------------------------------------------------
// MvPlainTextEdit::focusInEvent
// called when the widget gains input focus
// ---------------------------------------------------------------------------

void MvPlainTextEdit::focusInEvent(QFocusEvent* event)
{
    emit focusRegained();
    QPlainTextEdit::focusInEvent(event);
}

// ---------------------------------------------------------------------------
// MvPlainTextEdit::focusOutEvent
// called when the widget loses input focus
// ---------------------------------------------------------------------------

void MvPlainTextEdit::focusOutEvent(QFocusEvent* event)
{
    emit focusLost();
    QPlainTextEdit::focusOutEvent(event);
}

void MvPlainTextEdit::changeEvent(QEvent* e)
{
    QPlainTextEdit::changeEvent(e);
    if (e->type() == QEvent::FontChange) {
        lineNumberArea->setFont(font());
        updateLineNumberAreaWidth(-1);
    }
}

void MvPlainTextEdit::mouseDoubleClickEvent(QMouseEvent* e)
{
    // we try to handle the double-click selection for colour here!!
    if (languageHelper_ && languageHelper_->hasVerbFunctionHelp()) {
        QTextCursor cursor = textCursor();
        QString t = cursor.block().text();

        if (!t.contains("RGB(", Qt::CaseInsensitive) && !t.contains("RGBA(", Qt::CaseInsensitive)) {
            return QPlainTextEdit::mouseDoubleClickEvent(e);
        }

        int cursorPosInLine = cursor.positionInBlock();
        int startPos = 0;
        int endPos = t.size();

        QRegularExpression rx(R"((RGB\(.+\)|RGBA\(.+\)))");
        rx.setPatternOptions(QRegularExpression::CaseInsensitiveOption);
        int pos = 0;
        bool found = false;

        auto rmatch = rx.match(t, pos);
        while (rmatch.hasMatch()) {
            QString capText = rmatch.captured(1);
            int capPos = rmatch.capturedStart(1);

            // the cursor must be inside the captured text
            if (cursorPosInLine >= capPos && cursorPosInLine <= capPos + capText.length()) {
                startPos = capPos;
                endPos = capPos + capText.length();
                found = true;
                break;
            }
            pos += rmatch.capturedLength(1);
            rmatch = rx.match(t, pos);
        }

        if (found) {
            int dPos = cursor.position() - cursorPosInLine;
            cursor.setPosition(startPos + dPos);
            cursor.setPosition(endPos + dPos, QTextCursor::KeepAnchor);
            setTextCursor(cursor);
            e->accept();
        }
        else {
            QPlainTextEdit::mouseDoubleClickEvent(e);
        }
    }
    else {
        QPlainTextEdit::mouseDoubleClickEvent(e);
    }
}

// ---------------------------------------------------------------------------
// MvPlainTextEdit::lineNumberAreaPaintEvent
// called when the line number widget needs to be repainted. This is where we
// actually draw the numbers on the widget.
// ---------------------------------------------------------------------------

void MvPlainTextEdit::lineNumberAreaPaintEvent(QPaintEvent* event)
{
    int currentRow = 0, currentCol = 0;
    cursorRowCol(&currentRow, &currentCol);  // get the current line number so we can highlight it

    QPainter painter(lineNumberArea);
    painter.fillRect(event->rect(), lineNumberArea->bgColour());

    QTextBlock block = firstVisibleBlock();
    int blockNumber = block.blockNumber();
    int top = (int)blockBoundingGeometry(block).translated(contentOffset()).top();
    int bottom = top + (int)blockBoundingRect(block).height();

    painter.setPen(lineNumberArea->textColour());

    QFont f = font();  // the editor font

    QFont fontNormal(f.family(), f.pointSize());  // the font to use for most line numbers
    fontNormal.setFixedPitch(true);

    QFont fontBold(f.family(), f.pointSize());  // the font to use for the current line number
    fontBold.setFixedPitch(true);
    fontBold.setBold(true);

    painter.setFont(fontNormal);

    while (block.isValid() && top <= event->rect().bottom()) {
        if (block.isVisible() && bottom >= event->rect().top()) {
            QString number = QString::number(blockNumber + 1);

            if (blockNumber == currentRow - 1)  // is this the current line?
            {
                painter.setFont(fontBold);
                painter.fillRect(0, top, lineNumberArea->width(), fontMetrics().height(),
                                 lineNumberArea->selectBgColour());
            }

            // draw the line number
            painter.drawText(0, top, lineNumberArea->width() - lineNumberArea->rightMargin(),
                             fontMetrics().height(),
                             Qt::AlignRight | Qt::AlignVCenter, number);

            if (blockNumber == currentRow - 1)  // is this the current line?
            {
                painter.setFont(fontNormal);  // reset the font to normal
            }
        }

        block = block.next();
        top = bottom;
        bottom = top + (int)blockBoundingRect(block).height();
        ++blockNumber;
    }
}

//===========================
// Drop from the new Desktop
//===========================

void MvPlainTextEdit::dragEnterEvent(QDragEnterEvent* event)
{
    if ((event->proposedAction() == Qt::CopyAction ||
         event->proposedAction() == Qt::MoveAction)) {
        // move the text cursor to show where the text will be inserted
        QPoint p(MvQ::eventPos(event));
        QTextCursor tempCursor(cursorForPosition(p));
        setTextCursor(tempCursor);
        // setFocus(Qt::MouseFocusReason);
        event->accept();
    }
}

void MvPlainTextEdit::dragMoveEvent(QDragMoveEvent* event)
{
    if ((event->proposedAction() == Qt::CopyAction ||
         event->proposedAction() == Qt::MoveAction)) {
        // move the text cursor to show where the text will be inserted
        QPoint p(MvQ::eventPos(event));
        QTextCursor tempCursor(cursorForPosition(p));
        setTextCursor(tempCursor);
        event->accept();
    }
}

void MvPlainTextEdit::dropEvent(QDropEvent* event)
{
    MvQDrop drop(event);

    if (drop.hasData()) {
        // convert icon information to a Macro language text
        QString numSpaces = "";  // default, meaning 'use tabs, not spaces'
        if (useSpacesForTabs() && useSpacesForDrops() && numSpacesInTab())
            numSpaces = QString::number(numSpacesInTab());  // use spaces, not tabs

        // we know where the drop occurred, so get a new text cursor at that location
        // so that the generated text will be inserted there
        QPoint p(MvQ::eventPos(event));
        QTextCursor tempCursor(cursorForPosition(p));
        setTextCursor(tempCursor);


        // get the right language name
        QString lang = (language() == "PYTHON") ? "python" : "macro";

        for (int i = 0; i < drop.iconNum(); i++) {
            // create a temporary file
            Path temp(::marstmp());
            QString outpath(temp.str().c_str());

            /*QString st = "valgrind --tool=memcheck --leak-check=full -v --track-origins=yes --log-file=/var/tmp/dummy-user/PERFORCE/development/metview/log.txt $METVIEW_BIN/mvimportDesktop \"" + drop.iconPath(i) + "\" " + outpath + " \"" + drop.iconClass(i) + "\" " + numSpaces;*/

            QString st = "$METVIEW_BIN/mvimportDesktop \"" + drop.iconPath(i) + "\" " + outpath + " \"" + drop.iconClass(i) + "\" \"" + numSpaces + "\" " + lang;

            system(st.toUtf8().constData());

            // qDebug() << st;

            // insert text to the Macro editor
            QFile file(temp.str().c_str());
            if (file.open(QIODevice::ReadOnly | QIODevice::Text))
                insertPlainText(file.readAll());

            // delete temporary file
            temp.remove();
        }

        event->accept();
        return;
    }

    event->ignore();
    return;
}

// Set the colour scheme
void MvPlainTextEdit::setTheme(EditorTheme* theme)
{
    Q_ASSERT(theme);

    theme_ = theme;

    QColor col = theme->colour("background");
    QString styleStr;
    if (col.isValid()) {
        styleStr = "background: " + col.name() + ";";
    }
    col = theme->colour("text");
    if (col.isValid()) {
        styleStr += "color: " + col.name() + ";";
    }

    // qDebug() << "style" << styleStr;

    if (!styleStr.isEmpty())
        setStyleSheet("QPlainTextEdit:focus{" + styleStr +
                      "; border: none;} \
         QPlainTextEdit:!focus{" +
                      styleStr + "selection-color: black; selection-background-color: rgb(180,204,234);}");


    // QString family = theme->value("font");
    QFont f = MvQ::findMonospaceFont();
    f.setPointSize(font().pointSize());
    setFont(f);

    lineNumberArea->setTheme(theme);
}

QString MvPlainTextEdit::textUnderCursor() const
{
    QTextCursor tc = textCursor();
    tc.select(QTextCursor::WordUnderCursor);
    return tc.selectedText();
}

QStringList MvPlainTextEdit::textBeforeCursor(int maxLineNum) const
{
    Q_ASSERT(maxLineNum > 0);

    QTextCursor cursor = textCursor();
    QTextBlock block = cursor.block();
    QStringList res;
    QString t = block.text();
    t = t.left(cursor.positionInBlock());
    res << t;

    block = block.previous();
    while (block.isValid()) {
        res << block.text();

        if (res.count() >= maxLineNum)
            return res;

        block = block.previous();
    }

    return res;
}

void MvPlainTextEdit::textBeforeCursorLine(int maxLineNum, QStringList& lines, int& cursorPos) const
{
    Q_ASSERT(maxLineNum > 0);
    lines.clear();

    QTextCursor cursor = textCursor();
    cursorPos = cursor.positionInBlock();

    QString sel = cursor.selectedText();
    if (!sel.isEmpty() && cursor.position() == cursor.selectionEnd()) {
        cursorPos--;
    }

    // we suppose that one block is one line
    QTextBlock block = cursor.block();
    while (block.isValid()) {
        lines << block.text();

        if (lines.count() >= maxLineNum)
            return;

        block = block.previous();
    }
}

void MvPlainTextEdit::setLanguageHelper(LanguageHelper* languageHelper)
{
    languageHelper_ = languageHelper;
}

//=======================================
// LineNumberArea
//=======================================

LineNumberArea::LineNumberArea(MvPlainTextEdit* editor) :
    QWidget(editor),
    textEditor_(editor),
    digits_(3),
    leftMargin_(3),
    rightMargin_(3),
    bgCol_(QColor(240, 240, 240)),
    selectBgCol_(QColor(212, 212, 255)),
    textCol_(QColor(0, 0, 255))
{
}

QSize LineNumberArea::sizeHint() const
{
    return {computedWidth(textEditor_->totalLineNumber()), 0};
}

void LineNumberArea::setTheme(EditorTheme* theme)
{
    if (theme) {
        QColor col = theme->colour("lineNumBg");
        bgCol_ = col;

        col = theme->colour("lineNumSelectBg");
        selectBgCol_ = col;

        col = theme->colour("lineNumBorder");
        borderCol_ = col;

        col = theme->colour("lineNumText");
        textCol_ = col;
    }
}

// ---------------------------------------------------------------------------
// MvPlainTextEdit::lineNumberAreaWidth
// returns the required width to display the line numbers. This adapts to the
// maximum number of digits we need to display.
// ---------------------------------------------------------------------------

void LineNumberArea::updateWidth()
{
    setFixedWidth(computedWidth(textEditor_->totalLineNumber()));
}

int LineNumberArea::computedWidth(int maxLineNum) const
{
    int maxDigits = 1;
    int mx = maxLineNum;
    while (mx >= 10) {
        mx /= 10;
        ++maxDigits;
    }

    if (maxDigits > digits_)
        digits_ = maxDigits;

    int oneW = MvQ::textWidth(textEditor_->fontMetrics(), QLatin1Char('9'));
    rightMargin_ = oneW / 3;

    return leftMargin_ + oneW * maxDigits + rightMargin_;
}
