#include "PageView.h"

#include <algorithm>
#include <cmath>
#include <cstdlib>
#include <memory>

#include <gdk/gdk.h>

#include "control/Control.h"
#include "control/SearchControl.h"
#include "control/jobs/BlockingJob.h"
#include "control/settings/ButtonConfig.h"
#include "control/settings/Settings.h"
#include "control/tools/ArrowHandler.h"
#include "control/tools/CoordinateSystemHandler.h"
#include "control/tools/EllipseHandler.h"
#include "control/tools/EraseHandler.h"
#include "control/tools/ImageHandler.h"
#include "control/tools/InputHandler.h"
#include "control/tools/RectangleHandler.h"
#include "control/tools/RulerHandler.h"
#include "control/tools/Selection.h"
#include "control/tools/SplineHandler.h"
#include "control/tools/StrokeHandler.h"
#include "control/tools/VerticalToolHandler.h"
#include "model/Image.h"
#include "model/Layer.h"
#include "model/PageRef.h"
#include "model/Stroke.h"
#include "model/Text.h"
#include "undo/DeleteUndoAction.h"
#include "undo/InsertUndoAction.h"
#include "undo/TextBoxUndoAction.h"
#include "util/XojMsgBox.h"
#include "view/TextView.h"
#include "widgets/XournalWidget.h"

#include "PageViewFindObjectHelper.h"
#include "Range.h"
#include "Rectangle.h"
#include "RepaintHandler.h"
#include "TextEditor.h"
#include "XournalView.h"
#include "XournalppCursor.h"
#include "config-debug.h"
#include "config-features.h"
#include "config.h"
#include "i18n.h"
#include "pixbuf-utils.h"

using std::string;

XojPageView::XojPageView(XournalView* xournal, const PageRef& page) {
    this->page = page;
    this->registerListener(this->page);
    this->xournal = xournal;
    this->settings = xournal->getControl()->getSettings();

    // this does not have to be deleted afterwards:
    // (we need it for undo commands)
    this->oldtext = nullptr;

    this->eraser = new EraseHandler(xournal->getControl()->getUndoRedoHandler(), xournal->getControl()->getDocument(),
                                    this->page, xournal->getControl()->getToolHandler(), this);
}

XojPageView::~XojPageView() {
    // Unregister listener before destroying this handler
    this->unregisterListener();

    this->xournal->getControl()->getScheduler()->removePage(this);
    delete this->inputHandler;
    delete this->eraser;
    endText();
    deleteViewBuffer();
    delete this->search;
}

void XojPageView::setIsVisible(bool visible) {
    if (visible) {
        this->lastVisibleTime = 0;
    } else if (this->lastVisibleTime <= 0) {
        GTimeVal val;
        g_get_current_time(&val);
        this->lastVisibleTime = val.tv_sec;
    }
}

auto XojPageView::getLastVisibleTime() -> int {
    if (this->crBuffer == nullptr) {
        return -1;
    }

    return this->lastVisibleTime;
}

void XojPageView::deleteViewBuffer() {
    this->drawingMutex.lock();
    if (this->crBuffer) {
        cairo_surface_destroy(this->crBuffer);
        this->crBuffer = nullptr;
    }
    this->drawingMutex.unlock();
}

auto XojPageView::containsPoint(int x, int y, bool local) const -> bool {
    if (!local) {
        bool leftOk = this->getX() <= x;
        bool rightOk = x <= this->getX() + this->getDisplayWidth();
        bool topOk = this->getY() <= y;
        bool bottomOk = y <= this->getY() + this->getDisplayHeight();

        return leftOk && rightOk && topOk && bottomOk;
    }


    return x >= 0 && y >= 0 && x <= this->getWidth() && y <= this->getHeight();
}

