"""Draw borders."""

from math import ceil, cos, floor, pi, sin, sqrt, tan

from ..formatting_structure import boxes
from ..layout import replaced
from ..layout.percent import percentage
from ..matrix import Matrix
from .color import get_color, styled_color

SIDES = ('top', 'right', 'bottom', 'left')


def set_mask_border(stream, box):
    """Set ``box`` mask border as alpha state on ``stream``."""
    if box.style['mask_border_source'][0] == 'none' or box.mask_border_image is None:
        return
    x, y, w, h, tl, tr, br, bl = box.rounded_border_box()
    matrix = Matrix(e=x, f=y)
    matrix @= stream.ctm
    mask_stream = stream.set_alpha_state(x, y, w, h, box.style['mask_border_mode'])
    draw_border_image(
        box, mask_stream, box.mask_border_image, box.style['mask_border_slice'],
        box.style['mask_border_repeat'], box.style['mask_border_outset'],
        box.style['mask_border_width'])


def draw_column_rules(stream, box):
    """Draw the column rules to a ``pdf.stream.Stream``."""
    border_widths = (0, 0, 0, box.style['column_rule_width'])
    skip_next = True
    for child in box.children:
        if child.style['column_span'] == 'all':
            skip_next = True
            continue
        elif skip_next:
            skip_next = False
            continue
        with stream.stacked():
            rule_width = box.style['column_rule_width']
            rule_style = box.style['column_rule_style']
            if box.style['column_gap'] == 'normal':
                gap = box.style['font_size']  # normal equals 1em
            else:
                gap = percentage(box.style['column_gap'], box.style, box.width)
            position_x = (
                child.position_x - (box.style['column_rule_width'] + gap) / 2)
            border_box = position_x, child.position_y, rule_width, child.height
            clip_border_segment(
                stream, rule_style, rule_width, 'left', border_box, border_widths)
            color = styled_color(
                rule_style, get_color(box.style, 'column_rule_color'), 'left')
            draw_rect_border(stream, border_box, border_widths, rule_style, color)


def draw_border(stream, box):
    """Draw the box borders and column rules to a ``pdf.stream.Stream``."""

    # The box is hidden, easy.
    if box.style['visibility'] != 'visible':
        return

    # Draw column rules.
    columns = (
        isinstance(box, boxes.BlockContainerBox) and (
            box.style['column_width'] != 'auto' or
            box.style['column_count'] != 'auto'))
    if columns and box.style['column_rule_width']:
        with stream.artifact():
            draw_column_rules(stream, box)

    # If there's a border image, that takes precedence.
    if box.style['border_image_source'][0] != 'none' and box.border_image is not None:
        with stream.artifact():
            draw_border_image(
                box, stream, box.border_image, box.style['border_image_slice'],
                box.style['border_image_repeat'], box.style['border_image_outset'],
                box.style['border_image_width'])
        return

    widths = [getattr(box, f'border_{side}_width') for side in SIDES]

    if set(widths) == {0}:
        # No border, return early.
        return

    colors = [get_color(box.style, f'border_{side}_color') for side in SIDES]
    styles = [
        colors[i].alpha and box.style[f'border_{side}_style']
        for (i, side) in enumerate(SIDES)]

    simple_style = set(styles) in ({'solid'}, {'double'})  # one style, simple lines
    single_color = len(set(colors)) == 1  # one color
    four_sides = 0 not in widths  # no 0-width border, to avoid PDF artifacts
    if simple_style and single_color and four_sides:
        # Simple case, we only draw rounded rectangles.
        with stream.artifact():
            draw_rounded_border(stream, box, styles[0], colors[0])
        return

    # We're not smart enough to find a good way to draw the borders, we must
    # draw them side by side. Order is not specified, but this one seems to be
    # close to what other browsers do.
    values = tuple(zip(SIDES, widths, colors, styles))
    for index in (2, 3, 1, 0):
        side, width, color, style = values[index]
        if width == 0 or not color:
            continue
        with stream.artifact(), stream.stacked():
            clip_border_segment(
                stream, style, width, side, box.rounded_border_box()[:4],
                widths, box.rounded_border_box()[4:])
            draw_rounded_border(stream, box, style, styled_color(style, color, side))


