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

#include "QskSubcontrolLayoutEngine.h"
#include "QskLayoutElement.h"
#include "QskLayoutChain.h"
#include "QskTextRenderer.h"
#include "QskSkinnable.h"
#include "QskMargins.h"
#include "QskTextOptions.h"

#include <qdebug.h>
#include <qfont.h>
#include <qfontmetrics.h>
#include <qmath.h>

QskSubcontrolLayoutEngine::LayoutElement::LayoutElement(
        const QskSkinnable* skinnable, const QskAspect::Subcontrol subControl )
    : m_skinnable( skinnable )
    , m_subControl( subControl )
{
}

Qt::Alignment QskSubcontrolLayoutEngine::LayoutElement::alignment() const
{
    return m_skinnable->alignmentHint( m_subControl );
}

void QskSubcontrolLayoutEngine::LayoutElement::setMaximumSize( const QSizeF& size )
{
    setExplicitSizeHint( Qt::MaximumSize, size );
}

void QskSubcontrolLayoutEngine::LayoutElement::setMinimumSize( const QSizeF& size )
{
    setExplicitSizeHint( Qt::MinimumSize, size );
}

void QskSubcontrolLayoutEngine::LayoutElement::setPreferredSize( const QSizeF& size )
{
    setExplicitSizeHint( Qt::PreferredSize, size );
}

void QskSubcontrolLayoutEngine::LayoutElement::setFixedSize( const QSizeF& size )
{
    setSizePolicy( QskSizePolicy( QskSizePolicy::Fixed, QskSizePolicy::Fixed ) );

    const auto newSize = size.expandedTo( QSizeF( 0, 0 ) );
    setExplicitSizeHint( Qt::PreferredSize, newSize );
}

void QskSubcontrolLayoutEngine::LayoutElement::setExplicitSizeHint(
    Qt::SizeHint which, const QSizeF& size )
{
    if ( which >= Qt::MinimumSize && which <= Qt::MaximumSize )
    {
        const QSizeF newSize( ( size.width() < 0 ) ? -1.0 : size.width(),
            ( size.height() < 0 ) ? -1.0 : size.height() );

        m_explicitSizeHints[ which ] = size;
    }
}

QSizeF QskSubcontrolLayoutEngine::LayoutElement::sizeHint(
    Qt::SizeHint which, const QSizeF& constraint ) const
{
    if ( which < Qt::MinimumSize || which > Qt::MaximumSize )
        return QSizeF( 0, 0 );

    if ( constraint.isValid() )
        return constraint;

    const bool isConstrained =
        constraint.width() >= 0 || constraint.height() >= 0;

    QSizeF hint;

    if ( !isConstrained )
    {
        // explicit size hints are never constrained
        hint = m_explicitSizeHints[ which ];
    }

    if ( which == Qt::PreferredSize )
    {
        if ( isConstrained )
        {
            const QskMargins padding = m_skinnable->paddingHint( m_subControl );

            auto innerConstraint = constraint;

            if ( constraint.width() > 0 )
            {
                const auto w = constraint.width() - padding.width();
                if ( w <= 0 )
                    return QSizeF();

                innerConstraint.setWidth( w );
            }
            else if ( constraint.height() > 0 )
            {
                const auto h = constraint.height() - padding.height();
                if ( h <= 0 )
                    return QSizeF();

                innerConstraint.setHeight( h );
            }

            hint = implicitSize( innerConstraint );

            if ( hint.width() >= 0 )
                hint.setWidth( hint.width() + padding.width() );

            if ( hint.height() >= 0 )
                hint.setHeight( hint.height() + padding.height() );
        }
        else
        {
            if ( !hint.isValid() )
            {
                const auto sz = implicitSize( constraint );

                if ( hint.width() < 0 )
                    hint.setWidth( sz.width() );

                if ( hint.height() < 0 )
                    hint.setHeight( sz.height() );
            }
        }
    }
    else
    {
        if ( isConstrained )
            hint = QSizeF(); // not implemented
    }

    return hint;
}

