
/*
   SPDX-FileCopyrightText: 2003-2007 Clarence Dang <dang@kde.org>

   SPDX-License-Identifier: BSD-2-Clause
*/

#define DEBUG_KP_VIEW 0
#define DEBUG_KP_VIEW_RENDERER ((DEBUG_KP_VIEW && 1) || 0)

#include "kpViewPrivate.h"
#include "views/kpView.h"

#include <QPaintEvent>
#include <QPainter>
#include <QScrollBar>
#include <QTime>

#include "kpLogCategories.h"

#include "document/kpDocument.h"
#include "imagelib/kpColor.h"
#include "kpViewScrollableContainer.h"
#include "layers/selections/kpAbstractSelection.h"
#include "layers/selections/text/kpTextSelection.h"
#include "layers/tempImage/kpTempImage.h"
#include "views/manager/kpViewManager.h"

//---------------------------------------------------------------------

// protected
QRect kpView::paintEventGetDocRect(const QRect &viewRect) const
{
#if DEBUG_KP_VIEW_RENDERER && 1
    qCDebug(kpLogViews) << "kpView::paintEventGetDocRect(" << viewRect << ")";
#endif

    QRect docRect;

    // From the "we aren't sure whether to round up or round down" department:

    if (zoomLevelX() < 100 || zoomLevelY() < 100) {
        docRect = transformViewToDoc(viewRect);
    } else {
        // think of a grid - you need to fully cover the zoomed-in pixels
        // when docRect is zoomed back to the view later
        docRect = QRect(transformViewToDoc(viewRect.topLeft()), // round down
                        transformViewToDoc(viewRect.bottomRight())); // round down
    }

    if (zoomLevelX() % 100 || zoomLevelY() % 100) {
        // at least round up the bottom-right point and deal with matrix weirdness:
        // - helpful because it ensures we at least cover the required area
        //   at e.g. 67% or 573%
        docRect.setBottomRight(docRect.bottomRight() + QPoint(2, 2));
    }

#if DEBUG_KP_VIEW_RENDERER && 1
    qCDebug(kpLogViews) << "\tdocRect=" << docRect;
#endif
    kpDocument *doc = document();
    if (doc) {
        docRect = docRect.intersected(doc->rect());
#if DEBUG_KP_VIEW_RENDERER && 1
        qCDebug(kpLogViews) << "\tintersected with doc=" << docRect;
#endif
    }

    return docRect;
}

//---------------------------------------------------------------------

// public static
void kpView::drawTransparentBackground(QPainter *painter, const QPoint &patternOrigin, const QRect &viewRect, bool isPreview)
{
#if DEBUG_KP_VIEW_RENDERER && 1
    qCDebug(kpLogViews) << "kpView::drawTransparentBackground() patternOrigin=" << patternOrigin << " viewRect=" << viewRect << " isPreview=" << isPreview
                        << endl;
#endif

    const int cellSize = !isPreview ? 16 : 10;

    // TODO: % is unpredictable with negatives.

    int starty = viewRect.y();
    if ((starty - patternOrigin.y()) % cellSize) {
        starty -= ((starty - patternOrigin.y()) % cellSize);
    }

    int startx = viewRect.x();
    if ((startx - patternOrigin.x()) % cellSize) {
        startx -= ((startx - patternOrigin.x()) % cellSize);
    }

#if DEBUG_KP_VIEW_RENDERER && 1
    qCDebug(kpLogViews) << "\tstartXY=" << QPoint(startx, starty);
#endif

    painter->save();

    // Clip to <viewRect> as we may draw outside it on all sides.
    painter->setClipRect(viewRect, Qt::IntersectClip /*honor existing clip*/);

    for (int y = starty; y <= viewRect.bottom(); y += cellSize) {
        for (int x = startx; x <= viewRect.right(); x += cellSize) {
            bool parity = ((x - patternOrigin.x()) / cellSize + (y - patternOrigin.y()) / cellSize) % 2;
            QColor col;

            if (parity) {
                if (!isPreview) {
                    col = QColor(213, 213, 213);
                } else {
                    col = QColor(224, 224, 224);
                }
            } else {
                col = Qt::white;
            }

            painter->fillRect(x, y, cellSize, cellSize, col);
        }
    }

    painter->restore();
}