def draw_border_image(box, stream, image, border_slice, border_repeat, border_outset,
                      border_width):
    """Draw ``image`` as a border image for ``box`` on ``stream`` as specified."""
    # Shared by border-image-* and mask-border-*.
    width, height, ratio = image.get_intrinsic_size(
        box.style['image_resolution'], box.style['font_size'])
    intrinsic_width, intrinsic_height = replaced.default_image_sizing(
        width, height, ratio, specified_width=None, specified_height=None,
        default_width=box.border_width(), default_height=box.border_height())

    image_slice = border_slice[:4]
    should_fill = border_slice[4]

    def compute_slice_dimension(dimension, intrinsic):
        if isinstance(dimension, (int, float)):
            return min(dimension, intrinsic)
        else:
            assert dimension.unit == '%'
            return min(100, dimension.value) / 100 * intrinsic

    slice_top = compute_slice_dimension(image_slice[0], intrinsic_height)
    slice_right = compute_slice_dimension(image_slice[1], intrinsic_width)
    slice_bottom = compute_slice_dimension(image_slice[2], intrinsic_height)
    slice_left = compute_slice_dimension(image_slice[3], intrinsic_width)

    repeat_x, repeat_y = border_repeat

    x, y, w, h, tl, tr, br, bl = box.rounded_border_box()
    px, py, pw, ph, ptl, ptr, pbr, pbl = box.rounded_padding_box()
    border_left = px - x
    border_top = py - y
    border_right = w - pw - border_left
    border_bottom = h - ph - border_top

    def compute_outset_dimension(dimension, from_border):
        if dimension.unit is None:
            return dimension.value * from_border
        else:
            assert dimension.unit == 'px'
            return dimension.value

    outset_top = compute_outset_dimension(border_outset[0], border_top)
    outset_right = compute_outset_dimension(border_outset[1], border_right)
    outset_bottom = compute_outset_dimension(border_outset[2], border_bottom)
    outset_left = compute_outset_dimension(border_outset[3], border_left)

    x -= outset_left
    y -= outset_top
    w += outset_left + outset_right
    h += outset_top + outset_bottom

    def compute_width_adjustment(dimension, original, intrinsic,
                                 area_dimension):
        if dimension == 'auto':
            return intrinsic
        elif isinstance(dimension, (int, float)):
            return dimension * original
        elif dimension.unit == '%':
            return dimension.value / 100 * area_dimension
        else:
            assert dimension.unit == 'px'
            return dimension.value

    # We make adjustments to the border_* variables after handling outsets
    # because numerical outsets are relative to border-width, not
    # border-image-width. Also, the border image area that is used
    # for percentage-based border-image-width values includes any expanded
    # area due to border-image-outset.
    border_top = compute_width_adjustment(
        border_width[0], border_top, slice_top, h)
    border_right = compute_width_adjustment(
        border_width[1], border_right, slice_right, w)
    border_bottom = compute_width_adjustment(
        border_width[2], border_bottom, slice_bottom, h)
    border_left = compute_width_adjustment(
        border_width[3], border_left, slice_left, w)

    def draw_border_image_region(x, y, width, height, slice_x, slice_y, slice_width,
                                 slice_height, repeat_x='stretch', repeat_y='stretch',
                                 scale_x=None, scale_y=None):
        if 0 in (intrinsic_width, width, slice_width):
            scale_x = 0
        else:
            extra_dx = 0
            if not scale_x:
                scale_x = (height / slice_height) if height and slice_height else 1
            if repeat_x == 'repeat':
                n_repeats_x = ceil(width / slice_width / scale_x)
            elif repeat_x == 'space':
                n_repeats_x = floor(width / slice_width / scale_x)
                # Space is before the first repeat and after the last,
                # so there's one more space than repeat.
                extra_dx = (
                    (width / scale_x - n_repeats_x * slice_width) / (n_repeats_x + 1))
            elif repeat_x == 'round':
                n_repeats_x = max(1, round(width / slice_width / scale_x))
                scale_x = width / (n_repeats_x * slice_width)
            else:
                n_repeats_x = 1
                scale_x = width / slice_width

        if 0 in (intrinsic_height, height, slice_height):
            scale_y = 0
        else:
            extra_dy = 0
            if not scale_y:
                scale_y = (width / slice_width) if width and slice_width else 1
            if repeat_y == 'repeat':
                n_repeats_y = ceil(height / slice_height / scale_y)
            elif repeat_y == 'space':
                n_repeats_y = floor(height / slice_height / scale_y)
                # Space is before the first repeat and after the last,
                # so there's one more space than repeat.
                extra_dy = (
                    (height / scale_y - n_repeats_y * slice_height) / (n_repeats_y + 1))
            elif repeat_y == 'round':
                n_repeats_y = max(1, round(height / slice_height / scale_y))
                scale_y = height / (n_repeats_y * slice_height)
            else:
                n_repeats_y = 1
                scale_y = height / slice_height

        if 0 in (scale_x, scale_y):
            return scale_x, scale_y

        rendered_width = intrinsic_width * scale_x
        rendered_height = intrinsic_height * scale_y
        offset_x = rendered_width * slice_x / intrinsic_width
        offset_y = rendered_height * slice_y / intrinsic_height

        with stream.stacked():
            stream.rectangle(x, y, width, height)
            stream.clip()
            stream.end()
            stream.transform(e=x - offset_x + extra_dx, f=y - offset_y + extra_dy)
            stream.transform(a=scale_x, d=scale_y)
            for i in range(n_repeats_x):
                for j in range(n_repeats_y):
                    with stream.stacked():
                        translate_x = i * (slice_width + extra_dx)
                        translate_y = j * (slice_height + extra_dy)
                        stream.transform(e=translate_x, f=translate_y)
                        stream.rectangle(
                            offset_x / scale_x, offset_y / scale_y,
                            slice_width, slice_height)
                        stream.clip()
                        stream.end()
                        image.draw(stream, intrinsic_width, intrinsic_height, box.style)

        return scale_x, scale_y

    # Top left.
    scale_left, scale_top = draw_border_image_region(
        x, y, border_left, border_top, 0, 0, slice_left, slice_top)
    # Top right.
    draw_border_image_region(
        x + w - border_right, y, border_right, border_top,
        intrinsic_width - slice_right, 0, slice_right, slice_top)
    # Bottom right.
    scale_right, scale_bottom = draw_border_image_region(
        x + w - border_right, y + h - border_bottom, border_right, border_bottom,
        intrinsic_width - slice_right, intrinsic_height - slice_bottom,
        slice_right, slice_bottom)
    # Bottom left.
    draw_border_image_region(
        x, y + h - border_bottom, border_left, border_bottom,
        0, intrinsic_height - slice_bottom, slice_left, slice_bottom)
    if x_middle := slice_left + slice_right < intrinsic_width:
        # Top middle.
        draw_border_image_region(
            x + border_left, y, w - border_left - border_right, border_top,
            slice_left, 0, intrinsic_width - slice_left - slice_right,
            slice_top, repeat_x=repeat_x)
        # Bottom middle.
        draw_border_image_region(
            x + border_left, y + h - border_bottom,
            w - border_left - border_right, border_bottom,
            slice_left, intrinsic_height - slice_bottom,
            intrinsic_width - slice_left - slice_right, slice_bottom,
            repeat_x=repeat_x)
    if y_middle := slice_top + slice_bottom < intrinsic_height:
        # Right middle.
        draw_border_image_region(
            x + w - border_right, y + border_top,
            border_right, h - border_top - border_bottom,
            intrinsic_width - slice_right, slice_top,
            slice_right, intrinsic_height - slice_top - slice_bottom,
            repeat_y=repeat_y)
        # Left middle.
        draw_border_image_region(
            x, y + border_top, border_left, h - border_top - border_bottom,
            0, slice_top, slice_left,
            intrinsic_height - slice_top - slice_bottom,
            repeat_y=repeat_y)
    if should_fill and x_middle and y_middle:
        # Fill middle.
        draw_border_image_region(
            x + border_left, y + border_top, w - border_left - border_right,
            h - border_top - border_bottom, slice_left, slice_top,
            intrinsic_width - slice_left - slice_right,
            intrinsic_height - slice_top - slice_bottom,
            repeat_x=repeat_x, repeat_y=repeat_y,
            scale_x=scale_left or scale_right, scale_y=scale_top or scale_bottom)


