FlowLayout.kt

package androidx.compose.foundation.layout

import androidx.compose.runtime.Composable
import androidx.compose.runtime.collection.MutableVector
import androidx.compose.runtime.collection.mutableVectorOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.IntrinsicMeasurable
import androidx.compose.ui.layout.IntrinsicMeasureScope
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.MeasurePolicy
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.MeasureScope
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.constrainHeight
import androidx.compose.ui.unit.constrainWidth
import androidx.compose.ui.util.fastForEachIndexed
import kotlin.math.ceil
import kotlin.math.max

/**
 * [FlowRow] is a layout that fills items from left to right (ltr) in LTR layouts
 * or right to left (rtl) in RTL layouts and when it runs out of space, moves to
 * the next "row" or "line" positioned on the bottom, and then continues filling items
 * until the items run out.
 *
 * Example:
 * @sample androidx.compose.foundation.layout.samples.SimpleFlowRow
 *
 * When a Modifier [RowScope.weight] is provided, it scales the item
 * based on the number items that fall on the row it was placed in.
 *
 * Example:
 * @sample androidx.compose.foundation.layout.samples.SimpleFlowRowWithWeights
 *
 *
 * @param modifier The modifier to be applied to the Row.
 * @param horizontalArrangement The horizontal arrangement of the layout's children.
 * @param verticalArrangement The vertical arrangement of the layout's virtual rows.
 * @param maxItemsInEachRow The maximum number of items per row
 * @param content The content as a [RowScope]
 *
 * @see FlowColumn
 * @see [androidx.compose.foundation.layout.Row]
 */
@Composable
@ExperimentalLayoutApi
fun FlowRow(
    modifier: Modifier = Modifier,
    horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
    verticalArrangement: Arrangement.Vertical = Arrangement.Top,
    maxItemsInEachRow: Int = Int.MAX_VALUE,
    content: @Composable RowScope.() -> Unit
) {
    val measurePolicy = rowMeasurementHelper(
        horizontalArrangement,
        verticalArrangement,
        maxItemsInEachRow
    )
    Layout(
        content = { RowScopeInstance.content() },
        measurePolicy = measurePolicy,
        modifier = modifier
    )
}

/**
 * [FlowColumn] is a layout that fills items from top to bottom, and when it runs out of space
 * on the bottom, moves to the next "column" or "line"
 * on the right or left based on ltr or rtl layouts,
 * and then continues filling items from top to bottom.
 *
 * It supports ltr in LTR layouts, by placing the first column to the left, and then moving
 * to the right
 * It supports rtl in RTL layouts, by placing the first column to the right, and then moving
 * to the left
 *
 * Example:
 * @sample androidx.compose.foundation.layout.samples.SimpleFlowColumn
 *
 * When a Modifier [ColumnScope.weight] is provided, it scales the item
 * based on the number items that fall on the column it was placed in.
 *
 * Example:
 * @sample androidx.compose.foundation.layout.samples.SimpleFlowColumnWithWeights
 *
 * @param modifier The modifier to be applied to the Row.
 * @param verticalArrangement The vertical arrangement of the layout's children.
 * @param horizontalArrangement The horizontal arrangement of the layout's virtual columns
 * @param maxItemsInEachColumn The maximum number of items per column
 * @param content The content as a [ColumnScope]
 *
 * @see FlowRow
 * @see [androidx.compose.foundation.layout.Column]
 */
@Composable
@ExperimentalLayoutApi
fun FlowColumn(
    modifier: Modifier = Modifier,
    verticalArrangement: Arrangement.Vertical = Arrangement.Top,
    horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
    maxItemsInEachColumn: Int = Int.MAX_VALUE,
    content: @Composable ColumnScope.() -> Unit
) {
    val measurePolicy = columnMeasurementHelper(
        verticalArrangement,
        horizontalArrangement,
        maxItemsInEachColumn
    )
    Layout(
        content = { ColumnScopeInstance.content() },
        measurePolicy = measurePolicy,
        modifier = modifier
    )
}

private fun getVerticalArrangement(verticalArrangement: Arrangement.Vertical):
        (Int, IntArray, LayoutDirection, Density, IntArray) -> Unit =
    { totalSize: Int, size: IntArray, _, density: Density, outPosition: IntArray ->
        with(verticalArrangement) {
            density.arrange(totalSize, size, outPosition)
        }
    }

