// REFACTOR: Clean up bits of this file

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

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

#include "kpLogCategories.h"
#include "kpMainWindowPrivate.h"
#include "mainWindow/kpMainWindow.h"

#include <QRegularExpression>
#include <QScrollBar>

#include <KActionCollection>
#include <KLocalizedString>
#include <KSelectAction>
#include <KStandardAction>

#include "document/kpDocument.h"
#include "generic/kpWidgetMapper.h"
#include "kpDefs.h"
#include "kpThumbnail.h"
#include "kpViewScrollableContainer.h"
#include "tools/kpTool.h"
#include "views/kpUnzoomedThumbnailView.h"
#include "views/kpZoomedThumbnailView.h"
#include "views/kpZoomedView.h"
#include "views/manager/kpViewManager.h"
#include "widgets/toolbars/kpToolToolBar.h"

static int ZoomLevelFromString(const QString &stringIn)
{
#if DEBUG_KP_MAIN_WINDOW
    qCDebug(kpLogMainWindow) << "kpMainWindow_View.cpp:ZoomLevelFromString(" << stringIn << ")";
#endif

    // Remove any non-digits kdelibs sometimes adds behind our back :( e.g.:
    //
    // 1. kdelibs adds accelerators to actions' text directly
    // 2. ',' is automatically added to change "1000%" to "1,000%"
    QString string = stringIn;
    string.remove(QRegularExpression(QStringLiteral("[^0-9]")));
#if DEBUG_KP_MAIN_WINDOW
    qCDebug(kpLogMainWindow) << "\twithout non-digits='" << string << "'";
#endif

    // Convert zoom level to number.
    bool ok = false;
    int zoomLevel = string.toInt(&ok);
#if DEBUG_KP_MAIN_WINDOW
    qCDebug(kpLogMainWindow) << "\tzoomLevel=" << zoomLevel;
#endif

    if (!ok || zoomLevel < kpView::MinZoomLevel || zoomLevel > kpView::MaxZoomLevel) {
        return 0; // error
    }

    return zoomLevel;
}

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

static QString ZoomLevelToString(int zoomLevel)
{
    return i18n("%1%", zoomLevel);
}

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

// private
void kpMainWindow::setupViewMenuZoomActions()
{
    KActionCollection *ac = actionCollection();

    d->actionActualSize = KStandardAction::actualSize(this, SLOT(slotActualSize()), ac);
    d->actionFitToPage = KStandardAction::fitToPage(this, SLOT(slotFitToPage()), ac);
    d->actionFitToWidth = KStandardAction::fitToWidth(this, SLOT(slotFitToWidth()), ac);
    d->actionFitToHeight = KStandardAction::fitToHeight(this, SLOT(slotFitToHeight()), ac);

    d->actionZoomIn = KStandardAction::zoomIn(this, SLOT(slotZoomIn()), ac);
    d->actionZoomOut = KStandardAction::zoomOut(this, SLOT(slotZoomOut()), ac);

    d->actionZoom = ac->add<KSelectAction>(QStringLiteral("view_zoom_to"));
    d->actionZoom->setText(i18n("&Zoom"));
    connect(d->actionZoom, &KSelectAction::actionTriggered, this, &kpMainWindow::slotZoom);
    d->actionZoom->setEditable(true);

    // create the zoom list for the 1st call to zoomTo() below
    d->zoomList.append(10);
    d->zoomList.append(25);
    d->zoomList.append(33);
    d->zoomList.append(50);
    d->zoomList.append(67);
    d->zoomList.append(75);
    d->zoomList.append(100);
    d->zoomList.append(200);
    d->zoomList.append(300);
    d->zoomList.append(400);
    d->zoomList.append(600);
    d->zoomList.append(800);
    d->zoomList.append(1000);
    d->zoomList.append(1200);
    d->zoomList.append(1600);
    d->zoomList.append(2400);
    d->zoomList.append(3200);
    d->zoomList.append(4000);
    d->zoomList.append(5000);
    d->zoomList.append(6000);
    d->zoomList.append(7000);
    d->zoomList.append(10000);
    d->zoomList.append(15000);
    d->zoomList.append(20000);
}

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

// private
void kpMainWindow::enableViewMenuZoomDocumentActions(bool enable)
{
    d->actionActualSize->setEnabled(enable);
    d->actionFitToPage->setEnabled(enable);
    d->actionFitToWidth->setEnabled(enable);
    d->actionFitToHeight->setEnabled(enable);

    d->actionZoomIn->setEnabled(enable);
    d->actionZoomOut->setEnabled(enable);
    d->actionZoom->setEnabled(enable);

    // TODO: for the time being, assume that we start at zoom 100%
    //       with no grid

    // This function is only called when a new document is created
    // or an existing document is closed.  So the following will
    // always be correct:

    zoomTo(100);
}

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

