/*
 * Copyright (c) 2021 Frank Fischer <frank-fischer@shadow-soft.de>
 *
 * This program is free software: you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see  <http://www.gnu.org/licenses/>
 */

#include "CutView.hxx"

#include <QDebug>
#include <QtGui/QImage>
#include <QtGui/QPainter>

#include "Convert.hxx"
#include "EdgeDetection.hxx"
#include "ScanImage.hxx"
#include "Scanner.hxx"

static QPointF scale(const QPointF& p, int width, int height)
{
    return {p.x() / width, p.y() / height};
}

static QPointF scale(const QPointF& p, const std::unique_ptr<EdgeDetection>& edges)
{
    return scale(p, edges->width(), edges->height());
}

static QPointF unscale(const QPointF& p, int width, int height)
{
    return {p.x() * width, p.y() * height};
}

static QPointF unscale(const QPointF& p, const std::unique_ptr<EdgeDetection>& edges)
{
    return unscale(p, edges->width(), edges->height());
}

struct CutView::Data {
    std::unique_ptr<EdgeDetection> edges;
    std::shared_ptr<ScanImage> scanImage;

    QMetaObject::Connection orientationChangedConnection = {};
    bool updatePaintedSize = false;

    QPointF getPoint(int which) const
    {
        if (scanImage == nullptr || edges == nullptr) return {};

        auto orien = scanImage->orientation();
        QPointF pnt;
        switch ((4 + which - orien) % 4) {
            case 0: pnt = edges->topLeft(); break;
            case 1: pnt = edges->topRight(); break;
            case 2: pnt = edges->bottomRight(); break;
            case 3: pnt = edges->bottomLeft(); break;
            default: Q_ASSERT(false); break;
        };

        pnt = ::scale(pnt, edges);

        switch (orien % 4) {
            case 0: return pnt;
            case 1: return {1 - pnt.y(), pnt.x()};
            case 2: return {1 - pnt.x(), 1 - pnt.y()};
            case 3: return {pnt.y(), 1 - pnt.x()};
        }

        return {};
    }

    bool setPoint(int which, const QPointF& p)
    {
        if (scanImage == nullptr || edges == nullptr) return false;

        if (getPoint(which) == p) return false;

        auto orien = scanImage->orientation();
        QPointF pnt = p;
        switch (orien % 4) {
            case 0: break;
            case 1: pnt = {pnt.y(), 1 - pnt.x()}; break;
            case 2: pnt = {1 - pnt.x(), 1 - pnt.y()}; break;
            case 3: pnt = {1 - pnt.y(), pnt.x()}; break;
        }

        pnt = ::unscale(pnt, edges);

        switch ((4 + which - orien) % 4) {
            case 0: edges->setTopLeft(pnt); break;
            case 1: edges->setTopRight(pnt); break;
            case 2: edges->setBottomRight(pnt); break;
            case 3: edges->setBottomLeft(pnt); break;
        };

        return true;
    }

    QPointF getEdgePoint(int which) const
    {
        if (scanImage == nullptr || edges == nullptr) return {};

        auto orien = scanImage->orientation();
        QPointF pnt;
        switch ((4 + which - orien) % 4) {
            case 0: pnt = edges->topPoint(); break;
            case 1: pnt = edges->rightPoint(); break;
            case 2: pnt = edges->bottomPoint(); break;
            case 3: pnt = edges->leftPoint(); break;
            default: Q_ASSERT(false); break;
        };

        pnt = ::scale(pnt, edges);

        switch (orien % 4) {
            case 0: return pnt;
            case 1: return {1 - pnt.y(), pnt.x()};
            case 2: return {1 - pnt.x(), 1 - pnt.y()};
            case 3: return {pnt.y(), 1 - pnt.x()};
        }

        return {};
    }

    bool setEdgePoint(int which, const QPointF& p)
    {
        if (scanImage == nullptr || edges == nullptr) return false;

        if (getPoint(which) == p) return false;

        auto orien = scanImage->orientation();
        QPointF pnt = p;
        switch (orien % 4) {
            case 0: break;
            case 1: pnt = {pnt.y(), 1 - pnt.x()}; break;
            case 2: pnt = {1 - pnt.x(), 1 - pnt.y()}; break;
            case 3: pnt = {1 - pnt.y(), pnt.x()}; break;
        }

        pnt = ::unscale(pnt, edges);

        switch ((4 + which - orien) % 4) {
            case 0: edges->setTopPoint(pnt); break;
            case 1: edges->setRightPoint(pnt); break;
            case 2: edges->setBottomPoint(pnt); break;
            case 3: edges->setLeftPoint(pnt); break;
        };

        return true;
    }
};

