"""Layout for grid containers and grid-items."""

from collections import defaultdict
from itertools import count, cycle
from math import inf

from ..css.properties import Dimension
from ..formatting_structure import boxes
from ..logger import LOGGER
from .percent import percentage, resolve_percentages
from .preferred import max_content_width, min_content_width
from .table import find_in_flow_baseline


def _is_length(sizing):
    return isinstance(sizing, Dimension) and sizing.unit.lower() != 'fr'


def _is_fr(sizing):
    return isinstance(sizing, Dimension) and sizing.unit.lower() == 'fr'


def _intersect(position_1, size_1, position_2, size_2):
    return (
        position_1 < position_2 + size_2 and
        position_2 < position_1 + size_1)


def _intersect_with_children(x, y, width, height, positions):
    for full_x, full_y, full_width, full_height in positions:
        x_intersect = _intersect(x, width, full_x, full_width)
        y_intersect = _intersect(y, height, full_y, full_height)
        if x_intersect and y_intersect:
            return True
    return False


def _get_line(line, lines, side):
    span, number, ident = line
    if ident and span is None and number is None:
        for coord, line in enumerate(lines):
            if f'{ident}-{side}' in line:
                break
        else:
            number = 1
    if number is not None and span is None:
        if ident is None:
            coord = number - 1
        else:
            step = 1 if number > 0 else -1
            for coord, line in enumerate(lines[::step]):
                if ident in line:
                    number -= step
                    break
                if number == 0:
                    break
            else:
                coord += abs(number)
            if step == -1:
                coord = len(lines) - 1 - coord
    if span is not None:
        coord = None
    return span, number, ident, coord


def _get_placement(start, end, lines):
    # Input coordinates are 1-indexed, returned coordinates are 0-indexed.
    if start == 'auto' or start[0] == 'span':
        if end == 'auto' or end[0] == 'span':
            return
    if start != 'auto':
        span, number, ident, coord = _get_line(start, lines, 'start')
        if span is not None:
            size = number or 1
            span_ident = ident
    else:
        size = 1
        span_ident = coord = None
    if end != 'auto':
        span, number, ident, coord_end = _get_line(end, lines, 'end')
        if span is not None:
            size = span_number = number or 1
            span_ident = ident
            if span_ident is not None:
                for size, line in enumerate(lines[coord+1:], start=1):
                    if span_ident in line:
                        span_number -= 1
                    if span_number == 0:
                        break
                else:
                    size += span_number
        elif coord is not None:
            size = coord_end - coord
        if coord is None:
            if span_ident is None:
                coord = coord_end - size
            else:
                number = number or 1
                if coord_end > 0:
                    iterable = enumerate(lines[coord_end-1::-1])
                    for coord, line in iterable:
                        if span_ident in line:
                            number -= 1
                        if number == 0:
                            coord = coord_end - 1 - coord
                            break
                    else:
                        coord = -number
                else:
                    coord = -number
            size = coord_end - coord
    else:
        size = 1
    if size < 0:
        size = -size
        coord -= size
    if size == 0:
        size = 1
    return (coord, size)


def _get_span(place):
    # TODO: Handle lines.
    span = 1
    if place[0] == 'span':
        span = place[1] or 1
    return span


def _get_second_placement(first_placement, second_start, second_end,
                          second_tracks, children_positions, first_flow, dense):
    occupied_tracks = set()
    for x, y, width, height in children_positions.values():
        # Test whether cells overlap.
        if first_flow == 'row':
            if _intersect(y, height, *first_placement):
                for x in range(x, x + width):
                    occupied_tracks.add(x)
        else:
            if _intersect(x, width, *first_placement):
                for y in range(y, y + height):
                    occupied_tracks.add(y)
    if dense:
        for track in count():
            if track in occupied_tracks:
                continue
            if second_start == 'auto':
                placement = _get_placement(
                    (None, track + 1, None), second_end, second_tracks)
            else:
                assert second_start[0] == 'span'
                # If the placement contains two spans, remove the one
                # contributed by the end grid-placement property.
                # https://drafts.csswg.org/css-grid/#grid-placement-errors
                assert second_start == 'auto' or second_start[0] == 'span'
                span = _get_span(second_start)
                placement = _get_placement(
                    second_start, (None, track + 1 + span, None), second_tracks)
            tracks = range(placement[0], placement[0] + placement[1])
            if not set(tracks) & occupied_tracks:
                return placement
    else:
        track = max(occupied_tracks or [0]) + 1
        if second_start == 'auto':
            return _get_placement(
                (None, track + 1, None), second_end, second_tracks)
        else:
            assert second_start[0] == 'span'
            # If the placement contains two spans, remove the one contributed
            # by the end grid-placement property.
            # https://drafts.csswg.org/css-grid/#grid-placement-errors
            assert second_start == 'auto' or second_start[0] == 'span'
            for end_track in count(track + 1):
                placement = _get_placement(
                    second_start, (None, end_track + 1, None), second_tracks)
                if placement[0] >= track:
                    return placement


def _get_sizing_functions(size):
    min_sizing = max_sizing = size
    if size[0] == 'minmax()':
        min_sizing, max_sizing = size[1:]
    if min_sizing[0] == 'fit-content()':
        min_sizing = 'auto'
    elif _is_fr(min_sizing):
        min_sizing = 'auto'
    return (min_sizing, max_sizing)


def _get_template_tracks(tracks):
    if tracks == 'none':
        tracks = ((),)
    if 'subgrid' in tracks:
        # TODO: Support subgrids.
        LOGGER.warning('Subgrids are unsupported')
        return [[]]
    tracks_list = []
    for i, track in enumerate(tracks):
        if i % 2:
            # Track size.
            if track[0] == 'repeat()':
                repeat_number, repeat_track_list = track[1:]
                if not isinstance(repeat_number, int):
                    # TODO: Respect auto-fit and auto-fill.
                    LOGGER.warning(
                        '"auto-fit" and "auto-fill" are unsupported in repeat()')
                    repeat_number = 1
                for _ in range(repeat_number):
                    for j, repeat_track in enumerate(repeat_track_list):
                        if j % 2:
                            # Track size in repeat.
                            tracks_list.append(repeat_track)
                        else:
                            # Line names in repeat.
                            if len(tracks_list) % 2:
                                tracks_list[-1].extend(repeat_track)
                            else:
                                tracks_list.append(list(repeat_track))
            else:
                tracks_list.append(track)
        else:
            # Line names.
            if len(tracks_list) % 2:
                tracks_list[-1].extend(track)
            else:
                tracks_list.append(list(track))
    return tracks_list


