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

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 != 'fr'


def _is_fr(sizing):
    return isinstance(sizing, Dimension) and sizing.unit == '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, containing_block):
    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
    for functions in sizing_functions:
        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 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=containing_block, page_is_empty=True,
                    absolute_boxes=[], fixed_boxes=[])
                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.
    for i, increase in enumerate(planned_increases):
        if affected_sizes == 'max' and tracks_sizes[i][1] is inf:
            tracks_sizes[i][1] = tracks_sizes[i][0] + increase
        else:
            tracks_sizes[i][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, 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, 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, _, 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, page_is_empty=True,
                    absolute_boxes=[], fixed_boxes=[])
                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:
                tracks_children[coord - implicit_start].append(child)
        # 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,
            containing_block)
        # 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,
            containing_block)
        # 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,
            containing_block)
        # 1.2.3.4 Increase growth limit.
        for sizes in tracks_sizes:
            if None not in sizes:
                sizes[1] = max(sizes)
        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:
                tracks_children[coord - implicit_start].append(child)
        # 1.2.3.5 For intrinsic maximums.
        _distribute_extra_space(
            'max', 'intrinsic', 'min-content', tracks_children,
            sizing_functions, tracks_sizes, span, direction, context,
            containing_block)
        # 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,
            containing_block)
    # 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()

    # 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, 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, 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_positions = {}
    for child in box.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).
    children = sorted(box.children, key=lambda item: item.style['order'])
    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([])

    # 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.
    rows_sizes = _resolve_tracks_sizes(
        row_sizing_functions, box.height, children_positions, implicit_y1, 'y',
        row_gap, context, box, [size for size, _ in columns_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
    if skip_stack:
        skip_row = next(iter(skip_stack))
        skip_height = (
            sum(size for size, _ in rows_sizes[:skip_row]) +
            (len(rows_sizes[:skip_row]) - 1) * row_gap)
    else:
        skip_row = 0
        skip_height = 0
    resume_at = None
    total_height = (
        sum(size for size, _ in rows_sizes[skip_row:]) +
        (len(rows_sizes[skip_row:]) - 1) * row_gap)
    row_lines_positions = (
        rows_positions[skip_row + 1:] + [box.content_box_y() + total_height])
    for i, row_y in enumerate(row_lines_positions, start=skip_row + 1):
        if context.overflows_page(bottom_space, row_y - skip_height):
            if not page_is_empty:
                if i == 1:
                    return None, None, {'break': 'any', 'page': None}, [], False
                resume_row = i - 1
                resume_at = {i-1: None}
                for child in children:
                    _, y, _, _ = children_positions[child]
                    if skip_row <= y <= i-2:
                        this_page_children.append(child)
                break
        page_is_empty = False
    else:
        for child in children:
            _, y, _, _ = children_positions[child]
            if skip_row <= y:
                this_page_children.append(child)
    if box.height == 'auto':
        box.height = (
            sum(size for size, _ in rows_sizes[skip_row:resume_row]) +
            (len(rows_sizes[skip_row:resume_row]) - 1) * row_gap)
    # Lay out grid items.
    justify_items = set(box.style['justify_items'])
    align_items = set(box.style['align_items'])
    new_children = []
    baseline = None
    next_page = {'break': 'any', 'page': None}
    from .block import block_level_layout
    for child in this_page_children:
        x, y, width, height = children_positions[child]
        index = box.children.index(child)
        if skip_stack and skip_stack.get(y) and index in skip_stack[y]:
            child_skip_stack = skip_stack[y][index]
        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)

        # 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_width = width - (
            child.margin_left + child.border_left_width + child.padding_left +
            child.margin_right + child.border_right_width + child.padding_right)
        child_height = height - (
            child.margin_top + child.border_top_width + child.padding_top +
            child.margin_bottom + child.border_bottom_width + child.padding_bottom)

        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['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['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
            # TODO: Support fragmentation in grid items.
        else:
            # TODO: Support fragmentation in grid rows.
            continue

        if justify_self & {'normal', 'stretch'}:
            new_child.width = max(child_width, new_child.width)
        else:
            new_child.width = max_content_width(context, new_child)
            diff = child_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_height, new_child.height)
        else:
            diff = child_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)

        # TODO: Take care of page fragmentation.
        new_children.append(new_child)
        if baseline is None and y == implicit_y1:
            baseline = find_in_flow_baseline(new_child)

    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

    context.finish_block_formatting_context(box)

    return box, resume_at, next_page, [], False