private fun getHorizontalArrangement(horizontalArrangement: Arrangement.Horizontal) =
    { totalSize: Int, size: IntArray, layoutDirection: LayoutDirection,
        density: Density, outPosition: IntArray ->
        with(horizontalArrangement) {
            density.arrange(totalSize, size, layoutDirection, outPosition)
        }
    }

@Composable
private fun rowMeasurementHelper(
    horizontalArrangement: Arrangement.Horizontal,
    verticalArrangement: Arrangement.Vertical,
    maxItemsInMainAxis: Int,
): MeasurePolicy {
    return remember(horizontalArrangement, verticalArrangement, maxItemsInMainAxis) {
        flowMeasurePolicy(
            orientation = LayoutOrientation.Horizontal,
            mainAxisArrangement = getHorizontalArrangement(horizontalArrangement),
            mainAxisArrangementSpacing = horizontalArrangement.spacing,
            crossAxisSize = SizeMode.Wrap,
            crossAxisAlignment = CROSS_AXIS_ALIGNMENT_TOP,
            crossAxisArrangement = getVerticalArrangement(verticalArrangement),
            crossAxisArrangementSpacing = verticalArrangement.spacing,
            maxItemsInMainAxis = maxItemsInMainAxis
        )
    }
}

@Composable
private fun columnMeasurementHelper(
    verticalArrangement: Arrangement.Vertical,
    horizontalArrangement: Arrangement.Horizontal,
    maxItemsInMainAxis: Int,
): MeasurePolicy {
    return remember(verticalArrangement, horizontalArrangement, maxItemsInMainAxis) {
        flowMeasurePolicy(
            orientation = LayoutOrientation.Vertical,
            mainAxisArrangement = getVerticalArrangement(verticalArrangement),
            mainAxisArrangementSpacing = verticalArrangement.spacing,
            crossAxisSize = SizeMode.Wrap,
            crossAxisAlignment = CROSS_AXIS_ALIGNMENT_START,
            crossAxisArrangement = getHorizontalArrangement(horizontalArrangement),
            crossAxisArrangementSpacing = horizontalArrangement.spacing,
            maxItemsInMainAxis = maxItemsInMainAxis,
        )
    }
}

/**
 * Returns a Flow Measure Policy
 */