def _distribute_extra_space(affected_sizes, affected_tracks_types, size_contribution,
                            tracks_children, sizing_functions, tracks_sizes, span,
                            direction, context):
    assert affected_sizes in ('min', 'max')
    assert affected_tracks_types in (
        'intrinsic', 'content-based', 'max-content')
    assert size_contribution in ('minimum', 'min-content', 'max-content')
    assert direction in 'xy'

    # 1. Maintain separately for each affected track a planned increase.
    planned_increases = [0] * len(tracks_sizes)

    # 2. Distribute space.
    affected_tracks = []
    affected_size_index = 0 if affected_sizes == 'min' else 1
    current_span = 0
    for children, functions in zip(tracks_children, sizing_functions):
        if children:
            current_span = span
        if not current_span:
            affected_tracks.append(False)
            continue
        current_span -= 1
        function = functions[affected_size_index]
        if affected_tracks_types == 'intrinsic':
            if (function in ('min-content', 'max-content', 'auto') or
                    function[0] == 'fit-content()'):
                affected_tracks.append(True)
                continue
        elif affected_tracks_types == 'content-based':
            if function in ('min-content', 'max-content'):
                affected_tracks.append(True)
                continue
        elif affected_tracks_types == 'max-content':
            if function in ('max-content', 'auto'):
                affected_tracks.append(True)
                continue
        affected_tracks.append(False)
    for i, children in enumerate(tracks_children):
        if not children:
            continue
        for item, parent in children:
            # 2.1 Find the space distribution.
            # TODO: Differenciate minimum and min-content values.
            # TODO: Find a better way to get height.
            if direction == 'x':
                if size_contribution in ('minimum', 'min-content'):
                    space = min_content_width(context, item)
                else:
                    space = max_content_width(context, item)
            else:
                from .block import block_level_layout
                item = item.deepcopy()
                item.position_x = 0
                item.position_y = 0
                item, _, _, _, _, _ = block_level_layout(
                    context, item, bottom_space=-inf, skip_stack=None,
                    containing_block=parent)
                space = item.margin_height()
            for sizes in tracks_sizes[i:i+span]:
                space -= sizes[affected_size_index]
            space = max(0, space)
            # 2.2 Distribute space up to limits.
            tracks_numbers = list(
                enumerate(affected_tracks[i:i+span], start=i))
            item_incurred_increases = [0] * len(sizing_functions)
            affected_tracks_numbers = [
                j for j, affected in tracks_numbers if affected]
            distributed_space = space / (len(affected_tracks_numbers) or 1)
            for track_number in affected_tracks_numbers:
                base_size, growth_limit = tracks_sizes[track_number]
                item_incurred_increase = distributed_space
                affected_size = tracks_sizes[track_number][affected_size_index]
                limit = tracks_sizes[track_number][1]
                if affected_size + item_incurred_increase >= limit:
                    extra = (
                        item_incurred_increase + affected_size - limit)
                    item_incurred_increase -= extra
                space -= item_incurred_increase
                item_incurred_increases[track_number] = item_incurred_increase
            # 2.3 Distribute space to non-affected tracks.
            if space and affected_tracks_numbers:
                unaffected_tracks_numbers = [
                    j for j, affected in tracks_numbers if not affected]
                distributed_space = (
                    space / (len(unaffected_tracks_numbers) or 1))
                for track_number in unaffected_tracks_numbers:
                    base_size, growth_limit = tracks_sizes[track_number]
                    item_incurred_increase = distributed_space
                    affected_size = (
                        tracks_sizes[track_number][affected_size_index])
                    limit = tracks_sizes[track_number][1]
                    if affected_size + item_incurred_increase >= limit:
                        extra = (
                            item_incurred_increase + affected_size - limit)
                        item_incurred_increase -= extra
                    space -= item_incurred_increase
                    item_incurred_increases[track_number] = (
                        item_incurred_increase)
            # 2.4 Distribute space beyond limits.
            if space:
                # TODO: Distribute space beyond limits.
                pass
            # 2.5. Set the track’s planned increase.
            for k, extra in enumerate(item_incurred_increases):
                if extra > planned_increases[k]:
                    planned_increases[k] = extra

    # 3. Update the tracks’ affected size.
    iterator = zip(affected_tracks, tracks_sizes, planned_increases)
    for affected, track_sizes, increase in iterator:
        if not affected:
            continue
        if affected_sizes == 'max' and track_sizes[1] is inf:
            track_sizes[1] = track_sizes[0] + increase
        else:
            track_sizes[affected_size_index] += increase


