# TODO: Allow toggling of layers with 1-9 keypresses
# TODO: alias font should be slightly transparent


import sys
import warnings

import gdspy
import numpy as np

import phidl
from phidl.device_layout import (
    CellArray,
    Device,
    DeviceReference,
    Layer,
    Path,
    Polygon,
    _rotate_points,
)

_SUBPORT_RGB = (0, 120, 120)
_PORT_RGB = (190, 0, 0)


try:
    from PyQt5 import QtCore, QtGui
    from PyQt5.QtCore import (
        QCoreApplication,
        QLineF,
        QPoint,
        QPointF,
        QRect,
        QRectF,
        QSize,
        QSizeF,
        Qt,
    )
    from PyQt5.QtGui import QColor, QPen, QPolygonF
    from PyQt5.QtWidgets import (
        QApplication,
        QGraphicsItem,
        QGraphicsScene,
        QGraphicsView,
        QLabel,
        QMainWindow,
        QMessageBox,
        QRubberBand,
    )

    PORT_COLOR = QColor(*_PORT_RGB)
    SUBPORT_COLOR = QColor(*_SUBPORT_RGB)
    OUTLINE_PEN = QColor(200, 200, 200)
    qt_imported = True
except ImportError:
    QMainWindow = object
    QGraphicsView = object
    qt_imported = False

_quickplot_options = dict(
    show_ports=True,
    show_subports=True,
    label_aliases=False,
    new_window=False,
    blocking=False,
    zoom_factor=1.4,
    interactive_zoom=None,
)


def _zoom_factory(axis, scale_factor=1.4):
    """returns zooming functionality to axis.
    From https://gist.github.com/tacaswell/3144287"""

    def zoom_fun(event, ax, scale):
        """zoom when scrolling"""
        if event.inaxes == axis:
            scale_factor = np.power(scale, -event.step)
            xdata = event.xdata
            ydata = event.ydata
            x_left = xdata - ax.get_xlim()[0]
            x_right = ax.get_xlim()[1] - xdata
            y_top = ydata - ax.get_ylim()[0]
            y_bottom = ax.get_ylim()[1] - ydata

            ax.set_xlim([xdata - x_left * scale_factor, xdata + x_right * scale_factor])
            ax.set_ylim([ydata - y_top * scale_factor, ydata + y_bottom * scale_factor])
            ax.figure.canvas.draw()
            # Update toolbar so back/forward buttons work
            fig.canvas.toolbar.push_current()

    fig = axis.get_figure()
    fig.canvas.mpl_connect(
        "scroll_event", lambda event: zoom_fun(event, axis, scale_factor)
    )


_qp_objects = {}


def _rectangle_selector_factory(fig, ax):
    from matplotlib.widgets import RectangleSelector

    def line_select_callback(eclick, erelease):
        x1, y1 = eclick.xdata, eclick.ydata
        x2, y2 = erelease.xdata, erelease.ydata
        left = min(x1, x2)
        right = max(x1, x2)
        bottom = min(y1, y2)
        top = max(y1, y2)
        ax.set_xlim([left, right])
        ax.set_ylim([bottom, top])
        ax.figure.canvas.draw()
        # Update toolbar so back/forward buttons work
        fig.canvas.toolbar.push_current()

    rs = RectangleSelector(
        ax,
        line_select_callback,
        drawtype="box",
        useblit=True,
        button=[1, 3],
        minspanx=5,
        minspany=5,
        spancoords="pixels",
        interactive=False,
    )
    return rs


def set_quickplot_options(
    show_ports=None,
    show_subports=None,
    label_aliases=None,
    new_window=None,
    blocking=None,
    zoom_factor=None,
    interactive_zoom=None,
):
    """Sets plotting options for quickplot()

    Parameters
    ----------
    show_ports : bool
        Sets whether ports are drawn
    show_subports : bool
        Sets whether subports (ports that belong to references) are drawn
    label_aliases : bool
        Sets whether aliases are labeled with a text name
    new_window : bool
        If True, each call to quickplot() will generate a separate window
    blocking : bool
        If True, calling quickplot() will pause execution of ("block") the
        remainder of the python code until the quickplot() window is closed.  If
        False, the window will be opened and code will continue to run.
    zoom_factor : float
        Sets the scaling factor when zooming the quickplot window with the
        mousewheel/trackpad
    interactive_zoom : bool
        Enables/disables the ability to use mousewheel/trackpad to zoom
    """
    if show_ports is not None:
        _quickplot_options["show_ports"] = show_ports
    if show_subports is not None:
        _quickplot_options["show_subports"] = show_subports
    if label_aliases is not None:
        _quickplot_options["label_aliases"] = label_aliases
    if new_window is not None:
        _quickplot_options["new_window"] = new_window
    if blocking is not None:
        _quickplot_options["blocking"] = blocking
    if zoom_factor is not None:
        _quickplot_options["zoom_factor"] = zoom_factor
    if interactive_zoom is not None:
        _quickplot_options["interactive_zoom"] = interactive_zoom