auto XojPageView::searchTextOnPage(string& text, int* occures, double* top) -> bool {
    if (this->search == nullptr) {
        if (text.empty()) {
            return true;
        }

        auto pNr = this->page->getPdfPageNr();
        XojPdfPageSPtr pdf = nullptr;
        if (pNr != npos) {
            Document* doc = xournal->getControl()->getDocument();

            doc->lock();
            pdf = doc->getPdfPage(pNr);
            doc->unlock();
        }
        this->search = new SearchControl(page, pdf);
    }

    bool found = this->search->search(text, occures, top);

    repaintPage();

    return found;
}

void XojPageView::endText() {
    if (!this->textEditor) {
        return;
    }
    Text* txt = this->textEditor->getText();
    Layer* layer = this->page->getSelectedLayer();
    UndoRedoHandler* undo = xournal->getControl()->getUndoRedoHandler();

    // Text deleted
    if (txt->getText().empty()) {
        // old element
        int pos = layer->indexOf(txt);
        if (pos != -1) {
            auto eraseDeleteUndoAction = std::make_unique<DeleteUndoAction>(page, true);
            layer->removeElement(txt, false);
            eraseDeleteUndoAction->addElement(layer, txt, pos);
            undo->addUndoAction(std::move(eraseDeleteUndoAction));
        }
    } else {
        // new element
        if (layer->indexOf(txt) == -1) {
            undo->addUndoActionBefore(std::make_unique<InsertUndoAction>(page, layer, txt),
                                      this->textEditor->getFirstUndoAction());
            layer->addElement(txt);
            this->textEditor->textCopyed();
        }
        // or if the file was saved and reopened
        // and/or if we click away from the text window
        else {
            // TextUndoAction does not work because the textEdit object is destroyed
            // after endText() so we need to instead copy the information between an
            // old and new element that we can push and pop to recover.
            undo->addUndoAction(std::make_unique<TextBoxUndoAction>(page, layer, txt, this->oldtext));
        }
    }

    delete this->textEditor;
    this->textEditor = nullptr;
    this->xournal->getControl()->getWindow()->setFontButtonFont(settings->getFont());
    this->rerenderPage();
}

void XojPageView::startText(double x, double y) {
    this->xournal->endTextAllPages(this);
    this->xournal->getControl()->getSearchBar()->showSearchBar(false);

    if (this->textEditor != nullptr) {
        Text* text = this->textEditor->getText();
        GdkRectangle matchRect = {gint(x), gint(y), 1, 1};
        if (!text->intersectsArea(&matchRect)) {
            endText();
        } else {
            this->textEditor->mousePressed(x - text->getX(), y - text->getY());
        }
    }

    if (this->textEditor == nullptr) {
        // Is there already a textfield?
        Text* text = nullptr;

        for (Element* e: *this->page->getSelectedLayer()->getElements()) {
            if (e->getType() == ELEMENT_TEXT) {
                GdkRectangle matchRect = {gint(x), gint(y), 1, 1};
                if (e->intersectsArea(&matchRect)) {
                    text = dynamic_cast<Text*>(e);
                    break;
                }
            }
        }

        bool ownText = false;
        if (text == nullptr) {
            ToolHandler* h = xournal->getControl()->getToolHandler();
            ownText = true;
            text = new Text();
            text->setColor(h->getColor());
            text->setFont(settings->getFont());
            text->setX(x);
            text->setY(y - text->getElementHeight() / 2);

            if (xournal->getControl()->getAudioController()->isRecording()) {
                string audioFilename = xournal->getControl()->getAudioController()->getAudioFilename();
                size_t sttime = xournal->getControl()->getAudioController()->getStartTime();
                size_t milliseconds = ((g_get_monotonic_time() / 1000) - sttime);
                text->setTimestamp(milliseconds);
                text->setAudioFilename(audioFilename);
            }
        } else {

            // We can try to add an undo action here. The initial text shows up in this
            // textEditor element.
            this->oldtext = text;
            // text = new Text(*oldtext);
            // need to clone the old text so that references still work properly.
            // cloning breaks things a little. do it manually
            text = new Text();
            text->setX(oldtext->getX());
            text->setY(oldtext->getY());
            text->setColor(oldtext->getColor());
            text->setFont(oldtext->getFont());
            this->xournal->getControl()->getWindow()->setFontButtonFont(oldtext->getFont());
            text->setText(oldtext->getText());
            text->setTimestamp(oldtext->getTimestamp());
            text->setAudioFilename(oldtext->getAudioFilename());

            Layer* layer = this->page->getSelectedLayer();
            layer->removeElement(this->oldtext, false);
            layer->addElement(text);
            // perform the old swap onto the new text drawn.
        }

        this->textEditor = new TextEditor(this, xournal->getWidget(), text, ownText);
        if (!ownText) {
            this->textEditor->mousePressed(x - text->getX(), y - text->getY());
        }

        this->rerenderElement(text);
    }
}

