# /*##########################################################################
#
# Copyright (c) 2017-2023 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::t
#
# 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.
#
# ###########################################################################*/
"""This module provides the :class:`Histogram` item of the :class:`Plot`.
"""

__authors__ = ["H. Payno", "T. Vincent"]
__license__ = "MIT"
__date__ = "28/08/2018"

import logging
import typing

import numpy
from collections import abc

from ....utils.proxy import docstring
from .core import (
    DataItem,
    AlphaMixIn,
    BaselineMixIn,
    ColorMixIn,
    FillMixIn,
    LineMixIn,
    LineGapColorMixIn,
    YAxisMixIn,
    ItemChangedType,
)
from ._pick import PickingResult
from silx._utils import NP_OPTIONAL_COPY


_logger = logging.getLogger(__name__)


def _computeEdges(x, histogramType):
    """Compute the edges from a set of xs and a rule to generate the edges

    :param x: the x value of the curve to transform into an histogram
    :param histogramType: the type of histogram we wan't to generate.
         This define the way to center the histogram values compared to the
         curve value. Possible values can be::

         - 'left'
         - 'right'
         - 'center'

    :return: the edges for the given x and the histogramType
    """
    # for now we consider that the spaces between xs are constant
    edges = x.copy()
    if histogramType == "left":
        width = 1
        if len(x) > 1:
            width = x[1] - x[0]
        edges = numpy.append(x[0] - width, edges)
    if histogramType == "center":
        edges = _computeEdges(edges, "right")
        widths = (edges[1:] - edges[0:-1]) / 2.0
        widths = numpy.append(widths, widths[-1])
        edges = edges - widths
    if histogramType == "right":
        width = 1
        if len(x) > 1:
            width = x[-1] - x[-2]
        edges = numpy.append(edges, x[-1] + width)

    return edges


def _getHistogramCurve(histogram, edges):
    """Returns the x and y value of a curve corresponding to the histogram

    :param numpy.ndarray histogram: The values of the histogram
    :param numpy.ndarray edges: The bin edges of the histogram
    :return: a tuple(x, y) which contains the value of the curve to use
             to display the histogram
    """
    assert len(histogram) + 1 == len(edges)
    x = numpy.empty(len(histogram) * 2, dtype=edges.dtype)
    y = numpy.empty(len(histogram) * 2, dtype=histogram.dtype)
    # Make a curve with stairs
    x[:-1:2] = edges[:-1]
    x[1::2] = edges[1:]
    y[:-1:2] = histogram
    y[1::2] = histogram

    return x, y