def _resolve_tracks_sizes(sizing_functions, box_size, children_positions,
                          implicit_start, direction, gap, context, containing_block,
                          orthogonal_sizes=None):
    assert direction in 'xy'
    tracks_sizes = []
    # TODO: Check that auto box size is 0 for percentages.
    percent_box_size = 0 if box_size == 'auto' else box_size
    # 1.1 Initialize track sizes.
    for min_function, max_function in sizing_functions:
        base_size = None
        if _is_length(min_function):
            base_size = percentage(
                min_function, containing_block.style, percent_box_size)
        elif (min_function in ('min-content', 'max-content', 'auto') or
              min_function[0] == 'fit-content()'):
            base_size = 0
        growth_limit = None
        if _is_length(max_function):
            growth_limit = percentage(
                max_function, containing_block.style, percent_box_size)
        elif (max_function in ('min-content', 'max-content', 'auto') or
              max_function[0] == 'fit-content()' or _is_fr(max_function)):
            growth_limit = inf
        if None not in (base_size, growth_limit):
            growth_limit = max(base_size, growth_limit)
        tracks_sizes.append([base_size, growth_limit])

    # 1.2 Resolve intrinsic track sizes.
    # 1.2.1 Shim baseline-aligned items.
    # TODO: Shim items.
    # 1.2.2 Size tracks to fit non-spanning items.
    tracks_children = [[] for _ in range(len(tracks_sizes))]
    for child, (x, y, width, height) in children_positions.items():
        coord, size = (x, width) if direction == 'x' else (y, height)
        if size != 1:
            continue
        tracks_children[coord - implicit_start].append(child)
    iterable = zip(tracks_children, sizing_functions, tracks_sizes)
    for children, (min_function, max_function), sizes in iterable:
        if not children:
            continue
        if direction == 'y':
            # TODO: Find a better way to get height.
            from .block import block_level_layout
            height = 0
            for child in children:
                x, y, width, _ = children_positions[child]
                width = sum(orthogonal_sizes[x:x+width])
                child = child.deepcopy()
                child.position_x = 0
                child.position_y = 0
                parent = boxes.BlockContainerBox.anonymous_from(containing_block, ())
                resolve_percentages(parent, containing_block)
                parent.position_x = child.position_x
                parent.position_y = child.position_y
                parent.width = width
                parent.height = height
                bottom_space = -inf
                child, _, _, _, _, _ = block_level_layout(
                    context, child, bottom_space, skip_stack=None,
                    containing_block=parent)
                height = max(height, child.margin_height())
            if min_function in ('min-content', 'max_content', 'auto'):
                sizes[0] = height
            if max_function in ('min-content', 'max_content'):
                sizes[1] = height
            if None not in sizes:
                sizes[1] = max(sizes)
            continue
        if min_function == 'min-content':
            sizes[0] = max(0, *(
                min_content_width(context, child) for child in children))
        elif min_function == 'max-content':
            sizes[0] = max(0, *(
                max_content_width(context, child) for child in children))
        elif min_function == 'auto':
            # TODO: Handle min-/max-content constrained parents.
            # TODO: Use real "minimum contributions".
            sizes[0] = max(0, *(
                min_content_width(context, child) for child in children))
        if max_function == 'min-content':
            sizes[1] = max(
                min_content_width(context, child) for child in children)
        elif (max_function in ('auto', 'max-content') or
              max_function[0] == 'fit_content()'):
            sizes[1] = max(
                max_content_width(context, child) for child in children)
        if None not in sizes:
            sizes[1] = max(sizes)
    # 1.2.3 Increase sizes to accommodate items spanning content-sized tracks.
    spans = sorted({
        width if direction == 'x' else height
        for (_, _, width, height) in children_positions.values()
        if (width if direction == 'x' else height) >= 2})
    for span in spans:
        tracks_children = [[] for _ in range(len(sizing_functions))]
        iterable = enumerate(children_positions.items())
        for i, (child, (x, y, width, height)) in iterable:
            coord, size = (x, width) if direction == 'x' else (y, height)
            if size != span:
                continue
            for _, max_function in sizing_functions[i:i+span+1]:
                if _is_fr(max_function):
                    break
            else:
                parent = boxes.BlockContainerBox.anonymous_from(containing_block, ())
                resolve_percentages(parent, containing_block)
                if direction == 'y':
                    parent.width = sum(orthogonal_sizes[x:x+width])
                tracks_children[coord - implicit_start].append((child, parent))
        # 1.2.3.1 For intrinsic minimums.
        # TODO: Respect min-/max-content constraint.
        _distribute_extra_space(
            'min', 'intrinsic', 'minimum', tracks_children,
            sizing_functions, tracks_sizes, span, direction, context)
        # 1.2.3.2 For content-based minimums.
        _distribute_extra_space(
            'min', 'content-based', 'min-content', tracks_children,
            sizing_functions, tracks_sizes, span, direction, context)
        # 1.2.3.3 For max-content minimums.
        # TODO: Respect max-content constraint.
        _distribute_extra_space(
            'min', 'max-content', 'max-content', tracks_children,
            sizing_functions, tracks_sizes, span, direction, context)
        # 1.2.3.4 Increase growth limit.
        # TODO: Increase growth limit.
        # 1.2.3.5 For intrinsic maximums.
        _distribute_extra_space(
            'max', 'intrinsic', 'min-content', tracks_children,
            sizing_functions, tracks_sizes, span, direction, context)
        # 1.2.3.6 For max-content maximums.
        _distribute_extra_space(
            'max', 'max-content', 'max-content', tracks_children,
            sizing_functions, tracks_sizes, span, direction, context)
    # 1.2.4 Increase sizes to accommodate items spanning flexible tracks.
    # TODO: Support spans for flexible tracks.
    # 1.2.5 Fix infinite growth limits.
    for sizes in tracks_sizes:
        if sizes[1] is inf:
            sizes[1] = sizes[0]
    # 1.3 Maximize tracks.
    if box_size == 'auto':
        free_space = None
    else:
        free_space = (
            box_size -
            sum(size[0] for size in tracks_sizes) -
            (len(tracks_sizes) - 1) * gap)
    if free_space is not None and free_space > 0:
        distributed_free_space = free_space / len(tracks_sizes)
        for i, sizes in enumerate(tracks_sizes):
            base_size, growth_limit = sizes
            if base_size + distributed_free_space > growth_limit:
                sizes[0] = growth_limit
                free_space -= growth_limit - base_size
            else:
                sizes[0] += distributed_free_space
                free_space -= distributed_free_space
    # TODO: Respect max-width/-height.
    # 1.4 Expand flexible tracks.
    inflexible_tracks = set()
    if free_space is not None and free_space <= 0:
        # TODO: Respect min-content constraint.
        flex_fraction = 0
    elif free_space is not None:
        stop = False
        while not stop:
            leftover_space = free_space
            flex_factor_sum = 0
            iterable = enumerate(zip(tracks_sizes, sizing_functions))
            for i, (sizes, (_, max_function)) in iterable:
                if _is_fr(max_function):
                    leftover_space += sizes[0]
                    if i not in inflexible_tracks:
                        flex_factor_sum += max_function.value
            flex_factor_sum = max(1, flex_factor_sum)
            hypothetical_fr_size = leftover_space / flex_factor_sum
            stop = True
            iterable = enumerate(zip(tracks_sizes, sizing_functions))
            for i, (sizes, (_, max_function)) in iterable:
                if i not in inflexible_tracks and _is_fr(max_function):
                    if hypothetical_fr_size * max_function.value < sizes[0]:
                        inflexible_tracks.add(i)
                        free_space -= sizes[0]
                        stop = free_space > 0
        flex_fraction = hypothetical_fr_size
    else:
        flex_fraction = 0
        iterable = zip(tracks_sizes, sizing_functions)
        for sizes, (_, max_function) in iterable:
            if _is_fr(max_function):
                if max_function.value > 1:
                    flex_fraction = max(
                        flex_fraction, max_function.value * sizes[0])
                else:
                    flex_fraction = max(flex_fraction, sizes[0])
        # TODO: Respect grid items max-content contribution.
        # TODO: Respect min-* constraint.
    iterable = enumerate(zip(tracks_sizes, sizing_functions))
    for i, (sizes, (_, max_function)) in iterable:
        if _is_fr(max_function) and i not in inflexible_tracks:
            if flex_fraction * max_function.value > sizes[0]:
                if free_space is not None:
                    free_space -= flex_fraction * max_function.value
                sizes[0] = flex_fraction * max_function.value
    # 1.5 Expand stretched auto tracks.
    justify_content = containing_block.style['justify_content']
    align_content = containing_block.style['align_content']
    x_stretch = (
        direction == 'x' and set(justify_content) & {'normal', 'stretch'})
    y_stretch = (
        direction == 'y' and set(align_content) & {'normal', 'stretch'})
    if (x_stretch or y_stretch) and free_space is not None and free_space > 0:
        auto_tracks_sizes = [
            sizes for sizes, (min_function, _)
            in zip(tracks_sizes, sizing_functions)
            if min_function == 'auto']
        if auto_tracks_sizes:
            distributed_free_space = free_space / len(auto_tracks_sizes)
            for sizes in auto_tracks_sizes:
                sizes[0] += distributed_free_space

    return tracks_sizes