// private
void kpMainWindow::sendZoomListToActionZoom()
{
    QStringList items;

    const QList<int>::ConstIterator zoomListEnd(d->zoomList.end());
    for (QList<int>::ConstIterator it = d->zoomList.constBegin(); it != zoomListEnd; ++it) {
        items << ::ZoomLevelToString(*it);
    }

    // Work around a KDE bug - KSelectAction::setItems() enables the action.
    // David Faure said it won't be fixed because it's a feature used by
    // KRecentFilesAction.
    bool e = d->actionZoom->isEnabled();
    d->actionZoom->setItems(items);
    if (e != d->actionZoom->isEnabled()) {
        d->actionZoom->setEnabled(e);
    }
}

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

// private
void kpMainWindow::zoomToPre(int zoomLevel)
{
    // We're called quite early in the init process and/or when there might
    // not be a document or a view so we have a lot of "if (ptr)" guards.

#if DEBUG_KP_MAIN_WINDOW
    qCDebug(kpLogMainWindow) << "kpMainWindow::zoomToPre(" << zoomLevel << ")";
#endif

    zoomLevel = qBound(kpView::MinZoomLevel, zoomLevel, kpView::MaxZoomLevel);

    int index = 0;
    QList<int>::Iterator it = d->zoomList.begin();

    while (index < d->zoomList.count() && zoomLevel > *it) {
        it++;
        index++;
    }

    if (zoomLevel != *it) {
        d->zoomList.insert(it, zoomLevel);
    }

    // OPT: We get called twice on startup.  sendZoomListToActionZoom() is very slow.
    sendZoomListToActionZoom();

#if DEBUG_KP_MAIN_WINDOW
    qCDebug(kpLogMainWindow) << "\tsetCurrentItem(" << index << ")";
#endif
    d->actionZoom->setCurrentItem(index);
#if DEBUG_KP_MAIN_WINDOW
    qCDebug(kpLogMainWindow) << "\tcurrentItem=" << d->actionZoom->currentItem() << " action=" << d->actionZoom->action(d->actionZoom->currentItem())
                             << " checkedAction" << d->actionZoom->selectableActionGroup()->checkedAction();
#endif

    if (viewMenuDocumentActionsEnabled()) {
        d->actionActualSize->setEnabled(zoomLevel != 100);

        d->actionZoomIn->setEnabled(d->actionZoom->currentItem() < d->zoomList.count() - 1);
        d->actionZoomOut->setEnabled(d->actionZoom->currentItem() > 0);
    }

    // TODO: Is this actually needed?
    if (d->viewManager) {
        d->viewManager->setQueueUpdates();
    }

    if (d->scrollView) {
        d->scrollView->setUpdatesEnabled(false);
    }
}

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

// private
void kpMainWindow::zoomToPost()
{
#if DEBUG_KP_MAIN_WINDOW && 1
    qCDebug(kpLogMainWindow) << "kpMainWindow::zoomToPost()";
#endif

    if (d->mainView) {
        actionShowGridUpdate();
        updateMainViewGrid();

        // Since Zoom Level KSelectAction on ToolBar grabs focus after changing
        // Zoom, switch back to the Main View.
        // TODO: back to the last view
        d->mainView->setFocus();
    }

    // The view magnified and moved beneath the cursor
    if (tool()) {
        tool()->somethingBelowTheCursorChanged();
    }

    if (d->scrollView) {
        // TODO: setUpdatesEnabled() should really return to old value
        //       - not necessarily "true"
        d->scrollView->setUpdatesEnabled(true);
    }

    if (d->viewManager && d->viewManager->queueUpdates() /*just in case*/) {
        d->viewManager->restoreQueueUpdates();
    }

    setStatusBarZoom(d->mainView ? d->mainView->zoomLevelX() : 0);

#if DEBUG_KP_MAIN_WINDOW && 1
    qCDebug(kpLogMainWindow) << "kpMainWindow::zoomToPost() done";
#endif
}

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

