# coding: utf8
"""
    weasyprint.layout.preferred
    ---------------------------

    Preferred and minimum preferred width, aka. the shrink-to-fit algorithm.

    :copyright: Copyright 2011-2014 Simon Sapin and contributors, see AUTHORS.
    :license: BSD, see LICENSE for details.

"""

from __future__ import division, unicode_literals

import weakref

from ..formatting_structure import boxes
from .. import text
from .replaced import default_image_sizing


def shrink_to_fit(context, box, available_width):
    """Return the shrink-to-fit width of ``box``.

    *Warning:* both available_outer_width and the return value are
    for width of the *content area*, not margin area.

    http://www.w3.org/TR/CSS21/visudet.html#float-width

    """
    return min(
        max(
            preferred_minimum_width(context, box, outer=False),
            available_width),
        preferred_width(context, box, outer=False))


def preferred_minimum_width(context, box, outer=True):
    """Return the preferred minimum width for ``box``.

    This is the width by breaking at every line-break opportunity.

    """
    if isinstance(box, boxes.BlockContainerBox):
        if box.is_table_wrapper:
            return table_preferred_minimum_width(context, box, outer)
        else:
            return block_preferred_minimum_width(context, box, outer)
    elif isinstance(box, (boxes.InlineBox, boxes.LineBox)):
        return inline_preferred_minimum_width(
            context, box, outer, is_line_start=True)
    elif isinstance(box, boxes.ReplacedBox):
        return replaced_preferred_width(box, outer)
    else:
        raise TypeError(
            'Preferred minimum width for %s not handled yet' %
            type(box).__name__)


def preferred_width(context, box, outer=True):
    """Return the preferred width for ``box``.

    This is the width by only breaking at forced line breaks.

    """
    if isinstance(box, boxes.BlockContainerBox):
        if box.is_table_wrapper:
            return table_preferred_width(context, box, outer)
        else:
            return block_preferred_width(context, box, outer)
    elif isinstance(box, (boxes.InlineBox, boxes.LineBox)):
        return inline_preferred_width(context, box, outer, is_line_start=True)
    elif isinstance(box, boxes.ReplacedBox):
        return replaced_preferred_width(box, outer)
    else:
        raise TypeError(
            'Preferred width for %s not handled yet' % type(box).__name__)


def _block_preferred_width(context, box, function, outer):
    """Helper to create ``block_preferred_*_width.``"""
    width = box.style.width
    if width == 'auto' or width.unit == '%':
        # "percentages on the following properties are treated instead as
        #  though they were the following: width: auto"
        # http://dbaron.org/css/intrinsic/#outer-intrinsic
        children_widths = [
            function(context, child, outer=True) for child in box.children
            if not child.is_absolutely_positioned()]
        width = max(children_widths) if children_widths else 0
    else:
        assert width.unit == 'px'
        width = width.value

    return adjust(box, outer, width)


def adjust(box, outer, width, left=True, right=True):
    """Add paddings, borders and margins to ``width`` when ``outer`` is set."""
    if not outer:
        return width

    min_width = box.style.min_width
    max_width = box.style.max_width
    min_width = min_width.value if min_width.unit != '%' else 0
    max_width = max_width.value if max_width.unit != '%' else float('inf')

    fixed = max(min_width, min(width, max_width))
    percentages = 0

    for value in (
        (['margin_left', 'padding_left'] if left else []) +
        (['margin_right', 'padding_right'] if right else [])
    ):
        # Padding and border are set on the table, not on the wrapper
        # http://www.w3.org/TR/CSS21/tables.html#model
        # TODO: clean this horrible hack!
        if box.is_table_wrapper and value == 'padding_left':
            box = box.get_wrapped_table()

        style_value = box.style[value]
        if style_value != 'auto':
            if style_value.unit == 'px':
                fixed += style_value.value
            else:
                assert style_value.unit == '%'
                percentages += style_value.value

    if left:
        fixed += box.style.border_left_width
    if right:
        fixed += box.style.border_right_width

    if percentages < 100:
        return fixed / (1 - percentages / 100.)
    else:
        # Pathological case, ignore
        return 0


def block_preferred_minimum_width(context, box, outer=True):
    """Return the preferred minimum width for a ``BlockBox``."""
    return _block_preferred_width(
        context, box, preferred_minimum_width, outer)


def block_preferred_width(context, box, outer=True):
    """Return the preferred width for a ``BlockBox``."""
    return _block_preferred_width(context, box, preferred_width, outer)


