# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2004-2020 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.
#
# ###########################################################################*/
"""
:mod:`silx.gui.plot.actions.fit` module provides actions relative to fit.

The following QAction are available:

- :class:`.FitAction`

.. autoclass:`.FitAction`
"""

from __future__ import division

__authors__ = ["V.A. Sole", "T. Vincent", "P. Knobel"]
__license__ = "MIT"
__date__ = "10/10/2018"

import logging

import numpy

from .PlotToolAction import PlotToolAction
from .. import items
from ....utils.deprecation import deprecated
from silx.gui import qt
from silx.gui.plot.ItemsSelectionDialog import ItemsSelectionDialog

_logger = logging.getLogger(__name__)


def _getUniqueCurveOrHistogram(plot):
    """Returns unique :class:`Curve` or :class:`Histogram` in a `PlotWidget`.

    If there is an active curve, returns it, else return curve or histogram
    only if alone in the plot.

    :param PlotWidget plot:
    :rtype: Union[None,~silx.gui.plot.items.Curve,~silx.gui.plot.items.Histogram]
    """
    curve = plot.getActiveCurve()
    if curve is not None:
        return curve

    histograms = [item for item in plot.getItems()
                  if isinstance(item, items.Histogram) and item.isVisible()]
    curves = [item for item in plot.getItems()
              if isinstance(item, items.Curve) and item.isVisible()]

    if len(histograms) == 1 and len(curves) == 0:
        return histograms[0]
    elif len(curves) == 1 and len(histograms) == 0:
        return curves[0]
    else:
        return None