auto XojPageView::onButtonPressEvent(const PositionInputData& pos) -> bool {
    Control* control = xournal->getControl();

    if (!this->selected) {
        control->firePageSelected(this->page);
    }

    ToolHandler* h = control->getToolHandler();

    double x = pos.x;
    double y = pos.y;

    if (x < 0 || y < 0) {
        return false;
    }

    double zoom = xournal->getZoom();
    x /= zoom;
    y /= zoom;

    XournalppCursor* cursor = xournal->getCursor();
    cursor->setMouseDown(true);

    if ((h->getToolType() == TOOL_PEN || h->getToolType() == TOOL_HIGHLIGHTER) &&
                h->getDrawingType() != DRAWING_TYPE_SPLINE ||
        (h->getToolType() == TOOL_ERASER && h->getEraserType() == ERASER_TYPE_WHITEOUT)) {
        delete this->inputHandler;
        this->inputHandler = nullptr;

        if (h->getDrawingType() == DRAWING_TYPE_LINE) {
            this->inputHandler = new RulerHandler(this->xournal, this, getPage());
        } else if (h->getDrawingType() == DRAWING_TYPE_RECTANGLE) {
            this->inputHandler = new RectangleHandler(this->xournal, this, getPage());
        } else if (h->getDrawingType() == DRAWING_TYPE_ELLIPSE) {
            this->inputHandler = new EllipseHandler(this->xournal, this, getPage());
        } else if (h->getDrawingType() == DRAWING_TYPE_ARROW) {
            this->inputHandler = new ArrowHandler(this->xournal, this, getPage());
        } else if (h->getDrawingType() == DRAWING_TYPE_COORDINATE_SYSTEM) {
            this->inputHandler = new CoordinateSystemHandler(this->xournal, this, getPage());
        } else {
            this->inputHandler = new StrokeHandler(this->xournal, this, getPage());
        }

        this->inputHandler->onButtonPressEvent(pos);
    } else if ((h->getToolType() == TOOL_PEN || h->getToolType() == TOOL_HIGHLIGHTER) &&
               h->getDrawingType() == DRAWING_TYPE_SPLINE) {
        if (!this->inputHandler) {
            this->inputHandler = new SplineHandler(this->xournal, this, getPage());
        }
        this->inputHandler->onButtonPressEvent(pos);
    } else if (h->getToolType() == TOOL_ERASER) {
        this->eraser->erase(x, y);
        this->inEraser = true;
    } else if (h->getToolType() == TOOL_VERTICAL_SPACE) {
        this->verticalSpace = new VerticalToolHandler(this, this->page, this->settings, y, zoom);
    } else if (h->getToolType() == TOOL_SELECT_RECT || h->getToolType() == TOOL_SELECT_REGION ||
               h->getToolType() == TOOL_PLAY_OBJECT || h->getToolType() == TOOL_SELECT_OBJECT) {
        if (h->getToolType() == TOOL_SELECT_RECT) {
            if (this->selection) {
                delete this->selection;
                this->selection = nullptr;
                repaintPage();
            }
            this->selection = new RectSelection(x, y, this);
        } else if (h->getToolType() == TOOL_SELECT_REGION) {
            if (this->selection) {
                delete this->selection;
                this->selection = nullptr;
                repaintPage();
            }
            this->selection = new RegionSelect(x, y, this);
        } else if (h->getToolType() == TOOL_SELECT_OBJECT) {
            SelectObject select(this);
            select.at(x, y);
        } else if (h->getToolType() == TOOL_PLAY_OBJECT) {
            PlayObject play(this);
            play.at(x, y);
            if (play.playbackStatus) {
                auto& status = *play.playbackStatus;
                if (!status.success) {
                    string message = FS(_F("Unable to play audio recording {1}") % status.filename);
                    XojMsgBox::showErrorToUser(this->xournal->getControl()->getGtkWindow(), message);
                }
            }
        }
    } else if (h->getToolType() == TOOL_TEXT) {
        startText(x, y);
    } else if (h->getToolType() == TOOL_IMAGE) {
        ImageHandler imgHandler(control, this);
        imgHandler.insertImage(x, y);
    } else if (h->getToolType() == TOOL_FLOATING_TOOLBOX) {
        gint wx = 0, wy = 0;
        GtkWidget* widget = xournal->getWidget();
        gtk_widget_translate_coordinates(widget, gtk_widget_get_toplevel(widget), 0, 0, &wx, &wy);

        wx += std::lround(pos.x) + this->getX();
        wy += std::lround(pos.y) + this->getY();

        control->getWindow()->floatingToolbox->show(wx, wy);
    }

    return true;
}

