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

#include "QskMenuSkinlet.h"
#include "QskMenu.h"

#include "QskGraphic.h"
#include "QskColorFilter.h"
#include "QskTextOptions.h"
#include "QskFunctions.h"
#include "QskMargins.h"
#include "QskFunctions.h"
#include "QskLabelData.h"

#include "QskSGNode.h"

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

static inline int qskActionIndex( const QskMenu* menu, int optionIndex )
{
    if ( optionIndex < 0 )
        return -1;

    const auto& actions = menu->actions();

    auto it = std::lower_bound(
        actions.constBegin(), actions.constEnd(), optionIndex );

    return it - actions.constBegin();
}

static inline qreal qskPaddedSeparatorHeight( const QskMenu* menu )
{
    using Q = QskMenu;

    const auto margins = menu->marginHint( Q::Separator );

    return menu->metric( Q::Separator | QskAspect::Size )
        + margins.top() + margins.bottom();
}

class QskMenuSkinlet::PrivateData
{
  public:
    class CacheGuard
    {
      public:
        CacheGuard( PrivateData* data )
            : m_data( data )
        {
            m_data->enableCache( true );
        }

        ~CacheGuard()
        {
            m_data->enableCache( false );
        }

      private:
        PrivateData* m_data;
    };

    void enableCache( bool on )
    {
        m_isCaching = on;
        m_segmentHeight = m_segmentWidth = m_graphicWidth = m_textWidth = -1.0;
    }

    inline qreal graphicWidth( const QskMenu* menu ) const
    {
        if ( m_isCaching )
        {
            if ( m_graphicWidth < 0.0 )
                m_graphicWidth = graphicWidthInternal( menu );

            return m_graphicWidth;
        }

        return graphicWidthInternal( menu );
    }

    inline qreal textWidth( const QskMenu* menu ) const
    {
        if ( m_isCaching )
        {
            if ( m_textWidth < 0.0 )
                m_textWidth = textWidthInternal( menu );

            return m_textWidth;
        }

        return textWidthInternal( menu );
    }

    inline qreal segmentWidth( const QskMenu* menu ) const
    {
        if ( m_isCaching )
        {
            if ( m_segmentWidth < 0.0 )
                m_segmentWidth = segmentWidthInternal( menu );

            return m_segmentWidth;
        }

        return segmentWidthInternal( menu );
    }

    inline qreal segmentHeight( const QskMenu* menu ) const
    {
        if ( m_isCaching )
        {
            if ( m_segmentHeight < 0.0 )
                m_segmentHeight = segmentHeightInternal( menu );

            return m_segmentHeight;
        }

        return segmentHeightInternal( menu );
    }

  private:
    qreal graphicWidthInternal( const QskMenu* menu ) const
    {
        const auto hint = menu->strutSizeHint( QskMenu::Icon );
        const qreal textHeight = menu->effectiveFontHeight( QskMenu::Text );

        const auto h = qMax( hint.height(), textHeight );

        qreal maxW = 0.0;

        const auto options = menu->options();
        for ( auto& option : options )
        {
            const auto graphic = option.icon().graphic();
            if ( !graphic.isNull() )
            {
                const auto w = graphic.widthForHeight( h );
                if( w > maxW )
                    maxW = w;
            }
        }

        return qMax( hint.width(), maxW );
    }

    qreal textWidthInternal( const QskMenu* menu ) const
    {
        const QFontMetricsF fm( menu->effectiveFont( QskMenu::Text ) );

        auto maxWidth = 0.0;

        const auto options = menu->options();
        for ( auto& option : options )
        {
            if( !option.text().isEmpty() )
            {
                const auto w = qskHorizontalAdvance( fm, option.text() );
                if( w > maxWidth )
                    maxWidth = w;
            }
        }

        return maxWidth;
    }

    qreal segmentWidthInternal( const QskMenu* menu ) const
    {
        using Q = QskMenu;

        const auto spacing = menu->spacingHint( Q::Segment );
        const auto padding = menu->paddingHint( Q::Segment );

        auto w = graphicWidth( menu ) + spacing + textWidth( menu );

        w += padding.left() + padding.right();

        const auto minWidth = menu->strutSizeHint( Q::Segment ).width();
        return qMax( w, minWidth );
    }

