<?php
/**
 * Server-side layout analyzer — ports layoutAnalyzer.ts for runtime export.
 *
 * @package OpusPriceBuilder\Export
 */

declare(strict_types=1);

namespace OpusPriceBuilder\Export;

/**
 * Analyzes canvas element positions for flexbox HTML/CSS generation.
 */
class LayoutAnalyzer {

	private const BASE_THRESHOLD              = 5;
	private const MIN_THRESHOLD               = 2;
	private const MAX_THRESHOLD               = 12;
	private const THRESHOLD_RATIO             = 0.1;
	private const HEIGHT_DIFFERENCE_THRESHOLD = 0.3;
	private const HORIZONTAL_POSITION_THRESHOLD = 0.2;

	/**
	 * Perform full layout analysis.
	 *
	 * @param array<int, array<string, mixed>> $elements Visible elements.
	 * @return array<string, mixed>
	 */
	public static function analyze( array $elements ): array {
		$visible = array_values(
			array_filter(
				$elements,
				static fn( array $el ): bool => (bool) ( $el['visible'] ?? true )
			)
		);

		$absolute = array();
		$relative = array();

		foreach ( $visible as $element ) {
			if ( ( $element['positionType'] ?? 'relative' ) === 'absolute' ) {
				$absolute[] = $element;
				continue;
			}
			$relative[] = $element;
		}

		$container_bounds = self::calculateContainerBounds( $visible, 0 );

		$absolute_analysis = array();
		foreach ( $absolute as $element ) {
			$absolute_analysis[] = array(
				'element'           => $element,
				'relative_position' => self::calculateRelativePosition( $element, $container_bounds ),
			);
		}

		$column_groups = self::detectColumnLayout( $relative );

		if ( null !== $column_groups && count( $column_groups ) >= 2 ) {
			$columns = array();
			foreach ( $column_groups as $group ) {
				$content  = self::analyzeColumnContent( $group['elements'], $container_bounds );
				$columns[] = array(
					'elements'       => $group['elements'],
					'vertical_align' => $content['vertical_align'],
					'rows'           => $content['rows'],
				);
			}

			$column_gap = 8;
			if ( count( $column_groups ) >= 2 ) {
				$col_gaps = array();
				for ( $i = 1; $i < count( $column_groups ); $i++ ) {
					$col_gaps[] = max( 0, $column_groups[ $i ]['min_x'] - $column_groups[ $i - 1 ]['max_x'] );
				}
				$column_gap = self::getRecommendedGap( $col_gaps );
			}

			$row_gap      = 4;
			$all_row_gaps = array();
			foreach ( $columns as $col ) {
				if ( count( $col['rows'] ) > 1 ) {
					for ( $i = 1; $i < count( $col['rows'] ); $i++ ) {
						$prev_row   = $col['rows'][ $i - 1 ];
						$curr_row   = $col['rows'][ $i ];
						$prev_bottom = max( array_map( static fn( $el ) => (float) ( $el['position']['y'] ?? 0 ) + self::elementHeight( $el ), $prev_row['elements'] ) );
						$curr_top    = min( array_map( static fn( $el ) => (float) ( $el['position']['y'] ?? 0 ), $curr_row['elements'] ) );
						$all_row_gaps[] = max( 0, $curr_top - $prev_bottom );
					}
				}
			}
			if ( ! empty( $all_row_gaps ) ) {
				$row_gap = self::getRecommendedGap( $all_row_gaps );
			}

			return array(
				'layout_type'           => 'columns',
				'container_bounds'      => $container_bounds,
				'rows'                  => array(),
				'columns'               => $columns,
				'column_gap'            => $column_gap,
				'container_align_items' => self::calculateColumnsVerticalAlignment( $column_groups, $container_bounds ),
				'row_gap'               => $row_gap,
				'absolute_elements'     => $absolute_analysis,
			);
		}

		$rows = array();
		if ( ! empty( $relative ) ) {
			usort(
				$relative,
				static function ( array $a, array $b ): int {
					$ay = (float) ( $a['position']['y'] ?? 0 );
					$by = (float) ( $b['position']['y'] ?? 0 );
					if ( abs( $ay - $by ) > 0.001 ) {
						return $ay <=> $by;
					}
					return ( (float) ( $a['position']['x'] ?? 0 ) ) <=> ( (float) ( $b['position']['x'] ?? 0 ) );
				}
			);

			$current_row = array( $relative[0] );

			for ( $i = 1; $i < count( $relative ); $i++ ) {
				$el       = $relative[ $i ];
				$last_el  = $current_row[ count( $current_row ) - 1 ];
				$el_top   = (float) ( $el['position']['y'] ?? 0 );
				$el_bottom = $el_top + self::elementHeight( $el );
				$last_top = (float) ( $last_el['position']['y'] ?? 0 );
				$last_bottom = $last_top + self::elementHeight( $last_el );
				$overlaps = ! ( $el_bottom < $last_top || $el_top > $last_bottom );

				if ( $overlaps ) {
					$current_row[] = $el;
				} else {
					usort( $current_row, static fn( $a, $b ) => ( (float) ( $a['position']['x'] ?? 0 ) ) <=> ( (float) ( $b['position']['x'] ?? 0 ) ) );
					$gaps = self::detectGaps( $current_row );
					$rows[] = array(
						'elements'        => $current_row,
						'alignment'       => self::analyzeRowAlignment( $current_row, $container_bounds ),
						'gaps'            => $gaps,
						'recommended_gap' => self::getRecommendedGap( $gaps ),
					);
					$current_row = array( $el );
				}
			}

			if ( ! empty( $current_row ) ) {
				usort( $current_row, static fn( $a, $b ) => ( (float) ( $a['position']['x'] ?? 0 ) ) <=> ( (float) ( $b['position']['x'] ?? 0 ) ) );
				$gaps = self::detectGaps( $current_row );
				$rows[] = array(
					'elements'        => $current_row,
					'alignment'       => self::analyzeRowAlignment( $current_row, $container_bounds ),
					'gaps'            => $gaps,
					'recommended_gap' => self::getRecommendedGap( $gaps ),
				);
			}
		}

		$row_gap = 4;
		if ( count( $rows ) > 1 ) {
			$row_gaps = array();
			for ( $i = 1; $i < count( $rows ); $i++ ) {
				$prev_bottom = max( array_map( static fn( $el ) => (float) ( $el['position']['y'] ?? 0 ) + self::elementHeight( $el ), $rows[ $i - 1 ]['elements'] ) );
				$curr_top    = min( array_map( static fn( $el ) => (float) ( $el['position']['y'] ?? 0 ), $rows[ $i ]['elements'] ) );
				$row_gaps[]  = max( 0, $curr_top - $prev_bottom );
			}
			$row_gap = self::getRecommendedGap( $row_gaps );
		}

		return array(
			'layout_type'           => 'rows',
			'container_bounds'      => $container_bounds,
			'rows'                  => $rows,
			'columns'               => array(),
			'column_gap'            => 8,
			'container_align_items' => 'flex-start',
			'row_gap'               => $row_gap,
			'absolute_elements'     => $absolute_analysis,
		);
	}