def clip_border_segment(stream, style, width, side, border_box,
                        border_widths=None, radii=None):
    """Clip one segment of box border.

    The strategy is to remove the zones not needed because of the style or the
    side before painting.

    """
    bbx, bby, bbw, bbh = border_box
    (tlh, tlv), (trh, trv), (brh, brv), (blh, blv) = radii or 4 * ((0, 0),)
    bt, br, bb, bl = border_widths or 4 * (width,)

    def transition_point(x1, y1, x2, y2):
        """Get the point use for border transition.

        The extra boolean returned is ``True`` if the point is in the padding
        box (ie. the padding box is rounded).

        This point is not specified. We must be sure to be inside the rounded
        padding box, and in the zone defined in the "transition zone" allowed
        by the specification. We chose the corner of the transition zone. It's
        easy to get and gives quite good results, but it seems to be different
        from what other browsers do.

        """
        return (
            ((x1, y1), True) if abs(x1) > abs(x2) and abs(y1) > abs(y2)
            else ((x2, y2), False))

    def corner_half_length(a, b):
        """Return the length of the half of one ellipsis corner.

        Inspired by [Ramanujan, S., "Modular Equations and Approximations to
        pi" Quart. J. Pure. Appl. Math., vol. 45 (1913-1914), pp. 350-372],
        wonderfully explained by Dr Rob.

        https://mathforum.org/dr.math/faq/formulas/

        """
        x = (a - b) / (a + b)
        return pi / 8 * (a + b) * (
            1 + 3 * x ** 2 / (10 + sqrt(4 - 3 * x ** 2)))

    def draw_dash(cx, cy, width=0, height=0, r=0):
        """Draw a single dash or dot centered on cx, cy."""
        if style == 'dotted':
            ratio = r / sqrt(pi)
            stream.move_to(cx + r, cy)
            stream.curve_to(cx + r, cy + ratio, cx + ratio, cy + r, cx, cy + r)
            stream.curve_to(cx - ratio, cy + r, cx - r, cy + ratio, cx - r, cy)
            stream.curve_to(cx - r, cy - ratio, cx - ratio, cy - r, cx, cy - r)
            stream.curve_to(cx + ratio, cy - r, cx + r, cy - ratio, cx + r, cy)
            stream.close()
        elif style == 'dashed':
            stream.rectangle(cx - width / 2, cy - height / 2, width, height)

    if side == 'top':
        (px1, py1), rounded1 = transition_point(tlh, tlv, bl, bt)
        (px2, py2), rounded2 = transition_point(-trh, trv, -br, bt)
        width = bt
        way = 1
        angle = 1
        main_offset = bby
    elif side == 'right':
        (px1, py1), rounded1 = transition_point(-trh, trv, -br, bt)
        (px2, py2), rounded2 = transition_point(-brh, -brv, -br, -bb)
        width = br
        way = 1
        angle = 2
        main_offset = bbx + bbw
    elif side == 'bottom':
        (px1, py1), rounded1 = transition_point(blh, -blv, bl, -bb)
        (px2, py2), rounded2 = transition_point(-brh, -brv, -br, -bb)
        width = bb
        way = -1
        angle = 3
        main_offset = bby + bbh
    elif side == 'left':
        (px1, py1), rounded1 = transition_point(tlh, tlv, bl, bt)
        (px2, py2), rounded2 = transition_point(blh, -blv, bl, -bb)
        width = bl
        way = -1
        angle = 4
        main_offset = bbx

    if side in ('top', 'bottom'):
        a1, b1 = px1 - bl / 2, way * py1 - width / 2
        a2, b2 = -px2 - br / 2, way * py2 - width / 2
        line_length = bbw - px1 + px2
        length = bbw
        stream.move_to(bbx + bbw, main_offset)
        stream.line_to(bbx, main_offset)
        stream.line_to(bbx + px1, main_offset + py1)
        stream.line_to(bbx + bbw + px2, main_offset + py2)
    elif side in ('left', 'right'):
        a1, b1 = -way * px1 - width / 2, py1 - bt / 2
        a2, b2 = -way * px2 - width / 2, -py2 - bb / 2
        line_length = bbh - py1 + py2
        length = bbh
        stream.move_to(main_offset, bby + bbh)
        stream.line_to(main_offset, bby)
        stream.line_to(main_offset + px1, bby + py1)
        stream.line_to(main_offset + px2, bby + bbh + py2)

    if style in ('dotted', 'dashed'):
        dash = width if style == 'dotted' else 3 * width
        stream.clip(even_odd=True)
        stream.end()
        if rounded1 or rounded2:
            # At least one of the two corners is rounded.
            chl1 = corner_half_length(a1, b1)
            chl2 = corner_half_length(a2, b2)
            length = line_length + chl1 + chl2
            dash_length = round(length / dash)
            if rounded1 and rounded2:
                # 2x dashes.
                dash = length / (dash_length + dash_length % 2)
            else:
                # 2x - 1/2 dashes.
                dash = length / (dash_length + dash_length % 2 - 0.5)
            dashes1 = ceil((chl1 - dash / 2) / dash)
            dashes2 = ceil((chl2 - dash / 2) / dash)
            line = floor(line_length / dash)

            def draw_dashes(dashes, line, way, x, y, px, py, chl):
                if style == 'dotted':
                    if dashes == 0:
                        return line + 1, -1
                    elif dashes == 1:
                        return line + 1, -0.5

                    for i in range(1, dashes, 2):
                        a = ((2 * angle - way) + i * way * dash / chl) / 4 * pi
                        cx = x if side in ('top', 'bottom') else main_offset
                        cy = y if side in ('left', 'right') else main_offset
                        draw_dash(
                            cx + px - (abs(px) - dash / 2) * cos(a),
                            cy + py - (abs(py) - dash / 2) * sin(a),
                            r=(dash / 2))
                    next_a = ((2 * angle - way) + (i + 2) * way * dash / chl) / 4 * pi
                    offset = next_a / pi * 2 - angle
                    if dashes % 2:
                        line += 1
                    return line, offset

                if dashes == 0:
                    return line + 1, -1/3

                for i in range(0, dashes, 2):
                    i += 0.5  # half dash
                    angle1 = (
                        ((2 * angle - way) + i * way * dash / chl) /
                        4 * pi)
                    angle2 = (min if way > 0 else max)(
                        ((2 * angle - way) + (i + 1) * way * dash / chl) /
                        4 * pi,
                        angle * pi / 2)
                    if side in ('top', 'bottom'):
                        stream.move_to(x + px, main_offset + py)
                        stream.line_to(
                            x + px - way * px * 1 / tan(angle2), main_offset)
                        stream.line_to(
                            x + px - way * px * 1 / tan(angle1), main_offset)
                    elif side in ('left', 'right'):
                        stream.move_to(main_offset + px, y + py)
                        stream.line_to(
                            main_offset, y + py + way * py * tan(angle2))
                        stream.line_to(
                            main_offset, y + py + way * py * tan(angle1))
                    if angle2 == angle * pi / 2:
                        offset = (angle1 - angle2) / ((
                            ((2 * angle - way) + (i + 1) * way * dash / chl) /
                            4 * pi) - angle1)
                        line += 1
                        break
                else:
                    offset = 1 - (
                        (angle * pi / 2 - angle2) / (angle2 - angle1))
                return line, offset

            line, offset = draw_dashes(dashes1, line, way, bbx, bby, px1, py1, chl1)
            line = draw_dashes(
                dashes2, line, -way, bbx + bbw, bby + bbh, px2, py2, chl2)[0]

            if line_length > 1e-6:
                for i in range(0, line, 2):
                    i += offset
                    if side in ('top', 'bottom'):
                        x1 = bbx + px1 + i * dash
                        x2 = bbx + px1 + (i + 1) * dash
                        y1 = main_offset - (width if way < 0 else 0)
                        y2 = y1 + width
                    elif side in ('left', 'right'):
                        y1 = bby + py1 + i * dash
                        y2 = bby + py1 + (i + 1) * dash
                        x1 = main_offset - (width if way > 0 else 0)
                        x2 = x1 + width
                    draw_dash(
                        x1 + (x2 - x1) / 2, y1 + (y2 - y1) / 2,
                        x2 - x1, y2 - y1, width / 2)
        else:
            # No rounded corner, dashes on corners and evenly spaced between.
            number_of_spaces = floor(length / dash / 2)
            number_of_dashes = number_of_spaces + 1
            if style == 'dotted':
                dash = width
                if number_of_spaces:
                    space = (length - number_of_dashes * dash) / number_of_spaces
                else:
                    space = 0  # no space, unused
            elif style == 'dashed':
                space = dash = length / (number_of_spaces + number_of_dashes) or 1
            for i in range(number_of_dashes + 1):
                advance = i * (space + dash)
                if side == 'top':
                    cx, cy = bbx + advance + dash / 2, bby + width / 2
                    dash_width, dash_height = dash, width
                elif side == 'right':
                    cx, cy = bbx + bbw - width / 2, bby + advance + dash / 2
                    dash_width, dash_height = width, dash
                elif side == 'bottom':
                    cx, cy = bbx + advance + dash / 2, bby + bbh - width / 2
                    dash_width, dash_height = dash, width
                elif side == 'left':
                    cx, cy = bbx + width / 2, bby + advance + dash / 2
                    dash_width, dash_height = width, dash
                draw_dash(cx, cy, dash_width, dash_height, dash / 2)
    stream.clip(even_odd=True)
    stream.end()