private fun flowMeasurePolicy(
    orientation: LayoutOrientation,
    mainAxisArrangement: (Int, IntArray, LayoutDirection, Density, IntArray) -> Unit,
    mainAxisArrangementSpacing: Dp,
    crossAxisSize: SizeMode,
    crossAxisAlignment: CrossAxisAlignment,
    crossAxisArrangement: (Int, IntArray, LayoutDirection, Density, IntArray) -> Unit,
    crossAxisArrangementSpacing: Dp,
    maxItemsInMainAxis: Int,
): MeasurePolicy {

    return object : MeasurePolicy {

        override fun MeasureScope.measure(
            measurables: List<Measurable>,
            constraints: Constraints
        ): MeasureResult {
            if (measurables.isEmpty()) { return layout(0, 0) {} }
            val placeables: Array<Placeable?> = arrayOfNulls(measurables.size)
            val measureHelper = RowColumnMeasurementHelper(
                orientation,
                mainAxisArrangement,
                mainAxisArrangementSpacing,
                crossAxisSize,
                crossAxisAlignment,
                measurables,
                placeables,
            )
            val orientationIndependentConstraints =
                OrientationIndependentConstraints(constraints, orientation)
            val flowResult = breakDownItems(
                measureHelper,
                orientation,
                orientationIndependentConstraints,
                maxItemsInMainAxis,
            )
            val items = flowResult.items
            val crossAxisSizes = IntArray(items.size) { index ->
                items[index].crossAxisSize
            }
            // space in between children, except for the last child
            val outPosition = IntArray(crossAxisSizes.size)
            var totalCrossAxisSize = flowResult.crossAxisTotalSize
            val totalCrossAxisSpacing =
                crossAxisArrangementSpacing.roundToPx() * (items.size - 1)
            totalCrossAxisSize += totalCrossAxisSpacing
            crossAxisArrangement(
                totalCrossAxisSize,
                crossAxisSizes,
                layoutDirection,
                this@measure,
                outPosition
            )

            var layoutWidth: Int
            var layoutHeight: Int
            if (orientation == LayoutOrientation.Horizontal) {
                layoutWidth = flowResult.mainAxisTotalSize
                layoutHeight = totalCrossAxisSize
            } else {
                layoutWidth = totalCrossAxisSize
                layoutHeight = flowResult.mainAxisTotalSize
            }
            layoutWidth = constraints.constrainWidth(layoutWidth)
            layoutHeight = constraints.constrainHeight(layoutHeight)

            return layout(layoutWidth, layoutHeight) {
                flowResult.items.forEachIndexed { currentRowOrColumnIndex,
                    measureResult ->
                    measureHelper.placeHelper(
                        this,
                        measureResult,
                        outPosition[currentRowOrColumnIndex],
                        this@measure.layoutDirection
                    )
                }
            }
        }

        override fun IntrinsicMeasureScope.minIntrinsicWidth(
            measurables: List<IntrinsicMeasurable>,
            height: Int
        ) = if (orientation == LayoutOrientation.Horizontal) {
            minIntrinsicMainAxisSize(
                measurables,
                height,
                mainAxisArrangementSpacing.roundToPx(),
                crossAxisArrangementSpacing.roundToPx()
            )
        } else {
            intrinsicCrossAxisSize(
                measurables,
                height,
                mainAxisArrangementSpacing.roundToPx(),
                crossAxisArrangementSpacing.roundToPx()
            )
        }

        override fun IntrinsicMeasureScope.minIntrinsicHeight(
            measurables: List<IntrinsicMeasurable>,
            width: Int
        ) = if (orientation == LayoutOrientation.Horizontal) {
            intrinsicCrossAxisSize(
                measurables,
                width,
                mainAxisArrangementSpacing.roundToPx(),
                crossAxisArrangementSpacing.roundToPx()
            )
        } else {
            minIntrinsicMainAxisSize(
                measurables,
                width,
                mainAxisArrangementSpacing.roundToPx(),
                crossAxisArrangementSpacing.roundToPx(),
            )
        }

        override fun IntrinsicMeasureScope.maxIntrinsicHeight(
            measurables: List<IntrinsicMeasurable>,
            width: Int
        ) = if (orientation == LayoutOrientation.Horizontal) {
            intrinsicCrossAxisSize(
                measurables,
                width,
                mainAxisArrangementSpacing.roundToPx(),
                crossAxisArrangementSpacing.roundToPx()
            )
        } else {
            maxIntrinsicMainAxisSize(
                measurables,
                width,
                mainAxisArrangementSpacing.roundToPx(),
            )
        }

        override fun IntrinsicMeasureScope.maxIntrinsicWidth(
            measurables: List<IntrinsicMeasurable>,
            height: Int
        ) = if (orientation == LayoutOrientation.Horizontal) {
            maxIntrinsicMainAxisSize(
                measurables,
                height,
                mainAxisArrangementSpacing.roundToPx(),
            )
        } else {
            intrinsicCrossAxisSize(
                measurables,
                height,
                mainAxisArrangementSpacing.roundToPx(),
                crossAxisArrangementSpacing.roundToPx()
            )
        }

        fun minIntrinsicMainAxisSize(
            measurables: List<IntrinsicMeasurable>,
            crossAxisAvailable: Int,
            mainAxisSpacing: Int,
            crossAxisSpacing: Int,
        ) = minIntrinsicMainAxisSize(
            measurables,
            mainAxisSize = minMainAxisIntrinsicItemSize,
            crossAxisSize = minCrossAxisIntrinsicItemSize,
            crossAxisAvailable,
            mainAxisSpacing,
            crossAxisSpacing,
            maxItemsInMainAxis
        )

        fun maxIntrinsicMainAxisSize(
            measurables: List<IntrinsicMeasurable>,
            height: Int,
            arrangementSpacing: Int
        ) = maxIntrinsicMainAxisSize(
            measurables,
            maxMainAxisIntrinsicItemSize,
            height,
            arrangementSpacing,
            maxItemsInMainAxis
        )

        fun intrinsicCrossAxisSize(
            measurables: List<IntrinsicMeasurable>,
            mainAxisAvailable: Int,
            mainAxisSpacing: Int,
            crossAxisSpacing: Int
        ) = intrinsicCrossAxisSize(
            measurables,
            mainAxisSize = minMainAxisIntrinsicItemSize,
            crossAxisSize = minCrossAxisIntrinsicItemSize,
            mainAxisAvailable,
            mainAxisSpacing,
            crossAxisSpacing,
            maxItemsInMainAxis
        )

        val maxMainAxisIntrinsicItemSize: IntrinsicMeasurable.(Int, Int) -> Int =
            if (orientation == LayoutOrientation.Horizontal) { _, h ->
                maxIntrinsicWidth(h)
            }
            else { _, w ->
                maxIntrinsicHeight(w)
            }

        val maxCrossAxisIntrinsicItemSize: IntrinsicMeasurable.(Int, Int) -> Int =
            if (orientation == LayoutOrientation.Horizontal) { _, w ->
                maxIntrinsicHeight(w)
            }
            else { _, h ->
                maxIntrinsicWidth(h)
            }

        val minCrossAxisIntrinsicItemSize: IntrinsicMeasurable.(Int, Int) -> Int =
            if (orientation == LayoutOrientation.Horizontal) { _, w ->
                minIntrinsicHeight(w)
            }
            else { _, h ->
                minIntrinsicWidth(h)
            }

        val minMainAxisIntrinsicItemSize: IntrinsicMeasurable.(Int, Int) -> Int =
            if (orientation == LayoutOrientation.Horizontal) { _, h ->
                minIntrinsicWidth(h)
            }
            else { _, w ->
                minIntrinsicHeight(w)
            }
    }
}