// private
void kpMainWindow::zoomTo(int zoomLevel, bool centerUnderCursor)
{
    zoomToPre(zoomLevel);

    if (d->scrollView && d->mainView) {
#if DEBUG_KP_MAIN_WINDOW && 1
        qCDebug(kpLogMainWindow) << "\tscrollView   contentsX=" << d->scrollView->horizontalScrollBar()->value()
                                 << " contentsY=" << d->scrollView->verticalScrollBar()->value() << " contentsWidth=" << d->scrollView->widget()->width()
                                 << " contentsHeight=" << d->scrollView->widget()->height() << " visibleWidth=" << d->scrollView->viewport()->width()
                                 << " visibleHeight=" << d->scrollView->viewport()->height() << " oldZoomX=" << d->mainView->zoomLevelX()
                                 << " oldZoomY=" << d->mainView->zoomLevelY() << " newZoom=" << zoomLevel;
#endif

        // TODO: when changing from no scrollbars to scrollbars, Qt lies about
        //       visibleWidth() & visibleHeight() (doesn't take into account the
        //       space taken by the would-be scrollbars) until it updates the
        //       scrollview; hence the centering is off by about 5-10 pixels.

        // TODO: Use visibleRect() for greater accuracy?
        //       Or use kpAbstractScrollAreaUtils::EstimateUsableArea()
        //       instead of ScrollView::visible{Width,Height}(), as
        //       per zoomToRect()?

        int viewX, viewY;

        bool targetDocAvail = false;
        double targetDocX = -1, targetDocY = -1;

        if (centerUnderCursor && d->viewManager && d->viewManager->viewUnderCursor()) {
            kpView *const vuc = d->viewManager->viewUnderCursor();
            QPoint viewPoint = vuc->mouseViewPoint();

            // vuc->transformViewToDoc() returns QPoint which only has int
            // accuracy so we do X and Y manually.
            targetDocX = vuc->transformViewToDocX(viewPoint.x());
            targetDocY = vuc->transformViewToDocY(viewPoint.y());
            targetDocAvail = true;

            if (vuc != d->mainView) {
                viewPoint = vuc->transformViewToOtherView(viewPoint, d->mainView);
            }

            viewX = viewPoint.x();
            viewY = viewPoint.y();
        } else {
            viewX = d->scrollView->horizontalScrollBar()->value() + qMin(d->mainView->width(), d->scrollView->viewport()->width()) / 2;
            viewY = d->scrollView->verticalScrollBar()->value() + qMin(d->mainView->height(), d->scrollView->viewport()->height()) / 2;
        }

        int newCenterX = viewX * zoomLevel / d->mainView->zoomLevelX();
        int newCenterY = viewY * zoomLevel / d->mainView->zoomLevelY();

        // Do the zoom.
        d->mainView->setZoomLevel(zoomLevel, zoomLevel);

#if DEBUG_KP_MAIN_WINDOW && 1
        qCDebug(kpLogMainWindow) << "\tvisibleWidth=" << d->scrollView->viewport()->width() << " visibleHeight=" << d->scrollView->viewport()->height();
        qCDebug(kpLogMainWindow) << "\tnewCenterX=" << newCenterX << " newCenterY=" << newCenterY;
#endif

        d->scrollView->horizontalScrollBar()->setValue(newCenterX - (d->scrollView->viewport()->width() / 2));
        d->scrollView->verticalScrollBar()->setValue(newCenterY - (d->scrollView->viewport()->height() / 2));

        if (centerUnderCursor && targetDocAvail && d->viewManager && d->viewManager->viewUnderCursor()) {
            // Move the mouse cursor so that it is still above the same
            // document pixel as before the zoom.

            kpView *const vuc = d->viewManager->viewUnderCursor();

#if DEBUG_KP_MAIN_WINDOW
            qCDebug(kpLogMainWindow) << "\tcenterUnderCursor: reposition cursor; viewUnderCursor=" << vuc->objectName();
#endif

            const auto viewX = vuc->transformDocToViewX(targetDocX);
            const auto viewY = vuc->transformDocToViewY(targetDocY);
            // Rounding error from zooming in and out :(
            // TODO: do everything in terms of tool doc points in type "double".
            const QPoint viewPoint(static_cast<int>(viewX), static_cast<int>(viewY));
#if DEBUG_KP_MAIN_WINDOW
            qCDebug(kpLogMainWindow) << "\t\tdoc: (" << targetDocX << "," << targetDocY << ")"
                                     << " viewUnderCursor: (" << viewX << "," << viewY << ")";
#endif

            if (vuc->visibleRegion().contains(viewPoint)) {
                const QPoint globalPoint = kpWidgetMapper::toGlobal(vuc, viewPoint);
#if DEBUG_KP_MAIN_WINDOW
                qCDebug(kpLogMainWindow) << "\t\tglobalPoint=" << globalPoint;
#endif

                // TODO: Determine some sane cursor flashing indication -
                //       cursor movement is convenient but not conventional.
                //
                //       Major problem: if using QApplication::setOverrideCursor()
                //           and in some stage of flash and window quits.
                //
                //           Or if using kpView::setCursor() and change tool.
                QCursor::setPos(globalPoint);
            }
            // e.g. Zoom to 200%, scroll mainView to bottom-right.
            // Unzoomed Thumbnail shows top-left portion of bottom-right of
            // mainView.
            //
            // Aim cursor at bottom-right of thumbnail and zoom out with
            // CTRL+Wheel.
            //
            // If mainView is now small enough to largely not need scrollbars,
            // Unzoomed Thumbnail scrolls to show _top-left_ portion
            // _of top-left_ of mainView.
            //
            // Unzoomed Thumbnail no longer contains the point we zoomed out
            // on top of.
            else {
#if DEBUG_KP_MAIN_WINDOW
                qCDebug(kpLogMainWindow) << "\t\twon't move cursor - would get outside view";
#endif

                // TODO: Sane cursor flashing indication that indicates
                //       that the normal cursor movement didn't happen.
            }
        }

#if DEBUG_KP_MAIN_WINDOW && 1
        qCDebug(kpLogMainWindow) << "\t\tcheck (contentsX=" << d->scrollView->horizontalScrollBar()->value()
                                 << ",contentsY=" << d->scrollView->verticalScrollBar()->value() << ")";
#endif
    }

    zoomToPost();
}

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

