// Copyright 2010 Anders Bakken
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#include "TextPagerEdit.hpp"

#include <algorithm>
#include <cmath>

#include <QGuiApplication>
#include <QScrollArea>
#include <QScrollBar>
#include <QVariant>
#include <QtGlobal>

#include "TextPagerCursor_p.hpp"
#include "TextPagerDocument_p.hpp"
#include "TextPagerEdit_p.hpp"
#include "TextPagerSearchHighlighter.hpp"
#include "UiLog.hpp"
#include "VConfig.hpp"
#include "ViewerUtil.hpp"

// #define _UI_TEXTPAGER_DEBUG
// #define DEBUG_TEXTPAGER_LASTPAGESIZE
// #define DEBUG_TEXTPAGER

#ifdef DEBUG_TEXTPAGER
bool doLog = false;
QString logFileName;
#endif

/*!
    Constructs an empty TextPagerEdit with parent \a parent.
*/

TextPagerEdit::TextPagerEdit(QWidget* parent) : QAbstractScrollArea(parent), d(new TextEditPrivate(this)) {
    viewport()->setCursor(Qt::IBeamCursor);

    setProperty("pager", "1");

#ifdef DEBUG_TEXTPAGER
    if (logFileName.isEmpty())
        logFileName = QDateTime::currentDateTime().toString("yyyyMMdd_hhmmsszzz.log");
#endif

    connect(verticalScrollBar(), SIGNAL(valueChanged(int)), d, SLOT(onScrollBarValueChanged(int)));
    connect(verticalScrollBar(), SIGNAL(actionTriggered(int)), d, SLOT(onScrollBarActionTriggered(int)));
    connect(verticalScrollBar(), SIGNAL(sliderReleased()), d, SLOT(updateScrollBar()));

    setDocument(new TextPagerDocument(this));
    setViewportMargins(0, 0, 0, 0);
    struct
    {
        QString text;
        const char* member;
        QKeySequence::StandardKey key;
    } shortcuts[] = {{tr("Copy"), SLOT(copy()), QKeySequence::Copy},
                     {tr("Select All"), SLOT(selectAll()), QKeySequence::SelectAll},
                     {QString(), nullptr, QKeySequence::UnknownKey}};
    for (int i = 0; shortcuts[i].member; ++i) {
        d->actions[i] = new QAction(shortcuts[i].text, this);
        d->actions[i]->setShortcutContext(Qt::WidgetShortcut);
        d->actions[i]->setShortcut(QKeySequence(shortcuts[i].key));
        d->actions[i]->setShortcutContext(Qt::WidgetShortcut);
        connect(d->actions[i], SIGNAL(triggered(bool)), this, shortcuts[i].member);
        addAction(d->actions[i]);
    }
    // connect(this, SIGNAL(undoAvailableChanged(bool)), d->actions[UndoAction], SLOT(setEnabled(bool)));
    // connect(this, SIGNAL(redoAvailableChanged(bool)), d->actions[RedoAction], SLOT(setEnabled(bool)));
    // d->actions[UndoAction]->setEnabled(false);
    // d->actions[RedoAction]->setEnabled(false);
    setContextMenuPolicy(Qt::ActionsContextMenu);
    // setCursorVisible(true); // starts blinking
    connect(this, SIGNAL(selectionChanged()), d, SLOT(onSelectionChanged()));

    setReadOnly(true);

    searchHighlight_ = new TextPagerSearchHighlighter(this);
}

/*!
    returns the current cursor position
*/

int TextPagerEdit::cursorPosition() const {
    return d->textCursor.position();
}

/*!
    ensures that \a cursor is visible with \a linesMargin lines of
    margin on each side (if possible)
*/

void TextPagerEdit::ensureCursorVisible(const TextPagerCursor& cursor, int linesMargin) {
    linesMargin = qMin(d->visibleLines, linesMargin); // ### could this be a problem down at the bottom of the document?
    Q_ASSERT(cursor.document() == document());
    TextPagerCursor above = cursor;
    for (int i = 0; i < linesMargin; ++i) {
        if (!above.movePosition(TextPagerCursor::Up))
            break;
    }
    if (above.position() < d->viewportPosition) {
        d->updateViewportPosition(above.position(), TextPagerLayout::Backward);
        return;
    }

    TextPagerCursor below = cursor;
    for (int i = 0; i < linesMargin; ++i) {
        if (!below.movePosition(TextPagerCursor::Down))
            break;
    }

    if (below.position() > d->lastVisibleCharacter) {
        for (int i = 0; i < d->visibleLines; ++i) {
            below.movePosition(TextPagerCursor::Up);
            d->updateViewportPosition(below.position(), TextPagerLayout::Forward);
        }
    }
}

/*!
    returns the QAction * for \a type.
*/

QAction* TextPagerEdit::action(ActionType type) const {
    return d->actions[type];
}

/*!
    Called when the textEdit is deleted
*/

TextPagerEdit::~TextPagerEdit() {
    if (d->document) {
        Q_FOREACH (SyntaxHighlighter* syntaxHighlighter, d->syntaxHighlighters) {
            if (syntaxHighlighter->parent() == this)
                disconnect(syntaxHighlighter, nullptr, d, nullptr);
            syntaxHighlighter->d->textEdit   = nullptr;
            syntaxHighlighter->d->textLayout = nullptr;
        }
        disconnect(d->document, nullptr, this, nullptr);
        disconnect(d->document, nullptr, d, nullptr);
        disconnect(d->document->d, nullptr, this, nullptr);
        disconnect(d->document->d, nullptr, d, nullptr);

        if (d->document->parent() == this) {
            delete d->document;
            d->document = nullptr;
        }
        else {
            d->document->d->textEditDestroyed(this);
        }
        // to make sure we don't do anything drastic on shutdown
    }
    delete d;

    if (fontProp_)
        fontProp_->removeObserver(this);
}

/*!
    returns the text edit's document. document() always returns a
    valid TextPagerDocument *
    \sa setDocument
*/

TextPagerDocument* TextPagerEdit::document() const {
    return d->document;
}

/*!
    sets the text edit's document to \a doc. The previous document
    will be deleted if it's parented to the text edit.

    If \a doc is 0 a default TextPagerDocument will be set.
    \sa document
*/

void TextPagerEdit::setDocument(TextPagerDocument* doc) {
    if (doc == d->document)
        return;

    if (d->document) {
        disconnect(d->document, nullptr, this, nullptr);
        disconnect(d->document, nullptr, d, nullptr);
        disconnect(d->document->d, nullptr, this, nullptr);
        disconnect(d->document->d, nullptr, d, nullptr);
        if (d->document->parent() == this)
            delete d->document;
    }
    if (!doc)
        doc = new TextPagerDocument(this);

    d->sections.clear();
    d->buffer.clear();
    d->sectionsDirty  = true;
    d->document       = doc;
    d->sectionPressed = nullptr;
    d->layoutDirty    = true;
    qDeleteAll(d->unusedTextLayouts);
    d->unusedTextLayouts.clear();
    qDeleteAll(d->textLayouts);
    d->textLayouts.clear();
    viewport()->setCursor(Qt::IBeamCursor);
    viewport()->setMouseTracking(true);
    d->sectionCount = 0;

    d->textCursor          = TextPagerCursor(doc);
    d->textCursor.textEdit = this;

    /*connect(d->document->d, SIGNAL(undoRedoCommandInserted(DocumentCommand *)),
            d, SLOT(onDocumentCommandInserted(DocumentCommand *)));
    connect(d->document, SIGNAL(sectionAdded(TextPagerSection *)),
            d, SLOT(onTextSectionAdded(TextPagerSection *)));
    connect(d->document, SIGNAL(sectionRemoved(TextPagerSection *)),
            d, SLOT(onTextSectionRemoved(TextPagerSection *)));
    connect(d->document->d, SIGNAL(undoRedoCommandRemoved(DocumentCommand *)),
            d, SLOT(onDocumentCommandRemoved(DocumentCommand *)));
    connect(d->document->d, SIGNAL(undoRedoCommandTriggered(DocumentCommand *, bool)),
            d, SLOT(onDocumentCommandTriggered(DocumentCommand *, bool)));*/

    connect(d->document, SIGNAL(charactersAdded(int, int)), d, SLOT(onCharactersAddedOrRemoved(int, int)));
    connect(d->document, SIGNAL(charactersRemoved(int, int)), d, SLOT(onCharactersAddedOrRemoved(int, int)));

    /*connect(d->document, SIGNAL(textChanged()), this, SIGNAL(textChanged()));
    connect(d->document, SIGNAL(undoAvailableChanged(bool)),
            this, SIGNAL(undoAvailableChanged(bool)));
    connect(d->document, SIGNAL(redoAvailableChanged(bool)),
            this, SIGNAL(redoAvailableChanged(bool)));*/

    connect(d->document, SIGNAL(documentSizeChanged(int)), d, SLOT(onDocumentSizeChanged(int)));
    connect(d->document, SIGNAL(destroyed(QObject*)), d, SLOT(onDocumentDestroyed()));
    connect(d->document->d,
            SIGNAL(sectionFormatChanged(TextPagerSection*)),
            d,
            SLOT(onTextSectionFormatChanged(TextPagerSection*)));
    connect(d->document->d,
            SIGNAL(sectionCursorChanged(TextPagerSection*)),
            d,
            SLOT(onTextSectionCursorChanged(TextPagerSection*)));

    d->onDocumentSizeChanged(d->document->documentSize());

    viewport()->update();
}