QSizeF QskSubcontrolLayoutEngine::TextElement::implicitSize( const QSizeF& constraint ) const
{
    const auto font = skinnable()->effectiveFont( subControl() );
    const auto textOptions = skinnable()->textOptionsHint( subControl() );

#if 0
    // what about skinnable()->strutSizeHint( subControl() ); ????
#endif

    QSizeF hint;

    const qreal lineHeight = QFontMetricsF( font ).height();

    if ( m_text.isEmpty() )
    {
        if ( constraint.height() < 0.0 )
            hint.setHeight( qCeil( lineHeight ) );
    }
    else if ( constraint.width() >= 0.0 )
    {
        if ( textOptions.effectiveElideMode() != Qt::ElideNone )
        {
            hint.setHeight( qCeil( lineHeight ) );
        }
        else
        {
            /*
                In case of QskTextOptions::NoWrap we could count
                the line numbers and calculate the height from
                lineHeight. TODO ...
             */

            qreal maxHeight = std::numeric_limits< qreal >::max();
            if ( maxHeight / lineHeight > textOptions.maximumLineCount() )
            {
                // be careful with overflows
                maxHeight = textOptions.maximumLineCount() * lineHeight;
            }

            QSizeF size( constraint.width(), maxHeight );
            size = QskTextRenderer::textSize( m_text, font, textOptions, size );

            hint.setHeight( qCeil( size.height() ) );
        }
    }
    else if ( constraint.height() >= 0.0 )
    {
        const qreal maxWidth = std::numeric_limits< qreal >::max();

        QSizeF size( maxWidth, constraint.height() );
        size = QskTextRenderer::textSize( m_text, font, textOptions, size );

        hint.setWidth( qCeil( size.width() ) );
    }
    else
    {
        hint = QskTextRenderer::textSize( m_text, font, textOptions );
    }

#if 1
    if ( hint.width() > 0.0 )
    {
        /*
            workaround for a rounding problem that might lead to
            seeing dots, when being in elide mode. TODO ...
         */
        hint.rwidth() += 1;
    }
#endif

    return hint;
}

QSizeF QskSubcontrolLayoutEngine::GraphicElement::effectiveStrutSize() const
{
    auto size = skinnable()->strutSizeHint( subControl() );

    if ( size.isEmpty() )
    {
        const qreal aspectRatio = m_sourceSize.width() / m_sourceSize.height();

        if ( size.width() > 0 )
        {
            size.setHeight( size.width() / aspectRatio );
        }
        else if ( size.height() > 0 )
        {
            size.setWidth( size.height() * aspectRatio );
        }
    }

    return size;
}

QSizeF QskSubcontrolLayoutEngine::GraphicElement::implicitSize(
    const QSizeF& constraint ) const
{
    auto hint = m_sourceSize;

    if ( !hint.isEmpty() )
    {
        const qreal aspectRatio = hint.width() / hint.height();

        if ( constraint.width() >= 0.0 )
        {
            hint.setHeight( constraint.width() / aspectRatio );
            hint.setWidth( -1.0 );
        }
        else if ( constraint.height() > 0.0 )
        {
            hint.setWidth( constraint.height() * aspectRatio );
            hint.setHeight( -1.0 );
        }
    }

    return hint;
}

static QskLayoutChain::CellData qskCell(
    const QskSubcontrolLayoutEngine::LayoutElement* element,
    Qt::Orientation orientation, bool isLayoutOrientation, qreal constraint )
{
    QskLayoutChain::CellData cell;
    cell.isValid = true;

    const auto policy = element->sizePolicy().policy( orientation );

    if ( isLayoutOrientation )
    {
        const auto stretch = element->stretch();

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

    cell.canGrow = policy & QskSizePolicy::GrowFlag;
    cell.metrics = element->metrics( orientation, constraint );

    return cell;
}

class QskSubcontrolLayoutEngine::PrivateData
{
  public:
    PrivateData( Qt::Orientation orientation )
        : orientation( orientation )
    {
        elements.reserve( 2 ); // often Graphic + Text
    }

    Qt::Orientation orientation;
    QVector< LayoutElement* > elements;
};

QskSubcontrolLayoutEngine::QskSubcontrolLayoutEngine( Qt::Orientation orientation )
    : m_data( new PrivateData( orientation ) )
{
    setExtraSpacingAt( Qt::TopEdge | Qt::BottomEdge | Qt::LeftEdge | Qt::RightEdge );
}

QskSubcontrolLayoutEngine::~QskSubcontrolLayoutEngine()
{
    qDeleteAll( m_data->elements );
}

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

        return true;
    }

    return false;
}

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