auto XojPageView::onButtonDoublePressEvent(const PositionInputData& pos) -> bool {
    // This method assumes that it is called after onButtonPressEvent but before
    // onButtonReleaseEvent
    double zoom = this->xournal->getZoom();
    double x = pos.x / zoom;
    double y = pos.y / zoom;
    if (x < 0 || y < 0) {
        return false;
    }

    ToolHandler* toolHandler = this->xournal->getControl()->getToolHandler();
    ToolType toolType = toolHandler->getToolType();
    bool isSelectTool = toolType == TOOL_SELECT_OBJECT || TOOL_SELECT_RECT || TOOL_SELECT_REGION;

    DrawingType drawingType = toolHandler->getDrawingType();

    EditSelection* selection = xournal->getSelection();
    bool hasNoModifiers = !pos.isShiftDown() && !pos.isControlDown();

    if (hasNoModifiers && isSelectTool && selection != nullptr) {
        // Find a selected object under the cursor, if possible. The selection doesn't change the
        // element coordinates until it is finalized, so we need to use position relative to the
        // original coordinates of the selection.
        double origx = x - (selection->getXOnView() - selection->getOriginalXOnView());
        double origy = y - (selection->getYOnView() - selection->getOriginalYOnView());
        std::vector<Element*>* elems = selection->getElements();
        auto it = std::find_if(elems->begin(), elems->end(),
                               [&](Element*& elem) { return elem->intersectsArea(origx - 5, origy - 5, 5, 5); });
        if (it != elems->end()) {
            // Enter editing mode on the selected object
            Element* object = *it;
            ElementType elemType = object->getType();
            if (elemType == ELEMENT_TEXT) {
                this->xournal->clearSelection();
                toolHandler->selectTool(TOOL_TEXT);
                toolHandler->fireToolChanged();
                // Simulate a button press; there's too many things that we
                // could forget to do if we manually call startText
                this->onButtonPressEvent(pos);
            } else if (elemType == ELEMENT_TEXIMAGE) {
                Control* control = this->xournal->getControl();
                this->xournal->clearSelection();
                auto* sel = new EditSelection(control->getUndoRedoHandler(), object, this, this->getPage());
                this->xournal->setSelection(sel);
                control->runLatex();
            }
        }
    } else if (toolType == TOOL_TEXT) {
        this->startText(x, y);
        this->textEditor->selectAtCursor(TextEditor::SelectType::word);
    } else if (drawingType == DRAWING_TYPE_SPLINE) {
        if (this->inputHandler) {
            this->inputHandler->onButtonDoublePressEvent(pos);
            delete this->inputHandler;
            this->inputHandler = nullptr;
        }
    }

    return true;
}