# TODO: Yerror, test log scale
class Histogram(
    DataItem,
    AlphaMixIn,
    ColorMixIn,
    FillMixIn,
    LineMixIn,
    LineGapColorMixIn,
    YAxisMixIn,
    BaselineMixIn,
):
    """Description of an histogram"""

    _DEFAULT_Z_LAYER = 1
    """Default overlay layer for histograms"""

    _DEFAULT_SELECTABLE = False
    """Default selectable state for histograms"""

    _DEFAULT_LINEWIDTH = 1.0
    """Default line width of the histogram"""

    _DEFAULT_LINESTYLE = "-"
    """Default line style of the histogram"""

    _DEFAULT_BASELINE = None

    def __init__(self):
        DataItem.__init__(self)
        AlphaMixIn.__init__(self)
        BaselineMixIn.__init__(self)
        ColorMixIn.__init__(self)
        FillMixIn.__init__(self)
        LineMixIn.__init__(self)
        LineGapColorMixIn.__init__(self)
        YAxisMixIn.__init__(self)

        self._histogram = ()
        self._edges = ()
        self._setBaseline(Histogram._DEFAULT_BASELINE)

    def _addBackendRenderer(self, backend):
        """Update backend renderer"""
        values, edges, baseline = self.getData(copy=False)

        if values.size == 0:
            return None  # No data to display, do not add renderer

        if values.size == 0:
            return None  # No data to display, do not add renderer to backend

        x, y = _getHistogramCurve(values, edges)

        # Filter-out values <= 0
        plot = self.getPlot()
        if plot is not None:
            xPositive = plot.getXAxis()._isLogarithmic()
            yPositive = plot.getYAxis()._isLogarithmic()
        else:
            xPositive = False
            yPositive = False

        if xPositive or yPositive:
            clipped = numpy.logical_or(
                (x <= 0) if xPositive else False, (y <= 0) if yPositive else False
            )
            # Make a copy and replace negative points by NaN
            x = numpy.array(x, dtype=numpy.float64)
            y = numpy.array(y, dtype=numpy.float64)
            x[clipped] = numpy.nan
            y[clipped] = numpy.nan

        return backend.addCurve(
            x,
            y,
            color=self.getColor(),
            gapcolor=self.getLineGapColor(),
            symbol="",
            linestyle=self.getLineStyle(),
            linewidth=self.getLineWidth(),
            yaxis=self.getYAxis(),
            xerror=None,
            yerror=None,
            fill=self.isFill(),
            alpha=self.getAlpha(),
            baseline=baseline,
            symbolsize=1,
        )

    def _getBounds(self):
        values, edges, baseline = self.getData(copy=False)

        plot = self.getPlot()
        if plot is not None:
            xPositive = plot.getXAxis()._isLogarithmic()
            yPositive = plot.getYAxis()._isLogarithmic()
        else:
            xPositive = False
            yPositive = False

        if xPositive or yPositive:
            values = numpy.array(values, copy=True, dtype=numpy.float64)

            if xPositive:
                # Replace edges <= 0 by NaN and corresponding values by NaN
                clipped_edges = edges <= 0
                edges = numpy.array(edges, copy=True, dtype=numpy.float64)
                edges[clipped_edges] = numpy.nan
                clipped_values = numpy.logical_or(clipped_edges[:-1], clipped_edges[1:])
            else:
                clipped_values = numpy.zeros_like(values, dtype=bool)

            if yPositive:
                # Replace values <= 0 by NaN, do not modify edges
                clipped_values = numpy.logical_or(clipped_values, values <= 0)

            values[clipped_values] = numpy.nan

        if yPositive:
            return (
                numpy.nanmin(edges),
                numpy.nanmax(edges),
                numpy.nanmin(values),
                numpy.nanmax(values),
            )

        else:  # No log scale on y axis, include 0 in bounds
            if numpy.all(numpy.isnan(values)):
                return None
            return (
                numpy.nanmin(edges),
                numpy.nanmax(edges),
                min(0, numpy.nanmin(values)),
                max(0, numpy.nanmax(values)),
            )

    def __pickFilledHistogram(
        self, x: float, y: float
    ) -> typing.Optional[PickingResult]:
        """Picking implementation for filled histogram

        :param x: X position in pixels
        :param y: Y position in pixels
        """
        if not self.isFill():
            return None

        plot = self.getPlot()
        if plot is None:
            return None

        xData, yData = plot.pixelToData(x, y, axis=self.getYAxis())
        xmin, xmax, ymin, ymax = self.getBounds()
        if not xmin < xData < xmax or not ymin < yData < ymax:
            return None  # Outside bounding box

        # Check x
        edges = self.getBinEdgesData(copy=False)
        index = numpy.searchsorted(edges, (xData,), side="left")[0] - 1
        # Safe indexing in histogram values
        index = numpy.clip(index, 0, len(edges) - 2)

        # Check y
        baseline = self.getBaseline(copy=False)
        if baseline is None:
            baseline = 0  # Default value

        value = self.getValueData(copy=False)[index]
        if (baseline <= value and baseline <= yData <= value) or (
            value < baseline and value <= yData <= baseline
        ):
            return PickingResult(self, numpy.array([index]))
        else:
            return None

    @docstring(DataItem)
    def pick(self, x, y):
        if self.isFill():
            return self.__pickFilledHistogram(x, y)
        else:
            result = super().pick(x, y)
            if result is None:
                return None
            else:  # Convert from curve indices to histogram indices
                return PickingResult(self, numpy.unique(result.getIndices() // 2))

    def getValueData(self, copy=True):
        """The values of the histogram

        :param copy: True (Default) to get a copy,
                     False to use internal representation (do not modify!)
        :returns: The values of the histogram
        :rtype: numpy.ndarray
        """
        return numpy.array(self._histogram, copy=copy or NP_OPTIONAL_COPY)

    def getBinEdgesData(self, copy=True):
        """The bin edges of the histogram (number of histogram values + 1)

        :param copy: True (Default) to get a copy,
                     False to use internal representation (do not modify!)
        :returns: The bin edges of the histogram
        :rtype: numpy.ndarray
        """
        return numpy.array(self._edges, copy=copy or NP_OPTIONAL_COPY)

    def getData(self, copy=True):
        """Return the histogram values, bin edges and baseline

        :param copy: True (Default) to get a copy,
                     False to use internal representation (do not modify!)
        :returns: (N histogram value, N+1 bin edges)
        :rtype: 2-tuple of numpy.nadarray
        """
        return (
            self.getValueData(copy),
            self.getBinEdgesData(copy),
            self.getBaseline(copy),
        )

    def setData(self, histogram, edges, align="center", baseline=None, copy=True):
        """Set the histogram values and bin edges.

        :param numpy.ndarray histogram: The values of the histogram.
        :param numpy.ndarray edges:
            The bin edges of the histogram.
            If histogram and edges have the same length, the bin edges
            are computed according to the align parameter.
        :param str align:
            In case histogram values and edges have the same length N,
            the N+1 bin edges are computed according to the alignment in:
            'center' (default), 'left', 'right'.
        :param baseline: histogram baseline
        :type baseline: Union[None,float,numpy.ndarray]
        :param bool copy: True make a copy of the data (default),
                          False to use provided arrays.
        """
        histogram = numpy.array(histogram, copy=copy or NP_OPTIONAL_COPY)
        edges = numpy.array(edges, copy=copy or NP_OPTIONAL_COPY)

        assert histogram.ndim == 1
        assert edges.ndim == 1
        assert edges.size in (histogram.size, histogram.size + 1)
        assert align in ("center", "left", "right")

        if histogram.size == 0:  # No data
            self._histogram = ()
            self._edges = ()
        else:
            if edges.size == histogram.size:  # Compute true bin edges
                edges = _computeEdges(edges, align)

            # Check that bin edges are monotonic
            edgesDiff = numpy.diff(edges)
            edgesDiff = edgesDiff[numpy.logical_not(numpy.isnan(edgesDiff))]
            assert numpy.all(edgesDiff >= 0) or numpy.all(edgesDiff <= 0)
            # manage baseline
            if isinstance(baseline, abc.Iterable):
                baseline = numpy.array(baseline)
                if baseline.size == histogram.size:
                    new_baseline = numpy.empty(baseline.shape[0] * 2)
                    for i_value, value in enumerate(baseline):
                        new_baseline[i_value * 2 : i_value * 2 + 2] = value
                    baseline = new_baseline
            self._histogram = histogram
            self._edges = edges
            self._alignement = align
            self._setBaseline(baseline)

        self._boundsChanged()
        self._updated(ItemChangedType.DATA)

    def getAlignment(self):
        """

        :return: histogram alignement. Value in ('center', 'left', 'right').
        """
        return self._alignement

    def _revertComputeEdges(self, x, histogramType):
        """Compute the edges from a set of xs and a rule to generate the edges

        :param x: the x value of the curve to transform into an histogram
        :param histogramType: the type of histogram we wan't to generate.
             This define the way to center the histogram values compared to the
             curve value. Possible values can be::

             - 'left'
             - 'right'
             - 'center'

        :return: the edges for the given x and the histogramType
        """
        # for now we consider that the spaces between xs are constant
        edges = x.copy()
        if histogramType == "left":
            return edges[1:]
        if histogramType == "center":
            edges = (edges[1:] + edges[:-1]) / 2.0
        if histogramType == "right":
            width = 1
            if len(x) > 1:
                width = x[-1] + x[-2]
            edges = edges[:-1]
        return edges