	/**
	 * Convert vertical alignment to CSS align-items.
	 *
	 * @param string $vertical Vertical alignment key.
	 */
	public static function alignmentToCss( string $vertical ): string {
		return match ( $vertical ) {
			'baseline'   => 'baseline',
			'center'     => 'center',
			'flex-start' => 'flex-start',
			'flex-end'   => 'flex-end',
			'custom'     => 'center',
			default      => 'baseline',
		};
	}

	/**
	 * @param array<int, array<string, mixed>> $elements Elements.
	 * @param float                             $padding  Padding.
	 * @return array<string, mixed>
	 */
	public static function calculateContainerBounds( array $elements, float $padding = 0 ): array {
		if ( empty( $elements ) ) {
			return array(
				'x'              => 0.0,
				'y'              => 0.0,
				'width'          => 100.0,
				'height'         => 40.0,
				'content_padding' => array(
					'top'    => 0,
					'right'  => 0,
					'bottom' => 0,
					'left'   => 0,
				),
			);
		}

		$min_x = PHP_FLOAT_MAX;
		$min_y = PHP_FLOAT_MAX;
		$max_x = 0.0;
		$max_y = 0.0;

		foreach ( $elements as $el ) {
			$x = (float) ( $el['position']['x'] ?? 0 );
			$y = (float) ( $el['position']['y'] ?? 0 );
			$w = (float) ( $el['size']['width'] ?? 100 );
			$h = self::elementHeight( $el );
			$min_x = min( $min_x, $x );
			$min_y = min( $min_y, $y );
			$max_x = max( $max_x, $x + $w );
			$max_y = max( $max_y, $y + $h );
		}

		return array(
			'x'              => $min_x - $padding,
			'y'              => $min_y - $padding,
			'width'          => $max_x - $min_x + ( $padding * 2 ),
			'height'         => $max_y - $min_y + ( $padding * 2 ),
			'content_padding' => array(
				'top'    => $padding,
				'right'  => $padding,
				'bottom' => $padding,
				'left'   => $padding,
			),
		);
	}

