import logging

import numpy

from sas.qtgui.Plotting.PlotterData import Data1D, DataRole
from sas.qtgui.Plotting.SlicerModel import SlicerModel
from sas.qtgui.Plotting.Slicers.SlicerUtils import StackableMixin, generate_unique_plot_id

from .BaseInteractor import BaseInteractor

logger = logging.getLogger(__name__)


class AnnulusInteractor(BaseInteractor, SlicerModel, StackableMixin):
    """
    AnnulusInteractor plots a data1D average of an annulus area defined in a
    Data2D object. The data1D averaging itself is performed in sasdata by
    manipulations.py

    This class uses the RingInteractor class to define two rings of radius
    r1 and r2 (Q1 and Q2). All Q points at a constant angle phi from the x-axis
    are averaged together to provide a 1D array in phi from 0 to 180 degrees.
    """

    def __init__(self, base, axes, item=None, color="black", zorder=3):
        BaseInteractor.__init__(self, base, axes, color=color)
        SlicerModel.__init__(self)
        StackableMixin.__init__(self)

        self.markers = []
        self.axes = axes
        self.base = base
        self._item = item
        self.qmax = max(numpy.fabs(self.data.xmax), numpy.fabs(self.data.xmin))  # must be positive
        self.dqmin = min(numpy.fabs(self.data.qx_data))
        self.connect = self.base.connect

        # Number of points on the plot
        self.nbins = 100
        # Store the plot ID so it doesn't change when parameters are updated
        self._plot_id = None
        # Cursor position of Rings (Left(-1) or Right(1))
        self.xmaxd = self.data.xmax
        self.xmind = self.data.xmin

        if (self.xmaxd + self.xmind) > 0:
            self.sign = 1
        else:
            self.sign = -1
        # Inner circle
        self.inner_circle = RingInteractor(self, self.axes, color=color, zorder=zorder, r=self.qmax / 2.0, sign=self.sign)
        self.inner_circle.qmax = self.qmax
        self.outer_circle = RingInteractor(self, self.axes, color=color, zorder=zorder + 1, r=self.qmax / 1.8, sign=self.sign)
        self.outer_circle.qmax = self.qmax * 1.2
        self.update()
        self._post_data()
        self.draw()

        self.setModelFromParams()

    def _get_slicer_type_id(self):
        """Return the slicer type identifier"""
        return "AnnulusPhi" + self.data.name

    def set_layer(self, n):
        """
        Allow adding plot to the same panel
        :param n: the number of layer
        """
        self.layernum = n
        self.update()

    def clear(self):
        """
        Clear the slicer and all connected events related to this slicer
        """
        self.clear_markers()
        self.outer_circle.clear()
        self.inner_circle.clear()
        self.base.connect.clearall()

    def update(self):
        """
        Respond to changes in the model by recalculating the profiles and
        resetting the widgets.
        """
        # Update locations
        self.inner_circle.update()
        self.outer_circle.update()

    def save(self, ev):
        """
        Remember the roughness for this layer and the next so that we
        can restore on Esc.
        """
        self.inner_circle.save(ev)
        self.outer_circle.save(ev)

    def _post_data(self, nbins=None):
        """
        Uses annulus parameters to plot averaged data into 1D data.

        :param nbins: the number of points to plot

        """
        # Data to average
        data = self.data
        if data is None:
            return

        from sasdata.data_util.manipulations import Ring

        rmin = min(numpy.fabs(self.inner_circle.get_radius()), numpy.fabs(self.outer_circle.get_radius()))
        rmax = max(numpy.fabs(self.inner_circle.get_radius()), numpy.fabs(self.outer_circle.get_radius()))
        if nbins is not None:
            self.nbins = nbins
        # Create the data1D Q average of data2D
        sect = Ring(r_min=rmin, r_max=rmax, nbins=self.nbins)
        sector = sect(self.data)

        if hasattr(sector, "dxl"):
            dxl = sector.dxl
        else:
            dxl = None
        if hasattr(sector, "dxw"):
            dxw = sector.dxw
        else:
            dxw = None
        new_plot = Data1D(x=(sector.x - numpy.pi) * 180 / numpy.pi, y=sector.y, dy=sector.dy)
        new_plot.dxl = dxl
        new_plot.dxw = dxw

        new_plot.source = self.data.source
        new_plot.interactive = True
        new_plot.detector = self.data.detector
        # If the data file does not tell us what the axes are, just assume...
        new_plot.xaxis(r"\rm{\phi}", "degrees")
        new_plot.yaxis(r"\rm{Intensity} ", "cm^{-1}")
        new_plot.plot_role = DataRole.ROLE_ANGULAR_SLICE
        if hasattr(data, "scale") and data.scale == "linear" and self.data.name.count("Residuals") > 0:
            new_plot.ytransform = "y"
            new_plot.yaxis(r"\rm{Residuals} ", "/")

        # Assign unique id per slicer instance and use it as the display name
        if self._plot_id is None:
            base_id = "AnnulusPhi" + self.data.name
            self._plot_id = generate_unique_plot_id(base_id, self._item)

        new_plot.id = self._plot_id
        new_plot.name = new_plot.id
        new_plot.title = new_plot.id
        new_plot.is_data = True
        new_plot.xtransform = "x"
        new_plot.ytransform = "y"

        item = self._item
        if self._item.parent() is not None:
            item = self._item.parent()

        # Use the mixin to handle stacking/updating
        self._create_or_update_plot(new_plot, item)

        if self.update_model:
            self.setModelFromParams()

    def validate(self, param_name, param_value):
        """
        Test the proposed new value "value" for row "row" of parameters
        """
        # Set minimum difference in outer/inner ring to ensure data exists in annulus
        MIN_DIFFERENCE = self.dqmin
        isValid = True

        if param_name == "inner_radius":
            # First, check the closeness
            if numpy.fabs(param_value - self.getParams()["outer_radius"]) < MIN_DIFFERENCE:
                print("Inner and outer radii too close. Please adjust.")
                isValid = False
            elif param_value > self.qmax:
                print("Inner radius exceeds maximum range. Please adjust.")
                isValid = False
        elif param_name == "outer_radius":
            # First, check the closeness
            if numpy.fabs(param_value - self.getParams()["inner_radius"]) < MIN_DIFFERENCE:
                print("Inner and outer radii too close. Please adjust.")
                isValid = False
            elif param_value > self.qmax:
                print("Outer radius exceeds maximum range. Please adjust.")
                isValid = False
        elif param_name == "nbins":
            # Can't be 0
            if param_value < 1:
                print("Number of bins cannot be less than or equal to 0. Please adjust.")
                isValid = False

        return isValid

    def moveend(self, ev):
        """
        Called when any dragging motion ends.
        Redraw the plot with new parameters.
        """
        self._post_data(self.nbins)
        self.draw()

    def restore(self, ev):
        """
        Restore the roughness for this layer.
        """
        self.inner_circle.restore(ev)
        self.outer_circle.restore(ev)

    def move(self, x, y, ev):
        """
        Process move to a new position, making sure that the move is allowed.
        """
        pass

    def set_cursor(self, x, y):
        pass

    def getParams(self):
        """
        Store a copy of values of parameters of the slicer into a dictionary.
        :return params: the dictionary created
        """
        params = {}
        params["inner_radius"] = numpy.fabs(self.inner_circle._inner_mouse_x)
        params["outer_radius"] = numpy.fabs(self.outer_circle._inner_mouse_x)
        params["nbins"] = self.nbins
        return params

    def setParams(self, params):
        """
        Receive a dictionary and reset the slicer with values contained
        in the values of the dictionary.

        :param params: a dictionary containing name of slicer parameters and
            values the user assigned to the slicer.
        """
        inner = numpy.fabs(params["inner_radius"])
        outer = numpy.fabs(params["outer_radius"])
        self.nbins = int(params["nbins"])
        # Update the picture
        self.inner_circle.set_cursor(inner, self.inner_circle._inner_mouse_y)
        self.outer_circle.set_cursor(outer, self.outer_circle._inner_mouse_y)
        # Post the data given the nbins entered by the user
        self._post_data(self.nbins)
        self.draw()

    def draw(self):
        """ """
        self.base.draw()


