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

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

#define DEBUG_KP_PAINTER 0

#include "kpPainter.h"

#include "pixmapfx/kpPixmapFX.h"
#include "tools/flow/kpToolFlowBase.h"
#include "tools/kpTool.h"

#include <cstdio>

#include <QPainter>
#include <QRandomGenerator>

#include "kpLogCategories.h"

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

// public static
bool kpPainter::pointsAreCardinallyAdjacent(const QPoint &p, const QPoint &q)
{
    int dx = qAbs(p.x() - q.x());
    int dy = qAbs(p.y() - q.y());

    return (dx + dy == 1);
}

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

// public static
QList<QPoint> kpPainter::interpolatePoints(const QPoint &startPoint, const QPoint &endPoint, bool cardinalAdjacency, double probability)
{
#if DEBUG_KP_PAINTER
    qCDebug(kpLogImagelib) << "CALL(startPoint=" << startPoint << ",endPoint=" << endPoint << ")";
#endif

    QList<QPoint> ret;

    Q_ASSERT(probability >= 0.0 && probability <= 1.0);
    const int probabilityTimes1000 = qRound(probability * 1000);
#define SHOULD_DRAW() ((probabilityTimes1000 == 1000) /*avoid QRandomGenerator call*/ || (QRandomGenerator::global()->bounded(1000) < probabilityTimes1000))

    // Derived from the zSprite2 Graphics Engine.
    // "MODIFIED" comment shows deviation from zSprite2 and Bresenham's line
    // algorithm.

    const int x1 = startPoint.x(), y1 = startPoint.y(), x2 = endPoint.x(), y2 = endPoint.y();

    // Difference of x and y values
    const int dx = x2 - x1;
    const int dy = y2 - y1;

    // Absolute values of differences
    const int ix = qAbs(dx);
    const int iy = qAbs(dy);

    // Larger of the x and y differences
    const int inc = ix > iy ? ix : iy;

    // Plot location
    int plotx = x1;
    int ploty = y1;

    int x = 0;
    int y = 0;

    if (SHOULD_DRAW()) {
        ret.append(QPoint(plotx, ploty));
    }

    for (int i = 0; i <= inc; i++) {
        // oldplotx is equally as valid but would look different
        // (but nobody will notice which one it is)
        const int oldploty = ploty;
        int plot = 0;

        x += ix;
        y += iy;

        if (x > inc) {
            plot++;
            x -= inc;

            if (dx < 0) {
                plotx--;
            } else {
                plotx++;
            }
        }

        if (y > inc) {
            plot++;
            y -= inc;

            if (dy < 0) {
                ploty--;
            } else {
                ploty++;
            }
        }

        if (plot) {
            if (cardinalAdjacency && plot == 2) {
                // MODIFIED: Every point is
                // horizontally or vertically adjacent to another point (if there
                // is more than 1 point, of course).  This is in contrast to the
                // ordinary line algorithm which can create diagonal adjacencies.

                if (SHOULD_DRAW()) {
                    ret.append(QPoint(plotx, oldploty));
                }
            }

            if (SHOULD_DRAW()) {
                ret.append(QPoint(plotx, ploty));
            }
        }
    }

#undef SHOULD_DRAW

    return ret;
}

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

// public static
void kpPainter::fillRect(kpImage *image, int x, int y, int width, int height, const kpColor &color)
{
    kpPixmapFX::fillRect(image, x, y, width, height, color);
}

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

