// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
// Qt-Security score:significant reason:default

#include <QtQuickLayouts/private/qquickflexboxlayoutengine_p.h>

QT_BEGIN_NAMESPACE

QQuickFlexboxLayoutEngine::QQuickFlexboxLayoutEngine()
{
}

QQuickFlexboxLayoutEngine::~QQuickFlexboxLayoutEngine()
{
    clearItems();
}

void QQuickFlexboxLayoutEngine::setFlexboxParentItem(QQuickFlexboxLayoutItem *item)
{
    Q_ASSERT(item != nullptr);
    if (qobject_cast<QQuickFlexboxLayout *>(item->quickItem())) {
        m_flexboxParentItem = item;
        // Yoga parent item shouldn't have measure function
        if (m_flexboxParentItem->hasMeasureFunc())
            m_flexboxParentItem->resetMeasureFunc();
    }
}

void QQuickFlexboxLayoutEngine::clearItems()
{
    for (auto &flexItem: m_flexLayoutItems)
        delete flexItem;
    m_flexLayoutItems.clear();
    // Clear the size hints as we removed all the items from the flex layout
    for (int hintIndex = 0; hintIndex < Qt::NSizeHints; hintIndex++)
        m_cachedSizeHints[hintIndex] = QSizeF();
}

void QQuickFlexboxLayoutEngine::insertItem(QQuickFlexboxLayoutItem *item)
{
    m_flexboxParentItem->insertChild(item, m_flexLayoutItems.count());
    m_flexLayoutItems.append(item);
}

int QQuickFlexboxLayoutEngine::itemCount() const
{
    return m_flexLayoutItems.count();
}

QQuickItem *QQuickFlexboxLayoutEngine::itemAt(int index) const
{
    if (index < 0 || index >= m_flexLayoutItems.count())
        return nullptr;
    return m_flexLayoutItems.at(index)->quickItem();
}

QQuickFlexboxLayoutItem *QQuickFlexboxLayoutEngine::findFlexboxLayoutItem(QQuickItem *item) const
{
    if (!item || (m_flexLayoutItems.count() <= 0))
        return nullptr;
    auto iterator = std::find_if(m_flexLayoutItems.cbegin(), m_flexLayoutItems.cend(),
                                    [item] (QQuickFlexboxLayoutItem *flexLayoutItem){
                                        return (flexLayoutItem->quickItem() == item);
                                    });
    return (iterator == m_flexLayoutItems.cend()) ? nullptr : *iterator;
}

