/*
    SPDX-FileCopyrightText: 2003-2008 Cies Breijs <cies AT kde DOT nl>

    SPDX-License-Identifier: GPL-2.0-or-later
*/

#ifndef _EDITOR_H_
#define _EDITOR_H_

#include <cmath>

#include <QAbstractTextDocumentLayout>
#include <QFrame>
#include <QPainter>
#include <QScrollBar>
#include <QTextEdit>

#include <KFindDialog>

#include "highlighter.h"
#include "interpreter/token.h"
#include "interpreter/tokenizer.h"
#include "interpreter/treenode.h"

class QHBoxLayout;

static const QColor LINE_HIGHLIGHT_COLOR(239, 247, 255);
static const QColor WORD_HIGHLIGHT_COLOR(255, 255, 156);
static const QColor ERROR_HIGHLIGHT_COLOR(255, 200, 200);

static const int EXTRA_SATURATION = 30; // used for drawing the highlighted background
static const int EDITOR_MARGIN = 2; // some margin that can't be set to zero, yet painters should know it
static const int CURSOR_RECT_MARGIN = 5; // another margin that cannot be traced
static const int LINENUMBER_SPACING = 2; // sets the margin for the line numbers

const QString KTURTLE_MAGIC_1_0 = QStringLiteral("kturtle-script-v1.0");

// BEGIN LineNumbers class

class LineNumbers : public QWidget
{
    Q_OBJECT

public:
    LineNumbers(QWidget *parent, QTextEdit *te)
        : QWidget(parent)
        , editor(te)
        , maxWidth(0)
    {
    }
    ~LineNumbers() override
    {
    }

    void setFont(const QFont &f)
    {
        QWidget::setFont(f);
    }

    void setWidth(int w)
    {
        if (w == maxWidth)
            return; // save some cpu cycles
        maxWidth = w;
        QString s;
        for (; w > 0; w--)
            s += QLatin1Char('0');
        setFixedWidth(fontMetrics().boundingRect(s).width() + 2 * LINENUMBER_SPACING);
    }

    void paintEvent(QPaintEvent *) override
    {
        QAbstractTextDocumentLayout *layout = editor->document()->documentLayout();
        int contentsY = editor->verticalScrollBar()->value();
        qreal pageBottom = contentsY + editor->viewport()->height();
        const QFontMetrics fm = fontMetrics();
        const int ascent = fontMetrics().ascent() + 1; // height = ascent + descent + 1
        int lineCount = 1;
        QPainter painter(this);
        for (QTextBlock block = editor->document()->begin(); block.isValid(); block = block.next(), ++lineCount) {
            const QRectF boundingRect = layout->blockBoundingRect(block);
            QPointF position = boundingRect.topLeft();
            if (position.y() + boundingRect.height() < contentsY)
                continue;
            if (position.y() > pageBottom)
                break;
            const QString txt = QString::number(lineCount);
            painter.drawText(width() - fm.boundingRect(txt).width() - LINENUMBER_SPACING, qRound(position.y()) - contentsY + ascent, txt);
        }
        painter.end();
    }

private:
    QTextEdit *editor;
    int maxWidth;
};

// END class LineNumbers

// BEGIN QTextEdit sub-class

class TextEdit : public QTextEdit
{
    Q_OBJECT

public:
    explicit TextEdit(QWidget *parent = nullptr)
        : QTextEdit(parent)
    {
    }

    void markCurrentWord(int startRow, int startCol, int endRow, int endCol)
    {
        currentWord.setCoords(startRow, startCol, endRow, endCol);
        viewport()->update();
    }

    void removeCurrentWordMark()
    {
        currentWord = QRect();
        viewport()->update();
    }

    void markCurrentError(int startRow, int startCol, int endRow, int endCol)
    {
        currentError.setCoords(startRow, startCol, endRow, endCol);
        viewport()->update();
    }

    void removeCurrentErrorMark()
    {
        currentError = QRect();
        viewport()->update();
    }

    void highlightCurrentLine()
    {
        viewport()->update();
    }

    QRect currentLineRect()
    {
        // this method is also used for highlighting the background of the numbers
        QTextCursor cursor = textCursor();
        cursor.movePosition(QTextCursor::StartOfBlock);
        QRect rect = cursorRect(cursor);
        cursor.movePosition(QTextCursor::EndOfBlock);
        rect |= cursorRect(cursor); // get the bounding rectangle of both rects
        rect.setX(0);
        rect.setWidth(viewport()->width());
        return rect;
    }

protected:
    void paintEvent(QPaintEvent *event) override
    {
        QPainter painter(viewport());
        painter.fillRect(currentLineRect(), QBrush(LINE_HIGHLIGHT_COLOR));
        if (!currentWord.isNull()) {
            const auto coordsToRectsList{coordsToRects(currentWord)};
            for (const QRect &rect : coordsToRectsList)
                painter.fillRect(rect, QBrush(WORD_HIGHLIGHT_COLOR));
        }
        if (!currentError.isNull()) {
            const auto coordsToRectsLst{coordsToRects(currentError)};
            for (const QRect &rect : coordsToRectsLst)
                painter.fillRect(rect, QBrush(ERROR_HIGHLIGHT_COLOR));
        }
        painter.end();
        QTextEdit::paintEvent(event);
    }

private:
    QList<QRect> coordsToRects(QRect coords)
    {
        // this methods calculate the viewport rectangles that cover a (multi-line) word or error
        // after switching the tokenizer to use the QTextDocument we might optimize this methods
        int startRow, startCol, endRow, endCol;
        coords.getCoords(&startRow, &startCol, &endRow, &endCol);

        QTextCursor cursor(document());
        cursor.movePosition(QTextCursor::Start);
        QTextCursor endCursor(cursor);
        cursor.movePosition(QTextCursor::NextBlock, QTextCursor::MoveAnchor, startRow - 1);
        cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::MoveAnchor, startCol - 1);
        endCursor.movePosition(QTextCursor::NextBlock, QTextCursor::MoveAnchor, endRow - 1);
        endCursor.movePosition(QTextCursor::NextCharacter, QTextCursor::MoveAnchor, endCol - 1);