	/**
	 * @param array<string, mixed> $element Element.
	 * @param array<string, mixed> $bounds  Container bounds.
	 * @return array<string, mixed>
	 */
	private static function calculateRelativePosition( array $element, array $bounds ): array {
		$relative_x = (float) ( $element['position']['x'] ?? 0 ) - (float) $bounds['x'];
		$relative_y = (float) ( $element['position']['y'] ?? 0 ) - (float) $bounds['y'];

		return array(
			'element_id'   => (string) ( $element['id'] ?? '' ),
			'relative_x'   => $relative_x,
			'relative_y'   => $relative_y,
			'css_position' => ( $element['positionType'] ?? 'relative' ) === 'absolute' ? 'absolute' : 'static',
			'adjustments'  => array(),
		);
	}

	/**
	 * @param array<int, array<string, mixed>> $elements Row elements.
	 * @param array<string, mixed>|null        $container_bounds Container bounds.
	 * @return array<string, mixed>
	 */
	private static function analyzeRowAlignment( array $elements, ?array $container_bounds ): array {
		if ( count( $elements ) <= 1 ) {
			return array(
				'vertical'       => 'baseline',
				'horizontal'     => self::analyzeHorizontalAlignment( $elements, $container_bounds ),
				'offsets'        => array(),
				'reference_line' => (float) ( $elements[0]['position']['y'] ?? 0 ),
			);
		}

		$baselines = array();
		$centers   = array();
		$tops      = array();
		$heights   = array();

		foreach ( $elements as $el ) {
			$id        = (string) ( $el['id'] ?? '' );
			$baselines[] = array( 'id' => $id, 'value' => self::getBaseline( $el ) );
			$centers[]   = array( 'id' => $id, 'value' => self::getVerticalCenter( $el ) );
			$tops[]      = array( 'id' => $id, 'value' => self::getTop( $el ) );
			$heights[]   = self::elementHeight( $el );
		}

		$avg_baseline = array_sum( array_column( $baselines, 'value' ) ) / count( $baselines );
		$avg_center   = array_sum( array_column( $centers, 'value' ) ) / count( $centers );
		$avg_top      = array_sum( array_column( $tops, 'value' ) ) / count( $tops );

		$max_baseline_diff = max( array_map( static fn( $b ) => abs( $b['value'] - $avg_baseline ), $baselines ) );
		$max_center_diff   = max( array_map( static fn( $c ) => abs( $c['value'] - $avg_center ), $centers ) );
		$max_top_diff      = max( array_map( static fn( $t ) => abs( $t['value'] - $avg_top ), $tops ) );

		$min_height = min( $heights );
		$max_height = max( $heights );
		$height_diff_ratio = $min_height > 0 ? ( $max_height - $min_height ) / $max_height : 0;
		$has_significant_height_diff = $height_diff_ratio > self::HEIGHT_DIFFERENCE_THRESHOLD;

		$dynamic_threshold   = self::calculateDynamicThreshold( $heights );
		$alignment_threshold = $dynamic_threshold;

		$baseline_aligned = $max_baseline_diff < $alignment_threshold;
		$center_aligned   = $max_center_diff < $alignment_threshold;
		$top_aligned      = $max_top_diff < $alignment_threshold;

		$alignment_scores = array(
			array( 'type' => 'baseline', 'diff' => $max_baseline_diff, 'aligned' => $baseline_aligned, 'ref' => $avg_baseline ),
			array( 'type' => 'center', 'diff' => $max_center_diff, 'aligned' => $center_aligned, 'ref' => $avg_center ),
			array( 'type' => 'flex-start', 'diff' => $max_top_diff, 'aligned' => $top_aligned, 'ref' => $avg_top ),
		);

		usort( $alignment_scores, static fn( $a, $b ) => $a['diff'] <=> $b['diff'] );

		$vertical       = 'baseline';
		$reference_line = 0.0;
		$offsets        = array();

		if ( $has_significant_height_diff && $center_aligned ) {
			$vertical       = 'center';
			$reference_line = $avg_center;
		} else {
			$best_match = null;
			foreach ( $alignment_scores as $score ) {
				if ( $score['aligned'] ) {
					$best_match = $score;
					break;
				}
			}

			if ( null !== $best_match ) {
				$vertical       = $best_match['type'];
				$reference_line = $best_match['ref'];
			} else {
				$vertical       = 'custom';
				$reference_line = $avg_center;

				foreach ( $elements as $el ) {
					$el_center = self::getVerticalCenter( $el );
					$offset    = $el_center - $reference_line;
					$id        = (string) ( $el['id'] ?? '' );

					if ( abs( $offset ) > $alignment_threshold ) {
						if ( $offset > 0 ) {
							$offsets[ $id ] = array( 'top' => (int) round( $offset ), 'bottom' => 0 );
						} else {
							$offsets[ $id ] = array( 'top' => 0, 'bottom' => (int) round( abs( $offset ) ) );
						}
					}
				}
			}
		}

		return array(
			'vertical'       => $vertical,
			'horizontal'     => self::analyzeHorizontalAlignment( $elements, $container_bounds ),
			'offsets'        => $offsets,
			'reference_line' => $reference_line,
		);
	}