def table_cell_preferred_minimum_width(context, box, table,
                                       resolved_table_width, outer=True):
    """Return the preferred minimum width for a ``TableCellBox``."""
    # Try to solve the cell's width if it is a percentage
    width = box.style.width
    if (resolved_table_width and table.width != 'auto' and
            width != 'auto' and width.unit == '%'):
        return width.value / 100. * table.width

    # Else return standard block's preferred minimum width
    return _block_preferred_width(
        context, box, preferred_minimum_width, outer)


def table_cell_preferred_width(context, box, table, resolved_table_width,
                               outer=True):
    """Return the preferred width for a ``TableCellBox``."""
    # Try to solve the cell's width if it is a percentage
    width = box.style.width
    if (resolved_table_width and table.width != 'auto' and
            width != 'auto' and width.unit == '%'):
        return width.value / 100. * table.width

    # Else return standard block's preferred width
    return _block_preferred_width(context, box, preferred_width, outer)


def inline_preferred_minimum_width(context, box, outer=True, skip_stack=None,
                                   first_line=False, is_line_start=False):
    """Return the preferred minimum width for an ``InlineBox``.

    The width is calculated from the lines from ``skip_stack``. If
    ``first_line`` is ``True``, only the first line minimum width is
    calculated.

    """
    widths = list(inline_line_widths(
        context, box, outer, is_line_start, minimum=True,
        skip_stack=skip_stack))

    if first_line and len(widths) > 1:
        del widths[1:]
    else:
        widths[-1] -= trailing_whitespace_size(context, box)
    return adjust(box, outer, max(widths))


def inline_preferred_width(context, box, outer=True, is_line_start=False):
    """Return the preferred width for an ``InlineBox``."""
    widths = list(
        inline_line_widths(context, box, outer, is_line_start, minimum=False))
    widths[-1] -= trailing_whitespace_size(context, box)
    return adjust(box, outer, max(widths))


def inline_line_widths(context, box, outer, is_line_start, minimum,
                       skip_stack=None):
    current_line = 0
    if skip_stack is None:
        skip = 0
    else:
        skip, skip_stack = skip_stack
    for index, child in box.enumerate_skip(skip):
        if child.is_absolutely_positioned():
            continue  # Skip

        if isinstance(child, boxes.InlineBox):
            lines = list(inline_line_widths(
                context, child, outer, is_line_start, minimum, skip_stack))
            if len(lines) == 1:
                lines[0] = adjust(child, outer, lines[0])
            else:
                lines[0] = adjust(child, outer, lines[0], right=False)
                lines[-1] = adjust(child, outer, lines[-1], left=False)
        elif isinstance(child, boxes.TextBox):
            if skip_stack is None:
                skip = 0
            else:
                skip, skip_stack = skip_stack
                assert skip_stack is None
            child_text = child.text[(skip or 0):]
            if is_line_start:
                child_text = child_text.lstrip(' ')
            if minimum and child_text == ' ':
                lines = [0, 0]
            else:
                lines = list(text.line_widths(
                    child_text, child.style, context.enable_hinting,
                    width=0 if minimum else None))
        else:
            # http://www.w3.org/TR/css3-text/#line-break-details
            # "The line breaking behavior of a replaced element
            #  or other atomic inline is equivalent to that
            #  of the Object Replacement Character (U+FFFC)."
            # http://www.unicode.org/reports/tr14/#DescriptionOfProperties
            # "By default, there is a break opportunity
            #  both before and after any inline object."
            if minimum:
                lines = [0, preferred_width(context, child), 0]
            else:
                lines = [preferred_width(context, child)]
        # The first text line goes on the current line
        current_line += lines[0]
        if len(lines) > 1:
            # Forced line break
            yield current_line
            if len(lines) > 2:
                for line in lines[1:-1]:
                    yield line
            current_line = lines[-1]
        is_line_start = lines[-1] == 0
        skip_stack = None
    yield current_line


TABLE_CACHE = weakref.WeakKeyDictionary()