CutView::CutView(QQuickItem* parent)
    : ScanImageView(parent),
      d(new Data)
{
}

CutView::~CutView() = default;

int CutView::orientation() const
{
    return d->scanImage != nullptr ? d->scanImage->orientation() : 0;
}

void CutView::setOrientation(int orientation)
{
    if (d->scanImage != nullptr) d->scanImage->setOrientation(orientation);
}

QPointF CutView::topLeft() const
{
    return d->getPoint(0);
}

void CutView::setTopLeft(QPointF topleft)
{
    if (d->setPoint(0, topleft)) {
        emit topLeftChanged();
        emit topChanged();
        emit leftChanged();
    }
}

QPointF CutView::topRight() const
{
    return d->getPoint(1);
}

void CutView::setTopRight(QPointF topright)
{
    if (d->setPoint(1, topright)) {
        emit topRightChanged();
        emit topChanged();
        emit rightChanged();
    }
}

QPointF CutView::bottomRight() const
{
    return d->getPoint(2);
}

void CutView::setBottomRight(QPointF bottomright)
{
    if (d->setPoint(2, bottomright)) {
        emit bottomRightChanged();
        emit bottomChanged();
        emit rightChanged();
    }
}

QPointF CutView::bottomLeft() const
{
    return d->getPoint(3);
}

void CutView::setBottomLeft(QPointF bottomleft)
{
    if (d->setPoint(3, bottomleft)) {
        emit bottomLeftChanged();
        emit bottomChanged();
        emit leftChanged();
    }
}

void CutView::setTop(QPointF top)
{
    if (d->setEdgePoint(0, top)) {
        emit topChanged();
        emit topLeftChanged();
        emit topRightChanged();
        emit leftChanged();
        emit rightChanged();
    }
}

QPointF CutView::top() const
{
    return d->getEdgePoint(0);
}

void CutView::setRight(QPointF right)
{
    if (d->setEdgePoint(1, right)) {
        emit rightChanged();
        emit topRightChanged();
        emit bottomRightChanged();
        emit topChanged();
        emit bottomChanged();
    }
}

QPointF CutView::right() const
{
    return d->getEdgePoint(1);
}

void CutView::setBottom(QPointF bottom)
{
    if (d->setEdgePoint(2, bottom)) {
        emit bottomChanged();
        emit bottomLeftChanged();
        emit bottomRightChanged();
        emit leftChanged();
        emit rightChanged();
    }
}

QPointF CutView::bottom() const
{
    return d->getEdgePoint(2);
}

void CutView::setLeft(QPointF left)
{
    if (d->setEdgePoint(3, left)) {
        emit leftChanged();
        emit topLeftChanged();
        emit bottomLeftChanged();
        emit topChanged();
        emit bottomChanged();
    }
}

QPointF CutView::left() const
{
    return d->getEdgePoint(3);
}

void CutView::rotateLeft()
{
    if (d->scanImage != nullptr) {
        d->scanImage->setOrientation(d->scanImage->orientation() - 1);
        d->updatePaintedSize = true;
    }
}

void CutView::rotateRight()
{
    if (d->scanImage != nullptr) {
        d->scanImage->setOrientation(d->scanImage->orientation() + 1);
        d->updatePaintedSize = true;
    }
}

bool CutView::hasAutoSelection() const
{
    return d->edges != nullptr ? d->edges->hasAutoDetection() : false;
}

bool CutView::isAutoDetectionRunning() const
{
    return d->edges != nullptr ? d->edges->isAutoDetectionRunning() : false;
}

void CutView::selectAll()
{
    if (d->edges != nullptr) {
        d->edges->setTopLeft({0, 0});
        d->edges->setTopRight({static_cast<qreal>(d->edges->width()), 0});
        d->edges->setBottomRight({static_cast<qreal>(d->edges->width()), static_cast<qreal>(d->edges->height())});
        d->edges->setBottomLeft({0, static_cast<qreal>(d->edges->height())});

        emit topLeftChanged();
        emit topRightChanged();
        emit bottomRightChanged();
        emit bottomLeftChanged();
        emit topChanged();
        emit bottomChanged();
        emit leftChanged();
        emit rightChanged();
    }
}

void CutView::selectAuto()
{
    if (d->edges != nullptr && d->edges->selectAuto()) {
        emit topLeftChanged();
        emit topRightChanged();
        emit bottomRightChanged();
        emit bottomLeftChanged();
        emit topChanged();
        emit bottomChanged();
        emit leftChanged();
        emit rightChanged();
    }
}