    qreal segmentHeightInternal( const QskMenu* menu ) const
    {
        using Q = QskMenu;

        const auto graphicHeight = menu->strutSizeHint( Q::Icon ).height();
        const auto textHeight = menu->effectiveFontHeight( Q::Text );
        const auto padding = menu->paddingHint( Q::Segment );

        qreal h = qMax( graphicHeight, textHeight );
        h += padding.top() + padding.bottom();

        const auto minHeight = menu->strutSizeHint( Q::Segment ).height();
        h = qMax( h, minHeight );

        return h;
    }

    bool m_isCaching;

    mutable qreal m_graphicWidth = -1.0;
    mutable qreal m_textWidth = -1.0;
    mutable qreal m_segmentHeight = -1.0;
    mutable qreal m_segmentWidth = -1.0;
};

QskMenuSkinlet::QskMenuSkinlet( QskSkin* skin )
    : Inherited( skin )
    , m_data( new PrivateData() )
{
    appendNodeRoles( { ContentsRole, PanelRole } );
}

QskMenuSkinlet::~QskMenuSkinlet() = default;

QSGNode* QskMenuSkinlet::updateSubNode(
    const QskSkinnable* skinnable, quint8 nodeRole, QSGNode* node ) const
{
    switch ( nodeRole )
    {
        case ContentsRole:
        {
            const auto popup = static_cast< const QskPopup* >( skinnable );

            auto rect = popup->contentsRect();
            if ( rect.isEmpty() )
                return nullptr;

            return updateContentsNode( popup, node );
        }
    }

    return Inherited::updateSubNode( skinnable, nodeRole, node );
}

QRectF QskMenuSkinlet::cursorRect(
    const QskSkinnable* skinnable, const QRectF& contentsRect, int index ) const
{
    using Q = QskMenu;

    const auto menu = static_cast< const QskMenu* >( skinnable );
    const auto actions = menu->actions();

    index = qskActionIndex( menu, index );

    QRectF rect;

    if ( index < 0 )
    {
        rect = sampleRect( skinnable, contentsRect, Q::Segment, 0 );
        rect.setBottom( rect.top() );
    }
    else if ( index >= actions.count() )
    {
        rect = sampleRect( skinnable, contentsRect, Q::Segment, actions.count() - 1 );
        rect.setTop( rect.bottom() );
    }
    else
    {
        rect = sampleRect( skinnable, contentsRect, Q::Segment, index );
    }

    return rect;
}

QRectF QskMenuSkinlet::subControlRect(
    const QskSkinnable* skinnable, const QRectF& contentsRect,
    QskAspect::Subcontrol subControl ) const
{
    using Q = QskMenu;

    const auto menu = static_cast< const QskMenu* >( skinnable );

    if( subControl == Q::Panel )
    {
        return contentsRect;
    }

    if( subControl == Q::Cursor )
    {
        if ( menu->currentIndex() < 0 )
            return QRectF();

        const qreal pos = menu->positionHint( Q::Cursor );

        const int pos1 = qFloor( pos );
        const int pos2 = qCeil( pos );

        auto rect = cursorRect( skinnable, contentsRect, pos1 );

        if ( pos1 != pos2 )
        {
            const auto r = cursorRect( skinnable, contentsRect, pos2 );

            const qreal ratio = ( pos - pos1 ) / ( pos2 - pos1 );
            rect = qskInterpolatedRect( rect, r, ratio );
        }

        return rect;
    }

    return Inherited::subControlRect( skinnable, contentsRect, subControl );
}

