/*
 *  SPDX-FileCopyrightText: 2010 Dmitry Kazakov <dimula73@gmail.com>
 *
 *  SPDX-License-Identifier: GPL-2.0-or-later
 */

#include "kis_canvas_controller.h"

#include <QMouseEvent>
#include <QScrollBar>
#include <QTabletEvent>

#include <klocalizedstring.h>
#include <kactioncollection.h>
#include "kis_canvas_decoration.h"
#include "kis_coordinates_converter.h"
#include "kis_canvas2.h"
#include "opengl/kis_opengl_canvas2.h"
#include "KisDocument.h"
#include "kis_image.h"
#include "KisViewManager.h"
#include "KisView.h"
#include "krita_utils.h"
#include "kis_config.h"
#include "kis_signal_compressor_with_param.h"
#include "kis_config_notifier.h"

static const int gRulersUpdateDelay = 80 /* ms */;

struct KisCanvasController::Private {
    Private(KisCanvasController *qq)
        : q(qq)
    {
        using namespace std::placeholders;

        std::function<void (QPoint)> callback(
            std::bind(&KisCanvasController::Private::emitPointerPositionChangedSignals, this, _1));

        mousePositionCompressor.reset(
            new KisSignalCompressorWithParam<QPoint>(
                gRulersUpdateDelay,
                callback,
                KisSignalCompressor::FIRST_ACTIVE));
    }

    QPointer<KisView> view;
    KisCoordinatesConverter *coordinatesConverter {0};
    KisCanvasController *q {0};
    QScopedPointer<KisSignalCompressorWithParam<QPoint> > mousePositionCompressor;

    void emitPointerPositionChangedSignals(QPoint pointerPos);
    void updateDocumentSizeAfterTransform();
    void showRotationValueOnCanvas();
    void showMirrorStateOnCanvas();
};

void KisCanvasController::Private::emitPointerPositionChangedSignals(QPoint pointerPos)
{
    if (!coordinatesConverter) return;

    QPointF documentPos = coordinatesConverter->widgetToDocument(pointerPos);

    q->proxyObject->emitDocumentMousePositionChanged(documentPos);
    q->proxyObject->emitCanvasMousePositionChanged(pointerPos);
}

void KisCanvasController::Private::updateDocumentSizeAfterTransform()
{
    // round the size of the area to the nearest integer instead of getting aligned rect
    QSize widgetSize = coordinatesConverter->imageRectInWidgetPixels().toRect().size();
    q->updateDocumentSize(widgetSize, true);

    KisCanvas2 *kritaCanvas = dynamic_cast<KisCanvas2*>(q->canvas());
    Q_ASSERT(kritaCanvas);

    kritaCanvas->notifyZoomChanged();
}


KisCanvasController::KisCanvasController(QPointer<KisView>parent, KoCanvasSupervisor *observerProvider, KisKActionCollection * actionCollection)
    : KoCanvasControllerWidget(actionCollection, observerProvider, parent),
      m_d(new Private(this))
{
    m_d->view = parent;
}

KisCanvasController::~KisCanvasController()
{
    delete m_d;
}

void KisCanvasController::setCanvas(KoCanvasBase *canvas)
{
    if (canvas) {
        KisCanvas2 *kritaCanvas = qobject_cast<KisCanvas2*>(canvas);
        m_d->coordinatesConverter =
            const_cast<KisCoordinatesConverter*>(kritaCanvas->coordinatesConverter());
    } else {
        m_d->coordinatesConverter = 0;
    }

    KoCanvasControllerWidget::setCanvas(canvas);
}

void KisCanvasController::activate()
{
    KoCanvasControllerWidget::activate();
}

QPointF KisCanvasController::currentCursorPosition() const
{
    KoCanvasBase *canvas = m_d->view->canvasBase();
    QWidget *canvasWidget = canvas->canvasWidget();
    const QPointF cursorPosWidget = canvasWidget->mapFromGlobal(QCursor::pos());

    return m_d->coordinatesConverter->widgetToDocument(cursorPosWidget);
}

void KisCanvasController::keyPressEvent(QKeyEvent *event)
{
    /**
     * Dirty Hack Alert:
     * Do not call the KoCanvasControllerWidget::keyPressEvent()
     * to avoid activation of Pan and Default tool activation shortcuts
     */
    Q_UNUSED(event);
}