void QQuickFlexboxLayoutEngine::collectItemSizeHints(QQuickFlexboxLayoutItem *flexItem, QSizeF *sizeHints) const
{
    QQuickLayoutAttached *info = nullptr;
    QQuickLayout::effectiveSizeHints_helper(flexItem->quickItem(), sizeHints, &info, true);

    if (!info)
        return;

    // remove margins from the size added by the helper
    auto m = info->qMargins();
    const QSizeF margins{m.left() + m.right(), m.top() + m.bottom()};
    flexItem->cachedItemSizeHints().margins = margins; // save margins for later use

    QSizeF noMarginsHints[Qt::NSizeHints];
    for (int i = 0; i < Qt::NSizeHints; ++i)
        noMarginsHints[i] = sizeHints[i] - margins;

    // Set layout margins to the flex item (Layout.margins)
    if (info->isMarginsSet())
        flexItem->setFlexMargin(QQuickFlexboxLayout::EdgeAll, info->margins());
    if (info->isLeftMarginSet())
        flexItem->setFlexMargin(QQuickFlexboxLayout::EdgeLeft, info->leftMargin());
    if (info->isRightMarginSet())
        flexItem->setFlexMargin(QQuickFlexboxLayout::EdgeRight, info->rightMargin());
    if (info->isTopMarginSet())
        flexItem->setFlexMargin(QQuickFlexboxLayout::EdgeTop, info->topMargin());
    if (info->isBottomMarginSet())
        flexItem->setFlexMargin(QQuickFlexboxLayout::EdgeBottom, info->bottomMargin());

    // Set child item to grow, shrink and stretch depending on the layout
    // properties.
    // If Layout.fillWidth or Layout.fillHeight is set as true, then the child
    // item within the layout can grow or shrink (considering the minimum and
    // maximum sizes) along the main axis which depends upon the flex
    // direction.
    // If both Layout.fillWidth and Layout.fillHeight are set as true, then the
    // child item within the layout need to grow or shrink in cross section and
    // it require stretch need to be set for the yoga flex child item.
    if (info->isFillWidthSet() || info->isFillHeightSet()) {
        // Set stretch to child item both width and height
        if (auto *parentLayoutItem = qobject_cast<QQuickFlexboxLayout *>(m_flexboxParentItem->quickItem())) {
            const bool horz = parentLayoutItem->direction() == QQuickFlexboxLayout::Row ||
                              parentLayoutItem->direction() == QQuickFlexboxLayout::RowReverse;
            if (horz) {
                // If the Layout.fillHeight not been set, the preferred height
                // will be set as height
                if (!info->fillHeight())
                    flexItem->setHeight(noMarginsHints[Qt::PreferredSize].height());
                flexItem->setFlexBasis(noMarginsHints[Qt::PreferredSize].width(), !info->fillWidth());
                // Set child item to grow on main-axis (i.e. the flex
                // direction)
                flexItem->setItemGrowAlongMainAxis(info->fillWidth() ? 1.0f : 0.0f);
                // Set child item to shrink on main-axis (i.e. the flex
                // direction)
                flexItem->setItemShrinkAlongMainAxis(info->fillWidth() ? 1.0f : 0.0f);
            }
            else {
                // If the Layout.fillWidth not been set, the preferred width
                // will be set as width
                if (!info->fillWidth())
                    flexItem->setWidth(noMarginsHints[Qt::PreferredSize].width());
                flexItem->setFlexBasis(noMarginsHints[Qt::PreferredSize].height(), !info->fillHeight());
                // Set child item to grow on main-axis (i.e. the flex
                // direction)
                flexItem->setItemGrowAlongMainAxis(info->fillHeight() ? 1.0f : 0.0f);
                // Set child item to shrink on main-axis (i.e. the flex
                // direction)
                flexItem->setItemShrinkAlongMainAxis(info->fillHeight() ? 1.0f : 0.0f);
            }
            // If the Layout.fillHeight not been set, the preferred height will be
            // set as height in the previous condition. Otherwise (for
            // Layout.fillHeight been set as true), make flex item to AlignStretch.
            // Thus it can also grow vertically.
            // Note: The same applies for Layout.fillWidth to grow horizontally.
            if ((horz && (qt_is_nan(flexItem->size().height()) && info->fillHeight()))
                || (!horz && (qt_is_nan(flexItem->size().width()) && info->fillWidth()))) {
                flexItem->setItemStretchAlongCrossSection();
            } else {
                flexItem->inheritItemStretchAlongCrossSection();
            }
        }
    }
}

SizeHints &QQuickFlexboxLayoutEngine::cachedItemSizeHints(int index) const
{
    QQuickFlexboxLayoutItem *flexBoxLayoutItem = m_flexLayoutItems.at(index);
    Q_ASSERT(flexBoxLayoutItem);
    SizeHints &hints = flexBoxLayoutItem->cachedItemSizeHints();
    if (!hints.min().isValid())
        collectItemSizeHints(flexBoxLayoutItem, hints.array);
    return hints;
}