QRectF QskMenuSkinlet::sampleRect(
    const QskSkinnable* skinnable, const QRectF& contentsRect,
    QskAspect::Subcontrol subControl, int index ) const
{
    using Q = QskMenu;

    const auto menu = static_cast< const QskMenu* >( skinnable );

    if ( subControl == Q::Segment )
    {
        const auto h = m_data->segmentHeight( menu );

        auto dy = index * h;

        if ( const auto n = menu->actions()[ index ] - index )
            dy += n * qskPaddedSeparatorHeight( menu );

        const auto r = menu->subControlContentsRect( Q::Panel );
        return QRectF( r.x(), r.y() + dy, r.width(), h );
    }

    if ( subControl == QskMenu::Icon || subControl == QskMenu::Text )
    {
        const auto r = sampleRect( menu, contentsRect, Q::Segment, index );
        const auto graphicWidth = m_data->graphicWidth( menu );

        if ( subControl == QskMenu::Icon )
        {
            auto graphicRect = r;
            graphicRect.setWidth( graphicWidth );
            const auto padding = menu->paddingHint( QskMenu::Icon );
            graphicRect = graphicRect.marginsRemoved( padding );

            return graphicRect;
        }
        else
        {
            auto textRect = r;

            if ( graphicWidth > 0.0 )
            {
                const auto spacing = skinnable->spacingHint( Q::Segment );
                textRect.setX( r.x() + graphicWidth + spacing );
            }

            return textRect;
        }
    }

    if ( subControl == QskMenu::Separator )
    {
        const auto separators = menu->separators();
        if ( index >= separators.count() )
            return QRectF();

        const auto h = qskPaddedSeparatorHeight( menu );

        auto y = index * h;

        if ( const auto n = qskActionIndex( menu, separators[ index ] ) )
            y += n * m_data->segmentHeight( menu );

        const auto r = menu->subControlContentsRect( Q::Panel );
        return QRectF( r.left(), y, r.width(), h );
    }

    return Inherited::sampleRect(
        skinnable, contentsRect, subControl, index );
}

int QskMenuSkinlet::sampleIndexAt(
    const QskSkinnable* skinnable, const QRectF& contentsRect,
    QskAspect::Subcontrol subControl, const QPointF& pos ) const
{
    const PrivateData::CacheGuard guard( m_data.get() );
    return Inherited::sampleIndexAt( skinnable, contentsRect, subControl, pos );
}

int QskMenuSkinlet::sampleCount(
    const QskSkinnable* skinnable, QskAspect::Subcontrol subControl ) const
{
    using Q = QskMenu;

    if ( subControl == Q::Segment || subControl == Q::Icon || subControl == Q::Text )
    {
        const auto menu = static_cast< const QskMenu* >( skinnable );
        return menu->actions().count();
    }

    if ( subControl == Q::Separator )
    {
        const auto menu = static_cast< const QskMenu* >( skinnable );
        return menu->separators().count();
    }

    return Inherited::sampleCount( skinnable, subControl );
}

QskAspect::States QskMenuSkinlet::sampleStates(
    const QskSkinnable* skinnable, QskAspect::Subcontrol subControl, int index ) const
{
    using Q = QskMenu;
    using A = QskAspect;

    auto states = Inherited::sampleStates( skinnable, subControl, index );

    if ( subControl == Q::Segment || subControl == Q::Icon || subControl == Q::Text )
    {
        const auto menu = static_cast< const QskMenu* >( skinnable );

        if ( menu->currentIndex() == menu->actions()[ index ] )
        {
            states |= Q::Selected;

            if( menu->isPressed() )
            {
                states |= Q::Pressed;
            }
            else
            {
                states &= ~Q::Pressed;
            }
        }

        const auto cursorPos = menu->effectiveSkinHint(
            Q::Segment | Q::Hovered | A::Metric | A::Position ).toPointF();

        if( !cursorPos.isNull() && menu->indexAtPosition( cursorPos ) == index )
        {
            states |= Q::Hovered;
        }
        else
        {
            states &= ~Q::Hovered;
        }
    }

    return states;
}

QVariant QskMenuSkinlet::sampleAt( const QskSkinnable* skinnable,
    QskAspect::Subcontrol subControl, int index ) const
{
    using Q = QskMenu;

    if ( subControl == Q::Icon || subControl == Q::Text )
    {
        const auto menu = static_cast< const QskMenu* >( skinnable );

        const auto option = menu->optionAt( index );

        if ( subControl == Q::Icon )
            return QVariant::fromValue( option.icon().graphic() );
        else
            return QVariant::fromValue( option.text() );
    }

    return Inherited::sampleAt( skinnable, subControl, index );
}

QSGNode* QskMenuSkinlet::updateContentsNode(
    const QskPopup* popup, QSGNode* contentsNode ) const
{
    const PrivateData::CacheGuard guard( m_data.get() );
    return updateMenuNode( popup, contentsNode );
}