// private
void kpMainWindow::zoomToRect(const QRect &normalizedDocRect, bool accountForGrips, bool careAboutWidth, bool careAboutHeight)
{
#if DEBUG_KP_MAIN_WINDOW
    qCDebug(kpLogMainWindow) << "kpMainWindow::zoomToRect(normalizedDocRect=" << normalizedDocRect << ",accountForGrips=" << accountForGrips
                             << ",careAboutWidth=" << careAboutWidth << ",careAboutHeight=" << careAboutHeight << ")";
#endif
    // You can't care about nothing.
    Q_ASSERT(careAboutWidth || careAboutHeight);

    // The size of the scroll view minus the current or future scrollbars.
    const QSize usableScrollArea(d->scrollView->maximumViewportSize().width() - d->scrollView->verticalScrollBar()->sizeHint().width(),
                                 d->scrollView->maximumViewportSize().height() - d->scrollView->horizontalScrollBar()->sizeHint().height());

#if DEBUG_KP_MAIN_WINDOW
    qCDebug(kpLogMainWindow) << "size=" << d->scrollView->maximumViewportSize() << "scrollbar w=" << d->scrollView->verticalScrollBar()->sizeHint().width()
                             << "usableSize=" << usableScrollArea;
#endif
    // Handle rounding error, mis-estimating the scroll view size and
    // cosmic rays.  We do this because we really don't want unnecessary
    // scrollbars.  This seems to need to be at least 2 for slotFitToWidth()
    // and friends.
    // least 2.
    // TODO: I might have fixed this but check later.
    const int slack = 0;

    // The grip and slack are in view coordinates but are never zoomed.
    const int viewWidth = usableScrollArea.width() - (accountForGrips ? kpGrip::Size : 0) - slack;
    const int viewHeight = usableScrollArea.height() - (accountForGrips ? kpGrip::Size : 0) - slack;

    // We want the selected document rectangle to fill the scroll view.
    //
    // The integer arithmetic rounds down, rather than to the nearest zoom
    // level, as rounding down guarantees that the view, at the zoom level,
    // will fit inside <viewWidth> x <viewHeight>.
    const int zoomX = careAboutWidth ? qMax(1, viewWidth * 100 / normalizedDocRect.width()) : INT_MAX;
    const int zoomY = careAboutHeight ? qMax(1, viewHeight * 100 / normalizedDocRect.height()) : INT_MAX;

    // Since kpView only supports identical horizontal and vertical zooms,
    // choose the one that will show the greatest amount of document
    // content.
    const int zoomLevel = qMin(zoomX, zoomY);

#if DEBUG_KP_MAIN_WINDOW
    qCDebug(kpLogMainWindow) << "\tzoomX=" << zoomX << " zoomY=" << zoomY << " -> zoomLevel=" << zoomLevel;
#endif

    zoomToPre(zoomLevel);
    {
        d->mainView->setZoomLevel(zoomLevel, zoomLevel);

        const QPoint viewPoint = d->mainView->transformDocToView(normalizedDocRect.topLeft());

        d->scrollView->horizontalScrollBar()->setValue(viewPoint.x());
        d->scrollView->verticalScrollBar()->setValue(viewPoint.y());
    }
    zoomToPost();
}

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

