"""
    weasyprint.svg.defs
    -------------------

    Parse and draw definitions: gradients, patterns, masks, uses…

"""

from itertools import cycle
from math import ceil, hypot

import pydyf

from .bounding_box import is_valid_bounding_box
from .utils import color, parse_url, size, transform


def use(svg, node, font_size):
    """Draw use tags."""
    from . import NOT_INHERITED_ATTRIBUTES, SVG

    x, y = svg.point(node.get('x'), node.get('y'), font_size)

    for attribute in ('x', 'y', 'viewBox', 'mask'):
        if attribute in node.attrib:
            del node.attrib[attribute]

    parsed_url = parse_url(node.get_href(svg.url))
    if parsed_url.fragment and parsed_url[:3] == parse_url(svg.url)[:3]:
        if parsed_url.fragment in svg.use_cache:
            tree = svg.use_cache[parsed_url.fragment].copy()
        else:
            try:
                tree = svg.tree.get_child(parsed_url.fragment).copy()
            except Exception:
                return
            else:
                svg.use_cache[parsed_url.fragment] = tree
    else:
        url = parsed_url.geturl()
        try:
            bytestring_svg = svg.url_fetcher(url)
            use_svg = SVG(bytestring_svg, url)
        except Exception:
            return
        else:
            use_svg.get_intrinsic_size(font_size)
            tree = use_svg.tree

    if tree.tag in ('svg', 'symbol'):
        # Explicitely specified
        # http://www.w3.org/TR/SVG11/struct.html#UseElement
        tree._etree_node.tag = 'svg'
        if 'width' in node.attrib and 'height' in node.attrib:
            tree.attrib['width'] = node.attrib['width']
            tree.attrib['height'] = node.attrib['height']

    # Cascade
    for key, value in node.attrib.items():
        if key not in NOT_INHERITED_ATTRIBUTES:
            if key not in tree.attrib:
                tree.attrib[key] = value

    svg.stream.push_state()
    svg.stream.transform(e=x, f=y)
    svg.draw_node(tree, font_size)
    svg.stream.pop_state()


def draw_gradient_or_pattern(svg, node, name, font_size, opacity, stroke):
    """Draw given gradient or pattern."""
    if name in svg.gradients:
        return draw_gradient(
            svg, node, svg.gradients[name], font_size, opacity, stroke)
    elif name in svg.patterns:
        return draw_pattern(
            svg, node, svg.patterns[name], font_size, opacity, stroke)