//---------------------------------------------------------------------

// protected
void kpView::paintEventDrawCheckerBoard(QPainter *painter, const QRect &viewRect)
{
#if DEBUG_KP_VIEW_RENDERER && 1
    qCDebug(kpLogViews) << "kpView(" << objectName() << ")::paintEventDrawCheckerBoard(viewRect=" << viewRect << ") origin=" << origin();
#endif

    kpDocument *doc = document();
    if (!doc) {
        return;
    }

    QPoint patternOrigin = origin();

    if (scrollableContainer()) {
#if DEBUG_KP_VIEW_RENDERER && 1
        qCDebug(kpLogViews) << "\tscrollableContainer: contents[XY]="
                            << QPoint(scrollableContainer()->horizontalScrollBar()->value(), scrollableContainer()->verticalScrollBar()->value()) << endl;
#endif
        // Make checkerboard appear static relative to the scroll view.
        // This makes it more obvious that any visible bits of the
        // checkboard represent transparent pixels and not gray and white
        // squares.
        patternOrigin = QPoint(scrollableContainer()->horizontalScrollBar()->value(), scrollableContainer()->verticalScrollBar()->value());
#if DEBUG_KP_VIEW_RENDERER && 1
        qCDebug(kpLogViews) << "\t\tpatternOrigin=" << patternOrigin;
#endif
    }

    // TODO: this static business doesn't work yet
    patternOrigin = QPoint(0, 0);

    drawTransparentBackground(painter, patternOrigin, viewRect);
}

//---------------------------------------------------------------------

// protected
void kpView::paintEventDrawSelection(QImage *destPixmap, const QRect &docRect)
{
#if DEBUG_KP_VIEW_RENDERER && 1 || 0
    qCDebug(kpLogViews) << "kpView::paintEventDrawSelection() docRect=" << docRect;
#endif

    kpDocument *doc = document();
    if (!doc) {
#if DEBUG_KP_VIEW_RENDERER && 1 || 0
        qCDebug(kpLogViews) << "\tno doc - abort";
#endif
        return;
    }

    kpAbstractSelection *sel = doc->selection();
    if (!sel) {
#if DEBUG_KP_VIEW_RENDERER && 1 || 0
        qCDebug(kpLogViews) << "\tno sel - abort";
#endif
        return;
    }

    //
    // Draw selection pixmap (if there is one)
    //
#if DEBUG_KP_VIEW_RENDERER && 1 || 0
    qCDebug(kpLogViews) << "\tdraw sel pixmap @ " << sel->topLeft();
#endif
    sel->paint(destPixmap, docRect);

    //
    // Draw selection border
    //

    kpViewManager *vm = viewManager();
#if DEBUG_KP_VIEW_RENDERER && 1 || 0
    qCDebug(kpLogViews) << "\tsel border visible=" << vm->selectionBorderVisible();
#endif
    if (vm->selectionBorderVisible()) {
        sel->paintBorder(destPixmap, docRect, vm->selectionBorderFinished());
    }

    //
    // Draw text cursor
    //

    // TODO: It would be nice to display the text cursor even if it's not
    //       within the text box (this can happen if the text box is too
    //       small for the text it contains).
    //
    //       However, too much selection repaint code assumes that it
    //       only paints inside its kpAbstractSelection::boundingRect().
    auto *textSel = dynamic_cast<kpTextSelection *>(sel);
    if (textSel && vm->textCursorEnabled()
        && (vm->textCursorBlinkState() ||
            // For the current main window:
            //     As long as _any_ view has focus, blink _all_ views not just the
            //     one with focus.
            !vm->hasAViewWithFocus())) // sync: call will break when vm is not held by 1 mainWindow
    {
        QRect rect = vm->textCursorRect();
        rect = rect.intersected(textSel->textAreaRect());
        if (!rect.isEmpty()) {
            kpPixmapFX::fillRect(destPixmap,
                                 rect.x() - docRect.x(),
                                 rect.y() - docRect.y(),
                                 rect.width(),
                                 rect.height(),
                                 kpColor::LightGray,
                                 kpColor::DarkGray);
        }
    }
}