auto XojPageView::onButtonTriplePressEvent(const PositionInputData& pos) -> bool {
    // This method assumes that it is called after onButtonDoubleEvent but before
    // onButtonReleaseEvent
    double zoom = this->xournal->getZoom();
    double x = pos.x / zoom;
    double y = pos.y / zoom;
    if (x < 0 || y < 0) {
        return false;
    }

    ToolHandler* toolHandler = this->xournal->getControl()->getToolHandler();

    if (toolHandler->getToolType() == TOOL_TEXT) {
        this->startText(x, y);
        this->textEditor->selectAtCursor(TextEditor::SelectType::paragraph);
    }
    return true;
}

void XojPageView::resetShapeRecognizer() {
    if (this->inputHandler != nullptr) {
        this->inputHandler->resetShapeRecognizer();
    }
}

auto XojPageView::onMotionNotifyEvent(const PositionInputData& pos) -> bool {
    double zoom = xournal->getZoom();
    double x = pos.x / zoom;
    double y = pos.y / zoom;

    ToolHandler* h = xournal->getControl()->getToolHandler();

    if (containsPoint(std::lround(x), std::lround(y), true) && this->inputHandler &&
        this->inputHandler->onMotionNotifyEvent(pos)) {
        // input handler used this event
    } else if (this->selection) {
        this->selection->currentPos(x, y);
    } else if (this->verticalSpace) {
        this->verticalSpace->currentPos(x, y);
    } else if (this->textEditor) {
        XournalppCursor* cursor = getXournal()->getCursor();
        cursor->setInvisible(false);

        Text* text = this->textEditor->getText();
        this->textEditor->mouseMoved(x - text->getX(), y - text->getY());
    } else if (h->getToolType() == TOOL_ERASER && h->getEraserType() != ERASER_TYPE_WHITEOUT && this->inEraser) {
        this->eraser->erase(x, y);
    }

    return false;
}

void XojPageView::onMotionCancelEvent() {
    if (this->inputHandler) {
        this->inputHandler->onMotionCancelEvent();
    }
}

auto XojPageView::onButtonReleaseEvent(const PositionInputData& pos) -> bool {
    Control* control = xournal->getControl();

    if (this->inputHandler) {
        this->inputHandler->onButtonReleaseEvent(pos);

        if (this->inputHandler->userTapped) {
            bool doAction = control->getSettings()->getDoActionOnStrokeFiltered();
            if (control->getSettings()->getTrySelectOnStrokeFiltered()) {
                double zoom = xournal->getZoom();
                SelectObject select(this);
                if (select.at(pos.x / zoom, pos.y / zoom)) {
                    doAction = false;  // selection made.. no action.
                }
            }

            if (doAction)  // pop up a menu
            {
                gint wx = 0, wy = 0;
                GtkWidget* widget = xournal->getWidget();
                gtk_widget_translate_coordinates(widget, gtk_widget_get_toplevel(widget), 0, 0, &wx, &wy);
                wx += std::lround(pos.x + this->getX());
                wy += std::lround(pos.y + this->getY());
                control->getWindow()->floatingToolbox->show(wx, wy);
            }
        }

        ToolHandler* h = control->getToolHandler();
        bool isDrawingTypeSpline = h->getDrawingType() == DRAWING_TYPE_SPLINE;
        if (!isDrawingTypeSpline || !this->inputHandler->getStroke()) {  // The Spline Tool finalizes drawing manually
            delete this->inputHandler;
            this->inputHandler = nullptr;
        }
    }

    if (this->inEraser) {
        this->inEraser = false;
        Document* doc = this->xournal->getControl()->getDocument();
        doc->lock();
        this->eraser->finalize();
        doc->unlock();
    }

    if (this->verticalSpace) {
        control->getUndoRedoHandler()->addUndoAction(this->verticalSpace->finalize());
        delete this->verticalSpace;
        this->verticalSpace = nullptr;
    }

    if (this->selection) {
        if (this->selection->finalize(this->page)) {
            xournal->setSelection(new EditSelection(control->getUndoRedoHandler(), this->selection, this));
            delete this->selection;
            this->selection = nullptr;
        } else {
            double zoom = xournal->getZoom();
            if (this->selection->userTapped(zoom)) {
                SelectObject select(this);
                select.at(pos.x / zoom, pos.y / zoom);
            }
            delete this->selection;
            this->selection = nullptr;

            repaintPage();
        }
    } else if (this->textEditor) {
        this->textEditor->mouseReleased();
    }

    return false;
}