private fun maxIntrinsicMainAxisSize(
    children: List<IntrinsicMeasurable>,
    mainAxisSize: IntrinsicMeasurable.(Int, Int) -> Int,
    crossAxisAvailable: Int,
    mainAxisSpacing: Int,
    maxItemsInMainAxis: Int
): Int {
    var fixedSpace = 0
    var currentFixedSpace = 0
    var lastBreak = 0
    children.fastForEachIndexed { index, child ->
        val size = child.mainAxisSize(index, crossAxisAvailable) + mainAxisSpacing
        if (index + 1 - lastBreak == maxItemsInMainAxis || index + 1 == children.size) {
            lastBreak = index
            currentFixedSpace += size
            currentFixedSpace -= mainAxisSpacing // no mainAxisSpacing for last item in main axis
            fixedSpace = max(fixedSpace, currentFixedSpace)
            currentFixedSpace = 0
        } else {
            currentFixedSpace += size
        }
    }
    return fixedSpace
}

/**
 * Slower algorithm but needed to determine the minimum main axis size
 * Uses a binary search to search different scenarios to see the minimum main axis size
 */
private fun minIntrinsicMainAxisSize(
    children: List<IntrinsicMeasurable>,
    mainAxisSize: IntrinsicMeasurable.(Int, Int) -> Int,
    crossAxisSize: IntrinsicMeasurable.(Int, Int) -> Int,
    crossAxisAvailable: Int,
    mainAxisSpacing: Int,
    crossAxisSpacing: Int,
    maxItemsInMainAxis: Int
): Int {
    val mainAxisSizes = IntArray(children.size) { 0 }
    val crossAxisSizes = IntArray(children.size) { 0 }

    for (index in children.indices) {
        val child = children[index]
        val mainAxisItemSize = child.mainAxisSize(index, crossAxisAvailable)
        mainAxisSizes[index] = mainAxisItemSize
        crossAxisSizes[index] = child.crossAxisSize(index, mainAxisItemSize)
    }

    val maxMainAxisSize = mainAxisSizes.sum()
    var mainAxisUsed = maxMainAxisSize
    var crossAxisUsed = crossAxisSizes.maxOf { it }

    val minimumItemSize = mainAxisSizes.maxOf { it }
    var low = minimumItemSize
    var high = maxMainAxisSize
    while (low < high) {
        if (crossAxisUsed == crossAxisAvailable) {
            return mainAxisUsed
        }
        val mid = (low + high) / 2
        mainAxisUsed = mid
        crossAxisUsed = intrinsicCrossAxisSize(
            children,
            mainAxisSizes,
            crossAxisSizes,
            mainAxisUsed,
            mainAxisSpacing,
            crossAxisSpacing,
            maxItemsInMainAxis
        )

        if (crossAxisUsed == crossAxisAvailable) {
            return mainAxisUsed
        } else if (crossAxisUsed > crossAxisAvailable) {
            low = mid + 1
        } else {
            high = mid - 1
        }
    }

    return mainAxisUsed
}

/**
 * FlowRow: Intrinsic height (cross Axis) is based on a specified width
 * FlowColumn: Intrinsic width (crossAxis) based on a specified height
 */
private fun intrinsicCrossAxisSize(
    children: List<IntrinsicMeasurable>,
    mainAxisSizes: IntArray,
    crossAxisSizes: IntArray,
    mainAxisAvailable: Int,
    mainAxisSpacing: Int,
    crossAxisSpacing: Int,
    maxItemsInMainAxis: Int
): Int {
    return intrinsicCrossAxisSize(
        children,
        { index, _ -> mainAxisSizes[index] },
        { index, _ -> crossAxisSizes[index] },
        mainAxisAvailable,
        mainAxisSpacing,
        crossAxisSpacing,
        maxItemsInMainAxis
    )
}