def table_and_columns_preferred_widths(context, box, outer=True,
                                       resolved_table_width=False):
    """Return preferred widths for the table and its columns.

    If ``resolved_table_width`` is ``True``, the resolved width (instead of the
    one given in ``box.style``) is used to get the preferred widths.

    The tuple returned is
    ``(table_preferred_minimum_width, table_preferred_width,
    column_preferred_minimum_widths, column_preferred_widths)``

    http://www.w3.org/TR/CSS21/tables.html#auto-table-layout

    """
    table = box.get_wrapped_table()
    result = TABLE_CACHE.get(table)
    if result:
        return result

    if table.style.border_collapse == 'separate':
        border_spacing_x, _ = table.style.border_spacing
    else:
        border_spacing_x = 0

    nb_columns = 0
    if table.column_groups:
        last_column_group = table.column_groups[-1]
        # Column groups always have at least one child column.
        last_column = last_column_group.children[-1]
        # +1 as the grid starts at 0
        nb_columns = last_column.grid_x + 1

    rows = []
    for i, row_group in enumerate(table.children):
        for j, row in enumerate(row_group.children):
            rows.append(row)
            if row.children:
                last_cell = row.children[-1]
                row_grid_width = last_cell.grid_x + last_cell.colspan
                nb_columns = max(nb_columns, row_grid_width)
    nb_rows = len(rows)

    colspan_cells = []
    grid = [[None] * nb_columns for i in range(nb_rows)]
    for i, row in enumerate(rows):
        for cell in row.children:
            if cell.colspan == 1:
                grid[i][cell.grid_x] = cell
            else:
                cell.grid_y = i
                colspan_cells.append(cell)

    # Point #1
    column_preferred_widths = [[0] * nb_rows for i in range(nb_columns)]
    column_preferred_minimum_widths = [
        [0] * nb_rows for i in range(nb_columns)]
    for i, row in enumerate(grid):
        for j, cell in enumerate(row):
            if cell:
                column_preferred_widths[j][i] = \
                    table_cell_preferred_width(
                        context, cell, table, resolved_table_width)
                column_preferred_minimum_widths[j][i] = \
                    table_cell_preferred_minimum_width(
                        context, cell, table, resolved_table_width)

    column_preferred_widths = [
        max(widths) if widths else 0
        for widths in column_preferred_widths]
    column_preferred_minimum_widths = [
        max(widths) if widths else 0
        for widths in column_preferred_minimum_widths]

    # Point #2
    column_groups_widths = []
    column_widths = [None] * nb_columns
    for column_group in table.column_groups:
        assert isinstance(column_group, boxes.TableColumnGroupBox)
        column_groups_widths.append((column_group, column_group.style.width))
        for column in column_group.children:
            assert isinstance(column, boxes.TableColumnBox)
            column_widths[column.grid_x] = column.style.width

    if column_widths:
        for widths in (column_preferred_widths,
                       column_preferred_minimum_widths):
            for i, width in enumerate(widths):
                column_width = column_widths[i]
                if column_width and column_width != 'auto':
                    if column_width.unit == '%' and resolved_table_width:
                        # TODO: If resolved_table_width is false, we should try
                        # to use this percentage as a constraint
                        widths[i] = max(
                            column_width.value / 100. * table.width, widths[i])
                    elif column_width.unit != '%':
                        widths[i] = max(column_width.value, widths[i])

    # Point #3
    for cell in colspan_cells:
        column_slice = slice(cell.grid_x, cell.grid_x + cell.colspan)

        cell_width = (
            table_cell_preferred_width(
                context, cell, table, resolved_table_width) -
            border_spacing_x * (cell.colspan - 1))
        columns_width = sum(column_preferred_widths[column_slice])
        if cell_width > columns_width:
            added_space = (cell_width - columns_width) / cell.colspan
            for i in range(cell.grid_x, cell.grid_x + cell.colspan):
                column_preferred_widths[i] += added_space

        cell_minimum_width = (
            table_cell_preferred_minimum_width(
                context, cell, table, resolved_table_width) -
            border_spacing_x * (cell.colspan - 1))
        columns_minimum_width = sum(
            column_preferred_minimum_widths[column_slice])
        if cell_minimum_width > columns_minimum_width:
            added_space = (
                (cell_minimum_width - columns_minimum_width) / cell.colspan)
            for i in range(cell.grid_x, cell.grid_x + cell.colspan):
                column_preferred_minimum_widths[i] += added_space

    # Point #4
    for column_group, column_group_width in column_groups_widths:
        if (column_group_width and column_group_width != 'auto' and
                (column_group_width.unit != '%' or resolved_table_width)):
            column_indexes = [
                column.grid_x for column in column_group.children]
            columns_width = sum(
                column_preferred_minimum_widths[index]
                for index in column_indexes)
            if column_group_width.unit == '%':
                column_group_width = (
                    column_group_width.value / 100. * table.width)
            else:
                column_group_width = column_group_width.value
            if column_group_width > columns_width:
                added_space = (
                    (column_group_width - columns_width) / len(column_indexes))
                for i in column_indexes:
                    column_preferred_minimum_widths[i] += added_space
                    # The spec seems to say that the colgroup's width is just a
                    # hint for column group's columns minimum width, but if the
                    # sum of the preferred maximum width of the colums is lower
                    # or greater than the colgroup's one, then the columns
                    # don't follow the hint. These lines make the preferred
                    # width equal or greater than the minimum preferred width.
                    if (column_preferred_widths[i] <
                            column_preferred_minimum_widths[i]):
                        column_preferred_widths[i] = \
                            column_preferred_minimum_widths[i]

    total_border_spacing = (nb_columns + 1) * border_spacing_x
    table_preferred_minimum_width = (
        sum(column_preferred_minimum_widths) + total_border_spacing)
    table_preferred_width = sum(column_preferred_widths) + total_border_spacing

    captions = [child for child in box.children
                if child is not table and not child.is_absolutely_positioned()]

    if captions:
        caption_width = max(
            preferred_minimum_width(context, caption) for caption in captions)
    else:
        caption_width = 0

    if table.style.width != 'auto':
        # Take care of the table width
        if resolved_table_width:
            if table.width > table_preferred_minimum_width:
                table_preferred_minimum_width = table.width
        else:
            if (table.style.width.unit != '%' and
                    table.style.width.value > table_preferred_minimum_width):
                table_preferred_minimum_width = table.style.width.value

    if table_preferred_minimum_width < caption_width:
        table_preferred_minimum_width = caption_width

    if table_preferred_minimum_width > table_preferred_width:
        table_preferred_width = table_preferred_minimum_width

    result = (
        table_preferred_minimum_width, table_preferred_width,
        column_preferred_minimum_widths, column_preferred_widths)
    TABLE_CACHE[table] = result
    return result