void CutView::paint(QPainter* painter)
{
    if (d->scanImage == nullptr) return;

    auto cv_image = d->scanImage->original();
    auto image = cvMatToQImage(cv_image);

    auto orien = orientation();
    auto imgw = image.width();
    auto imgh = image.height();

    if (imgw > 0 && imgh > 0) {
        auto wratio = static_cast<qreal>(orien % 2 == 0 ? width() : height()) / imgw;
        auto hratio = static_cast<qreal>(orien % 2 == 0 ? height() : width()) / imgh;
        auto ratio = std::min(wratio, hratio);

        auto w = imgw * ratio;
        auto h = imgh * ratio;

        if (orien % 2 == 1) std::swap(w, h);

        setPaintedSize(w, h);

        painter->save();
        painter->rotate(orien * 90);

        QPointF target;
        switch (orien) {
            case 0: target = {(width() - w) / 2, (height() - h) / 2}; break;
            case 1: target = {height() / 2 - h / 2, -width() / 2 - w / 2}; break;
            case 2: target = {-width() / 2 - w / 2, -height() / 2 - h / 2}; break;
            case 3: target = {-height() / 2 - h / 2, width() / 2 - w / 2}; break;
            default: Q_ASSERT(false);
        }

        painter->drawImage(QRectF{target, QSize(imgw * ratio, imgh * ratio)}, image);

        painter->restore();
    }

    if (d->updatePaintedSize) {
        d->updatePaintedSize = false;

        // We need to postpone the rotation until the repaint is
        // complete because otherwise paintedWidth and paintedHeight
        // will not be up-to-date. The problem is that we do not know
        // `width` and `height` until we actually repaint the widget,
        // hence we do not now `paintedWidth` and `paintedHeight`
        // either.

        emit topLeftChanged();
        emit topRightChanged();
        emit bottomRightChanged();
        emit bottomLeftChanged();
        emit topChanged();
        emit bottomChanged();
        emit leftChanged();
        emit rightChanged();
        emit rotationChanged();
    }
}

void CutView::onNewImage()
{
    disconnect(d->orientationChangedConnection);
    d->orientationChangedConnection = {};
    if (auto s = scanner(); s != nullptr) {
        d->scanImage = s->currentImage();
        if (d->scanImage != nullptr) {
            // set up connections for the new ScanImage ...
            d->orientationChangedConnection = connect(d->scanImage.get(), &ScanImage::orientationChanged, this, &CutView::onOrientationChanged);

            // ... and its edge-detection data structure
            d->edges = std::make_unique<EdgeDetection>(d->scanImage->original());
            connect(d->edges.get(), &EdgeDetection::hasAutoDetectionChanged, this, &CutView::hasAutoSelectionChanged);
            connect(d->edges.get(), &EdgeDetection::isAutoDetectionRunningChanged, this, &CutView::isAutoDetectionRunningChanged);

            emit hasAutoSelectionChanged();
            emit isAutoDetectionRunningChanged();

            // start right away
            d->edges->startAutoDetect();

            // this means that the settings will be initialized from the scanImage
            d->setPoint(0, d->scanImage->topLeft());
            d->setPoint(1, d->scanImage->topRight());
            d->setPoint(2, d->scanImage->bottomRight());
            d->setPoint(3, d->scanImage->bottomLeft());

            emit topLeftChanged();
            emit topRightChanged();
            emit bottomRightChanged();
            emit bottomLeftChanged();
            emit topChanged();
            emit bottomChanged();
            emit leftChanged();
            emit rightChanged();
            emit rotationChanged();
        }
    }

    update();
}

QImage CutView::image() const
{
    return d->scanImage != nullptr ? cvMatToQImage(d->scanImage->original()).copy() : QImage();
}

void CutView::onOrientationChanged()
{
    if (d->scanImage != nullptr) {
        update();
    }

    emit topLeftChanged();
    emit topRightChanged();
    emit bottomRightChanged();
    emit bottomLeftChanged();
    emit topChanged();
    emit bottomChanged();
    emit leftChanged();
    emit rightChanged();
    emit rotationChanged();

    emit orientationChanged();
}

void CutView::updateSnappyEdges()
{
    if (d->edges != nullptr) {
        d->edges->fixNonSnappyEdges();
    }
}

void CutView::apply()
{
    if (d->edges != nullptr && d->scanImage != nullptr) {
        d->scanImage->setTopLeft(topLeft());
        d->scanImage->setTopRight(topRight());
        d->scanImage->setBottomRight(bottomRight());
        d->scanImage->setBottomLeft(bottomLeft());
        d->scanImage->applyCut();
    }
}
