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

#include "QskStackBoxAnimator.h"
#include "QskStackBox.h"
#include "QskEvent.h"
#include "QskQuick.h"
#include "QskFunctions.h"

QSK_QT_PRIVATE_BEGIN
#include <private/qquickitem_p.h>
QSK_QT_PRIVATE_END

namespace
{
    class RotationTransform : public QQuickTransform
    {
        Q_OBJECT

      public:
        RotationTransform( Qt::Axis axis, qreal radians, QQuickItem* item )
            : QQuickTransform( item )
            , m_axis( axis )
            , m_radians( radians )
        {
            prependToItem( item );
        }

        void setRadians( qreal radians )
        {
            if ( m_radians != radians )
            {
                m_radians = radians;
                update();
            }
        }

        void applyTo( QMatrix4x4* matrix) const override
        {
            if ( const auto item = qobject_cast< QQuickItem* >( parent() ) )
            {
                const auto dx = 0.5 * item->width();
                const auto dy = 0.5 * item->height();

                QTransform transform;
                transform.translate( dx, dy );
                transform.rotateRadians( m_radians, m_axis );
                transform.translate( -dx, -dy );

                *matrix *= transform;
            }
        }

      private:
        const Qt::Axis m_axis;
        qreal m_radians;
    };

    class QuickTransform final : public QQuickTransform
    {
        Q_OBJECT

      public:
        QuickTransform( QQuickItem* item )
            : QQuickTransform( item )
        {
            prependToItem( item );
        }

        void setTransform( const QTransform& transform )
        {
            if ( transform != m_transform )
            {
                m_transform = transform;
                update();
            }
        }

        void applyTo( QMatrix4x4* matrix ) const override
        {
            if ( const auto item = qobject_cast< QQuickItem* >( parent() ) )
                *matrix *= m_transform;
        }

      private:
        QTransform m_transform;
    };

    template< typename Transform >
    Transform* qskFindTransform( const QQuickItem* item )
    {
        const auto& transforms = QQuickItemPrivate::get( item )->transforms;
        for ( const auto& t : transforms )
        {
            if ( auto transform = qobject_cast< Transform* >( t ) )
                return transform;
        }

        return nullptr;
    }
}

QskStackBoxAnimator::QskStackBoxAnimator( QskStackBox* parent )
    : QObject( parent )
    , m_startIndex( -1 )
    , m_endIndex( -1 )
{
}

QskStackBoxAnimator::~QskStackBoxAnimator()
{
}

void QskStackBoxAnimator::setStartIndex( int index )
{
    m_transientIndex = m_startIndex = index;
}

void QskStackBoxAnimator::setEndIndex( int index )
{
    m_endIndex = index;
}

int QskStackBoxAnimator::startIndex() const
{
    return m_startIndex;
}

int QskStackBoxAnimator::endIndex() const
{
    return m_endIndex;
}

QskStackBox* QskStackBoxAnimator::stackBox() const
{
    return static_cast< QskStackBox* >( parent() );
}

QQuickItem* QskStackBoxAnimator::itemAt( int index ) const
{
    return stackBox()->itemAtIndex(
        ( index == 0 ) ? m_startIndex : m_endIndex );
}

qreal QskStackBoxAnimator::transientIndex() const
{
    return m_transientIndex;
}

void QskStackBoxAnimator::advance( qreal progress )
{
    qreal transientIndex;

    if ( qFuzzyIsNull( progress ) )
    {
        transientIndex = m_startIndex;
    }
    else if ( qFuzzyCompare( progress, 1.0 ) )
    {
        transientIndex = m_endIndex;
    }
    else
    {
        const auto box = stackBox();

        auto startIndex = m_startIndex;
        auto endIndex = m_endIndex;

        if ( startIndex == 0 )
        {
            if ( endIndex == box->itemCount() - 1 )
                startIndex += box->itemCount();
        }
        else if ( startIndex == box->itemCount() - 1 )
        {
            if ( endIndex == 0 )
                endIndex += box->itemCount();
        }

        transientIndex = startIndex + ( endIndex - startIndex ) * progress;
    }

    if ( !qskFuzzyCompare( m_transientIndex, transientIndex ) )
    {
        m_transientIndex = transientIndex;
        advanceIndex( progress );

        Q_EMIT stackBox()->transientIndexChanged( m_transientIndex );
    }
}

QskStackBoxAnimator1::QskStackBoxAnimator1( QskStackBox* parent )
    : QskStackBoxAnimator( parent )
    , m_direction( Qsk::LeftToRight )
    , m_isDirty( false )
    , m_hasClip( false )
{
    // catching geometryChanges to know about resizing
}

QskStackBoxAnimator1::~QskStackBoxAnimator1()
{
}

void QskStackBoxAnimator1::setDirection( Qsk::Direction direction )
{
    if ( m_direction != direction )
    {
        stop();
        m_direction = direction;
    }
}

