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

import sys
from math import inf, log10

from ..css.properties import Dimension
from ..formatting_structure import boxes
from . import percent
from .absolute import AbsolutePlaceholder, absolute_layout
from .preferred import max_content_width, min_content_width, min_max
from .table import find_in_flow_baseline, table_wrapper_width


class FlexLine(list):
    """Flex container line."""


def flex_layout(context, box, bottom_space, skip_stack, containing_block, page_is_empty,
                absolute_boxes, fixed_boxes, discard):
    from . import block

    # TODO: merge this with block_container_layout.
    context.create_flex_formatting_context(box)
    resume_at = None
    next_page = {'break': 'any', 'page': None}

    is_start = skip_stack is None
    box.remove_decoration(start=not is_start, end=False)

    discard |= box.style['continue'] == 'discard'
    draw_bottom_decoration = discard or box.style['box_decoration_break'] == 'clone'

    row_gap, column_gap = box.style['row_gap'], box.style['column_gap']

    if draw_bottom_decoration:
        bottom_space += box.padding_bottom + box.border_bottom_width + box.margin_bottom

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

    # References are to: https://www.w3.org/TR/css-flexbox-1/#layout-algorithm.

    # 1 Initial setup, done in formatting_structure.build.

    # 2 Determine the available main and cross space for the flex items.
    if box.style['flex_direction'].startswith('row'):
        main, cross = 'width', 'height'
    else:
        main, cross = 'height', 'width'

    margin_left = 0 if box.margin_left == 'auto' else box.margin_left
    margin_right = 0 if box.margin_right == 'auto' else box.margin_right

    # Define available main space.
    # TODO: min- and max-content not implemented.
    if getattr(box, main) != 'auto':
        # If that dimension of the flex container’s content box is a definite size…
        available_main_space = getattr(box, main)
    else:
        # Otherwise, subtract the flex container’s margin, border, and padding…
        if main == 'width':
            available_main_space = (
                containing_block.width -
                margin_left - margin_right -
                box.padding_left - box.padding_right -
                box.border_left_width - box.border_right_width)
        else:
            available_main_space = inf

    # Same as above for available cross space.
    # TODO: min- and max-content not implemented.
    if getattr(box, cross) != 'auto':
        available_cross_space = getattr(box, cross)
    else:
        if cross == 'width':
            available_cross_space = (
                containing_block.width -
                margin_left - margin_right -
                box.padding_left - box.padding_right -
                box.border_left_width - box.border_right_width)
        else:
            available_cross_space = inf

    # 3 Determine the flex base size and hypothetical main size of each item.
    parent_box = box.copy()
    percent.resolve_percentages(parent_box, containing_block)
    parent_box.remove_decoration(start=not is_start, end=False)
    block.block_level_width(parent_box, containing_block)
    children = sorted(box.children, key=lambda item: item.style['order'])
    if skip_stack is not None:
        (skip, skip_stack), = skip_stack.items()
        if box.style['flex_direction'].endswith('-reverse'):
            children = children[:skip + 1]
        else:
            children = children[skip:]
        skip_stack = skip_stack
    else:
        skip, skip_stack = 0, None
    child_skip_stack = skip_stack

    if row_gap == 'normal':
        row_gap = 0
    elif row_gap.unit == '%':
        if box.height == 'auto':
            row_gap = 0
        else:
            row_gap = row_gap.value / 100 * box.height
    else:
        row_gap = row_gap.value
    if column_gap == 'normal':
        column_gap = 0
    elif column_gap.unit == '%':
        if box.width == 'auto':
            column_gap = 0
        else:
            column_gap = column_gap.value / 100 * box.width
    else:
        column_gap = column_gap.value
    if main == 'width':
        main_gap, cross_gap = column_gap, row_gap
    else:
        main_gap, cross_gap = row_gap, column_gap

    position_x = (
        parent_box.position_x + parent_box.border_left_width + parent_box.padding_left)
    if parent_box.margin_left != 'auto':
        position_x += parent_box.margin_left
    position_y = (
        parent_box.position_y + parent_box.border_top_width + parent_box.padding_top)
    if parent_box.margin_top != 'auto':
        position_y += parent_box.margin_top
    for index, child in enumerate(children):
        if not child.is_flex_item:
            # Absolute child layout: create placeholder.
            if child.is_absolutely_positioned():
                child.position_x = position_x
                child.position_y = position_y
                new_child = placeholder = AbsolutePlaceholder(child)
                placeholder.index = index
                children[index] = placeholder
                if child.style['position'] == 'absolute':
                    absolute_boxes.append(placeholder)
                else:
                    fixed_boxes.append(placeholder)
            elif child.is_running():
                running_name = child.style['position'][1]
                page = context.current_page
                context.running_elements[running_name][page].append(child)
            continue
        # See https://www.w3.org/TR/css-flexbox-1/#min-size-auto.
        if main == 'width':
            child_containing_block = (available_main_space, parent_box.height)
        else:
            child_containing_block = (parent_box.width, available_main_space)
        percent.resolve_percentages(child, child_containing_block)
        if child.is_table_wrapper:
            table_wrapper_width(context, child, child_containing_block)
        child.position_x = position_x
        child.position_y = position_y
        if child.style['min_width'] == 'auto':
            specified_size = child.width
            new_child = child.copy()
            new_child.style = child.style.copy()
            new_child.style['width'] = 'auto'
            new_child.style['min_width'] = Dimension(0, 'px')
            new_child.style['max_width'] = Dimension(inf, 'px')
            content_size = min_content_width(context, new_child, outer=False)
            transferred_size = None
            if isinstance(child, boxes.ReplacedBox):
                image = child.replacement
                _, intrinsic_height, intrinsic_ratio = image.get_intrinsic_size(
                    child.style['image_resolution'], child.style['font_size'])
                if intrinsic_ratio and intrinsic_height:
                    transferred_size = intrinsic_height * intrinsic_ratio
                    content_size = max(
                        child.min_width, min(child.max_width, content_size))
            if specified_size != 'auto':
                child.min_width = min(specified_size, content_size)
            elif transferred_size is not None:
                child.min_width = min(transferred_size, content_size)
            else:
                child.min_width = content_size
        if child.style['min_height'] == 'auto':
            # TODO: avoid calling block_level_layout, write min_content_height instead.
            specified_size = child.height
            new_child = child.copy()
            new_child.style = child.style.copy()
            new_child.style['height'] = 'auto'
            new_child.style['min_height'] = Dimension(0, 'px')
            new_child.style['max_height'] = Dimension(inf, 'px')
            if new_child.style['width'] == 'auto':
                new_child_width = max_content_width(context, new_child)
                new_child.style['width'] = Dimension(new_child_width, 'px')
            new_child = block.block_level_layout(
                context, new_child, bottom_space, child_skip_stack, parent_box,
                page_is_empty)[0]
            content_size = new_child.height if new_child else 0
            transferred_size = None
            if isinstance(child, boxes.ReplacedBox):
                image = child.replacement
                intrinsic_width, _, intrinsic_ratio = image.get_intrinsic_size(
                    child.style['image_resolution'], child.style['font_size'])
                if intrinsic_ratio and intrinsic_width:
                    transferred_size = intrinsic_width / intrinsic_ratio
                    content_size = max(
                        child.min_height, min(child.max_height, content_size))
                elif not intrinsic_width:
                    # TODO: wrongly set by block_level_layout, would be OK with
                    # min_content_height.
                    content_size = 0
            if specified_size != 'auto':
                child.min_height = min(specified_size, content_size)
            elif transferred_size is not None:
                child.min_height = min(transferred_size, content_size)
            else:
                child.min_height = content_size

        if child.style['flex_basis'] == 'content':
            flex_basis = 'content'
        else:
            flex_basis = percent.percentage(
                child.style['flex_basis'], child.style, available_main_space)
            if flex_basis == 'auto':
                if (flex_basis := getattr(child, main)) == 'auto':
                    flex_basis = 'content'

        # 3.A If the item has a definite used flex basis…
        if flex_basis != 'content':
            child.flex_base_size = flex_basis
            if main == 'width':
                child.main_outer_extra = (
                    child.border_left_width + child.border_right_width +
                    child.padding_left + child.padding_right)
                if child.margin_left != 'auto':
                    child.main_outer_extra += child.margin_left
                if child.margin_right != 'auto':
                    child.main_outer_extra += child.margin_right
            else:
                child.main_outer_extra = (
                    child.border_top_width + child.border_bottom_width +
                    child.padding_top + child.padding_bottom)
                if child.margin_top != 'auto':
                    child.main_outer_extra += child.margin_top
                if child.margin_bottom != 'auto':
                    child.main_outer_extra += child.margin_bottom
        elif False:
            # TODO: 3.B If the flex item has an intrinsic aspect ratio…
            # TODO: 3.C If the used flex basis is 'content'…
            # TODO: 3.D Otherwise, if the used flex basis is 'content'…
            pass
        else:
            # 3.E Otherwise…
            new_child = child.copy()
            new_child.style = child.style.copy()
            if main == 'width':
                # … the item’s min and max main sizes are ignored.
                new_child.style['min_width'] = Dimension(0, 'px')
                new_child.style['max_width'] = Dimension(inf, 'px')

                child.flex_base_size = max_content_width(
                    context, new_child, outer=False)
                child.main_outer_extra = (
                    max_content_width(context, child) - child.flex_base_size)
            else:
                # … the item’s min and max main sizes are ignored.
                new_child.style['min_height'] = Dimension(0, 'px')
                new_child.style['max_height'] = Dimension(inf, 'px')

                new_child.width = inf
                new_child, _, _, adjoining_margins, _, _ = block.block_level_layout(
                    context, new_child, bottom_space, child_skip_stack, parent_box,
                    page_is_empty, absolute_boxes, fixed_boxes)
                if new_child:
                    # As flex items margins never collapse (with other flex items or
                    # with the flex container), we can add the adjoining margins to the
                    # child height.
                    new_child.height += block.collapse_margin(adjoining_margins)
                    child.flex_base_size = new_child.height
                    child.main_outer_extra = (
                        new_child.margin_height() - new_child.height)
                else:
                    child.flex_base_size = child.main_outer_extra = 0

        if main == 'width':
            position_x += child.flex_base_size + child.main_outer_extra
        else:
            position_y += child.flex_base_size + child.main_outer_extra

        min_size = getattr(child, f'min_{main}')
        max_size = getattr(child, f'max_{main}')
        child.hypothetical_main_size = max(
            min_size, min(child.flex_base_size, max_size))

        # Skip stack is only for the first child.
        child_skip_stack = None

    # 4 Determine the main size of the flex container using the rules of the formatting
    # context in which it participates.
    original_box_height = box.height
    if main == 'width':
        block.block_level_width(box, containing_block)
    else:
        if box.height == 'auto':
            box.height = 0
            flex_items = (child for child in children if child.is_flex_item)
            for i, child in enumerate(flex_items):
                box.height += child.hypothetical_main_size + child.main_outer_extra
                if i:
                    box.height += main_gap
        box.height = max(box.min_height, min(box.height, box.max_height))

    # 5 If the flex container is single-line, collect all the flex items into a single
    # flex line.
    flex_lines = []
    line = []
    line_size = 0
    main_size = getattr(box, main)
    for i, child in enumerate(children, start=skip):
        if not child.is_flex_item:
            continue
        line_size += child.hypothetical_main_size + child.main_outer_extra
        if i > skip:
            line_size += main_gap
        if box.style['flex_wrap'] != 'nowrap' and line_size > main_size:
            if line:
                flex_lines.append(FlexLine(line))
                line = [(i, child)]
                line_size = child.hypothetical_main_size + child.main_outer_extra
            else:
                line.append((i, child))
                flex_lines.append(FlexLine(line))
                line = []
                line_size = 0
        else:
            line.append((i, child))
    if line:
        flex_lines.append(FlexLine(line))

    # TODO: Handle *-reverse using the terminology from the specification.
    if box.style['flex_wrap'] == 'wrap-reverse':
        flex_lines.reverse()
    if box.style['flex_direction'].endswith('-reverse'):
        for line in flex_lines:
            line.reverse()

    # 6 Resolve the flexible lengths of all the flex items to find their used main size.
    available_main_space = getattr(box, main)
    for line in flex_lines:
        # 9.7.1 Determine the used flex factor.
        hypothetical_main_size = sum(
            child.hypothetical_main_size + child.main_outer_extra
            for index, child in line)
        if hypothetical_main_size < available_main_space:
            flex_factor_type = 'grow'
        else:
            flex_factor_type = 'shrink'

        # 9.7.3 Size inflexible items.
        for index, child in line:
            if flex_factor_type == 'grow':
                child.flex_factor = child.style['flex_grow']
                flex_condition = child.flex_base_size > child.hypothetical_main_size
            else:
                child.flex_factor = child.style['flex_shrink']
                flex_condition = child.flex_base_size < child.hypothetical_main_size
            if child.flex_factor == 0 or flex_condition:
                child.target_main_size = child.hypothetical_main_size
                child.frozen = True
            else:
                child.frozen = False

        # 9.7.4 Calculate initial free space.
        initial_free_space = available_main_space
        for i, (index, child) in enumerate(line):
            if child.frozen:
                initial_free_space -= child.target_main_size + child.main_outer_extra
            else:
                initial_free_space -= child.flex_base_size + child.main_outer_extra
            if i:
                initial_free_space -= main_gap

        # 9.7.5.a Check for flexible items.
        while not all(child.frozen for index, child in line):
            unfrozen_factor_sum = 0
            remaining_free_space = available_main_space

            # 9.7.5.b Calculate the remaining free space.
            for i, (index, child) in enumerate(line):
                if child.frozen:
                    remaining_free_space -= (
                        child.target_main_size + child.main_outer_extra)
                else:
                    remaining_free_space -= (
                        child.flex_base_size + child.main_outer_extra)
                    unfrozen_factor_sum += child.flex_factor
                if i:
                    remaining_free_space -= main_gap

            if unfrozen_factor_sum < 1:
                initial_free_space *= unfrozen_factor_sum

            if initial_free_space == inf:
                initial_free_space = sys.maxsize
            if remaining_free_space == inf:
                remaining_free_space = sys.maxsize

            initial_magnitude = (
                int(log10(initial_free_space)) if initial_free_space > 0 else -inf)
            remaining_magnitude = (
                int(log10(remaining_free_space)) if remaining_free_space > 0 else -inf)
            if initial_magnitude < remaining_magnitude:
                remaining_free_space = initial_free_space

            # 9.7.5.c Distribute free space proportional to the flex factors.
            if remaining_free_space == 0:
                # If the remaining free space is zero: "Do nothing", but we at least set
                # the flex_base_size as target_main_size for next step.
                for index, child in line:
                    if not child.frozen:
                        child.target_main_size = child.flex_base_size
            else:
                scaled_flex_shrink_factors_sum = 0
                flex_grow_factors_sum = 0
                for index, child in line:
                    if not child.frozen:
                        child.scaled_flex_shrink_factor = (
                            child.flex_base_size * child.style['flex_shrink'])
                        scaled_flex_shrink_factors_sum += (
                            child.scaled_flex_shrink_factor)
                        flex_grow_factors_sum += child.style['flex_grow']
                for index, child in line:
                    if not child.frozen:
                        # If using the flex grow factor…
                        if flex_factor_type == 'grow':
                            ratio = child.style['flex_grow'] / flex_grow_factors_sum
                            child.target_main_size = (
                                child.flex_base_size + remaining_free_space * ratio)
                        # If using the flex shrink factor…
                        elif flex_factor_type == 'shrink':
                            if scaled_flex_shrink_factors_sum == 0:
                                child.target_main_size = child.flex_base_size
                            else:
                                ratio = (
                                    child.scaled_flex_shrink_factor /
                                    scaled_flex_shrink_factors_sum)
                                child.target_main_size = (
                                    child.flex_base_size + remaining_free_space * ratio)
                        child.target_main_size = min_max(child, child.target_main_size)

            # 9.7.5.d Fix min/max violations.
            for index, child in line:
                child.adjustment = 0
                if not child.frozen:
                    min_size = getattr(child, f'min_{main}')
                    max_size = getattr(child, f'max_{main}')
                    min_size = max(min_size, min(child.target_main_size, max_size))
                    if child.target_main_size < min_size:
                        child.adjustment = min_size - child.target_main_size
                        child.target_main_size = min_size

            # 9.7.5.e Freeze over-flexed items.
            adjustments = sum(child.adjustment for index, child in line)
            for index, child in line:
                # Zero: Freeze all items.
                if adjustments == 0:
                    child.frozen = True
                # Positive: Freeze all the items with min violations.
                elif adjustments > 0 and child.adjustment > 0:
                    child.frozen = True
                # Negative: Freeze all the items with max violations.
                elif adjustments < 0 and child.adjustment < 0:
                    child.frozen = True

        # 9.7.6 Set each item’s used main size to its target main size.
        for index, child in line:
            if main == 'width':
                child.width = child.target_main_size
            else:
                child.height = child.target_main_size

    # 7 Determine the hypothetical cross size of each item.
    # TODO: Handle breaks.
    new_flex_lines = []
    child_skip_stack = skip_stack
    for line in flex_lines:
        new_flex_line = FlexLine()
        for index, child in line:
            # TODO: Fix this value, see test_flex_item_auto_margin_cross.
            if child.margin_top == 'auto':
                child.margin_top = 0
            if child.margin_bottom == 'auto':
                child.margin_bottom = 0
            # TODO: Find another way than calling block_level_layout_switch.
            new_child = child.copy()
            new_child, _, _, adjoining_margins, _, _ = block.block_level_layout_switch(
                context, new_child, -inf, child_skip_stack, parent_box, page_is_empty,
                absolute_boxes, fixed_boxes, adjoining_margins=[],
                first_letter_style=None, first_line_style=None, discard=discard,
                max_lines=None)
            child._baseline = find_in_flow_baseline(new_child) or 0
            if cross == 'height':
                child.height = new_child.height
                # As flex items margins never collapse (with other flex items or
                # with the flex container), we can add the adjoining margins to the
                # child height.
                child.height += block.collapse_margin(adjoining_margins)
            else:
                if child.width == 'auto':
                    min_width = min_content_width(context, child, outer=False)
                    max_width = max_content_width(context, child, outer=False)
                    child.width = min(max(min_width, new_child.width), max_width)
                else:
                    child.width = new_child.width

            new_flex_line.append((index, child))

            # Skip stack is only for the first child.
            child_skip_stack = None

        if new_flex_line:
            new_flex_lines.append(new_flex_line)
    flex_lines = new_flex_lines

    # 8 Calculate the cross size of each flex line.
    cross_size = getattr(box, cross)
    if len(flex_lines) == 1 and cross_size != 'auto':
        # If the flex container is single-line…
        flex_lines[0].cross_size = cross_size
    else:
        # Otherwise, for each flex line…
        # 8.1 Collect all the flex items whose inline-axis is parallel to the main-axis…
        for line in flex_lines:
            collected_items = []
            not_collected_items = []
            for index, child in line:
                align_self = child.style['align_self']
                collect = (
                    box.style['flex_direction'].startswith('row') and
                    'baseline' in align_self and
                    'auto' not in (child.margin_top, child.margin_bottom))
                (collected_items if collect else not_collected_items).append(child)
            cross_start_distance = cross_end_distance = 0
            for child in collected_items:
                baseline = child._baseline - child.position_y
                cross_start_distance = max(cross_start_distance, baseline)
                cross_end_distance = max(
                    cross_end_distance, child.margin_height() - baseline)
            collected_cross_size = cross_start_distance + cross_end_distance
            non_collected_cross_size = 0
            # 8.2 Find the largest outer hypothetical cross size.
            if not_collected_items:
                non_collected_cross_size = -inf
                for child in not_collected_items:
                    if cross == 'height':
                        child_cross_size = child.border_height()
                        if child.margin_top != 'auto':
                            child_cross_size += child.margin_top
                        if child.margin_bottom != 'auto':
                            child_cross_size += child.margin_bottom
                    else:
                        child_cross_size = child.border_width()
                        if child.margin_left != 'auto':
                            child_cross_size += child.margin_left
                        if child.margin_right != 'auto':
                            child_cross_size += child.margin_right
                    non_collected_cross_size = max(
                        child_cross_size, non_collected_cross_size)
            # 8.3 Set the used cross-size of the flex line.
            line.cross_size = max(collected_cross_size, non_collected_cross_size)

    # 8.3 If the flex container is single-line…
    if len(flex_lines) == 1:
        line, = flex_lines
        min_cross_size = getattr(box, f'min_{cross}')
        if min_cross_size == 'auto':
            min_cross_size = -inf
        max_cross_size = getattr(box, f'max_{cross}')
        if max_cross_size == 'auto':
            max_cross_size = inf
        line.cross_size = max(min_cross_size, min(line.cross_size, max_cross_size))

    # 9 Handle 'align-content: stretch'.
    align_content = box.style['align_content']
    if 'normal' in align_content:
        align_content = ('stretch',)
    if 'stretch' in align_content:
        definite_cross_size = None
        if cross == 'height' and box.height != 'auto':
            definite_cross_size = box.height
        elif cross == 'width':
            if isinstance(box, boxes.FlexBox):
                if box.width == 'auto':
                    definite_cross_size = available_cross_space
                else:
                    definite_cross_size = box.width
        if definite_cross_size is not None:
            extra_cross_size = definite_cross_size
            extra_cross_size -= sum(line.cross_size for line in flex_lines)
            extra_cross_size -= (len(flex_lines) - 1) * cross_gap
            if extra_cross_size:
                for line in flex_lines:
                    line.cross_size += extra_cross_size / len(flex_lines)

    # TODO: 10 Collapse 'visibility: collapse' items.

    # 11 Determine the used cross size of each flex item.
    align_items = box.style['align_items']
    if 'normal' in align_items:
        align_items = ('stretch',)
    for line in flex_lines:
        for index, child in line:
            align_self = child.style['align_self']
            if 'normal' in align_self:
                align_self = ('stretch',)
            elif 'auto' in align_self:
                align_self = align_items
            if 'stretch' in align_self and child.style[cross] == 'auto':
                cross_margins = (
                    (child.style['margin_top'], child.style['margin_bottom'])
                    if cross == 'height' else
                    (child.style['margin_left'], child.style['margin_right']))
                if 'auto' not in cross_margins:
                    cross_size = line.cross_size
                    if cross == 'height':
                        cross_size -= (
                            child.margin_top + child.margin_bottom +
                            child.padding_top + child.padding_bottom +
                            child.border_top_width +
                            child.border_bottom_width)
                    else:
                        cross_size -= (
                            child.margin_left + child.margin_right +
                            child.padding_left + child.padding_right +
                            child.border_left_width +
                            child.border_right_width)
                    setattr(child, cross, cross_size)
            # else: Cross size has been set by step 7.

    # 12 Distribute any remaining free space.
    original_position_main = (
        box.content_box_x() if main == 'width'
        else box.content_box_y())
    justify_content = box.style['justify_content']
    if 'normal' in justify_content:
        justify_content = ('flex-start',)
    if box.style['flex_direction'].endswith('-reverse'):
        if 'flex-start' in justify_content:
            justify_content = ('flex-end',)
        elif 'flex-end' in justify_content:
            justify_content = ('flex-start',)
        elif 'start' in justify_content:
            justify_content = ('end',)
        elif 'end' in justify_content:
            justify_content = ('start',)

    for line in flex_lines:
        position_main = original_position_main
        if main == 'width':
            free_space = box.width
            for index, child in line:
                free_space -= child.border_width()
                if child.margin_left != 'auto':
                    free_space -= child.margin_left
                if child.margin_right != 'auto':
                    free_space -= child.margin_right
        else:
            free_space = box.height
            for index, child in line:
                free_space -= child.border_height()
                if child.margin_top != 'auto':
                    free_space -= child.margin_top
                if child.margin_bottom != 'auto':
                    free_space -= child.margin_bottom
        free_space -= (len(line) - 1) * main_gap

        # 12.1 If the remaining free space is positive…
        margins = 0
        for index, child in line:
            if main == 'width':
                if child.margin_left == 'auto':
                    margins += 1
                if child.margin_right == 'auto':
                    margins += 1
            else:
                if child.margin_top == 'auto':
                    margins += 1
                if child.margin_bottom == 'auto':
                    margins += 1
        if margins:
            free_space /= margins
            for index, child in line:
                if main == 'width':
                    if child.margin_left == 'auto':
                        child.margin_left = free_space
                    if child.margin_right == 'auto':
                        child.margin_right = free_space
                else:
                    if child.margin_top == 'auto':
                        child.margin_top = free_space
                    if child.margin_bottom == 'auto':
                        child.margin_bottom = free_space
            free_space = 0

        if box.style['direction'] == 'rtl' and main == 'width':
            free_space *= -1

        # 12.2 Align the items along the main-axis per justify-content.
        if {'end', 'flex-end', 'right'} & set(justify_content):
            position_main += free_space
        elif 'center' in justify_content:
            position_main += free_space / 2
        elif 'space-around' in justify_content:
            position_main += free_space / len(line) / 2
        elif 'space-evenly' in justify_content:
            position_main += free_space / (len(line) + 1)

        growths = sum(child.style['flex_grow'] for child in children)
        if box.style['direction'] == 'rtl' and main == 'width':
            main_gap *= -1
        for i, (index, child) in enumerate(line):
            if i:
                position_main += main_gap
            if main == 'width':
                child.position_x = position_main
                if 'stretch' in justify_content and growths:
                    child.width += free_space * child.style['flex_grow'] / growths
            else:
                child.position_y = position_main
            margin_main = (
                child.margin_width() if main == 'width' else child.margin_height())
            if box.style['direction'] == 'rtl' and main == 'width':
                margin_main *= -1
            position_main += margin_main
            if 'space-around' in justify_content:
                position_main += free_space / len(line)
            elif 'space-between' in justify_content:
                if len(line) > 1:
                    position_main += free_space / (len(line) - 1)
            elif 'space-evenly' in justify_content:
                position_main += free_space / (len(line) + 1)

    # 13 Resolve cross-axis auto margins.
    if cross == 'width':
        # Make sure width/margins are no longer "auto", as we did not do it above in
        # step 4.
        block.block_level_width(box, containing_block)
    position_cross = box.content_box_y() if cross == 'height' else box.content_box_x()
    for line in flex_lines:
        line.lower_baseline = -inf
        # TODO: Don't duplicate this loop.
        for index, child in line:
            align_self = child.style['align_self']
            if 'auto' in align_self:
                align_self = align_items
            if 'baseline' in align_self and main == 'width':
                # TODO: handle vertical text.
                child.baseline = child._baseline - position_cross
                line.lower_baseline = max(line.lower_baseline, child.baseline)
        if line.lower_baseline == -inf:
            line.lower_baseline = line[0][1]._baseline if line else 0
        for index, child in line:
            cross_margins = (
                (child.style['margin_top'], child.style['margin_bottom'])
                if cross == 'height' else
                (child.style['margin_left'], child.style['margin_right']))
            auto_margins = sum([margin == 'auto' for margin in cross_margins])
            # If a flex item has auto cross-axis margins…
            if auto_margins:
                extra_cross = line.cross_size
                if cross == 'height':
                    extra_cross -= child.border_height()
                    if child.margin_top != 'auto':
                        extra_cross -= child.margin_top
                    if child.margin_bottom != 'auto':
                        extra_cross -= child.margin_bottom
                else:
                    extra_cross -= child.border_width()
                    if child.margin_left != 'auto':
                        extra_cross -= child.margin_left
                    if child.margin_right != 'auto':
                        extra_cross -= child.margin_right
                if extra_cross > 0:
                    # If its outer cross size is less than the cross size…
                    extra_cross /= auto_margins
                    if cross == 'height':
                        if child.style['margin_top'] == 'auto':
                            child.margin_top = extra_cross
                        if child.style['margin_bottom'] == 'auto':
                            child.margin_bottom = extra_cross
                    else:
                        if child.style['margin_left'] == 'auto':
                            child.margin_left = extra_cross
                        if child.style['margin_right'] == 'auto':
                            child.margin_right = extra_cross
                else:
                    # Otherwise…
                    if cross == 'height':
                        if child.margin_top == 'auto':
                            child.margin_top = 0
                        child.margin_bottom = extra_cross
                    else:
                        if child.margin_left == 'auto':
                            child.margin_left = 0
                        child.margin_right = extra_cross
            else:
                # 14 Align all flex items along the cross-axis.
                align_self = child.style['align_self']
                if 'normal' in align_self:
                    align_self = ('stretch',)
                elif 'auto' in align_self:
                    align_self = align_items
                position = 'position_y' if cross == 'height' else 'position_x'
                setattr(child, position, position_cross)
                if {'end', 'self-end', 'flex-end'} & set(align_self):
                    if cross == 'height':
                        child.position_y += line.cross_size - child.margin_height()
                    else:
                        child.position_x += line.cross_size - child.margin_width()
                elif 'center' in align_self:
                    if cross == 'height':
                        child.position_y += (
                            line.cross_size - child.margin_height()) / 2
                    else:
                        child.position_x += (line.cross_size - child.margin_width()) / 2
                elif 'baseline' in align_self:
                    if cross == 'height':
                        child.position_y += line.lower_baseline - child.baseline
                    else:
                        # TODO: Handle vertical text.
                        pass
                elif 'stretch' in align_self:
                    if child.style[cross] == 'auto':
                        if cross == 'height':
                            margins = child.margin_top + child.margin_bottom
                        else:
                            margins = child.margin_left + child.margin_right
                        if child.style['box_sizing'] == 'content-box':
                            if cross == 'height':
                                margins += (
                                    child.border_top_width + child.border_bottom_width +
                                    child.padding_top + child.padding_bottom)
                            else:
                                margins += (
                                    child.border_left_width + child.border_right_width +
                                    child.padding_left + child.padding_right)
        position_cross += line.cross_size

    # 15 Determine the flex container’s used cross size.
    # TODO: Use the updated algorithm.
    if getattr(box, cross) == 'auto':
        # Otherwise, use the sum of the flex lines' cross sizes…
        # TODO: Handle min-max.
        # TODO: What about align-content here?
        cross_size = sum(line.cross_size for line in flex_lines)
        cross_size += (len(flex_lines) - 1) * cross_gap
        setattr(box, cross, cross_size)

    if len(flex_lines) > 1:
        # 15 If the cross size property is a definite size, use that…
        extra_cross_size = getattr(box, cross)
        extra_cross_size -= sum(line.cross_size for line in flex_lines)
        extra_cross_size -= (len(flex_lines) - 1) * cross_gap
        # 16 Align all flex lines per align-content.
        cross_translate = 0
        direction = 'position_y' if cross == 'height' else 'position_x'
        for i, line in enumerate(flex_lines):
            flex_items = tuple(child for _, child in line if child.is_flex_item)
            if i:
                cross_translate += cross_gap
            for child in flex_items:
                current_value = getattr(child, direction) + cross_translate
                setattr(child, direction, current_value)
            if extra_cross_size == 0:
                continue
            for child in flex_items:
                if {'flex-end', 'end'} & set(align_content):
                    setattr(child, direction, current_value + extra_cross_size)
                elif 'center' in align_content:
                    setattr(child, direction, current_value + extra_cross_size / 2)
                elif 'space-around' in align_content:
                    setattr(
                        child, direction,
                        current_value + extra_cross_size / len(flex_lines) / 2)
                elif 'space-evenly' in align_content:
                    setattr(
                        child, direction,
                        current_value + extra_cross_size / (len(flex_lines) + 1))
            if 'space-between' in align_content:
                cross_translate += extra_cross_size / (len(flex_lines) - 1)
            elif 'space-around' in align_content:
                cross_translate += extra_cross_size / len(flex_lines)
            elif 'space-evenly' in align_content:
                cross_translate += extra_cross_size / (len(flex_lines) + 1)

    # Now we are no longer in the flex algorithm.
    box = box.copy_with_children(
        [child for child in children if child.is_absolutely_positioned()])
    child_skip_stack = skip_stack
    for line in flex_lines:
        for index, child in line:
            if child.is_flex_item:
                # TODO: Don't use block_level_layout_switch.
                new_child, child_resume_at, next_page = block.block_level_layout_switch(
                    context, child, bottom_space, child_skip_stack, box, page_is_empty,
                    absolute_boxes, fixed_boxes, adjoining_margins=[],
                    first_letter_style=None, first_line_style=None, discard=discard,
                    max_lines=None)[:3]
                if new_child is None:
                    if resume_at:
                        resume_index, = resume_at
                        resume_index -= 1
                    else:
                        resume_index = 0
                    resume_at = {resume_index + index: None}
                else:
                    page_is_empty = False
                    box.children.append(new_child)
                    position_y = new_child.position_y + new_child.border_height()
                    if child_resume_at is not None:
                        first_level_skip = 0
                        if resume_at:
                            resume_index, = resume_at
                            first_level_skip = resume_index
                        resume_at = {first_level_skip + index: child_resume_at}
                if resume_at:
                    break

            # Skip stack is only for the first child.
            child_skip_stack = None
        if resume_at:
            break

    if original_box_height != 'auto':
        # Don’t resume if flex items overflow flex container bottom.
        box_bottom = box.position_y + box.border_height()
        if context.overflows(box_bottom, position_y):
            resume_at = None

    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:
        block.relative_positioning(child, (box.width, box.height))

    # TODO: Use real algorithm, see https://www.w3.org/TR/css-flexbox-1/#flex-baselines.
    if isinstance(box, boxes.InlineFlexBox):
        if main == 'width':  # and main text direction is horizontal
            box.baseline = flex_lines[0].lower_baseline if flex_lines else 0
        else:
            for child in box.children:
                if child.is_in_normal_flow():
                    box.baseline = find_in_flow_baseline(child) or 0
                    break
            else:
                box.baseline = 0

    box.remove_decoration(start=False, end=resume_at and not discard)

    context.finish_flex_formatting_context(box)

    return box, resume_at, next_page, [], False