/*!
    returns the textCursor's width (in pixels)
    \sa setCursorWidth
*/

int TextPagerEdit::cursorWidth() const {
    return d->cursorWidth;
}

/*!
    sets the cursorWidth of the text edit to \a cw pixels.
    \a cw must be a valid integer larger than 0.

    \sa setCursorVisible
*/

void TextPagerEdit::setCursorWidth(int cw) {
    Q_ASSERT(d->cursorWidth > 0);
    d->cursorWidth = cw;
    viewport()->update();
}

/*!
    Loads the contenst of \a dev into the text edit's document.
    Equivalent to calling document()->load(\a dev, \a mode);

    \sa setCursorVisible
*/

#if 0
bool TextPagerEdit::load(QIODevice *dev, TextPagerDocument::DeviceMode mode, QTextCodec *codec)
{
    #ifdef DEBUG_TEXTPAGER
    if (doLog) {
        QFile f(logFileName);
        f.open(QIODevice::WriteOnly);
        QDataStream ds(&f);
        if (QFile *ff = qobject_cast<QFile*>(dev)) {
            ds << ff->fileName();
        } else {
            ds << QString::fromLatin1(dev->metaObject()->className());
        }
    }
    #endif
    //we have to check to font here because the initial setting in setFontProperty doe not have any effect
    updateFont();
    return d->document->load(dev, mode, codec);
}
#endif

bool TextPagerEdit::load(const QString& file, TextPagerDocument::DeviceMode mode, TextCodecWrapper codec) {
#ifdef DEBUG_TEXTPAGER
    if (doLog) {
        QFile f(logFileName);
        f.open(QIODevice::WriteOnly);
        QDataStream ds(&f);
        ds << file;
    }
#endif

    UiLog().dbg() << "TextPagerEdit::load fileName" << file;
    // we have to check to font here because the initial setting in setFontProperty doe not have any effect
    updateFont();
    bool ret = d->document->load(file, mode, codec);
    UiLog().dbg() << "  cursor: " << textCursor().position();
    return ret;
}

void TextPagerEdit::setText(const QString& txt) {
    // we have to check to font here because the initial setting in setFontProperty doe not have any effect
    updateFont();
    d->document->setText(txt);
}

enum SelectionAddStatus { Invalid, Before, After, Success };

static inline SelectionAddStatus
addSelection(int layoutStart, int layoutLength, const TextPagerCursor& cursor, QTextLayout::FormatRange* format) {
    Q_ASSERT(format);
    if (!cursor.hasSelection())
        return Invalid;
    if (cursor.selectionEnd() < layoutStart)
        return Before;
    if (cursor.selectionStart() > layoutStart + layoutLength)
        return After;

    format->start  = qMax(0, cursor.selectionStart() - layoutStart);
    format->length = qMin(layoutLength - format->start, cursor.selectionEnd() - layoutStart - format->start);
    return Success;
}

void TextPagerEdit::paintEvent(QPaintEvent* e) {
    d->updateScrollBarPosition();
    d->relayout();
    if (d->updateScrollBarPageStepPending) {
        d->updateScrollBarPageStepPending = false;
        d->updateScrollBarPageStep();
    }

    QPainter p(viewport());

    const QRect er = e->rect();
    p.translate(-horizontalScrollBar()->value(), 0);
    p.setFont(font());
    QVector<QTextLayout::FormatRange> selections;
    selections.reserve(d->extraSelections.size() + 1);
    int textLayoutOffset = d->viewportPosition;

    const QTextLayout* cursorLayout = d->cursorVisible ? d->layoutForPosition(d->textCursor.position()) : nullptr;
    int extraSelectionIndex         = 0;
    QTextLayout::FormatRange selectionRange;
    selectionRange.start = -1;
    Q_FOREACH (QTextLayout* l, d->textLayouts) {
        const int textSize = l->text().size();
        const QRect r      = l->boundingRect().toRect();
        if (r.intersects(er)) {
            const QBrush background = d->blockFormats.value(l).background();
            if (background.style() != Qt::NoBrush) {
                p.fillRect(r, background);
            }
            if (::addSelection(textLayoutOffset, textSize, d->textCursor, &selectionRange) == Success) {
                selectionRange.format.setBackground(palette().highlight());
                selectionRange.format.setForeground(palette().highlightedText());
            }
            int lowestIncompleteSelection = -1;
            while (extraSelectionIndex < d->extraSelections.size()) {
                QTextLayout::FormatRange range;
                const SelectionAddStatus s = ::addSelection(
                    textLayoutOffset, textSize, d->extraSelections.at(extraSelectionIndex).cursor, &range);
                if (s == Success) {
                    range.format = d->extraSelections.at(extraSelectionIndex).format;
                    selections.append(range);

                    const TextPagerCursor& cursor = d->extraSelections.at(extraSelectionIndex).cursor;
                    int lastPos                   = cursor.position() + cursor.selectionSize();
                    if (lastPos > textLayoutOffset + textSize && lowestIncompleteSelection < 0) {
                        lowestIncompleteSelection = extraSelectionIndex;
                    }
                }
                else if (s == After) {
                    break;
                }
                ++extraSelectionIndex;
            }
            if (lowestIncompleteSelection > -1) {
                extraSelectionIndex = lowestIncompleteSelection;
            }

            // is this the current line?
            /*if(cursorLayout == l)
            {
                p.fillRect(r, QColor(216,228,239));  // highlight the current line
            }*/

            if (selectionRange.start != -1) {
                // The last range in the vector has priority, that
                // should probably be the real selection
                selections.append(selectionRange);
                selectionRange.start = -1;
            }

            l->draw(&p, QPoint(0, 0), selections);
            if (!selections.isEmpty())
                selections.clear();
            if (cursorLayout == l) {
                cursorLayout->drawCursor(&p, QPoint(0, 0), d->textCursor.position() - textLayoutOffset, d->cursorWidth);
            }
        }
        else if (r.top() > er.bottom()) {
            break;
        }
        textLayoutOffset += l->text().size() + 1;
    }

    // paintLineNumberArea(e);
}

void TextPagerEdit::scrollContentsBy(int dx, int dy) {
    Q_UNUSED(dx);
    Q_UNUSED(dy);
    //    viewport()->update();
    viewport()->scroll(dx, dy); // seems to jitter more
}

int TextPagerEdit::viewportPosition() const {
    return d->viewportPosition;
}