class RingInteractor(BaseInteractor):
    """
    Draw a ring on a data2D plot centered at (0,0) given a radius
    """

    def __init__(self, base, axes, color="black", zorder=5, r=1.0, sign=1):
        """
        :param: the color of the line that defined the ring
        :param r: the radius of the ring
        :param sign: the direction of motion the marker

        """
        BaseInteractor.__init__(self, base, axes, color=color)
        self.markers = []
        self.axes = axes
        # Current radius of the ring
        self._inner_mouse_x = r
        # Value of the center of the ring
        self._inner_mouse_y = 0
        # previous value of that radius
        self._inner_save_x = r
        # Save value of the center of the ring
        self._inner_save_y = 0
        # Class instantiating RingIterator class
        self.base = base
        # the direction of the motion of the marker
        self.sign = sign
        # # Create a marker
        # Inner circle marker
        x_value = [self.sign * numpy.fabs(self._inner_mouse_x)]
        self.inner_marker = self.axes.plot(
            x_value,
            [0],
            linestyle="",
            marker="s",
            markersize=10,
            color=self.color,
            alpha=0.6,
            pickradius=5,
            label="pick",
            zorder=zorder,
            visible=True,
        )[0]
        # Draw a circle
        [self.inner_circle] = self.axes.plot([], [], linestyle="-", marker="", color=self.color)
        # The number of points that make the ring line
        self.npts = 40

        self.connect_markers([self.inner_marker])
        self.update()

    def set_layer(self, n):
        """
        Allow adding plot to the same panel

        :param n: the number of layer

        """
        self.layernum = n
        self.update()

    def clear(self):
        """
        Clear the slicer and all connected events related to this slicer
        """
        self.clear_markers()
        if self.inner_marker.axes is not None:
            self.inner_marker.remove()
        if self.inner_circle.axes is not None:
            self.inner_circle.remove()

    def get_radius(self):
        """
        :return self._inner_mouse_x: the current radius of the ring
        """
        return self._inner_mouse_x

    def update(self):
        """
        Draw the new roughness on the graph.
        """
        # Plot inner circle
        x = []
        y = []
        for i in range(self.npts):
            phi = 2.0 * numpy.pi / (self.npts - 1) * i

            xval = 1.0 * self._inner_mouse_x * numpy.cos(phi)
            yval = 1.0 * self._inner_mouse_x * numpy.sin(phi)

            x.append(xval)
            y.append(yval)

        self.inner_marker.set(xdata=[self.sign * numpy.fabs(self._inner_mouse_x)], ydata=[0])
        self.inner_circle.set_data(x, y)

    def save(self, ev):
        """
        Remember the roughness for this layer and the next so that we
        can restore on Esc.
        """
        self._inner_save_x = self._inner_mouse_x
        self._inner_save_y = self._inner_mouse_y

    def moveend(self, ev):
        """
        Called after a dragging motion
        """
        self.base.moveend(ev)

    def restore(self, ev):
        """
        Restore the roughness for this layer.
        """
        self._inner_mouse_x = self._inner_save_x
        self._inner_mouse_y = self._inner_save_y

    def move(self, x, y, ev):
        """
        Process move to a new position, making sure that the move is allowed.
        """
        self._inner_mouse_x = x
        self._inner_mouse_y = y
        self.base.update()
        self.base.draw()

    def set_cursor(self, x, y):
        """
        draw the ring given x, y value
        """
        self.move(x, y, None)
        self.update()

    def getParams(self):
        """
        Store a copy of values of parameters of the slicer into a dictionary.
        :return params: the dictionary created
        """
        params = {}
        params["radius"] = numpy.fabs(self._inner_mouse_x)
        return params

    def setParams(self, params):
        """
        Receive a dictionary and reset the slicer with values contained
        in the values of the dictionary.

        :param params: a dictionary containing name of slicer parameters and
            values the user assigned to the slicer.

        """
        x = params["radius"]
        self.set_cursor(x, self._inner_mouse_y)