def table_preferred_minimum_width(context, box, outer=True):
    """Return the preferred minimum width for a ``TableBox``. wrapper"""
    resolved_table_width = box.style.width != 'auto'
    minimum_width, _, _, _ = table_and_columns_preferred_widths(
        context, box, resolved_table_width)
    return adjust(box, outer, minimum_width)


def table_preferred_width(context, box, outer=True):
    """Return the preferred width for a ``TableBox`` wrapper."""
    resolved_table_width = box.style.width != 'auto'
    _, width, _, _ = table_and_columns_preferred_widths(
        context, box, resolved_table_width)
    return adjust(box, outer, width)


def replaced_preferred_width(box, outer=True):
    """Return the preferred minimum width for an ``InlineReplacedBox``."""
    width = box.style.width
    if width == 'auto' or width.unit == '%':
        height = box.style.height
        if height == 'auto' or height.unit == '%':
            height = 'auto'
        else:
            assert height.unit == 'px'
            height = height.value
        image = box.replacement
        iwidth, iheight = image.get_intrinsic_size(box.style.image_resolution)
        width, _ = default_image_sizing(
            iwidth, iheight, image.intrinsic_ratio, 'auto', height,
            default_width=300, default_height=150)
    else:
        assert width.unit == 'px'
        width = width.value
    return adjust(box, outer, width)


def trailing_whitespace_size(context, box):
    """Return the size of the trailing whitespace of ``box``."""
    from .inlines import split_text_box, split_first_line

    while isinstance(box, (boxes.InlineBox, boxes.LineBox)):
        if not box.children:
            return 0
        box = box.children[-1]
    if not (isinstance(box, boxes.TextBox) and box.text and
            box.style.white_space in ('normal', 'nowrap', 'pre-line')):
        return 0
    stripped_text = box.text.rstrip(' ')
    if box.style.font_size == 0 or len(stripped_text) == len(box.text):
        return 0
    if stripped_text:
        old_box, _, _ = split_text_box(context, box, None, None, 0)
        assert old_box
        stripped_box = box.copy_with_text(stripped_text)
        stripped_box, resume, _ = split_text_box(
            context, stripped_box, None, None, 0)
        assert stripped_box is not None
        assert resume is None
        return old_box.width - stripped_box.width
    else:
        _, _, _, width, _, _ = split_first_line(
            box.text, box.style, context.enable_hinting,
            None, None)
        return width