def quickplot(items):  # noqa: C901
    """Takes a list of devices/references/polygons or single one of those, and
    plots them. Use `set_quickplot_options()` to modify the viewer behavior
    (e.g. displaying ports, creating new windows, etc)

    Parameters
    ----------
    items : PHIDL object or list of PHIDL objects
        The item(s) which are to be plotted

    Examples
    --------
    >>> R = pg.rectangle()
    >>> quickplot(R)

    >>> R = pg.rectangle()
    >>> E = pg.ellipse()
    >>> quickplot([R, E])
    """

    try:
        from matplotlib import pyplot as plt

        matplotlib_imported = True
    except ImportError:
        matplotlib_imported = False

    # Override default options with _quickplot_options
    show_ports = _quickplot_options["show_ports"]
    show_subports = _quickplot_options["show_subports"]
    label_aliases = _quickplot_options["label_aliases"]
    new_window = _quickplot_options["new_window"]
    blocking = _quickplot_options["blocking"]

    if not matplotlib_imported:
        raise ImportError(
            "PHIDL tried to import matplotlib but it failed. PHIDL "
            + "will still work but quickplot() will not"
        )

    if new_window:
        fig, ax = plt.subplots(1)
        ax.autoscale(enable=True, tight=True)
    else:
        if plt.fignum_exists(num="PHIDL quickplot"):
            fig = plt.figure("PHIDL quickplot")
            plt.clf()  # Erase figure so toolbar at top works correctly
            ax = fig.add_subplot(111)
        else:
            fig, ax = plt.subplots(num="PHIDL quickplot")

    ax.axis("equal")
    ax.grid(True, which="both", alpha=0.4)
    ax.axhline(y=0, color="k", alpha=0.2, linewidth=1)
    ax.axvline(x=0, color="k", alpha=0.2, linewidth=1)
    bbox = None

    # Iterate through each each Device/DeviceReference/Polygon
    if not isinstance(items, list):
        items = [items]
    for item in items:
        if isinstance(item, (Device, DeviceReference, CellArray)):
            polygons_spec = item.get_polygons(by_spec=True, depth=None)
            for key in sorted(polygons_spec):
                polygons = polygons_spec[key]
                layerprop = _get_layerprop(layer=key[0], datatype=key[1])
                new_bbox = _draw_polygons(
                    polygons,
                    ax,
                    facecolor=layerprop["color"],
                    edgecolor="k",
                    alpha=layerprop["alpha"],
                )
                bbox = _update_bbox(bbox, new_bbox)
            # If item is a Device or DeviceReference, draw ports
            if isinstance(item, (Device, DeviceReference)) and show_ports is True:
                for name, port in item.ports.items():
                    if (port.width is None) or (port.width == 0):
                        new_bbox = _draw_port_as_point(ax, port)
                    else:
                        new_bbox = _draw_port(ax, port, is_subport=False, color="r")
                    bbox = _update_bbox(bbox, new_bbox)
            if isinstance(item, Device) and show_subports is True:
                for sd in item.references:
                    if not isinstance(sd, (gdspy.CellArray)):
                        for name, port in sd.ports.items():
                            new_bbox = _draw_port(
                                ax,
                                port,
                                is_subport=True,
                                color=np.array(_SUBPORT_RGB) / 255,
                            )
                            bbox = _update_bbox(bbox, new_bbox)
            if isinstance(item, Device) and label_aliases is True:
                for name, ref in item.aliases.items():
                    ax.text(
                        ref.x,
                        ref.y,
                        str(name),
                        style="italic",
                        color="blue",
                        weight="bold",
                        size="large",
                        ha="center",
                        fontsize=14,
                    )
        elif isinstance(item, Polygon):
            polygons = item.polygons
            layerprop = _get_layerprop(item.layers[0], item.datatypes[0])
            new_bbox = _draw_polygons(
                polygons,
                ax,
                facecolor=layerprop["color"],
                edgecolor="k",
                alpha=layerprop["alpha"],
            )
            bbox = _update_bbox(bbox, new_bbox)
        elif isinstance(item, Path):
            points = item.points
            new_bbox = _draw_line(
                x=points[:, 0],
                y=points[:, 1],
                ax=ax,
                linestyle="--",
                linewidth=2,
                color="b",
            )
            _draw_arrow(ax=ax, x=points[-1, 0], y=points[-1, 1], angle=item.end_angle)
            _draw_dot(ax=ax, x=points[0, 0], y=points[0, 1])
            bbox = _update_bbox(bbox, new_bbox)

    if bbox is None:
        bbox = [-1, -1, 1, 1]
    xmargin = (bbox[2] - bbox[0]) * 0.1 + 1e-9
    ymargin = (bbox[3] - bbox[1]) * 0.1 + 1e-9
    ax.set_xlim([bbox[0] - xmargin, bbox[2] + xmargin])
    ax.set_ylim([bbox[1] - ymargin, bbox[3] + ymargin])

    # When using inline Jupyter notebooks, this may fail so allow it to fail gracefully
    try:
        if _use_interactive_zoom():
            _zoom_factory(ax, scale_factor=_quickplot_options["zoom_factor"])
        # Need to hang on to RectangleSelector so it doesn't get garbage collected
        _qp_objects["rectangle_selector"] = _rectangle_selector_factory(fig, ax)
        # Update matplotlib toolbar so the Home button works
        fig.canvas.toolbar.update()
        fig.canvas.toolbar.push_current()
    except Exception:
        pass

    plt.draw()
    plt.show(block=blocking)


