# -*- coding: utf-8 -*-
import time
from functools import WRAPPER_ASSIGNMENTS, lru_cache

import pyqtgraph as pg
from ophyd.signal import ReadTimeoutError
from pyqtgraph import ImageView, InfiniteLine, mkPen, ScatterPlotItem, ImageItem, PlotItem, InfLineLabel
from qtpy.QtGui import QTransform, QPolygonF, QIcon, QPixmap
from qtpy.QtWidgets import QLabel, QErrorMessage, QSizePolicy, QPushButton, QHBoxLayout, QVBoxLayout, QComboBox, \
    QWidget, QToolBar, QActionGroup, QAction, QLayout, QCheckBox, QProgressBar
from qtpy.QtCore import Qt, Signal, Slot, QSize, QPointF, QRectF
import numpy as np
from databroker.core import BlueskyRun
from xarray import DataArray

# from pyFAI.geometry import Geometry
from camsaxs.remesh_bbox import remesh, q_from_geometry
from xicam.core import msg, threads
from xicam.core.data import MetaXArray
from xicam.core.data.bluesky_utils import fields_from_stream, streams_from_run, is_image_field
from xicam.core.threads import invoke_as_event
from xicam.gui.actions import ROIAction
from xicam.gui.widgets.elidedlabel import ElidedLabel
from xicam.gui.static import path
from xicam.gui.widgets.metadataview import MetadataWidget
from xicam.gui.widgets.ROI import BetterPolyLineROI, BetterCrosshairROI, BetterRectROI, ArcROI, SegmentedArcROI, \
    SegmentedRectROI, ArcQROI, ArcPXROI
import enum
from typing import Callable
from functools import partial

from xicam.plugins import manager as pluginmanager, live_plugin
import inspect

# NOTE: PyQt widget mixins have pitfalls; note #2 here: http://trevorius.com/scrapbook/python/pyqt-multiple-inheritance/

# NOTE: PyFAI geometry position vector is: x = up
#                                          y = right
#                                          z = beam

def q_from_angles(phi, alpha, wavelength):
    r = 2 * np.pi / wavelength
    qx = r * np.sin(phi) * np.cos(alpha)
    qy = r * np.cos(phi) * np.sin(alpha)
    qz = r * (np.cos(phi) * np.cos(alpha) - 1)

    return np.array([qx, qy, qz])

def alpha(x, y, z):
    return np.arctan2(y, z)

def phi(x, y, z):
    return np.arctan2(x, z)

class DisplayMode(enum.Enum):
    raw = enum.auto()
    cake = enum.auto()
    remesh = enum.auto()

class RowMajor(ImageView):
    def __init__(self, *args, **kwargs):
        super(RowMajor, self).__init__(*args, **kwargs)
        self.imageItem.setOpts(replace=True, axisOrder='row-major')

class BetterTicks(ImageView):
    def __init__(self, *args, **kwargs):
        super(BetterTicks, self).__init__(*args, **kwargs)

        # Make ticks span the whole Y range (good for plots)
        self.frameTicks.setYRange([0, 1])

    def setImage(self, img, **kwargs):
        # Only show ticks if they don't drown out everything else
        if img is not None:
            self.frameTicks.setVisible(img.shape[0] < 100)

        super(BetterTicks, self).setImage(img, **kwargs)

class BetterPlots(BetterTicks):
    def __init__(self, *args, **kwargs):
        super(BetterPlots, self).__init__(*args, **kwargs)

        self.ui.roiPlot.setMinimumSize(QSize(0, 200))

class BetterLayout(ImageView):
    # Replaces awkward gridlayout with more structured v/hboxlayouts, and removes useless buttons
    def __init__(self, *args, **kwargs):
        super(BetterLayout, self).__init__(*args, **kwargs)
        self._reset_layout()
        self._set_layout()

    def _set_layout(self, layout=None):
        # Replace the layout
        QWidget().setLayout(self.ui.layoutWidget.layout())
        if layout is not None:
            self.ui.layoutWidget.setLayout(layout)
        else:
            self.ui.layoutWidget.setLayout(self.ui.outer_layout)

    def _reset_layout(self):
        self.ui.outer_layout = QHBoxLayout()
        self.ui.left_layout = QVBoxLayout()
        self.ui.right_layout = QVBoxLayout()
        self.ui.outer_layout.addLayout(self.ui.left_layout)
        self.ui.outer_layout.addLayout(self.ui.right_layout)
        for layout in [self.ui.outer_layout, self.ui.left_layout, self.ui.right_layout]:
            layout.setContentsMargins(0,0,0,0)
            layout.setSpacing(0)

        self.ui.left_layout.addWidget(self.ui.graphicsView)
        self.ui.right_layout.addWidget(self.ui.histogram)

        # Must keep the roiBtn around; ImageView expects to be able to check its state
        self.ui.roiBtn.setParent(self)
        self.ui.roiBtn.hide()

    def add_button(self, button):
        self.ui.right_layout.addWidget(button)

class BetterButtons(BetterLayout):
    def __init__(self, *args, **kwargs):
        super(BetterButtons, self).__init__(*args, **kwargs)

        # Setup axes reset button
        self.resetAxesBtn = QPushButton("Reset Axes")
        sizePolicy = QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(1)
        sizePolicy.setHeightForWidth(self.resetAxesBtn.sizePolicy().hasHeightForWidth())
        self.resetAxesBtn.setSizePolicy(sizePolicy)
        self.resetAxesBtn.setObjectName("resetAxes")
        self.ui.right_layout.addWidget(self.resetAxesBtn)
        self.resetAxesBtn.clicked.connect(self.autoRange)

        # Setup LUT reset button
        self.resetLUTBtn = QPushButton("Reset LUT")
        sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(1)
        sizePolicy.setHeightForWidth(self.resetLUTBtn.sizePolicy().hasHeightForWidth())
        # self.resetLUTBtn.setSizePolicy(sizePolicy)
        # self.resetLUTBtn.setObjectName("resetLUTBtn")
        self.ui.right_layout.addWidget(self.resetLUTBtn)
        self.resetLUTBtn.clicked.connect(self.autoLevels)

class ExportButton(BetterLayout):
    def __init__(self, *args, **kwargs):
        super(ExportButton, self).__init__(*args, **kwargs)

        # Export button
        self.exportBtn = QPushButton('Export')
        sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(1)
        sizePolicy.setHeightForWidth(self.exportBtn.sizePolicy().hasHeightForWidth())
        self.ui.right_layout.addWidget(self.exportBtn)
        self.exportBtn.clicked.connect(self.export)