class FitAction(PlotToolAction):
    """QAction to open a :class:`FitWidget` and set its data to the
    active curve if any, or to the first curve.

    :param plot: :class:`.PlotWidget` instance on which to operate
    :param parent: See :class:`QAction`
    """

    def __init__(self, plot, parent=None):
        self.__item = None
        self.__activeCurveSynchroEnabled = False
        self.__range = 0, 1
        self.__rangeAutoUpdate = False
        self.__x, self.__y = None, None  # Data to fit
        self.__curveParams = {}  # Store curve parameters to use for fit result
        self.__legend = None

        super(FitAction, self).__init__(
            plot, icon='math-fit', text='Fit curve',
            tooltip='Open a fit dialog',
            parent=parent)

    @property
    @deprecated(replacement='getXRange()[0]', since_version='0.13.0')
    def xmin(self):
        return self.getXRange()[0]

    @property
    @deprecated(replacement='getXRange()[1]', since_version='0.13.0')
    def xmax(self):
        return self.getXRange()[1]

    @property
    @deprecated(replacement='getXData()', since_version='0.13.0')
    def x(self):
        return self.getXData()

    @property
    @deprecated(replacement='getYData()', since_version='0.13.0')
    def y(self):
        return self.getYData()

    @property
    @deprecated(since_version='0.13.0')
    def xlabel(self):
        return self.__curveParams.get('xlabel', None)

    @property
    @deprecated(since_version='0.13.0')
    def ylabel(self):
        return self.__curveParams.get('ylabel', None)

    @property
    @deprecated(since_version='0.13.0')
    def legend(self):
        return self.__legend

    def _createToolWindow(self):
        # import done here rather than at module level to avoid circular import
        # FitWidget -> BackgroundWidget -> PlotWindow -> actions -> fit -> FitWidget
        from ...fit.FitWidget import FitWidget

        window = FitWidget(parent=self.plot)
        window.setWindowFlags(qt.Qt.Dialog)
        window.sigFitWidgetSignal.connect(self.handle_signal)
        return window

    def _connectPlot(self, window):
        if self.isXRangeUpdatedOnZoom():
            self.__setAutoXRangeEnabled(True)
        else:
            plot = self.plot
            if plot is None:
                _logger.error("No associated PlotWidget")
                return
            self._setXRange(*plot.getXAxis().getLimits())

        if self.isFittedItemUpdatedFromActiveCurve():
            self.__setFittedItemAutoUpdateEnabled(True)
        else:
            # Wait for the next iteration, else the plot is not yet initialized
            # No curve available
            qt.QTimer.singleShot(10, self._initFit)

    def _disconnectPlot(self, window):
        if self.isXRangeUpdatedOnZoom():
            self.__setAutoXRangeEnabled(False)

        if self.isFittedItemUpdatedFromActiveCurve():
            self.__setFittedItemAutoUpdateEnabled(False)

    def _initFit(self):
        plot = self.plot
        if plot is None:
            _logger.error("No associated PlotWidget")
            return

        item = _getUniqueCurveOrHistogram(plot)
        if item is None:
            # ambiguous case, we need to ask which plot item to fit
            isd = ItemsSelectionDialog(parent=plot, plot=plot)
            isd.setWindowTitle("Select item to be fitted")
            isd.setItemsSelectionMode(qt.QTableWidget.SingleSelection)
            isd.setAvailableKinds(["curve", "histogram"])
            isd.selectAllKinds()

            if not isd.exec_():  # Cancel
                self._getToolWindow().setVisible(False)
            else:
                selectedItems = isd.getSelectedItems()
                item = selectedItems[0] if len(selectedItems) == 1 else None

        self._setXRange(*plot.getXAxis().getLimits())
        self._setFittedItem(item)

    def __updateFitWidget(self):
        """Update the data/range used by the FitWidget"""
        fitWidget = self._getToolWindow()

        item = self._getFittedItem()
        xdata = self.getXData(copy=False)
        ydata = self.getYData(copy=False)
        if item is None or xdata is None or ydata is None:
            fitWidget.setData(y=None)
            fitWidget.setWindowTitle("No curve selected")

        else:
            xmin, xmax = self.getXRange()
            fitWidget.setData(
                xdata, ydata, xmin=xmin, xmax=xmax)
            fitWidget.setWindowTitle(
                "Fitting " + item.getName() +
                " on x range %f-%f" % (xmin, xmax))

    # X Range management

    def getXRange(self):
        """Returns the range on the X axis on which to perform the fit."""
        return self.__range

    def _setXRange(self, xmin, xmax):
        """Set the range on which the fit is done.

        :param float xmin:
        :param float xmax:
        """
        range_ = float(xmin), float(xmax)
        if self.__range != range_:
            self.__range = range_
            self.__updateFitWidget()

    def __setAutoXRangeEnabled(self, enabled):
        """Implement the change of update mode of the X range.

        :param bool enabled:
        """
        plot = self.plot
        if plot is None:
            _logger.error("No associated PlotWidget")
            return

        if enabled:
            self._setXRange(*plot.getXAxis().getLimits())
            plot.getXAxis().sigLimitsChanged.connect(self._setXRange)
        else:
            plot.getXAxis().sigLimitsChanged.disconnect(self._setXRange)

    def setXRangeUpdatedOnZoom(self, enabled):
        """Set whether or not to update the X range on zoom change.

        :param bool enabled:
        """
        if enabled != self.__rangeAutoUpdate:
            self.__rangeAutoUpdate = enabled
            if self._getToolWindow().isVisible():
                self.__setAutoXRangeEnabled(enabled)

    def isXRangeUpdatedOnZoom(self):
        """Returns the current mode of fitted data X range update.

        :rtype: bool
        """
        return self.__rangeAutoUpdate

    # Fitted item update

    def getXData(self, copy=True):
        """Returns the X data used for the fit or None if undefined.

        :param bool copy:
            True to get a copy of the data, False to get the internal data.
        :rtype: Union[numpy.ndarray,None]
        """
        return None if self.__x is None else numpy.array(self.__x, copy=copy)

    def getYData(self, copy=True):
        """Returns the Y data used for the fit or None if undefined.

        :param bool copy:
            True to get a copy of the data, False to get the internal data.
        :rtype: Union[numpy.ndarray,None]
        """
        return None if self.__y is None else numpy.array(self.__y, copy=copy)

    def _getFittedItem(self):
        """Returns the current item used for the fit

        :rtype: Union[~silx.gui.plot.items.Curve,~silx.gui.plot.items.Histogram,None]
        """
        return self.__item

    def _setFittedItem(self, item):
        """Set the curve to use for fitting.

        :param Union[~silx.gui.plot.items.Curve,~silx.gui.plot.items.Histogram,None] item:
        """
        plot = self.plot
        if plot is None:
            _logger.error("No associated PlotWidget")

        if plot is None or item is None:
            self.__item = None
            self.__curveParams = {}
            self.__updateFitWidget()
            return

        axis = item.getYAxis() if isinstance(item, items.YAxisMixIn) else 'left'
        self.__curveParams = {
            'yaxis': axis,
            'xlabel': plot.getXAxis().getLabel(),
            'ylabel': plot.getYAxis(axis).getLabel(),
            }
        self.__legend = item.getName()

        if isinstance(item, items.Histogram):
            bin_edges = item.getBinEdgesData(copy=False)
            # take the middle coordinate between adjacent bin edges
            self.__x = (bin_edges[1:] + bin_edges[:-1]) / 2
            self.__y = item.getValueData(copy=False)
        # else take the active curve, or else the unique curve
        elif isinstance(item, items.Curve):
            self.__x = item.getXData(copy=False)
            self.__y = item.getYData(copy=False)

        self.__item  = item
        self.__updateFitWidget()

    def __activeCurveChanged(self, previous, current):
        """Handle change of active curve in the PlotWidget
        """
        if current is None:
            self._setFittedItem(None)
        else:
            item = self.plot.getCurve(current)
            self._setFittedItem(item)

    def __setFittedItemAutoUpdateEnabled(self, enabled):
        """Implement the change of fitted item update mode

        :param bool enabled:
        """
        plot = self.plot
        if plot is None:
            _logger.error("No associated PlotWidget")
            return

        if enabled:
            self._setFittedItem(plot.getActiveCurve())
            plot.sigActiveCurveChanged.connect(self.__activeCurveChanged)

        else:
            plot.sigActiveCurveChanged.disconnect(
                self.__activeCurveChanged)

    def setFittedItemUpdatedFromActiveCurve(self, enabled):
        """Toggle fitted data synchronization with plot active curve.

        :param bool enabled:
        """
        enabled = bool(enabled)
        if enabled != self.__activeCurveSynchroEnabled:
            self.__activeCurveSynchroEnabled = enabled
            if self._getToolWindow().isVisible():
                self.__setFittedItemAutoUpdateEnabled(enabled)

    def isFittedItemUpdatedFromActiveCurve(self):
        """Returns True if fitted data is synchronized with plot.

        :rtype: bool
        """
        return self.__activeCurveSynchroEnabled

    # Handle fit completed

    def handle_signal(self, ddict):
        xdata = self.getXData(copy=False)
        if xdata is None:
            _logger.error("No reference data to display fit result for")
            return

        xmin, xmax = self.getXRange()
        x_fit = xdata[xmin <= xdata]
        x_fit = x_fit[x_fit <= xmax]
        fit_legend = "Fit <%s>" % self.__legend
        fit_curve = self.plot.getCurve(fit_legend)

        if ddict["event"] == "FitFinished":
            fit_widget = self._getToolWindow()
            if fit_widget is None:
                return
            y_fit = fit_widget.fitmanager.gendata()
            if fit_curve is None:
                self.plot.addCurve(x_fit, y_fit,
                                   fit_legend,
                                   resetzoom=False,
                                   **self.__curveParams)
            else:
                fit_curve.setData(x_fit, y_fit)
                fit_curve.setVisible(True)
                fit_curve.setYAxis(self.__curveParams.get('yaxis', 'left'))

        if ddict["event"] in ["FitStarted", "FitFailed"]:
            if fit_curve is not None:
                fit_curve.setVisible(False)