def draw_gradient(svg, node, gradient, font_size, opacity, stroke):
    """Draw given gradient node."""
    # TODO: merge with Gradient.draw
    from ..document import Matrix

    positions = []
    colors = []
    for child in gradient:
        positions.append(max(
            positions[-1] if positions else 0,
            size(child.get('offset'), font_size, 1)))
        stop_opacity = float(child.get('stop-opacity', 1)) * opacity
        stop_color = color(child.get('stop-color', 'black'))
        if stop_opacity < 1:
            stop_color = tuple(
                stop_color[:3] + (stop_color[3] * stop_opacity,))
        colors.append(stop_color)

    if not colors:
        return False
    elif len(colors) == 1:
        red, green, blue, alpha = colors[0]
        svg.stream.set_color_rgb(red, green, blue)
        if alpha != 1:
            svg.stream.set_alpha(alpha, stroke=stroke)
        return True

    bounding_box = svg.calculate_bounding_box(node, font_size, stroke)
    if not is_valid_bounding_box(bounding_box):
        return False
    x, y = bounding_box[0], bounding_box[1]
    matrix = Matrix(e=x, f=y)
    if gradient.get('gradientUnits') == 'userSpaceOnUse':
        width, height = svg.inner_width, svg.inner_height
    else:
        width, height = bounding_box[2], bounding_box[3]

    spread = gradient.get('spreadMethod', 'pad')
    if spread in ('repeat', 'reflect'):
        if positions[0] > 0:
            positions.insert(0, 0)
            colors.insert(0, colors[0])
        if positions[-1] < 1:
            positions.append(1)
            colors.append(colors[-1])
    else:
        # Add explicit colors at boundaries if needed, because PDF doesn’t
        # extend color stops that are not displayed
        if positions[0] == positions[1]:
            if gradient.tag == 'radialGradient':
                # Avoid negative radius for radial gradients
                positions.insert(0, 0)
            else:
                positions.insert(0, positions[0] - 1)
            colors.insert(0, colors[0])
        if positions[-2] == positions[-1]:
            positions.append(positions[-1] + 1)
            colors.append(colors[-1])

    if gradient.tag == 'linearGradient':
        shading_type = 2
        x1, y1 = (
            size(gradient.get('x1', 0), font_size, 1),
            size(gradient.get('y1', 0), font_size, 1))
        x2, y2 = (
            size(gradient.get('x2', '100%'), font_size, 1),
            size(gradient.get('y2', 0), font_size, 1))
        if gradient.get('gradientUnits') == 'userSpaceOnUse':
            x1 -= x
            y1 -= y
            x2 -= x
            y2 -= y
        else:
            length = min(width, height)
            x1 *= length
            y1 *= length
            x2 *= length
            y2 *= length
            a = (width / height) if height < width else 1
            d = (height / width) if height > width else 1
            matrix = Matrix(a=a, d=d) @ matrix
        positions, colors, coords = spread_linear_gradient(
            spread, positions, colors, x1, y1, x2, y2)
    else:
        assert gradient.tag == 'radialGradient'
        shading_type = 3
        cx, cy = (
            size(gradient.get('cx', '50%'), font_size, 1),
            size(gradient.get('cy', '50%'), font_size, 1))
        r = size(gradient.get('r', '50%'), font_size, 1)
        fx, fy = (
            size(gradient.get('fx', cx), font_size, width),
            size(gradient.get('fy', cy), font_size, height))
        fr = size(gradient.get('fr', 0), font_size, 1)
        if gradient.get('gradientUnits') == 'userSpaceOnUse':
            cx -= x
            cy -= y
            fx -= x
            fy -= y
        else:
            length = min(width, height)
            cx *= length
            cy *= length
            r *= length
            fx *= length
            fy *= length
            fr *= length
            a = (width / height) if height < width else 1
            d = (height / width) if height > width else 1
            matrix = Matrix(a=a, d=d) @ matrix
        positions, colors, coords = spread_radial_gradient(
            spread, positions, colors, fx, fy, fr, cx, cy, r, width, height)

    alphas = [color[3] for color in colors]
    alpha_couples = [
        (alphas[i], alphas[i + 1])
        for i in range(len(alphas) - 1)]
    color_couples = [
        [colors[i][:3], colors[i + 1][:3], 1]
        for i in range(len(colors) - 1)]

    # Premultiply colors
    for i, alpha in enumerate(alphas):
        if alpha == 0:
            if i > 0:
                color_couples[i - 1][1] = color_couples[i - 1][0]
            if i < len(colors) - 1:
                color_couples[i][0] = color_couples[i][1]
    for i, (a0, a1) in enumerate(alpha_couples):
        if 0 not in (a0, a1) and (a0, a1) != (1, 1):
            color_couples[i][2] = a0 / a1

    if 'gradientTransform' in gradient.attrib:
        transform_matrix = transform(
            gradient.get('gradientTransform'), font_size,
            svg.normalized_diagonal)
        matrix = transform_matrix @ matrix

    matrix = matrix @ svg.stream.ctm
    pattern = svg.stream.add_pattern(width, height, width, height, matrix)
    group = pattern.add_group([0, 0, width, height])

    shading = group.add_shading()
    shading['ShadingType'] = shading_type
    shading['ColorSpace'] = '/DeviceRGB'
    shading['Domain'] = pydyf.Array([positions[0], positions[-1]])
    shading['Coords'] = pydyf.Array(coords)
    shading['Function'] = pydyf.Dictionary({
        'FunctionType': 3,
        'Domain': pydyf.Array([positions[0], positions[-1]]),
        'Encode': pydyf.Array((len(colors) - 1) * [0, 1]),
        'Bounds': pydyf.Array(positions[1:-1]),
        'Functions': pydyf.Array([
            pydyf.Dictionary({
                'FunctionType': 2,
                'Domain': pydyf.Array([positions[0], positions[-1]]),
                'C0': pydyf.Array(c0),
                'C1': pydyf.Array(c1),
                'N': n,
            }) for c0, c1, n in color_couples
        ]),
    })
    if spread not in ('repeat', 'reflect'):
        shading['Extend'] = pydyf.Array([b'true', b'true'])

    if any(alpha != 1 for alpha in alphas):
        alpha_stream = group.add_group(
            [0, 0, svg.concrete_width, svg.concrete_height])
        state = pydyf.Dictionary({
            'Type': '/ExtGState',
            'SMask': pydyf.Dictionary({
                'Type': '/Mask',
                'S': '/Luminosity',
                'G': alpha_stream,
            }),
            'ca': 1,
            'AIS': 'false',
        })
        group.set_state(state)

        alpha_shading = alpha_stream.add_shading()
        alpha_shading['ShadingType'] = shading_type
        alpha_shading['ColorSpace'] = '/DeviceGray'
        alpha_shading['Domain'] = pydyf.Array(
            [positions[0], positions[-1]])
        alpha_shading['Coords'] = pydyf.Array(coords)
        alpha_shading['Function'] = pydyf.Dictionary({
            'FunctionType': 3,
            'Domain': pydyf.Array([positions[0], positions[-1]]),
            'Encode': pydyf.Array((len(colors) - 1) * [0, 1]),
            'Bounds': pydyf.Array(positions[1:-1]),
            'Functions': pydyf.Array([
                pydyf.Dictionary({
                    'FunctionType': 2,
                    'Domain': pydyf.Array([0, 1]),
                    'C0': pydyf.Array([c0]),
                    'C1': pydyf.Array([c1]),
                    'N': 1,
                }) for c0, c1 in alpha_couples
            ]),
        })
        if spread not in ('repeat', 'reflect'):
            alpha_shading['Extend'] = pydyf.Array([b'true', b'true'])
        alpha_stream.stream = [f'/{alpha_shading.id} sh']

    group.shading(shading.id)
    pattern.draw_x_object(group.id)
    svg.stream.color_space('Pattern', stroke=stroke)
    svg.stream.set_color_special(pattern.id, stroke=stroke)
    return True