void QskSubcontrolLayoutEngine::setSpacing( qreal spacing )
{
    Inherited::setSpacing( spacing, Qt::Horizontal | Qt::Vertical );
}

qreal QskSubcontrolLayoutEngine::spacing() const
{
    return Inherited::spacing( m_data->orientation );
}

void QskSubcontrolLayoutEngine::setGraphicTextElements( const QskSkinnable* skinnable,
    QskAspect::Subcontrol textSubcontrol, const QString& text,
    QskAspect::Subcontrol graphicSubControl, const QSizeF& graphicSize )
{
    /*
        QskSubcontrolLayoutEngine was initially created to support the
        situation of an icon and a text, that can be found at several places
        in conrols. This method supports to set up such a layout without
        having to deal with the details of the layout classes.
     */

    auto graphicElement = appendGraphicElement( skinnable, graphicSubControl, graphicSize );
    auto textElement = appendTextElement( skinnable, textSubcontrol, text );

    /*
        Now the difficult part: setting up size policies and the preferred size.
        The code below is probably not the final word - let's see what type of
        default settings we need most often. TODO ...
     */

    using SP = QskSizePolicy;

    if ( textElement && graphicElement == nullptr )
    {
        textElement->setSizePolicy( SP::Preferred, SP::Constrained );
    }
    else if ( graphicElement && textElement == nullptr )
    {
        const auto size = graphicElement->effectiveStrutSize();

        if ( !size.isEmpty() )
            graphicElement->setFixedSize( size );
        else
            graphicElement->setSizePolicy( SP::Ignored, SP::ConstrainedExpanding );
    }
    else if ( textElement && graphicElement )
    {
        if ( orientation() == Qt::Horizontal )
        {
            graphicElement->setSizePolicy( SP::Constrained, SP::Fixed );
            textElement->setSizePolicy( SP::Preferred, SP::Preferred );
        }
        else
        {
            graphicElement->setSizePolicy( SP::Preferred, SP::Fixed );
            textElement->setSizePolicy( SP::Preferred, SP::Constrained );
        }

        auto size = graphicElement->effectiveStrutSize();

        if ( size.isEmpty() )
        {
            const auto h = 1.5 * skinnable->effectiveFontHeight( textSubcontrol );

            size.setWidth( graphicElement->widthForHeight( h ) );
            size.setHeight( h );
        }

        graphicElement->setPreferredSize( size );
    }
}

QskSubcontrolLayoutEngine::GraphicElement* QskSubcontrolLayoutEngine::appendGraphicElement( const QskSkinnable* skinnable,
    QskAspect::Subcontrol graphicSubcontrol, const QSizeF& graphicSize )
{
    GraphicElement* graphicElement = nullptr;

    if ( !graphicSize.isEmpty() && ( graphicSubcontrol != QskAspect::NoSubcontrol ) )
    {
        graphicElement = dynamic_cast< GraphicElement* >( element( graphicSubcontrol ) );
        if ( graphicElement == nullptr )
        {
            graphicElement = new GraphicElement( skinnable, graphicSubcontrol );
            m_data->elements.prepend( graphicElement );
        }

        graphicElement->setSourceSize( graphicSize );
    }

    return graphicElement;
}

QskSubcontrolLayoutEngine::TextElement* QskSubcontrolLayoutEngine::appendTextElement( const QskSkinnable* skinnable,
    QskAspect::Subcontrol textSubcontrol, const QString& text )
{
    TextElement* textElement = nullptr;

    if ( !text.isEmpty() && ( textSubcontrol != QskAspect::NoSubcontrol ) )
    {
        textElement = dynamic_cast< TextElement* >( element( textSubcontrol ) );
        if ( textElement == nullptr )
        {
            textElement = new TextElement( skinnable, textSubcontrol );
            m_data->elements.append( textElement );
        }

        textElement->setText( text );
    }

    return textElement;
}