def draw_rounded_border(stream, box, style, color):
    if style in ('ridge', 'groove'):
        stream.set_color(color[0])
        rounded_box(stream, box.rounded_padding_box())
        rounded_box(stream, box.rounded_box_ratio(1 / 2))
        stream.fill(even_odd=True)
        stream.set_color(color[1])
        rounded_box(stream, box.rounded_box_ratio(1 / 2))
        rounded_box(stream, box.rounded_border_box())
        stream.fill(even_odd=True)
        return
    stream.set_color(color)
    rounded_box(stream, box.rounded_padding_box())
    if style == 'double':
        rounded_box(stream, box.rounded_box_ratio(1 / 3))
        rounded_box(stream, box.rounded_box_ratio(2 / 3))
    rounded_box(stream, box.rounded_border_box())
    stream.fill(even_odd=True)


def draw_rect_border(stream, box, widths, style, color):
    bbx, bby, bbw, bbh = box
    bt, br, bb, bl = widths
    if style in ('ridge', 'groove'):
        stream.set_color(color[0])
        stream.rectangle(*box)
        stream.rectangle(
            bbx + bl / 2, bby + bt / 2,
            bbw - (bl + br) / 2, bbh - (bt + bb) / 2)
        stream.fill(even_odd=True)
        stream.rectangle(
            bbx + bl / 2, bby + bt / 2,
            bbw - (bl + br) / 2, bbh - (bt + bb) / 2)
        stream.rectangle(bbx + bl, bby + bt, bbw - bl - br, bbh - bt - bb)
        stream.set_color(color[1])
        stream.fill(even_odd=True)
        return
    stream.set_color(color)
    stream.rectangle(*box)
    if style == 'double':
        stream.rectangle(
            bbx + bl / 3, bby + bt / 3,
            bbw - (bl + br) / 3, bbh - (bt + bb) / 3)
        stream.rectangle(
            bbx + bl * 2 / 3, bby + bt * 2 / 3,
            bbw - (bl + br) * 2 / 3, bbh - (bt + bb) * 2 / 3)
    stream.rectangle(bbx + bl, bby + bt, bbw - bl - br, bbh - bt - bb)
    stream.fill(even_odd=True)