@live_plugin('ImageMixinPlugin')
class XArrayView(ImageView):
    def __init__(self, *args, **kwargs):
        # Add axes
        self.axesItem = PlotItem()
        self.axesItem.axes["left"]["item"].setZValue(10)
        self.axesItem.axes["top"]["item"].setZValue(10)
        self._min_max_cache = dict()

        if "view" not in kwargs:
            kwargs["view"] = self.axesItem

        super(XArrayView, self).__init__(*args, **kwargs)

        self.view.invertY(False)

    def setImage(self, img, **kwargs):

        if hasattr(img, 'coords'):

            if 'transform' not in kwargs:

                xvals = img.coords[img.dims[-2]]
                yvals = img.coords[img.dims[-1]]
                xmin = float(xvals.min())
                xmax = float(xvals.max())
                ymin = float(yvals.min())
                ymax = float(yvals.max())

                # Position the image according to coords
                shape = img.shape
                a = [(0, shape[-2]), (shape[-1]-1, shape[-2]), (shape[-1]-1, 1), (0, 1)]

                b = [(ymin, xmin), (ymax, xmin), (ymax, xmax), (ymin, xmax)]

                quad1 = QPolygonF()
                quad2 = QPolygonF()
                for p, q in zip(a, b):
                    quad1.append(QPointF(*p))
                    quad2.append(QPointF(*q))

                transform = QTransform()
                QTransform.quadToQuad(quad1, quad2, transform)

                kwargs['transform'] = transform

            if 'xvals' not in kwargs:
                kwargs['xvals'] = np.asarray(img.coords[img.dims[0]])

            # Set the timeline axis label from dims
            self.ui.roiPlot.setLabel('bottom', img.dims[0])

            # Label the image axes
            self.axesItem.setLabel('left', img.dims[-2])
            self.axesItem.setLabel('bottom', img.dims[-1])

            # Add a bit more size
            self.ui.roiPlot.setMinimumSize(QSize(0, 70))

        # Bind coords from the xarray to the timeline axis
        super(XArrayView, self).setImage(img, **kwargs)

    def updateImage(self, autoHistogramRange=True):
        if hasattr(self.image, 'dims'):
            ## Redraw image on screen
            if self.image is None:
                return

            image = self.getProcessedImage()

            if autoHistogramRange:
                self.ui.histogram.setHistogramRange(self.levelMin, self.levelMax)

            # Transpose image into order expected by ImageItem
            if self.imageItem.axisOrder == 'col-major':
                axorder = ['t', 'x', 'y', 'c']
            else:
                axorder = ['t', 'y', 'x', 'c']

            if isinstance(image, DataArray):
                axorder = [self.axes[ax] for ax in axorder if self.axes[ax] is not None]
                ax_swap = [image.dims[ax_index] for ax_index in axorder]
                image = image.transpose(*ax_swap)

            # Select time index
            if self.axes['t'] is not None:
                self.ui.roiPlot.show()
                image = image[self.currentIndex]

            self.imageItem.updateImage(np.asarray(image))

        else:
            super(XArrayView, self).updateImage(autoHistogramRange)

    def quickMinMax(self, data):
        """
        Estimate the min/max values of *data* by subsampling. MODIFIED TO USE:
        - second lowest value as min
        - 99TH PERCENTILE instead of max
        NOTE: memoization assumes that data does not mutate
        """

        if id(data) in self._min_max_cache:
            return self._min_max_cache[id(data)]

        if data is None:
            return 0, 0

        sl = slice(None, None, max(1, int(np.prod(np.asarray(data.shape, dtype=np.float_)) // 1e6)))  # can't trust data.size due to likely overflow
        data = np.asarray(data[sl])

        img_max = np.nanmax(data)
        img_min = np.nanmin(data)
        levels = np.min(data, where=data > img_min, initial=img_max), np.nanpercentile(
            np.where(data < img_max, data, img_min), 99)

        self._min_max_cache[id(data)] = [levels]
        # TODO: prune cache

        return [levels]

class PixelSpace(XArrayView, RowMajor):
    def __init__(self, *args, **kwargs):
        # Add axes
        self.axesItem = PlotItem()
        self.axesItem.axes["left"]["item"].setZValue(10)
        self.axesItem.axes["top"]["item"].setZValue(10)
        if "view" not in kwargs:
            kwargs["view"] = self.axesItem

        self._transform = QTransform()

        super(PixelSpace, self).__init__(*args, **kwargs)

        self.imageItem.sigImageChanged.connect(self.updateAxes)

    def transform(self, img=None):
        # # Build Quads
        # shape = img.shape
        # a = [(0, shape[-2] - 1), (shape[-1] - 1, shape[-2] - 1), (shape[-1] - 1, 0), (0, 0)]
        #
        # b = [(0, 1), (shape[-1] - 1, 1), (shape[-1] - 1, shape[-2]), (0, shape[-2])]
        #
        # quad1 = QPolygonF()
        # quad2 = QPolygonF()
        # for p, q in zip(a, b):
        #     quad1.append(QPointF(*p))
        #     quad2.append(QPointF(*q))
        #
        # transform = QTransform()
        # QTransform.quadToQuad(quad1, quad2, transform)
        #
        # for item in self.view.items:
        #     if isinstance(item, ImageItem):
        #         item.setTransform(transform)
        # self._transform = transform
        return QTransform()

    def setImage(self, img, *args, **kwargs):
        if img is None:
            return

        if not kwargs.get("transform", None):
            transform = self.transform(img)
            self.updateAxes()
            super(PixelSpace, self).setImage(img, *args, transform=transform, **kwargs)

        else:
            super(PixelSpace, self).setImage(img, *args, **kwargs)

    def setTransform(self):
        self.setImage(self.image)  # this should loop back around to the respective transforms

    def updateAxes(self):
        self.axesItem.setLabel("bottom", "x (px)")  # , units='s')
        self.axesItem.setLabel("left", "z (px)")

class QSpace(PixelSpace):
    def __init__(self, *args, geometry=None, **kwargs):
        self.displaymode = DisplayMode.raw
        self._geometry = None  # type: AzimuthalIntegrator

        super(QSpace, self).__init__(*args, **kwargs)

        self.setGeometry(geometry)

    def setGeometry(self, geometry):
        if callable(geometry):
            geometry = geometry()
        self._geometry = geometry
        self.setTransform()

    def setImage(self, img, *args, geometry=None, **kwargs):
        if geometry:
            self.setGeometry(geometry)
        super(QSpace, self).setImage(img, *args, **kwargs)

class Pseudo3DFrameArray(object):
    """Array that pretends it is a 3D array, but really only has one frame.

    Data passed into it should be the already sliced (single-frame) array.
    """
    def __init__(self, data, shape):
        self.data = data
        self.dims = []
        self.ndim = 3
        self.shape = shape

    def transpose(self, *args, **kwargs):
        return self

    def __getitem__(self, item):
        """Override getitem to pretend it is a 3D array.

        Ignores the item slice and return the contained frame data."""
        return self.data

    def __array__(self, dtype=None):
        return np.asarray(self.data, dtype=dtype)

class ProcessingView(pg.ImageView):
    def getProcessedImage(self):

        # FIXME: do all incoming images need to be treated as Pseudo3DFrameArrays?
        #   - (1, X, Y) vs (X, Y) images
        image = self.image
        if image.ndim == 3:
            image = Pseudo3DFrameArray(self.process(np.array(image[self.currentIndex])), shape=image.shape)
        else:
            image = self.process(image)

        self._imageLevels = self.quickMinMax(image)

        self.levelMin, self.levelMax = self.process_levels(self._imageLevels)

        return image

    def process(self, image):
        raise NotImplementedError

    def process_levels(self, levels):
        level_min = min([level[0] for level in levels])
        level_max = max([level[1] for level in levels])

        return level_min, level_max

class CenterMarker(QSpace):
    def __init__(self, *args, **kwargs):
        self.centerplot = ScatterPlotItem(brush="r")
        self.centerplot.setZValue(100)

        super(CenterMarker, self).__init__(*args, **kwargs)

        self.addItem(self.centerplot)
        self.drawCenter()

    def drawCenter(self):
        try:
            fit2d = self._geometry.getFit2D()
        except (TypeError, AttributeError):
            pass
        else:
            if self.imageItem.image is not None:
                if self.displaymode == DisplayMode.raw:
                    x = fit2d["centerX"]
                    y = fit2d["centerY"]
                    self.centerplot.setData(x=[x], y=[y])
                elif self.displaymode == DisplayMode.remesh:
                    self.centerplot.setData(x=[0], y=[0])

    def setGeometry(self, geometry):
        super(CenterMarker, self).setGeometry(geometry)
        self.drawCenter()

class Crosshair(ImageView):
    def __init__(self, *args, **kwargs):
        super(Crosshair, self).__init__(*args, **kwargs)
        linepen = mkPen("#FFA500")
        self._vline = InfiniteLine((0, 0), angle=90, movable=False, pen=linepen)
        self._hline = InfiniteLine((0, 0), angle=0, movable=False, pen=linepen)

        self._vline.setVisible(False)
        self._hline.setVisible(False)

        self.addItem(self._vline)
        self.addItem(self._hline)

        self.scene.sigMouseMoved.connect(self.moveCrosshair)

    def moveCrosshair(self, pos):
        if self.view.getViewBox().sceneBoundingRect().contains(pos):
            mousePoint = self.view.getViewBox().mapSceneToView(pos)
            x, y = mousePoint.x(), mousePoint.y()

            if self.imageItem.mapRectToView(self.imageItem.boundingRect()).contains(mousePoint):  # within bounds
                self._vline.setPos(x)
                self._hline.setPos(y)
                self._hline.setVisible(True)
                self._vline.setVisible(True)
            else:
                self._hline.setVisible(False)
                self._vline.setVisible(False)

class PixelCoordinates(PixelSpace, BetterLayout):
    def __init__(self, *args, **kwargs):
        super(PixelCoordinates, self).__init__(*args, **kwargs)

        self._coordslabel = QLabel(parent=self)

        font = self._coordslabel.font()
        font.setPixelSize(14)
        self._coordslabel.setFont(font)

        self.ui.left_layout.addWidget(self._coordslabel, alignment=Qt.AlignHCenter)
        self.ui.right_layout.setSizeConstraint(QLayout.SetMinAndMaxSize)

        # Accommodate vertical height
        self._coordslabel.setMinimumSize(self._coordslabel.minimumSize().width(),
                                         self._coordslabel.minimumHeight() + self._coordslabel.height())

        self.scene.sigMouseMoved.connect(self.displayCoordinates)

    def displayCoordinates(self, pos):
        if self.view.sceneBoundingRect().contains(pos):
            mousePoint = self.view.getViewBox().mapSceneToView(pos)
            pos = QPointF(mousePoint.x(), mousePoint.y())

            if self.imageItem.mapRectToView(self.imageItem.boundingRect()).contains(mousePoint):  # within bounds
                # angstrom=QChar(0x00B5)
                pxpos = self.imageItem.mapFromView(pos)

                self.formatCoordinates(pxpos, pos)
            else:
                self._coordslabel.setText("")

    def formatCoordinates(self, pxpos, pos):
        """
        when the mouse is moved in the viewer, recalculate coordinates
        """

        try:
            I = self.imageItem.image[int(pxpos.y()), int(pxpos.x())]
        except IndexError:
            I = 0

        self._coordslabel.setText(f"x={pxpos.x():0.1f} y={pxpos.y():0.1f} I={I:0.0f}")

class QCoordinates(QSpace, PixelCoordinates):

    def formatCoordinates(self, pxpos, pos):

        """
        when the mouse is moved in the viewer, recalculate coordinates
        """

        if self._geometry:
            if self.displaymode == DisplayMode.remesh:
                try:
                    I = self.imageItem.image[int(pxpos.y()), int(pxpos.x())]
                except IndexError:
                    I = 0
                self._coordslabel.setText(
                    f"x={pxpos.x():0.1f}, "
                    f"y={self.imageItem.image.shape[-2] - pxpos.y():0.1f}, "
                    f"I={I:0.0f}, "
                    f"q={np.sqrt(pos.x() ** 2 + pos.y() ** 2):0.3f} \u212B\u207B\u00B9, "
                    f"qz={pos.y():0.3f} \u212B\u207B\u00B9, "
                    f"q\u2225={pos.x():0.3f} \u212B\u207B\u00B9, "
                    f"d={2 * np.pi / np.sqrt(pos.x() ** 2 + pos.y() ** 2) * 10:0.3f} nm, "
                    f"\u03B8={np.rad2deg(np.arctan2(pos.y(), pos.x())):.2f}°"
                )
            elif self.displaymode == DisplayMode.raw:
                try:
                    I = self.imageItem.image[int(pxpos.y()), int(pxpos.x())]
                except IndexError:
                    I = 0

                q = q_from_geometry(self.imageItem.image.shape,
                                    self._geometry,
                                    reflection=False,
                                    alphai=0)[int(self.imageItem.image.shape[-2] - pxpos.y()), int(pxpos.x())]

                self._coordslabel.setText(
                    f"x={pxpos.x():0.1f}, "
                    f"y={self.imageItem.image.shape[-2] - pxpos.y():0.1f}, "
                    f"I={I:0.0f}, "
                    f"q={np.sqrt(np.sum(np.square(q))):0.3f} \u212B\u207B\u00B9, "
                    f"qz={-q[1]:0.3f} \u212B\u207B\u00B9, "
                    f"q\u2225={q[0]:0.3f} \u212B\u207B\u00B9, "
                    f"d={2 * np.pi / np.sqrt(q[0] ** 2 + q[1] ** 2) * 10:0.3f} nm, "
                    f"\u03B8={np.rad2deg(np.arctan2(-q[1], q[0])):.2f}°"
                )
        else:
            super(QCoordinates, self).formatCoordinates(pxpos, pos)

class PolygonROI(ImageView):
    def __init__(self, *args, **kwargs):
        """
        Image view extended with an adjustable polygon region-of-interest (ROI).

        When first displayed, the polygon ROI's corners will be set to the image item's corners.

        Parameters
        ----------
        args, optional
            Positional arguments for the ImageView.
        kwargs, optional
            Keyword arguments for the ImageView.
        """
        super(PolygonROI, self).__init__(*args, **kwargs)
        rect = self.imageItem.boundingRect()  # type: QRectF
        positions = [
            (rect.bottomLeft().x(), rect.bottomLeft().y()),
            (rect.bottomRight().x(), rect.bottomRight().y()),
            (rect.topRight().x(), rect.topRight().y()),
            (rect.topLeft().x(), rect.topLeft().y()),
        ]
        self._roiItem = BetterPolyLineROI(positions=positions, closed=True, scaleSnap=True, translateSnap=True)
        self.addItem(self._roiItem)

    def __repr__(self):
        return type(self).__name__ + repr(self._roiItem)

    def poly_mask(self):
        """
        Gets the mask array for a ROI polygon on the image.

        The mask array's shape will match the image's shape.
        Any pixel inside both the ROI polygon and the image will be set to 1 in the mask array;
        all other values in the mask will be set to 0.

        Returns
        -------
        ndarray:
            Mask array of the ROI polygon within image space (mask shape matches image shape).

        """
        result, mapped = self._roiItem.getArrayRegion(
            np.ones_like(self.imageItem.image), self.imageItem, returnMappedCoords=True
        )

        # TODO -- move this code to own function and test
        # Reverse the result array to make indexing calculations easier, then revert back
        result = result[::-1, ::-1]
        mapped = mapped[::-1, ::-1]

        # Pad result mask rect into bounding rect of mask and image
        floorRow = np.floor(mapped[0]).astype(int)
        floorCol = np.floor(mapped[1]).astype(int)

        # Return empty mask if ROI bounding box does not intersect image bounding box
        resultRect = QRectF(QPointF(np.min(floorRow), np.min(floorCol)), QPointF(np.max(floorRow), np.max(floorCol)))
        if not self._intersectsImage(resultRect):
            # TODO -- is zeros(shape) the right return value for a non-intersecting polygon?
            return np.zeros(self.imageItem.image.shape)

        # Find the bounds of the ROI polygon
        minX = np.min(floorRow)
        maxX = np.max(floorRow)
        minY = np.min(floorCol)
        maxY = np.max(floorCol)

        width = self.imageItem.width()
        height = self.imageItem.height()
        # Pad the ROI polygon into the image shape
        # Don't need padding if a polygon boundary is outside of the image shape
        padXBefore = minX
        if minX < 0:
            padXBefore = 0
        padXAfter = height - maxX
        if padXAfter < 0:
            padXAfter = 0
        padYBefore = minY
        if minY < 0:
            padYBefore = 0
        padYAfter = width - maxY
        if padYAfter < 0:
            padYAfter = 0

        boundingBox = np.pad(result, ((padYBefore, padYAfter), (padXBefore, padXAfter)), "constant")

        # For trimming, any negative minimums need to be shifted into the image shape
        offsetX = 0
        offsetY = 0
        if minX < 0:
            offsetX = abs(minX)
        if minY < 0:
            offsetY = abs(minY)
        trimmed = boundingBox[abs(offsetY) : abs(offsetY) + height, abs(offsetX) : abs(offsetX) + width]

        # Reorient the trimmed mask array
        trimmed = trimmed[::-1, ::-1]

        # # TODO remove plotting code below
        # from matplotlib import pyplot as plt
        # plt.figure('bounding_box, origin="lower"')
        # plt.imshow(boundingBox, origin='lower')
        # plt.show()
        #
        #
        # plt.figure(f'trimmed, origin="lower", [{abs(offsetY)}:{abs(offsetY)+height}, {abs(offsetX)}:{abs(offsetX)+width}]')
        # plt.imshow(trimmed, origin='lower')
        # plt.show()
        # # TODO remove the plotting code above
        return trimmed

    def _intersectsImage(self, rectangle: QRectF):
        """
        Checks if a rectangle intersects the image's bounding rectangle.

        Parameters
        ----------
        rectangle
            Rectangle to test intersection with the image item's bounding rectangle.

        Returns
        -------
        bool
            True if the rectangle and the image bounding rectangle intersect; otherwise False.

        """
        # TODO -- test
        return self.imageItem.boundingRect().intersects(rectangle)

from pyqtgraph import functions as fn
from pyqtgraph import debug
from pyqtgraph import Point

class ComposableItemImageView(ImageView):
    """
    Used to compose together different image view mixins that may use different ItemImage subclasses.
    See LogScaleIntensity, LogScaleImageItem, ImageViewHistogramOverflowFIx, ImageItemHistorgramOverflowFix.
    Note that any imageItem named argument passed into the ImageView mixins above will discard the item and instead
    create a composition of imageItem_bases with their respective ImageItem class.
    """

    imageItem_bases = tuple()

class LogScaleImageItem(ImageItem):
    def __init__(self, *args, **kwargs):
        super(LogScaleImageItem, self).__init__(*args, **kwargs)
        self.logScale = True

    def render(self):
        # Convert data to QImage for display.

        profile = debug.Profiler()
        if self.image is None or self.image.size == 0:
            return
        if isinstance(self.lut, Callable):
            lut = self.lut(self.image)
        else:
            lut = self.lut

        if self.logScale:
            image = self.image + 1
            with np.errstate(invalid="ignore", divide='ignore'):
                image = image.astype(np.float)
                np.log(image, where=image >= 0, out=image)  # map to 0-255
        else:
            image = self.image

        if self.autoDownsample:
            # reduce dimensions of image based on screen resolution
            o = self.mapToDevice(QPointF(0, 0))
            x = self.mapToDevice(QPointF(1, 0))
            y = self.mapToDevice(QPointF(0, 1))
            w = Point(x - o).length()
            h = Point(y - o).length()
            if w == 0 or h == 0:
                self.qimage = None
                return
            xds = max(1, int(1.0 / w))
            yds = max(1, int(1.0 / h))
            axes = [1, 0] if self.axisOrder == "row-major" else [0, 1]
            image = fn.downsample(image, xds, axis=axes[0])
            image = fn.downsample(image, yds, axis=axes[1])
            self._lastDownsample = (xds, yds)
        else:
            pass

        # if the image data is a small int, then we can combine levels + lut
        # into a single lut for better performance
        levels = self.levels
        if levels is not None and levels.ndim == 1 and image.dtype in (np.ubyte, np.uint16):
            if self._effectiveLut is None:
                eflsize = 2 ** (image.itemsize * 8)
                ind = np.arange(eflsize)
                minlev, maxlev = levels
                levdiff = maxlev - minlev
                levdiff = 1 if levdiff == 0 else levdiff  # don't allow division by 0
                if lut is None:
                    efflut = fn.rescaleData(ind, scale=255.0 / levdiff, offset=minlev, dtype=np.ubyte)
                else:
                    lutdtype = np.min_scalar_type(lut.shape[0] - 1)
                    efflut = fn.rescaleData(
                        ind, scale=(lut.shape[0] - 1) / levdiff, offset=minlev, dtype=lutdtype, clip=(0, lut.shape[0] - 1)
                    )
                    efflut = lut[efflut]

                self._effectiveLut = efflut
            lut = self._effectiveLut
            levels = None

        # Assume images are in column-major order for backward compatibility
        # (most images are in row-major order)

        if self.axisOrder == "col-major":
            image = image.transpose((1, 0, 2)[: image.ndim])

        if self.logScale:
            with np.errstate(invalid="ignore"):
                levels = np.log(np.add(levels, 1))
            levels[0] = np.nanmax([levels[0], 0])

        argb, alpha = fn.makeARGB(image, lut=lut, levels=levels)
        self.qimage = fn.makeQImage(argb, alpha, transpose=False)

class LogScaleIntensity(BetterLayout, ComposableItemImageView):
    def __init__(self, *args, log_scale=True, **kwargs):
        # Composes a new type consisting of any ImageItem types in imageItem_bases with this classes's helper ImageItem
        # class (LogScaleImageItem)
        self.imageItem_bases += (LogScaleImageItem,)
        imageItem = type("DynamicImageItem", tuple(self.imageItem_bases), {})()
        if "imageItem" in kwargs:
            del kwargs["imageItem"]
        super(LogScaleIntensity, self).__init__(imageItem=imageItem, *args, **kwargs)

        self.logScale = log_scale

        # Setup log scale button
        self.logIntensityButton = QPushButton("Log Intensity")
        sizePolicy = QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(1)
        sizePolicy.setHeightForWidth(self.logIntensityButton.sizePolicy().hasHeightForWidth())
        self.logIntensityButton.setSizePolicy(sizePolicy)
        self.logIntensityButton.setObjectName("logIntensity")
        self.ui.right_layout.addWidget(self.logIntensityButton)
        self.logIntensityButton.setCheckable(True)
        self.setLogScale(self.logScale)
        self.logIntensityButton.clicked.connect(self._setLogScale)

    def _setLogScale(self, value):
        self.imageItem.logScale = value
        self.imageItem.qimage = None
        self.imageItem.update()
        self.getHistogramWidget().region.setBounds([0 if value else None, None])

    def setLogScale(self, value):
        self._setLogScale(value)
        self.logIntensityButton.setChecked(value)

class CatalogView(XArrayView):
    sigCatalogChanged = Signal(BlueskyRun)
    sigStreamChanged = Signal(str)
    sigFieldChanged = Signal(str)

    def __init__(self, catalog=None, stream=None, field=None, *args, **kwargs):
        self.catalog = catalog
        self.stream = stream
        self.field = field
        super(CatalogView, self).__init__(*args, **kwargs)
        self.setCatalog(self.catalog, self.stream, self.field)

    def setCatalog(self, catalog, stream=None, field=None, *args, **kwargs):
        self.catalog = catalog
        self.stream = stream
        self.field = field
        if not catalog:
            return
        if not field:
            try:
                field = catalog.metadata['start']['detectors'][0]
            except Exception as ex:
                msg.logError(ex)
            self.field = field
        self._updateCatalog(*args, **kwargs)
        self.sigCatalogChanged.emit(self.catalog)

    def _updateCatalog(self, *args, **kwargs):
        if all([self.catalog, self.stream, self.field]):
            try:
                stream = getattr(self.catalog, self.stream)
            except AttributeError as ex:
                msg.logError(ex)
                return

            eventStream = stream.to_dask()[self.field]

            # Trim off event dimension (so transpose works)
            if eventStream.ndim > 3:
                if eventStream.shape[0] == 1:  # if only one event, drop the event axis
                    eventStream = eventStream[0]
                if eventStream.shape[1] == 1:  # if z axis is unitary, drop that axis
                    eventStream = eventStream[:, 0]
            self.xarray = eventStream
            self.setImage(img=self.xarray, *args, **kwargs)
        else:
            # TODO -- clear the current image
            pass

    def setStream(self, stream):
        self.clear()
        self.stream = stream
        self._updateCatalog()
        self.sigStreamChanged.emit(stream)

    def setField(self, field):
        self.clear()
        self.field = field
        self._updateCatalog()
        # TODO -- figure out where to put the geometry update
        if QSpace in inspect.getmro(type(self)):
            self.setGeometry(pluginmanager.get_plugin_by_name("xicam.SAXS.calibration", "SettingsPlugin").AI(field))
        self.sigFieldChanged.emit(field)

class CrosshairROI(ImageView):

    def __init__(self, *args, **kwargs):
        super(CrosshairROI, self).__init__(*args, **kwargs)

        self.crosshair = BetterCrosshairROI(parent=self.view)

    def setImage(self, *args, reset_crosshair=True, **kwargs):
        super(CrosshairROI, self).setImage(*args, **kwargs)

        if reset_crosshair:
            transform = self.imageItem.viewTransform()
            new_pos = transform.map(self.imageItem.boundingRect().center())
            self.crosshair.setPos(new_pos)
            self.crosshair.sigMoved.emit(new_pos)

        # Must autorange again, since crosshair just moved
        if kwargs.get('autoRange'):
            self.autoRange()

class DepthPlot(XArrayView, CrosshairROI):
    def __init__(self, *args, **kwargs):
        super(DepthPlot, self).__init__()

        # self.roi = pg.RectROI((0,0), (1,1))
        # self.region_roi = pg.RectROI((0,0), (1,1))

        self.crosshair.sigMoved.connect(self.plotDepth)
        self._plotitem = self.ui.roiPlot.plot()  # type: pg.PlotDataItem

    def plotDepth(self):
        x, y = self.crosshair.pos()
        self._plotitem.setData(x=np.asarray(self.image.coords[self.image.dims[0]]),
                               y=self.image.sel(**{self.image.dims[2]: x, self.image.dims[1]: y}, method='nearest'))

class StreamSelector(CatalogView, BetterLayout):
    def __init__(self, *args, stream_filter=None, **kwargs):
        self.stream_filter = stream_filter
        self.streamComboBox = QComboBox()
        super(StreamSelector, self).__init__(*args, **kwargs)
        self.ui.right_layout.insertWidget(0, self.streamComboBox)
        self.streamComboBox.currentTextChanged.connect(self.setStream)

    def setCatalog(self, catalog, stream=None, field=None, *args, **kwargs):
        stream = self.updateStreamNames(catalog)
        super(StreamSelector, self).setCatalog(catalog, stream, field, *args, **kwargs)

    def updateStreamNames(self, catalog):
        self.streamComboBox.clear()
        if catalog:
            streams = streams_from_run(catalog)
            if self.stream_filter:
                streams = list(filter(is_image_field, streams))
            self.streamComboBox.addItems(streams)
            if 'primary' in streams:
                self.streamComboBox.setCurrentText('primary')

        return self.streamComboBox.currentText()

class FieldSelector(CatalogView, BetterLayout):
    def __init__(self, *args, field_filter: Callable = is_image_field, **kwargs):
        self.field_filter = field_filter
        self.fieldComboBox = QComboBox()
        super(FieldSelector, self).__init__(*args, **kwargs)

        self.ui.right_layout.insertWidget(0, self.fieldComboBox)
        self.fieldComboBox.currentTextChanged.connect(self.setField)

    def setCatalog(self, catalog, stream=None, field=None, *args, **kwargs):
        field = self.updateFieldNames(catalog, stream)
        super(FieldSelector, self).setCatalog(catalog, stream, field, *args, **kwargs)

    def setStream(self, stream_name):
        self.stream = stream_name
        self.updateFieldNames(self.catalog, stream_name)
        super(FieldSelector, self).setStream(stream_name)

    def updateFieldNames(self, catalog, stream):
        self.fieldComboBox.clear()
        if catalog and stream:
            fields = fields_from_stream(catalog, stream)
            if self.field_filter:
                fields = list(filter(partial(self.field_filter, catalog, stream), fields))
            self.fieldComboBox.addItems(fields)
            for field in fields:
                if '_image' in field:
                    self.fieldComboBox.setCurrentText(field)
                    break
        return self.fieldComboBox.currentText()

class MetaDataView(CatalogView, BetterLayout):
    def __init__(self, *args, **kwargs):
        self.metadataView = MetadataWidget()
        super(MetaDataView, self).__init__(*args, **kwargs)

        self.ui.splitter.addWidget(self.metadataView)
        self.ui.splitter.setCollapsible(self.ui.splitter.indexOf(self.metadataView), False)

        self.sigCatalogChanged.connect(self._updateView)

    def _updateView(self, catalog):
        self.metadataView.show_catalog(catalog)

class TimelineIndex(ImageView):
    def __init__(self, *args, **kwargs):
        super(TimelineIndex, self).__init__(*args, **kwargs)
        (ind, time) = self.timeIndex(self.timeLine)
        self.timeline_label = InfLineLabel(self.timeLine,text=f'{{value}} [{ind}]')
        # self.view.addItem(self.timeline_label)
    def timeLineChanged(self):
        super(TimelineIndex, self).timeLineChanged()
        (ind, time) = self.timeIndex(self.timeLine)
        self.timeline_label.setFormat(f'{{value}} [{ind}]')

class CatalogImagePlotView(StreamSelector, FieldSelector, TimelineIndex, MetaDataView):
    def __init__(self, catalog=None, stream=None, field=None, *args, **kwargs):
        # Turn off image field filtering for this mixin
        super(CatalogImagePlotView, self).__init__(catalog, stream, field, *args, **kwargs)
        self._breadcrumbs = []
        self.back_button = QPushButton('Back')
        self.add_button(self.back_button)
        self.back_button.hide()
        self.back_button.clicked.connect(self.go_back)

    def setData(self, data, *args, **kwargs):
        self._breadcrumbs.append((data, args, kwargs))
        self.axesItem.clearPlots()
        if len(self.catalog.metadata['start'].get('shape', [])) == 2 and len(data.shape) == 1 and len(self._breadcrumbs)==1:
            self.ui.roiPlot.hide()
            self.ui.histogram.hide()
            self.view.removeItem(self.imageItem)
            x = self.catalog.primary.to_dask()[self.catalog.metadata['start']['motors'][0]]
            y = self.catalog.primary.to_dask()[self.catalog.metadata['start']['motors'][1]]
            self.axesItem.plot(x=x, y=y, symbol='o', pen=mkPen(width=2))
            self.axesItem.setLabels(bottom=self.catalog.metadata['start']['motors'][0],
                                    left=self.catalog.metadata['start']['motors'][1])
            self.view.enableAutoRange(x=True, y=True)
            self.view.setAspectLocked(False)
        elif len(self.catalog.metadata['start'].get('shape', [])) == 2 and len(data.shape) > 1 and len(self._breadcrumbs)==1:
            self.ui.roiPlot.hide()
            self.ui.histogram.hide()
            self.view.removeItem(self.imageItem)
            x = self.catalog.primary.to_dask()[self.catalog.metadata['start']['motors'][0]]
            y = self.catalog.primary.to_dask()[self.catalog.metadata['start']['motors'][1]]
            self.plot_item = self.axesItem.plot(x=x, y=y, symbol='o', pen=mkPen(width=2))
            self.axesItem.setLabels(bottom=self.catalog.metadata['start']['motors'][0],
                                    left=self.catalog.metadata['start']['motors'][1])
            self.view.enableAutoRange(x=True, y=True)
            self.view.setAspectLocked(False)
            self.plot_item.sigPointsClicked.connect(self.points_clicked)

        elif len(data.shape) == 1:
            self.ui.roiPlot.hide()
            self.ui.histogram.hide()
            self.view.removeItem(self.imageItem)
            self.axesItem.plot(y=data, *args, **kwargs)
            if "labels" in kwargs:
                self.axesItem.setLabels(**kwargs['labels'])
            self.view.enableAutoRange(x=True, y=True)
            self.view.setAspectLocked(False)
        else:
            # TODO: find a way to represent x,y,j,k grid_scan arrays
            self.view.addItem(self.imageItem)
            img_kwargs = {}
            if 'x' in kwargs:
                img_kwargs['xvals'] = np.asarray(np.squeeze(kwargs['x']))
            self.setImage(data, **img_kwargs)
            self.ui.roiPlot.show()
            self.ui.histogram.show()
            self.view.setAspectLocked(True)
            if "labels" in kwargs:
                self.ui.roiPlot.plotItem.setLabels(**kwargs['labels'])

    def points_clicked(self, item, points, ev):
        index = points[0].index()
        data, args, kwargs = self._breadcrumbs[-1]
        self.setData(data[index])
        self.back_button.show()

    def go_back(self):
        self._breadcrumbs.pop()
        data, args, kwargs = self._breadcrumbs[-1]
        self._breadcrumbs.pop()
        self.setData(data, *args, **kwargs)
        if len(self._breadcrumbs) < 2:
            self.back_button.hide()

    def _updateCatalog(self, *args, **kwargs):
        if all([self.catalog, self.stream, self.field]):
            try:
                stream = getattr(self.catalog, self.stream)
            except AttributeError as ex:
                msg.logError(ex)
                return

            self.xarray = stream.to_dask()[self.field]

            if self.xarray.ndim > 3:
                self.xarray = np.squeeze(self.xarray)

            kwargs['antialias'] = True
            kwargs['pen'] = pg.mkPen(width=2)
            kwargs['symbol'] = 'o'
            kwargs['labels'] = {'left': self.field}

            if 'hints' in self.catalog.metadata['start']:
                try:
                    scan_axis_field_name = self.catalog.metadata['start']['hints']['dimensions'][0][0][0]
                except Exception as ex:
                    msg.logError(ex)
                else:
                    kwargs['x'] = stream.to_dask()[scan_axis_field_name]
                    kwargs['labels']['bottom'] = scan_axis_field_name

            self.setData(data=self.xarray, *args, **kwargs)
        else:
            # TODO -- clear the current image
            pass

class ImageItemHistogramOverflowFix(ImageItem):
    def getHistogram(self, bins="auto", step="auto", targetImageSize=200, targetHistogramSize=500, **kwds):
        """Returns x and y arrays containing the histogram values for the current image.
                For an explanation of the return format, see numpy.histogram().

                The *step* argument causes pixels to be skipped when computing the histogram to save time.
                If *step* is 'auto', then a step is chosen such that the analyzed data has
                dimensions roughly *targetImageSize* for each axis.

                The *bins* argument and any extra keyword arguments are passed to
                np.histogram(). If *bins* is 'auto', then a bin number is automatically
                chosen based on the image characteristics:

                * Integer images will have approximately *targetHistogramSize* bins,
                  with each bin having an integer width.
                * All other types will have *targetHistogramSize* bins.

                This method is also used when automatically computing levels.
                """
        if self.image is None:
            return None, None
        if step == "auto":
            step = (int(np.ceil(self.image.shape[0] / targetImageSize)), int(np.ceil(self.image.shape[1] / targetImageSize)))
        if np.isscalar(step):
            step = (step, step)
        stepData = self.image[:: step[0], :: step[1]]

        if bins == "auto":
            if stepData.dtype.kind in "ui":
                mn = stepData.min()
                mx = stepData.max()
                # print(f"\n*** mx, mn: {mx}, {mn} ({type(mx)}, {type(mn)})***\n")
                # PATCH -- explicit subtract with np.int to avoid overflow
                step = max(1, np.ceil(np.subtract(mx, mn, dtype=np.int) / 500.0))
                bins = np.arange(mn, mx + 1.01 * step, step, dtype=np.int)
                if len(bins) == 0:
                    bins = [mn, mx]
            else:
                bins = 500

        kwds["bins"] = bins
        stepData = stepData[np.isfinite(stepData)]
        hist = np.histogram(stepData, **kwds)

        return hist[1][:-1], hist[0]

class ImageViewHistogramOverflowFix(ComposableItemImageView):
    def __init__(self, *args, **kwargs):
        self.imageItem_bases += (ImageItemHistogramOverflowFix,)
        # Create a dynamic image item type, composing existing ImageItem classes with this classes's appropriate
        # ImageItem class (ImageItemHistogramOverflowFix)
        imageItem = type("DynamicImageItem", tuple(self.imageItem_bases), {})()
        if "imageItem" in kwargs:
            del kwargs["imageItem"]
        super(ImageViewHistogramOverflowFix, self).__init__(imageItem=imageItem, *args, **kwargs)

@live_plugin('ImageMixinPlugin')
class SliceSelector(BetterLayout):

    def __init__(self, *args, **kwargs):
        super(SliceSelector, self).__init__(*args, **kwargs)

        self.slice_selector = QComboBox()
        self.slice_selector.currentIndexChanged.connect(self.setCurrentIndex)
        self.ui.right_layout.addWidget(self.slice_selector)
        self.ui.roiPlot.setVisible(False)

    def setImage(self, img, *args, **kwargs):
        super(SliceSelector, self).setImage(img, *args, **kwargs)
        self.ui.roiPlot.setVisible(False)
        self.slice_selector.clear()
        self.slice_selector.addItems(list(map(str, self.tVals.ravel())))

    def setCurrentIndex(self, ind):
        super(SliceSelector, self).setCurrentIndex(ind)
        self.ui.roiPlot.setVisible(False)

@live_plugin("ImageMixinPlugin")
class ToolbarLayout(BetterLayout):
    """Mixin to support a toolbar at the top of the ImageView.

    Can pass in the toolbar you want via `toolbar`,
    and can modify toolbar via the `toolbar` attribute.
    """
    def __init__(self, *args, toolbar=None, **kwargs):
        super(ToolbarLayout, self).__init__(*args, **kwargs)
        self.toolbar = toolbar or QToolBar()

        # Define new layout
        self.toolbar_outer_layout = QVBoxLayout()
        self.toolbar_outer_layout.addWidget(self.toolbar)

        # Reinitialize the better layout
        self._reset_layout()  # FIXME: Find a way to remove this to prevent sensitivity to order of inheritance

        # Create new layout hierarchy (in this case, a new outer_layout that contains the original layouts within)
        outer_layout = self.ui.outer_layout
        self.toolbar_outer_layout.addLayout(outer_layout)
        self._set_layout(self.toolbar_outer_layout)

    def mkAction(self, iconpath: str = None, text=None, receiver=None, group=None, checkable=False, checked=False):
        actn = QAction(self)
        if iconpath: actn.setIcon(QIcon(QPixmap(str(path(iconpath)))))
        if text: actn.setText(text)
        if receiver: actn.triggered.connect(receiver)
        actn.setCheckable(checkable)
        if checked: actn.setChecked(checked)
        if group: actn.setActionGroup(group)
        return actn

class EwaldCorrected(QSpace, RowMajor, ToolbarLayout, ProcessingView):
    def __init__(self, *args, **kwargs):
        self.geometry_mode = 'transmission'
        self.incidence_angle = 0

        super(EwaldCorrected, self).__init__(*args, **kwargs)

        self.mode_group = QActionGroup(self)
        self.raw_action = self.mkAction('icons/raw.png', 'Raw', checkable=True, group=self.mode_group, checked=True)
        self.toolbar.addAction(self.raw_action)
        self.raw_action.triggered.connect(partial(self.setDisplayMode, DisplayMode.raw))
        self.cake_action = self.mkAction('icons/cake.png', 'Cake (q/chi plot)', checkable=True, group=self.mode_group)
        self.toolbar.addAction(self.cake_action)
        self.cake_action.triggered.connect(partial(self.setDisplayMode, DisplayMode.cake))
        self.remesh_action = self.mkAction('icons/remesh.png', 'Wrap Ewald Sphere', checkable=True,
                                           group=self.mode_group)
        self.toolbar.addAction(self.remesh_action)
        self.remesh_action.triggered.connect(partial(self.setDisplayMode, DisplayMode.remesh))

        # Disable these views initially, if geometry gets set, they will be enabled
        self.cake_action.setEnabled(False)
        self.remesh_action.setEnabled(False)
        self.toolbar.addSeparator()

    def setGeometry(self, geometry):
        super(EwaldCorrected, self).setGeometry(geometry)
        if geometry:
            self.cake_action.setEnabled(True)
            self.remesh_action.setEnabled(True)

    def setDisplayMode(self, mode):
        self.displaymode = mode
        if hasattr(self, "drawCenter"):
            self.drawCenter()
        self.setTransform()
        invoke_as_event(self.autoRange)
        self.setROIVisibility()

    def setROIVisibility(self):
        for item in self.view.items:
            if getattr(item, 'is_q_based', False):
                item.setVisible(self.displaymode == DisplayMode.remesh)
            elif getattr(item, 'is_px_based', False):
                item.setVisible(self.displaymode == DisplayMode.raw)

    def process(self, image):
        if self.displaymode == DisplayMode.remesh:
            image, q_x, q_z = remesh(np.asarray(image), self._geometry, reflection=self.geometry_mode == 'reflection', alphai=self.incidence_angle)
        return image

    def transform(self, img=None):
        if not self._geometry or not self.displaymode == DisplayMode.remesh:
            return super(EwaldCorrected, self).transform(img)  # Do pixel space transform when not calibrated

        while len(img.shape) > 2:
            img = img[0]

        img, q_x, q_z = remesh(np.asarray(img), self._geometry,
                               reflection=(self.geometry_mode or 'transmission') != 'transmission',
                               alphai=self.incidence_angle)

        # Build Quads
        shape = img.shape
        a = shape[-2] - 1, 0  # bottom-left
        b = shape[-2] - 1, shape[-1] - 1  # bottom-right
        c = 0, shape[-1] - 1  # top-right
        d = 0, 0  # top-left

        quad1 = QPolygonF()
        quad2 = QPolygonF()
        for p, q in zip([a, b, c, d], [d, c, b, a]):
            quad1.append(QPointF(*p[::-1]))
            quad2.append(QPointF(q_x[q], q_z[q]))

        transform = QTransform()
        QTransform.quadToQuad(quad1, quad2, transform)

        for item in self.view.items:
            if isinstance(item, ImageItem):
                item.setTransform(transform)
        self._transform = transform

        return self._transform

    def setImage(self, img, *args, geometry=None, geometry_mode=None, incidence_angle=None, **kwargs):
        if geometry_mode:
            self.geometry_mode = geometry_mode

        if incidence_angle is not None:
            self.incidence_angle = incidence_angle

        if geometry:
            self.setGeometry(geometry)

        if img is None:
            return

        if self._geometry:
            transform = self.transform(img)
            super(EwaldCorrected, self).setImage(img, *args, transform=transform, **kwargs)

        else:
            super(EwaldCorrected, self).setImage(img, *args, **kwargs)

    def updateAxes(self):
        if self.displaymode == DisplayMode.remesh:
            self.axesItem.setLabel("bottom", "qx (Å⁻¹)")  # , units='s')
            self.axesItem.setLabel("left", "qz (Å⁻¹)")
        else:
            super(EwaldCorrected, self).updateAxes()

class ROICreator(ToolbarLayout):
    """Implements a combo-box widget in the toolbar to select and create ROIs.

    Clicking on the combo-box will show all available ROIs;
    clicking on an ROI will generate the ROI intent and reset the combo-box back to its placeholder text.
    """
    def __init__(self, *args, **kwargs):
        super(ROICreator, self).__init__(*args, **kwargs)
        self.combobox = QComboBox()
        self.combobox.setPlaceholderText("Create ROI...")

        def get_icon(static_path: str):
            return QIcon(QPixmap(str((path(static_path))))) or QIcon()

        # Add roi options
        self.combobox.addItem(get_icon("icons/roi_arc.png"),
                              "Arc ROI",
                              partial(self._create_roi_action, self._create_arc_roi))
        self.combobox.addItem(get_icon("icons/roi_segmented_arc.png"),
                              "Segmented Arc ROI",
                              partial(self._create_roi_action, self._create_segmented_arc_roi))
        self.combobox.addItem(get_icon("icons/roi_rect.png"),
                              "Rectangle ROI",
                              partial(self._create_roi_action, self._create_rect_roi))
        self.combobox.addItem(get_icon("icons/roi_rect_segmented.png"),
                              "Segmented Rectangle ROI",
                              partial(self._create_roi_action, self._create_segmented_rect_roi))

        self.combobox.activated.connect(self._roi_activated)
        self.toolbar.addWidget(self.combobox)

    def _bounding_rect(self):
        rect = QRectF(self.imageItem.boundingRect())
        rect.setSize(rect.size() / 2)
        rect.moveCenter(self.imageItem.boundingRect().center())
        return rect

    def _create_arc_roi(self):
        r = self.transform(self.image).map(self.imageItem.boundingRect().bottomRight()).y() / 3
        if self._geometry is not None:
            fit = self._geometry.getFit2D()
            mode = getattr(self, 'displaymode', DisplayMode.raw)
            if mode == DisplayMode.raw:
                c = (fit['centerX'], fit['centerY'])
                return ArcPXROI(pos=c, radius=r, removable=False, movable=(self._geometry is None))
            elif mode == DisplayMode.remesh:
                c = (0, 0)
                return ArcQROI(pos=c, radius=r, removable=False, movable=(self._geometry is None))

    def _create_segmented_arc_roi(self):
        # FIXME: code duplication
        c = (0.0, 0.0)
        r = min(*self.image.shape[-2:]) / 3
        if self._geometry is not None:
            fit = self._geometry.getFit2D()
            c = (fit['centerX'], fit['centerY'])
        return SegmentedArcROI(pos=c, radius=r, removable=False, movable=(self._geometry is None))

    def _create_rect_roi(self):
        rect = self._bounding_rect()
        return BetterRectROI(rect.topLeft(), rect.size(), removable=False)

    def _create_segmented_rect_roi(self):
        rect = self._bounding_rect()
        return SegmentedRectROI(rect.topLeft(), rect.size(), removable=False)

    def _create_roi_action(self, roi_creator):
        roi_action = ROIAction(roi_creator())
        if roi_action.roi:
            self.parent().sigInteractiveAction.emit(roi_action, self.parent())

    def _roi_activated(self, index: int):
        # Ignore invalid index
        if index == -1:
            return

        self.combobox.itemData(index, Qt.UserRole)()
        self.combobox.setCurrentIndex(-1)

# class RectROIAction(BetterLayout):
#     def __init__(self, *args, **kwargs):
#         super(RectROIAction, self).__init__(*args, **kwargs)
#
#         self.button = QPushButton("Rectangle ROI")
#         self.button.clicked.connect(self._add_roi_action)
#         self.ui.right_layout.addWidget(self.button)
#
#     def _add_roi_action(self, _):
#         rect = QRectF(self.imageItem.boundingRect())
#         rect.setSize(rect.size()/2)
#         rect.moveCenter(self.imageItem.boundingRect().center())
#
#         roi_action = ROIAction(BetterRectROI(rect.topLeft(), rect.size()))
#         # parent is the XicamIntentCanvas
#         self.parent().sigInteractiveAction.emit(roi_action, self.parent())
#         # FIXME: removing ROIs
#         # self.button.setEnabled(False)

@live_plugin("ImageMixinPlugin")
class AxesLabels(ImageView):
    """Mixin for custom axes labels on an image view.

    This reserves usage of the kwarg "labels".
    """

    def __init__(self, *args, labels=None, **kwargs):
        if "view" in kwargs:
            raise ValueError("view cannot be passed as a kwarg")
        view = None
        if labels:
            view = pg.PlotItem()
            for axis, text in labels.items():
                view.setLabel(axis=axis, text=text)
        kwargs["view"] = view
        super(AxesLabels, self).__init__(*args, **kwargs)

@live_plugin('ImageMixinPlugin')
class DeviceView(BetterLayout):
    def __init__(self, *args, device=None, preprocess=None, max_fps=4, allow_active=True, **kwargs):
        super(DeviceView, self).__init__(*args, **kwargs)
        self.device = device
        self.preprocess = preprocess
        self.max_fps = max_fps
        self.thread = None
        self.passive = QPushButton('Passive')
        self.passive.setCheckable(True)
        self.passive.setChecked(True)
        if allow_active:
            self.ui.right_layout.addWidget(self.passive)
        self.getting_frame = False
        self._last_timestamp = time.time()
        self._autolevel = True

        self.acquire_progress = QProgressBar()
        self.acquire_progress.setTextVisible(True)
        self.acquire_progress.setFormat("%v of %m (%p%)")
        self.acquire_progress.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Minimum)
        self.ui.right_layout.addWidget(self.acquire_progress)

        self.error_text = pg.TextItem('Waiting for data...')
        self.view.addItem(self.error_text)

        self._update_action = None
        self.setPassive(self.passive.isChecked())
        self.passive.clicked.connect(self.setPassive)

        self.thread = threads.QThreadFuture(self._update_thread, showBusy=False,
                                            except_slot=lambda ex: self.device.unstage())
        self.thread.start()

    def active_trigger(self):
        self.device.trigger()
        self.updateFrame()

    def setPassive(self, passive):
        if passive:
            self._update_action = self.updateFrame
            self.passive.setText("Passive")
            self.passive.setStyleSheet('background-color: initial;')
        else:
            self._update_action = self.active_trigger
            self.passive.setText("Active")
            self.passive.setStyleSheet('background-color: red;')

    def _update_thread(self):
        from caproto import CaprotoTimeoutError
        from ophyd.signal import ConnectionTimeoutError
        while True:
            if self.visibleRegion().isEmpty():
                time.sleep(1)  # Sleep for 1 sec if the display is not in view
                continue

            try:
                if not self.device.connected:
                    with msg.busyContext():
                        msg.showMessage('Connecting to device...')
                        self.device.wait_for_connection()

                self._update_action()

                if hasattr(self.device, 'hdf5'):
                    num_captured = self.device.hdf5.num_captured.get()
                    num_images = self.device.cam.num_images.get()
                    capturing = self.device.hdf5.capture.get()
                    acquiring = self.device.cam.acquire.get()
                    if capturing:
                        threads.invoke_in_main_thread(self._update_progress, num_captured, num_images)
                    elif self._update_action == self.active_trigger or acquiring:  # Show 'busy' for just one exposure or active mode
                        threads.invoke_in_main_thread(self._update_progress, 0, 0)
                    else:
                        threads.invoke_in_main_thread(self._update_progress, 0, 1, show_text=False)

                while self.getting_frame:
                    time.sleep(.01)

            except (RuntimeError, CaprotoTimeoutError, ConnectionTimeoutError, TimeoutError) as ex:
                threads.invoke_in_main_thread(self.error_text.setText,
                                              'An error occurred communicating with this device.')
                msg.logError(ex)
            except Exception as e:
                threads.invoke_in_main_thread(self.error_text.setText,
                                              'Unknown error occurred when attempting to communicate with device.')
                msg.logError(e)

            t = time.time()
            max_period = 1 / self.max_fps
            current_elapsed = t - self._last_timestamp

            if current_elapsed < max_period:
                time.sleep(max_period - current_elapsed)

            self._last_timestamp = time.time()

    def updateFrame(self):
        try:
            image = self.device.image1.shaped_image.get()
        except ReadTimeoutError as ex:
            msg.logMessage('Unable to get shaped image from device:', self.device, level=msg.WARNING)
            return

        if image is not None and len(image):
            if self.preprocess:
                try:
                    image = self.preprocess(image)
                except Exception as ex:
                    pass
                    # msg.logError(ex)
            self.getting_frame = True
            threads.invoke_in_main_thread(self._setFrame, image)

    def _setFrame(self, image):
        try:
            if self.image is None and len(image):
                self.setImage(image, autoHistogramRange=True, autoLevels=True)
            else:
                self.imageDisp = None
                self.error_text.setText('')
                self.image = image
                # self.imageview.updateImage(autoHistogramRange=kwargs['autoLevels'])
                image = self.getProcessedImage()
                if self._autolevel:
                    self.ui.histogram.setHistogramRange(self.levelMin, self.levelMax)
                    self.autoLevels()
                self.imageItem.updateImage(image)

                self._autolevel = False

            self.error_text.setText(f'Update time: {(time.time() - self._last_timestamp):.2f} s')
        finally:
            self.getting_frame = False

    def _update_progress(self, current, total, show_text=True):
        self.acquire_progress.show()
        self.acquire_progress.setMaximum(total)
        self.acquire_progress.setValue(current)
        self.acquire_progress.setTextVisible(show_text)

@live_plugin('ImageMixinPlugin')
class AreaDetectorROI(DeviceView):
    def __init__(self, *args, roi_plugin=None, **kwargs):
        super(AreaDetectorROI, self).__init__(*args, **kwargs)

        if roi_plugin is None:
            roi_plugin = getattr(self.device, 'roi_stat1', None)
        self.roi_plugin = roi_plugin

        self.areadetector_roi = None

        self.roi_stat_text = pg.TextItem()
        self.view.addItem(self.roi_stat_text)

    def setImage(self, image,  *args, **kwargs):
        super(AreaDetectorROI, self).setImage(image, *args, **kwargs)
        if image is not None and image.size:
            if self.roi_plugin:
                pos = list(self.roi_plugin.min_.get())
                size = self.roi_plugin.size.get()
                pos[1] = self.image.shape[-2] - pos[1] - size[1]
                self.areadetector_roi = BetterRectROI(pos=pos, size=size)
                self.view.addItem(self.areadetector_roi)
                self.areadetector_roi.sigRegionChangeFinished.connect(self.roi_changed)

    def updateFrame(self):  # on frame updates, also get stats
        super(AreaDetectorROI, self).updateFrame()

        if self.roi_plugin:
            stats = self.roi_plugin.get()
            text = '\nROI Stats\n'
            for stat_name in ['max_value', 'mean_value', 'min_value', 'net', 'total']:
                text += f'{stat_name}: {getattr(stats, stat_name)}\n'
            threads.invoke_in_main_thread(self.roi_stat_text.setText, text)

    def roi_changed(self, roi):
        pos = roi.pos()
        size = roi.size()
        # pos[1] = self.image.shape[-2] - pos[1] - size[1]
        self.roi_plugin.min_.put(pos)
        self.roi_plugin.size.put(size)

if __name__ == "__main__":
    from qtpy.QtWidgets import QApplication

    qapp = QApplication([])

    from xicam.Acquire.devices.fastccd import ProductionCamTriggered

    d = ProductionCamTriggered('ES7011:FastCCD:', name='fastccd')
    w = AreaDetectorROI(device=d)
    w.show()

    qapp.exec_()


import logging

import google.cloud.logging
from tqdm import tqdm

logging.basicConfig(format="%(levelname)-7s %(asctime)s %(module)10s:%(lineno)5s %(message)s", datefmt="%m-%d %H:%M:%S")
logger = logging.getLogger("honeycomb")

def init():
    logger.setLevel(logging.INFO)
    client = google.cloud.logging.Client()
    client.setup_logging()

#: https://github.com/tqdm/tqdm/issues/313#issuecomment-812224667
class logging_tqdm(tqdm):
    def __init__(self, *args, **kwargs):
        self._logger = logger
        self._last_log_n = -1
        ncols = 80
        mininterval = 60 * 5  #: seconds
        super().__init__(*args, mininterval=mininterval, ncols=ncols, **kwargs)

    @property
    def logger(self):
        return logger

    def display(self, msg=None, pos=None):
        if not self.n:
            # skip progress bar before having processed anything
            return
        if self.n == self._last_log_n:
            # avoid logging for the same progress multiple times
            return
        self._last_log_n = self.n
        if msg is None:
            msg = self.__str__()
        if not msg:
            return

        self.logger.info("%s", msg)


# -*- coding: utf-8 -*-
"""
Created on Wed Dec  7 01:10:53 2022

@author: Louis
"""

import numpy as np
import matplotlib.pyplot as plt

D = 1000
a = 0.3
n1 = 150
b = 0.3
n2 = 200
c = 0

wA = 0.02
wB = 0.015
wC = 0.035
dA = 0.001
dB = 0.0006
dC = 0.00006
eA = 0.0006
eB = 0.0004
eC = 0.0001

days = list(range(1,D+1))
alist = []
blist = []
clist = []
totlist = []

for k in days:
    alist.append(a)
    blist.append(b)
    clist.append(c)
    totlist.append((a*n1*(n1 - 1) + b*n2*(n2 - 1) + c*n1*n2*2)/(n1*(n1 - 1) + n2*(n2 - 1) + n1*n2*2))
    anew = (1 - wA)*a + (1 - a)*(dA + eA*((n1 - 2)*a**2 + n2*c**2))
    bnew = (1 - wB)*b + (1 - b)*(dB + eB*((n2 - 2)*b**2 + n1*c**2))
    cnew = (1 - wC)*c + (1 - c)*(dC + eC*((n1 - 1)*a*c + (n2 - 1)*b*c))
    a = anew
    b = bnew
    c = cnew

fig, axs = plt.subplots(2, 2)
axs[0,0].plot(days, alist)
axs[0,1].plot(days, blist)
axs[1,0].plot(days, clist)
axs[1,1].plot(days, totlist)
axs[0,0].set_title('School 1')
axs[0,1].set_title('School 2')
axs[0,0].set(ylabel='edge density')
axs[0,1].set()
axs[1,0].set_title('Between Schools')
axs[1,1].set_title('Total System')
axs[1,0].set(xlabel='day', ylabel='edge density')
axs[1,1].set(xlabel='day')


from flask import Blueprint, request
from rpc import ArticleService
from server import app
from server.message import Message

article = Blueprint('article', app.name)

@article.get('/create')
def create():
    """
    Create new article

    :rtype: flask.Response
    """
    m = Message()

    id = request.args.get('id')
    title = request.args.get('title')
    description = request.args.get('description')
    user_id = int(request.args.get('user_id'))

    ars = ArticleService()

    # create new article
    res = ars.create(id, title, description, user_id)

    m.status = res.status

    return m.json()

@article.get('/list')
def list():
    """
    Get all articles

    :rtype: flask.Response
    """
    m = Message()

    ars = ArticleService()

    # create new article
    res = ars.list()

    m.status = res.status

    if res.status:
        m.articles = res.articles

    return m.json()


from selenium import webdriver
from oop.test_home_search import HomePage
from test_searching_item import SearchPage
from test_item import ItemPage
from test_home_item_added import HomeItemAddedPage
from test_basket import BasketPage

driver = webdriver.Chrome()
driver.get('https://shop.kz/')

def test_home():
    home_page = HomePage(driver)
    home_page.click_search()
    home_page.search("a4tech")
    assert driver.current_url != "http://www.google.com/"

search_page = SearchPage(driver)
search_page.click_item()

item_page = ItemPage(driver)
item_page.add_item()
item_page.go_basket()
item_page.go_home()

home_item_added_page = HomeItemAddedPage(driver)
home_item_added_page.click_basket()

basket_page = BasketPage(driver)
basket_page.click_delete_item()


streak = 0
frame = 0
columns = [
    {
        'key': 'j',
        'x': 220,
        'color': color(255, 0, 100),
        'notes': []
    },{
        'key': 'k',
        'x': 300,
        'color': color(0, 255, 100),
        'notes': []
    },{
        'key': 'l',
        'x': 380,
        'color': color(100, 0, 255),
        'notes': []
    }
]

def setup():
    size(600, 400)

def draw():
    global streak, notes, columns, frame

    if frame % int(frameRate) == 0:
        for column in columns:
            if (len(column['notes']) == 0 or column['notes'][-1] > 30) and int(random(3)) == 0:
                column['notes'].append(-25)

    background(0, 0, 0)
    fill(255, 255, 255)
    text(str(streak), 10, 20)

    for column in columns:
        # circle
        fill(0, 0, 0)
        stroke(column['color'])
        ellipse(column['x'],300, 70, 70)

        # notes
        noStroke()
        fill(column['color'])
        for i in range(len(column['notes'])):
            noteY = column['notes'][i]
            ellipse(column['x'], noteY, 50, 50)
            column['notes'][i] += 1
        if column['notes'][0] >= 425:
            column['notes'].pop(0)

    frame += 1

def keyPressed():
    global streak, notes

    for column in columns:
        if key == column['key']:
            for i in range(len(column['notes'])):
                noteY = column['notes'][i]
                if noteY > 280 and noteY < 320:
                    streak += 1
                    column['notes'].pop(i)
                    break
                else:
                    streak = 0


from concurrent.futures import ThreadPoolExecutor
from datetime import datetime
import json
from time import sleep
from typing import List
from numpy.random import uniform, randint

from benchmark.utils import create_dirs, generate_customer_ids, generate_customer_object, generate_random_id
from clients.client import ApplicationClient

def run_test(
    client: ApplicationClient,
    *,
    sample_size: int = 100,
    batch_step: int = 10,
    write_ratio: float = 0.2,
    output_dir: str = 'out/scalability'
):
    """_summary_

    Args:
        client (ApplicationClient): _description_
        sample_size (int, optional): _description_. Defaults to 100.
        batch_step (int, optional): _description_. Defaults to 10.
        write_ratio (float, optional): _description_. Defaults to 0.2.

    Returns:
        _type_: _description_
    """
    create_dirs(['out/', 'out/scalability/'])

    warmup_results = warmup_service(client, experiment_sample_size=sample_size)

    sleep(60.0)

    start_time = datetime.now()
    output = []

    size = batch_step
    customer_ids = generate_customer_ids(sample_size // batch_step)
    while size < sample_size + 1:
        print(
            f'running batch Nr {size // batch_step} with {size} Threads')

        res = run_batch_scalability_experiment(
            client,
            customer_ids,
            batch_size=size,
            write_ratio=write_ratio
        )

        output.append(res)

        # increment step
        size = size + batch_step

    result = {
        'provider': client.provider,
        'location': client.location,
        'time': start_time.isoformat(),
        'result': output,
        'warmup_result': warmup_results,
        'test_type': 'scalability',
        'test_parametres': {
            'sample_size': sample_size,
            'batch_step': batch_step,
            'write_ratio': write_ratio
        }
    }

    with open(f'{output_dir}/scalability_{client.provider}_{client.location}_{start_time.isoformat()}.json', 'w') as f:
        json.dump(result, f)

    return result

def run_incremental_test(
    client: ApplicationClient,
    *,
    sample_size: int = 20,
    round_duration: float = 60.0,
    time_offset: float = 5.0,
    write_ratio: float = 0.2,
    output_dir: str = 'out/scalability'
):
    """_summary_

    Args:
        client (ApplicationClient): _description_
        sample_size (int, optional): _description_. Defaults to 20.
        round_duration (float, optional): _description_. Defaults to 60.0.
        time_offset (float, optional): _description_. Defaults to 5.0.
    """
    # create_dirs(['out/', 'out/scalability/', output_dir])

    start_time = datetime.now()
    warmup_results = warmup_service(client, experiment_sample_size=sample_size)

    customer_ids = generate_customer_ids(sample_size)
    output = []
    for i in range(1, sample_size + 1):
        print(
            f'------- running round number {i} for {round_duration}s ------------')
        res = run_scalability_round(
            client,
            customer_ids,
            duration=round_duration,
            size=i,
            time_offset=time_offset
        )
        output.append(res)

    result = {
        'provider': client.provider,
        'location': client.location,
        'time': start_time.isoformat(),
        'result': output,
        'warmup_result': warmup_results,
        'test_type': 'scalability',
        'test_parametres': {
            'sample_size': sample_size,
            'round_duration': round_duration,
            'time_offset': time_offset,
            'write_ratio': write_ratio
        }
    }

    # with open(f'{output_dir}/scalability_{client.provider}_{client.location}_{start_time.isoformat()}.json', 'w') as f:
    #    json.dump(result, f)

    from utils.storage import upload_json
    upload_json(
        result,
        f'{output_dir}/scalability_{client.provider}_{client.location}_{start_time.isoformat()}.json'
    )

def run_scalability_round(client: ApplicationClient, customer_ids: List[str], *, duration: float = 60.0, size: int = 1, time_offset: float = 5.0):
    output = []
    start_time = datetime.now().timestamp()
    now = start_time
    while now < start_time + duration:
        res = run_batch_scalability_experiment(
            client, customer_ids, batch_size=size)
        output.extend(res)
        sleep(time_offset)
        now = datetime.now().timestamp()

    return output

def run_batch_scalability_experiment(client: ApplicationClient, customer_ids: List[str], *, batch_size: int = 10, write_ratio: float = 0.2) -> List[dict]:
    """_summary_

    Args:
        client (ApplicationClient): _description_
        batch_size (int, optional): _description_. Defaults to 10.
        write_ratio (float, optional): _description_. Defaults to 0.2.

    Returns:
        List[dict]: _description_
    """
    args = list()
    for _ in range(batch_size):
        customer_id = customer_ids[randint(0, len(customer_ids) - 1)]
        args.append((
            client,
            customer_id,
            write_ratio,
        ))

    with ThreadPoolExecutor(max_workers=batch_size) as pool:
        responses = list(pool.map(
            run_scalability_experiment,
            args
        ))

    return responses

# def run_scalability_experiment(client: ApplicationClient, customer_id: str, write_ratio: float = 0.2) -> dict:
def run_scalability_experiment(args) -> dict:
    """send a single request

    Args:
        client (ApplicationClient): _description_
        customer_id (str): customer id
        write_ratio (float, optional): _description_. Defaults to 0.2.

    Returns:
        dict: _description_
    """
    r = uniform()

    if r > args[2]:
        res = args[0].get_customer(args[1])
    else:
        res = args[0].update_customer(
            args[1], generate_customer_object(args[1]))

    print(f'request {res.method} returned with {res.status_code} in {res.rtt}')
    return res.to_dict()

def warmup_service(client: ApplicationClient, *, experiment_sample_size: int = 100):
    """warms up a service to avoid result distortion due to cold starts

    Args:
        client (ApplicationClient): _description_
        experiment_sample_size (int, optional): _description_. Defaults to 100.
    """

    batch_size = experiment_sample_size // 5

    random_customer_ids = generate_customer_ids(batch_size)

    results = run_batch_scalability_experiment(
        client, random_customer_ids, batch_size=batch_size)

    return results


from os import name
from django.urls import path

from . import views

urlpatterns = [
    path('categories', views.CategoryAPIView.as_view(), name='categories'),
    path('categories/', views.CategorySearchAPIView.as_view()),
    path('categories/<uuid:category_id>', views.CategoryUpdateDeleteAPIView.as_view()),
    path('categories/<uuid:category_id>/products', views.CategoryProductsAPIView.as_view(), name='products-category'),
    path('categories/latest', views.LatestCategoryAPIView.as_view()),
    path('categories/most-viewed', views.MostViewedCategoryAPIView.as_view()),

]


'''
Question Description :-
Add Two Numbers II

You are given two non-empty linked lists representing two non-negative integers. The most significant digit comes first and each of their nodes contain a single digit.
Add the two numbers and return it as a linked list.
You may assume the two numbers do not contain any leading zero, except the number 0 itself.

Follow up:
What if you cannot modify the input lists? In other words, reversing the lists is not allowed.

Example:
    Input: (7 -> 2 -> 4 -> 3) + (5 -> 6 -> 4)
    Output: 7 -> 8 -> 0 -> 7
'''
# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next
class Solution:
    def addTwoNumbers(self, l1: ListNode, l2: ListNode) -> ListNode:

        if not l1 and not l2:
            return None
        num1 = 0
        while l1:
            num1 = num1*10 + l1.val
            l1 = l1.next
        num2 = 0
        while l2:
            num2 = num2*10 + l2.val
            l2 = l2.next
        num1 += num2

        ans = []
        while num1 > 0:
            ans.insert(0,num1%10)
            num1 = num1//10
        if len(ans) == 0:
            temp = ListNode(0)
            return temp
        temp = ListNode(ans[0])
        temp1 = temp
        for x in ans[1:]:
            temp1.next = ListNode(x)
            temp1 = temp1.next

        return temp


import sys
import jieba

class WordToken(object):
    def __init__(self):
        # 最小起始id号, 保留的用于表示特殊标记
        self.START_ID = 4
        self.word2id_dict = {}
        self.id2word_dict = {}

    def load_file_list(self, file_list, min_freq):
        """
        加载样本文件列表，全部切词后统计词频，按词频由高到低排序后顺次编号
        并存到self.word2id_dict和self.id2word_dict中
        """
        words_count = {}
        for file in file_list:
            with open(file, 'r') as file_object:
                for line in file_object.readlines():
                    line = line.strip()
                    seg_list = jieba.cut(line)
                    for str in seg_list:
                        if str in words_count:
                            words_count[str] = words_count[str] + 1
                        else:
                            words_count[str] = 1

        sorted_list = [[v[1], v[0]] for v in words_count.items()]
        sorted_list.sort(reverse=True)
        for index, item in enumerate(sorted_list):
            word = item[1]
            if item[0] < min_freq:
                break
            self.word2id_dict[word] = self.START_ID + index
            self.id2word_dict[self.START_ID + index] = word
        return index

    def word2id(self, word):
        if not isinstance(word, str):
            print("Exception: error word not unicode")
            sys.exit(1)
        if word in self.word2id_dict:
            return self.word2id_dict[word]
        else:
            return None

    def id2word(self, id):
        id = int(id)
        if id in self.id2word_dict:
            return self.id2word_dict[id]
        else:
            return None

    def get_train_set():
        global num_encoder_symbols, num_decoder_symbols
        train_set = []
        with open('../chatbot/question.txt', 'rb') as question_file:
            with open('../chatbot/answer.txt', 'rb') as answer_file:
                while True:
                    question = question_file.readline()
                    answer = answer_file.readline()
                    if question and answer:
                        question = question.strip()
                        answer = answer.strip()

                        question_id_list = get_id_list_from(question)
                        answer_id_list = get_id_list_from(answer)
                        answer_id_list.append(EOS_ID)
                        train_set.append([question_id_list, answer_id_list])
                    else:
                        break
        return train_set

    def get_id_list_from(sentence):
        sentence_id_list = []
        seg_list = jieba.cut(sentence)
        for str in seg_list:
            id = wordToken.word2id(str)
            if id:
                sentence_id_list.append(wordToken.word2id(str))
        return sentence_id_list


# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, x):
#         self.val = x
#         self.next = None

class Solution:
    """my 2-pointers sol, 2nd attempt"""
    def detectCycle(self, head: ListNode) -> ListNode:

        #         1 2 3 4 5 6 7 8 9
        #         1 2 3 4 2 3 4 2 3

        #         1 3 5 7 9 11131517
        #         1 3 2 4 3 2 4 3 2

        # find circle
        slow = fast = head
        while fast and fast.next:
            slow, fast = slow.next, fast.next.next
            if slow == fast:
                p1, p2 = head, slow
                break
        else:
            return None

        # find cycle start
        while p1 != p2:
            p1, p2 = p1.next, p2.next
        return p1

Problems/src/142. Linked List Cycle II.py


from microbit import *
import radio

radio.config(group=0)
radio.on()

uart.init(baudrate=115200)

def print(*args, **kwargs):
    pass # disable print function

display.show("-")
while True:
    try:
        msg = radio.receive()
        if msg is not None:
            display.show("_")
            uart.write(msg + "\r")

        msg = uart.readline()
        if msg is not None:
            display.show("^")
            radio.send(msg)
    except:
        display.show("X")
        radio.off()
        sleep(250)
        radio.on()
        display.show("-")


class TreeNode(object):

    def __init__(self, val: str):
        if val.isalnum():
            self.val = val
            self.children = [None]*26

class Solution(object):

    def __init__(self):
        self.root = TreeNode('0')
        self.result = ""

    def longestWord(self, words):

        """
        :type words: List[str]
        :rtype: str
        """

        words = list(set(words))
        for word in words:
            word = word.lower()
            self.insert(word)
        self.find_longest_word(self.root, [], words)

        if len(self.result) <= 0:
            return None
        return self.result

    def insert(self, word):
        curr = self.root
        for w in word:
            if curr.children[ord(w) - ord('a')] is None:
                tmp = TreeNode(w)
                curr.children[ord(w) - ord('a')] = tmp
            curr = curr.children[ord(w) - ord('a')]

    def find_longest_word(self, node, curr, words):
        ret = ''.join(curr)
        if ret in "" or ret in words:
            if len(ret) > len(self.result):
                self.result = ret

        for x in node.children:
            if x is not None:
                if ret in "" or ret in words:
                    curr.append(x.val)
                    self.find_longest_word(x, curr, words)
                    curr.pop()

words = ["a", "banana", "app", "appl", "ap", "apply", "apple"]
s = Solution()
print(s.longestWord(words))


import struct
import json
import array
from hitbox import *
from util import *

assert array.array('H').itemsize == 2

MAGIC_NUMBER = b"PlatElevel"

# Header data
#  length of name
#  4 floats for boundaries
#  number of tilemaps
#  number of static objects
#  number of entity spawn points
#  number of areas
#  number of edge triggers
#  total number of tiles in all tilemaps
#  total number of colliders
#  total number of hitboxes
Header = struct.Struct("<I4f5I3I")

# Tilemap struct
#  Length of filename of tileset
#  width x height of tilemap
#  z_order
#  top-left offset
#  scale
#  parallax
#  solidness of tile data
Tilemap = struct.Struct("<I2Ii2f2f2f?")

# SceneObject struct
#  length of filename of texture
#  SDL_Rect clip;
#  Vector2 position;
#  int z_order;
#  float rotation;
#  Vector2 scale;
#  CollisionData collision;
SceneObject = struct.Struct("<I4I2f2fif2fI")

# EntitySpawnPoint struct
#  location of spawn
#  length of name of entity class
EntitySpawnPoint = struct.Struct("<2fI")

# LevelArea
#  AABB of boundaries
#  priority
#  r, g, b color values
LevelArea = struct.Struct("<4fi3B")

# EdgeTrigger
#  side enum
#  position
#  size
#  strictness
EdgeTrigger = struct.Struct("<cfff")
EdgeTriggerSides = {
    "top": b't',
    "bottom": b'b',
    "left": b'l',
    "right": b'r'
}

'tests if nested list is regular (i.e. not jagged)'
def regular(l2d):
    if len(l2d) == 0:
        return True

    width = len(l2d[0])
    for row in l2d:
        if len(row) != width:
            return False

    return True

def build(infile, outfile):
    level = None
    with open(infile, 'r') as f:
        try:
            level = json.load(f)
        except Exception as e:
            raise Exception("Error in parsing '{}': {}".format(infile, str(e)))

    # Sanity check / header prep
    name = level["name"].encode()

    n_vertices = 0
    nested_hitboxes = 0
    n_tiles = 0
    for index, tilemap in enumerate(level["tilemaps"]):
        if len(tilemap['tiles']) == 0:
            raise Exception("Tilemap #{} is empty".format(index))
        if not regular(tilemap['tiles']):
            raise Exception("Tilemap #{} has inconsistent widths and is therefore invalid".format(index))

        n_tiles += len(tilemap["tiles"][0]) * len(tilemap["tiles"])

    for obj in level["objects"]:
        for collider in obj["collision"]:
            h, v = count_nested_hitboxes_and_vertices(collider["hitbox"])
            n_vertices += v
            nested_hitboxes += h

    with open(outfile, "wb") as f:
        f.write(MAGIC_NUMBER)

        f.write(Header.pack(
            len(name),
            level["boundary"]["left"],
            level["boundary"]["right"],
            level["boundary"]["top"],
            level["boundary"]["bottom"],
            len(level["tilemaps"]),
            len(level["objects"]),
            len(level["entities"]),
            len(level["areas"]),
            len(level["edge_triggers"]),
            n_tiles,
            n_vertices,
            nested_hitboxes
        ))

        f.write(name)

        for tilemap in level["tilemaps"]:
            tileset = tilemap["tileset"].encode()

            scale = tilemap.get("scale", 1)
            parallax = tilemap.get("parallax", 1)

            if isinstance(scale, int) or isinstance(scale, float):
                scale = {'x': scale, 'y': scale}

            if isinstance(parallax, int) or isinstance(parallax, float):
                parallax = {'x': parallax, "y": parallax}

            f.write(Tilemap.pack(
                len(tileset),
                len(tilemap["tiles"][0]),
                len(tilemap["tiles"]),
                tilemap["z_order"],
                tilemap["offset"]["x"],
                tilemap["offset"]["y"],
                scale["x"],
                scale["y"],
                parallax["x"],
                parallax["y"],
                tilemap.get("solid")
            ))

            f.write(tileset)

            for row in tilemap["tiles"]:
                array.array("H", row).tofile(f)

        for obj in level["objects"]:
            texture = obj["texture"].encode()

            f.write(SceneObject.pack(
                len(texture),
                obj["clip"]["x"],
                obj["clip"]["y"],
                obj["clip"]["w"],
                obj["clip"]["h"],
                obj["display"]["x"],
                obj["display"]["y"],
                obj["position"]["x"],
                obj["position"]["y"],
                obj["z_order"],
                obj.get("rotation", 0),
                obj.get("scale", {}).get("x", 1),
                obj.get("scale", {}).get("y", 1),
                len(obj["collision"])
            ))

            f.write(texture)

            for collider in obj["collision"]:
                write_collider(f, collider)

        for ent in level["entities"]:
            entityclass = ent["class"].encode()

            f.write(EntitySpawnPoint.pack(
                ent["location"]["x"],
                ent["location"]["y"],
                len(entityclass)
            ))

            f.write(entityclass)

        for area in level["areas"]:
            f.write(LevelArea.pack(
                area["boundary"]["left"],
                area["boundary"]["right"],
                area["boundary"]["top"],
                area["boundary"]["bottom"],
                area["priority"],
                str2rgb(area["ui_color"])
            ))

        for edgetrig in level["edge_triggers"]:
            f.write(EdgeTrigger.pack(
                EdgeTriggerSides[edgetrig["side"].lower()],
                edgetrig["position"],
                edgetrig["size"],
                edgetrig.get("strictness", 0)
            ))


import os
from typing import Dict
import plotly.graph_objects as go
from plotly.graph_objects import Figure

IMAGE_DIR = 'images'
show_interactive_figure = True

def create_sankey_plot_for_overall_data(category_overall_data: Dict[str, Dict[str, float]]):
    """Create sankey plot for `category_overall_data`. Get the file path to the generated image."""
    # data
    label = []
    source = []
    target = []
    value = []

    # parse data into sankey lists
    total_income_index = len(list(category_overall_data['income'].keys()))
    for index, minor_category in enumerate(category_overall_data['income'].keys()):
        label.append(minor_category)
        source.append(index)
        target.append(total_income_index)
        value.append(category_overall_data['income'][minor_category])

    label.append('Total Income')

    for index, minor_category in enumerate(category_overall_data['expenses'].keys(), start=total_income_index + 1):
        label.append(minor_category)
        source.append(total_income_index)
        target.append(index)
        value.append(category_overall_data['expenses'][minor_category])

    # data to dict, dict to sankey
    link = dict(source=source, target=target, value=value)
    node = dict(label=label, pad=50, thickness=5)
    data = go.Sankey(link=link, node=node)
    # plot
    fig = go.Figure(data)
    if show_interactive_figure:
        fig.show()
    return write_image_file(fig)

def write_image_file(figure: Figure) -> str:
    """Write image file and get the image path."""
    if not os.path.exists(IMAGE_DIR):
        os.mkdir(IMAGE_DIR)
    path = f'{IMAGE_DIR}/sankey1.png'
    figure.write_image(path)
    return path


class Tools:
    count = 0

    @classmethod
    def show_tool(cls):
        print("工具对象的数量 %d" % cls.count)

    def __init__(self, name):
        self.name = name
        Tools.count += 1

tool1 = Tools("1")
Tools.show_tool()


import motor.motor_asyncio
from decouple import config

# name of the database
MONGODB_DB = config('DB_NAME', default='news_api')

# name of the collection
MONGODB_COLLECTION = config('COLLECTION_NAME', default='news')

#  mongodb://<dbUser>:<dbUserPassword>@host:port
MONGO_DETAILS = config('MONGO_DETAILS', default='mongodb://127.0.0.1:27017')

client = motor.motor_asyncio.AsyncIOMotorClient(MONGO_DETAILS)

database = client[MONGODB_DB]

news_collection = database[MONGODB_COLLECTION]

async def retrieve_news(source: str = None):
    news_list = []

    if source:
        async for news in news_collection.find({'source': source}):
            news_list.append(news_helper(news))
    else:
        async for news in news_collection.find():
            news_list.append(news_helper(news))

    return news_list

# helpers
def news_helper(news) -> dict:
    """Parsing the results from a database query into a Python dict."""
    return {
        'id': str(news['_id']),
        'title': news['title'],
        'link': news['link'],
        'image_url': news['image_url'],
        'source': news['source'],
    }


# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class BSTIterator:
  def __init__(self, root: Optional[TreeNode]):
    # current node
    self._current = root
    # values of all nodes traversed inorder
    self._stack = []

    # descend furthest down left subtree
    while self._current:
      self._stack.append(self._current)
      self._current = self._current.left

  def next(self) -> int:
    while True:
      # attempt to traverse left subtree
      if self._current:
        self._stack.append(self._current)
        self._current = self._current.left
      # if left subtree is None, pop from stack and look at right subtree
      # return value of popped node
      elif self._stack:
        cur = self._stack.pop()
        self._current = cur.right
        return cur.val

  def hasNext(self) -> bool:
    # node cannot have next inorder if tree is empty
    # tree is empty if left subtree is None and no parent node to backtrack to
    if not self._current and not self._stack:
      return False
    return True

# Your BSTIterator object will be instantiated and called as such:
# obj = BSTIterator(root)
# param_1 = obj.next()
# param_2 = obj.hasNext()

Problems/0173. (M) Binary Search Tree Iterator/Python3/solution_2.py