// <rgbPainter> are operating on the original image
// (the original image is not passed to this function).
//
// <image> = subset of the original image containing all the pixels in
//           <imageRect>
// <drawRect> = the rectangle, relative to the painters, whose pixels we
//              want to change
static bool ReadableImageWashRect(QPainter *rgbPainter,
                                  const QImage &image,
                                  const kpColor &colorToReplace,
                                  const QRect &imageRect,
                                  const QRect &drawRect,
                                  int processedColorSimilarity)
{
    bool didSomething = false;

#if DEBUG_KP_PAINTER && 0
    qCDebug(kpLogImagelib) << "kppixmapfx.cpp:WashRect(imageRect=" << imageRect << ",drawRect=" << drawRect << ")" << endl;
#endif

    // If you're going to pass painter pointers, those painters had better be
    // active (i.e. QPainter::begin() has been called).
    Q_ASSERT(!rgbPainter || rgbPainter->isActive());

// make use of scanline coherence
#define FLUSH_LINE()                                                                                                                                           \
    {                                                                                                                                                          \
        if (rgbPainter) {                                                                                                                                      \
            if (startDrawX == x - 1)                                                                                                                           \
                rgbPainter->drawPoint(startDrawX + imageRect.x(), y + imageRect.y());                                                                          \
            else                                                                                                                                               \
                rgbPainter->drawLine(startDrawX + imageRect.x(), y + imageRect.y(), x - 1 + imageRect.x(), y + imageRect.y());                                 \
        }                                                                                                                                                      \
        didSomething = true;                                                                                                                                   \
        startDrawX = -1;                                                                                                                                       \
    }

    const int maxY = drawRect.bottom() - imageRect.top();

    const int minX = drawRect.left() - imageRect.left();
    const int maxX = drawRect.right() - imageRect.left();

    for (int y = drawRect.top() - imageRect.top(); y <= maxY; y++) {
        int startDrawX = -1;

        int x; // for FLUSH_LINE()
        for (x = minX; x <= maxX; x++) {
#if DEBUG_KP_PAINTER && 0
            fprintf(stderr,
                    "y=%i x=%i colorAtPixel=%08X colorToReplace=%08X ... ",
                    y,
                    x,
                    kpPixmapFX::getColorAtPixel(image, QPoint(x, y)).toQRgb(),
                    colorToReplace.toQRgb());
#endif
            if (kpPixmapFX::getColorAtPixel(image, QPoint(x, y)).isSimilarTo(colorToReplace, processedColorSimilarity)) {
#if DEBUG_KP_PAINTER && 0
                fprintf(stderr, "similar\n");
#endif
                if (startDrawX < 0) {
                    startDrawX = x;
                }
            } else {
#if DEBUG_KP_PAINTER && 0
                fprintf(stderr, "different\n");
#endif
                if (startDrawX >= 0) {
                    FLUSH_LINE();
                }
            }
        }

        if (startDrawX >= 0) {
            FLUSH_LINE();
        }
    }

#undef FLUSH_LINE

    return didSomething;
}

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

struct WashPack {
    QPoint startPoint, endPoint;
    kpColor color;
    int penWidth{}, penHeight{};
    kpColor colorToReplace;
    int processedColorSimilarity{};

    QRect readableImageRect;
    QImage readableImage;
};

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

static QRect Wash(kpImage *image,
                  const QPoint &startPoint,
                  const QPoint &endPoint,
                  const kpColor &color,
                  int penWidth,
                  int penHeight,
                  const kpColor &colorToReplace,
                  int processedColorSimilarity,
                  QRect (*drawFunc)(QPainter * /*rgbPainter*/, void * /*data*/))
{
    WashPack pack;
    pack.startPoint = startPoint;
    pack.endPoint = endPoint;
    pack.color = color;
    pack.penWidth = penWidth;
    pack.penHeight = penHeight;
    pack.colorToReplace = colorToReplace;
    pack.processedColorSimilarity = processedColorSimilarity;

    // Get the rectangle that bounds the changes and the pixmap for that
    // rectangle.
    const QRect normalizedRect = kpPainter::normalizedRect(pack.startPoint, pack.endPoint);
    pack.readableImageRect = kpTool::neededRect(normalizedRect, qMax(pack.penWidth, pack.penHeight));
#if DEBUG_KP_PAINTER
    qCDebug(kpLogImagelib) << "kppainter.cpp:Wash() startPoint=" << startPoint << " endPoint=" << endPoint << " --> normalizedRect=" << normalizedRect
                           << " readableImageRect=" << pack.readableImageRect << endl;
#endif
    pack.readableImage = kpPixmapFX::getPixmapAt(*image, pack.readableImageRect);

    QPainter painter(image);
    return (*drawFunc)(&painter, &pack);
}

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

void WashHelperSetup(QPainter *rgbPainter, const WashPack *pack)
{
    // Set the drawing colors for the painters.

    if (rgbPainter) {
        rgbPainter->setPen(pack->color.toQColor());
    }
}

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