def grid_layout(context, box, bottom_space, skip_stack, containing_block,
                page_is_empty, absolute_boxes, fixed_boxes):
    context.create_block_formatting_context(box)

    if skip_stack and box.style['box_decoration_break'] != 'clone':
        box.remove_decoration(start=True, end=False)

    if box.style['position'] == 'relative':
        # New containing block, use a new absolute list
        absolute_boxes = []

    # Define explicit grid
    grid_areas = box.style['grid_template_areas']
    flow = box.style['grid_auto_flow']
    auto_rows = cycle(box.style['grid_auto_rows'])
    auto_columns = cycle(box.style['grid_auto_columns'])
    auto_rows_back = cycle(box.style['grid_auto_rows'][::-1])
    auto_columns_back = cycle(box.style['grid_auto_columns'][::-1])
    column_gap = box.style['column_gap']
    if column_gap == 'normal':
        column_gap = 0
    else:
        refer_to = containing_block.width if box.width == 'auto' else box.width
        column_gap = percentage(column_gap, box.style, refer_to)
    row_gap = box.style['row_gap']
    if row_gap == 'normal':
        row_gap = 0
    else:
        refer_to = 0 if box.height == 'auto' else box.height
        row_gap = percentage(row_gap, box.style, refer_to)

    if grid_areas == 'none':
        grid_areas = ((None,),)
    grid_areas = [list(row) for row in grid_areas]

    rows = _get_template_tracks(box.style['grid_template_rows'])
    columns = _get_template_tracks(box.style['grid_template_columns'])

    # Adjust rows number
    grid_areas_columns = len(grid_areas[0]) if grid_areas else 0
    rows_diff = int((len(rows) - 1) / 2) - len(grid_areas)
    if rows_diff > 0:
        for _ in range(rows_diff):
            grid_areas.append([None] * grid_areas_columns)
    elif rows_diff < 0:
        for _ in range(-rows_diff):
            rows.append(next(auto_rows))
            rows.append([])

    # Adjust columns number
    columns_diff = int((len(columns) - 1) / 2) - grid_areas_columns
    if columns_diff > 0:
        for row in grid_areas:
            for _ in range(columns_diff):
                row.append(None)
    elif columns_diff < 0:
        for _ in range(-columns_diff):
            columns.append(next(auto_columns))
            columns.append([])

    # Add implicit line names
    for y, row in enumerate(grid_areas):
        for x, area_name in enumerate(row):
            if area_name is None:
                continue
            start_name = f'{area_name}-start'
            names = [name for row in rows[::2] for name in row]
            if start_name not in names:
                rows[2*y].append(start_name)
            names = [name for column in columns[::2] for name in column]
            if start_name not in names:
                columns[2*x].append(start_name)
    for y, row in enumerate(grid_areas[::-1]):
        for x, area_name in enumerate(row[::-1]):
            if area_name is None:
                continue
            end_name = f'{area_name}-end'
            names = [name for row in rows[::2] for name in row]
            if end_name not in names:
                rows[-2*y-1].append(end_name)
            names = [name for column in columns[::2] for name in column]
            if end_name not in names:
                columns[-2*x-1].append(end_name)

    # 1. Run the grid placement algorithm.

    first_flow = 'column' if 'column' in flow else 'row'  # auto flow axis
    second_flow = 'row' if 'column' in flow else 'column'  # other axis
    first_tracks = rows if first_flow == 'row' else columns
    second_tracks = rows if second_flow == 'row' else columns

    # 1.1 Position anything that’s not auto-positioned.
    children = sorted(box.children, key=lambda item: item.style['order'])
    children_positions = {}
    for child in children:
        column_start = child.style['grid_column_start']
        column_end = child.style['grid_column_end']
        row_start = child.style['grid_row_start']
        row_end = child.style['grid_row_end']

        column_placement = _get_placement(
            column_start, column_end, columns[::2])
        row_placement = _get_placement(row_start, row_end, rows[::2])

        if column_placement and row_placement:
            x, width = column_placement
            y, height = row_placement
            children_positions[child] = (x, y, width, height)

    # 1.2 Process the items locked to a given row (resp. column).
    for child in children:
        if child in children_positions:
            continue
        first_start = child.style[f'grid_{first_flow}_start']
        first_end = child.style[f'grid_{first_flow}_end']
        first_placement = _get_placement(first_start, first_end, first_tracks[::2])
        if not first_placement:
            continue
        second_start = child.style[f'grid_{second_flow}_start']
        second_end = child.style[f'grid_{second_flow}_end']
        second_placement = _get_second_placement(
            first_placement, second_start, second_end, second_tracks,
            children_positions, first_flow, 'dense' in flow)
        if first_flow == 'row':
            y, height = first_placement
            x, width = second_placement
        else:
            x, width = first_placement
            y, height = second_placement
        children_positions[child] = (x, y, width, height)

    # 1.3 Determine the columns (resp. rows) in the implicit grid.
    # 1.3.1 Start with the columns (resp. rows) from the explicit grid.
    implicit_second_1 = 0
    if second_flow == 'column':
        implicit_second_2 = len(grid_areas[0]) if grid_areas else 0
    else:
        implicit_second_2 = len(grid_areas)
    # 1.3.2 Add columns (resp. rows) to the beginning and end of the implicit grid.
    remaining_grid_items = []
    for child in children:
        if child in children_positions:
            if second_flow == 'column':
                i, _, size, _ = children_positions[child]
            else:
                _, i, _, size = children_positions[child]
        else:
            second_start = child.style[f'grid_{second_flow}_start']
            second_end = child.style[f'grid_{second_flow}_end']
            second_placement = _get_placement(
                second_start, second_end, second_tracks[::2])
            remaining_grid_items.append(child)
            if second_placement:
                i, size = second_placement
            else:
                continue
        implicit_second_1 = min(i, implicit_second_1)
        implicit_second_2 = max(i + size, implicit_second_2)
    # 1.3.3 Add columns (resp. rows) to accommodate max track span.
    for child in remaining_grid_items:
        second_start = child.style[f'grid_{second_flow}_start']
        second_end = child.style[f'grid_{second_flow}_end']
        span = 1
        if second_start != 'auto' and second_start[0] == 'span':
            span = second_start[1]
        elif second_end != 'auto' and second_end[0] == 'span':
            span = second_end[1]
        implicit_second_2 = max(implicit_second_1 + (span or 1), implicit_second_2)

    # 1.4 Position the remaining grid items.
    implicit_first_1 = 0
    if first_flow == 'row':
        implicit_first_2 = len(grid_areas)
    else:
        implicit_first_2 = len(grid_areas[0]) if grid_areas else 0
    for position in children_positions.values():
        if first_flow == 'row':
            _, i, _, size = position
        else:
            i, _, size, _ = position
        implicit_first_1 = min(i, implicit_first_1)
        implicit_first_2 = max(i + size, implicit_first_2)
    cursor_first, cursor_second = implicit_first_1, implicit_second_1
    if 'dense' in flow:
        for child in remaining_grid_items:
            first_start = child.style[f'grid_{first_flow}_start']
            first_end = child.style[f'grid_{first_flow}_end']
            second_start = child.style[f'grid_{second_flow}_start']
            second_end = child.style[f'grid_{second_flow}_end']
            second_placement = _get_placement(
                second_start, second_end, second_tracks[::2])
            if second_placement:
                # 1. Set the row (resp. column) position of the cursor.
                cursor_first = implicit_first_1
                second_i, second_size = second_placement
                cursor_second = second_i
                # 2. Increment the cursor’s row (resp. column) position.
                for first_i in count(cursor_first):
                    if first_start == 'auto':
                        first_i, first_size = _get_placement(
                            (None, first_i + 1, None), first_end, first_tracks[::2])
                    else:
                        assert first_start[0] == 'span'
                        span = _get_span(first_start)
                        first_i, first_size = _get_placement(
                            first_start, (None, first_i + 1 + span, None),
                            first_tracks[::2])
                    if first_i < cursor_first:
                        continue
                    for _ in range(first_i, first_i + first_size):
                        if first_flow == 'row':
                            x, y = second_i, first_i
                            width, height = second_size, first_size
                        else:
                            x, y = first_i, second_i
                            width, height = first_size, second_size
                        intersect = _intersect_with_children(
                            x, y, width, height, children_positions.values())
                        if intersect:
                            # Child intersects with a positioned child on
                            # current row.
                            break
                    else:
                        # Child doesn’t intersect with any positioned child on
                        # any row.
                        break
                first_diff = first_i + first_size - implicit_first_2
                if first_diff > 0:
                    implicit_first_2 += first_diff
                # 3. Set the item’s row-start line.
                if first_flow == 'row':
                    x, y = second_i, first_i
                    width, height = second_size, first_size
                else:
                    x, y = first_i, second_i
                    width, height = first_size, second_size
                children_positions[child] = (x, y, width, height)
            else:
                # 1. Set the cursor’s row and column positions.
                cursor_first, cursor_second = implicit_first_1, implicit_second_1
                while True:
                    # 2. Increment the column (resp. row) position of the cursor.
                    first_i = cursor_first
                    for second_i in range(cursor_second, implicit_second_2):
                        if first_start == 'auto':
                            first_i, first_size = _get_placement(
                                (None, first_i + 1, None), first_end, first_tracks[::2])
                        else:
                            assert first_start[0] == 'span'
                            span = _get_span(first_start)
                            first_i, first_size = _get_placement(
                                first_start, (None, first_i + 1 + span, None),
                                first_tracks[::2])
                        if second_start == 'auto':
                            second_i, second_size = _get_placement(
                                (None, second_i + 1, None), second_end,
                                second_tracks[::2])
                        else:
                            span = _get_span(second_start)
                            second_i, second_size = _get_placement(
                                second_start, (None, second_i + 1 + span, None),
                                second_tracks[::2])
                        if first_flow == 'row':
                            x, y = second_i, first_i
                            width, height = second_size, first_size
                        else:
                            x, y = first_i, second_i
                            width, height = first_size, second_size
                        intersect = _intersect_with_children(
                            x, y, width, height, children_positions.values())
                        overflow = second_i + second_size > implicit_second_2
                        if intersect or overflow:
                            # Child intersects with a positioned child or overflows.
                            continue
                        else:
                            # Free place found.
                            # 3. Set the item’s row-/column-start lines.
                            children_positions[child] = (x, y, width, height)
                            first_diff = (
                                cursor_first + first_size - 1 - implicit_first_2)
                            if first_diff > 0:
                                implicit_first_2 += first_diff
                            break
                    else:
                        # No room found.
                        # 2. Return to the previous step.
                        cursor_first += 1
                        first_diff = cursor_first + 1 - implicit_first_2
                        if first_diff > 0:
                            implicit_first_2 += first_diff
                        cursor_second = implicit_second_1
                        continue
                    break
    else:
        for child in remaining_grid_items:
            first_start = child.style[f'grid_{first_flow}_start']
            first_end = child.style[f'grid_{first_flow}_end']
            second_start = child.style[f'grid_{second_flow}_start']
            second_end = child.style[f'grid_{second_flow}_end']
            second_placement = _get_placement(
                second_start, second_end, second_tracks[::2])
            if second_placement:
                # 1. Set the column (resp. row) position of the cursor.
                second_i, second_size = second_placement
                if second_i < cursor_second:
                    cursor_first += 1
                cursor_second = second_i
                # 2. Increment the cursor’s row (resp. column) position.
                for cursor_first in count(cursor_first):
                    if first_start == 'auto':
                        first_i, first_size = _get_placement(
                            (None, cursor_first + 1, None), first_end,
                            first_tracks[::2])
                    else:
                        assert first_start[0] == 'span'
                        span = _get_span(first_start)
                        first_i, first_size = _get_placement(
                            first_start, (None, first_i + 1 + span, None),
                            first_tracks[::2])
                    if first_i < cursor_first:
                        continue
                    for row in range(first_i, first_i + first_size):
                        if first_flow == 'row':
                            x, y = second_i, first_i
                            width, height = second_size, first_size
                        else:
                            x, y = first_i, second_i
                            width, height = first_size, second_size
                        intersect = _intersect_with_children(
                            x, y, width, height, children_positions.values())
                        if intersect:
                            # Child intersects with a positioned child on
                            # current row.
                            break
                    else:
                        # Child doesn’t intersect with any positioned child on
                        # any row.
                        break
                first_diff = first_i + first_size - implicit_first_2
                if first_diff > 0:
                    implicit_first_2 += first_diff
                # 3. Set the item’s row-start line.
                children_positions[child] = (x, y, width, height)
            else:
                while True:
                    # 1. Increment the column position of the cursor.
                    first_i = cursor_first
                    for second_i in range(cursor_second, implicit_second_2):
                        if first_start == 'auto':
                            first_i, first_size = _get_placement(
                                (None, first_i + 1, None), first_end, first_tracks[::2])
                        else:
                            span = _get_span(first_start)
                            first_i, first_size = _get_placement(
                                first_start, (None, first_i + 1 + span, None),
                                first_tracks[::2])
                        if second_start == 'auto':
                            second_i, second_size = _get_placement(
                                (None, second_i + 1, None), second_end,
                                second_tracks[::2])
                        else:
                            span = _get_span(second_start)
                            second_i, second_size = _get_placement(
                                second_start, (None, second_i + 1 + span, None),
                                second_tracks[::2])
                        if first_flow == 'row':
                            x, y = second_i, first_i
                            width, height = second_size, first_size
                        else:
                            x, y = first_i, second_i
                            width, height = first_size, second_size
                        intersect = _intersect_with_children(
                            x, y, width, height, children_positions.values())
                        overflow = second_i + second_size > implicit_second_2
                        if intersect or overflow:
                            # Child intersects with a positioned child or overflows.
                            continue
                        else:
                            # Free place found.
                            # 2. Set the item’s row-/column-start lines.
                            children_positions[child] = (x, y, width, height)
                            break
                    else:
                        # No room found.
                        # 2. Return to the previous step.
                        cursor_first += 1
                        first_diff = cursor_first + 1 - implicit_first_2
                        if first_diff > 0:
                            implicit_first_2 += first_diff
                        cursor_second = implicit_second_1
                        continue
                    break

    if first_flow == 'row':
        implicit_x1, implicit_x2 = implicit_second_1, implicit_second_2
        implicit_y1, implicit_y2 = implicit_first_1, implicit_first_2
    else:
        implicit_x1, implicit_x2 = implicit_first_1, implicit_first_2
        implicit_y1, implicit_y2 = implicit_second_1, implicit_second_2
    for _ in range(0 - implicit_x1):
        columns.insert(0, next(auto_columns_back))
        columns.insert(0, [])
    for _ in range(len(grid_areas[0]) if grid_areas else 0, implicit_x2):
        columns.append(next(auto_columns))
        columns.append([])
    for _ in range(0 - implicit_y1):
        rows.insert(0, next(auto_rows_back))
        rows.insert(0, [])
    for _ in range(len(grid_areas), implicit_y2):
        rows.append(next(auto_rows))
        rows.append([])

    page_breaks_by_row = defaultdict(
        lambda: {'before': 'auto', 'after': 'auto', 'inside': 'auto'})
    for child, (x, y, width, height) in children_positions.items():
        row_page_break = page_breaks_by_row[y]
        if child.style['break_inside'] in ('avoid', 'avoid-page'):
            row_page_break['inside'] = 'avoid'
        for side in ('before', 'after'):
            if child.style[f'break_{side}'] in ('avoid', 'avoid-page'):
                if row_page_break[side] != 'page':
                    row_page_break[side] = 'avoid'
            elif child.style[f'break_{side}'] in ('page', 'always'):
                row_page_break[side] = 'page'
            elif child.style[f'break_{side}'] in ('left', 'right', 'recto', 'verso'):
                row_page_break[side] = child.style[f'break_{side}']

    # 2. Find the size of the grid container.

    if isinstance(box, boxes.GridBox):
        from .block import block_level_width
        block_level_width(box, containing_block)
    else:
        assert isinstance(box, boxes.InlineGridBox)
        from .inline import inline_block_width
        inline_block_width(box, context, containing_block)
    if box.width == 'auto':
        # TODO: Calculate max-width.
        box.width = containing_block.width

    # 3. Run the grid sizing algorithm.

    # 3.0 List min/max sizing functions.
    row_sizing_functions = [_get_sizing_functions(row) for row in rows[1::2]]
    column_sizing_functions = [
        _get_sizing_functions(column) for column in columns[1::2]]

    # 3.1 Resolve the sizes of the grid columns.
    columns_sizes = _resolve_tracks_sizes(
        column_sizing_functions, box.width, children_positions, implicit_second_1,
        'x', column_gap, context, box)

    # 3.2 Resolve the sizes of the grid rows.
    vertical_sizes = [size for size, _ in columns_sizes]
    rows_sizes = _resolve_tracks_sizes(
        row_sizing_functions, box.height, children_positions, implicit_y1, 'y',
        row_gap, context, box, vertical_sizes)

    # 3.3 Re-resolve the sizes of the grid columns with min-/max-content.
    # TODO: Re-resolve.

    # 3.4 Re-re-resolve the sizes of the grid columns with min-/max-content.
    # TODO: Re-re-resolve.

    # 3.5 Align the tracks within the grid container.
    # TODO: Support safe/unsafe.
    justify_content = set(box.style['justify_content'])
    x = box.content_box_x()
    free_width = max(0, box.width - sum(size for size, _ in columns_sizes))
    columns_positions = []
    columns_number = len(columns_sizes)
    if justify_content & {'center'}:
        x += free_width / 2
        for size, _ in columns_sizes:
            columns_positions.append(x)
            x += size + column_gap
    elif justify_content & {'right', 'end', 'flex-end'}:
        x += free_width
        for size, _ in columns_sizes:
            columns_positions.append(x)
            x += size + column_gap
    elif justify_content & {'space-around'}:
        x += free_width / 2 / columns_number
        for size, _ in columns_sizes:
            columns_positions.append(x)
            x += size + free_width / columns_number + column_gap
    elif justify_content & {'space-between'}:
        for size, _ in columns_sizes:
            columns_positions.append(x)
            if columns_number >= 2:
                x += size + free_width / (columns_number - 1) + column_gap
    elif justify_content & {'space-evenly'}:
        x += free_width / (columns_number + 1)
        for size, _ in columns_sizes:
            columns_positions.append(x)
            x += size + free_width / (columns_number + 1) + column_gap
    else:
        for size, _ in columns_sizes:
            columns_positions.append(x)
            x += size + column_gap

    align_content = set(box.style['align_content'])
    y = box.content_box_y()
    if box.height == 'auto':
        free_height = 0
    else:
        free_height = (
            box.height -
            sum(size for size, _ in rows_sizes) -
            (len(rows_sizes) - 1) * row_gap)
        free_height = max(0, free_height)
    rows_positions = []
    rows_number = len(rows_sizes)
    if align_content & {'center'}:
        y += free_height / 2
        for size, _ in rows_sizes:
            rows_positions.append(y)
            y += size + row_gap
    elif align_content & {'right', 'end', 'flex-end'}:
        y += free_height
        for size, _ in rows_sizes:
            rows_positions.append(y)
            y += size + row_gap
    elif align_content & {'space-around'}:
        y += free_height / 2 / rows_number
        for size, _ in rows_sizes:
            rows_positions.append(y)
            y += size + free_height / rows_number + row_gap
    elif align_content & {'space-between'}:
        for size, _ in rows_sizes:
            rows_positions.append(y)
            if rows_number >= 2:
                y += size + free_height / (rows_number - 1) + row_gap
    elif align_content & {'space-evenly'}:
        y += free_height / (rows_number + 1)
        for size, _ in rows_sizes:
            rows_positions.append(y)
            y += size + free_height / (rows_number + 1) + row_gap
    else:
        if align_content & {'baseline'}:
            # TODO: Support baseline value.
            LOGGER.warning('Baseline alignment is not supported for grid layout')
        for size, _ in rows_sizes:
            rows_positions.append(y)
            y += size + row_gap

    # 4. Lay out the grid items into their respective containing blocks.

    # Find resume_at row.
    this_page_children = []
    resume_row = None
    extra_skip_height = 0
    if skip_stack:
        from .block import block_level_layout
        first_skip_row = min(skip_stack)
        last_skip_row = max(skip_stack)
        skip_height = (
            sum(size for size, _ in rows_sizes[:last_skip_row]) +
            (len(rows_sizes[:last_skip_row]) - 1) * row_gap)
        for child, (x, y, width, height) in children_positions.items():
            if (advancement := box.advancements.get((x, y))) is None:
                continue
            span = _get_span(child.style['grid_row_start'])
            span_height = (
                sum(size for size, _ in rows_sizes[y:y+span]) +
                (span - 1) * row_gap)
            index = tuple(children_positions).index(child)
            width = sum(vertical_sizes[x:x+width])
            child = child.deepcopy()
            child.position_x = 0
            child.position_y = 0
            if y in skip_stack:
                child_skip_stack = skip_stack[y].get(index)
            else:
                child_skip_stack = None
            parent = boxes.BlockContainerBox.anonymous_from(containing_block, ())
            resolve_percentages(parent, containing_block)
            parent.position_x = 0
            parent.position_y = 0
            parent.width = width
            parent.height = 0
            child, _, _, _, _, _ = block_level_layout(
                context, child, bottom_space=-inf, skip_stack=child_skip_stack,
                containing_block=parent)
            skip_stack_advancement = span_height - child.margin_height()
            if skip_stack_advancement < advancement:
                extra_skip_height = max(
                    extra_skip_height, advancement - skip_stack_advancement)
        for (x, y), advancement in box.advancements.items():
            if y != last_skip_row:
                continue
            skip_height += advancement
            break
        else:
            extra_skip_height = 0
        skip_height -= extra_skip_height
    else:
        first_skip_row = last_skip_row = skip_height = 0
    resume_at = None
    total_height = (
        sum(size for size, _ in rows_sizes[last_skip_row:]) +
        (len(rows_sizes[last_skip_row:]) - 1) * row_gap)

    def _add_page_children(max_row=inf):
        for j, child in enumerate(children):
            _, y, _, _ = children_positions[child]
            if not first_skip_row <= y < max_row:
                # Item in previous or next rows.
                continue
            if skip_stack is None:
                # No skip stack, draw on this page.
                this_page_children.append((j, child))
            elif y > last_skip_row:
                # Row after the skip stack, draw on this page.
                this_page_children.append((j, child))
            elif y in skip_stack:
                child_skip_stack = skip_stack[y]
                if child_skip_stack is None or j in child_skip_stack:
                    # Child in skip stack, draw on this page.
                    this_page_children.append((j, child))

    row_lines_positions = (
        [*rows_positions[first_skip_row + 1:], box.content_box_y() + total_height])
    for i, row_y in enumerate(row_lines_positions, start=first_skip_row):
        # TODO: handle break-before and break-after for rows.
        if not context.overflows_page(bottom_space, row_y - skip_height):
            page_is_empty = False
            continue
        resume_row = i
        if box.style['break_inside'] == 'avoid' and not page_is_empty:
            # Avoid breaks inside grid container, break before.
            context.finish_block_formatting_context(box)
            return None, None, {'break': 'any', 'page': None}, [], False
        if page_breaks_by_row[i]['inside'] == 'avoid' and not page_is_empty:
            # Break before current row.
            if resume_row == 0:
                # First row, break before grid container.
                context.finish_block_formatting_context(box)
                return None, None, {'break': 'any', 'page': None}, [], False
            # Mark all children before and in current row as drawn on the page.
            _add_page_children(resume_row)
            resume_at = {resume_row: None}
        else:
            # Break inside current row.
            # Mark all children before current row as drawn on the page.
            page_is_empty = False
            _add_page_children(resume_row + 1)
        break
    else:
        # Mark all children as drawn on the page.
        _add_page_children()
    if box.height == 'auto':
        box.height = (
            sum(size for size, _ in rows_sizes[:resume_row]) +
            (len(rows_sizes[:resume_row]) - 1) * row_gap) - skip_height

    # Lay out grid items.
    justify_items = set(box.style['justify_items'])
    align_items = set(box.style['align_items'])
    new_children = []
    new_children_by_rows = defaultdict(list)
    baseline = None
    next_page = {'break': 'any', 'page': None}
    from .block import block_level_layout
    for i, child in this_page_children:
        x, y, width, height = children_positions[child]
        index = children.index(child)
        if skip_stack and skip_stack.get(y):
            if index in skip_stack[y]:
                child_skip_stack = skip_stack[y][index]
            else:
                assert isinstance(child, boxes.ParentBox)
                child_skip_stack = {len(child.children): None}
        else:
            child_skip_stack = None
        child = child.deepcopy()
        child.position_x = columns_positions[x]
        child.position_y = rows_positions[y] - skip_height
        resolve_percentages(child, box)
        width = (
            sum(size for size, _ in columns_sizes[x:x+width]) +
            (width - 1) * column_gap)
        height = (
            sum(size for size, _ in rows_sizes[y:y+height]) +
            (height - 1) * row_gap)
        if skip_stack and (x, y) in box.advancements:
            child.position_y += box.advancements[x, y] - extra_skip_height
            height -= box.advancements[x, y] - extra_skip_height

        # TODO: Apply auto margin.
        if child.margin_top == 'auto':
            child.margin_top = 0
        if child.margin_right == 'auto':
            child.margin_right = 0
        if child.margin_bottom == 'auto':
            child.margin_bottom = 0
        if child.margin_left == 'auto':
            child.margin_left = 0

        child_border_width = width - (child.margin_left + child.margin_right)
        child_content_width = child_border_width - (
                child.border_left_width + child.padding_left +
                child.border_right_width + child.padding_right)
        child_border_height = height - child.margin_bottom
        child_content_height = child_border_height - (
            child.border_bottom_width + child.padding_bottom)
        if not child_skip_stack or child.style['box_decoration_break'] == 'clone':
            child_border_height -= child.margin_top
            child_content_height -= (
                child.margin_top + child.border_top_width + child.padding_top)

        justify_self = set(child.style['justify_self'])
        if justify_self & {'auto'}:
            justify_self = justify_items
        if justify_self & {'normal', 'stretch'}:
            if child.style['width'] == 'auto':
                child.style = child.style.copy()
                child_width = (
                    child_content_width if child.style['box_sizing'] == 'content-box'
                    else child_border_width)
                child.style['width'] = Dimension(child_width, 'px')
        align_self = set(child.style['align_self'])
        if align_self & {'auto'}:
            align_self = align_items
        if align_self & {'normal', 'stretch'}:
            if child.style['height'] == 'auto':
                child.style = child.style.copy()
                child_height = (
                    child_content_height if child.style['box_sizing'] == 'content-box'
                    else child_border_height)
                child.style['height'] = Dimension(child_height, 'px')

        # TODO: Find a better solution for the layout.
        parent = boxes.BlockContainerBox.anonymous_from(box, ())
        resolve_percentages(parent, containing_block)
        parent.position_x = child.position_x
        parent.position_y = child.position_y
        parent.width = width
        parent.height = height
        new_child, child_resume_at, child_next_page = block_level_layout(
            context, child, bottom_space, child_skip_stack, parent,
            page_is_empty, absolute_boxes, fixed_boxes)[:3]
        if new_child:
            page_is_empty = False
            broken_child = False
            span = _get_span(child.style['grid_row_start'])
            if child_resume_at:
                broken_child = True
            elif resume_row is not None and y + span >= resume_row + 1:
                broken_child = True
            if broken_child:
                # Child is broken, add row to resume_at.
                if resume_at is None:
                    resume_at = {}
                if y not in resume_at:
                    resume_at[y] = {}
            if child_resume_at:
                # There is some content left for next page, save the cell’s resume_at.
                resume_at[y][i] = child_resume_at
            elif broken_child:
                # Everything fits but the cell overflows. Only display the bottom of an
                # empty cell on next page, set the cell’s resume_at after the cell’s
                # last child.
                assert isinstance(new_child, boxes.ParentBox)
                previous_skip_child = max(child_skip_stack) if child_skip_stack else 0
                resume_at[y][i] = {previous_skip_child + len(new_child.children): None}
        else:
            if resume_at is None:
                resume_at = {}
            if y not in resume_at:
                resume_at[y] = {}
            resume_at[y][i] = None
            continue

        if justify_self & {'normal', 'stretch'}:
            new_child.width = max(child_content_width, new_child.width)
        else:
            if new_child.style['width'] == 'auto':
                new_child.width = max_content_width(context, new_child, outer=False)
            diff = child_content_width - new_child.width
            if justify_self & {'center'}:
                new_child.translate(diff / 2, 0)
            elif justify_self & {'right', 'end', 'flex-end', 'self-end'}:
                new_child.translate(diff, 0)

        # TODO: Apply auto margins.
        if align_self & {'normal', 'stretch'}:
            new_child.height = max(child_content_height, new_child.height)
        else:
            diff = child_content_height - new_child.height
            if align_self & {'center'}:
                new_child.translate(0, diff / 2)
            elif align_self & {'end', 'flex-end', 'self-end'}:
                new_child.translate(0, diff)

        new_children.append(new_child)
        new_children_by_rows[y].append((x, new_child))
        if baseline is None and y == implicit_y1:
            baseline = find_in_flow_baseline(new_child)

    # Abort whole grid rendering if no child fits.
    if this_page_children and not new_children:
        context.finish_block_formatting_context(box)
        return None, None, {'break': 'any', 'page': None}, [], False

    old_advancements = box.advancements.copy()
    advancements = box.advancements
    advancements.clear()
    box = box.copy_with_children(new_children)
    if isinstance(box, boxes.InlineGridBox):
        # TODO: Synthetize a real baseline value.
        LOGGER.warning('Inline grids are not supported')
        box.baseline = baseline or 0

    from .absolute import absolute_layout
    from .block import relative_positioning

    if box.style['position'] == 'relative':
        # New containing block, resolve the layout of the absolute descendants
        for absolute_box in absolute_boxes:
            absolute_layout(
                context, absolute_box, box, fixed_boxes, bottom_space,
                skip_stack=None)

    for child in box.children:
        relative_positioning(child, (box.width, box.height))

    # Resume early when there’s no resume_at.
    if not resume_at:
        context.finish_block_formatting_context(box)
        return box, resume_at, next_page, [], False

    # Set broken rows’ bottom at the bottom of the page.
    last_page_row = max(new_children_by_rows)
    next_page_first_row = min(resume_at)
    next_page_last_row = max(resume_at)
    extra_advancement = 0
    for y in range(last_page_row, next_page_first_row - 1, -1):
        for x, child in new_children_by_rows[y]:
            span = _get_span(child.style['grid_row_start'])
            if y + span < last_page_row + 1:
                # Child finishing before the last row, do nothing.
                continue
            broken_child = y + span >= next_page_last_row + 1
            if broken_child and child.style['box_decoration_break'] != 'clone':
                child.remove_decoration(start=False, end=True)
            child.height = (
                context.page_bottom - bottom_space - child.position_y -
                child.margin_top - child.border_top_width - child.padding_top -
                child.margin_bottom - child.border_bottom_width - child.padding_bottom)
            if broken_child:
                # Child not fully drawn, keep advancement.
                advancements[x, y] = child.margin_height()
                if (x, y) in old_advancements:
                    advancements[x, y] += old_advancements[x, y] - extra_skip_height
            else:
                # Child fully drawn, save the extra height added to reach the bottom of
                # the page to substract it from the advancements.
                extra_advancement = max(
                    extra_advancement, child.height - child_content_height)

    # Substract the extra height added to reach the bottom of the page from all the
    # advancements.
    if extra_advancement:
        for x, y in advancements:
            advancements[x, y] -= extra_advancement

    # Set box height and remove bottom decoration.
    box.height = (
        context.page_bottom - bottom_space - box.position_y -
        box.margin_top - box.border_top_width - box.padding_top)
    if box.style['box_decoration_break'] != 'clone':
        box.remove_decoration(start=False, end=True)
        box.height -= box.margin_bottom + box.border_bottom_width + box.padding_bottom

    context.finish_block_formatting_context(box)
    return box, resume_at, next_page, [], False
