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

#include "QskLinearBox.h"
#include "QskLayoutItem.h"
#include "QskLayoutEngine.h"

#include <QtEndian>

class QskLinearBox::PrivateData
{
public:
    PrivateData( Qt::Orientation orient, uint dim ):
        dimension( dim ),
        orientation( orient ),
        transposeAlignments( false )
    {
    }

    uint dimension;
    Qt::Edges extraSpacingAt;

    Qt::Orientation orientation : 2;
    bool transposeAlignments : 1;
};

QskLinearBox::QskLinearBox( QQuickItem* parent ):
    QskLinearBox( Qt::Horizontal, std::numeric_limits< uint >::max(), parent )
{
}

QskLinearBox::QskLinearBox( Qt::Orientation orientation, QQuickItem* parent ):
    QskLinearBox( orientation, std::numeric_limits< uint >::max(), parent )
{
}

QskLinearBox::QskLinearBox( Qt::Orientation orientation,
        uint dimension, QQuickItem* parent ):
    Inherited( parent ),
    m_data ( new PrivateData( orientation, dimension ) )
{
}

QskLinearBox::~QskLinearBox()
{
}

void QskLinearBox::setDimension( uint dimension )
{
    if ( dimension < 1 )
        dimension = 1;

    if ( dimension != m_data->dimension )
    {
        m_data->dimension = dimension;

        rearrange();
        polish();
    }
}

uint QskLinearBox::dimension() const
{
    return m_data->dimension;
}

void QskLinearBox::setOrientation( Qt::Orientation orientation )
{
    if ( m_data->orientation != orientation )
        transpose();
}

Qt::Orientation QskLinearBox::orientation() const
{
    return m_data->orientation;
}

void QskLinearBox::transpose()
{
    const Qt::Orientation orientation =
        ( m_data->orientation == Qt::Horizontal ) ? Qt::Vertical : Qt::Horizontal;

    const int numItems = itemCount();

    if ( numItems > 0 )
    {
        for ( int i = 0; i < numItems; i++ )
        {
            QskLayoutItem* layoutItem = engine().layoutItemAt( i );

            const int row = layoutItem->firstRow( Qt::Horizontal );
            const int col = layoutItem->firstRow( Qt::Vertical );

            engine().removeItem( layoutItem );

            layoutItem->setFirstRow( row, Qt::Vertical );
            layoutItem->setFirstRow( col, Qt::Horizontal );

#if 1
            if ( m_data->transposeAlignments )
            {
                // Is it worth to blow the API with this flag, or would
                // it be even better to have an indvidual flag for each
                // item - and what about the size policies: do we want to
                // transpose them too ?

                const auto alignment = static_cast< Qt::Alignment >(
                    qbswap( static_cast< quint16 >( layoutItem->alignment() ) ) );

                layoutItem->setAlignment( alignment );
            }
#endif

            if ( layoutItem->item() == nullptr )
            {
                // a spacing or stretch
                layoutItem->setSpacingHint(
                    layoutItem->spacingHint().transposed() );
            }

            engine().insertLayoutItem( layoutItem, i );
        }

        invalidate();
    }

    m_data->orientation = orientation;
    Q_EMIT orientationChanged();
}

void QskLinearBox::setSpacing( qreal spacing )
{
    spacing = qMax( spacing, 0.0 );

    if ( spacing != engine().spacing( Qt::Horizontal ) )
    {
        engine().setSpacing( spacing, Qt::Horizontal | Qt::Vertical );
        activate();

        Q_EMIT spacingChanged();
    }
}

void QskLinearBox::resetSpacing()
{
    const qreal spacing = QskLayoutEngine::defaultSpacing( Qt::Horizontal );
    setSpacing( spacing );
}

qreal QskLinearBox::spacing() const
{
    // do we always want to have the same spacing for both orientations
    return engine().spacing( Qt::Horizontal );
}

void QskLinearBox::setExtraSpacingAt( Qt::Edges edges )
{
    if ( edges != m_data->extraSpacingAt )
    {
        m_data->extraSpacingAt = edges;
        activate();

        Q_EMIT extraSpacingAtChanged();
    }
}

Qt::Edges QskLinearBox::extraSpacingAt() const
{
    return m_data->extraSpacingAt;
}

void QskLinearBox::addSpacer( qreal spacing, int stretchFactor )
{
    insertSpacer( -1, spacing, stretchFactor );
}