QSizeF QQuickFlexboxLayoutEngine::sizeHint(Qt::SizeHint whichSizeHint) const
{
    QSizeF &askingFor = m_cachedSizeHints[whichSizeHint];
    if (!askingFor.isValid()) {
        QSizeF &minS = m_cachedSizeHints[Qt::MinimumSize];
        QSizeF &prefS = m_cachedSizeHints[Qt::PreferredSize];
        QSizeF &maxS = m_cachedSizeHints[Qt::MaximumSize];

        minS = QSizeF(0,0);
        prefS = QSizeF(0,0);
        maxS = QSizeF(0,0);

        qreal flexColumnGap = 0;
        qreal flexRowGap = 0;
        bool wrap = false;
        auto *qFlexLayout = qobject_cast<QQuickFlexboxLayout *>(m_flexboxParentItem->quickItem());
        if (qFlexLayout) {
            flexColumnGap = qFlexLayout->columnGap();
            flexRowGap = qFlexLayout->rowGap();
            wrap = qFlexLayout->wrap() != QQuickFlexboxLayout::NoWrap;
        }

        const int count = itemCount();
        for (int i = 0; i < count; ++i) {
            SizeHints &hints = cachedItemSizeHints(i);

            QSizeF noMarginsHints[Qt::NSizeHints];
            for (int i = 0; i < Qt::NSizeHints; ++i)
                noMarginsHints[i] = hints.array[i] - hints.margins;

            auto &flexLayoutItem = m_flexLayoutItems.at(i);
            flexLayoutItem->setMinSize(noMarginsHints[Qt::MinimumSize]);
            if (flexLayoutItem->isFlexBasisUndefined()) {
                // If flex basis is undefined and item is still stretched, it
                // meant the flex child item has a const width or height but
                // want to stretch vertically or horizontally
                if (flexLayoutItem->isItemStreched()) {
                    if (qFlexLayout) {
                        // Reset the size of the child item if the parent sets
                        // its property 'align-item' to strecth
                        // Note: The child item can also override the parent
                        // align-item property through align-self
                        // (this is FlexboxLayout.alignItem for quick items)
                        flexLayoutItem->resetSize();
                        if (qFlexLayout->direction() == QQuickFlexboxLayout::Row ||
                            qFlexLayout->direction() == QQuickFlexboxLayout::RowReverse) {
                            flexLayoutItem->setWidth(noMarginsHints[Qt::PreferredSize].width());
                        } else {
                            flexLayoutItem->setHeight(noMarginsHints[Qt::PreferredSize].height());
                        }
                    }
                } else {
                    flexLayoutItem->setSize(noMarginsHints[Qt::PreferredSize]);
                }
            }
            flexLayoutItem->setMaxSize(noMarginsHints[Qt::MaximumSize]);
            // The preferred size, minimum and maximum size of the parent item
            // will be calculated as follows
            // If no wrap enabled in the flex layout:
            //      For flex direction Row or RowReversed:
            //              Parent pref, min and max width:
            //                  Sum of the pref, min and max width of the child
            //                  items
            //              Parent pref, min and max height:
            //                  Max of pref, min and max height of the child
            //                  items
            //      For flex direction Column or ColumnReversed:
            //              Parent pref, min and max width:
            //                  Max of pref, min and max width of the child
            //                  items
            //              Parent pref, min and max height:
            //                  Sum of the pref, min and max height of the
            //                  child items
            // Else if wrap enabled in the flex layout: (either Wrap or
            // WrapReversed)
            //      For flex direction Row or RowReversed or Column or
            //      ColumnReversed:
            //              Parent pref, min, max width/height:
            //                  Sum of the pref, min and max width/height of
            //                  the child items
            if (qFlexLayout) {
                const bool last = (i == count - 1);
                if (qFlexLayout->direction() == QQuickFlexboxLayout::Row ||
                    qFlexLayout->direction() == QQuickFlexboxLayout::RowReverse) {
                    if (wrap) {
                        // Minimum size
                        minS.setWidth(qMax(minS.width(), hints.min().width()));
                        minS.setHeight(qMax(minS.height(), hints.min().height()));
                        // Preferred size. Same as no-wrap preferred size
                        prefS.setWidth(prefS.width() + hints.pref().width());
                        prefS.setHeight(qMax(prefS.height(), hints.pref().height()));
                        // Maximum size
                        maxS.setWidth(maxS.width() + hints.max().width());
                        maxS.setHeight(maxS.height() + hints.max().height());

                        if (!last) {
                            prefS.rwidth() += flexColumnGap;
                            maxS.rwidth() += flexColumnGap;
                            maxS.rheight() += flexRowGap;
                        }
                    } else {
                        // Minimum size
                        minS.setWidth(minS.width() + hints.min().width());
                        minS.setHeight(qMax(minS.height(), hints.min().height()));
                        // Preferred size
                        prefS.setWidth(prefS.width() + hints.pref().width());
                        prefS.setHeight(qMax(prefS.height(), hints.pref().height()));
                        // Maximum size
                        maxS.setWidth(maxS.width() + hints.max().width());
                        maxS.setHeight(qMax(maxS.height(), hints.max().height()));

                        if (!last) {
                            minS.rwidth() += flexColumnGap;
                            prefS.rwidth() += flexColumnGap;
                            maxS.rwidth() += flexColumnGap;
                        }
                    }

                } else if (qFlexLayout->direction() == QQuickFlexboxLayout::Column ||
                            qFlexLayout->direction() == QQuickFlexboxLayout::ColumnReverse) {
                    if (wrap) {
                        // Minimum size
                        minS.setWidth(qMax(minS.width(), hints.min().width()));
                        minS.setHeight(qMax(minS.height(), hints.min().height()));
                        // Preferred size. Same as no-wrap preferred size
                        prefS.setWidth(qMax(prefS.width(), hints.pref().width()));
                        prefS.setHeight(prefS.height() + hints.pref().height());
                        // Maximum size
                        maxS.setWidth(maxS.width() + hints.max().width());
                        maxS.setHeight(maxS.height() + hints.max().height());

                        if (!last) {
                            prefS.rheight() += flexRowGap;
                            maxS.rwidth() += flexColumnGap;
                            maxS.rheight() += flexRowGap;
                        }
                    } else {
                        // Minimum size
                        minS.setWidth(qMax(minS.width(), hints.min().width()));
                        minS.setHeight(minS.height() + hints.min().height());
                        // Preferred size
                        prefS.setWidth(qMax(prefS.width(), hints.pref().width()));
                        prefS.setHeight(prefS.height() + hints.pref().height());
                        // Maximum size
                        maxS.setWidth(qMax(maxS.width(), hints.max().width()));
                        maxS.setHeight(maxS.height() + hints.max().height());

                        if (!last) {
                            minS.rheight() += flexRowGap;
                            prefS.rheight() += flexRowGap;
                            maxS.rheight() += flexRowGap;
                        }
                    }
                }
            }
        }
    }
    return askingFor;
}