def _draw_arrow(ax, x, y, angle):
    from matplotlib.markers import MarkerStyle

    rotated_marker = MarkerStyle(marker=9)
    rotated_marker._transform = rotated_marker.get_transform().rotate_deg(angle)
    ax.scatter(x, y, marker=rotated_marker, s=50, facecolors="b", alpha=0.5)


def _draw_dot(ax, x, y):
    ax.scatter(x, y, marker=".", color="b", s=50, alpha=0.9)


def _use_interactive_zoom():
    """Checks whether the current matplotlib backend is compatible with
    interactive zoom"""
    import matplotlib

    if _quickplot_options["interactive_zoom"] is not None:
        return _quickplot_options["interactive_zoom"]
    forbidden_backends = ["nbagg"]
    backend = matplotlib.get_backend().lower()
    usable = not any([(fb.lower() in backend) for fb in forbidden_backends])
    return usable


def _update_bbox(bbox, new_bbox):
    if bbox is None:
        return new_bbox
    if new_bbox[0] < bbox[0]:
        bbox[0] = new_bbox[0]  # xmin
    if new_bbox[1] < bbox[1]:
        bbox[1] = new_bbox[1]  # ymin
    if new_bbox[2] > bbox[2]:
        bbox[2] = new_bbox[2]  # xmin
    if new_bbox[3] > bbox[3]:
        bbox[3] = new_bbox[3]  # ymin
    return bbox


def _get_layerprop(layer, datatype):
    # Colors generated from here: http://phrogz.net/css/distinct-colors.html
    layer_colors = [
        "#3dcc5c",
        "#2b0fff",
        "#cc3d3d",
        "#e5dd45",
        "#7b3dcc",
        "#cc860c",
        "#73ff0f",
        "#2dccb4",
        "#ff0fa3",
        "#0ec2e6",
        "#3d87cc",
        "#e5520e",
    ]

    _layer = Layer.layer_dict.get((layer, datatype))
    if _layer is not None:
        color = _layer.color
        alpha = _layer.alpha
        if color is None:
            color = layer_colors[np.mod(layer, len(layer_colors))]
    else:
        color = layer_colors[np.mod(layer, len(layer_colors))]
        alpha = 0.6
    return {"color": color, "alpha": alpha}


def _draw_polygons(polygons, ax, **kwargs):
    from matplotlib.collections import PolyCollection

    coll = PolyCollection(polygons, **kwargs)
    ax.add_collection(coll)
    stacked_polygons = np.vstack(polygons)
    xmin, ymin = np.min(stacked_polygons, axis=0)
    xmax, ymax = np.max(stacked_polygons, axis=0)
    bbox = [xmin, ymin, xmax, ymax]
    return bbox


def _draw_line(x, y, ax, **kwargs):
    from matplotlib.lines import Line2D

    line = Line2D(x, y, **kwargs)
    ax.add_line(line)
    xmin, ymin = np.min(x), np.min(y)
    xmax, ymax = np.max(x), np.max(y)
    bbox = [xmin, ymin, xmax, ymax]
    return bbox