static QRect WashLineHelper(QPainter *rgbPainter, void *data)
{
#if DEBUG_KP_PAINTER && 0
    qCDebug(kpLogImagelib) << "Washing pixmap (w=" << rect.width() << ",h=" << rect.height() << ")" << endl;
    QTime timer;
    int convAndWashTime;
#endif

    auto *pack = static_cast<WashPack *>(data);

    // Setup painters.
    ::WashHelperSetup(rgbPainter, pack);

    bool didSomething = false;

    const QList<QPoint> points = kpPainter::interpolatePoints(pack->startPoint, pack->endPoint);
    for (const QPoint &p : points) {
        // OPT: This may be reading and possibly writing pixels that were
        //      visited on a previous iteration, since the pen is usually
        //      bigger than 1 pixel.  Maybe we could use QRegion to determine
        //      all the non-intersecting regions and only wash each region once.
        //
        //      Profiling needs to be done as QRegion is known to be a CPU hog.
        if (::ReadableImageWashRect(rgbPainter,
                                    pack->readableImage,
                                    pack->colorToReplace,
                                    pack->readableImageRect,
                                    kpToolFlowBase::hotRectForMousePointAndBrushWidthHeight(p, pack->penWidth, pack->penHeight),
                                    pack->processedColorSimilarity)) {
            didSomething = true;
        }
    }

#if DEBUG_KP_PAINTER && 0
    int ms = timer.restart();
    qCDebug(kpLogImagelib) << "\ttried to wash: " << ms << "ms"
                           << " (" << (ms ? (rect.width() * rect.height() / ms) : -1234) << " pixels/ms)" << endl;
    convAndWashTime += ms;
#endif

    // TODO: Rectangle may be too big.  Use QRect::united() incrementally?
    //       Efficiency?
    return didSomething ? pack->readableImageRect : QRect();
}

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

// public static
QRect kpPainter::washLine(kpImage *image,
                          int x1,
                          int y1,
                          int x2,
                          int y2,
                          const kpColor &color,
                          int penWidth,
                          int penHeight,
                          const kpColor &colorToReplace,
                          int processedColorSimilarity)
{
    return ::Wash(image, QPoint(x1, y1), QPoint(x2, y2), color, penWidth, penHeight, colorToReplace, processedColorSimilarity, &::WashLineHelper);
}

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

static QRect WashRectHelper(QPainter *rgbPainter, void *data)
{
    auto *pack = static_cast<WashPack *>(data);
#if DEBUG_KP_PAINTER && 0
    qCDebug(kpLogImagelib) << "Washing pixmap (w=" << rect.width() << ",h=" << rect.height() << ")" << endl;
    QTime timer;
    int convAndWashTime;
#endif

    // Setup painters.
    ::WashHelperSetup(rgbPainter, pack);

    const QRect drawRect(pack->startPoint, pack->endPoint);

    bool didSomething = false;

    if (::ReadableImageWashRect(rgbPainter, pack->readableImage, pack->colorToReplace, pack->readableImageRect, drawRect, pack->processedColorSimilarity)) {
        didSomething = true;
    }

#if DEBUG_KP_PAINTER && 0
    int ms = timer.restart();
    qCDebug(kpLogImagelib) << "\ttried to wash: " << ms << "ms"
                           << " (" << (ms ? (rect.width() * rect.height() / ms) : -1234) << " pixels/ms)" << endl;
    convAndWashTime += ms;
#endif

    return didSomething ? drawRect : QRect();
}

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

// public static
QRect kpPainter::washRect(kpImage *image,
                          int x,
                          int y,
                          int width,
                          int height,
                          const kpColor &color,
                          const kpColor &colorToReplace,
                          int processedColorSimilarity)
{
    return ::Wash(image,
                  QPoint(x, y),
                  QPoint(x + width - 1, y + height - 1),
                  color,
                  1 /*pen width*/,
                  1 /*pen height*/,
                  colorToReplace,
                  processedColorSimilarity,
                  &::WashRectHelper);
}

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

// public static
void kpPainter::sprayPoints(kpImage *image, const QList<QPoint> &points, const kpColor &color, int spraycanSize)
{
#if DEBUG_KP_PAINTER
    qCDebug(kpLogImagelib) << "kpPainter::sprayPoints()";
#endif

    Q_ASSERT(spraycanSize > 0);

    QPainter painter(image);
    const int radius = spraycanSize / 2;

    // Set the drawing colors for the painters.

    painter.setPen(color.toQColor());

    for (const auto &p : points) {
        for (int i = 0; i < 10; i++) {
            const int dx = (QRandomGenerator::global()->generate() % spraycanSize) - radius;
            const int dy = (QRandomGenerator::global()->generate() % spraycanSize) - radius;

            // Make it look circular.
            // TODO: Can be done better by doing a random vector angle & length
            //       but would sin and cos be too slow?
            if ((dx * dx) + (dy * dy) > (radius * radius)) {
                continue;
            }

            const QPoint p2(p.x() + dx, p.y() + dy);

            painter.drawPoint(p2);
        }
    }
}

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