void QskSubcontrolLayoutEngine::setFixedContent(
    QskAspect::Subcontrol subcontrol, Qt::Orientation orientation, Qt::Alignment alignment )
{
    if( auto e = element( subcontrol ) )
        e->setSizePolicy( QskSizePolicy::Maximum, e->sizePolicy().verticalPolicy() );

    Qt::Edges extraSpacing;

    switch( orientation )
    {
        case Qt::Horizontal:
        {
            extraSpacing |= ( extraSpacingAt() & ( Qt::TopEdge | Qt::BottomEdge ) );

            if( alignment & Qt::AlignLeft )
            {
                extraSpacing |= Qt::RightEdge;
            }
            else if( alignment & Qt::AlignRight )
            {
                extraSpacing |= Qt::LeftEdge;
            }
            else if( alignment & Qt::AlignHCenter )
            {
                extraSpacing |= Qt::LeftEdge | Qt::RightEdge;
            }
            break;
        }
        case Qt::Vertical:
        {
            extraSpacing |= ( extraSpacingAt() & ( Qt::LeftEdge | Qt::RightEdge ) );

            if( alignment & Qt::AlignTop )
            {
                extraSpacing |= Qt::BottomEdge;
            }
            else if( alignment & Qt::AlignBottom )
            {
                extraSpacing |= Qt::TopEdge;
            }
            else if( alignment & Qt::AlignVCenter )
            {
                extraSpacing |= Qt::TopEdge | Qt::BottomEdge;
            }
            break;
        }
    }

    setExtraSpacingAt( extraSpacing );
}

void QskSubcontrolLayoutEngine::addElement( LayoutElement* element )
{
    m_data->elements += element;
}

QskSubcontrolLayoutEngine::LayoutElement* QskSubcontrolLayoutEngine::elementAt( int index ) const
{
    if ( index >= 0 && index < count() )
        return m_data->elements[ index ];

    return nullptr;
}

QskSubcontrolLayoutEngine::LayoutElement* QskSubcontrolLayoutEngine::element(
    QskAspect::Subcontrol subControl ) const
{
    for ( auto element : m_data->elements )
    {
        if ( element->subControl() == subControl )
            return element;
    }

    return nullptr;
}

QskSizePolicy QskSubcontrolLayoutEngine::sizePolicyAt( int index ) const
{
    if ( index >= 0 && index < count() )
    {
        if ( auto element = m_data->elements[ index ] )
            return element->sizePolicy();
    }

    return QskSizePolicy();
}

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

void QskSubcontrolLayoutEngine::layoutItems()
{
    int row = 0;
    int col = 0;

    int& index = m_data->orientation == Qt::Horizontal ? col : row;

    for ( auto element : m_data->elements )
    {
        const auto rect = geometryAt( element, QRect( col, row, 1, 1 ) );
        element->setGeometry( rect );

        index++;
    }
}

int QskSubcontrolLayoutEngine::effectiveCount( Qt::Orientation orientation ) const
{
    const auto count = m_data->elements.count();

    if ( orientation == m_data->orientation )
        return count;
    else
        return ( count >= 1 ) ? 1 : 0;
}

QRectF QskSubcontrolLayoutEngine::subControlRect( QskAspect::Subcontrol subControl ) const
{
    if ( const auto el = element( subControl ) )
        return el->geometry();

    return QRectF( 0.0, 0.0, -1.0, -1.0 ); // something invalid
}

void QskSubcontrolLayoutEngine::invalidateElementCache()
{
}

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

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

    for ( auto element : m_data->elements )
    {
        qreal constraint = -1.0;
        if ( !constraints.isEmpty() )
            constraint = constraints[ index1 ].length;

        const auto cell = qskCell( element, orientation, isLayoutOrientation, constraint );
        chain.expandCell( index2, cell );

        if ( isLayoutOrientation )
            index2++;
        else
            index1++;
    }
}
