/******************************************************************************
 * QSkinny - Copyright (C) The authors
 *           SPDX-License-Identifier: BSD-3-Clause
 *****************************************************************************/

#include "QskPopup.h"
#include "QskAspect.h"
#include "QskInputGrabber.h"
#include "QskQuick.h"
#include "QskWindow.h"
#include "QskEvent.h"
#include "QskPlatform.h"
#include "QskHintAnimator.h"

#include <qpa/qplatformintegration.h>

QSK_QT_PRIVATE_BEGIN
#include <private/qquickwindow_p.h>
#include <private/qquickitem_p.h>
QSK_QT_PRIVATE_END

QSK_SUBCONTROL( QskPopup, Overlay )
QSK_SYSTEM_STATE( QskPopup, Closed, QskAspect::FirstSystemState << 1 )

static void qskSetFocus( QQuickItem* item, bool on )
{
    if ( item->window() == nullptr )
        return;

    /*
        For unknown reasons Qt::PopupFocusReason is blocked inside of
        QQuickItem::setFocus. So let's bypass it calling
        QQuickWindowPrivate::setFocusInScope/clearFocusInScope directly,
     */

    if ( const auto scope = qskNearestFocusScope( item ) )
    {
#if QT_VERSION >= QT_VERSION_CHECK( 6, 1, 0 )
        auto dw = QQuickItemPrivate::get( item )->deliveryAgentPrivate();
#else
        auto dw = QQuickWindowPrivate::get( item->window() );
#endif
        if ( on )
        {
            dw->setFocusInScope( scope, item, Qt::PopupFocusReason );
        }
        else
        {
            // to avoid an assertion we have to check if we have the subFocus
            if ( QQuickItemPrivate::get( scope )->subFocusItem == item )
                dw->clearFocusInScope( scope, item, Qt::PopupFocusReason );
        }
    }
}

static inline void qskSendPopupEvent(
    QQuickWindow* window, QskPopup* popup, bool on )
{
    if ( window )
    {
        const auto type = on ? QskEvent::PopupAdded : QskEvent::PopupRemoved;

        QskPopupEvent event( type, popup );
        QCoreApplication::sendEvent( window, &event );
    }
}

static bool qskReplayMousePress()
{
    if ( const auto pf = qskPlatformIntegration() )
    {
        const auto styleHint = QPlatformIntegration::ReplayMousePressOutsidePopup;
        return pf->styleHint( styleHint ).toBool();
    }

    return false;
}

static inline QskAspect qskEffectiveFadingAspect( const QskPopup* popup )
{
    auto aspect = popup->fadingAspect();
    aspect.setSubcontrol( popup->effectiveSubcontrol( aspect.subControl() ) );

    return aspect;
}

static void qskStartFading( QskPopup* popup, bool on )
{
    const auto aspect = qskEffectiveFadingAspect( popup );

    auto hint = popup->animationHint( aspect );

    if ( hint.isValid() )
    {
        hint.updateFlags = QskAnimationHint::UpdatePolish | QskAnimationHint::UpdateNode;

        const qreal from = on ? 0.0 : 1.0;
        const qreal to = on ? 1.0 : 0.0;

        popup->startTransition( aspect, hint, from, to );
    }
}

namespace
{
    class InputGrabber final : public QskInputGrabber
    {
        using Inherited = QskInputGrabber;

      public:
        InputGrabber( QskPopup* parent )
            : Inherited( parent )
        {
        }

      protected:
        bool event( QEvent* event ) override
        {
            bool ok = Inherited::event( event );

            const int eventType = event->type();

            if ( eventType == QEvent::MouseButtonPress )
            {
                if ( auto popup = static_cast< QskPopup* >( parentItem() ) )
                {
                    if ( event->isAccepted() &&
                        ( popup->popupFlags() & QskPopup::CloseOnPressOutside ) )
                    {
                        popup->close();

                        if ( qskReplayMousePress() )
                        {
                            // TODO ...
                        }
                    }
                }
            }
            else if ( eventType == QskEvent::GeometryChange )
            {
                if ( auto popup = static_cast< QskPopup* >( parentItem() ) )
                {
                    if ( popup->hasOverlay() )
                        popup->update();
                }
            }

            return ok;
        }
    };
}

class QskPopup::PrivateData
{
  public:
    PrivateData()
        : flags( 0 )
        , isModal( false )
        , autoGrabFocus( true )
        , handoverFocus( true )
    {
    }