//---------------------------------------------------------------------

// protected
void kpView::paintEventDrawSelectionResizeHandles(const QRect &clipRect)
{
#if DEBUG_KP_VIEW_RENDERER && 1
    qCDebug(kpLogViews) << "kpView::paintEventDrawSelectionResizeHandles(" << clipRect << ")";
#endif

    if (!selectionLargeEnoughToHaveResizeHandles()) {
#if DEBUG_KP_VIEW_RENDERER && 1
        qCDebug(kpLogViews) << "\tsel not large enough to have resize handles";
#endif
        return;
    }

    kpViewManager *vm = viewManager();
    if (!vm || !vm->selectionBorderVisible() || !vm->selectionBorderFinished()) {
#if DEBUG_KP_VIEW_RENDERER && 1
        qCDebug(kpLogViews) << "\tsel border not visible or not finished";
#endif

        return;
    }

    const QRect selViewRect = selectionViewRect();
#if DEBUG_KP_VIEW_RENDERER && 1
    qCDebug(kpLogViews) << "\tselViewRect=" << selViewRect;
#endif
    if (!selViewRect.intersects(clipRect)) {
#if DEBUG_KP_VIEW_RENDERER && 1
        qCDebug(kpLogViews) << "\tdoesn't intersect viewRect";
#endif
        return;
    }

    QRegion selResizeHandlesRegion = selectionResizeHandlesViewRegion(true /*for renderer*/);
#if DEBUG_KP_VIEW_RENDERER && 1
    qCDebug(kpLogViews) << "\tsel resize handles view region=" << selResizeHandlesRegion << endl;
#endif

    QPainter painter(this);
    painter.setPen(Qt::black);
    painter.setBrush(Qt::cyan);

    for (const QRect &r : selResizeHandlesRegion)
        painter.drawRect(r);
}

//---------------------------------------------------------------------

// protected
void kpView::paintEventDrawTempImage(QImage *destPixmap, const QRect &docRect)
{
    kpViewManager *vm = viewManager();
    if (!vm) {
        return;
    }

    const kpTempImage *tpi = vm->tempImage();
#if DEBUG_KP_VIEW_RENDERER && 1
    qCDebug(kpLogViews) << "kpView::paintEventDrawTempImage() tempImage=" << tpi << " isVisible=" << (tpi ? tpi->isVisible(vm) : false) << endl;
#endif

    if (!tpi || !tpi->isVisible(vm)) {
        return;
    }

    tpi->paint(destPixmap, docRect);
}

//---------------------------------------------------------------------

// protected
void kpView::paintEventDrawGridLines(QPainter *painter, const QRect &viewRect)
{
    int hzoomMultiple = zoomLevelX() / 100;
    int vzoomMultiple = zoomLevelY() / 100;

    painter->setPen(Qt::gray);

    // horizontal lines
    int starty = viewRect.top();
    if (starty % vzoomMultiple) {
        starty = (starty + vzoomMultiple) / vzoomMultiple * vzoomMultiple;
    }

    for (int y = starty; y <= viewRect.bottom(); y += vzoomMultiple) {
        painter->drawLine(viewRect.left(), y, viewRect.right(), y);
    }

    // vertical lines
    int startx = viewRect.left();
    if (startx % hzoomMultiple) {
        startx = (startx + hzoomMultiple) / hzoomMultiple * hzoomMultiple;
    }

    for (int x = startx; x <= viewRect.right(); x += hzoomMultiple) {
        painter->drawLine(x, viewRect.top(), x, viewRect.bottom());
    }
}

//---------------------------------------------------------------------