        QRect rect = cursorRect(cursor).adjusted(CURSOR_RECT_MARGIN, 0, 0, 0);
        cursor.movePosition(QTextCursor::EndOfLine);
        QList<QRect> rects;
        while (cursor < endCursor) {
            cursor.movePosition(QTextCursor::PreviousCharacter);
            rects << (rect | cursorRect(cursor).adjusted(0, 0, fontMetrics().boundingRect(QStringLiteral("0")).width() - CURSOR_RECT_MARGIN, 0));
            cursor.movePosition(QTextCursor::Down);
            cursor.movePosition(QTextCursor::StartOfLine);
            rect = cursorRect(cursor).adjusted(CURSOR_RECT_MARGIN, 0, 0, 0);
            cursor.movePosition(QTextCursor::EndOfLine);
        }
        rects << (rect | cursorRect(endCursor).adjusted(0, 0, -CURSOR_RECT_MARGIN, 0));
        return rects;
    }

    // stores the start/end row/col of currentWord and currentError in the coods of 2 rectangles
    QRect currentWord, currentError;
};

// END QTextEdit sub-class

class Editor : public QFrame
{
    Q_OBJECT

public:
    explicit Editor(QWidget *parent = nullptr);
    ~Editor() override;

    QTextEdit *view() const
    {
        return editor;
    }
    QTextDocument *document() const
    {
        return editor->document();
    }

    void enable();
    void disable();

    const QUrl &currentUrl()
    {
        return m_currentUrl;
    }
    void setCurrentUrl(const QUrl &url = QUrl());

    bool maybeSave();

    bool isModified()
    {
        return editor->document()->isModified();
    }
    QString content()
    {
        return editor->document()->toPlainText();
    }
    QString toHtml(const QString &title, const QString &lang);

    int row()
    {
        return currentRow;
    }
    int col()
    {
        return currentCol;
    }
    Token *currentToken();

    void removeMarkings()
    {
        editor->removeCurrentWordMark();
        editor->removeCurrentErrorMark();
    }

public Q_SLOTS:
    bool newFile();
    bool openFile(const QUrl &url = QUrl());
    void openExample(const QString &example, const QString &exampleName);
    bool saveFile(const QUrl &url = QUrl());
    bool saveFileAs();
    void toggleLineNumbers(bool b)
    {
        numbers->setVisible(b);
    }
    void setModified(bool);
    void setOverwriteMode(bool b);

    void markCurrentWord(TreeNode *node)
    {
        editor->markCurrentWord(node->token()->startRow(), node->token()->startCol(), node->token()->endRow(), node->token()->endCol());
    }
    void markCurrentError(int startRow, int startCol, int endRow, int endCol)
    {
        editor->markCurrentError(startRow, startCol, endRow, endCol);
    }
    void find();
    void findNext();
    void findPrev();
    void insertPlainText(const QString &txt);
    void rehighlight()
    {
        highlighter->rehighlight();
    }

Q_SIGNALS:
    void contentNameChanged(const QString &);
    void fileOpened(const QUrl &);
    void fileSaved(const QUrl &);
    void modificationChanged();
    void contentChanged();
    void cursorPositionChanged();

protected Q_SLOTS:
    void textChanged(int pos, int added, int removed);
    // 		void cursorPositionChanged();

protected:
    void paintEvent(QPaintEvent *event) override;

private Q_SLOTS:
    void updateOnCursorPositionChange();
    void highlightCurrentLine()
    {
        this->update();
    }

private:
    void setContent(const QString &);

    TextEdit *editor; // TODO why pointers?
    Highlighter *highlighter; // TODO could this class become a singleton? (shared with the inspector, errdlg)
    Tokenizer *tokenizer; // TODO could this class become a singleton? (shared with the highlighter, interpreter)
    LineNumbers *numbers;
    QHBoxLayout *box; // TODO is this relly needed?
    KFindDialog *fdialog;
    QUrl m_currentUrl; // contains url to the currently load file or the exampleName
    QColor highlightedLineBackgroundColor; // the bg color of the current line's line number space
    QString currentLine;
    int currentRow;
    int currentCol;
};

#endif // _EDITOR_H_
