/******************************************************************************
 * QSkinny - Copyright (C) 2016 Uwe Rathmann
 * This file may be used under the terms of the QSkinny License, Version 1.0
 *****************************************************************************/

#include "QskScrollView.h"
#include "QskPanGestureRecognizer.h"
#include "QskFlickAnimator.h"
#include "QskBoxBorderMetrics.h"
#include "QskGesture.h"
#include "QskAspect.h"
#include "QskEvent.h"

#include <QSGNode>

QSK_SUBCONTROL( QskScrollView, Panel )
QSK_SUBCONTROL( QskScrollView, Viewport )
QSK_SUBCONTROL( QskScrollView, HorizontalScrollBar )
QSK_SUBCONTROL( QskScrollView, HorizontalScrollHandle )
QSK_SUBCONTROL( QskScrollView, VerticalScrollBar )
QSK_SUBCONTROL( QskScrollView, VerticalScrollHandle )

QSK_STATE( QskScrollView, VerticalHandlePressed, QskAspect::FirstSystemState << 1 )
QSK_STATE( QskScrollView, HorizontalHandlePressed, QskAspect::FirstSystemState << 2 )

namespace
{
    class FlickAnimator : public QskFlickAnimator
    {
    public:
        FlickAnimator()
        {
            setDuration( 1000 );
            setEasingCurve( QEasingCurve::OutCubic );
        }

        void setScrollView( QskScrollView* scrollView )
        {
            m_scrollView = scrollView;
        }

        virtual void translate( qreal dx, qreal dy ) override final
        {
            const QPointF pos = m_scrollView->scrollPos();
            m_scrollView->setScrollPos( pos - QPointF( dx, -dy ) );
        }

    private:
        QskScrollView* m_scrollView;
    };
}

class QskScrollView::PrivateData
{
public:
    PrivateData():
        horizontalScrollBarPolicy( Qt::ScrollBarAsNeeded ),
        verticalScrollBarPolicy( Qt::ScrollBarAsNeeded ),
        scrollableSize( 0.0, 0.0 ),
        isScrolling( 0 )
    {
    }

    Qt::ScrollBarPolicy horizontalScrollBarPolicy;
    Qt::ScrollBarPolicy verticalScrollBarPolicy;

    QPointF scrollPos;
    QSizeF scrollableSize;

    QskPanGestureRecognizer panRecognizer;
    FlickAnimator flicker;

    qreal scrollPressPos;
    int isScrolling;
};

QskScrollView::QskScrollView( QQuickItem* parent ):
    Inherited( parent ),
    m_data( new PrivateData() )
{
    m_data->flicker.setScrollView( this );

    m_data->panRecognizer.setWatchedItem( this );
    m_data->panRecognizer.setOrientations( Qt::Horizontal | Qt::Vertical );
    m_data->panRecognizer.setTimeout( 200 );

    setAcceptedMouseButtons( Qt::LeftButton );
    setWheelEnabled( true );
    setFocusPolicy( Qt::StrongFocus );
}

QskScrollView::~QskScrollView()
{
}

void QskScrollView::setFlickableOrientations( Qt::Orientations orientations )
{
    if ( m_data->panRecognizer.orientations() != orientations )
    {
        m_data->panRecognizer.setOrientations( orientations );
        Q_EMIT flickableOrientationsChanged();
    }
}

Qt::Orientations QskScrollView::flickableOrientations() const
{
    return m_data->panRecognizer.orientations();
}

void QskScrollView::setVerticalScrollBarPolicy( Qt::ScrollBarPolicy policy )
{
    if ( policy != m_data->verticalScrollBarPolicy )
    {
        m_data->verticalScrollBarPolicy = policy;
        update();

        Q_EMIT verticalScrollBarPolicyChanged();
    }
}

Qt::ScrollBarPolicy QskScrollView::verticalScrollBarPolicy() const
{
    return m_data->verticalScrollBarPolicy;
}

void QskScrollView::setHorizontalScrollBarPolicy( Qt::ScrollBarPolicy policy )
{
    if ( policy != m_data->horizontalScrollBarPolicy )
    {
        m_data->horizontalScrollBarPolicy = policy;
        update();

        Q_EMIT horizontalScrollBarPolicyChanged();
    }
}

Qt::ScrollBarPolicy QskScrollView::horizontalScrollBarPolicy() const
{
    return m_data->horizontalScrollBarPolicy;
}

void QskScrollView::setScrollPos( const QPointF& pos )
{
    const QPointF boundedPos = boundedScrollPos( pos );
    if ( boundedPos != m_data->scrollPos )
    {
        m_data->scrollPos = boundedPos;
        update();

        Q_EMIT scrollPosChanged();
        Q_EMIT scrolledTo( boundedPos );
    }
}