def _port_marker(port, is_subport):
    if is_subport:
        arrow_scale = 0.75
        rad = (port.orientation + 45) * np.pi / 180
        pm = +1
    else:
        arrow_scale = 1
        rad = (port.orientation - 45) * np.pi / 180
        pm = -1
    arrow_points = (
        np.array([[0, 0], [10, 0], [6, pm * 4], [6, pm * 2], [0, pm * 2]])
        / 35
        * port.width
        * arrow_scale
    )
    arrow_points += port.midpoint
    arrow_points = _rotate_points(
        arrow_points, angle=port.orientation, center=port.center
    )
    text_pos = np.array([np.cos(rad), np.sin(rad)]) * port.width / 3 + port.center
    return arrow_points, text_pos


def _draw_port(ax, port, is_subport, color):
    xbound, ybound = np.column_stack(port.endpoints)
    # plt.plot(x, y, 'rp', markersize = 12) # Draw port midpoint
    arrow_points, text_pos = _port_marker(port, is_subport)
    xmin, ymin = np.min(np.vstack([arrow_points, port.endpoints]), axis=0)
    xmax, ymax = np.max(np.vstack([arrow_points, port.endpoints]), axis=0)
    ax.plot(xbound, ybound, alpha=0.5, linewidth=3, color=color)  # Draw port edge
    ax.plot(
        arrow_points[:, 0], arrow_points[:, 1], alpha=0.8, linewidth=2, color=color
    )  # Draw port edge
    ax.text(
        text_pos[0],
        text_pos[1],
        port.name,
        horizontalalignment="center",
        verticalalignment="center",
        fontsize=14,
        color=color,
    )
    bbox = [xmin, ymin, xmax, ymax]
    return bbox


def _draw_port_as_point(ax, port, **kwargs):
    from matplotlib import pyplot as plt

    x = port.midpoint[0]
    y = port.midpoint[1]
    plt.plot(x, y, "r+", alpha=0.5, markersize=15, markeredgewidth=2)  # Draw port edge
    bbox = [
        x - port.width / 2,
        y - port.width / 2,
        x + port.width / 2,
        y + port.width / 2,
    ]
    ax.text(port.midpoint[0], port.midpoint[1], port.name, fontsize=14)
    return bbox


class ViewerWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        self.setGeometry(QRect(100, 100, 800, 600))
        self.setWindowTitle("PHIDL quickplot")

        # Create "grid size = 40.0" label
        self.gridsize_label = QLabel("ABCDEF", self)
        self.gridsize_label.setFont(QtGui.QFont("SansSerif", 10))
        self.gridsize_label.move(0, 200)
        self.gridsize_label.setAlignment(Qt.AlignLeft)
        self.gridsize_label.setStyleSheet("color: gray")
        self.gridsize_label.setFixedWidth(120)
        self.gridsize_label.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents)

        # Create "X=40.001, Y = 70.183" label
        self.position_label = QLabel("ABCDEF", self)
        self.position_label.setFont(QtGui.QFont("SansSerif", 10))
        self.position_label.move(50, 200)
        self.position_label.setAlignment(Qt.AlignRight)
        self.position_label.setStyleSheet("color: gray")
        self.position_label.setFixedWidth(240)
        self.position_label.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents)

        # Create "Press ? for help" label
        self.help_label = QLabel("ABCDEF", self)
        self.help_label.setFont(QtGui.QFont("SansSerif", 10))
        self.help_label.move(50, 200)
        self.help_label.setAlignment(Qt.AlignCenter)
        self.help_label.setStyleSheet("color: gray")
        self.help_label.setFixedWidth(200)
        self.help_label.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents)

        # Create label useful for debugging
        self.debug_label = QLabel("", self)
        self.debug_label.setFont(QtGui.QFont("SansSerif", 10))
        self.debug_label.move(200, 200)
        self.debug_label.setAlignment(Qt.AlignCenter)
        self.debug_label.setStyleSheet("color: gray")
        self.debug_label.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents)

        # Create QGraphicsView
        self.viewer = Viewer(
            gridsize_label=self.gridsize_label,
            position_label=self.position_label,
            help_label=self.help_label,
        )
        self.setCentralWidget(self.viewer)

        # Reorder widgets
        self.gridsize_label.raise_()
        self.position_label.raise_()
        self.debug_label.raise_()
        self.help_label.raise_()
        self.show()