def spread_linear_gradient(spread, positions, colors, x1, y1, x2, y2):
    """Repeat linear gradient."""
    # TODO: merge with LinearGradient.layout
    from ..images import gradient_average_color, normalize_stop_positions

    first, last, positions = normalize_stop_positions(positions)
    if spread in ('repeat', 'reflect'):
        # Render as a solid color if the first and last positions are equal
        # See https://drafts.csswg.org/css-images-3/#repeating-gradients
        if first == last:
            average_color = gradient_average_color(colors, positions)
            return 1, 'solid', None, [], [average_color]

        # Define defined gradient length and steps between positions
        stop_length = last - first
        assert stop_length > 0
        position_steps = [
            positions[i + 1] - positions[i]
            for i in range(len(positions) - 1)]

        # Create cycles used to add colors
        if spread == 'repeat':
            next_steps = cycle([0] + position_steps)
            next_colors = cycle(colors)
            previous_steps = cycle([0] + position_steps[::-1])
            previous_colors = cycle(colors[::-1])
        else:
            assert spread == 'reflect'
            next_steps = cycle(
                [0] + position_steps[::-1] + [0] + position_steps)
            next_colors = cycle(colors[::-1] + colors)
            previous_steps = cycle(
                [0] + position_steps + [0] + position_steps[::-1])
            previous_colors = cycle(colors + colors[::-1])

        # Add colors after last step
        while last < hypot(x2 - x1, y2 - y1):
            step = next(next_steps)
            colors.append(next(next_colors))
            positions.append(positions[-1] + step)
            last += step * stop_length

        # Add colors before last step
        while first > 0:
            step = next(previous_steps)
            colors.insert(0, next(previous_colors))
            positions.insert(0, positions[0] - step)
            first -= step * stop_length

    x1, x2 = x1 + (x2 - x1) * first, x1 + (x2 - x1) * last
    y1, y2 = y1 + (y2 - y1) * first, y1 + (y2 - y1) * last
    coords = (x1, y1, x2, y2)
    return positions, colors, coords