QSGNode* QskMenuSkinlet::updateMenuNode(
    const QskSkinnable* skinnable, QSGNode* contentsNode ) const
{
    enum { Panel, Segment, Cursor, Icon, Text, Separator };
    static QVector< quint8 > roles = { Panel, Separator, Segment, Cursor, Icon, Text };

    if ( contentsNode == nullptr )
        contentsNode = new QSGNode();

    for ( const auto role : roles )
    {
        auto oldNode = QskSGNode::findChildNode( contentsNode, role );

        QSGNode* newNode = nullptr;

        switch( role )
        {
            case Panel:
            {
                newNode = updateBoxNode( skinnable, oldNode, QskMenu::Panel );
                break;
            }
            case Segment:
            {
                newNode = updateSeriesNode( skinnable, QskMenu::Segment, oldNode );
                break;
            }
            case Cursor:
            {
                newNode = updateBoxNode( skinnable, oldNode, QskMenu::Cursor );
                break;
            }
            case Icon:
            {
                newNode = updateSeriesNode( skinnable, QskMenu::Icon, oldNode );
                break;
            }
            case Text:
            {
                newNode = updateSeriesNode( skinnable, QskMenu::Text, oldNode );
                break;
            }
            case Separator:
            {
                newNode = updateSeriesNode( skinnable, QskMenu::Separator, oldNode );
                break;
            }
        }

        QskSGNode::replaceChildNode( roles, role, contentsNode, oldNode, newNode );
    }

    return contentsNode;
}

QSGNode* QskMenuSkinlet::updateSampleNode( const QskSkinnable* skinnable,
    QskAspect::Subcontrol subControl, int index, QSGNode* node ) const
{
    using Q = QskMenu;

    auto menu = static_cast< const QskMenu* >( skinnable );

    const auto rect = sampleRect( menu, menu->contentsRect(), subControl, index );

    if ( subControl == Q::Segment )
    {
        return updateBoxNode( menu, node, rect, subControl );
    }

    if ( subControl == Q::Icon )
    {
        index = menu->actions()[ index ];

        const auto graphic = menu->optionAt( index ).icon().graphic();
        if ( graphic.isNull() )
            return nullptr;

        const auto alignment = menu->alignmentHint( subControl, Qt::AlignCenter );
        const auto filter = menu->effectiveGraphicFilter( subControl );

        return QskSkinlet::updateGraphicNode(
            menu, node, graphic, filter, rect, alignment );
    }

    if ( subControl == Q::Text )
    {
        index = menu->actions()[ index ];

        const auto text = menu->optionAt( index ).text();
        if ( text.isEmpty() )
            return nullptr;

        const auto alignment = menu->alignmentHint(
            subControl, Qt::AlignVCenter | Qt::AlignLeft );

        return QskSkinlet::updateTextNode( menu, node, rect,
            alignment, text, Q::Text );
    }

    if ( subControl == Q::Separator )
    {
        auto gradient = menu->gradientHint( subControl );
        if ( ( gradient.type() == QskGradient::Stops ) && !gradient.isMonochrome() )
            gradient.setLinearDirection( Qt::Vertical );

        return updateBoxNode( menu, node, rect, gradient, subControl );
    }

    return nullptr;
}

QSizeF QskMenuSkinlet::sizeHint( const QskSkinnable* skinnable,
    Qt::SizeHint which, const QSizeF& ) const
{
    if ( which != Qt::PreferredSize )
        return QSizeF();

    using Q = QskMenu;
    const auto menu = static_cast< const QskMenu* >( skinnable );

    const PrivateData::CacheGuard guard( m_data.get() );

    qreal w = 0.0;
    qreal h = 0.0;

    if ( const auto count = sampleCount( skinnable, Q::Segment ) )
    {
        w = m_data->segmentWidth( menu );
        h = count * m_data->segmentHeight( menu );
    }

    if ( const auto count = sampleCount( skinnable, Q::Separator ) )
    {
        h += count * qskPaddedSeparatorHeight( menu );
    }

    auto hint = skinnable->outerBoxSize( QskMenu::Panel, QSizeF( w, h ) );
    hint = hint.expandedTo( skinnable->strutSizeHint( QskMenu::Panel ) );

    return hint;
}

#include "moc_QskMenuSkinlet.cpp"