Qsk::Direction QskStackBoxAnimator1::direction() const
{
    return m_direction;
}

void QskStackBoxAnimator1::setup()
{
    auto stackBox = this->stackBox();

    m_hasClip = stackBox->clip();
    if ( !m_hasClip )
        stackBox->setClip( true );

    stackBox->installEventFilter( this );
    m_isDirty = true;
}

void QskStackBoxAnimator1::advanceIndex( qreal value )
{
    auto stackBox = this->stackBox();

    const bool isHorizontal = ( m_direction == Qsk::LeftToRight )
        || ( m_direction == Qsk::RightToLeft );

    for ( int i = 0; i < 2; i++ )
    {
        if ( auto item = itemAt( i ) )
        {
            QRectF rect = qskItemGeometry( item );

            if ( m_isDirty )
            {
                const int index = ( i == 0 ) ? startIndex() : endIndex();
                rect = stackBox->geometryForItemAt( index );

                m_itemOffset[ i ] = isHorizontal ? rect.x() : rect.y();
            }

            qreal x, y;

            if ( isHorizontal )
            {
                qreal off = stackBox->width() * ( value - i );
                if ( m_direction == Qsk::LeftToRight )
                    off = -off;

                x = m_itemOffset[ i ] + off;
                y = rect.y();
            }
            else
            {
                qreal off = stackBox->height() * ( value - i );
                if ( m_direction == Qsk::BottomToTop )
                    off = -off;

                x = rect.x();
                y = m_itemOffset[ i ] + off;
            }

            qskSetItemGeometry( item, x, y, rect.width(), rect.height() );

            if ( !item->isVisible() )
                item->setVisible( true );
        }
    }

    m_isDirty = false;
}

void QskStackBoxAnimator1::done()
{
    for ( int i = 0; i < 2; i++ )
    {
        if ( auto item = itemAt( i ) )
        {
            item->removeEventFilter( this );
            item->setVisible( i == 1 );
        }
    }

    if ( !m_hasClip )
        stackBox()->setClip( false );
}

bool QskStackBoxAnimator1::eventFilter( QObject* object, QEvent* event )
{
    if ( !m_isDirty && object == stackBox() )
    {
        switch( static_cast< int >( event->type() ) )
        {
            case QskEvent::GeometryChange:
            case QskEvent::ContentsRectChange:
            case QskEvent::LayoutRequest:
            {
                m_isDirty = true;
                break;
            }
        }
    }

    return QObject::eventFilter( object, event );
}

QskStackBoxAnimator2::QskStackBoxAnimator2( QskStackBox* parent )
    : QskStackBoxAnimator( parent )
    , m_orientation( Qt::Horizontal )
    , m_inverted( false )
{
}

QskStackBoxAnimator2::~QskStackBoxAnimator2()
{
}

void QskStackBoxAnimator2::setOrientation( Qt::Orientation orientation )
{
    if ( m_orientation != orientation )
    {
        stop();
        m_orientation = orientation;
    }
}

Qt::Orientation QskStackBoxAnimator2::orientation() const
{
    return m_orientation;
}

void QskStackBoxAnimator2::setInverted( bool on )
{
    if ( m_inverted != on )
    {
        stop();
        m_inverted = on;
    }
}

bool QskStackBoxAnimator2::isInverted() const
{
    return m_inverted;
}

void QskStackBoxAnimator2::setup()
{
    const auto axis = ( m_orientation == Qt::Horizontal )
        ? Qt::YAxis : Qt::XAxis;

    if ( auto item = itemAt( 0 ) )
        ( void ) new RotationTransform( axis, 0.0, item );

    if ( auto item = itemAt( 1 ) )
        ( void ) new RotationTransform( axis, M_PI_2, item );
}

void QskStackBoxAnimator2::advanceIndex( qreal value )
{
    auto radians = value * M_PI;

    if ( radians < M_PI_2 && radians > -M_PI_2 )
    {
        if ( auto item = itemAt( 0 ) )
        {
            if ( !m_inverted )
                radians = 2 * M_PI - radians;

            auto rotation = qskFindTransform< RotationTransform >( item );
            rotation->setRadians( radians );

            item->setVisible( true );
        }

        if ( auto item = itemAt( 1 ) )
        {
            item->setVisible( false );
        }
    }
    else
    {
        if ( auto item = itemAt( 0 ) )
        {
            item->setVisible( false );
        }

        if ( auto item = itemAt( 1 ) )
        {
            radians = radians - M_PI;
            if ( !m_inverted )
                radians = 2 * M_PI - radians;

            auto rotation = qskFindTransform< RotationTransform >( item );
            rotation->setRadians( radians );

            item->setVisible( true );
        }
    }
}