class Viewer(QGraphicsView):
    def __init__(self, gridsize_label, position_label, help_label):
        QGraphicsView.__init__(self)

        self.gridsize_label = gridsize_label
        self.position_label = position_label
        self.help_label = help_label

        # Create a QGraphicsScene which this view looks at
        self.scene = QGraphicsScene(self)
        self.scene.setSceneRect(QRectF())
        self.setScene(self.scene)

        # Customize QGraphicsView
        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.setInteractive(False)
        self.scale(1, -1)  # Flips around the Y axis
        # Use OpenGL http://ralsina.me/stories/BBS53.html
        #        self.setViewport(QtOpenGL.QGLWidget())
        self.rubberBand = QRubberBand(QRubberBand.Rectangle, self)
        self.pen = QPen(QtCore.Qt.black, 0)
        self.portpen = QPen(PORT_COLOR, 3)
        self.portpen.setCosmetic(True)  # Makes constant width
        self.portfont = QtGui.QFont("Arial", pointSize=14)
        self.portfontcolor = PORT_COLOR
        self.subportpen = QPen(SUBPORT_COLOR, 3)
        self.subportpen.setCosmetic(True)  # Makes constant width
        self.subportfont = QtGui.QFont("Arial", pointSize=14)
        self.subportfontcolor = SUBPORT_COLOR

        # Tracking ports

        # Various status variables
        self._mousePressed = None
        self._rb_origin = QPoint()
        self.zoom_factor_total = 1

        # Grid variables
        self.gridpen = QPen(QtCore.Qt.black, 0)
        self.gridpen.setStyle(QtCore.Qt.DotLine)
        self.gridpen.setDashPattern([1, 4])
        self.gridpen.setColor(QtGui.QColor(0, 0, 0, 125))
        #        self.gridpen = QPen(QtCore.Qt.black, 1)
        #        self.gridpen.setCosmetic(True) # Makes constant width
        self.scene_polys = []

        self.initialize()

    def add_polygons(self, polygons, color="#A8F22A", alpha=1):
        qcolor = QColor()
        qcolor.setNamedColor(color)
        qcolor.setAlphaF(alpha)
        for points in polygons:
            qpoly = QPolygonF([QPointF(p[0], p[1]) for p in points])
            scene_poly = self.scene.addPolygon(qpoly)
            scene_poly.setBrush(qcolor)
            scene_poly.setPen(self.pen)
            self.scene_polys.append(scene_poly)
            # Update custom bounding box
            sr = scene_poly.sceneBoundingRect()
            if len(self.scene_polys) == 1:
                self.scene_xmin = sr.left()
                self.scene_xmax = sr.right()
                self.scene_ymin = sr.top()
                self.scene_ymax = sr.bottom()
            else:
                self.scene_xmin = min(self.scene_xmin, sr.left())
                self.scene_xmax = max(self.scene_xmax, sr.right())
                self.scene_ymin = min(self.scene_ymin, sr.top())
                self.scene_ymax = max(self.scene_ymax, sr.bottom())

    def reset_view(self):
        # The SceneRect controls how far you can pan, make it larger than
        # just the bounding box so middle-click panning works
        panning_rect = QRectF(self.scene_bounding_rect)
        panning_rect_center = panning_rect.center()
        panning_rect_size = max(panning_rect.width(), panning_rect.height()) * 3
        panning_rect.setSize(QSizeF(panning_rect_size, panning_rect_size))
        panning_rect.moveCenter(panning_rect_center)
        self.setSceneRect(panning_rect)
        self.fitInView(self.scene_bounding_rect, Qt.KeepAspectRatio)
        self.zoom_view(0.8)

        self.update_grid()

    def add_port(self, port, is_subport=False):
        if (port.width is None) or (port.width == 0):
            x, y = port.midpoint
            cs = 1  # cross size
            pn = QPointF(x, y + cs)
            ps = QPointF(x, y - cs)
            pe = QPointF(x + cs, y)
            pw = QPointF(x - cs, y)
            qline1 = self.scene.addLine(QLineF(pn, ps))
            qline2 = self.scene.addLine(QLineF(pw, pe))
            port_shapes = [qline1, qline2]
        else:
            point1, point2 = port.endpoints
            point1 = QPointF(point1[0], point1[1])
            point2 = QPointF(point2[0], point2[1])
            qline = self.scene.addLine(QLineF(point1, point2))
            arrow_points, text_pos = _port_marker(port, is_subport)
            arrow_qpoly = QPolygonF([QPointF(p[0], p[1]) for p in arrow_points])
            port_scene_poly = self.scene.addPolygon(arrow_qpoly)
            # port_scene_poly.setRotation(port.orientation)
            # port_scene_poly.moveBy(port.midpoint[0], port.midpoint[1])
            port_shapes = [qline, port_scene_poly]
        qtext = self.scene.addText(str(port.name), self.portfont)
        qtext.setPos(QPointF(text_pos[0], text_pos[1]))
        qtext.setFlag(QGraphicsItem.ItemIgnoresTransformations)
        port_items = port_shapes + [qtext]

        if not is_subport:
            [shape.setPen(self.portpen) for shape in port_shapes]
            qtext.setDefaultTextColor(self.portfontcolor)
            self.portitems += port_items
        else:
            [shape.setPen(self.subportpen) for shape in port_shapes]
            qtext.setDefaultTextColor(self.subportfontcolor)
            self.subportitems += port_items

    #        self.portlabels.append(qtext)

    def add_aliases(self, aliases):
        for name, ref in aliases.items():
            qtext = self.scene.addText(str(name), self.portfont)
            x, y = ref.center
            qtext.setPos(QPointF(x, y))
            qtext.setFlag(QGraphicsItem.ItemIgnoresTransformations)
            self.aliasitems += [qtext]

    def set_port_visibility(self, visible=True):
        for item in self.portitems:
            item.setVisible(visible)
        self.ports_visible = visible

    def set_subport_visibility(self, visible=True):
        for item in self.subportitems:
            item.setVisible(visible)
        self.subports_visible = visible

    def set_alias_visibility(self, visible=True):
        for item in self.aliasitems:
            item.setVisible(visible)
        self.aliases_visible = visible

    def initialize(self):
        self.scene.clear()
        self.polygons = {}
        self.portitems = []
        self.subportitems = []
        self.aliasitems = []
        self.aliases_visible = True
        self.ports_visible = True
        self.subports_visible = True
        self.mouse_position = [0, 0]
        self.grid_size_snapped = 0
        self.setMouseTracking(True)
        self.scene_bounding_rect = None
        self.scene_polys = []
        self.scene_xmin = 0
        self.scene_xmax = 1
        self.scene_ymin = 0
        self.scene_ymax = 1

    def finalize(self):
        self.scene_bounding_rect = QRectF(
            QPointF(self.scene_xmin, self.scene_ymin),
            QPointF(self.scene_xmax, self.scene_ymax),
        )
        # self.scene_center = [self.scene_bounding_rect.center().x(), self.scene_bounding_rect.center().y()]
        self.scene_size = [
            self.scene_bounding_rect.width(),
            self.scene_bounding_rect.height(),
        ]
        self.create_grid()
        self.update_grid()

    # ==============================================================================
    #   Grid creation
    # ==============================================================================
    def update_grid(self):
        grid_pixels = 50
        grid_snaps = [1, 2, 4]

        # Number of pixels in the viewer
        view_width, _ = self.rect().width(), self.rect().height()
        # Rectangle of viewport in terms of scene coordinates
        r = self.mapToScene(self.rect()).boundingRect()
        width, height = r.width(), r.height()
        xmin, ymin, _, _ = r.x(), r.y(), r.x() + width, r.y() + height

        grid_size = grid_pixels * (width / view_width)
        exponent = np.floor(np.log10(grid_size))
        digits = round(grid_size / 10 ** (exponent), 2)
        digits_snapped = min(grid_snaps, key=lambda x: abs(x - digits))
        grid_size_snapped = digits_snapped * 10 ** (exponent)

        # Starting coordinates for gridlines
        x = round((xmin - 2 * width) / grid_size_snapped) * grid_size_snapped
        y = round((ymin - 2 * height) / grid_size_snapped) * grid_size_snapped

        for gl in self.gridlinesx:
            gl.setLine(x, -1e10, x, 1e10)
            x += grid_size_snapped
        for gl in self.gridlinesy:
            gl.setLine(-1e10, y, 1e10, y)
            y += grid_size_snapped
        self.grid_size_snapped = grid_size_snapped
        self.update_gridsize_label()

    def update_gridsize_label(self):
        self.gridsize_label.setText("grid size = " + str(self.grid_size_snapped))
        self.gridsize_label.move(QPoint(5, self.height() - 25))

    def update_mouse_position_label(self):
        self.position_label.setText(
            f"X = {self.mouse_position[0]:0.4f} / Y = {self.mouse_position[1]:0.4f}"
        )
        self.position_label.move(QPoint(self.width() - 250, self.height() - 25))

    def update_help_label(self):
        self.help_label.setText('Press "?" key for help')
        self.help_label.move(QPoint(self.width() - 175, 0))

    def create_grid(self):
        self.gridlinesx = [
            self.scene.addLine(-10, -10, 10, 10, self.gridpen) for n in range(300)
        ]
        self.gridlinesy = [
            self.scene.addLine(-10, -10, 10, 10, self.gridpen) for n in range(300)
        ]
        self.update_grid()

    # ==============================================================================
    #  Mousewheel zoom, taken from http://stackoverflow.com/a/29026916
    # ==============================================================================
    def wheelEvent(self, event):
        # Zoom Factor
        zoom_percentage = 1.4

        # Set Anchors
        self.setTransformationAnchor(QGraphicsView.NoAnchor)
        self.setResizeAnchor(QGraphicsView.NoAnchor)

        # Save the scene pos
        oldPos = self.mapToScene(event.pos())

        # Zoom
        mousewheel_rotation = (
            event.angleDelta().y()
        )  # Typically = 120 on most mousewheels
        zoom_factor = zoom_percentage ** (mousewheel_rotation / 120)
        zoom_factor = np.clip(zoom_factor, 0.5, 2.0)

        # Check to make sure we're not overzoomed
        min_width = 0.01
        min_height = 0.01

        window_width = self.rect().width()
        window_height = self.rect().height()
        scene_upper_left_corner = self.mapToScene(QPoint(0, 0))
        scene_bottom_right_corner = self.mapToScene(QPoint(window_width, window_height))
        scene_width = (scene_bottom_right_corner - scene_upper_left_corner).x()
        scene_height = (scene_upper_left_corner - scene_bottom_right_corner).y()

        max_width = self.scene_bounding_rect.width() * 3
        max_height = self.scene_bounding_rect.height() * 3

        if ((scene_width > max_width) and (scene_height > max_height)) and (
            zoom_factor < 1
        ):
            pass
        elif ((scene_width < min_width) and (scene_height < min_height)) and (
            zoom_factor > 1
        ):
            pass
        else:
            self.zoom_view(zoom_factor)

        # Get the new position and move scene to old position
        newPos = self.mapToScene(event.pos())
        delta = newPos - oldPos
        self.translate(delta.x(), delta.y())

        self.update_grid()

    def zoom_view(self, zoom_factor):
        old_center = self.mapToScene(self.rect().center())
        self.scale(zoom_factor, zoom_factor)
        self.centerOn(old_center)
        self.zoom_factor_total *= zoom_factor

    def resizeEvent(self, event):
        super(QGraphicsView, self).resizeEvent(event)
        if self.scene_bounding_rect is not None:
            self.reset_view()
        self.update_gridsize_label()
        self.update_mouse_position_label()
        self.update_help_label()

    def mousePressEvent(self, event):
        super(QGraphicsView, self).mousePressEvent(event)
        # ==============================================================================
        #  Zoom to rectangle, from
        #  https://wiki.python.org/moin/PyQt/Selecting%20a%20region%20of%20a%20widget
        # ==============================================================================
        if event.button() == Qt.RightButton:
            self._mousePressed = Qt.RightButton
            self._rb_origin = QPoint(event.pos())
            self.rubberBand.setGeometry(QRect(self._rb_origin, QSize()))
            self.rubberBand.show()
        # ==============================================================================
        # Mouse panning, taken from
        # http://stackoverflow.com/a/15043279
        # ==============================================================================
        elif event.button() == Qt.MidButton:
            self._mousePressed = Qt.MidButton
            self._mousePressedPos = event.pos()
            self.setCursor(QtCore.Qt.ClosedHandCursor)
            self._dragPos = event.pos()

    def mouseMoveEvent(self, event):
        super(QGraphicsView, self).mouseMoveEvent(event)

        # # Useful debug
        # try:
        #     self.debug_label.setText(str(itemsBoundingRect_nogrid().width()))
        # except Exception:
        #     print('Debug statement failed')

        # Update the X,Y label indicating where the mouse is on the geometry
        mouse_position = self.mapToScene(event.pos())
        self.mouse_position = [mouse_position.x(), mouse_position.y()]
        self.update_mouse_position_label()

        if not self._rb_origin.isNull() and self._mousePressed == Qt.RightButton:
            self.rubberBand.setGeometry(
                QRect(self._rb_origin, event.pos()).normalized()
            )

        # Middle-click-to-pan
        if self._mousePressed == Qt.MidButton:
            newPos = event.pos()
            diff = newPos - self._dragPos
            self._dragPos = newPos
            self.horizontalScrollBar().setValue(
                self.horizontalScrollBar().value() - diff.x()
            )
            self.verticalScrollBar().setValue(
                self.verticalScrollBar().value() - diff.y()
            )

    #            event.accept()

    def mouseReleaseEvent(self, event):
        if event.button() == Qt.RightButton:
            self.rubberBand.hide()
            rb_rect = QRect(self._rb_origin, event.pos())
            rb_center = rb_rect.center()
            rb_size = rb_rect.size()

            if abs(rb_size.width()) > 3 and abs(rb_size.height()) > 3:
                viewport_size = self.viewport().geometry().size()

                zoom_factor_x = abs(viewport_size.width() / rb_size.width())
                zoom_factor_y = abs(viewport_size.height() / rb_size.height())

                new_center = self.mapToScene(rb_center)

                zoom_factor = min(zoom_factor_x, zoom_factor_y)
                self.zoom_view(zoom_factor)
                self.centerOn(new_center)

            self.update_grid()

        if event.button() == Qt.MidButton:
            self.setCursor(Qt.ArrowCursor)
            self._mousePressed = None
            self.update_grid()

    def keyPressEvent(self, event):
        if event.key() == Qt.Key_Escape:
            self.reset_view()

        if event.key() == Qt.Key_F1:
            self.set_alias_visibility(not self.aliases_visible)

        if event.key() == Qt.Key_F2:
            self.set_port_visibility(not self.ports_visible)

        if event.key() == Qt.Key_F3:
            self.set_subport_visibility(not self.subports_visible)

        if event.key() == Qt.Key_Question:
            help_str = """
            Mouse control:
              Mousewheel: Zoom in and out
              Right-click & drag: Zoom to rectangle
              Middle-click & drag: Pan

            Keyboard shortcuts:
              Esc: Reset view
              F1: Show/hide alias names
              F2: Show/hide ports
              F3: Show/hide subports (ports in underlying references)
            """
            QMessageBox.about(self, "PHIDL Help", help_str)