def spread_radial_gradient(spread, positions, colors, fx, fy, fr, cx, cy, r,
                           width, height):
    """Repeat radial gradient."""
    # TODO: merge with RadialGradient._repeat
    from ..images import gradient_average_color, normalize_stop_positions

    first, last, positions = normalize_stop_positions(positions)
    fr, r = fr + (r - fr) * first, fr + (r - fr) * last

    if spread in ('repeat', 'reflect'):
        # Keep original lists and values, they’re useful
        original_colors = colors.copy()
        original_positions = positions.copy()
        gradient_length = r - fr

        # Get the maximum distance between the center and the corners, to find
        # how many times we have to repeat the colors outside
        max_distance = max(
            hypot(width - fx, height - fy),
            hypot(width - fx, -fy),
            hypot(-fx, height - fy),
            hypot(-fx, -fy))
        repeat_after = ceil((max_distance - r) / gradient_length)
        if repeat_after > 0:
            # Repeat colors and extrapolate positions
            repeat = 1 + repeat_after
            if spread == 'repeat':
                colors *= repeat
            else:
                assert spread == 'reflect'
                colors = []
                for i in range(repeat):
                    colors += original_colors[::1 if i % 2 else -1]
            positions = [
                i + position for i in range(repeat) for position in positions]
            r += gradient_length * repeat_after

        if fr == 0:
            # Inner circle has 0 radius, no need to repeat inside, return
            coords = (fx, fy, fr, cx, cy, r)
            return positions, colors, coords

        # Find how many times we have to repeat the colors inside
        repeat_before = fr / gradient_length

        # Set the inner circle size to 0
        fr = 0

        # Find how many times the whole gradient can be repeated
        full_repeat = int(repeat_before)
        if full_repeat:
            # Repeat colors and extrapolate positions
            if spread == 'repeat':
                colors += original_colors * full_repeat
            else:
                assert spread == 'reflect'
                for i in range(full_repeat):
                    colors += original_colors[::1 if i % 2 else -1]
            positions = [
                i - full_repeat + position for i in range(full_repeat)
                for position in original_positions] + positions

        # Find the ratio of gradient that must be added to reach the center
        partial_repeat = repeat_before - full_repeat
        if partial_repeat == 0:
            # No partial repeat, return
            coords = (fx, fy, fr, cx, cy, r)
            return positions, colors, coords

        # Iterate through positions in reverse order, from the outer
        # circle to the original inner circle, to find positions from
        # the inner circle (including full repeats) to the center
        assert (original_positions[0], original_positions[-1]) == (0, 1)
        assert 0 < partial_repeat < 1
        reverse = original_positions[::-1]
        ratio = 1 - partial_repeat
        for i, position in enumerate(reverse, start=1):
            if position == ratio:
                # The center is a color of the gradient, truncate original
                # colors and positions and prepend them
                colors = original_colors[-i:] + colors
                new_positions = [
                    position - full_repeat - 1
                    for position in original_positions[-i:]]
                positions = new_positions + positions
                coords = (fx, fy, fr, cx, cy, r)
                return positions, colors, coords
            if position < ratio:
                # The center is between two colors of the gradient,
                # define the center color as the average of these two
                # gradient colors
                color = original_colors[-i]
                next_color = original_colors[-(i - 1)]
                next_position = original_positions[-(i - 1)]
                average_colors = [color, color, next_color, next_color]
                average_positions = [position, ratio, ratio, next_position]
                zero_color = gradient_average_color(
                    average_colors, average_positions)
                colors = [zero_color] + original_colors[-(i - 1):] + colors
                new_positions = [
                    position - 1 - full_repeat for position
                    in original_positions[-(i - 1):]]
                positions = (
                    [ratio - 1 - full_repeat] + new_positions + positions)

    coords = (fx, fy, fr, cx, cy, r)
    return positions, colors, coords


