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

#include "QskScrollView.h"
#include "QskAnimationHint.h"
#include "QskBoxBorderMetrics.h"
#include "QskEvent.h"

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_SYSTEM_STATE( QskScrollView, Pressed, QskAspect::FirstSystemState << 1 )

static inline QskAspect::Subcontrol qskSubControlAt(
    const QskScrollView* scrollView, const QPointF& pos )
{
    using Q = QskScrollView;

    const auto rect = scrollView->contentsRect();

    // order is important as the handles are inside the bars !

    for ( auto subControl : { Q::VerticalScrollHandle, Q::VerticalScrollBar,
        Q::HorizontalScrollHandle, Q::HorizontalScrollBar } )
    {
        if ( scrollView->subControlRect( rect, subControl ).contains( pos ) )
            return subControl;
    }

    return QskAspect::NoSubcontrol;
}

class QskScrollView::PrivateData
{
  public:
    inline void resetScrolling( QskScrollView* scrollView )
    {
        setScrolling( scrollView, QskAspect::NoSubcontrol, 0.0 );
    }

    inline void setScrolling( QskScrollView* scrollView,
        QskAspect::Subcontrol subControl, qreal pos )
    {
        if ( subControl == pressedSubControl )
            return;

        QskAspect::Subcontrol subControls[2];
        if ( subControl == VerticalScrollHandle || pressedSubControl == VerticalScrollHandle )
        {
            subControls[0] = VerticalScrollHandle;
            subControls[1] = VerticalScrollBar;
        }
        else
        {
            subControls[0] = HorizontalScrollHandle;;
            subControls[1] = HorizontalScrollBar;
        }

        pressedSubControl = subControl;
        scrollPressPos = pos;

        scrollView->update();

        auto oldStates = scrollView->skinStates() | scrollView->scrollBarStates( subControl );
        auto newStates = oldStates | QskScrollView::Pressed;

        if ( pressedSubControl == QskAspect::NoSubcontrol )
            qSwap( oldStates, newStates );

        scrollView->startHintTransitions( { subControls[0] }, oldStates, newStates );
        scrollView->startHintTransitions( { subControls[1] }, oldStates, newStates );
    }

    void setHovered( QskScrollView* scrollView, QskAspect::Subcontrol subControl )
    {
        if ( subControl == this->hoveredSubControl )
            return;

        QskAspect::Subcontrol subControls[2];
        if ( subControl == VerticalScrollHandle
            || hoveredSubControl == VerticalScrollHandle
            || subControl == VerticalScrollBar
            || hoveredSubControl == VerticalScrollBar )
        {
            subControls[0] = VerticalScrollHandle;
            subControls[1] = VerticalScrollBar;
        }
        else
        {
            subControls[0] = HorizontalScrollHandle;
            subControls[1] = HorizontalScrollBar;
        }

        const bool wasHovered = hasState( subControls[1], QskScrollView::Hovered );

        hoveredSubControl = subControl;

        auto oldStates = scrollView->skinStates();
        auto newStates = oldStates | QskScrollView::Hovered;

        if ( hoveredSubControl == QskAspect::NoSubcontrol )
            qSwap( oldStates, newStates );

        scrollView->startHintTransitions( { subControls[0] }, oldStates, newStates );

        if ( wasHovered != hasState( subControls[1], QskScrollView::Hovered ) )
            scrollView->startHintTransitions( { subControls[1] }, oldStates, newStates );
    }

    bool hasState( QskAspect::Subcontrol subControl, QskAspect::State state ) const
    {
        if ( subControl == QskAspect::NoSubcontrol )
            return false;

        const auto stateSubcontrol =
            ( state == QskControl::Hovered ) ? hoveredSubControl : pressedSubControl;

        if ( subControl == stateSubcontrol )
            return true;

        // the scroll bar inherits pressed/hovered from the handle
        if ( subControl == VerticalScrollBar )
            return stateSubcontrol == VerticalScrollHandle;

        if ( subControl == HorizontalScrollBar )
            return stateSubcontrol == HorizontalScrollHandle;

        return false;
    }

    Qt::ScrollBarPolicy horizontalScrollBarPolicy = Qt::ScrollBarAsNeeded;
    Qt::ScrollBarPolicy verticalScrollBarPolicy = Qt::ScrollBarAsNeeded;

    qreal scrollPressPos = 0.0;

    QskAspect::Subcontrol pressedSubControl = QskAspect::NoSubcontrol;
    QskAspect::Subcontrol hoveredSubControl = QskAspect::NoSubcontrol;
};

QskScrollView::QskScrollView( QQuickItem* parent )
    : Inherited( parent )
    , m_data( new PrivateData() )
{
    setAcceptHoverEvents( true );
}

QskScrollView::~QskScrollView()
{
}

QskAnimationHint QskScrollView::flickHint() const
{
    return effectiveAnimation( QskAspect::Metric,
        QskScrollView::Viewport, QskAspect::NoState );
}

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;
}

bool QskScrollView::isScrolling( Qt::Orientation orientation ) const
{
    if ( orientation == Qt::Vertical )
        return m_data->pressedSubControl == VerticalScrollHandle;
    else
        return m_data->pressedSubControl == HorizontalScrollHandle;
}

QskAspect::States QskScrollView::scrollBarStates(
    QskAspect::Subcontrol subControl ) const
{
    auto states = skinStates();

    if ( m_data->hasState( subControl, Pressed ) )
        states |= Pressed;

    if ( m_data->hasState( subControl, Hovered ) )
        states |= Hovered;

    return states;
}