    InputGrabber* inputGrabber = nullptr;

    uint priority = 0;

    int flags           : 4;
    bool isModal        : 1;

    const bool autoGrabFocus : 1;
    const bool handoverFocus : 1;
};

QskPopup::QskPopup( QQuickItem* parent )
    : Inherited( parent )
    , m_data( new PrivateData() )
{
    // initially the popup is closed and invisible
    Inherited::setVisible( false );
    setSkinStateFlag( QskPopup::Closed );

    /*
        We need to stop event propagation.

        Unfortunatly derived classes can't use setAcceptedMouseButtons anymore.
        Need to think about a solution TODO ...
     */

    setAcceptedMouseButtons( Qt::AllButtons );
    setWheelEnabled( true );

    // we don't want to be resized by layout code
    setPlacementPolicy( QskPlacementPolicy::Ignore );

    setFlag( ItemIsFocusScope, true );
    setTabFence( true );
    setFocusPolicy( Qt::StrongFocus );

    /*
        sending a notification, that can be used to register popups
        for some sort of popup/window management
     */
    qskSendPopupEvent( window(), this, true );
}

QskPopup::~QskPopup()
{
    qskSendPopupEvent( window(), this, false );
}

void QskPopup::open()
{
    setOpen( true );
}

void QskPopup::close()
{
    setOpen( false );
}

void QskPopup::toggle()
{
    setOpen( !isOpen() );
}

void QskPopup::setOpen( bool on )
{
    if ( on == isOpen() )
        return;

    if ( on )
        QskControl::setVisible( on );

    setSkinStateFlag( QskPopup::Closed, !on );

    Q_EMIT openChanged( on );

    if ( on )
        Q_EMIT opened();
    else
        Q_EMIT closed();

    if ( !on )
        grabFocus( false );

    qskStartFading( this, on );

    if ( isFading() )
    {
        Q_EMIT fadingChanged( true );
    }
    else
    {
        if ( !on )
        {
            Inherited::setVisible( false );

            if ( testPopupFlag( QskPopup::DeleteOnClose ) )
                deleteLater();
        }
    }
}

bool QskPopup::isOpen() const
{
    return !hasSkinState( QskPopup::Closed );
}

QskAspect QskPopup::fadingAspect() const
{
    return QskAspect();
}

bool QskPopup::isFading() const
{
    const auto aspect = qskEffectiveFadingAspect( this );
    return runningHintAnimator( aspect ) != nullptr;
}

qreal QskPopup::fadingFactor() const
{
    const auto aspect = qskEffectiveFadingAspect( this );
    if ( auto animator = runningHintAnimator( aspect ) )
        return animator->currentValue().value< qreal >();

    return isOpen() ? 1.0 : 0.0;
}

QRectF QskPopup::overlayRect() const
{
    if ( hasOverlay() && m_data->inputGrabber )
        return m_data->inputGrabber->grabberRect();

    return QRectF();
}

void QskPopup::updateInputGrabber()
{
    if ( parentItem() && isVisible() &&
        ( isModal() || testPopupFlag( CloseOnPressOutside ) ) )
    {
        if ( m_data->inputGrabber == nullptr )
        {
            const auto children = childItems();
            m_data->inputGrabber = new InputGrabber( this );

            if ( !children.isEmpty() )
            {
                /*
                    Even if the input grabber has no content it has an effect
                    on QQuickItem::childAt. Also tools like Squish struggle with
                    sorting out items without content.
                    So let's better move the grabber to the beginning.
                 */
                m_data->inputGrabber->stackBefore( children.first() );
            }
        }
    }
    else
    {
        if ( m_data->inputGrabber )
        {
            /*
                In QQuickWindowPrivate::deliverPressOrReleaseEvent ( 5.12 )
                might crash, when we delete the grabber as a result of a
                mouse event somewehere below the popup.
             */
            m_data->inputGrabber->setParentItem( nullptr );
            m_data->inputGrabber->setParent( nullptr );
            m_data->inputGrabber->deleteLater();

            m_data->inputGrabber = nullptr;
        }
    }
}