/** FlowRow: Intrinsic height (cross Axis) is based on a specified width
 ** FlowColumn: Intrinsic width (crossAxis) based on a specified height
 */
private fun intrinsicCrossAxisSize(
    children: List<IntrinsicMeasurable>,
    mainAxisSize: IntrinsicMeasurable.(Int, Int) -> Int,
    crossAxisSize: IntrinsicMeasurable.(Int, Int) -> Int,
    mainAxisAvailable: Int,
    mainAxisSpacing: Int,
    crossAxisSpacing: Int,
    maxItemsInMainAxis: Int
): Int {
    if (children.isEmpty()) {
        return 0
    }
    var nextChild = children.getOrNull(0)
    var nextCrossAxisSize = nextChild?.crossAxisSize(0, mainAxisAvailable) ?: 0
    var nextMainAxisSize = nextChild?.mainAxisSize(0, nextCrossAxisSize) ?: 0

    var remaining = mainAxisAvailable
    var currentCrossAxisSize = 0
    var totalCrossAxisSize = 0
    var lastBreak = 0

    children.fastForEachIndexed { index, _ ->
        nextChild!!
        val childCrossAxisSize = nextCrossAxisSize
        val childMainAxisSize = nextMainAxisSize
        remaining -= childMainAxisSize
        currentCrossAxisSize = maxOf(currentCrossAxisSize, childCrossAxisSize)

        // look ahead to simplify logic
        nextChild = children.getOrNull(index + 1)
        nextCrossAxisSize = nextChild?.crossAxisSize(index + 1, mainAxisAvailable) ?: 0
        nextMainAxisSize = nextChild?.mainAxisSize(index + 1, nextCrossAxisSize)
            ?.plus(mainAxisSpacing) ?: 0

        if (remaining < 0 || index + 1 == children.size ||
            (index + 1) - lastBreak == maxItemsInMainAxis ||
            remaining - nextMainAxisSize < 0
        ) {
            totalCrossAxisSize += currentCrossAxisSize + crossAxisSpacing
            currentCrossAxisSize = 0
            remaining = mainAxisAvailable
            lastBreak = index + 1
            nextMainAxisSize -= mainAxisSpacing
        }
    }
    // remove the last spacing for the last row or column
    totalCrossAxisSize -= crossAxisSpacing
    return totalCrossAxisSize
}

/**
 * Breaks down items based on space, size and maximum items in main axis.
 * When items run out of space or the maximum items to fit in the main axis is reached,
 * it moves to the next "line" and moves the next batch of items to a new list of items
 */
