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

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

// TODO: Color Similarity is obviously useful in Autocrop but it isn't
//       obvious as to how to implement it.  The current heuristic,
//       for each side, chooses an arbitrary reference color for which
//       all other candidate pixels in that side are tested against
//       for similarity.  But if the reference color happens to be at
//       one extreme of the range of colors in that side, then pixels
//       at the other extreme would not be deemed similar enough.  The
//       key is to find the median color as the reference but how do
//       you do this if you don't know which pixels to sample in the first
//       place (that's what you're trying to find)?  Chicken and egg situation.
//
//       The other heuristic that is in doubt is the use of the average
//       color in determining the similarity of sides (it is possible
//       to get vastly differently colors in both sides yet they will be
//       considered similar).

#define DEBUG_KP_TOOL_AUTO_CROP 0

#include "kpTransformAutoCrop.h"

#include "commands/kpCommandHistory.h"
#include "document/kpDocument.h"
#include "environments/commands/kpCommandEnvironment.h"
#include "generic/kpSetOverrideCursorSaver.h"
#include "imagelib/kpPainter.h"
#include "layers/selections/image/kpAbstractImageSelection.h"
#include "layers/selections/image/kpRectangularImageSelection.h"
#include "mainWindow/kpMainWindow.h"
#include "pixmapfx/kpPixmapFX.h"
#include "tools/kpTool.h"
#include "views/manager/kpViewManager.h"
#include "widgets/toolbars/kpColorToolBar.h"

#include "kpLogCategories.h"
#include <KLocalizedString>
#include <KMessageBox>

#include <QImage>

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

class kpTransformAutoCropBorder
{
public:
    // WARNING: Only call the <ctor> with imagePtr = 0 if you are going to use
    //          operator= to fill it in with a valid imagePtr immediately
    //          afterwards.
    explicit kpTransformAutoCropBorder(const kpImage *imagePtr = nullptr, int processedColorSimilarity = 0);

    kpCommandSize::SizeType size() const;

    const kpImage *image() const;
    int processedColorSimilarity() const;
    QRect rect() const;
    int left() const;
    int right() const;
    int top() const;
    int bottom() const;
    kpColor referenceColor() const;
    kpColor averageColor() const;
    bool isSingleColor() const;

    // (returns true on success (even if no rect) or false on error)
    bool calculate(int isX, int dir);

    bool fillsEntireImage() const;
    bool exists() const;
    void invalidate();

private:
    const kpImage *m_imagePtr;
    int m_processedColorSimilarity;

    QRect m_rect;
    kpColor m_referenceColor;
    int m_redSum, m_greenSum, m_blueSum;
    bool m_isSingleColor;
};

kpTransformAutoCropBorder::kpTransformAutoCropBorder(const kpImage *imagePtr, int processedColorSimilarity)
    : m_imagePtr(imagePtr)
    , m_processedColorSimilarity(processedColorSimilarity)
{
    invalidate();
}

// public
kpCommandSize::SizeType kpTransformAutoCropBorder::size() const
{
    return sizeof(kpTransformAutoCropBorder);
}

// public
const kpImage *kpTransformAutoCropBorder::image() const
{
    return m_imagePtr;
}

// public
int kpTransformAutoCropBorder::processedColorSimilarity() const
{
    return m_processedColorSimilarity;
}

// public
QRect kpTransformAutoCropBorder::rect() const
{
    return m_rect;
}

// public
int kpTransformAutoCropBorder::left() const
{
    return m_rect.left();
}

// public
int kpTransformAutoCropBorder::right() const
{
    return m_rect.right();
}

// public
int kpTransformAutoCropBorder::top() const
{
    return m_rect.top();
}

// public
int kpTransformAutoCropBorder::bottom() const
{
    return m_rect.bottom();
}

// public
kpColor kpTransformAutoCropBorder::referenceColor() const
{
    return m_referenceColor;
}

// public
kpColor kpTransformAutoCropBorder::averageColor() const
{
    if (!m_rect.isValid())
        return kpColor::Invalid;

    if (m_referenceColor.isTransparent())
        return kpColor::Transparent;

    if (m_processedColorSimilarity == 0)
        return m_referenceColor;

    int numPixels = (m_rect.width() * m_rect.height());
    Q_ASSERT(numPixels > 0);

    return kpColor(m_redSum / numPixels, m_greenSum / numPixels, m_blueSum / numPixels);
}

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

bool kpTransformAutoCropBorder::isSingleColor() const
{
    return m_isSingleColor;
}

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