bool QskPopup::isTransitionAccepted( QskAspect aspect ) const
{
    if ( isVisible() && !isInitiallyPainted() )
    {
        /*
            Usually we suppress transitions, when a control has never been
            painted before as there is no valid starting point. Popups are
            different as we want to have smooth fade/slide appearances.
         */
        if ( ( aspect.value() == 0 ) )
            return true;

        if ( aspect.subControl() == qskEffectiveFadingAspect( this ).subControl() )
            return true;

        if ( aspect.subControl() == effectiveSubcontrol( QskPopup::Overlay ) )
            return true;
    }

    return Inherited::isTransitionAccepted( aspect );
}

void QskPopup::setPriority( uint priority )
{
    if ( m_data->priority != priority )
    {
        m_data->priority = priority;
        Q_EMIT priorityChanged( priority );
    }
}

uint QskPopup::priority() const
{
    return m_data->priority;
}

void QskPopup::setModal( bool on )
{
    if ( on == m_data->isModal )
        return;

    m_data->isModal = on;
    updateInputGrabber();

    Q_EMIT modalChanged( on );
}

bool QskPopup::isModal() const
{
    return m_data->isModal;
}

void QskPopup::setPopupFlags( PopupFlags flags )
{
    const auto newFlags = static_cast< int >( flags );

    if ( newFlags != m_data->flags )
    {
        m_data->flags = newFlags;
        updateInputGrabber();
    }
}

QskPopup::PopupFlags QskPopup::popupFlags() const
{
    return static_cast< PopupFlags >( m_data->flags );
}

void QskPopup::setPopupFlag( PopupFlag flag, bool on )
{
    auto flags = m_data->flags;

    if ( on )
        flags |= flag;
    else
        flags &= ~flag;

    setPopupFlags( PopupFlags( flags ) );
}

bool QskPopup::testPopupFlag( PopupFlag flag ) const
{
    return m_data->flags & flag;
}

void QskPopup::setOverlay( bool on )
{
    if ( setFlagHint( Overlay | QskAspect::Style, on ) )
        Q_EMIT overlayChanged( on );
}

void QskPopup::resetOverlay()
{
    if ( resetSkinHint( Overlay | QskAspect::Style ) )
        Q_EMIT overlayChanged( hasOverlay() );
}

bool QskPopup::hasOverlay() const
{
    return flagHint< bool >( QskPopup::Overlay | QskAspect::Style, true );
}

void QskPopup::grabFocus( bool on )
{
    if ( on == hasFocus() )
        return;

    if ( on )
    {
        qskSetFocus( this, true );
    }
    else
    {
        QQuickItem* successor = nullptr;

        if ( m_data->handoverFocus )
        {
            /*
                Qt/Quick does not handover the focus to another item,
                when the active focus gets lost. For the situation of
                a popup being closed we try to do it.
             */
            successor = focusSuccessor();
        }

        if ( successor )
            qskSetFocus( successor, true );

        if ( hasFocus() )
            qskSetFocus( this, false );
    }
}

bool QskPopup::event( QEvent* event )
{
    bool ok = Inherited::event( event );

    switch ( static_cast< int >( event->type() ) )
    {
        case QEvent::KeyPress:
        case QEvent::KeyRelease:

        case QEvent::Wheel:

        case QEvent::MouseButtonPress:
        case QEvent::MouseMove:
        case QEvent::MouseButtonRelease:

        case QEvent::HoverEnter:
        case QEvent::HoverLeave:
        {
            // swallow the event
            event->accept();
            if ( auto w = qobject_cast< QskWindow* >( window() ) )
                w->setEventAcceptance( QskWindow::EventPropagationStopped );

            break;
        }
        case QskEvent::Animator:
        {
            const auto animatorEvent = static_cast< QskAnimatorEvent* >( event );

            if ( ( animatorEvent->state() == QskAnimatorEvent::Terminated )
                && ( animatorEvent->aspect() == qskEffectiveFadingAspect( this ) ) )
            {
                if ( !isOpen() )
                {
                    Inherited::setVisible( false );

                    if ( testPopupFlag( QskPopup::DeleteOnClose ) )
                        deleteLater();
                }

                Q_EMIT fadingChanged( false );
            }

            break;
        }
        default:
        {
            /*
                Don't accept touch events otherwise we don't receive the
                synthesized mouse events and need to handle both type of
                events from now on.
                But by accepting the mouse event propagation of the touch
                events also stops.
             */

            break;
        }
    }

    return ok;
}

void QskPopup::keyPressEvent( QKeyEvent* event )
{
    if ( qskIsStandardKeyInput( event, QKeySequence::Cancel ) )
    {
        close();
        return;
    }

    return Inherited::keyPressEvent( event );
}