void TextPagerEdit::mousePressEvent(QMouseEvent* e) {
    d->inMouseEvent = true;
    if (e->button() == Qt::LeftButton) {
#ifdef DEBUG_TEXTPAGER
        if (doLog) {
            QFile file(logFileName);
            file.open(QIODevice::Append);
            QDataStream ds(&file);
            ds << int(e->type()) << e->pos() << e->button() << e->buttons() << e->modifiers();
        }
#endif
        if (d->tripleClickTimer.isActive()) {
            d->tripleClickTimer.stop();
            d->textCursor.movePosition(TextPagerCursor::StartOfBlock);
            d->textCursor.movePosition(TextPagerCursor::EndOfBlock, TextPagerCursor::KeepAnchor);
        }
        else {
            const bool shift = e->modifiers() & Qt::ShiftModifier;
            if (!shift) {
                clearSelection();
            }
            int pos = textPositionAt(e->pos());
            if (pos == -1)
                pos = d->document->documentSize() - 1;
            d->sectionPressed = d->document->d->sectionAt(pos, this);
            setCursorPosition(pos, shift ? TextPagerCursor::KeepAnchor : TextPagerCursor::MoveAnchor);
        }

        e->accept();

        // The cursor changed so wee need to update the selected line number
        if (lineNumArea_ && showLineNum_)
            lineNumArea_->update();
    }
    else {
        QAbstractScrollArea::mousePressEvent(e);
    }
}

void TextPagerEdit::mouseDoubleClickEvent(QMouseEvent* e) {
    if (e->button() == Qt::LeftButton) {
#ifdef DEBUG_TEXTPAGER
        if (doLog) {
            QFile file(logFileName);
            file.open(QIODevice::Append);
            QDataStream ds(&file);
            ds << int(e->type()) << e->pos() << e->button() << e->buttons() << e->modifiers();
        }
#endif
        const int pos = textPositionAt(e->pos());
        if (pos == d->textCursor.position()) {
            d->tripleClickTimer.start(qApp->doubleClickInterval(), d);
            if (d->document->isWordCharacter(d->textCursor.cursorCharacter(), d->textCursor.position())) {
                // ### this is not quite right
                d->textCursor.movePosition(TextPagerCursor::StartOfWord);
                d->textCursor.movePosition(TextPagerCursor::EndOfWord, TextPagerCursor::KeepAnchor);
                return;
            }
        }
        mousePressEvent(e);
    }
}

