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

#include "QskLinearLayoutEngine.h"
#include "QskLayoutMetrics.h"
#include "QskLayoutChain.h"
#include "QskLayoutElement.h"
#include "QskSizePolicy.h"
#include "QskQuick.h"

#include <qvector.h>

namespace
{
    inline QskLayoutMetrics qskItemMetrics(
        QQuickItem* item, Qt::Orientation orientation, qreal constraint )
    {
        const QskItemLayoutElement layoutItem( item );
        return layoutItem.metrics( orientation, constraint );
    }

    class Element
    {
      public:
        Element( QQuickItem* item );
        Element( qreal spacing );
        Element( const Element& );

        Element& operator=( const Element& );

        qreal spacing() const;
        QQuickItem* item() const;

        int stretch() const;
        void setStretch( int );

        bool isIgnored() const;

        QskLayoutChain::CellData cell(
            Qt::Orientation, bool isLayoutOrientation ) const;

      private:

        union
        {
            QQuickItem* m_item;
            qreal m_spacing;
        };

        int m_stretch = -1;
        bool m_isSpacer;
    };

    class ElementsVector : public std::vector< Element >
    {
      public:
        // to avoid warnings when assigning size_t to int
        inline int count() const { return static_cast< int >( size() ); }
    };
}

Element::Element( QQuickItem* item )
    : m_item( item )
    , m_isSpacer( false )
{
}

Element::Element( qreal spacing )
    : m_spacing( spacing )
    , m_isSpacer( true )
{
}

Element::Element( const Element& other )
    : m_stretch( other.m_stretch )
    , m_isSpacer( other.m_isSpacer )
{
    if ( other.m_isSpacer )
        m_spacing = other.m_spacing;
    else
        m_item = other.m_item;
}

Element& Element::operator=( const Element& other )
{
    m_isSpacer = other.m_isSpacer;

    if ( other.m_isSpacer )
        m_spacing = other.m_spacing;
    else
        m_item = other.m_item;

    m_stretch = other.m_stretch;

    return *this;
}

inline qreal Element::spacing() const
{
    return m_isSpacer ? m_spacing : -1.0;
}

inline QQuickItem* Element::item() const
{
    return m_isSpacer ? nullptr : m_item;
}

inline int Element::stretch() const
{
    return m_stretch;
}

inline void Element::setStretch( int stretch )
{
    m_stretch = stretch;
}

bool Element::isIgnored() const
{
    return !( m_isSpacer || qskIsVisibleToLayout( m_item ) );
}

QskLayoutChain::CellData Element::cell(
    Qt::Orientation orientation, bool isLayoutOrientation ) const
{
    QskLayoutChain::CellData cell;
    cell.canGrow = true;
    cell.isValid = true;

    if ( !m_isSpacer )
    {
        const auto policy = qskSizePolicy( m_item ).policy( orientation );

        if ( isLayoutOrientation )
        {
            if ( m_stretch < 0 )
                cell.stretch = ( policy & QskSizePolicy::ExpandFlag ) ? 1 : 0;
            else
                cell.stretch = m_stretch;
        }

        cell.canGrow = policy & QskSizePolicy::GrowFlag;
    }
    else
    {
        if ( isLayoutOrientation )
        {
            cell.metrics.setMinimum( m_spacing );
            cell.metrics.setPreferred( m_spacing );

            if ( m_stretch <= 0 )
                cell.metrics.setMaximum( m_spacing );

            cell.stretch = qMax( m_stretch, 0 );
        }
    }

    return cell;
}

class QskLinearLayoutEngine::PrivateData
{
  public:

    PrivateData( Qt::Orientation orientation, uint dimension )
        : dimension( dimension )
        , sumIgnored( -1 )
        , orientation( orientation )
    {
    }

    inline Element* elementAt( int index ) const
    {
        if ( ( index < 0 ) || ( index >= this->elements.count() ) )
            return nullptr;

        return const_cast< Element* >( &this->elements[index] );
    }

    ElementsVector elements;

    uint dimension;

    mutable int sumIgnored : 30;
    unsigned int orientation : 2;
};

QskLinearLayoutEngine::QskLinearLayoutEngine(
        Qt::Orientation orientation, uint dimension )
    : m_data( new PrivateData( orientation, dimension ) )
{
}

QskLinearLayoutEngine::~QskLinearLayoutEngine()
{
}

bool QskLinearLayoutEngine::setOrientation( Qt::Orientation orientation )
{
    if ( m_data->orientation != orientation )
    {
        m_data->orientation = orientation;
        invalidate( LayoutCache );

        return true;
    }

    return false;
}

Qt::Orientation QskLinearLayoutEngine::orientation() const
{
    return static_cast< Qt::Orientation >( m_data->orientation );
}

bool QskLinearLayoutEngine::setDimension( uint dimension )
{
    if ( dimension < 1 )
        dimension = 1;

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

        return true;
    }

    return false;
}

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

int QskLinearLayoutEngine::count() const
{
    return m_data->elements.count();
}

bool QskLinearLayoutEngine::setStretchFactorAt( int index, int stretchFactor )
{
    if ( auto element = m_data->elementAt( index ) )
    {
        if ( stretchFactor < 0 )
            stretchFactor = -1;

        if ( element->stretch() != stretchFactor )
        {
            element->setStretch( stretchFactor );
            invalidate( LayoutCache );

            return true;
        }
    }

    return false;
}

