import svgwrite
from svgwrite.container import Group, Hyperlink
from svgwrite.shapes import Line, Rect
from svgwrite.text import Text

from django.conf import settings
from django.urls import reverse
from django.utils.http import urlencode

from utilities.utils import foreground_color
from .choices import DeviceFaceChoices
from .constants import RACK_ELEVATION_BORDER_WIDTH


__all__ = (
    'CableTraceSVG',
    'RackElevationSVG',
)


def get_device_name(device):
    if device.virtual_chassis:
        return f'{device.virtual_chassis.name}:{device.vc_position}'
    elif device.name:
        return device.name
    else:
        return str(device.device_type)


class RackElevationSVG:
    """
    Use this class to render a rack elevation as an SVG image.

    :param rack: A NetBox Rack instance
    :param user: User instance. If specified, only devices viewable by this user will be fully displayed.
    :param include_images: If true, the SVG document will embed front/rear device face images, where available
    :param base_url: Base URL for links within the SVG document. If none, links will be relative.
    """
    def __init__(self, rack, user=None, include_images=True, base_url=None):
        self.rack = rack
        self.include_images = include_images
        if base_url is not None:
            self.base_url = base_url.rstrip('/')
        else:
            self.base_url = ''

        # Determine the subset of devices within this rack that are viewable by the user, if any
        permitted_devices = self.rack.devices
        if user is not None:
            permitted_devices = permitted_devices.restrict(user, 'view')
        self.permitted_device_ids = permitted_devices.values_list('pk', flat=True)

    @staticmethod
    def _get_device_description(device):
        return '{} ({}) — {} {} ({}U) {} {}'.format(
            device.name,
            device.device_role,
            device.device_type.manufacturer.name,
            device.device_type.model,
            device.device_type.u_height,
            device.asset_tag or '',
            device.serial or ''
        )

    @staticmethod
    def _add_gradient(drawing, id_, color):
        gradient = drawing.linearGradient(
            start=(0, 0),
            end=(0, 25),
            spreadMethod='repeat',
            id_=id_,
            gradientTransform='rotate(45, 0, 0)',
            gradientUnits='userSpaceOnUse'
        )
        gradient.add_stop_color(offset='0%', color='#f7f7f7')
        gradient.add_stop_color(offset='50%', color='#f7f7f7')
        gradient.add_stop_color(offset='50%', color=color)
        gradient.add_stop_color(offset='100%', color=color)
        drawing.defs.add(gradient)

    @staticmethod
    def _setup_drawing(width, height):
        drawing = svgwrite.Drawing(size=(width, height))

        # add the stylesheet
        with open('{}/rack_elevation.css'.format(settings.STATIC_ROOT)) as css_file:
            drawing.defs.add(drawing.style(css_file.read()))

        # add gradients
        RackElevationSVG._add_gradient(drawing, 'reserved', '#c7c7ff')
        RackElevationSVG._add_gradient(drawing, 'occupied', '#d7d7d7')
        RackElevationSVG._add_gradient(drawing, 'blocked', '#ffc0c0')

        return drawing

    def _draw_device_front(self, drawing, device, start, end, text):
        name = get_device_name(device)
        if device.devicebay_count:
            name += ' ({}/{})'.format(device.get_children().count(), device.devicebay_count)

        color = device.device_role.color
        link = drawing.add(
            drawing.a(
                href='{}{}'.format(self.base_url, reverse('dcim:device', kwargs={'pk': device.pk})),
                target='_top',
                fill='black'
            )
        )
        link.set_desc(self._get_device_description(device))
        link.add(drawing.rect(start, end, style='fill: #{}'.format(color), class_='slot'))
        hex_color = '#{}'.format(foreground_color(color))
        link.add(drawing.text(str(name), insert=text, fill=hex_color))

        # Embed front device type image if one exists
        if self.include_images and device.device_type.front_image:
            image = drawing.image(
                href=device.device_type.front_image.url,
                insert=start,
                size=end,
                class_='device-image'
            )
            image.fit(scale='slice')
            link.add(image)
            link.add(drawing.text(str(name), insert=text, stroke='black',
                     stroke_width='0.2em', stroke_linejoin='round', class_='device-image-label'))
            link.add(drawing.text(str(name), insert=text, fill='white', class_='device-image-label'))

    def _draw_device_rear(self, drawing, device, start, end, text):
        link = drawing.add(
            drawing.a(
                href='{}{}'.format(self.base_url, reverse('dcim:device', kwargs={'pk': device.pk})),
                target='_top',
                fill='black'
            )
        )
        link.set_desc(self._get_device_description(device))
        link.add(drawing.rect(start, end, class_="slot blocked"))
        link.add(drawing.text(get_device_name(device), insert=text))

        # Embed rear device type image if one exists
        if self.include_images and device.device_type.rear_image:
            image = drawing.image(
                href=device.device_type.rear_image.url,
                insert=start,
                size=end,
                class_='device-image'
            )
            image.fit(scale='slice')
            link.add(image)
            link.add(drawing.text(get_device_name(device), insert=text, stroke='black',
                     stroke_width='0.2em', stroke_linejoin='round', class_='device-image-label'))
            link.add(drawing.text(get_device_name(device), insert=text, fill='white', class_='device-image-label'))

    @staticmethod
    def _draw_empty(drawing, rack, start, end, text, id_, face_id, class_, reservation):
        link_url = '{}?{}'.format(
            reverse('dcim:device_add'),
            urlencode({
                'site': rack.site.pk,
                'location': rack.location.pk if rack.location else '',
                'rack': rack.pk,
                'face': face_id,
                'position': id_
            })
        )
        link = drawing.add(
            drawing.a(href=link_url, target='_top')
        )
        if reservation:
            link.set_desc('{} — {} · {}'.format(
                reservation.description, reservation.user, reservation.created
            ))
        link.add(drawing.rect(start, end, class_=class_))
        link.add(drawing.text("add device", insert=text, class_='add-device'))

    def merge_elevations(self, face):
        elevation = self.rack.get_rack_units(face=face, expand_devices=False)
        if face == DeviceFaceChoices.FACE_REAR:
            other_face = DeviceFaceChoices.FACE_FRONT
        else:
            other_face = DeviceFaceChoices.FACE_REAR
        other = self.rack.get_rack_units(face=other_face)

        unit_cursor = 0
        for u in elevation:
            o = other[unit_cursor]
            if not u['device'] and o['device'] and o['device'].device_type.is_full_depth:
                u['device'] = o['device']
                u['height'] = 1
            unit_cursor += u.get('height', 1)

        return elevation

    def render(self, face, unit_width, unit_height, legend_width):
        """
        Return an SVG document representing a rack elevation.
        """
        drawing = self._setup_drawing(
            unit_width + legend_width + RACK_ELEVATION_BORDER_WIDTH * 2,
            unit_height * self.rack.u_height + RACK_ELEVATION_BORDER_WIDTH * 2
        )
        reserved_units = self.rack.get_reserved_units()

        unit_cursor = 0
        for ru in range(0, self.rack.u_height):
            start_y = ru * unit_height
            position_coordinates = (legend_width / 2, start_y + unit_height / 2 + RACK_ELEVATION_BORDER_WIDTH)
            unit = ru + 1 if self.rack.desc_units else self.rack.u_height - ru
            drawing.add(
                drawing.text(str(unit), position_coordinates, class_="unit")
            )

        for unit in self.merge_elevations(face):

            # Loop through all units in the elevation
            device = unit['device']
            height = unit.get('height', 1)

            # Setup drawing coordinates
            x_offset = legend_width + RACK_ELEVATION_BORDER_WIDTH
            y_offset = unit_cursor * unit_height + RACK_ELEVATION_BORDER_WIDTH
            end_y = unit_height * height
            start_cordinates = (x_offset, y_offset)
            end_cordinates = (unit_width, end_y)
            text_cordinates = (x_offset + (unit_width / 2), y_offset + end_y / 2)

            # Draw the device
            if device and device.face == face and device.pk in self.permitted_device_ids:
                self._draw_device_front(drawing, device, start_cordinates, end_cordinates, text_cordinates)
            elif device and device.device_type.is_full_depth and device.pk in self.permitted_device_ids:
                self._draw_device_rear(drawing, device, start_cordinates, end_cordinates, text_cordinates)
            elif device:
                # Devices which the user does not have permission to view are rendered only as unavailable space
                drawing.add(drawing.rect(start_cordinates, end_cordinates, class_='blocked'))
            else:
                # Draw shallow devices, reservations, or empty units
                class_ = 'slot'
                reservation = reserved_units.get(unit["id"])
                if device:
                    class_ += ' occupied'
                if reservation:
                    class_ += ' reserved'
                self._draw_empty(
                    drawing,
                    self.rack,
                    start_cordinates,
                    end_cordinates,
                    text_cordinates,
                    unit["id"],
                    face,
                    class_,
                    reservation
                )

            unit_cursor += height

        # Wrap the drawing with a border
        border_width = RACK_ELEVATION_BORDER_WIDTH
        border_offset = RACK_ELEVATION_BORDER_WIDTH / 2
        frame = drawing.rect(
            insert=(legend_width + border_offset, border_offset),
            size=(unit_width + border_width, self.rack.u_height * unit_height + border_width),
            class_='rack'
        )
        drawing.add(frame)

        return drawing