// This is called "_Unclipped" because it may draw outside of
// <viewRect>.
//
// There are 2 reasons for doing so:
//
// A. If, for instance:
//
//    1. <viewRect> = QRect (0, 0, 2, 3) [top-left of the view]
//    2. zoomLevelX() == 800
//    3. zoomLevelY() == 800
//
//    Then, the local variable <docRect> will be QRect (0, 0, 1, 1).
//    When the part of the document corresponding to <docRect>
//    (a single document pixel) is drawn with QPainter::scale(), the
//    view rectangle QRect (0, 0, 7, 7) will be overwritten due to the
//    8x zoom.  This view rectangle is bigger than <viewRect>.
//
//    We can't use QPainter::setClipRect() since it is buggy in Qt 4.3.1
//    and clips too many pixels when used in combination with scale()
//    [qt-bugs@trolltech.com issue N181038].  ==> MK 10.2.2011 - fixed since Qt-4.4.4
//
// B. paintEventGetDocRect() may, by design, return a larger document
//    rectangle than what <viewRect> corresponds to, if the zoom levels
//    are not perfectly divisible by 100.
//
// This over-drawing is dangerous -- see the comments in paintEvent().
// This over-drawing is only safe from Qt's perspective since Qt
// automatically clips all drawing in paintEvent() (which calls us) to
// QPaintEvent::region().
void kpView::paintEventDrawDoc_Unclipped(const QRect &viewRect)
{
#if DEBUG_KP_VIEW_RENDERER
    QTime timer;
    timer.start();
    qCDebug(kpLogViews) << "\tviewRect=" << viewRect;
#endif

    kpViewManager *vm = viewManager();
    const kpDocument *doc = document();

    Q_ASSERT(vm);
    Q_ASSERT(doc);

    if (viewRect.isEmpty()) {
        return;
    }

    QRect docRect = paintEventGetDocRect(viewRect);

#if DEBUG_KP_VIEW_RENDERER && 1
    qCDebug(kpLogViews) << "\tdocRect=" << docRect;
#endif

    QPainter painter(this);
    // painter.setCompositionMode(QPainter::CompositionMode_Source);

    QImage docPixmap;
    bool tempImageWillBeRendered = false;

    // LOTODO: I think <docRect> being empty would be a bug.
    if (!docRect.isEmpty()) {
        docPixmap = doc->getImageAt(docRect);

#if DEBUG_KP_VIEW_RENDERER && 1
        qCDebug(kpLogViews) << "\tdocPixmap.hasAlphaChannel()=" << docPixmap.hasAlphaChannel();
#endif

        tempImageWillBeRendered = (!doc->selection() && vm->tempImage() && vm->tempImage()->isVisible(vm) && docRect.intersects(vm->tempImage()->rect()));

#if DEBUG_KP_VIEW_RENDERER && 1
        qCDebug(kpLogViews) << "\ttempImageWillBeRendered=" << tempImageWillBeRendered << " (sel=" << doc->selection() << " tempImage=" << vm->tempImage()
                            << " tempImage.isVisible=" << (vm->tempImage() ? vm->tempImage()->isVisible(vm) : false)
                            << " docRect.intersects(tempImage.rect)=" << (vm->tempImage() ? docRect.intersects(vm->tempImage()->rect()) : false) << ")" << endl;
#endif
    }

    //
    // Draw checkboard for transparent images and/or views with borders
    //

    if (docPixmap.hasAlphaChannel() || (tempImageWillBeRendered && vm->tempImage()->paintMayAddMask())) {
        paintEventDrawCheckerBoard(&painter, viewRect);
    }

    if (!docRect.isEmpty()) {
        //
        // Draw docPixmap + tempImage
        //

        if (doc->selection()) {
            paintEventDrawSelection(&docPixmap, docRect);
        } else if (tempImageWillBeRendered) {
            paintEventDrawTempImage(&docPixmap, docRect);
        }

#if DEBUG_KP_VIEW_RENDERER && 1
        qCDebug(kpLogViews) << "\torigin=" << origin();
#endif
        // Blit scaled version of docPixmap + tempImage.
#if DEBUG_KP_VIEW_RENDERER && 1
        QTime scaleTimer;
        scaleTimer.start();
#endif
        // This is the only troublesome part of the method that draws unclipped.
        painter.translate(origin().x(), origin().y());
        painter.scale(double(zoomLevelX()) / 100.0, double(zoomLevelY()) / 100.0);
        painter.drawImage(docRect, docPixmap);
        // painter.resetMatrix ();  // back to 1-1 scaling
#if DEBUG_KP_VIEW_RENDERER && 1
        qCDebug(kpLogViews) << "\tscale time=" << scaleTimer.elapsed();
#endif

    } // if (!docRect.isEmpty ()) {

#if DEBUG_KP_VIEW_RENDERER && 1
    qCDebug(kpLogViews) << "\tdrawDocRect done in: " << timer.restart() << "ms";
#endif
}