QRectF QskScrollView::viewContentsRect() const
{
    const auto borderMetrics = boxBorderMetricsHint( Viewport );

    const QRectF r = subControlRect( Viewport );
    return r.marginsRemoved( borderMetrics.widths() );
}

void QskScrollView::mousePressEvent( QMouseEvent* event )
{
    const auto mousePos = qskMousePosition( event );

    if ( subControlRect( VerticalScrollBar ).contains( mousePos ) )
    {
        const auto handleRect = subControlRect( VerticalScrollHandle );

        if ( handleRect.contains( mousePos ) )
        {
            m_data->setScrolling( this, VerticalScrollHandle, mousePos.y() );
        }
        else
        {
            const QRectF vRect = viewContentsRect();

            qreal y = scrollPos().y();

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

            setScrollPos( QPointF( scrollPos().x(), y ) );
        }

        return;
    }

    if ( subControlRect( HorizontalScrollBar ).contains( mousePos ) )
    {
        const QRectF handleRect = subControlRect( HorizontalScrollHandle );

        if ( handleRect.contains( mousePos ) )
        {
            m_data->setScrolling( this, HorizontalScrollHandle, mousePos.x() );
        }
        else
        {
            const QRectF vRect = viewContentsRect();

            qreal x = scrollPos().x();

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

            setScrollPos( QPointF( x, scrollPos().y() ) );
        }

        return;
    }

    Inherited::mousePressEvent( event );
}

void QskScrollView::mouseMoveEvent( QMouseEvent* event )
{
    if ( m_data->pressedSubControl == QskAspect::NoSubcontrol )
    {
        Inherited::mouseMoveEvent( event );
        return;
    }

    const auto mousePos = qskMousePosition( event );
    QPointF pos = scrollPos();

    if ( m_data->pressedSubControl == HorizontalScrollHandle )
    {
        const qreal dx = mousePos.x() - m_data->scrollPressPos;
        const qreal w = subControlRect( HorizontalScrollBar ).width();

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

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

    if ( pos != scrollPos() )
        setScrollPos( pos );
}

void QskScrollView::mouseReleaseEvent( QMouseEvent* event )
{
    if ( m_data->pressedSubControl == QskAspect::NoSubcontrol )
    {
        Inherited::mouseReleaseEvent( event );
        return;
    }

    m_data->resetScrolling( this );
}

void QskScrollView::mouseUngrabEvent()
{
    m_data->resetScrolling( this );
}

void QskScrollView::hoverEnterEvent( QHoverEvent* event )
{
    const auto subControl = qskSubControlAt( this, qskHoverPosition( event ) );
    m_data->setHovered( this, subControl );
}

void QskScrollView::hoverMoveEvent( QHoverEvent* event )
{
    const auto subControl = qskSubControlAt( this, qskHoverPosition( event ) );
    m_data->setHovered( this, subControl );
}

void QskScrollView::hoverLeaveEvent( QHoverEvent* )
{
    m_data->setHovered( this, QskAspect::NoSubcontrol );
}

#ifndef QT_NO_WHEELEVENT

QPointF QskScrollView::scrollOffset( const QWheelEvent* event ) const
{
    QPointF offset;

    const auto pos = qskWheelPosition( event );
    const auto viewRect = viewContentsRect();

    if ( subControlRect( VerticalScrollBar ).contains( pos ) )
    {
        const auto steps = qskWheelSteps( event );
        offset.setY( steps );
    }
    else if ( subControlRect( HorizontalScrollBar ).contains( pos ) )
    {
        const auto steps = qskWheelSteps( event );
        offset.setX( steps );
    }
    else if ( viewRect.contains( pos ) )
    {
        offset = event->pixelDelta();
        if ( offset.isNull() )
            offset = event->angleDelta() / QWheelEvent::DefaultDeltasPerStep;
    }

    if ( !offset.isNull() )
    {
        const auto vs = viewRect.size() / 3.0;

        offset.rx() *= vs.width();
        offset.ry() *= vs.height();
    }

    return offset;
}

#endif

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

    auto policyVertical = m_data->verticalScrollBarPolicy;
    auto policyHorizontal = m_data->horizontalScrollBarPolicy;

    if ( policyVertical == Qt::ScrollBarAsNeeded )
    {
        qreal height = vr.height();

        if ( policyHorizontal == Qt::ScrollBarAlwaysOn )
            height -= metric( HorizontalScrollBar | QskAspect::Size );

        if ( scrollableSize().height() > height )
            policyVertical = Qt::ScrollBarAlwaysOn;
    }

    if ( policyHorizontal == Qt::ScrollBarAsNeeded )
    {
        qreal width = vr.width();

        if ( policyVertical == Qt::ScrollBarAlwaysOn )
            width -= metric( VerticalScrollBar | QskAspect::Size );

        if ( scrollableSize().width() > width )
        {
            policyHorizontal = Qt::ScrollBarAlwaysOn;

            // we have to check the vertical once more

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

    Qt::Orientations orientations;

    if ( policyHorizontal == Qt::ScrollBarAlwaysOn )
        orientations |= Qt::Horizontal;

    if ( policyVertical == Qt::ScrollBarAlwaysOn )
        orientations |= Qt::Vertical;

    return orientations;
}

#include "moc_QskScrollView.cpp"