void KisCanvasController::wheelEvent(QWheelEvent *event)
{
    /**
     * Dirty Hack Alert:
     * Do not call the KoCanvasControllerWidget::wheelEvent()
     * to disable the default behavior of KoCanvasControllerWidget and QAbstractScrollArea
     */
    Q_UNUSED(event);
}

bool KisCanvasController::eventFilter(QObject *watched, QEvent *event)
{
    KoCanvasBase *canvas = this->canvas();
    if (!canvas || !canvas->canvasWidget() || canvas->canvasWidget() != watched) return false;

    if (event->type() == QEvent::MouseMove) {
        QMouseEvent *mevent = static_cast<QMouseEvent*>(event);
        m_d->mousePositionCompressor->start(mevent->pos());
    } else if (event->type() == QEvent::TabletMove) {
        QTabletEvent *tevent = static_cast<QTabletEvent*>(event);
        m_d->mousePositionCompressor->start(tevent->pos());
    } else if (event->type() == QEvent::FocusIn) {
        m_d->view->syncLastActiveNodeToDocument();
    }

    return false;
}

void KisCanvasController::updateDocumentSize(const QSizeF &sz, bool recalculateCenter)
{
    KoCanvasControllerWidget::updateDocumentSize(sz, recalculateCenter);

    emit documentSizeChanged();
}

void KisCanvasController::Private::showMirrorStateOnCanvas()
{
    bool isXMirrored = coordinatesConverter->xAxisMirrored();

    view->viewManager()->
        showFloatingMessage(
            i18nc("floating message about mirroring",
                  "Horizontal mirroring: %1 ", isXMirrored ? i18n("ON") : i18n("OFF")),
            QIcon(), 500, KisFloatingMessage::Low);
}

void KisCanvasController::mirrorCanvas(bool enable)
{
    QPoint newOffset = m_d->coordinatesConverter->mirror(m_d->coordinatesConverter->widgetCenterPoint(), enable, false);
    m_d->updateDocumentSizeAfterTransform();
    setScrollBarValue(newOffset);
    m_d->showMirrorStateOnCanvas();

    emit canvasMirrorModeChanged(enable);
}

void KisCanvasController::mirrorCanvasAroundCursor(bool enable)
{
    QVariant customPos = sender()->property("customPosition");
    QPoint pos = customPos.isValid()
        ? customPos.value<QPoint>()
        : QCursor::pos();
    KoCanvasBase* canvas = m_d->view->canvasBase();
    QWidget *canvasWidget = canvas->canvasWidget();
    const QPointF cursorPosWidget = canvasWidget->mapFromGlobal(pos);
    
    if (!canvasWidget->rect().contains(cursorPosWidget.toPoint())) {
        mirrorCanvas(enable);
    } else {
        QPoint newOffset = m_d->coordinatesConverter->mirror(cursorPosWidget, enable, false);
        m_d->updateDocumentSizeAfterTransform();
        setScrollBarValue(newOffset);
        m_d->showMirrorStateOnCanvas();
    }

    emit canvasMirrorModeChanged(enable);
}

void KisCanvasController::mirrorCanvasAroundCanvas(bool enable)
{
    int docWidth = KoCanvasControllerWidget::documentSize().width();
    int docOffsetX = KoCanvasControllerWidget::canvasOffsetX();
        
    QPoint center = QPoint(docOffsetX + (docWidth / 2), 0);
    QPoint newOffset = m_d->coordinatesConverter->mirror(center, enable, false);
    
    setScrollBarValue(newOffset);
    
    // When the width is odd one side will be 1 pixel bigger, mirroring will 
    // make the canvas move left by 1 pixel hence we pan it back to place.
    if (docWidth % 2 == 1) 
        KoCanvasControllerWidget::pan(QPoint(-1, 0));
    
    m_d->updateDocumentSizeAfterTransform();
    m_d->showMirrorStateOnCanvas();

    emit canvasMirrorModeChanged(enable);
}

void KisCanvasController::Private::showRotationValueOnCanvas()
{
    qreal rotationAngle = coordinatesConverter->rotationAngle();
    view->viewManager()->
        showFloatingMessage(
            i18nc("floating message about rotation", "Rotation: %1° ",
                  KritaUtils::prettyFormatReal(rotationAngle)),
            QIcon(), 500, KisFloatingMessage::Low, Qt::AlignCenter);
}