def quickplot2(item_list, *args, **kwargs):
    """
    .. deprecated:: 1.6.4
        `quickplot2` will be removed in April 2025, please replace with quickplot()
    """
    warnings.warn(
        """[PHIDL] Warning: `quickplot2` will be removed in April 2025, please replace with quickplot() """
    )

    if not qt_imported:
        raise ImportError(
            "PHIDL tried to import PyQt5 but it failed. PHIDL will"
            + "still work but quickplot2() may not.  Try using"
            + "quickplot() instead (based on matplotlib)"
        )

    global app
    if QCoreApplication.instance() is None:
        app = QApplication(sys.argv)
    if "viewer_window" not in globals():
        global viewer_window
        viewer_window = ViewerWindow()
    viewer = viewer_window.viewer
    viewer.initialize()
    if not isinstance(item_list, (list, tuple)):
        item_list = [item_list]
    for element in item_list:
        if isinstance(
            element,
            (
                phidl.device_layout.Device,
                phidl.device_layout.DeviceReference,
                gdspy.CellArray,
            ),
        ):
            # Draw polygons in the element
            polygons_spec = element.get_polygons(by_spec=True, depth=None)
            for key in sorted(polygons_spec):
                polygons = polygons_spec[key]
                layerprop = _get_layerprop(layer=key[0], datatype=key[1])
                viewer.add_polygons(
                    polygons, color=layerprop["color"], alpha=layerprop["alpha"]
                )
            # If element is a Device, draw ports and aliases
            if isinstance(element, phidl.device_layout.Device):
                for ref in element.references:
                    if not isinstance(ref, gdspy.CellArray):
                        for name, port in ref.ports.items():
                            viewer.add_port(port, is_subport=True)
                for name, port in element.ports.items():
                    viewer.add_port(port)
                    viewer.add_aliases(element.aliases)
            # If element is a DeviceReference, draw ports as subports
            if isinstance(element, phidl.device_layout.DeviceReference):
                for name, port in element.ports.items():
                    viewer.add_port(port, is_subport=True)
        elif isinstance(element, (phidl.device_layout.Polygon)):
            layerprop = _get_layerprop(
                layer=element.layers[0], datatype=element.datatypes[0]
            )
            viewer.add_polygons(
                element.polygons, color=layerprop["color"], alpha=layerprop["alpha"]
            )
    viewer.finalize()
    viewer.reset_view()
    viewer_window.setVisible(True)
    viewer_window.show()
    viewer_window.raise_()
    return viewer