void QQuickFlexboxLayoutEngine::invalidateItemSizeHint(QQuickItem *item)
{
    if (auto *flexLayoutItem = findFlexboxLayoutItem(item)) {
        SizeHints &hints = flexLayoutItem->cachedItemSizeHints();
        hints.min() = QSizeF();
        hints.pref() = QSizeF();
        hints.max() = QSizeF();
        hints.margins = {0,0};
    }
}

void QQuickFlexboxLayoutEngine::setGeometries(const QSizeF &contentSize)
{
    m_flexboxParentItem->setSize(contentSize);
    m_flexboxParentItem->computeLayout(contentSize);
    for (auto *item : m_flexLayoutItems) {
        item->quickItem()->setPosition(item->position());
        QSizeF oldSize = item->quickItem()->size();
        QSizeF newSize = item->size();
        if (oldSize == newSize) {
            // Enforce rearrange as the size remains the same.
            // This can happen in a case where we add a child item to the layout
            // (which is already a child to a layout)
            if (auto *layout = qobject_cast<QQuickLayout *>(item->quickItem())) {
                if (layout->invalidatedArrangement())
                    layout->rearrange(newSize);
            }
        } else {
            item->quickItem()->setSize(newSize);
        }
    }
}

// TODO: Need to check whether its needed to get the size of the flex item
// through the callback measure function
// QSizeF QQuickFlexboxLayoutItem::getSizeHint(float width,
// YGMeasureMode widthMode, float height, YGMeasureMode heightMode)
// {
//     QSizeF newSize(width, height);
//     switch (widthMode) {
//     case YGMeasureModeAtMost:
//         newSize.setWidth(m_cachedSizeHint.max().width());
//         break;
//     case YGMeasureModeExactly:
//     case YGMeasureModeUndefined:
//         newSize.setWidth(m_cachedSizeHint.pref().width());
//         break;
//     default: break;
//     }
//     switch (heightMode) {
//     case YGMeasureModeAtMost:
//         newSize.setHeight(m_cachedSizeHint.max().height());
//         break;
//     case YGMeasureModeExactly:
//     case YGMeasureModeUndefined:
//         newSize.setHeight(m_cachedSizeHint.pref().height());
//         break;
//     default: break;
//     }
//     return newSize;
// }

// YGSize QQuickFlexboxLayoutItem::measureFunc(YGNodeRef nodeRef, float width,
// YGMeasureMode widthMode, float height, YGMeasureMode heightMode)
// {
//     YGSize defaultSize;
//     auto *layoutItem = static_cast<QQuickFlexboxLayoutItem *>(YGNodeGetContext(nodeRef));
//     if (layoutItem) {
//         QSizeF size = layoutItem->getSizeHint(width, widthMode, height, heightMode);
//         defaultSize.width = (qt_is_nan(size.width()) || qt_is_inf(size.width())) ? YGUndefined : size.width();
//         defaultSize.height = (qt_is_nan(size.height()) || qt_is_inf(size.height())) ? YGUndefined : size.height();
//     }
//     return defaultSize;
// }

QT_END_NAMESPACE