void QskPopup::focusInEvent( QFocusEvent* event )
{
    Inherited::focusInEvent( event );

    if ( isFocusScope() && isTabFence() && ( scopedFocusItem() == nullptr ) )
    {
        if ( event->reason() == Qt::PopupFocusReason )
        {
            /*
                When receiving the focus we need to have a focused
                item, so that the tab focus chain has a starting point.

                But we only do it when the reason is Qt::PopupFocusReason
                as we also receive focus events during the process of reparenting
                children and setting the focus there can leave the item tree
                in an invalid state.
             */

            if ( auto focusItem = nextItemInFocusChain( true ) )
            {
                if ( !qskIsItemInDestructor( focusItem )
                    && qskIsAncestorOf( this, focusItem ) )
                {
                    focusItem->setFocus( true );
                }
            }
        }
    }
}

void QskPopup::focusOutEvent( QFocusEvent* event )
{
    Inherited::focusOutEvent( event );
}

QQuickItem* QskPopup::focusSuccessor() const
{
    if ( const auto scope = qskNearestFocusScope( this ) )
    {
        const auto children = qskPaintOrderChildItems( scope );
        for ( auto it = children.crbegin(); it != children.crend(); ++it )
        {
            auto child = *it;

            if ( ( child != this ) && child->isFocusScope() &&
                child->activeFocusOnTab() && child->isVisible() )
            {
                return child;
            }
        }
    }

    return nullptr;
}

void QskPopup::aboutToShow()
{
    if ( m_data->autoGrabFocus )
    {
        // What to do, when we are hidden below another popup ??
        grabFocus( true );
    }

    Inherited::aboutToShow();
}

void QskPopup::itemChange( QQuickItem::ItemChange change,
    const QQuickItem::ItemChangeData& value )
{
    Inherited::itemChange( change, value );

    if ( change == QQuickItem::ItemVisibleHasChanged )
    {
        updateInputGrabber();

        if ( value.boolValue )
        {
            polish(); // so that aboutToShow is called. TODO ...
        }
        else
        {
            grabFocus( false );
        }
    }
    else if ( change == QQuickItem::ItemParentHasChanged )
    {
        delete m_data->inputGrabber;
        m_data->inputGrabber = nullptr;

        updateInputGrabber();
    }
}

void QskPopup::windowChangeEvent( QskWindowChangeEvent* event )
{
    qskSendPopupEvent( event->oldWindow(), this, false );
    qskSendPopupEvent( event->window(), this, true );

    Inherited::windowChangeEvent( event );
}

int QskPopup::execPopup()
{
    class EventLoop : public QEventLoop
    {
      public:
        EventLoop( QskPopup* popup )
            : QEventLoop( popup )
        {
            /*
                We want popup being the parent, so that the loop can be found
                by popup->findChild< QEventLoop* >()
             */

            connect( popup, &QObject::destroyed, this, &EventLoop::reject );
            connect( popup, &QskPopup::fadingChanged, this, &EventLoop::maybeQuit );
            connect( popup, &QskPopup::openChanged, this, &EventLoop::maybeQuit );
        }

      private:
        void reject()
        {
            setParent( nullptr );
            QEventLoop::exit( 1 );
        }

        void maybeQuit()
        {
            if ( auto popup = qobject_cast< const QskPopup* >( parent() ) )
            {
                if ( popup->isOpen() || popup->isFading() )
                    return;
            }

            QEventLoop::exit( 0 );
        }
    };

    if ( isOpen() || isFading() )
    {
        qWarning() << "QskPopup::exec: popup is already opened";
        return -1;
    }

    if ( priority() > 0 )
    {
        qWarning( "QskPopup::exec for a popup with non default priority." );
    }

    open();

    if ( window() == nullptr )
    {
        qWarning( "trying to exec a popup without window." );
        return -1;
    }

    if ( auto mouseGrabber = window()->mouseGrabberItem() )
    {
        // when being called from QQuickWindow::mouseReleaseEvent
        // the mouse grabber has not yet been released.

        if( !qskIsAncestorOf( this, mouseGrabber ) )
            qskUngrabMouse( mouseGrabber );
    }

    return EventLoop( this ).exec( QEventLoop::DialogExec );
}

#include "moc_QskPopup.cpp"