// public
bool kpTransformAutoCropBorder::calculate(int isX, int dir)
{
#if DEBUG_KP_TOOL_AUTO_CROP && 1
    qCDebug(kpLogImagelib) << "kpTransformAutoCropBorder::calculate() CALLED!";
#endif
    int maxX = m_imagePtr->width() - 1;
    int maxY = m_imagePtr->height() - 1;

    QImage qimage = *m_imagePtr;
    Q_ASSERT(!qimage.isNull());

    // (sync both branches)
    if (isX) {
        int numCols = 0;
        int startX = (dir > 0) ? 0 : maxX;

        kpColor col = kpPixmapFX::getColorAtPixel(qimage, startX, 0);
        for (int x = startX; x >= 0 && x <= maxX; x += dir) {
            int y;
            for (y = 0; y <= maxY; y++) {
                if (!kpPixmapFX::getColorAtPixel(qimage, x, y).isSimilarTo(col, m_processedColorSimilarity))
                    break;
            }

            if (y <= maxY)
                break;
            else
                numCols++;
        }

        if (numCols) {
            m_rect = kpPainter::normalizedRect(QPoint(startX, 0), QPoint(startX + (numCols - 1) * dir, maxY));
            m_referenceColor = col;
        }
    } else {
        int numRows = 0;
        int startY = (dir > 0) ? 0 : maxY;

        kpColor col = kpPixmapFX::getColorAtPixel(qimage, 0, startY);
        for (int y = startY; y >= 0 && y <= maxY; y += dir) {
            int x;
            for (x = 0; x <= maxX; x++) {
                if (!kpPixmapFX::getColorAtPixel(qimage, x, y).isSimilarTo(col, m_processedColorSimilarity))
                    break;
            }

            if (x <= maxX)
                break;
            else
                numRows++;
        }

        if (numRows) {
            m_rect = kpPainter::normalizedRect(QPoint(0, startY), QPoint(maxX, startY + (numRows - 1) * dir));
            m_referenceColor = col;
        }
    }

    if (m_rect.isValid()) {
        m_isSingleColor = true;

        if (m_processedColorSimilarity != 0) {
            for (int y = m_rect.top(); y <= m_rect.bottom(); y++) {
                for (int x = m_rect.left(); x <= m_rect.right(); x++) {
                    kpColor colAtPixel = kpPixmapFX::getColorAtPixel(qimage, x, y);

                    if (m_isSingleColor && colAtPixel != m_referenceColor)
                        m_isSingleColor = false;

                    m_redSum += colAtPixel.red();
                    m_greenSum += colAtPixel.green();
                    m_blueSum += colAtPixel.blue();
                }
            }
        }
    }

    return true;
}

// public
bool kpTransformAutoCropBorder::fillsEntireImage() const
{
    return (m_rect == m_imagePtr->rect());
}

// public
bool kpTransformAutoCropBorder::exists() const
{
    // (will use in an addition so make sure returns 1 or 0)
    return (m_rect.isValid() ? 1 : 0);
}

// public
void kpTransformAutoCropBorder::invalidate()
{
    m_rect = QRect();
    m_referenceColor = kpColor::Invalid;
    m_redSum = m_greenSum = m_blueSum = 0;
    m_isSingleColor = false;
}

struct kpTransformAutoCropCommandPrivate {
    bool actOnSelection{};
    kpTransformAutoCropBorder leftBorder, rightBorder, topBorder, botBorder;
    kpImage *leftImage{}, *rightImage{}, *topImage{}, *botImage{};

    QRect contentsRect;
    int oldWidth{}, oldHeight{};
    kpAbstractImageSelection *oldSelectionPtr{};
};

// REFACTOR: Move to /commands/
kpTransformAutoCropCommand::kpTransformAutoCropCommand(bool actOnSelection,
                                                       const kpTransformAutoCropBorder &leftBorder,
                                                       const kpTransformAutoCropBorder &rightBorder,
                                                       const kpTransformAutoCropBorder &topBorder,
                                                       const kpTransformAutoCropBorder &botBorder,
                                                       kpCommandEnvironment *environ)
    : kpNamedCommand(text(actOnSelection, DontShowAccel), environ)
    , d(new kpTransformAutoCropCommandPrivate())
{
    d->actOnSelection = actOnSelection;
    d->leftBorder = leftBorder;
    d->rightBorder = rightBorder;
    d->topBorder = topBorder;
    d->botBorder = botBorder;
    d->leftImage = nullptr;
    d->rightImage = nullptr;
    d->topImage = nullptr;
    d->botImage = nullptr;

    kpDocument *doc = document();
    Q_ASSERT(doc);

    d->oldWidth = doc->width(d->actOnSelection);
    d->oldHeight = doc->height(d->actOnSelection);

    d->oldSelectionPtr = nullptr;
}

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

