# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2018-2019 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# ###########################################################################*/
"""A widget dedicated to display scatter plots

It is based on a :class:`~silx.gui.plot.PlotWidget` with additional tools
for scatter plots.
"""

__authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "14/06/2018"


import logging
import weakref

import numpy

from . import items
from . import PlotWidget
from . import tools
from .tools.profile import ScatterProfileToolBar
from .ColorBar import ColorBarWidget
from .ScatterMaskToolsWidget import ScatterMaskToolsWidget

from ..widgets.BoxLayoutDockWidget import BoxLayoutDockWidget
from .. import qt, icons
from ...utils.proxy import docstring
from ...utils.weakref import WeakMethodProxy


_logger = logging.getLogger(__name__)


class ScatterView(qt.QMainWindow):
    """Main window with a PlotWidget and tools specific for scatter plots.

    :param parent: The parent of this widget
    :param backend: The backend to use for the plot (default: matplotlib).
                    See :class:`~silx.gui.plot.PlotWidget` for the list of supported backend.
    :type backend: Union[str,~silx.gui.plot.backends.BackendBase.BackendBase]
    """

    _SCATTER_LEGEND = ' '
    """Legend used for the scatter item"""

    def __init__(self, parent=None, backend=None):
        super(ScatterView, self).__init__(parent=parent)
        if parent is not None:
            # behave as a widget
            self.setWindowFlags(qt.Qt.Widget)
        else:
            self.setWindowTitle('ScatterView')

        # Create plot widget
        plot = PlotWidget(parent=self, backend=backend)
        self._plot = weakref.ref(plot)

        # Add an empty scatter
        self.__createEmptyScatter()

        # Create colorbar widget with white background
        self._colorbar = ColorBarWidget(parent=self, plot=plot)
        self._colorbar.setAutoFillBackground(True)
        palette = self._colorbar.palette()
        palette.setColor(qt.QPalette.Background, qt.Qt.white)
        palette.setColor(qt.QPalette.Window, qt.Qt.white)
        self._colorbar.setPalette(palette)

        # Create PositionInfo widget
        self.__lastPickingPos = None
        self.__pickingCache = None
        self._positionInfo = tools.PositionInfo(
            plot=plot,
            converters=(('X', WeakMethodProxy(self._getPickedX)),
                        ('Y', WeakMethodProxy(self._getPickedY)),
                        ('Data', WeakMethodProxy(self._getPickedValue)),
                        ('Index', WeakMethodProxy(self._getPickedIndex))))

        # Combine plot, position info and colorbar into central widget
        gridLayout = qt.QGridLayout()
        gridLayout.setSpacing(0)
        gridLayout.setContentsMargins(0, 0, 0, 0)
        gridLayout.addWidget(plot, 0, 0)
        gridLayout.addWidget(self._colorbar, 0, 1)
        gridLayout.addWidget(self._positionInfo, 1, 0, 1, -1)
        gridLayout.setRowStretch(0, 1)
        gridLayout.setColumnStretch(0, 1)
        centralWidget = qt.QWidget(self)
        centralWidget.setLayout(gridLayout)
        self.setCentralWidget(centralWidget)

        # Create mask tool dock widget
        self._maskToolsWidget = ScatterMaskToolsWidget(parent=self, plot=plot)
        self._maskDock = BoxLayoutDockWidget()
        self._maskDock.setWindowTitle('Scatter Mask')
        self._maskDock.setWidget(self._maskToolsWidget)
        self._maskDock.setVisible(False)
        self.addDockWidget(qt.Qt.BottomDockWidgetArea, self._maskDock)

        self._maskAction = self._maskDock.toggleViewAction()
        self._maskAction.setIcon(icons.getQIcon('image-mask'))
        self._maskAction.setToolTip("Display/hide mask tools")

        # Create toolbars
        self._interactiveModeToolBar = tools.InteractiveModeToolBar(
            parent=self, plot=plot)

        self._scatterToolBar = tools.ScatterToolBar(
            parent=self, plot=plot)
        self._scatterToolBar.addAction(self._maskAction)

        self._profileToolBar = ScatterProfileToolBar(parent=self, plot=plot)

        self._outputToolBar = tools.OutputToolBar(parent=self, plot=plot)

        # Activate shortcuts in PlotWindow widget:
        for toolbar in (self._interactiveModeToolBar,
                        self._scatterToolBar,
                        self._profileToolBar,
                        self._outputToolBar):
            self.addToolBar(toolbar)
            for action in toolbar.actions():
                self.addAction(action)


    def __createEmptyScatter(self):
        """Create an empty scatter item that is used to display the data

        :rtype: ~silx.gui.plot.items.Scatter
        """
        plot = self.getPlotWidget()
        plot.addScatter(x=(), y=(), value=(), legend=self._SCATTER_LEGEND)
        scatter = plot._getItem(
            kind='scatter', legend=self._SCATTER_LEGEND)
        # Profile is not selectable,
        # so it does not interfere with profile interaction
        scatter._setSelectable(False)
        return scatter

    def _pickScatterData(self, x, y):
        """Get data and index and value of top most scatter plot at position (x, y)

        :param float x: X position in plot coordinates
        :param float y: Y position in plot coordinates
        :return: The data index and value at that point or None
        """
        pickingPos = x, y
        if self.__lastPickingPos != pickingPos:
            self.__pickingCache = None
            self.__lastPickingPos = pickingPos

            plot = self.getPlotWidget()
            if plot is not None:
                pixelPos = plot.dataToPixel(x, y)
                if pixelPos is not None:
                    # Start from top-most item
                    result = plot._pickTopMost(
                        pixelPos[0], pixelPos[1],
                        lambda item: isinstance(item, items.Scatter))
                    if result is not None:
                        # Get last index
                        # with matplotlib it should be the top-most point
                        dataIndex = result.getIndices(copy=False)[-1]
                        item = result.getItem()
                        self.__pickingCache = (
                            dataIndex,
                            item.getXData(copy=False)[dataIndex],
                            item.getYData(copy=False)[dataIndex],
                            item.getValueData(copy=False)[dataIndex])

        return self.__pickingCache

    def _getPickedIndex(self, x, y):
        """Get data index of top most scatter plot at position (x, y)

        :param float x: X position in plot coordinates
        :param float y: Y position in plot coordinates
        :return: The data index at that point or '-'
        """
        picking = self._pickScatterData(x, y)
        return '-' if picking is None else picking[0]

    def _getPickedX(self, x, y):
        """Returns X position snapped to scatter plot when close enough

        :param float x:
        :param float y:
        :rtype: float
        """
        picking = self._pickScatterData(x, y)
        return x if picking is None else picking[1]

    def _getPickedY(self, x, y):
        """Returns Y position snapped to scatter plot when close enough

        :param float x:
        :param float y:
        :rtype: float
        """
        picking = self._pickScatterData(x, y)
        return y if picking is None else picking[2]

    def _getPickedValue(self, x, y):
        """Get data value of top most scatter plot at position (x, y)

        :param float x: X position in plot coordinates
        :param float y: Y position in plot coordinates
        :return: The data value at that point or '-'
        """
        picking = self._pickScatterData(x, y)
        return '-' if picking is None else picking[3]

    def _mouseInPlotArea(self, x, y):
        """Clip mouse coordinates to plot area coordinates

        :param float x: X position in pixels
        :param float y: Y position in pixels
        :return: (x, y) in data coordinates
        """
        plot = self.getPlotWidget()
        left, top, width, height = plot.getPlotBoundsInPixels()
        xPlot = numpy.clip(x, left, left + width - 1)
        yPlot = numpy.clip(y, top, top + height - 1)
        return xPlot, yPlot

    def getPlotWidget(self):
        """Returns the :class:`~silx.gui.plot.PlotWidget` this window is based on.

        :rtype: ~silx.gui.plot.PlotWidget
        """
        return self._plot()

    def getPositionInfoWidget(self):
        """Returns the widget display mouse coordinates information.

        :rtype: ~silx.gui.plot.tools.PositionInfo
        """
        return self._positionInfo

    def getMaskToolsWidget(self):
        """Returns the widget controlling mask drawing

        :rtype: ~silx.gui.plot.ScatterMaskToolsWidget
        """
        return self._maskToolsWidget

    def getInteractiveModeToolBar(self):
        """Returns QToolBar controlling interactive mode.

        :rtype: ~silx.gui.plot.tools.InteractiveModeToolBar
        """
        return self._interactiveModeToolBar

    def getScatterToolBar(self):
        """Returns QToolBar providing scatter plot tools.

        :rtype: ~silx.gui.plot.tools.ScatterToolBar
        """
        return self._scatterToolBar

    def getScatterProfileToolBar(self):
        """Returns QToolBar providing scatter profile tools.

        :rtype: ~silx.gui.plot.tools.profile.ScatterProfileToolBar
        """
        return self._profileToolBar

    def getOutputToolBar(self):
        """Returns QToolBar containing save, copy and print actions

        :rtype: ~silx.gui.plot.tools.OutputToolBar
        """
        return self._outputToolBar

    def setColormap(self, colormap=None):
        """Set the colormap for the displayed scatter and the
        default plot colormap.

        :param ~silx.gui.colors.Colormap colormap:
            The description of the colormap.
        """
        self.getScatterItem().setColormap(colormap)
        # Resilient to call to PlotWidget API (e.g., clear)
        self.getPlotWidget().setDefaultColormap(colormap)

    def getColormap(self):
        """Return the colormap object in use.

        :return: Colormap currently in use
        :rtype: ~silx.gui.colors.Colormap
        """
        return self.getScatterItem().getColormap()

    # Control displayed scatter plot

    def setData(self, x, y, value, xerror=None, yerror=None, alpha=None, copy=True):
        """Set the data of the scatter plot.

        To reset the scatter plot, set x, y and value to None.

        :param Union[numpy.ndarray,None] x: X coordinates.
        :param Union[numpy.ndarray,None] y: Y coordinates.
        :param Union[numpy.ndarray,None] value:
            The data corresponding to the value of the data points.
        :param xerror: Values with the uncertainties on the x values.
            If it is an array, it can either be a 1D array of
            same length as the data or a 2D array with 2 rows
            of same length as the data: row 0 for positive errors,
            row 1 for negative errors.
        :type xerror: A float, or a numpy.ndarray of float32.

        :param yerror: Values with the uncertainties on the y values
        :type yerror: A float, or a numpy.ndarray of float32. See xerror.
        :param alpha: Values with the transparency (between 0 and 1)
        :type alpha: A float, or a numpy.ndarray of float32 
        :param bool copy: True make a copy of the data (default),
                          False to use provided arrays.
        """
        x = () if x is None else x
        y = () if y is None else y
        value = () if value is None else value

        self.getScatterItem().setData(
            x=x, y=y, value=value, xerror=xerror, yerror=yerror, alpha=alpha, copy=copy)

    @docstring(items.Scatter)
    def getData(self, *args, **kwargs):
        return self.getScatterItem().getData(*args, **kwargs)

    def getScatterItem(self):
        """Returns the plot item displaying the scatter data.

        This allows to set the style of the displayed scatter.

        :rtype: ~silx.gui.plot.items.Scatter
        """
        plot = self.getPlotWidget()
        scatter = plot._getItem(kind='scatter', legend=self._SCATTER_LEGEND)
        if scatter is None:  # Resilient to call to PlotWidget API (e.g., clear)
            scatter = self.__createEmptyScatter()
        return scatter

    # Convenient proxies

    @docstring(PlotWidget)
    def getXAxis(self, *args, **kwargs):
        return self.getPlotWidget().getXAxis(*args, **kwargs)

    @docstring(PlotWidget)
    def getYAxis(self, *args, **kwargs):
        return self.getPlotWidget().getYAxis(*args, **kwargs)

    @docstring(PlotWidget)
    def setGraphTitle(self, *args, **kwargs):
        return self.getPlotWidget().setGraphTitle(*args, **kwargs)

    @docstring(PlotWidget)
    def getGraphTitle(self, *args, **kwargs):
        return self.getPlotWidget().getGraphTitle(*args, **kwargs)

    @docstring(PlotWidget)
    def resetZoom(self, *args, **kwargs):
        return self.getPlotWidget().resetZoom(*args, **kwargs)

    @docstring(ScatterMaskToolsWidget)
    def getSelectionMask(self, *args, **kwargs):
        return self.getMaskToolsWidget().getSelectionMask(*args, **kwargs)

    @docstring(ScatterMaskToolsWidget)
    def setSelectionMask(self, *args, **kwargs):
        return self.getMaskToolsWidget().setSelectionMask(*args, **kwargs)