auto XojPageView::onKeyPressEvent(GdkEventKey* event) -> bool {
    // Esc leaves text edition
    if (event->keyval == GDK_KEY_Escape) {
        if (this->textEditor) {
            endText();
            return true;
        }
        if (xournal->getSelection()) {
            xournal->clearSelection();
            return true;
        }

        return false;
    }

    if (this->textEditor) {
        return this->textEditor->onKeyPressEvent(event);
    }


    if (this->inputHandler) {
        return this->inputHandler->onKeyEvent(event);
    }


    return false;
}

auto XojPageView::onKeyReleaseEvent(GdkEventKey* event) -> bool {
    if (this->textEditor && this->textEditor->onKeyReleaseEvent(event)) {
        return true;
    }

    if (this->inputHandler && this->inputHandler->onKeyEvent(event)) {
        DrawingType drawingType = this->xournal->getControl()->getToolHandler()->getDrawingType();
        if (drawingType == DRAWING_TYPE_SPLINE) {  // Spline drawing has been finalized
            if (this->inputHandler) {
                delete this->inputHandler;
                this->inputHandler = nullptr;
            }
        }

        return true;
    }

    return false;
}

void XojPageView::rerenderPage() {
    this->rerenderComplete = true;
    this->xournal->getControl()->getScheduler()->addRerenderPage(this);
}

void XojPageView::repaintPage() { xournal->getRepaintHandler()->repaintPage(this); }

void XojPageView::repaintArea(double x1, double y1, double x2, double y2) {
    double zoom = xournal->getZoom();
    xournal->getRepaintHandler()->repaintPageArea(this, std::lround(x1 * zoom) - 10, std::lround(y1 * zoom) - 10,
                                                  std::lround(x2 * zoom) + 20, std::lround(y2 * zoom) + 20);
}

void XojPageView::rerenderRect(double x, double y, double width, double height) {
    int rx = std::lround(std::max(x - 10, 0.0));
    int ry = std::lround(std::max(y - 10, 0.0));
    int rwidth = std::lround(width + 20);
    int rheight = std::lround(height + 20);

    addRerenderRect(rx, ry, rwidth, rheight);
}

void XojPageView::addRerenderRect(double x, double y, double width, double height) {
    if (this->rerenderComplete) {
        return;
    }

    auto rect = Rectangle<double>{x, y, width, height};

    this->repaintRectMutex.lock();

    for (auto&& r: this->rerenderRects) {
        // its faster to redraw only one rect than repaint twice the same area
        // so loop through the rectangles to be redrawn, if new rectangle
        // intersects any of them, replace it by the union with the new one
        if (r.intersects(rect)) {
            r.unite(rect);
            this->repaintRectMutex.unlock();
            return;
        }
    }

    this->rerenderRects.push_back(rect);
    this->repaintRectMutex.unlock();

    this->xournal->getControl()->getScheduler()->addRerenderPage(this);
}

void XojPageView::setSelected(bool selected) {
    this->selected = selected;

    if (selected) {
        this->xournal->requestFocus();
        this->xournal->getRepaintHandler()->repaintPageBorder(this);
    }
}

auto XojPageView::cut() -> bool {
    if (this->textEditor) {
        this->textEditor->cutToClipboard();
        return true;
    }
    return false;
}