void QskStackBoxAnimator2::done()
{
    for ( int i = 0; i < 2; i++ )
    {
        if ( auto item = itemAt( i ) )
        {
            delete qskFindTransform< RotationTransform >( item );
            item->setVisible( i == 1 );
        }
    }
}

QskStackBoxAnimator3::QskStackBoxAnimator3( QskStackBox* parent )
    : QskStackBoxAnimator( parent )
{
}

QskStackBoxAnimator3::~QskStackBoxAnimator3()
{
}

void QskStackBoxAnimator3::setup()
{
    if ( auto item = itemAt( 1 ) )
    {
        item->setOpacity( 0.0 );
        item->setVisible( true );
    }
}

void QskStackBoxAnimator3::advanceIndex( qreal value )
{
    if ( auto item1 = itemAt( 0 ) )
        item1->setOpacity( 1.0 - value );

    if ( auto item2 = itemAt( 1 ) )
        item2->setOpacity( value );
}

void QskStackBoxAnimator3::done()
{
    for ( int i = 0; i < 2; i++ )
    {
        if ( auto item = itemAt( i ) )
        {
            item->setOpacity( 1.0 );
            item->setVisible( i == 1 ); // not here !!
        }
    }
}

QskStackBoxAnimator4::QskStackBoxAnimator4( QskStackBox* parent )
    : QskStackBoxAnimator( parent )
    , m_orientation( Qt::Horizontal )
    , m_inverted( false )
{
}

QskStackBoxAnimator4::~QskStackBoxAnimator4()
{
}

void QskStackBoxAnimator4::setOrientation( Qt::Orientation orientation )
{
    if ( m_orientation != orientation )
    {
        stop();
        m_orientation = orientation;
    }
}

Qt::Orientation QskStackBoxAnimator4::orientation() const
{
    return m_orientation;
}

void QskStackBoxAnimator4::setInverted( bool on )
{
    if ( m_inverted != on )
    {
        stop();
        m_inverted = on;
    }
}

bool QskStackBoxAnimator4::isInverted() const
{
    return m_inverted;
}

void QskStackBoxAnimator4::setup()
{
    if ( auto item = itemAt( 0 ) )
    {
        ( void ) new QuickTransform( item );
        item->setVisible( true );
    }

    if ( auto item = itemAt( 1 ) )
    {
        ( void ) new QuickTransform( item );
        item->setVisible( true );
    }
}

void QskStackBoxAnimator4::advanceIndex( qreal value )
{
    auto item1 = itemAt( 1 );
    auto item2 = itemAt( 0 );

    if ( isInverted() )
        std::swap( item1, item2 );

    const qreal posEdge = isInverted() ? value : 1.0 - value;

    if ( item1 )
    {
        const auto transform = transformation( item1, false, posEdge );

        auto rotation = qskFindTransform< QuickTransform >( item1 );
        rotation->setTransform( transform );
    }

    if ( item2 )
    {
        const auto transform = transformation( item2, true, posEdge );

        auto rotation = qskFindTransform< QuickTransform >( item2 );
        rotation->setTransform( transform );
    }
}

QTransform QskStackBoxAnimator4::transformation(
    const QQuickItem* item, bool first, qreal posEdge ) const
{
    /*
        first: left or top item
        posEdge: position of the edge in the range of [0-1]
             ( left->right, top->bottom ).
     */

    const qreal radians = M_PI_2 * ( 1.0 - posEdge );

    QTransform transform;

    if( orientation() == Qt::Horizontal )
    {
        const qreal dx = posEdge * ( item->x() + item->width() );
        const qreal dy = 0.5 * item->height();

        if ( first )
        {
            transform.translate( -item->x() + dx, dy );
            transform.rotateRadians( radians, Qt::YAxis );
            transform.translate( -item->width(), -dy );
        }
        else
        {
            transform.translate( dx, dy );
            transform.rotateRadians( radians - M_PI_2, Qt::YAxis );
            transform.translate( 0.0, -dy );
        }
    }
    else
    {
        const qreal dx = 0.5 * item->width();
        const qreal dy = posEdge * ( item->y() + item->height() );

        if ( first )
        {
            transform.translate( dx, -item->y() + dy );
            transform.rotateRadians( radians, Qt::XAxis );
            transform.translate( -dx, -item->height() );
        }
        else
        {
            transform.translate( dx, dy );
            transform.rotateRadians( radians - M_PI_2, Qt::XAxis );
            transform.translate( -dx, 0.0 );
        }
    }

    return transform;
}

void QskStackBoxAnimator4::done()
{
    for ( int i = 0; i < 2; i++ )
    {
        if ( auto item = itemAt( i ) )
        {
            delete qskFindTransform< QuickTransform >( item );
            item->setVisible( i == 1 );
        }
    }
}

#include "moc_QskStackBoxAnimator.cpp"
#include "QskStackBoxAnimator.moc"