	/**
	 * @param array<int, array<string, mixed>> $elements Row elements.
	 * @param array<string, mixed>|null        $container_bounds Container bounds.
	 */
	private static function analyzeHorizontalAlignment( array $elements, ?array $container_bounds ): string {
		if ( empty( $elements ) ) {
			return 'flex-start';
		}

		$sorted = $elements;
		usort( $sorted, static fn( $a, $b ) => ( (float) ( $a['position']['x'] ?? 0 ) ) <=> ( (float) ( $b['position']['x'] ?? 0 ) ) );

		$row_left  = (float) ( $sorted[0]['position']['x'] ?? 0 );
		$last      = $sorted[ count( $sorted ) - 1 ];
		$row_right = (float) ( $last['position']['x'] ?? 0 ) + (float) ( $last['size']['width'] ?? 0 );

		if ( null === $container_bounds || (float) $container_bounds['width'] <= 0 ) {
			return 'flex-start';
		}

		$container_left  = (float) $container_bounds['x'];
		$container_right = $container_left + (float) $container_bounds['width'];
		$container_width = (float) $container_bounds['width'];

		$left_margin  = $row_left - $container_left;
		$right_margin = $container_right - $row_right;

		if ( count( $elements ) >= 2 ) {
			$total_content_width = array_sum( array_map( static fn( $el ) => (float) ( $el['size']['width'] ?? 0 ), $sorted ) );
			$row_width           = $row_right - $row_left;
			$total_gap_space     = $row_width - $total_content_width;

			if ( $total_gap_space > $total_content_width * 0.5 ) {
				return 'space-between';
			}
		}

		$threshold = $container_width * self::HORIZONTAL_POSITION_THRESHOLD;

		if ( $right_margin < $threshold && $left_margin > $threshold ) {
			return 'flex-end';
		}

		if ( abs( $left_margin - $right_margin ) < $threshold ) {
			return 'center';
		}

		if ( $left_margin < $threshold && $right_margin > $threshold ) {
			return 'flex-start';
		}

		return 'flex-start';
	}