void QskLinearBox::insertSpacer( int index, qreal spacing, int stretchFactor )
{
    spacing = qMax( spacing, 0.0 );
    stretchFactor = qMax( stretchFactor, 0 );

    QskLayoutItem* layoutItem;
    if ( m_data->orientation == Qt::Horizontal )
        layoutItem = new QskLayoutItem( QSizeF( spacing, 0 ), stretchFactor, 0, 0 );
    else
        layoutItem = new QskLayoutItem( QSizeF( 0, spacing ), stretchFactor, 0, 0 );

#if 1
    if ( stretchFactor >= 0 )
        layoutItem->setStretchFactor( stretchFactor, m_data->orientation ); // already above ???
#endif

    insertLayoutItem( layoutItem, index );
}

void QskLinearBox::addStretch( int stretchFactor )
{
    insertSpacer( -1, 0, stretchFactor );
}

void QskLinearBox::insertStretch( int index, int stretchFactor )
{
    insertSpacer( index, 0, stretchFactor );
}

void QskLinearBox::setStretchFactor( int index, int stretchFactor )
{
    if ( QskLayoutItem* layoutItem = engine().layoutItemAt( index ) )
    {
        if ( layoutItem->stretchFactor( m_data->orientation ) != stretchFactor )
        {
            layoutItem->setStretchFactor( stretchFactor, m_data->orientation );
            // activate();
        }
    }
}

int QskLinearBox::stretchFactor( int index ) const
{
    if ( QskLayoutItem* layoutItem = engine().layoutItemAt( index ) )
        return layoutItem->stretchFactor( m_data->orientation );

    return 0;
}

void QskLinearBox::setStretchFactor( QQuickItem* item, int stretch )
{
    setStretchFactor( engine().indexOf( item ), stretch );
}

int QskLinearBox::stretchFactor( QQuickItem* item ) const
{
    return stretchFactor( engine().indexOf( item ) );
}

void QskLinearBox::setRetainSizeWhenHidden( int index, bool on )
{
    QskLayoutItem* layoutItem = engine().layoutItemAt( index );
    if ( layoutItem && on != layoutItem->retainSizeWhenHidden() )
    {
        layoutItem->setRetainSizeWhenHidden( on );
        invalidate();
    }
}

bool QskLinearBox::retainSizeWhenHidden( int index ) const
{
    QskLayoutItem* layoutItem = engine().layoutItemAt( index );
    if ( layoutItem )
        return layoutItem->retainSizeWhenHidden();

    return false;
}

void QskLinearBox::setRetainSizeWhenHidden( QQuickItem* item, bool on )
{
    setRetainSizeWhenHidden( engine().indexOf( item ), on );
}

bool QskLinearBox::retainSizeWhenHidden( QQuickItem* item ) const
{
    return retainSizeWhenHidden( engine().indexOf( item ) );
}

void QskLinearBox::setRowSpacing( int row, qreal spacing )
{
    if ( row >= 0 )
    {
        engine().setRowSpacing( row, spacing, Qt::Horizontal );
        activate();
    }
}

qreal QskLinearBox::rowSpacing( int row ) const
{
    return engine().rowSpacing( row, Qt::Horizontal );
}

void QskLinearBox::setColumnSpacing( int column, qreal spacing )
{
    if ( column >= 0 )
    {
        engine().setRowSpacing( column, spacing, Qt::Vertical );
        activate();
    }
}

qreal QskLinearBox::columnSpacing( int column ) const
{
    return engine().rowSpacing( column, Qt::Vertical );
}

void QskLinearBox::setRowStretchFactor( int row, int stretchFactor )
{
    if ( row >= 0 )
    {
        engine().setRowStretchFactor( row, stretchFactor, Qt::Vertical );
        activate();
    }
}

int QskLinearBox::rowStretchFactor( int row ) const
{
    return engine().rowStretchFactor( row, Qt::Vertical );
}

void QskLinearBox::setColumnStretchFactor( int column, int stretchFactor )
{
    if ( column >= 0 )
    {
        engine().setRowStretchFactor( column, stretchFactor, Qt::Horizontal );
        activate();
    }
}

int QskLinearBox::columnStretchFactor( int column ) const
{
    return engine().rowStretchFactor( column, Qt::Horizontal );
}

QSizeF QskLinearBox::contentsSizeHint() const
{
    if ( !isActive() )
        return QSizeF( -1, -1 );

    if ( itemCount() == 0 )
        return QSizeF( 0, 0 );
   
    return engine().sizeHint( Qt::PreferredSize );
}