QPointF QskScrollView::scrollPos() const
{
    return m_data->scrollPos;
}

bool QskScrollView::isScrolling( Qt::Orientation orientation ) const
{
    return m_data->isScrolling == orientation;
}

void QskScrollView::setScrollableSize( const QSizeF& size )
{
    const QSizeF boundedSize = size.expandedTo( QSizeF( 0, 0 ) );

    if ( boundedSize != m_data->scrollableSize )
    {
        m_data->scrollableSize = boundedSize;

        setScrollPos( m_data->scrollPos ); // scroll pos might need to be re-bounded

        update();
    }
}

QSizeF QskScrollView::scrollableSize() const
{
    return m_data->scrollableSize;
}

QRectF QskScrollView::viewContentsRect() const
{
    // This code should be done in the skinlet. TODO ...
    const qreal bw = boxBorderMetricsHint( Viewport ).widthAt( Qt::TopEdge );

    const QRectF r = subControlRect( Viewport );
    return r.adjusted( bw, bw, -bw, -bw );
}

QRectF QskScrollView::gestureRect() const
{
    return subControlRect( Viewport );
}

void QskScrollView::geometryChangeEvent( QskGeometryChangeEvent* event )
{
    if ( event->isResized() )
        setScrollPos( scrollPos() );

    Inherited::geometryChangeEvent( event );
}

void QskScrollView::mousePressEvent( QMouseEvent* event )
{
    if ( subControlRect( VerticalScrollBar ).contains( event->pos() ) )
    {
        const QRectF handleRect = subControlRect( VerticalScrollHandle );

        if ( handleRect.contains( event->pos() ) )
        {
            m_data->isScrolling = Qt::Vertical;
            m_data->scrollPressPos = event->y();

            setSkinStateFlag( VerticalHandlePressed, true );
        }
        else
        {
            const QRectF vRect = viewContentsRect();

            qreal y = m_data->scrollPos.y();

            if ( event->y() < handleRect.top() )
                y -= vRect.height();
            else
                y += vRect.height();

            setScrollPos( QPointF( m_data->scrollPos.x(), y ) );
        }

        return;
    }

    if ( subControlRect( HorizontalScrollBar ).contains( event->pos() ) )
    {
        const QRectF handleRect = subControlRect( HorizontalScrollHandle );

        if ( handleRect.contains( event->pos() ) )
        {
            m_data->isScrolling = Qt::Horizontal;
            m_data->scrollPressPos = event->x();

            setSkinStateFlag( HorizontalHandlePressed, true );
        }
        else
        {
            const QRectF vRect = viewContentsRect();

            qreal x = m_data->scrollPos.x();

            if ( event->x() < handleRect.left() )
                x -= vRect.width();
            else
                x += vRect.width();

            setScrollPos( QPointF( x, m_data->scrollPos.y() ) );
        }
    }
}

void QskScrollView::mouseMoveEvent( QMouseEvent* event )
{
    if ( !m_data->isScrolling )
    {
        Inherited::mouseMoveEvent( event );
        return;
    }

    QPointF pos = m_data->scrollPos;

    if ( m_data->isScrolling == Qt::Horizontal )
    {
        const qreal dx = event->x() - m_data->scrollPressPos;
        const qreal w = subControlRect( HorizontalScrollBar ).width();

        pos.rx() += dx / w * m_data->scrollableSize.width();
        m_data->scrollPressPos = event->x();
    }
    else if ( m_data->isScrolling == Qt::Vertical )
    {
        const qreal dy = event->y() - m_data->scrollPressPos;
        const qreal h = subControlRect( VerticalScrollBar ).height();

        pos.ry() += dy / h * m_data->scrollableSize.height();
        m_data->scrollPressPos = event->y();
    }

    if ( pos != m_data->scrollPos )
        setScrollPos( pos );
}

void QskScrollView::mouseReleaseEvent( QMouseEvent* )
{
    if ( m_data->isScrolling )
    {
        m_data->isScrolling = 0;
        m_data->scrollPressPos = 0;

        setSkinStateFlag( HorizontalHandlePressed, false );
        setSkinStateFlag( VerticalHandlePressed, false );
    }
}