kpTransformAutoCropCommand::~kpTransformAutoCropCommand()
{
    deleteUndoImages();

    delete d->oldSelectionPtr;
    delete d;
}

//---------------------------------------------------------------------
// public static

QString kpTransformAutoCropCommand::text(bool actOnSelection, int options)
{
    if (actOnSelection) {
        if (options & kpTransformAutoCropCommand::ShowAccel) {
            return i18n("Remove Internal B&order");
        }

        return i18n("Remove Internal Border");
    }

    if (options & kpTransformAutoCropCommand::ShowAccel)
        return i18n("Autocr&op");

    return i18n("Autocrop");
}

//---------------------------------------------------------------------
// public virtual [base kpCommand]

kpCommandSize::SizeType kpTransformAutoCropCommand::size() const
{
    return d->leftBorder.size() + d->rightBorder.size() + d->topBorder.size() + d->botBorder.size() + ImageSize(d->leftImage) + ImageSize(d->rightImage)
        + ImageSize(d->topImage) + ImageSize(d->botImage) + SelectionSize(d->oldSelectionPtr);
}

//---------------------------------------------------------------------
// private

void kpTransformAutoCropCommand::getUndoImage(const kpTransformAutoCropBorder &border, kpImage **image)
{
    kpDocument *doc = document();
    Q_ASSERT(doc);

#if DEBUG_KP_TOOL_AUTO_CROP && 1
    qCDebug(kpLogImagelib) << "kpTransformAutoCropCommand::getUndoImage()";
    qCDebug(kpLogImagelib) << "\timage=" << image << " border: rect=" << border.rect() << " isSingleColor=" << border.isSingleColor();
#endif

    if (image && border.exists() && !border.isSingleColor()) {
        if (*image) {
#if DEBUG_KP_TOOL_AUTO_CROP && 1
            qCDebug(kpLogImagelib) << "\talready have *image - delete it";
#endif
            delete *image;
        }

        *image = new kpImage(kpPixmapFX::getPixmapAt(doc->image(d->actOnSelection), border.rect()));
    }
}

// private
void kpTransformAutoCropCommand::getUndoImages()
{
    getUndoImage(d->leftBorder, &d->leftImage);
    getUndoImage(d->rightBorder, &d->rightImage);
    getUndoImage(d->topBorder, &d->topImage);
    getUndoImage(d->botBorder, &d->botImage);
}

// private
void kpTransformAutoCropCommand::deleteUndoImages()
{
#if DEBUG_KP_TOOL_AUTO_CROP && 1
    qCDebug(kpLogImagelib) << "kpTransformAutoCropCommand::deleteUndoImages()";
#endif

    delete d->leftImage;
    d->leftImage = nullptr;
    delete d->rightImage;
    d->rightImage = nullptr;
    delete d->topImage;
    d->topImage = nullptr;
    delete d->botImage;
    d->botImage = nullptr;
}

// public virtual [base kpCommand]
void kpTransformAutoCropCommand::execute()
{
    if (!d->contentsRect.isValid()) {
        d->contentsRect = contentsRect();
    }

    getUndoImages();

    kpDocument *doc = document();
    Q_ASSERT(doc);

    kpImage imageWithoutBorder = kpTool::neededPixmap(doc->image(d->actOnSelection), d->contentsRect);

    if (!d->actOnSelection) {
        doc->setImage(imageWithoutBorder);
    } else {
        d->oldSelectionPtr = doc->imageSelection()->clone();
        d->oldSelectionPtr->setBaseImage(kpImage());

        // d->contentsRect is relative to the top of the sel
        // while sel is relative to the top of the doc
        QRect rect = d->contentsRect;
        rect.translate(d->oldSelectionPtr->x(), d->oldSelectionPtr->y());

        kpRectangularImageSelection sel(rect, imageWithoutBorder, d->oldSelectionPtr->transparency());

        doc->setSelection(sel);

        environ()->somethingBelowTheCursorChanged();
    }
}