def draw_line(stream, x1, y1, x2, y2, thickness, style, color, offset=0):
    assert x1 == x2 or y1 == y2  # Only works for vertical or horizontal lines

    with stream.stacked():
        if style not in ('ridge', 'groove'):
            stream.set_color(color, stroke=True)

        if style == 'dashed':
            stream.set_dash([5 * thickness], offset)
        elif style == 'dotted':
            stream.set_line_cap(1)
            stream.set_dash([0, 2 * thickness], offset)

        if style == 'double':
            stream.set_line_width(thickness / 3)
            if x1 == x2:
                stream.move_to(x1 - thickness / 3, y1)
                stream.line_to(x2 - thickness / 3, y2)
                stream.move_to(x1 + thickness / 3, y1)
                stream.line_to(x2 + thickness / 3, y2)
            elif y1 == y2:
                stream.move_to(x1, y1 - thickness / 3)
                stream.line_to(x2, y2 - thickness / 3)
                stream.move_to(x1, y1 + thickness / 3)
                stream.line_to(x2, y2 + thickness / 3)
        elif style in ('ridge', 'groove'):
            stream.set_line_width(thickness / 2)
            stream.set_color(color[0], stroke=True)
            if x1 == x2:
                stream.move_to(x1 + thickness / 4, y1)
                stream.line_to(x2 + thickness / 4, y2)
            elif y1 == y2:
                stream.move_to(x1, y1 + thickness / 4)
                stream.line_to(x2, y2 + thickness / 4)
            stream.stroke()
            stream.set_color(color[1], stroke=True)
            if x1 == x2:
                stream.move_to(x1 - thickness / 4, y1)
                stream.line_to(x2 - thickness / 4, y2)
            elif y1 == y2:
                stream.move_to(x1, y1 - thickness / 4)
                stream.line_to(x2, y2 - thickness / 4)
        elif style == 'wavy':
            assert y1 == y2  # Only allowed for text decoration
            up = 1
            radius = 0.75 * thickness

            stream.rectangle(x1, y1 - 2 * radius, x2 - x1, 4 * radius)
            stream.clip()
            stream.end()

            x = x1 - offset
            stream.move_to(x, y1)
            while x < x2:
                stream.set_line_width(thickness)
                stream.curve_to(
                    x + radius / 2, y1 + up * radius,
                    x + 3 * radius / 2, y1 + up * radius,
                    x + 2 * radius, y1)
                x += 2 * radius
                up *= -1
        else:
            stream.set_line_width(thickness)
            stream.move_to(x1, y1)
            stream.line_to(x2, y2)
        stream.stroke()