	/**
	 * @param array<int, array<string, mixed>> $elements Row elements.
	 * @return array<int, float>
	 */
	private static function detectGaps( array $elements ): array {
		if ( count( $elements ) <= 1 ) {
			return array();
		}

		$sorted = $elements;
		usort( $sorted, static fn( $a, $b ) => ( (float) ( $a['position']['x'] ?? 0 ) ) <=> ( (float) ( $b['position']['x'] ?? 0 ) ) );

		$gaps = array();
		for ( $i = 1; $i < count( $sorted ); $i++ ) {
			$prev_right = (float) ( $sorted[ $i - 1 ]['position']['x'] ?? 0 ) + (float) ( $sorted[ $i - 1 ]['size']['width'] ?? 0 );
			$curr_left  = (float) ( $sorted[ $i ]['position']['x'] ?? 0 );
			$gaps[]     = max( 0.0, $curr_left - $prev_right );
		}

		return $gaps;
	}

	/**
	 * @param array<int, float|int> $gaps Gap values.
	 */
	private static function getRecommendedGap( array $gaps ): int {
		if ( empty( $gaps ) ) {
			return 4;
		}
		$avg = array_sum( $gaps ) / count( $gaps );

		return (int) ( round( $avg / 2 ) * 2 );
	}

	/**
	 * @param array<int, float|int> $heights Element heights.
	 */
	private static function calculateDynamicThreshold( array $heights ): int {
		if ( empty( $heights ) ) {
			return self::BASE_THRESHOLD;
		}

		$avg_height = array_sum( $heights ) / count( $heights );
		$min_height = min( $heights );
		$threshold  = min( $avg_height * self::THRESHOLD_RATIO, $min_height * 0.15 );

		return (int) max( self::MIN_THRESHOLD, min( self::MAX_THRESHOLD, round( $threshold ) ) );
	}