void TextPagerEdit::mouseMoveEvent(QMouseEvent* e) {
    if (e->buttons() == Qt::NoButton) {
        d->updateCursorPosition(e->pos());
    }
    else if (e->buttons() == Qt::LeftButton && d->document->documentSize()) {
#ifdef DEBUG_TEXTPAGER
        if (doLog) {
            QFile file(logFileName);
            file.open(QIODevice::Append);
            QDataStream ds(&file);
            ds << int(e->type()) << e->pos() << e->button() << e->buttons() << e->modifiers();
        }
#endif
        const QRect r    = viewport()->rect();
        d->lastMouseMove = e->pos();
        e->accept();
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
        if (e->position().y() < r.top()) {
#else
        if (e->y() < r.top()) {
#endif
            if (d->atBeginning()) {
                d->updateHorizontalPosition();
                d->autoScrollTimer.stop();
                return;
            }
        }
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
        else if (e->position().y() > r.bottom()) {
#else
        else if (e->y() > r.bottom()) {
#endif
            if (d->atEnd()) {
                d->updateHorizontalPosition();
                d->autoScrollTimer.stop();
                return;
            }
        }
        else {
            int pos = textPositionAt(QPoint(qBound(0, d->lastMouseMove.x(), r.right()), d->lastMouseMove.y()));
            if (pos == -1)
                pos = d->document->documentSize();
            d->autoScrollTimer.stop();
            setCursorPosition(pos, TextPagerCursor::KeepAnchor);

            // The cursor changed so wee need to update the selected line number
            if (lineNumArea_ && showLineNum_)
                lineNumArea_->update();

            return;
        }
        const int distance = qMax(r.top() - d->lastMouseMove.y(), d->lastMouseMove.y() - r.bottom());
        Q_ASSERT(distance != 0);
        enum { MinimumTimeOut = 3 };
        int timeout = qMax<int>(MinimumTimeOut, 100 - distance);
        enum { Margin = 3 };
        auto sc = QGuiApplication::primaryScreen();
        if (sc && (sc->geometry().bottom() - mapToGlobal(d->lastMouseMove).y()) <= Margin) {
            timeout = MinimumTimeOut;
        }
        d->autoScrollLines = 1 + ((100 - timeout) / 30);
        if (d->autoScrollTimer.isActive()) {
            d->pendingTimeOut = timeout;
        }
        else {
            d->pendingTimeOut = -1;
            d->autoScrollTimer.start(timeout, d);
        }
    }
    else {
        QAbstractScrollArea::mouseMoveEvent(e);
    }
}

void TextPagerEdit::mouseReleaseEvent(QMouseEvent* e) {
    d->inMouseEvent = false;
    if (e->button() == Qt::LeftButton) {
#ifdef DEBUG_TEXTPAGER
        if (doLog) {
            QFile file(logFileName);
            file.open(QIODevice::Append);
            QDataStream ds(&file);
            ds << int(e->type()) << e->pos() << e->button() << e->buttons() << e->modifiers();
        }
#endif
        d->autoScrollTimer.stop();
        d->pendingTimeOut = -1;
        e->accept();
        if (d->sectionPressed && sectionAt(e->pos()) == d->sectionPressed) {
            Q_EMIT sectionClicked(d->sectionPressed, e->pos());
        }
        d->sectionPressed = nullptr;
    }
    else {
        QAbstractScrollArea::mouseReleaseEvent(e);
    }
}

void TextPagerEdit::resizeEvent(QResizeEvent* e) {
#ifdef DEBUG_TEXTPAGER
    if (doLog) {
        QFile file(logFileName);
        file.open(QIODevice::Append);
        QDataStream ds(&file);
        ds << int(e->type()) << e->size();
    }
#endif
    QAbstractScrollArea::resizeEvent(e);
    if (e->oldSize().height() != e->size().height())
        d->adjustVerticalScrollBar();
    d->updateScrollBarPageStepPending = true;
    d->layoutDirty                    = true;
}

int TextPagerEdit::textPositionAt(const QPoint& pos) const {
    if (!viewport()->rect().contains(pos))
        return -1;

    QPoint realPos = pos;
    realPos += QPoint(horizontalScrollBar()->value(), 0);

    // Adjust the horizontal position
    /*  if(pos.x()  > horizontalScrollBar()->value()+viewport()->rect().width())
      {
          int hval=realCrect.left()-r.width()/2;
          horizontalScrollBar()->setValue((hval < horizontalScrollBar()->maximum())?
                                              hval:
                                              horizontalScrollBar()->maximum());
      }
      else if(realCrect.right() < horizontalScrollBar()->value())
      {
          int hval=realCrect.left()-r.width()/2;
          horizontalScrollBar()->setValue((hval > horizontalScrollBar()->minimum())?
                                              hval:
                                              horizontalScrollBar()->minimum());
      }
  }*/

    return d->textPositionAt(realPos);
}

bool TextPagerEdit::readOnly() const {
    return d->readOnly;
}

void TextPagerEdit::setReadOnly(bool rr) {
    d->readOnly = rr;

    setCursorVisible(!rr);

    /*  //d->actions[PasteAction]->setEnabled(!rr);
      //d->actions[CutAction]->setEnabled(!rr);
      //d->actions[PasteAction]->setVisible(!rr);
      //d->actions[CutAction]->setVisible(!rr);

      const bool redoWasAvailable = isRedoAvailable();
      const bool undoWasAvailable = isUndoAvailable();

     // d->actions[UndoAction]->setEnabled(!rr);
    //  d->actions[RedoAction]->setEnabled(!rr);
     // d->actions[UndoAction]->setVisible(!rr);
     // d->actions[RedoAction]->setVisible(!rr);


      if (undoWasAvailable != isUndoAvailable())
          Q_EMIT undoAvailableChanged(!undoWasAvailable);
      if (redoWasAvailable != isRedoAvailable())
          Q_EMIT redoAvailableChanged(!redoWasAvailable);
          */
}

bool TextPagerEdit::lineBreaking() const {
    return d->lineBreaking;
}

void TextPagerEdit::setLineBreaking(bool lineBreaking) {
    if (d->lineBreaking != lineBreaking) {
        d->lineBreaking = lineBreaking;
        d->layoutDirty  = true;
        viewport()->update();
    }
}

int TextPagerEdit::maximumSizeCopy() const {
    return d->maximumSizeCopy;
}

void TextPagerEdit::setMaximumSizeCopy(int max) {
    d->maximumSizeCopy = qMax(0, max);
    d->updateCopyAndCutEnabled();
}

QRect TextPagerEdit::cursorBlockRect(const TextPagerCursor& textCursor) const {
    if (const QTextLayout* l = d->layoutForPosition(textCursor.position())) {
        return l->boundingRect().toRect();
    }
    return {};
}

QRect TextPagerEdit::cursorRect(const TextPagerCursor& textCursor) const {
    int offset = -1;
    if (d->layoutForPosition(textCursor.position(), &offset)) {
        ASSUME(offset != -1);
        QTextLine line = d->lineForPosition(textCursor.position());

        // if line is empty line.cursorToX() crashes. A qt bug?
        if (line.isValid()) {
            qreal x = line.cursorToX(offset);
            return {static_cast<int>(x), static_cast<int>(line.y()), d->cursorWidth, static_cast<int>(line.height())};
        }
    }
    return {};
}

int TextPagerEdit::lineNumber(int position) const {
    return d->document->lineNumber(position);
}

int TextPagerEdit::columnNumber(int position) const {
    TextPagerCursor cursor(this, position);
    return cursor.isNull() ? -1 : cursor.columnNumber();
}

int TextPagerEdit::lineNumber(const TextPagerCursor& cursor) const {
    return cursor.document() == d->document ? d->document->lineNumber(cursor.position()) : -1;
}

int TextPagerEdit::columnNumber(const TextPagerCursor& cursor) const {
    return cursor.document() == d->document ? cursor.columnNumber() : -1;
}

void TextPagerEdit::wheelEvent(QWheelEvent* e) {
    auto delta = e->angleDelta();
    if (delta.y() != 0) {
        d->scrollLines(3 * (delta.y() > 0 ? -1 : 1));
    }
    else {
        QAbstractScrollArea::wheelEvent(e);
    }

    if (e->modifiers() & Qt::ControlModifier) {
        if (delta.y() < 0)
            zoomOut();
        else if (delta.y() > 0)
            zoomIn();
        return;
    }
}

void TextPagerEdit::changeEvent(QEvent* e) {
    if (e->type() == QEvent::FontChange) {
        d->font = font();
        Q_FOREACH (QTextLayout* l, d->textLayouts) {
            l->setFont(d->font);
        }
        Q_FOREACH (QTextLayout* l, d->unusedTextLayouts) {
            l->setFont(d->font);
        }

        d->adjustVerticalScrollBar();

        if (lineNumArea_ && showLineNum_)
            lineNumArea_->updateWidth();

        d->layoutDirty = true;
        viewport()->update();
    }
}

void TextPagerEdit::keyPressEvent(QKeyEvent* e) {
#ifdef DEBUG_TEXTPAGER
    if (doLog) {
        QFile file(logFileName);
        file.open(QIODevice::Append);
        QDataStream ds(&file);
        ds << int(e->type()) << e->key() << int(e->modifiers()) << e->text() << e->isAutoRepeat() << e->count();
    }
#endif

    Q_ASSERT(d->textCursor.textEdit == this);
    if (d->readOnly) {
        d->cursorMoveKeyEventReadOnly(e);
        return;
    }
}

void TextPagerEdit::keyReleaseEvent(QKeyEvent* e) {
#ifdef DEBUG_TEXTPAGER
    if (doLog) { // ### does it make any sense to replay these? Probably not
        QFile file(logFileName);
        file.open(QIODevice::Append);
        QDataStream ds(&file);
        ds << int(e->type()) << e->key() << int(e->modifiers()) << e->text() << e->isAutoRepeat() << e->count();
    }
#endif
    QAbstractScrollArea::keyReleaseEvent(e);
}

void TextPagerEdit::setCursorPosition(int pos, TextPagerCursor::MoveMode mode) {
    d->textCursor.setPosition(pos, mode);
}

bool TextPagerEdit::moveCursorPosition(TextPagerCursor::MoveOperation op, TextPagerCursor::MoveMode mode, int n) {
    return d->textCursor.movePosition(op, mode, n);
}

void TextPagerEdit::copy(QClipboard::Mode mode) {
    if (d->textCursor.selectionSize() <= d->maximumSizeCopy) {
        QApplication::clipboard()->setText(selectedText(), mode);
    }
}

bool TextPagerEdit::cursorVisible() const {
    return d->cursorBlinkTimer.isActive();
}

void TextPagerEdit::setCursorVisible(bool cc) {
    if (cc == d->cursorBlinkTimer.isActive())
        return;

    d->cursorVisible = cc;
    if (cc) {
        d->cursorBlinkTimer.start(QApplication::cursorFlashTime(), d);
    }
    else {
        d->cursorBlinkTimer.stop();
    }
    const QRect r = cursorRect(d->textCursor) & viewport()->rect();
    if (!r.isNull()) {
        viewport()->update(r);
    }
}

void TextPagerEdit::clearSelection() {
    if (!d->textCursor.hasSelection())
        return;

    d->textCursor.clearSelection();
}

QString TextPagerEdit::selectedText() const {
    return d->textCursor.selectedText();
}

bool TextPagerEdit::hasSelection() const {
    return d->textCursor.hasSelection();
}

void TextPagerEdit::selectAll() {
    TextPagerCursor cursor(d->document);
    Q_ASSERT(cursor.position() == 0);
    cursor.movePosition(TextPagerCursor::End, TextPagerCursor::KeepAnchor);
    setTextCursor(cursor);
    Q_EMIT selectionChanged();
}

QString TextPagerEdit::read(int pos, int size) const {
    return d->document->read(pos, size);
}

QChar TextPagerEdit::readCharacter(int index) const {
    return d->document->readCharacter(index);
}

void TextEditPrivate::onDocumentSizeChanged(int /*size*/) {
    adjustVerticalScrollBar();

    /*int s=findLastPageSize();

    qDebug() << "lat page position" << s;

    if(s != -1)
       textEdit->verticalScrollBar()->setRange(0, s);
    else
       textEdit->verticalScrollBar()->setRange(0, qMax(0, size));


    //textEdit->verticalScrollBar()->setRange(0, qMax(0, size));
//    qDebug() << findLastPageSize();
    maxViewportPosition = textEdit->verticalScrollBar()->maximum();
    updateScrollBarPageStepPending = true;*/
}

void TextEditPrivate::adjustVerticalScrollBar() {
    int s = findLastPageSize();

    int size = (document != nullptr) ? (document->documentSize()) : 0;

#ifdef DEBUG_TEXTPAGER_LASTPAGESIZE
    qDebug() << "last page position" << s;
#endif
    if (s == 0) {
        textEdit->verticalScrollBar()->hide();
        textEdit->verticalScrollBar()->setEnabled(false);
    }
    else {
        textEdit->verticalScrollBar()->setEnabled(true);
        textEdit->verticalScrollBar()->show();

        if (s != -1)
            textEdit->verticalScrollBar()->setRange(0, s);
        else
            textEdit->verticalScrollBar()->setRange(0, qMax(0, size));
    }

    // textEdit->verticalScrollBar()->setRange(0, qMax(0, size));
    //    qDebug() << findLastPageSize();
    maxViewportPosition            = textEdit->verticalScrollBar()->maximum();
    updateScrollBarPageStepPending = true;
}

void TextEditPrivate::updateCopyAndCutEnabled() {
    const bool wasEnabled = actions[TextPagerEdit::CopyAction]->isEnabled();
    const bool enable     = qAbs(textCursor.position() - textCursor.anchor()) <= maximumSizeCopy;
    actions[TextPagerEdit::CopyAction]->setEnabled(enable);
    // actions[TextPagerEdit::CutAction]->setEnabled(enable);
    if (wasEnabled != enable) {
        Q_EMIT textEdit->copyAvailable(enable);
    }
}

void TextPagerEdit::takeSyntaxHighlighter(SyntaxHighlighter* highlighter) {
    Q_ASSERT(highlighter);
    const bool found = d->syntaxHighlighters.removeOne(highlighter);
    Q_ASSERT(found);
    Q_UNUSED(found);
    highlighter->d->textEdit   = nullptr;
    highlighter->d->textLayout = nullptr;
    disconnect(highlighter, nullptr, d, nullptr);
}

void TextPagerEdit::removeSyntaxHighlighter(SyntaxHighlighter* highlighter) {
    takeSyntaxHighlighter(highlighter);
    delete highlighter;
}

void TextPagerEdit::addSyntaxHighlighter(SyntaxHighlighter* highlighter) {
    Q_ASSERT(highlighter);
    if (highlighter->textEdit() != this) {
        if (highlighter->textEdit()) {
            qWarning("A SyntaxHighlighter can only be added to 1 TextPagerEdit. If this is a "
                     "use case you care about (having a syntaxHighlighter added to multiple "
                     "text edits) I could fix it. Anders");
            return;
        }
        d->syntaxHighlighters.append(highlighter);
        connect(highlighter, SIGNAL(destroyed(QObject*)), d, SLOT(onSyntaxHighlighterDestroyed(QObject*)));
        highlighter->d->textEdit   = this;
        highlighter->d->textLayout = d;
        d->layoutDirty             = true;
        viewport()->update();
    }
}

void TextPagerEdit::clearSyntaxHighlighters() {
    Q_FOREACH (SyntaxHighlighter* highlighter, d->syntaxHighlighters) {
        if (highlighter->parent() == this) {
            removeSyntaxHighlighter(highlighter);
        }
        else {
            takeSyntaxHighlighter(highlighter);
        }
    }
    Q_ASSERT(d->syntaxHighlighters.isEmpty());
}

void TextEditPrivate::onSyntaxHighlighterDestroyed(QObject* o) {
    const bool found = syntaxHighlighters.removeOne(static_cast<SyntaxHighlighter*>(o));
    Q_ASSERT(found);
    Q_UNUSED(found);
    layoutDirty = true;
    textEdit->viewport()->update();
}

QList<SyntaxHighlighter*> TextPagerEdit::syntaxHighlighters() const {
    return d->syntaxHighlighters;
}

static inline bool compareExtraSelection(const TextPagerEdit::ExtraSelection& left,
                                         const TextPagerEdit::ExtraSelection& right) {
    return left.cursor < right.cursor;
}

void TextPagerEdit::setExtraSelections(const QList<ExtraSelection>& selections) {
    d->extraSelections = selections;
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
    std::sort(d->extraSelections.begin(), d->extraSelections.end(), compareExtraSelection);
#else
    qSort(d->extraSelections.begin(), d->extraSelections.end(), compareExtraSelection);
#endif
    d->layoutDirty = true;
    viewport()->update();
}

QList<TextPagerEdit::ExtraSelection> TextPagerEdit::extraSelections() const {
    return d->extraSelections;
}

void TextEditPrivate::onTextSectionRemoved(TextPagerSection* section) {
    Q_ASSERT(section);
    if (!dirtyForSection(section))
        return;

    sectionsDirty = true;
    if (section == sectionPressed) {
        sectionPressed = nullptr;
    }
    if (section->hasCursor()) {
        updateCursorPosition(lastHoverPos);
    }
}

void TextEditPrivate::onTextSectionAdded(TextPagerSection* section) {
    dirtyForSection(section);
    updateCursorPosition(lastHoverPos);
    sectionsDirty = true;
}

void TextEditPrivate::onScrollBarValueChanged(int value) {
    if (blockScrollBarUpdate || value == requestedScrollBarPosition || value == viewportPosition)
        return;
    requestedScrollBarPosition = value;
    layoutDirty                = true;
    textEdit->viewport()->update();

    Q_EMIT scrollBarChanged();
}

void TextEditPrivate::onScrollBarActionTriggered(int action) {
    // IR: see comment at the top of cursorMoveKeyEventReadOnly().
    // Also: added code to handle SliderPageStepAdd and SliderPageStepSub because
    // these cases were not handled well by default. These cases occur when the user clicks inside
    // the scrollbar area, but not on the bar itself; this should do something similar or
    // identical to a page up/down operation.

    switch (action) {
        case QAbstractSlider::SliderSingleStepAdd:
            scrollLines(1);
            requestedScrollBarPosition = -1;
            break;
        case QAbstractSlider::SliderSingleStepSub:
            scrollLines(-1);
            requestedScrollBarPosition = -1;
            break;
        case QAbstractSlider::SliderPageStepAdd:
            scrollLines(qMax(1, visibleLines - 1));
            requestedScrollBarPosition = -1;
            break;
        case QAbstractSlider::SliderPageStepSub:
            scrollLines(-qMax(1, visibleLines - 2));
            requestedScrollBarPosition = -1;
            break;
        default:
            break;
    }

    Q_EMIT scrollBarChanged();
}

void TextEditPrivate::updateScrollBar() {
    Q_ASSERT(!textEdit->verticalScrollBar()->isSliderDown());
    if (pendingScrollBarUpdate) {
        const bool old       = blockScrollBarUpdate;
        blockScrollBarUpdate = true;
        textEdit->verticalScrollBar()->setValue(viewportPosition);
        blockScrollBarUpdate   = old;
        pendingScrollBarUpdate = false;

        Q_EMIT scrollBarChanged();
    }
}

void TextEditPrivate::onCharactersAddedOrRemoved(int /*from*/, int count) {
    Q_ASSERT(count >= 0);
    Q_UNUSED(count);

    textCursor.clearSelection();

    if (textCursor.position() > document->documentSize())
        textCursor.setPosition(0);

    //    UiLog().dbg() <<
    //              "TextEditPrivate::onCharactersAddedOrRemoved --> textCursor: " << textCursor.position();

    /*if (from > qMin(bufferPosition + buffer.size(), layoutEnd)) {
        return;
    }*/
    buffer.clear(); // isn't it better to just add them here?
    layoutDirty = true;
    textEdit->viewport()->update();
}

void TextPagerEdit::ensureCursorVisible() {
    if (d->textCursor.position() < d->viewportPosition) {
        d->updateViewportPosition(qMax(0, d->textCursor.position() - 1), TextPagerLayout::Backward);
    }
    else if (d->textCursor.position() > d->layoutEnd) {
        d->updateViewportPosition(d->textCursor.position(), TextPagerLayout::Backward);
        viewport()->update();
    }
    else {
        const QRect r   = viewport()->rect();
        QRect realCrect = cursorRect(d->textCursor);
        QRect crect     = realCrect;
        crect.setLeft(r.left());
        crect.setRight(r.right());
        // ### what if the cursor is out of bounds horizontally?

        // qDebug() << "ensureCursorVisible" << r << crect << cursorRect(d->textCursor) <<
        // horizontalScrollBar()->value() << horizontalScrollBar()->maximum() <<  d->widest;
        if (!r.contains(crect)) {
            if (r.intersects(crect)) {
                int scroll;
                if (d->autoScrollLines != 0) {
                    scroll = d->autoScrollLines;
                }
                else {
                    scroll = (crect.top() < r.top() ? -1 : 1);
                }
                d->scrollLines(scroll);
            }
            else {
                d->updateViewportPosition(d->textCursor.position(), TextPagerLayout::Backward);
            }
            viewport()->update();
        }

        // Adjust the horizontal position
        if (realCrect.left() > horizontalScrollBar()->value() + r.width()) {
            int hval = realCrect.left() - r.width() / 2;
            horizontalScrollBar()->setValue(
                (hval < horizontalScrollBar()->maximum()) ? hval : horizontalScrollBar()->maximum());
        }
        else if (realCrect.right() < horizontalScrollBar()->value()) {
            int hval = realCrect.left() - r.width() / 2;
            horizontalScrollBar()->setValue(
                (hval > horizontalScrollBar()->minimum()) ? hval : horizontalScrollBar()->minimum());
        }
    }
}

// textEdit->horizontalScrollBar()->setPageStep(s.width());
// textEdit->horizontalScrollBar()->setMaximum(qMax(0, widest - s.width()));

TextPagerCursor& TextPagerEdit::textCursor() {
    return d->textCursor;
}

const TextPagerCursor& TextPagerEdit::textCursor() const {
    return d->textCursor;
}

void TextPagerEdit::setTextCursor(const TextPagerCursor& textCursor) {
    const bool doEmit      = (d->textCursor != textCursor);
    d->textCursor          = textCursor;
    d->textCursor.textEdit = this;
    if (doEmit) {
        ensureCursorVisible();
        viewport()->update();
        Q_EMIT cursorPositionChanged(textCursor.position());
    }
}

TextPagerCursor TextPagerEdit::cursorForPosition(const QPoint& pos) const {
    const int idx = textPositionAt(pos);
    if (idx == -1)
        return TextPagerCursor();
    return TextPagerCursor(this, idx);
}

TextPagerSection* TextPagerEdit::sectionAt(const QPoint& pos) const {
    Q_ASSERT(d->document);
    int textPos = textPositionAt(pos);
    if (textPos == -1)
        textPos = d->document->d->documentSize - 1;
    return d->document->d->sectionAt(textPos, this);
}

QList<TextPagerSection*> TextPagerEdit::sections(int from, int size, TextPagerSection::TextSectionOptions opt) const {
    Q_ASSERT(d->document);
    QList<TextPagerSection*> sections = d->document->d->getSections(from, size, opt, this);
    return sections;
}

TextPagerSection*
TextPagerEdit::insertTextSection(int pos, int size, const QTextCharFormat& format, const QVariant& data) {
    Q_ASSERT(d->document);
    TextPagerSection* section = d->document->insertTextSection(pos, size, format, data);
    if (section) {
        section->d.textEdit = this;
        section->d.priority = 100;
    }
    return section;
}

void TextEditPrivate::updateHorizontalPosition() {
    const QRect r = textEdit->viewport()->rect();
    const QPoint p(qBound(0, lastMouseMove.x(), r.right()), qBound(0, lastMouseMove.y(), r.bottom()));
    int pos = textPositionAt(p);
    if (pos == -1)
        pos = document->documentSize() - 1;
    textEdit->setCursorPosition(pos, TextPagerCursor::KeepAnchor);
}

void TextEditPrivate::updateScrollBarPosition() {
    if (requestedScrollBarPosition == -1) {
        return;
    }
    else if (requestedScrollBarPosition == viewportPosition) {
        requestedScrollBarPosition = -1;
        return;
    }

    const int req              = requestedScrollBarPosition;
    requestedScrollBarPosition = -1;

    Direction direction = Forward;
    if (lastRequestedScrollBarPosition != -1 && lastRequestedScrollBarPosition != req) {
        if (lastRequestedScrollBarPosition > req) {
            direction = Backward;
        }
    }
    else if (req < viewportPosition) {
        direction = Backward;
    }

    lastRequestedScrollBarPosition = req;

    updateViewportPosition(req, direction);
}

void TextEditPrivate::updateScrollBarPageStep() {
    if (lines.isEmpty()) {
        textEdit->verticalScrollBar()->setPageStep(1);
        return;
    }
    const int visibleCharacters = lines.at(qMin(visibleLines, lines.size() - 1)).first - lines.at(0).first;
    textEdit->verticalScrollBar()->setPageStep(visibleCharacters);
}

void TextEditPrivate::onDocumentDestroyed() {
    document = nullptr;
    textEdit->setDocument(new TextPagerDocument(this)); // there should always be a document
}

void TextEditPrivate::scrollLines(int lines) {
    // IR: see comment at the top of cursorMoveKeyEventReadOnly().

    int pos           = viewportPosition;
    const Direction d = (lines < 0 ? Backward : Forward);
    const int add     = lines < 0 ? -1 : 1;

    while (pos + add >= 0 && pos + add < document->documentSize()) {
        if (d == Forward && (lines - add == 0) && bufferReadCharacter(pos) == '\n') {
            // When iterating forwards, be sure not to skip over blank lines
            // (ie. lines containing only '\n') by just ignoring them here
            // - updateViewportPosition automatically takes us one index past
            // this newline, thus displaying the next line)

            // IR: the above comment does not seem to be true, at least it is not always true.
            // If performing a 'page down' operation, this 'break' causes one less line to
            // be scrolled. This is because pos is already on the newline at the end of
            // the previous line; this check re-reads that newline and then breaks without
            // reading the last line at all. This is not done when paging backwards. So
            // scrolling down 5 lines actually scrolls down 4 lines, and scrolling up 5
            // lines does indeed scroll up 5 lines.
            break;
        }
        else {
            pos += add;
            if (bufferReadCharacter(pos) == '\n') {
                if ((lines -= add) == 0) {
                    break;
                }
            }
        }
    }
    updateViewportPosition(pos, d);
}

void TextEditPrivate::timerEvent(QTimerEvent* e) {
    if (e->timerId() == tripleClickTimer.timerId()) {
        tripleClickTimer.stop();
    }
    else if (e->timerId() == autoScrollTimer.timerId()) {
        if (pendingTimeOut != -1) {
            autoScrollTimer.start(pendingTimeOut, this);
            pendingTimeOut = -1;
        }
        const QRect r = textEdit->viewport()->rect();
        Q_ASSERT(!r.contains(lastMouseMove));
        enum { LineCount = 1 };
        if (lastMouseMove.y() < r.top()) {
            scrollLines(-LineCount);
            if (atBeginning())
                autoScrollTimer.stop();
        }
        else {
            Q_ASSERT(lastMouseMove.y() > r.bottom());
            scrollLines(LineCount);
            if (atEnd())
                autoScrollTimer.stop();
        }
        const QPoint p(qBound(0, lastMouseMove.x(), r.right()), qBound(0, lastMouseMove.y(), r.bottom()));
        int pos = textPositionAt(p);
        if (pos == -1)
            pos = document->documentSize() - 1;
        textEdit->setCursorPosition(pos, TextPagerCursor::KeepAnchor);
    }
    else if (e->timerId() == cursorBlinkTimer.timerId()) {
        cursorVisible = !cursorVisible;
        const QRect r = textEdit->cursorRect(textCursor) & textEdit->viewport()->rect();
        if (!r.isNull())
            textEdit->viewport()->update(r);
    }
    else {
        Q_ASSERT(0);
    }
}

static inline bool compareTextSectionByPriority(const TextPagerSection* left, const TextPagerSection* right) {
    return left->priority() > right->priority();
}

void TextEditPrivate::updateCursorPosition(const QPoint& pos) {
    lastHoverPos      = pos;
    const int textPos = textPositionAt(pos);
    if (textPos != -1) {
        QList<TextPagerSection*> hovered = textEdit->sections(textPos, 1, TextPagerSection::IncludePartial);
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
        std::sort(hovered.begin(), hovered.end(), compareTextSectionByPriority);
#else
        qSort(hovered.begin(), hovered.end(), compareTextSectionByPriority);
#endif
        Q_FOREACH (TextPagerSection* section, hovered) {
            if (section->hasCursor()) {
                delete sectionCursor;
                sectionCursor = new QCursor(section->cursor());
                textEdit->viewport()->setCursor(*sectionCursor);
                return;
            }
        }
    }
    // no cursor
    if (sectionCursor) {
        textEdit->viewport()->setCursor(Qt::IBeamCursor);
        delete sectionCursor;
        sectionCursor = nullptr;
    }

    // To notify the line num area widget!
    Q_EMIT scrollBarChanged();
}

bool TextEditPrivate::isSectionOnScreen(const TextPagerSection* section) const {
    Q_ASSERT(section->document() == document);
    return (::matchSection(section, textEdit) && section->position() <= layoutEnd &&
            section->position() + section->size() >= viewportPosition);
}

void TextEditPrivate::cursorMoveKeyEventReadOnly(QKeyEvent* e) {
    // IR: changed MoveToPreviousPage from visibleLines to (visibleLines-2) because:
    // the scrollLines routine does not perform consistently between page up and page down;
    // performing a page down followed by a page up does not get you back to where you started.
    // See my comment in scrollLines, which might point to why this is. The quickest way
    // to make the functions consistent was to make this change. The primary reason was to
    // ensure that the user does not miss any lines of text when they page up and down through
    // the document. The same change was made in onScrollBarActionTriggered.

    if (1) {
        if (e == QKeySequence::MoveToNextLine) {
            scrollLines(1);
        }
        else if (e == QKeySequence::MoveToPreviousLine) {
            scrollLines(-1);
        }
        else if (e == QKeySequence::MoveToStartOfDocument) {
            textEdit->verticalScrollBar()->setValue(0);
        }
        else if (e == QKeySequence::MoveToEndOfDocument) {
            textEdit->verticalScrollBar()->setValue(textEdit->verticalScrollBar()->maximum());
        }
        else if (e == QKeySequence::MoveToNextPage) {
            scrollLines(qMax(1, visibleLines - 1));
        }
        else if (e == QKeySequence::MoveToPreviousPage) {
            scrollLines(-qMax(1, visibleLines - 2));
        }
        else if (e == QKeySequence::MoveToStartOfLine) {
            textCursor.movePosition(TextPagerCursor::StartOfLine);
            e->accept();
            return;
        }
        else if (e == QKeySequence::MoveToEndOfLine) {
            textCursor.movePosition(TextPagerCursor::EndOfLine);
            e->accept();
            return;
        }
        else {
            e->ignore();
            return;
        }
    }

    e->accept();
    textCursor.setPosition(viewportPosition);
}

void TextEditPrivate::relayout() {
    const QSize s = textEdit->viewport()->size();

    int widestOri = widest;

    widest = -1;
    relayoutByGeometry(s.height());
    if (widest == -1)
        widest = widestOri;

    textEdit->horizontalScrollBar()->setPageStep(s.width());
    textEdit->horizontalScrollBar()->setMaximum(qMax(0, widest - s.width()));
#ifdef _UI_TEXTPAGER_DEBUG
    qDebug() << widest << s.width() << textEdit->horizontalScrollBar()->maximum();
#endif
}

bool TextEditPrivate::dirtyForSection(TextPagerSection* section) {
    if (isSectionOnScreen(section)) {
        layoutDirty = true;
        textEdit->viewport()->update();
        return true;
    }
    else {
        return false;
    }
}

void TextEditPrivate::onTextSectionFormatChanged(TextPagerSection* section) {
    dirtyForSection(section);
}

void TextEditPrivate::onTextSectionCursorChanged(TextPagerSection* section) {
    if (isSectionOnScreen(section)) {
        updateCursorPosition(lastHoverPos);
    }
}

void TextEditPrivate::onSelectionChanged() {
    if (inMouseEvent && textCursor.hasSelection() && QApplication::clipboard()->supportsSelection()) {
        textEdit->copy(QClipboard::Selection);
    }

    textEdit->viewport()->update();
    // ### could figure out old selection rect and | it with new one
    // ### and update that but is it worth it?
    updateCopyAndCutEnabled();
}

bool TextEditPrivate::canInsertFromMimeData(const QMimeData* data) const {
    return data->hasText();
}

// Find the viewport position for the end of the document supposing the the last line
// is exactly located at the bottom of the viewport.
int TextEditPrivate::findLastPageSize() const {
    if (!document || document->documentSize() == 0)
        return -1;

#ifdef DEBUG_TEXTPAGER_LASTPAGESIZE
    qDebug() << "TextEditPrivate::findLastPageSize -->";
#endif
    TextEditPrivate p(textEdit);
    p.font                 = textEdit->font();
    p.viewportPosition     = 0;
    p.document             = document;
    const int documentSize = document->documentSize();
    p.maxViewportPosition  = documentSize;

    // The viewport height (including the height of the horizontal slider)
    int vh = p.textEdit->viewport()->height(); //-p.textEdit->horizontalScrollBar()->height();

    // Max 1.5*MinimumBufferSize can be kept in the buffer after the viewportPosition
    int maxCharNum = p.MinimumBufferSize * 1.5 - 200;

    // We use the lastpage cache to find a better start position

#ifdef DEBUG_TEXTPAGER_LASTPAGESIZE
    qDebug() << "  lastPageCache height:" << lastPage.height << "position:" << lastPage.position
             << "documentSize:" << lastPage.documentSize << "widest:" << lastPage.widest;
#endif

    if (lastPage.documentSize != documentSize) {
        lastPage.clear();
        lastPage.documentSize = documentSize;
    }

    Direction sizeChangeDir = Forward;
    if (lastPage.height != -1) {
        if (lastPage.height > vh) {
            maxCharNum    = documentSize - lastPage.position + 1;
            sizeChangeDir = Backward;
        }
        else if (lastPage.height < vh) {
            maxCharNum    = documentSize - lastPage.position + 1000;
            sizeChangeDir = Backward;
        }
        else {
            return lastPage.position;
        }
    }

    lastPage.height = vh;

    Q_ASSERT(maxCharNum >= 0);

    int start = documentSize - maxCharNum;
    if (start < 0)
        start = 0;
#ifdef DEBUG_TEXTPAGER_LASTPAGESIZE
    QTime tm;
    tm.start();
#endif

    // Try to find the last page by iteration
    int step          = 0;
    const int maxStep = 10;
    bool reachedTop   = (start == 0);

    while (step < maxStep) {
#ifdef DEBUG_TEXTPAGER_LASTPAGESIZE
        qDebug() << "  ITERATION STEP:" << step;
        qDebug() << "  start:" << start << "maxCharNum:" << maxCharNum;
#endif
        // We get the viewportPosition closest to start
        p.updateViewportPosition(start, sizeChangeDir, false);

#ifdef DEBUG_TEXTPAGER_LASTPAGESIZE
        qDebug() << "  after updateviewport> start-viewportPos:" << start - p.viewportPosition;
#endif
        // adjust the max number of characters
        if (start > p.viewportPosition)
            maxCharNum += (start - p.viewportPosition) + 1;

        // Relayout using the max number of characters
        p.relayoutByPosition(maxCharNum);

#ifdef DEBUG_TEXTPAGER_LASTPAGESIZE
        qDebug() << "  vh:" << vh << "fontSize:" << font.pointSize();
        qDebug() << "  viewport size" << textEdit->viewport()->size();
        qDebug() << "  layoutEnd:" << p.layoutEnd << "documentSize:" << documentSize
                 << "viewportPosition:" << p.viewportPosition << p.contentRect;
        qDebug() << "  bottom:" << p.textLayouts.last()->boundingRect().bottom();
#endif

        // The end of the layout should be the end of the document
        if (p.layoutEnd == documentSize && p.textLayouts.count() > 0) {

            int top = p.textLayouts.last()->boundingRect().bottom() + 4 - vh;
            int pos = documentSize - p.textLayouts.last()->text().size();

            for (int i = p.textLayouts.count() - 2; i >= 0; i--) {

                if (p.textLayouts.at(i)->boundingRect().top() <= top) {
#ifdef DEBUG_TEXTPAGER_LASTPAGESIZE
                    qDebug() << "  find position:" << pos; // << "line:" << p.document->lineNumber(pos);
#endif
                    p.updateViewportPosition(pos, Backward, false);
#ifdef DEBUG_TEXTPAGER_LASTPAGESIZE
                    qDebug() << "  viewPortPosition:" << p.viewportPosition
                             << "time:" << tm.elapsed(); // << "line:" << p.document->lineNumber(p.viewportPosition);
#endif
                    lastPage.position = p.viewportPosition;
                    lastPage.widest   = p.widest;
                    return p.viewportPosition;
                }
                pos -= p.textLayouts.at(i)->text().size();
            }
        }
        // We could not find the last page. We try again from a larger number of characters from the
        // end of the document
        step++;
        maxCharNum += 1000;
        start = documentSize - maxCharNum;

        if (start < 0 && reachedTop)
            break;

        if (start < 0) {
            reachedTop = true;
            start      = 0;
        }
    }

    lastPage.clear();

    // the whole text is in the viewport, no vertical scrollbar is needed
    if (reachedTop)
        return -1;

    // If we are here we the last page position will be the end of the document and there will be
    // a white space block after the last row in the editor.
    // TODO: improve it!
    return documentSize;
}

void TextEditPrivate::toDocStart() {
    textCursor.movePosition(TextPagerCursor::Start);
}

void TextEditPrivate::toDocEnd() {
    textCursor.movePosition(TextPagerCursor::End);
}

void TextEditPrivate::toLineStart() {
    textCursor.movePosition(TextPagerCursor::StartOfLine);
}

void TextEditPrivate::toLineEnd() {
    textCursor.movePosition(TextPagerCursor::EndOfLine);
}

void TextPagerEdit::setSyntaxHighlighter(SyntaxHighlighter* h) {
    if (h && h->textEdit() == this) {
        if (d->syntaxHighlighters.size() == 1)
            return;
        takeSyntaxHighlighter(h);
    }
    clearSyntaxHighlighters();
    if (h) {
        addSyntaxHighlighter(h);
    }
}

void TextPagerEdit::setEnableSearchHighlighter(bool b) {
    useSearchHighlight_ = b;
    if (useSearchHighlight_) {
        setSyntaxHighlighter(searchHighlight_);
    }
    else {
        // searchHighlight_ can only be deleted when the editor is deleted.
        // Because clearSyntaxHighlighters() deletes the highlighters  we need to use
        // takeSyntaxHighlighter() instead.
        if (d->syntaxHighlighters.contains(searchHighlight_)) {
            takeSyntaxHighlighter(searchHighlight_);
            d->layoutDirty = true;
            viewport()->update();
            // clearSyntaxHighlighters();
        }
    }
}

void TextPagerEdit::clearSearchHighlighter() {
    searchHighlight_->clear();
}

void TextPagerEdit::setSearchHighlighter(QString txt, TextPagerDocument::FindMode mode) {
    searchHighlight_->reset(txt, mode, useSearchHighlight_);
}

void TextPagerEdit::setSearchHighlighter(QRegExp rx, TextPagerDocument::FindMode mode) {
    searchHighlight_->reset(rx, mode, useSearchHighlight_);
}

void TextPagerEdit::gotoLine(int lineNum) {
    TextPagerCursor cursor = d->document->findLine(lineNum, textCursor());
    if (!cursor.isNull())
        setTextCursor(cursor);
}

//---------------------------------------------
// Navigation
//---------------------------------------------

void TextPagerEdit::toDocStart() {
    d->toDocStart();
}

void TextPagerEdit::toDocEnd() {
    d->toDocEnd();
}

void TextPagerEdit::toLineStart() {
    d->toLineStart();
}

void TextPagerEdit::toLineEnd() {
    d->toLineEnd();
}

//---------------------------------------------
// Fontsize management
//---------------------------------------------

void TextPagerEdit::setFontProperty(VProperty* p) {
    fontProp_ = p;
    fontProp_->addObserver(this);
    updateFont();
}

void TextPagerEdit::zoomIn() {
    QFont f = font();
    int fps = f.pointSize();
    f.setPointSize(fps + 1);
    setFont(f);

    fontSizeChangedByZoom();
}

void TextPagerEdit::zoomOut() {
    int oriSize = font().pointSize();

    QFont f = font();
    int fps = f.pointSize();
    if (fps > 1) {
        f.setPointSize(fps - 1);
        setFont(f);
    }

    if (font().pointSize() != oriSize)
        fontSizeChangedByZoom();
}

void TextPagerEdit::fontSizeChangedByZoom() {
    if (fontProp_)
        fontProp_->setValue(font());
}

void TextPagerEdit::updateFont() {
    if (fontProp_) {
        QFont f = fontProp_->value().value<QFont>();
        if (font() != f)
            setFont(f);
    }
}

void TextPagerEdit::notifyChange(VProperty* p) {
    if (fontProp_ == p) {
        setFont(p->value().value<QFont>());
    }
}

void TextPagerEdit::updateLineNumberArea() {
    if (lineNumArea_ && showLineNum_)
        lineNumArea_->update();
}

void TextPagerEdit::setShowLineNumbers(bool b) {
    showLineNum_ = b;
    if (lineNumArea_) {
        lineNumArea_->setVisible(b);
        // Initialise the width
        lineNumArea_->updateWidth();
    }
}

void TextPagerEdit::setLineNumberArea(TextPagerLineNumberArea* a) {
    lineNumArea_ = a;
    connect(d, SIGNAL(scrollBarChanged()), lineNumArea_, SLOT(update()));

    // Initialise the width
    lineNumArea_->updateWidth();
}

void TextPagerEdit::lineNumberAreaPaintEvent(QPaintEvent* e) {
    if (!lineNumArea_ || !showLineNum_)
        return;

    QPainter painter(lineNumArea_);
    const QRect er = e->rect();
    // painter.translate(-horizontalScrollBar()->value(), 0);
    // painter.setFont(font());

    QVector<QTextLayout::FormatRange> selections;
    selections.reserve(d->extraSelections.size() + 1);
    int textLayoutOffset = d->viewportPosition;

    QRect numRect = er;

    // Background and border
    painter.fillRect(numRect, lineNumArea_->bgColour());
    painter.setPen(QPen(lineNumArea_->separatorColour()));
    painter.drawLine(lineNumArea_->width() - 1, er.y(), lineNumArea_->width() - 1, er.bottom());

    int numWidth = lineNumArea_->width() - lineNumArea_->rightMargin();

    QFont fontNormal(font());   // the font to use for most line numbers
    QFont fontBold(fontNormal); // the font to use for the current line number
    fontBold.setBold(true);
    painter.setPen(lineNumArea_->fontColour());
    painter.setFont(fontNormal);

    int cursorPos = d->textCursor.position();
    bool last     = d->document->documentSize() == d->textCursor.position();

    // const QTextLayout *cursorLayout = d->cursorVisible ? d->layoutForPosition(d->textCursor.position()) : 0;
    // int extraSelectionIndex = 0;
    QTextLayout::FormatRange selectionRange;
    selectionRange.start = -1;
    int maxLineNum       = -1;
    Q_FOREACH (QTextLayout* l, d->textLayouts) {
        const int textSize = l->text().size();
        const QRect r      = l->boundingRect().toRect();
        if (r.intersects(er)) {

            const int lineNum = lineNumber(textLayoutOffset) + 1;
            maxLineNum        = lineNum;
            QRect lRect(0, r.y(), numWidth, r.height());

            // is this the current line?
            if (cursorPos >= textLayoutOffset &&
                (cursorPos <= textLayoutOffset + textSize || (last && textLayoutOffset + textSize + 1 == cursorPos))) {
                painter.setFont(fontBold);
                painter.fillRect(lRect, lineNumArea_->currentColour()); // highlight the background
                painter.drawText(lRect, QString::number(lineNum), Qt::AlignRight | Qt::AlignVCenter);
                painter.setFont(fontNormal);
            }
            else {
                painter.drawText(lRect, QString::number(lineNum), Qt::AlignRight | Qt::AlignVCenter);
            }
        }
        else if (r.top() > er.bottom()) {
            break;
        }
        textLayoutOffset += textSize + 1;
    }

    if (maxLineNum != -1)
        lineNumArea_->updateWidth(maxLineNum);
}

//==========================================================================
//
// TextPagerLineNumberArea
//
//==========================================================================

TextPagerLineNumberArea::TextPagerLineNumberArea(TextPagerEdit* editor)
    : QWidget(editor),
      textEditor_(editor),
      digits_(6),
      rightMargin_(3),
      bgCol_(232, 231, 230),
      fontCol_(220, 220, 200),
      separatorCol_(220, 220, 200),
      currentCol_(212, 212, 255) {
    Q_ASSERT(textEditor_);

    if (VProperty* p = VConfig::instance()->find("view.textEdit.numAreaBackground"))
        bgCol_ = p->value().value<QColor>();

    if (VProperty* p = VConfig::instance()->find("view.textEdit.numAreaFontColour"))
        fontCol_ = p->value().value<QColor>();

    if (VProperty* p = VConfig::instance()->find("view.textEdit.numAreaSeparator"))
        separatorCol_ = p->value().value<QColor>();

    if (VProperty* p = VConfig::instance()->find("view.textEdit.numAreaCurrent"))
        currentCol_ = p->value().value<QColor>();

    editor->setLineNumberArea(this);
}

void TextPagerLineNumberArea::updateWidth(int maxLineNum) {
    if (maxLineNum == -1 || pow(10, digits_) - 1 < maxLineNum) {
        setFixedWidth(computeWidth(maxLineNum));
    }
}

int TextPagerLineNumberArea::computeWidth(int maxLineNum) const {
    if (maxLineNum > 0) {
        int maxDigits = 1;
        int mx        = maxLineNum;
        while (mx >= 10) {
            mx /= 10;
            ++maxDigits;
        }

        if (maxDigits > digits_)
            digits_ = maxDigits;
    }

    return (textEditor_)
               ? (3 + ViewerUtil::textWidth(textEditor_->fontMetrics(), QLatin1Char('9')) * digits_ + rightMargin_)
               : 3 + rightMargin_;
}