int QskLinearLayoutEngine::stretchFactorAt( int index ) const
{
    if ( const auto element = m_data->elementAt( index ) )
        return element->stretch();

    return -1;
}

int QskLinearLayoutEngine::insertItem( QQuickItem* item, int index )
{
    auto& elements = m_data->elements;

    if ( index < 0 || index > count() )
    {
        index = elements.count();
        elements.emplace_back( item );
    }
    else
    {
        elements.emplace( elements.begin() + index, item );
    }

    invalidate();
    return index;
}

int QskLinearLayoutEngine::insertSpacerAt( int index, qreal spacing )
{
    spacing = qMax( spacing, static_cast< qreal >( 0.0 ) );

    auto& elements = m_data->elements;

    if ( index < 0 || index > count() )
    {
        index = elements.count();
        elements.emplace_back( spacing );
    }
    else
    {
        elements.emplace( elements.begin() + index, spacing );
    }

    invalidate( LayoutCache );
    return index;
}

bool QskLinearLayoutEngine::removeAt( int index )
{
    auto element = m_data->elementAt( index );
    if ( element == nullptr )
        return false;

    if ( element->isIgnored() )
        m_data->sumIgnored--;

    const auto itemType = qskSizePolicy( element->item() ).constraintType();

    int invalidationMode = LayoutCache;

    if ( itemType > QskSizePolicy::Unconstrained )
        invalidationMode |= ElementCache;

    m_data->elements.erase( m_data->elements.begin() + index );
    invalidate( invalidationMode );

    return true;
}

bool QskLinearLayoutEngine::clear()
{
    if ( count() <= 0 )
        return false;

    m_data->elements.clear();
    invalidate();

    return true;
}

QskSizePolicy QskLinearLayoutEngine::sizePolicyAt( int index ) const
{
    return qskSizePolicy( itemAt( index ) );
}

int QskLinearLayoutEngine::indexOf( const QQuickItem* item ) const
{
    if ( item )
    {
        /*
           indexOf is often called after inserting an item to
           set additinal properties. So we search in reverse order
         */

        for ( int i = count() - 1; i >= 0; --i )
        {
            if ( itemAt( i ) == item )
                return i;
        }
    }

    return -1;
}

QQuickItem* QskLinearLayoutEngine::itemAt( int index ) const
{
    if ( const auto element = m_data->elementAt( index ) )
        return element->item();

    return nullptr;
}

qreal QskLinearLayoutEngine::spacerAt( int index ) const
{
    if ( const auto element = m_data->elementAt( index ) )
        return element->spacing();

    return -1.0;
}

void QskLinearLayoutEngine::layoutItems()
{
    uint row = 0;
    uint col = 0;

    for ( const auto& element : m_data->elements )
    {
        if ( element.isIgnored() )
            continue;

        if ( auto item = element.item() )
        {
            if ( qskIsAdjustableByLayout( item ) )
            {
                const QRect grid( col, row, 1, 1 );

                const QskItemLayoutElement layoutElement( item );

                const auto rect = geometryAt( &layoutElement, grid );
                if ( rect.size().isValid() )
                    qskSetItemGeometry( item, rect );
            }
        }

        if ( m_data->orientation == Qt::Horizontal )
        {
            if ( ++col == m_data->dimension )
            {
                col = 0;
                row++;
            }
        }
        else
        {
            if ( ++row == m_data->dimension )
            {
                row = 0;
                col++;
            }
        }
    }
}

int QskLinearLayoutEngine::effectiveCount( Qt::Orientation orientation ) const
{
    const uint cellCount = effectiveCount();

    if ( orientation == m_data->orientation )
    {
        return qMin( cellCount, m_data->dimension );
    }
    else
    {
        int count = cellCount / m_data->dimension;
        if ( cellCount % m_data->dimension )
            count++;

        return count;
    }
}

int QskLinearLayoutEngine::effectiveCount() const
{
    if ( m_data->sumIgnored < 0 )
    {
        m_data->sumIgnored = 0;

        for ( const auto& element : m_data->elements )
        {
            if ( element.isIgnored() )
                m_data->sumIgnored++;
        }
    }

    return m_data->elements.count() - m_data->sumIgnored;
}

void QskLinearLayoutEngine::invalidateElementCache()
{
    m_data->sumIgnored = -1;
}

void QskLinearLayoutEngine::setupChain( Qt::Orientation orientation,
    const QskLayoutChain::Segments& constraints, QskLayoutChain& chain ) const
{
    uint index1 = 0;
    uint index2 = 0;

    const bool isLayoutOrientation = ( orientation == m_data->orientation );

    qreal constraint = -1.0;

    for ( const auto& element : m_data->elements )
    {
        if ( element.isIgnored() )
            continue;

        if ( !constraints.isEmpty() )
            constraint = constraints[index1].length;

        auto cell = element.cell( orientation, isLayoutOrientation );

        if ( element.item() )
            cell.metrics = qskItemMetrics( element.item(), orientation, constraint );

        chain.expandCell( index2, cell );

        if ( isLayoutOrientation )
        {
            if ( ++index2 == m_data->dimension )
            {
                index2 = 0;
                index1++;
            }
        }
        else
        {
            if ( ++index1 == m_data->dimension )
            {
                index1 = 0;
                index2++;
            }
        }
    }
}