def draw_pattern(svg, node, pattern, font_size, opacity, stroke):
    """Draw given gradient node."""
    from ..document import Matrix
    from . import Pattern

    pattern._etree_node.tag = 'svg'

    bounding_box = svg.calculate_bounding_box(node, font_size, stroke)
    if not is_valid_bounding_box(bounding_box):
        return False
    x, y = bounding_box[0], bounding_box[1]
    matrix = Matrix(e=x, f=y)
    if pattern.get('patternUnits') == 'userSpaceOnUse':
        width, height = svg.inner_width, svg.inner_height
    else:
        width, height = bounding_box[2], bounding_box[3]

    if pattern.get('patternUnits') == 'userSpaceOnUse':
        x = size(pattern.get('x'), font_size, 1)
        y = size(pattern.get('y'), font_size, 1)
        pattern_width = size(pattern.get('width', 0), font_size, 1)
        pattern_height = size(pattern.get('height', 0), font_size, 1)
    else:
        x = size(pattern.get('x'), font_size, 1) * width
        y = size(pattern.get('y'), font_size, 1) * height
        pattern_width = (
            size(pattern.attrib.pop('width', '1'), font_size, 1) * width)
        pattern_height = (
            size(pattern.attrib.pop('height', '1'), font_size, 1) * height)
        if 'viewBox' not in pattern:
            pattern.attrib['width'] = pattern_width
            pattern.attrib['height'] = pattern_height
            if pattern.get('patternContentUnits') == 'objectBoundingBox':
                pattern.attrib['transform'] = f'scale({width}, {height})'

    # Fail if pattern has an invalid size
    if pattern_width == 0 or pattern_height == 0:
        return False

    if 'patternTransform' in pattern.attrib:
        transform_matrix = transform(
            pattern.get('patternTransform'), font_size, svg.inner_diagonal)
        matrix = transform_matrix @ matrix

    matrix = matrix @ svg.stream.ctm
    stream_pattern = svg.stream.add_pattern(
        pattern_width, pattern_height, pattern_width, pattern_height, matrix)
    stream_pattern.set_alpha(opacity)

    group = stream_pattern.add_group([0, 0, pattern_width, pattern_height])
    Pattern(pattern, svg).draw(
        group, pattern_width, pattern_height, svg.base_url,
        svg.url_fetcher, svg.context)
    stream_pattern.draw_x_object(group.id)
    svg.stream.color_space('Pattern', stroke=stroke)
    svg.stream.set_color_special(stream_pattern.id, stroke=stroke)
    return True


def apply_filters(svg, node, filter_node, font_size):
    """Apply filters defined in given filter node."""
    for child in filter_node:
        if child.tag == 'feOffset':
            if filter_node.get('primitiveUnits') == 'objectBoundingBox':
                bounding_box = svg.calculate_bounding_box(node, font_size)
                if is_valid_bounding_box(bounding_box):
                    _, _, width, height = bounding_box
                    dx = size(child.get('dx', 0), font_size, 1) * width
                    dy = size(child.get('dy', 0), font_size, 1) * height
                else:
                    dx = dy = 0
            else:
                dx, dy = svg.point(
                    child.get('dx', 0), child.get('dy', 0), font_size)
            svg.stream.transform(e=dx, f=dy)
        elif child.tag == 'feBlend':
            mode = child.get('mode', 'normal')
            mode = mode.replace('-', ' ').title().replace(' ', '')
            blend_mode = pydyf.Dictionary({
                'Type': '/ExtGState',
                'BM': f'/{mode}',
            })
            svg.stream.set_state(blend_mode)


def paint_mask(svg, node, mask, font_size):
    """Apply given mask node."""
    mask._etree_node.tag = 'g'

    if mask.get('maskUnits') == 'userSpaceOnUse':
        width_ref, height_ref = svg.inner_width, svg.inner_height
    else:
        width_ref, height_ref = svg.point(
            node.get('width'), node.get('height'), font_size)

    mask.attrib['x'] = size(mask.get('x', '-10%'), font_size, width_ref)
    mask.attrib['y'] = size(mask.get('y', '-10%'), font_size, height_ref)
    mask.attrib['height'] = size(
        mask.get('height', '120%'), font_size, height_ref)
    mask.attrib['width'] = size(
        mask.get('width', '120%'), font_size, width_ref)

    if mask.get('maskUnits') == 'userSpaceOnUse':
        x, y = mask.get('x'), mask.get('y')
        width, height = mask.get('width'), mask.get('height')
        mask.attrib['viewBox'] = f'{x} {y} {width} {height}'
    else:
        x, y = 0, 0
        width, height = width_ref, height_ref

    alpha_stream = svg.stream.add_group([x, y, width, height])
    state = pydyf.Dictionary({
        'Type': '/ExtGState',
        'SMask': pydyf.Dictionary({
            'Type': '/Mask',
            'S': '/Luminance',
            'G': alpha_stream,
        }),
        'ca': 1,
        'AIS': 'false',
    })
    svg.stream.set_state(state)

    svg_stream = svg.stream
    svg.stream = alpha_stream
    svg.draw_node(mask, font_size)
    svg.stream = svg_stream


def clip_path(svg, node, font_size):
    """Store a clip path definition."""
    if 'id' in node.attrib:
        svg.paths[node.attrib['id']] = node