// public slot
void kpMainWindow::slotActualSize()
{
    zoomTo(100);
}

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

// public slot
void kpMainWindow::slotFitToPage()
{
    if (d->document) {
        zoomToRect(d->document->rect(), true /*account for grips*/, true /*care about width*/, true /*care about height*/);
    }
}

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

// public slot
void kpMainWindow::slotFitToWidth()
{
    if (d->document) {
        const QRect docRect(0 /*x*/,
                            static_cast<int>(d->mainView->transformViewToDocY(d->scrollView->verticalScrollBar()->value())) /*maintain y*/,
                            d->document->width(),
                            1 /*don't care about height*/);
        zoomToRect(docRect, true /*account for grips*/, true /*care about width*/, false /*don't care about height*/);
    }
}

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

// public slot
void kpMainWindow::slotFitToHeight()
{
    if (d->document) {
        const QRect docRect(static_cast<int>(d->mainView->transformViewToDocX(d->scrollView->horizontalScrollBar()->value())) /*maintain x*/,
                            0 /*y*/,
                            1 /*don't care about width*/,
                            d->document->height());
        zoomToRect(docRect, true /*account for grips*/, false /*don't care about width*/, true /*care about height*/);
    }
}

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

// public
void kpMainWindow::zoomIn(bool centerUnderCursor)
{
#if DEBUG_KP_MAIN_WINDOW
    qCDebug(kpLogMainWindow) << "kpMainWindow::zoomIn(centerUnderCursor=" << centerUnderCursor << ") currentItem=" << d->actionZoom->currentItem();
#endif
    const int targetItem = d->actionZoom->currentItem() + 1;

    if (targetItem >= static_cast<int>(d->zoomList.count())) {
        return;
    }

    d->actionZoom->setCurrentItem(targetItem);

#if DEBUG_KP_MAIN_WINDOW
    qCDebug(kpLogMainWindow) << "\tnew currentItem=" << d->actionZoom->currentItem();
#endif

    zoomAccordingToZoomAction(centerUnderCursor);
}

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

// public
void kpMainWindow::zoomOut(bool centerUnderCursor)
{
#if DEBUG_KP_MAIN_WINDOW
    qCDebug(kpLogMainWindow) << "kpMainWindow::zoomOut(centerUnderCursor=" << centerUnderCursor << ") currentItem=" << d->actionZoom->currentItem();
#endif
    const int targetItem = d->actionZoom->currentItem() - 1;

    if (targetItem < 0) {
        return;
    }

    d->actionZoom->setCurrentItem(targetItem);

#if DEBUG_KP_MAIN_WINDOW
    qCDebug(kpLogMainWindow) << "\tnew currentItem=" << d->actionZoom->currentItem();
#endif

    zoomAccordingToZoomAction(centerUnderCursor);
}

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

// public slot
void kpMainWindow::slotZoomIn()
{
#if DEBUG_KP_MAIN_WINDOW
    qCDebug(kpLogMainWindow) << "kpMainWindow::slotZoomIn ()";
#endif

    zoomIn(false /*don't center under cursor*/);
}

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

// public slot
void kpMainWindow::slotZoomOut()
{
#if DEBUG_KP_MAIN_WINDOW
    qCDebug(kpLogMainWindow) << "kpMainWindow::slotZoomOut ()";
#endif

    zoomOut(false /*don't center under cursor*/);
}

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

// public
void kpMainWindow::zoomAccordingToZoomAction(bool centerUnderCursor)
{
#if DEBUG_KP_MAIN_WINDOW
    qCDebug(kpLogMainWindow) << "kpMainWindow::zoomAccordingToZoomAction(centerUnderCursor=" << centerUnderCursor
                             << ") currentItem=" << d->actionZoom->currentItem() << " currentText=" << d->actionZoom->currentText();
#endif

    // This might be a new zoom level the user has typed in.
    zoomTo(::ZoomLevelFromString(d->actionZoom->currentText()), centerUnderCursor);
}

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

// private slot
void kpMainWindow::slotZoom()
{
#if DEBUG_KP_MAIN_WINDOW
    qCDebug(kpLogMainWindow) << "kpMainWindow::slotZoom () index=" << d->actionZoom->currentItem() << " text='" << d->actionZoom->currentText() << "'";
#endif

    zoomAccordingToZoomAction(false /*don't center under cursor*/);
}

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