//---------------------------------------------------------------------

// protected virtual [base QWidget]
void kpView::paintEvent(QPaintEvent *e)
{
    // sync: kpViewPrivate
    // WARNING: document(), viewManager() and friends might be 0 in this method.
    // TODO: I'm not 100% convinced that we always check if their friends are 0.

#if DEBUG_KP_VIEW_RENDERER && 1
    QTime timer;
    timer.start();
#endif

    kpViewManager *vm = viewManager();

#if DEBUG_KP_VIEW_RENDERER && 1
    qCDebug(kpLogViews) << "kpView(" << objectName() << ")::paintEvent() vm=" << (bool)vm << " queueUpdates=" << (vm && vm->queueUpdates())
                        << " fastUpdates=" << (vm && vm->fastUpdates()) << " viewRect=" << e->rect() << " topLeft=" << QPoint(x(), y()) << endl;
#endif

    if (!vm) {
        return;
    }

    if (vm->queueUpdates()) {
        // OPT: if this update was due to the document,
        //      use document coordinates (in case of a zoom change in
        //      which view coordinates become out of date)
        addToQueuedArea(e->region());
        return;
    }

    kpDocument *doc = document();
    if (!doc) {
        return;
    }

    // It seems that e->region() is already clipped by Qt to the visible
    // part of the view (which could be quite small inside a scrollview).
    const QRegion viewRegion = e->region();

    // Draw all the requested regions of the document _before_ drawing
    // the grid lines, buddy rectangle and selection resize handles.
    // This ordering is important since paintEventDrawDoc_Unclipped()
    // may draw outside of the view rectangle passed to it.
    //
    // To illustrate this, suppose we changed each iteration of the loop
    // to call paintEventDrawDoc_Unclipped() _and_ then,
    // paintEventDrawGridLines().  If there are 2 or more iterations of this
    // loop, paintEventDrawDoc_Unclipped() in one iteration may draw over
    // parts of nearby grid lines (which were drawn in a previous iteration)
    // with document pixels.  Those grid line parts are probably not going to
    // be redrawn, so will appear to be missing.
    for (const QRect &r : viewRegion)
        paintEventDrawDoc_Unclipped(r);

    //
    // Draw Grid Lines
    //

    if (isGridShown()) {
        QPainter painter(this);
        for (const QRect &r : viewRegion)
            paintEventDrawGridLines(&painter, r);
    }

    const QRect r = buddyViewScrollableContainerRectangle();
    if (!r.isEmpty()) {
        QPainter painter(this);

        painter.setPen(QPen(Qt::lightGray, 1 /*width*/, Qt::DotLine));
        painter.setBackground(Qt::darkGray);
        painter.setBackgroundMode(Qt::OpaqueMode);

        painter.drawRect(r.x(), r.y(), r.width() - 1, r.height() - 1);
    }

    if (doc->selection()) {
        // Draw resize handles on top of possible grid lines
        paintEventDrawSelectionResizeHandles(e->rect());
    }

#if DEBUG_KP_VIEW_RENDERER && 1
    qCDebug(kpLogViews) << "\tall done in: " << timer.restart() << "ms";
#endif
}

//---------------------------------------------------------------------