qreal QskLinearBox::heightForWidth( qreal width ) const 
{
    const QSizeF constraint( width, -1 );
    return engine().sizeHint( Qt::PreferredSize, constraint ).height();
}

qreal QskLinearBox::widthForHeight( qreal height ) const 
{
    const QSizeF constraint( -1, height );
    return engine().sizeHint( Qt::PreferredSize, constraint ).width();
}

void QskLinearBox::setupLayoutItem( QskLayoutItem* layoutItem, int index )
{
    int col = index % m_data->dimension;
    int row = index / m_data->dimension;

    if ( m_data->orientation == Qt::Vertical )
        qSwap( col, row );

    layoutItem->setFirstRow( col, Qt::Horizontal );
    layoutItem->setFirstRow( row, Qt::Vertical );
}

void QskLinearBox::layoutItemInserted( QskLayoutItem*, int index )
{
    if ( index < itemCount() - 1 )
        rearrange();
}

void QskLinearBox::layoutItemRemoved( QskLayoutItem*, int index )
{
    Q_UNUSED( index )
    rearrange();
}

void QskLinearBox::rearrange()
{
    bool doInvalidate = false;

    const int numItems = itemCount();

    for ( int i = 0; i < numItems; i++ )
    {
        int row = i / m_data->dimension;
        int col = i % m_data->dimension;

        if ( m_data->orientation == Qt::Vertical )
            qSwap( col, row );

        QskLayoutItem* layoutItem = engine().layoutItemAt( i );

        if ( layoutItem->firstRow( Qt::Horizontal ) != col
            || layoutItem->firstRow( Qt::Vertical ) != row )
        {
            engine().removeItem( layoutItem );

            layoutItem->setFirstRow( col, Qt::Horizontal );
            layoutItem->setFirstRow( row, Qt::Vertical );

            engine().insertLayoutItem( layoutItem, i );

            doInvalidate = true;
        }
    }

    if ( doInvalidate )
        invalidate();
}

QRectF QskLinearBox::alignedLayoutRect( const QRectF& rect ) const
{
    if ( m_data->extraSpacingAt == 0 )
        return rect;

    const QskLayoutEngine& engine = this->engine();

    QRectF r = rect;

    // not 100% sure if this works for dynamic constraints
    // and having extraSpacingAt for both directions ...

    if ( ( m_data->extraSpacingAt & Qt::LeftEdge ) ||
        ( m_data->extraSpacingAt & Qt::RightEdge ) )
    {
        bool isExpandable = false;

        for ( int i = 0; i < engine.itemCount(); i++ )
        {
            const QskLayoutItem* item = engine.layoutItemAt( i );

            if ( !item->isIgnored() &&
                ( item->sizePolicy( Qt::Horizontal ) & QskSizePolicy::GrowFlag ) )
            {
                isExpandable = true;
                break;
            }
        }

        if ( !isExpandable )
        {
            const qreal w = widthForHeight( r.height() );

            if ( m_data->extraSpacingAt & Qt::LeftEdge )
            {
                if ( m_data->extraSpacingAt & Qt::RightEdge )
                {
                    r.moveLeft( r.center().x() - w / 2 );
                    r.setWidth( w );
                }
                else
                {
                    r.setLeft( r.right() - w );
                }
            }
            else
            {
                r.setRight( r.left() + w );
            }
        }
    }

    if ( ( m_data->extraSpacingAt & Qt::TopEdge ) ||
         ( m_data->extraSpacingAt & Qt::BottomEdge ) )
    {
        bool isExpandable = false;

        for ( int i = 0; i < engine.itemCount(); i++ )
        {
            const QskLayoutItem* item = engine.layoutItemAt( i );

            if ( !item->isIgnored() &&
                ( item->sizePolicy( Qt::Vertical ) & QskSizePolicy::GrowFlag ) )
            {
                isExpandable = true;
                break;
            }
        }

        if ( !isExpandable )
        {
            const qreal h = heightForWidth( r.width() );

            if ( m_data->extraSpacingAt & Qt::TopEdge )
            {
                if ( m_data->extraSpacingAt & Qt::BottomEdge )
                {
                    r.moveTop( r.center().y() - h / 2 );
                    r.setHeight( h );
                }
                else
                {
                    r.setTop( r.bottom() - h );
                }
            }
            else
            {
                r.setBottom( r.top() + h );
            }
        }
    }

    return r;
}

#include "moc_QskLinearBox.cpp"