	/**
	 * @param array<int, array<string, mixed>> $elements Relative elements.
	 * @return array<int, array<string, mixed>>|null
	 */
	private static function detectColumnLayout( array $elements ): ?array {
		if ( count( $elements ) < 2 ) {
			return null;
		}

		$sorted_by_x = $elements;
		usort( $sorted_by_x, static fn( $a, $b ) => ( (float) ( $a['position']['x'] ?? 0 ) ) <=> ( (float) ( $b['position']['x'] ?? 0 ) ) );

		$horizontal_gaps = array();
		for ( $i = 1; $i < count( $sorted_by_x ); $i++ ) {
			$left_el   = $sorted_by_x[ $i - 1 ];
			$right_el  = $sorted_by_x[ $i ];
			$left_right = (float) ( $left_el['position']['x'] ?? 0 ) + (float) ( $left_el['size']['width'] ?? 0 );
			$right_left = (float) ( $right_el['position']['x'] ?? 0 );
			$gap        = $right_left - $left_right;

			if ( $gap > 0 ) {
				$horizontal_gaps[] = array(
					'index'    => $i,
					'gap'      => $gap,
					'left_el'  => $left_el,
					'right_el' => $right_el,
				);
			}
		}

		if ( empty( $horizontal_gaps ) ) {
			return null;
		}

		$max_gap = max( array_column( $horizontal_gaps, 'gap' ) );
		$min_width = min( array_map( static fn( $el ) => (float) ( $el['size']['width'] ?? 0 ), $elements ) );
		$min_gap_threshold = max( 10, $min_width * 0.2 );

		if ( $max_gap < $min_gap_threshold ) {
			return null;
		}

		$split_gap = null;
		foreach ( $horizontal_gaps as $gap_info ) {
			if ( $gap_info['gap'] === $max_gap ) {
				$split_gap = $gap_info;
				break;
			}
		}

		if ( null === $split_gap ) {
			return null;
		}

		$left_elements  = array_slice( $sorted_by_x, 0, $split_gap['index'] );
		$right_elements = array_slice( $sorted_by_x, $split_gap['index'] );

		$right_y = array_map( static fn( $el ) => (float) ( $el['position']['y'] ?? 0 ), $right_elements );
		$left_y  = array_map( static fn( $el ) => (float) ( $el['position']['y'] ?? 0 ), $left_elements );
		$right_heights = array_map( static fn( $el ) => self::elementHeight( $el ), $right_elements );
		$left_heights  = array_map( static fn( $el ) => self::elementHeight( $el ), $left_elements );

		$right_is_stacked = count( $right_elements ) > 1 && ( max( $right_y ) - min( $right_y ) ) > 10;
		$left_is_stacked  = count( $left_elements ) > 1 && ( max( $left_y ) - min( $left_y ) ) > 10;
		$left_is_tall     = count( $left_elements ) === 1 && count( $right_elements ) > 1 && $left_heights[0] > max( $right_heights ) * 0.8;
		$right_is_tall    = count( $right_elements ) === 1 && count( $left_elements ) > 1 && $right_heights[0] > max( $left_heights ) * 0.8;

		$is_column_layout = $right_is_stacked || $left_is_stacked || $left_is_tall || $right_is_tall
			|| ( count( $left_elements ) === 1 && count( $right_elements ) > 1 )
			|| ( count( $right_elements ) === 1 && count( $left_elements ) > 1 );

		if ( ! $is_column_layout ) {
			return null;
		}

		$columns = array();
		if ( ! empty( $left_elements ) ) {
			$columns[] = self::buildColumnGroup( $left_elements );
		}
		if ( ! empty( $right_elements ) ) {
			$columns[] = self::buildColumnGroup( $right_elements );
		}

		return count( $columns ) >= 2 ? $columns : null;
	}

	/**
	 * @param array<int, array<string, mixed>> $elements Column elements.
	 * @return array<string, mixed>
	 */
	private static function buildColumnGroup( array $elements ): array {
		return array(
			'elements' => $elements,
			'min_x'    => min( array_map( static fn( $el ) => (float) ( $el['position']['x'] ?? 0 ), $elements ) ),
			'max_x'    => max( array_map( static fn( $el ) => (float) ( $el['position']['x'] ?? 0 ) + (float) ( $el['size']['width'] ?? 0 ), $elements ) ),
			'min_y'    => min( array_map( static fn( $el ) => (float) ( $el['position']['y'] ?? 0 ), $elements ) ),
			'max_y'    => max( array_map( static fn( $el ) => (float) ( $el['position']['y'] ?? 0 ) + self::elementHeight( $el ), $elements ) ),
		);
	}

	/**
	 * @param array<int, array<string, mixed>> $column_groups Column groups.
	 * @param array<string, mixed>             $container_bounds Container bounds.
	 */
	private static function calculateColumnsVerticalAlignment( array $column_groups, array $container_bounds ): string {
		if ( empty( $column_groups ) ) {
			return 'flex-start';
		}

		$container_top    = (float) $container_bounds['y'];
		$container_bottom = $container_top + (float) $container_bounds['height'];
		$container_height = (float) $container_bounds['height'];

		$total_top_margin    = 0.0;
		$total_bottom_margin = 0.0;

		foreach ( $column_groups as $col ) {
			$total_top_margin    += $col['min_y'] - $container_top;
			$total_bottom_margin += $container_bottom - $col['max_y'];
		}

		$avg_top_margin    = $total_top_margin / count( $column_groups );
		$avg_bottom_margin = $total_bottom_margin / count( $column_groups );
		$threshold         = $container_height * 0.1;

		if ( $avg_bottom_margin < $threshold && $avg_top_margin > $threshold ) {
			return 'flex-end';
		}

		if ( $avg_top_margin < $threshold && $avg_bottom_margin > $threshold ) {
			return 'flex-start';
		}

		if ( abs( $avg_top_margin - $avg_bottom_margin ) < $threshold ) {
			return 'center';
		}

		return 'flex-start';
	}