void KisCanvasController::beginCanvasRotation()
{
    m_d->coordinatesConverter->beginRotation();
}

void KisCanvasController::endCanvasRotation()
{
    m_d->coordinatesConverter->endRotation();
}

void KisCanvasController::rotateCanvas(qreal angle, const QPointF &center, bool isNativeGesture)
{
    if(isNativeGesture) {
        m_d->coordinatesConverter->enableNatureGestureFlag();
    }
    QPoint newOffset = m_d->coordinatesConverter->rotate(center, angle);
    m_d->updateDocumentSizeAfterTransform();
    setScrollBarValue(newOffset);
    m_d->showRotationValueOnCanvas();
}

void KisCanvasController::rotateCanvas(qreal angle)
{
    rotateCanvas(angle, m_d->coordinatesConverter->widgetCenterPoint());
}

void KisCanvasController::rotateCanvasRight15()
{
    rotateCanvas(15.0);
}

void KisCanvasController::rotateCanvasLeft15()
{
    rotateCanvas(-15.0);
}

qreal KisCanvasController::rotation() const
{
    return m_d->coordinatesConverter->rotationAngle();
}

void KisCanvasController::resetCanvasRotation()
{
    QPoint newOffset = m_d->coordinatesConverter->resetRotation(m_d->coordinatesConverter->widgetCenterPoint());
    m_d->updateDocumentSizeAfterTransform();
    setScrollBarValue(newOffset);
    m_d->showRotationValueOnCanvas();
}

void KisCanvasController::slotToggleWrapAroundMode(bool value)
{
    KisCanvas2 *kritaCanvas = dynamic_cast<KisCanvas2*>(canvas());
    Q_ASSERT(kritaCanvas);

    if (!canvas()->canvasIsOpenGL() && value) {
        m_d->view->viewManager()->showFloatingMessage(i18n("You are activating wrap-around mode, but have not enabled OpenGL.\n"
                                                          "To visualize wrap-around mode, enable OpenGL."), QIcon());
    }
    kritaCanvas->setWrapAroundViewingMode(value);
    kritaCanvas->image()->setWrapAroundModePermitted(value);
}

bool KisCanvasController::wrapAroundMode() const
{
    KisCanvas2 *kritaCanvas = dynamic_cast<KisCanvas2*>(canvas());
    Q_ASSERT(kritaCanvas);

    return kritaCanvas->wrapAroundViewingMode();
}

void KisCanvasController::slotSetWrapAroundModeAxis(WrapAroundAxis value)
{
    KisCanvas2 *kritaCanvas = dynamic_cast<KisCanvas2*>(canvas());
    Q_ASSERT(kritaCanvas);

    kritaCanvas->setWrapAroundViewingModeAxis(value);
    kritaCanvas->image()->setWrapAroundModeAxis(value);
}

void KisCanvasController::slotSetWrapAroundModeAxisHV()
{
    slotSetWrapAroundModeAxis(WRAPAROUND_BOTH);
}

void KisCanvasController::slotSetWrapAroundModeAxisH()
{
    slotSetWrapAroundModeAxis(WRAPAROUND_HORIZONTAL);
}

void KisCanvasController::slotSetWrapAroundModeAxisV()
{
    slotSetWrapAroundModeAxis(WRAPAROUND_VERTICAL);
}

WrapAroundAxis KisCanvasController::wrapAroundModeAxis() const
{
    KisCanvas2 *kritaCanvas = dynamic_cast<KisCanvas2*>(canvas());
    Q_ASSERT(kritaCanvas);

    return kritaCanvas->wrapAroundViewingModeAxis();
}

void KisCanvasController::slotTogglePixelGrid(bool value)
{
    KisConfig cfg(false);
    cfg.enablePixelGrid(value);

    KisConfigNotifier::instance()->notifyPixelGridModeChanged();
}