// public virtual [base kpCommand]
void kpTransformAutoCropCommand::unexecute()
{
#if DEBUG_KP_TOOL_AUTO_CROP && 1
    qCDebug(kpLogImagelib) << "kpTransformAutoCropCommand::unexecute()";
#endif

    kpDocument *doc = document();
    Q_ASSERT(doc);

    kpImage image(d->oldWidth, d->oldHeight, QImage::Format_ARGB32_Premultiplied);

    // restore the position of the center image
    kpPixmapFX::setPixmapAt(&image, d->contentsRect, doc->image(d->actOnSelection));

    // draw the borders

    const kpTransformAutoCropBorder *borders[] = {&d->leftBorder, &d->rightBorder, &d->topBorder, &d->botBorder, nullptr};

    const kpImage *images[] = {d->leftImage, d->rightImage, d->topImage, d->botImage, nullptr};

    const kpImage **p = images;
    for (const kpTransformAutoCropBorder **b = borders; *b; b++, p++) {
        if (!(*b)->exists()) {
            continue;
        }

        if ((*b)->isSingleColor()) {
            kpColor col = (*b)->referenceColor();
#if DEBUG_KP_TOOL_AUTO_CROP && 1
            qCDebug(kpLogImagelib) << "\tdrawing border " << (*b)->rect() << " rgb=" << (int *)col.toQRgb() /* %X hack */;
#endif

            const QRect r = (*b)->rect();
            kpPainter::fillRect(&image, r.x(), r.y(), r.width(), r.height(), col);
        } else {
#if DEBUG_KP_TOOL_AUTO_CROP && 1
            qCDebug(kpLogImagelib) << "\trestoring border image " << (*b)->rect();
#endif
            if (*p) {
                // REFACTOR: Add equivalent method to kpPainter and use.
                kpPixmapFX::setPixmapAt(&image, (*b)->rect(), **p);
            }
        }
    }

    if (!d->actOnSelection) {
        doc->setImage(image);
    } else {
        d->oldSelectionPtr->setBaseImage(image);

        doc->setSelection(*d->oldSelectionPtr);
        delete d->oldSelectionPtr;
        d->oldSelectionPtr = nullptr;

        environ()->somethingBelowTheCursorChanged();
    }

    deleteUndoImages();
}

// private
QRect kpTransformAutoCropCommand::contentsRect() const
{
    const kpImage image = document()->image(d->actOnSelection);

    QPoint topLeft(d->leftBorder.exists() ? d->leftBorder.rect().right() + 1 : 0, d->topBorder.exists() ? d->topBorder.rect().bottom() + 1 : 0);
    QPoint botRight(d->rightBorder.exists() ? d->rightBorder.rect().left() - 1 : image.width() - 1,
                    d->botBorder.exists() ? d->botBorder.rect().top() - 1 : image.height() - 1);

    return {topLeft, botRight};
}

static void ShowNothingToAutocropMessage(kpMainWindow *mainWindow, bool actOnSelection)
{
    kpSetOverrideCursorSaver cursorSaver(Qt::ArrowCursor);

    if (actOnSelection) {
        KMessageBox::information(mainWindow,
                                 i18n("KolourPaint cannot remove the selection's internal border as it"
                                      " could not be located."),
                                 i18nc("@title:window", "Cannot Remove Internal Border"),
                                 QStringLiteral("NothingToAutoCrop"));
    } else {
        KMessageBox::information(mainWindow,
                                 i18n("KolourPaint cannot automatically crop the image as its"
                                      " border could not be located."),
                                 i18nc("@title:window", "Cannot Autocrop"),
                                 QStringLiteral("NothingToAutoCrop"));
    }
}