internal fun MeasureScope.breakDownItems(
    measureHelper: RowColumnMeasurementHelper,
    orientation: LayoutOrientation,
    constraints: OrientationIndependentConstraints,
    maxItemsInMainAxis: Int,
): FlowResult {
    val items = mutableVectorOf<RowColumnMeasureHelperResult>()
    val mainAxisMax = constraints.mainAxisMax
    val mainAxisMin = constraints.mainAxisMin
    val crossAxisMax = constraints.crossAxisMax
    val measurables = measureHelper.measurables
    val placeables = measureHelper.placeables

    val spacing = ceil(measureHelper.arrangementSpacing.toPx()).toInt()
    val subsetConstraints = OrientationIndependentConstraints(
        mainAxisMin,
        mainAxisMax,
        0,
        crossAxisMax
    )
    // nextSize of the list, pre-calculated
    var nextSize: Int? = measurables.getOrNull(0)?.measureAndCache(
        subsetConstraints, orientation
    ) { placeable ->
        placeables[0] = placeable
    }

    var startBreakLineIndex = 0
    val endBreakLineList = arrayOfNulls<Int>(measurables.size)
    var endBreakLineIndex = 0

    var leftOver = mainAxisMax
    // figure out the mainAxisTotalSize which will be minMainAxis when measuring the row/column
    var mainAxisTotalSize = mainAxisMin
    var currentLineMainAxisSize = 0
    for (index in measurables.indices) {
        val itemMainAxisSize = nextSize!!
        currentLineMainAxisSize += itemMainAxisSize
        leftOver -= itemMainAxisSize
        nextSize = measurables.getOrNull(index + 1)?.measureAndCache(
            subsetConstraints, orientation
        ) { placeable ->
            placeables[index + 1] = placeable
        }?.plus(spacing)
        if (index + 1 >= measurables.size ||
            (index + 1) - startBreakLineIndex >= maxItemsInMainAxis ||
            leftOver - (nextSize ?: 0) < 0
        ) {
            mainAxisTotalSize = maxOf(mainAxisTotalSize, currentLineMainAxisSize)
            currentLineMainAxisSize = 0
            leftOver = mainAxisMax
            startBreakLineIndex = index + 1
            endBreakLineList[endBreakLineIndex] = index + 1
            endBreakLineIndex++
            // only add spacing for next items in the row or column, not the starting indexes
            nextSize = nextSize?.minus(spacing)
        }
    }

    val subsetBoxConstraints = subsetConstraints.copy(
        mainAxisMin = mainAxisTotalSize
    ).toBoxConstraints(orientation)

    startBreakLineIndex = 0
    var crossAxisTotalSize = 0

    endBreakLineIndex = 0
    var endIndex = endBreakLineList.getOrNull(endBreakLineIndex)
    while (endIndex != null) {
        val result = measureHelper.measureWithoutPlacing(
            this,
            subsetBoxConstraints,
            startBreakLineIndex,
            endIndex
        )
        crossAxisTotalSize += result.crossAxisSize
        mainAxisTotalSize = maxOf(mainAxisTotalSize, result.mainAxisSize)
        items.add(
            result
        )
        startBreakLineIndex = endIndex
        endBreakLineIndex++
        endIndex = endBreakLineList.getOrNull(endBreakLineIndex)
    }

    crossAxisTotalSize = maxOf(crossAxisTotalSize, constraints.crossAxisMin)
    mainAxisTotalSize = maxOf(mainAxisTotalSize, constraints.mainAxisMin)
    return FlowResult(
        mainAxisTotalSize,
        crossAxisTotalSize,
        items,
    )
}

internal fun Measurable.mainAxisMin(orientation: LayoutOrientation, crossAxisSize: Int) =
    if (orientation == LayoutOrientation.Horizontal) {
        minIntrinsicWidth(crossAxisSize)
    } else {
        minIntrinsicHeight(crossAxisSize)
    }

internal fun Measurable.crossAxisMin(orientation: LayoutOrientation, mainAxisSize: Int) =
    if (orientation == LayoutOrientation.Horizontal) {
        minIntrinsicHeight(mainAxisSize)
    } else {
        minIntrinsicWidth(mainAxisSize)
    }

internal fun Placeable.mainAxisSize(orientation: LayoutOrientation) =
    if (orientation == LayoutOrientation.Horizontal) width else height

internal fun Placeable.crossAxisSize(orientation: LayoutOrientation) =
    if (orientation == LayoutOrientation.Horizontal) height else width

private val CROSS_AXIS_ALIGNMENT_TOP = CrossAxisAlignment.vertical(Alignment.Top)
private val CROSS_AXIS_ALIGNMENT_START = CrossAxisAlignment.horizontal(Alignment.Start)

// We measure and cache to improve performance dramatically, instead of using intrinsics
// This only works so far for fixed size items.
// For weighted items, we continue to use their intrinsic widths.
// This is because their fixed sizes are only determined after we determine
// the number of items that can fit in the row/column it only lies on.
private fun Measurable.measureAndCache(
    constraints: OrientationIndependentConstraints,
    orientation: LayoutOrientation,
    storePlaceable: (Placeable?) -> Unit
): Int {
    val itemSize: Int = if (rowColumnParentData.weight == 0f) {
        // fixed sizes: measure once
        val placeable = measure(
            constraints.copy(
                mainAxisMin = 0,
            ).toBoxConstraints(orientation)
        ).also(storePlaceable)
        placeable.mainAxisSize(orientation)
    } else {
        mainAxisMin(orientation, Constraints.Infinity)
    }
    return itemSize
}

/**
 * FlowResult when broken down to multiple rows or columns based on [breakDownItems] algorithm
 *
 * @param mainAxisTotalSize the total size of the main axis
 * @param crossAxisTotalSize the total size of the cross axis when taken into account
 * the cross axis sizes of all items
 * @param items the row or column measurements for each row or column
 */
internal class FlowResult(
    val mainAxisTotalSize: Int,
    val crossAxisTotalSize: Int,
    val items: MutableVector<RowColumnMeasureHelperResult>,
)