void KisCanvasController::slotToggleLevelOfDetailMode(bool value)
{
    KisCanvas2 *kritaCanvas = dynamic_cast<KisCanvas2*>(canvas());
    Q_ASSERT(kritaCanvas);

    kritaCanvas->setLodPreferredInCanvas(value);

    bool result = levelOfDetailMode();

    if (!value || result) {
        m_d->view->viewManager()->showFloatingMessage(
            i18n("Instant Preview Mode: %1", result ?
                 i18n("ON") : i18n("OFF")),
            QIcon(), 500, KisFloatingMessage::Low);
    } else {
        QString reason;

        if (!kritaCanvas->canvasIsOpenGL()) {
            reason = i18n("Instant Preview is only supported with OpenGL activated");
        }
        else if (kritaCanvas->openGLFilterMode() == KisOpenGL::BilinearFilterMode ||
                   kritaCanvas->openGLFilterMode() == KisOpenGL::NearestFilterMode) {
            QString filteringMode =
                kritaCanvas->openGLFilterMode() == KisOpenGL::BilinearFilterMode ?
                i18n("Bilinear") : i18n("Nearest Neighbour");
            reason = i18n("Instant Preview is supported\n in Trilinear or High Quality filtering modes.\nCurrent mode is %1", filteringMode);
        }

        m_d->view->viewManager()->showFloatingMessage(
            i18n("Failed activating Instant Preview mode!\n\n%1", reason),
            QIcon(), 5000, KisFloatingMessage::Low);
    }


}

bool KisCanvasController::levelOfDetailMode() const
{
    KisCanvas2 *kritaCanvas = dynamic_cast<KisCanvas2*>(canvas());
    Q_ASSERT(kritaCanvas);

    return kritaCanvas->lodPreferredInCanvas();
}

void KisCanvasController::saveCanvasState(KisPropertiesConfiguration &config) const
{
    const QPointF &center = preferredCenter();
    config.setProperty("panX", center.x());
    config.setProperty("panY", center.y());

    config.setProperty("rotation", rotation());
    config.setProperty("mirror", m_d->coordinatesConverter->xAxisMirrored());
    config.setProperty("wrapAround", wrapAroundMode());
    config.setProperty("wrapAroundAxis", wrapAroundModeAxis());
    config.setProperty("enableInstantPreview", levelOfDetailMode());
}

void KisCanvasController::restoreCanvasState(const KisPropertiesConfiguration &config)
{
    KisCanvas2 *kritaCanvas = dynamic_cast<KisCanvas2*>(canvas());
    Q_ASSERT(kritaCanvas);

    mirrorCanvas(config.getBool("mirror", false));
    rotateCanvas(config.getFloat("rotation", 0.0f));

    const QPointF &center = preferredCenter();
    float panX = config.getFloat("panX", center.x());
    float panY = config.getFloat("panY", center.y());
    setPreferredCenter(QPointF(panX, panY));

    slotToggleWrapAroundMode(config.getBool("wrapAround", false));
    slotSetWrapAroundModeAxis((WrapAroundAxis)config.getInt("wrapAroundAxis", 0));
    kritaCanvas->setLodPreferredInCanvas(config.getBool("enableInstantPreview", false));
}

void KisCanvasController::resetScrollBars()
{
    // The scrollbar value always points at the top-left corner of the
    // bit of image we paint.

    KisDocument *doc = m_d->view->document();
    if (!doc) return;

    QRectF documentBounds = doc->documentBounds();
    QRectF viewRect = m_d->coordinatesConverter->imageToWidget(documentBounds);

    // Cancel out any existing pan
    const QRectF imageBounds = m_d->view->image()->bounds();
    const QRectF imageBB = m_d->coordinatesConverter->imageToWidget(imageBounds);
    QPointF pan = imageBB.topLeft();
    viewRect.translate(-pan);

    int drawH = viewport()->height();
    int drawW = viewport()->width();

    qreal horizontalReserve = vastScrollingFactor() * drawW;
    qreal verticalReserve = vastScrollingFactor() * drawH;

    qreal xMin = viewRect.left() - horizontalReserve;
    qreal yMin = viewRect.top() - verticalReserve;

    qreal xMax = viewRect.right() - drawW + horizontalReserve;
    qreal yMax = viewRect.bottom() - drawH + verticalReserve;

    QScrollBar *hScroll = horizontalScrollBar();
    QScrollBar *vScroll = verticalScrollBar();

    hScroll->setRange(static_cast<int>(xMin), static_cast<int>(xMax));
    vScroll->setRange(static_cast<int>(yMin), static_cast<int>(yMax));

    int fontHeight = QFontMetrics(font()).height();

    vScroll->setPageStep(drawH);
    vScroll->setSingleStep(fontHeight);
    hScroll->setPageStep(drawW);
    hScroll->setSingleStep(fontHeight);
}