bool kpTransformAutoCrop(kpMainWindow *mainWindow)
{
#if DEBUG_KP_TOOL_AUTO_CROP
    qCDebug(kpLogImagelib) << "kpTransformAutoCrop() CALLED!";
#endif

    Q_ASSERT(mainWindow);
    kpDocument *doc = mainWindow->document();
    Q_ASSERT(doc);

    // OPT: if already pulled selection image, no need to do it again here
    kpImage image = doc->selection() ? doc->getSelectedBaseImage() : doc->image();
    Q_ASSERT(!image.isNull());

    kpViewManager *vm = mainWindow->viewManager();
    Q_ASSERT(vm);

    int processedColorSimilarity = mainWindow->colorToolBar()->processedColorSimilarity();
    kpTransformAutoCropBorder leftBorder(&image, processedColorSimilarity), rightBorder(&image, processedColorSimilarity),
        topBorder(&image, processedColorSimilarity), botBorder(&image, processedColorSimilarity);

    kpSetOverrideCursorSaver cursorSaver(Qt::WaitCursor);

    mainWindow->colorToolBar()->flashColorSimilarityToolBarItem();

    // TODO: With Colour Similarity, a lot of weird (and wonderful) things can
    //       happen resulting in a huge number of code paths.  Needs refactoring
    //       and regression testing.
    //
    // TODO: e.g. When the top fills entire rect but bot doesn't we could
    //       invalidate top and continue autocrop.
    int numRegions = 0;
    if (!leftBorder.calculate(true /*x*/, +1 /*going right*/) || leftBorder.fillsEntireImage() || !rightBorder.calculate(true /*x*/, -1 /*going left*/)
        || rightBorder.fillsEntireImage() || !topBorder.calculate(false /*y*/, +1 /*going down*/) || topBorder.fillsEntireImage()
        || !botBorder.calculate(false /*y*/, -1 /*going up*/) || botBorder.fillsEntireImage()
        || ((numRegions = leftBorder.exists() + rightBorder.exists() + topBorder.exists() + botBorder.exists()) == 0)) {
#if DEBUG_KP_TOOL_AUTO_CROP
        qCDebug(kpLogImagelib) << "\tcan't find border; leftBorder.rect=" << leftBorder.rect() << " rightBorder.rect=" << rightBorder.rect()
                               << " topBorder.rect=" << topBorder.rect() << " botBorder.rect=" << botBorder.rect();
#endif
        ::ShowNothingToAutocropMessage(mainWindow, static_cast<bool>(doc->selection()));
        return false;
    }

#if DEBUG_KP_TOOL_AUTO_CROP
    qCDebug(kpLogImagelib) << "\tnumRegions=" << numRegions;
    qCDebug(kpLogImagelib) << "\t\tleft=" << leftBorder.rect() << " refCol=" << (leftBorder.exists() ? (int *)leftBorder.referenceColor().toQRgb() : nullptr)
                           << " avgCol=" << (leftBorder.exists() ? (int *)leftBorder.averageColor().toQRgb() : nullptr);
    qCDebug(kpLogImagelib) << "\t\tright=" << rightBorder.rect()
                           << " refCol=" << (rightBorder.exists() ? (int *)rightBorder.referenceColor().toQRgb() : nullptr)
                           << " avgCol=" << (rightBorder.exists() ? (int *)rightBorder.averageColor().toQRgb() : nullptr);
    qCDebug(kpLogImagelib) << "\t\ttop=" << topBorder.rect() << " refCol=" << (topBorder.exists() ? (int *)topBorder.referenceColor().toQRgb() : nullptr)
                           << " avgCol=" << (topBorder.exists() ? (int *)topBorder.averageColor().toQRgb() : nullptr);
    qCDebug(kpLogImagelib) << "\t\tbot=" << botBorder.rect() << " refCol=" << (botBorder.exists() ? (int *)botBorder.referenceColor().toQRgb() : nullptr)
                           << " avgCol=" << (botBorder.exists() ? (int *)botBorder.averageColor().toQRgb() : nullptr);
#endif

    // In case e.g. the user pastes a solid, colored-in rectangle,
    // we favor killing the bottom and right regions
    // (these regions probably contain the unwanted whitespace due
    //  to the doc being bigger than the pasted selection to start with).
    //
    // We also kill if they kiss or even overlap.

    if (leftBorder.exists() && rightBorder.exists()) {
        const kpColor leftCol = leftBorder.averageColor();
        const kpColor rightCol = rightBorder.averageColor();

        if ((numRegions == 2 && !leftCol.isSimilarTo(rightCol, processedColorSimilarity))
            || leftBorder.right() >= rightBorder.left() - 1) // kissing or overlapping
        {
#if DEBUG_KP_TOOL_AUTO_CROP
            qCDebug(kpLogImagelib) << "\tignoring left border";
#endif
            leftBorder.invalidate();
        }
    }

    if (topBorder.exists() && botBorder.exists()) {
        const kpColor topCol = topBorder.averageColor();
        const kpColor botCol = botBorder.averageColor();

        if ((numRegions == 2 && !topCol.isSimilarTo(botCol, processedColorSimilarity)) || topBorder.bottom() >= botBorder.top() - 1) // kissing or overlapping
        {
#if DEBUG_KP_TOOL_AUTO_CROP
            qCDebug(kpLogImagelib) << "\tignoring top border";
#endif
            topBorder.invalidate();
        }
    }

    mainWindow->addImageOrSelectionCommand(
        new kpTransformAutoCropCommand(static_cast<bool>(doc->selection()), leftBorder, rightBorder, topBorder, botBorder, mainWindow->commandEnvironment()));

    return true;
}