auto XojPageView::copy() -> bool {
    if (this->textEditor) {
        this->textEditor->copyToCliboard();
        return true;
    }
    return false;
}

auto XojPageView::paste() -> bool {
    if (this->textEditor) {
        this->textEditor->pasteFromClipboard();
        return true;
    }
    return false;
}

auto XojPageView::actionDelete() -> bool {
    if (this->textEditor) {
        this->textEditor->deleteFromCursor(GTK_DELETE_CHARS, 1);
        return true;
    }
    return false;
}

void XojPageView::drawLoadingPage(cairo_t* cr) {
    static const string txtLoading = _("Loading...");

    double zoom = xournal->getZoom();
    int dispWidth = getDisplayWidth();
    int dispHeight = getDisplayHeight();

    cairo_set_source_rgb(cr, 1, 1, 1);
    cairo_rectangle(cr, 0, 0, dispWidth, dispHeight);
    cairo_fill(cr);

    cairo_scale(cr, zoom, zoom);

    cairo_set_source_rgb(cr, 0.5, 0.5, 0.5);
    cairo_select_font_face(cr, "Sans", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_BOLD);
    cairo_set_font_size(cr, 32.0);
    cairo_text_extents_t ex;
    cairo_text_extents(cr, txtLoading.c_str(), &ex);
    cairo_move_to(cr, (page->getWidth() - ex.width) / 2 - ex.x_bearing,
                  (page->getHeight() - ex.height) / 2 - ex.y_bearing);
    cairo_show_text(cr, txtLoading.c_str());

    rerenderPage();
}

/**
 * Does the painting, called in synchronized block
 */
void XojPageView::paintPageSync(cairo_t* cr, GdkRectangle* rect) {
    if (this->crBuffer == nullptr) {
        drawLoadingPage(cr);
        return;
    }

    double zoom = xournal->getZoom();
    int dispWidth = getDisplayWidth();

    cairo_save(cr);

    double width = cairo_image_surface_get_width(this->crBuffer);

    bool rerender = true;
    if (width / xournal->getDpiScaleFactor() == dispWidth) {
        rerender = false;
    }

    if (width != dispWidth) {
        double scale = (static_cast<double>(dispWidth)) / (width);

        // Scale current image to fit the zoom level
        cairo_scale(cr, scale, scale);
        cairo_pattern_set_filter(cairo_get_source(cr), CAIRO_FILTER_FAST);

        cairo_set_source_surface(cr, this->crBuffer, 0, 0);

        if (rerender) {
            rerenderPage();
        }

        rect = nullptr;
    } else {
        cairo_set_source_surface(cr, this->crBuffer, 0, 0);
    }

    if (rect) {
        cairo_rectangle(cr, rect->x, rect->y, rect->width, rect->height);
        cairo_fill(cr);

#ifdef DEBUG_SHOW_PAINT_BOUNDS
        cairo_set_source_rgb(cr, 1.0, 0.5, 1.0);
        cairo_set_line_width(cr, 1. / zoom);
        cairo_rectangle(cr, rect->x, rect->y, rect->width, rect->height);
        cairo_stroke(cr);
#endif
    } else {
        cairo_paint(cr);
    }

    cairo_restore(cr);

    // don't paint this with scale, because it needs a 1:1 zoom
    if (this->verticalSpace) {
        this->verticalSpace->paint(cr, rect, zoom);
    }

    if (this->textEditor) {
        cairo_scale(cr, zoom, zoom);
        this->textEditor->paint(cr, rect, zoom);
    }
    if (this->selection) {
        cairo_scale(cr, zoom, zoom);
        this->selection->paint(cr, rect, zoom);
    }

    if (this->search) {
        cairo_save(cr);
        cairo_scale(cr, zoom, zoom);
        this->search->paint(cr, rect, zoom, getSelectionColor());
        cairo_restore(cr);
    }

    if (this->inputHandler) {
        int dpiScaleFactor = xournal->getDpiScaleFactor();
        cairo_scale(cr, 1.0 / dpiScaleFactor, 1.0 / dpiScaleFactor);
        this->inputHandler->draw(cr);
    }
}