def draw_outline(stream, box):
    width = box.style['outline_width']
    offset = box.style['outline_offset']
    color = get_color(box.style, 'outline_color')
    style = box.style['outline_style']
    if box.style['visibility'] == 'visible' and width and color.alpha:
        outline_box = (
            box.border_box_x() - width - offset,
            box.border_box_y() - width - offset,
            box.border_width() + 2 * width + 2 * offset,
            box.border_height() + 2 * width + 2 * offset)
        for side in SIDES:
            with stream.artifact(), stream.stacked():
                clip_border_segment(stream, style, width, side, outline_box)
                draw_rect_border(
                    stream, outline_box, 4 * (width,), style,
                    styled_color(style, color, side))

    for child in box.children:
        if isinstance(child, boxes.Box):
            draw_outline(stream, child)


def rounded_box(stream, radii):
    """Draw the path of the border radius box.

    ``widths`` is a tuple of the inner widths (top, right, bottom, left) from
    the border box. Radii are adjusted from these values. Default is (0, 0, 0,
    0).

    """
    x, y, w, h, tl, tr, br, bl = radii

    if all(0 in corner for corner in (tl, tr, br, bl)):
        # No radius, draw a rectangle
        stream.rectangle(x, y, w, h)
        return

    r = 0.45

    stream.move_to(x + tl[0], y)
    stream.line_to(x + w - tr[0], y)
    stream.curve_to(
        x + w - tr[0] * r, y, x + w, y + tr[1] * r, x + w, y + tr[1])
    stream.line_to(x + w, y + h - br[1])
    stream.curve_to(
        x + w, y + h - br[1] * r, x + w - br[0] * r, y + h, x + w - br[0],
        y + h)
    stream.line_to(x + bl[0], y + h)
    stream.curve_to(
        x + bl[0] * r, y + h, x, y + h - bl[1] * r, x, y + h - bl[1])
    stream.line_to(x, y + tl[1])
    stream.curve_to(
        x, y + tl[1] * r, x + tl[0] * r, y, x + tl[0], y)