	/**
	 * @param array<int, array<string, mixed>> $elements Column elements.
	 * @param array<string, mixed>             $container_bounds Container bounds.
	 * @return array{rows: array<int, array<string, mixed>>, vertical_align: string}
	 */
	private static function analyzeColumnContent( array $elements, array $container_bounds ): array {
		if ( empty( $elements ) ) {
			return array( 'rows' => array(), 'vertical_align' => 'flex-start' );
		}

		if ( count( $elements ) === 1 ) {
			return array(
				'rows' => array(
					array(
						'elements'        => $elements,
						'alignment'       => self::analyzeRowAlignment( $elements, $container_bounds ),
						'gaps'            => array(),
						'recommended_gap' => 0,
					),
				),
				'vertical_align' => 'flex-start',
			);
		}

		$sorted_by_y = $elements;
		usort( $sorted_by_y, static fn( $a, $b ) => ( (float) ( $a['position']['y'] ?? 0 ) ) <=> ( (float) ( $b['position']['y'] ?? 0 ) ) );

		$rows         = array();
		$current_row  = array( $sorted_by_y[0] );

		for ( $i = 1; $i < count( $sorted_by_y ); $i++ ) {
			$el          = $sorted_by_y[ $i ];
			$last_el     = $current_row[ count( $current_row ) - 1 ];
			$el_top      = (float) ( $el['position']['y'] ?? 0 );
			$el_bottom   = $el_top + self::elementHeight( $el );
			$last_top    = (float) ( $last_el['position']['y'] ?? 0 );
			$last_bottom = $last_top + self::elementHeight( $last_el );
			$overlaps    = ! ( $el_bottom < $last_top || $el_top > $last_bottom );

			if ( $overlaps ) {
				$current_row[] = $el;
			} else {
				usort( $current_row, static fn( $a, $b ) => ( (float) ( $a['position']['x'] ?? 0 ) ) <=> ( (float) ( $b['position']['x'] ?? 0 ) ) );
				$gaps   = self::detectGaps( $current_row );
				$rows[] = array(
					'elements'        => $current_row,
					'alignment'       => self::analyzeRowAlignment( $current_row, $container_bounds ),
					'gaps'            => $gaps,
					'recommended_gap' => self::getRecommendedGap( $gaps ),
				);
				$current_row = array( $el );
			}
		}

		if ( ! empty( $current_row ) ) {
			usort( $current_row, static fn( $a, $b ) => ( (float) ( $a['position']['x'] ?? 0 ) ) <=> ( (float) ( $b['position']['x'] ?? 0 ) ) );
			$gaps   = self::detectGaps( $current_row );
			$rows[] = array(
				'elements'        => $current_row,
				'alignment'       => self::analyzeRowAlignment( $current_row, $container_bounds ),
				'gaps'            => $gaps,
				'recommended_gap' => self::getRecommendedGap( $gaps ),
			);
		}

		return array( 'rows' => $rows, 'vertical_align' => 'flex-start' );
	}

	/**
	 * @param array<string, mixed> $element Element data.
	 */
	private static function elementHeight( array $element ): float {
		return (float) ( $element['size']['height'] ?? 30 );
	}

	/**
	 * @param array<string, mixed> $element Element data.
	 */
	private static function getBaseline( array $element ): float {
		return (float) ( $element['position']['y'] ?? 0 ) + self::elementHeight( $element );
	}

	/**
	 * @param array<string, mixed> $element Element data.
	 */
	private static function getVerticalCenter( array $element ): float {
		return (float) ( $element['position']['y'] ?? 0 ) + ( self::elementHeight( $element ) / 2 );
	}

	/**
	 * @param array<string, mixed> $element Element data.
	 */
	private static function getTop( array $element ): float {
		return (float) ( $element['position']['y'] ?? 0 );
	}
}