void QskScrollView::gestureEvent( QskGestureEvent* event )
{
    if ( event->gesture()->type() == QskGesture::Pan )
    {
        const auto gesture = static_cast< const QskPanGesture* >( event->gesture() );
        switch( gesture->state() )
        {
            case QskGesture::Updated:
            {
                setScrollPos( scrollPos() - gesture->delta() );
                break;
            }
            case QskGesture::Finished:
            {
                m_data->flicker.setWindow( window() );
                m_data->flicker.accelerate( gesture->angle(), gesture->velocity() );
                break;
            }
            case QskGesture::Canceled:
            {
                // what to do here: maybe going back to the origin of the gesture ??
                break;
            }
            default:
                break;
        }

        return;
    }

    Inherited::gestureEvent( event );
}

#ifndef QT_NO_WHEELEVENT

void QskScrollView::wheelEvent( QWheelEvent* event )
{
    const qreal stepSize = 20.0; // how to find this value

    QPointF offset;

    if ( subControlRect( Viewport ).contains( event->posF() ) )
    {
        /*
             Not sure if that code makes sense, but I don't have an input device
             that generates wheel events in both directions. TODO ...
         */

        //offset = event->pixelDelta();

        if ( offset.isNull() )
        {
            offset = event->angleDelta();
            offset *= stepSize / QWheelEvent::DefaultDeltasPerStep;
        }
    }
    else
    {
        const qreal delta = stepSize * event->delta() / QWheelEvent::DefaultDeltasPerStep;

        if ( subControlRect( VerticalScrollBar ).contains( event->posF() ) )
        {
            offset.setY( delta );
        }
        else if ( subControlRect( HorizontalScrollBar ).contains( event->posF() ) )
        {
            offset.setX( delta );
        }
    }

    if ( !offset.isNull() )
    {
#if QT_VERSION >= QT_VERSION_CHECK(5, 7, 0)
        if ( event->inverted() )
            offset = -offset;
#endif

        setScrollPos( m_data->scrollPos - offset );
    }
}

#endif

bool QskScrollView::gestureFilter( QQuickItem* item, QEvent* event )
{
    const auto o = m_data->panRecognizer.orientations();
    if ( o )
    {
        bool maybeGesture = m_data->panRecognizer.state() > QskGestureRecognizer::Idle;

        if ( !maybeGesture && ( event->type() == QEvent::MouseButtonPress ) )
        {
            const QRectF vr = viewContentsRect();
            const QSizeF& scrollableSize = m_data->scrollableSize;

            if ( ( ( o & Qt::Vertical ) && ( vr.height() < scrollableSize.height() ) )
                || ( ( o & Qt::Horizontal ) && ( vr.width() < scrollableSize.width() ) ) )
            {
                maybeGesture = true;
            }
        }

        if ( maybeGesture )
            return m_data->panRecognizer.processEvent( item, event );
    }

    return false;
}

QPointF QskScrollView::boundedScrollPos( const QPointF& pos ) const
{
    const QRectF vr = viewContentsRect();

    const qreal maxX = qMax( 0.0, scrollableSize().width() - vr.width() );
    const qreal maxY = qMax( 0.0, scrollableSize().height() - vr.height() );

    return QPointF( qBound( 0.0, pos.x(), maxX ), qBound( 0.0, pos.y(), maxY ) );
}

Qt::Orientations QskScrollView::scrollableOrientations() const
{
    const QRectF vr = contentsRect();

    Qt::ScrollBarPolicy policyV = m_data->verticalScrollBarPolicy;
    Qt::ScrollBarPolicy policyH = m_data->horizontalScrollBarPolicy;

    if ( policyV == Qt::ScrollBarAsNeeded )
    {
        qreal h = vr.height();
        if ( policyH == Qt::ScrollBarAlwaysOn )
            h -= metric( HorizontalScrollBar | QskAspect::Size );

        if ( m_data->scrollableSize.height() > h )
            policyV = Qt::ScrollBarAlwaysOn;
    }

    if ( policyH == Qt::ScrollBarAsNeeded )
    {
        qreal w = vr.width();
        if ( policyV == Qt::ScrollBarAlwaysOn )
            w -= metric( VerticalScrollBar | QskAspect::Size );

        if ( m_data->scrollableSize.width() > w )
        {
            policyH = Qt::ScrollBarAlwaysOn;

            // we have to check the vertical once more

            if ( ( policyV == Qt::ScrollBarAsNeeded ) &&
                 ( m_data->scrollableSize.height() > vr.height() - metric( HorizontalScrollBar | QskAspect::Size ) ) )
            {
                policyV = Qt::ScrollBarAlwaysOn;
            }
        }
    }

    Qt::Orientations o;
    if ( policyH == Qt::ScrollBarAlwaysOn )
        o |= Qt::Horizontal;

    if ( policyV == Qt::ScrollBarAlwaysOn )
        o |= Qt::Vertical;

    return o;
}

#include "moc_QskScrollView.cpp"