auto XojPageView::paintPage(cairo_t* cr, GdkRectangle* rect) -> bool {
    this->drawingMutex.lock();

    paintPageSync(cr, rect);

    this->drawingMutex.unlock();
    return true;
}

/**
 * GETTER / SETTER
 */

auto XojPageView::isSelected() const -> bool { return selected; }

auto XojPageView::getBufferPixels() -> int {
    if (crBuffer) {
        return cairo_image_surface_get_width(crBuffer) * cairo_image_surface_get_height(crBuffer);
    }
    return 0;
}

auto XojPageView::getSelectionColor() -> GdkRGBA { return Util::rgb_to_GdkRGBA(settings->getSelectionColor()); }

auto XojPageView::getTextEditor() -> TextEditor* { return textEditor; }

auto XojPageView::getX() const -> int { return this->dispX; }

void XojPageView::setX(int x) { this->dispX = x; }

auto XojPageView::getY() const -> int { return this->dispY; }

void XojPageView::setY(int y) { this->dispY = y; }

void XojPageView::setMappedRowCol(int row, int col) {
    this->mappedRow = row;
    this->mappedCol = col;
}


auto XojPageView::getMappedRow() const -> int { return this->mappedRow; }


auto XojPageView::getMappedCol() const -> int { return this->mappedCol; }


auto XojPageView::getPage() -> PageRef { return page; }

auto XojPageView::getXournal() -> XournalView* { return this->xournal; }

auto XojPageView::getHeight() const -> double { return this->page->getHeight(); }

auto XojPageView::getWidth() const -> double { return this->page->getWidth(); }

auto XojPageView::getDisplayWidth() const -> int {
    return std::lround(this->page->getWidth() * this->xournal->getZoom());
}

auto XojPageView::getDisplayHeight() const -> int {
    return std::lround(this->page->getHeight() * this->xournal->getZoom());
}

auto XojPageView::getDisplayWidthDouble() const -> double { return this->page->getWidth() * this->xournal->getZoom(); }

auto XojPageView::getDisplayHeightDouble() const -> double {
    return this->page->getHeight() * this->xournal->getZoom();
}

auto XojPageView::getSelectedTex() -> TexImage* {
    EditSelection* theSelection = this->xournal->getSelection();
    if (!theSelection) {
        return nullptr;
    }

    for (Element* e: *theSelection->getElements()) {
        if (e->getType() == ELEMENT_TEXIMAGE) {
            return dynamic_cast<TexImage*>(e);
        }
    }
    return nullptr;
}

auto XojPageView::getSelectedText() -> Text* {
    EditSelection* theSelection = this->xournal->getSelection();
    if (!theSelection) {
        return nullptr;
    }

    for (Element* e: *theSelection->getElements()) {
        if (e->getType() == ELEMENT_TEXT) {
            return dynamic_cast<Text*>(e);
        }
    }
    return nullptr;
}

auto XojPageView::getRect() const -> Rectangle<double> {
    return Rectangle<double>(getX(), getY(), getDisplayWidth(), getDisplayHeight());
}

void XojPageView::rectChanged(Rectangle<double>& rect) { rerenderRect(rect.x, rect.y, rect.width, rect.height); }

void XojPageView::rangeChanged(Range& range) { rerenderRange(range); }

void XojPageView::pageChanged() { rerenderPage(); }

void XojPageView::elementChanged(Element* elem) {
    if (this->inputHandler && elem == this->inputHandler->getStroke()) {
        this->drawingMutex.lock();

        cairo_t* cr = cairo_create(this->crBuffer);

        this->inputHandler->draw(cr);

        cairo_destroy(cr);

        this->drawingMutex.unlock();
    } else {
        rerenderElement(elem);
    }
}