OFFSET = 0.5
PADDING = 10
LINE_HEIGHT = 20


class CableTraceSVG:
    """
    Generate a graphical representation of a CablePath in SVG format.

    :param origin: The originating termination
    :param width: Width of the generated image (in pixels)
    :param base_url: Base URL for links within the SVG document. If none, links will be relative.
    """
    def __init__(self, origin, width=400, base_url=None):
        self.origin = origin
        self.width = width
        self.base_url = base_url.rstrip('/') if base_url is not None else ''

        # Establish a cursor to track position on the y axis
        # Center edges on pixels to render sharp borders
        self.cursor = OFFSET

    @property
    def center(self):
        return self.width / 2

    @classmethod
    def _get_labels(cls, instance):
        """
        Return a list of text labels for the given instance based on model type.
        """
        labels = [str(instance)]
        if instance._meta.model_name == 'device':
            labels.append(f'{instance.device_type.manufacturer} {instance.device_type}')
            location_label = f'{instance.site}'
            if instance.location:
                location_label += f' / {instance.location}'
            if instance.rack:
                location_label += f' / {instance.rack}'
            labels.append(location_label)
        elif instance._meta.model_name == 'circuit':
            labels[0] = f'Circuit {instance}'
            labels.append(instance.provider)
        elif instance._meta.model_name == 'circuittermination':
            if instance.xconnect_id:
                labels.append(f'{instance.xconnect_id}')
        elif instance._meta.model_name == 'providernetwork':
            labels.append(instance.provider)

        return labels

    @classmethod
    def _get_color(cls, instance):
        """
        Return the appropriate fill color for an object within a cable path.
        """
        if hasattr(instance, 'parent_object'):
            # Termination
            return 'f0f0f0'
        if hasattr(instance, 'device_role'):
            # Device
            return instance.device_role.color
        else:
            # Other parent object
            return 'e0e0e0'

    def _draw_box(self, width, color, url, labels, y_indent=0, padding_multiplier=1, radius=10):
        """
        Return an SVG Link element containing a Rect and one or more text labels representing a
        parent object or cable termination point.

        :param width: Box width
        :param color: Box fill color
        :param url: Hyperlink URL
        :param labels: Iterable of text labels
        :param y_indent: Vertical indent (for overlapping other boxes) (default: 0)
        :param padding_multiplier: Add extra vertical padding (default: 1)
        :param radius: Box corner radius (default: 10)
        """
        self.cursor -= y_indent

        # Create a hyperlink
        link = Hyperlink(href=f'{self.base_url}{url}', target='_blank')

        # Add the box
        position = (
            OFFSET + (self.width - width) / 2,
            self.cursor
        )
        height = PADDING * padding_multiplier \
            + LINE_HEIGHT * len(labels) \
            + PADDING * padding_multiplier
        box = Rect(position, (width - 2, height), rx=radius, class_='parent-object', style=f'fill: #{color}')
        link.add(box)
        self.cursor += PADDING * padding_multiplier

        # Add text label(s)
        for i, label in enumerate(labels):
            self.cursor += LINE_HEIGHT
            text_coords = (self.center, self.cursor - LINE_HEIGHT / 2)
            text_color = f'#{foreground_color(color, dark="303030")}'
            text = Text(label, insert=text_coords, fill=text_color, class_='bold' if not i else [])
            link.add(text)

        self.cursor += PADDING * padding_multiplier

        return link

    def _draw_cable(self, color, url, labels):
        """
        Return an SVG group containing a line element and text labels representing a Cable.

        :param color: Cable (line) color
        :param url: Hyperlink URL
        :param labels: Iterable of text labels
        """
        group = Group(class_='connector')

        # Draw a "shadow" line to give the cable a border
        start = (OFFSET + self.center, self.cursor)
        height = PADDING * 2 + LINE_HEIGHT * len(labels) + PADDING * 2
        end = (start[0], start[1] + height)
        cable_shadow = Line(start=start, end=end, class_='cable-shadow')
        group.add(cable_shadow)

        # Draw the cable
        cable = Line(start=start, end=end, style=f'stroke: #{color}')
        group.add(cable)

        self.cursor += PADDING * 2

        # Add link
        link = Hyperlink(href=f'{self.base_url}{url}', target='_blank')

        # Add text label(s)
        for i, label in enumerate(labels):
            self.cursor += LINE_HEIGHT
            text_coords = (self.center + PADDING * 2, self.cursor - LINE_HEIGHT / 2)
            text = Text(label, insert=text_coords, class_='bold' if not i else [])
            link.add(text)

        group.add(link)
        self.cursor += PADDING * 2

        return group

    def _draw_wirelesslink(self, url, labels):
        """
        Draw a line with labels representing a WirelessLink.

        :param url: Hyperlink URL
        :param labels: Iterable of text labels
        """
        group = Group(class_='connector')

        # Draw the wireless link
        start = (OFFSET + self.center, self.cursor)
        height = PADDING * 2 + LINE_HEIGHT * len(labels) + PADDING * 2
        end = (start[0], start[1] + height)
        line = Line(start=start, end=end, class_='wireless-link')
        group.add(line)

        self.cursor += PADDING * 2

        # Add link
        link = Hyperlink(href=f'{self.base_url}{url}', target='_blank')

        # Add text label(s)
        for i, label in enumerate(labels):
            self.cursor += LINE_HEIGHT
            text_coords = (self.center + PADDING * 2, self.cursor - LINE_HEIGHT / 2)
            text = Text(label, insert=text_coords, class_='bold' if not i else [])
            link.add(text)

        group.add(link)
        self.cursor += PADDING * 2

        return group

    def _draw_attachment(self):
        """
        Return an SVG group containing a line element and "Attachment" label.
        """
        group = Group(class_='connector')

        # Draw attachment (line)
        start = (OFFSET + self.center, OFFSET + self.cursor)
        height = PADDING * 2 + LINE_HEIGHT + PADDING * 2
        end = (start[0], start[1] + height)
        line = Line(start=start, end=end, class_='attachment')
        group.add(line)
        self.cursor += PADDING * 4

        return group

    def render(self):
        """
        Return an SVG document representing a cable trace.
        """
        from dcim.models import Cable
        from wireless.models import WirelessLink

        traced_path = self.origin.trace()

        # Prep elements list
        parent_objects = []
        terminations = []
        connectors = []

        # Iterate through each (term, cable, term) segment in the path
        for i, segment in enumerate(traced_path):
            near_end, connector, far_end = segment

            # Near end parent
            if i == 0:
                # If this is the first segment, draw the originating termination's parent object
                parent_object = self._draw_box(
                    width=self.width,
                    color=self._get_color(near_end.parent_object),
                    url=near_end.parent_object.get_absolute_url(),
                    labels=self._get_labels(near_end.parent_object),
                    padding_multiplier=2
                )
                parent_objects.append(parent_object)

            # Near end termination
            if near_end is not None:
                termination = self._draw_box(
                    width=self.width * .8,
                    color=self._get_color(near_end),
                    url=near_end.get_absolute_url(),
                    labels=self._get_labels(near_end),
                    y_indent=PADDING,
                    radius=5
                )
                terminations.append(termination)

            # Connector (a Cable or WirelessLink)
            if connector is not None:

                # Cable
                if type(connector) is Cable:
                    connector_labels = [
                        f'Cable {connector}',
                        connector.get_status_display()
                    ]
                    if connector.type:
                        connector_labels.append(connector.get_type_display())
                    if connector.length and connector.length_unit:
                        connector_labels.append(f'{connector.length} {connector.get_length_unit_display()}')
                    cable = self._draw_cable(
                        color=connector.color or '000000',
                        url=connector.get_absolute_url(),
                        labels=connector_labels
                    )
                    connectors.append(cable)

                # WirelessLink
                elif type(connector) is WirelessLink:
                    connector_labels = [
                        f'Wireless link {connector}',
                        connector.get_status_display()
                    ]
                    if connector.ssid:
                        connector_labels.append(connector.ssid)
                    wirelesslink = self._draw_wirelesslink(
                        url=connector.get_absolute_url(),
                        labels=connector_labels
                    )
                    connectors.append(wirelesslink)

                # Far end termination
                termination = self._draw_box(
                    width=self.width * .8,
                    color=self._get_color(far_end),
                    url=far_end.get_absolute_url(),
                    labels=self._get_labels(far_end),
                    radius=5
                )
                terminations.append(termination)

                # Far end parent
                parent_object = self._draw_box(
                    width=self.width,
                    color=self._get_color(far_end.parent_object),
                    url=far_end.parent_object.get_absolute_url(),
                    labels=self._get_labels(far_end.parent_object),
                    y_indent=PADDING,
                    padding_multiplier=2
                )
                parent_objects.append(parent_object)

            elif far_end:

                # Attachment
                attachment = self._draw_attachment()
                connectors.append(attachment)

                # ProviderNetwork
                parent_object = self._draw_box(
                    width=self.width,
                    color=self._get_color(far_end),
                    url=far_end.get_absolute_url(),
                    labels=self._get_labels(far_end),
                    padding_multiplier=2
                )
                parent_objects.append(parent_object)

        # Determine drawing size
        self.drawing = svgwrite.Drawing(
            size=(self.width, self.cursor + 2)
        )

        # Attach CSS stylesheet
        with open(f'{settings.STATIC_ROOT}/cable_trace.css') as css_file:
            self.drawing.defs.add(self.drawing.style(css_file.read()))

        # Add elements to the drawing in order of depth (Z axis)
        for element in connectors + parent_objects + terminations:
            self.drawing.add(element)

        return self.drawing
