import numpy as np

from sasdata.dataloader.data_info import Detector, Source

from sas.qtgui.Plotting.PlotterData import Data1D, Data2D, DataRole


class FittingLogic:
    """
    All the data-related logic. This class deals exclusively with Data1D/2D
    No QStandardModelIndex here.
    """
    def __init__(self, data=None):
        self._data = data
        self.data_is_loaded = False
        #dq data presence in the dataset
        self.dq_flag = False
        #di data presence in the dataset
        self.di_flag = False
        if data is not None:
            self.data_is_loaded = True
            self.setDataProperties()
        self.model_parameters = None
        self.kernel_module = None
        # List of all shell-unique parameters
        self.shell_names = []
        self.current_shell_displayed = 0

    @property
    def data(self):
        return self._data

    @data.setter
    def data(self, value):
        """ data setter """
        self._data = value
        self.data_is_loaded = True
        self.setDataProperties()

    def isLoadedData(self):
        """ accessor """
        return self.data_is_loaded

    def setDataProperties(self):
        """
        Analyze data and set up some properties important for
        the Presentation layer
        """
        if self._data.__class__.__name__ == "Data2D":
            if self._data.err_data is not None and np.any(self._data.err_data):
                self.di_flag = True
            if self._data.dqx_data is not None and np.any(self._data.dqx_data):
                self.dq_flag = True
        else:
            if self._data.dy is not None and np.any(self._data.dy):
                self.di_flag = True
            if self._data.dx is not None and np.any(self._data.dx):
                self.dq_flag = True
            elif self._data.dxl is not None and np.any(self._data.dxl):
                self.dq_flag = True

    def createDefault1dData(self, interval, tab_id=0):
        """
        Create default data for fitting perspective
        Only when the page is on theory mode.
        """
        self._data = Data1D(x=interval)
        self._data.xaxis('\\rm{Q}', "A^{-1}")
        self._data.yaxis('\\rm{Intensity}', "cm^{-1}")
        self._data.is_data = False
        self._data.id = str(tab_id) + " data"
        self._data.group_id = str(tab_id) + " Model1D"

    def createDefault2dData(self, qmax, qstep, tab_id=0):
        """
        Create 2D data by default
        Only when the page is on theory mode.
        """
        self._data = Data2D()
        self._data.xaxis('\\rm{Q_{x}}', 'A^{-1}')
        self._data.yaxis('\\rm{Q_{y}}', 'A^{-1}')
        self._data.is_data = False
        self._data.id = str(tab_id) + " data"
        self._data.group_id = str(tab_id) + " Model2D"

        # Default detector
        self._data.detector.append(Detector())
        index = len(self._data.detector) - 1
        self._data.detector[index].distance = 8000   # mm
        self._data.source.wavelength = 6             # A
        self._data.detector[index].pixel_size.x = 5  # mm
        self._data.detector[index].pixel_size.y = 5  # mm
        self._data.detector[index].beam_center.x = qmax
        self._data.detector[index].beam_center.y = qmax
        # theory default: assume the beam
        #center is located at the center of sqr detector
        xmax = qmax
        xmin = -qmax
        ymax = qmax
        ymin = -qmax

        x = np.linspace(start=xmin, stop=xmax, num=qstep, endpoint=True)
        y = np.linspace(start=ymin, stop=ymax, num=qstep, endpoint=True)
        # Use data info instead
        new_x = np.tile(x, (len(y), 1))
        new_y = np.tile(y, (len(x), 1))
        new_y = new_y.swapaxes(0, 1)

        # all data required in 1d array
        qx_data = new_x.flatten()
        qy_data = new_y.flatten()
        q_data = np.sqrt(qx_data * qx_data + qy_data * qy_data)

        # set all True (standing for unmasked) as default
        mask = np.ones(len(qx_data), dtype=bool)
        # calculate the range of qx and qy: this way,
        # it is a little more independent
        # store x and y bin centers in q space
        x_bins = x
        y_bins = y

        self._data.source = Source()
        self._data.data = np.ones(len(mask))
        self._data.err_data = np.ones(len(mask))
        self._data.qx_data = qx_data
        self._data.qy_data = qy_data
        self._data.q_data = q_data
        self._data.mask = mask
        self._data.x_bins = x_bins
        self._data.y_bins = y_bins
        # max and min taking account of the bin sizes
        self._data.xmin = xmin
        self._data.xmax = xmax
        self._data.ymin = ymin
        self._data.ymax = ymax

    def _create1DPlot(self, tab_id, x, y, model, data, component=None):
        """
        For internal use: create a new 1D data instance based on fitting results.
        'component' is a string indicating the model component, e.g. "P(Q)"
        """
        # Create the new plot
        new_plot = Data1D(x=x, y=y)
        new_plot.is_data = False
        new_plot.dy = np.zeros(len(y))
        _yaxis, _yunit = data.get_yaxis()
        _xaxis, _xunit = data.get_xaxis()

        new_plot.group_id = data.group_id
        new_plot.id = str(tab_id) + " " + ("[" + component + "] " if component else "") + model.id

        # use data.filename for data, use model.id for theory
        id_str = data.name if data.name else model.id
        new_plot.name = model.name + ((" " + component) if component else "") + " [" + id_str + "]"

        new_plot.title = new_plot.name
        new_plot.xaxis(_xaxis, _xunit)
        new_plot.yaxis(_yaxis, _yunit)

        if component is not None:
            new_plot.plot_role = DataRole.ROLE_DELETABLE #deletable

        return new_plot

    def new1DPlot(self, return_data, tab_id):
        """
        Create a new 1D data instance based on fitting results
        """
        return self._create1DPlot(tab_id, return_data['x'], return_data['y'],
                                  return_data['model'], return_data['data'])

    def new2DPlot(self, return_data):
        """
        Create a new 2D data instance based on fitting results
        """
        image = return_data['image']
        data = return_data['data']
        model = return_data['model']

        np.nan_to_num(image)
        new_plot = Data2D(image=image, err_image=data.err_data)
        new_plot.name = model.name + '2d'
        new_plot.title = "Analytical model 2D "
        new_plot.id = str(return_data['page_id']) + " " + data.name
        new_plot.group_id = str(return_data['page_id']) + " Model2D"
        new_plot.detector = data.detector
        new_plot.source = data.source
        new_plot.is_data = False
        new_plot.qx_data = data.qx_data
        new_plot.qy_data = data.qy_data
        new_plot.q_data = data.q_data
        new_plot.mask = data.mask
        ## plot boundaries
        new_plot.ymin = data.ymin
        new_plot.ymax = data.ymax
        new_plot.xmin = data.xmin
        new_plot.xmax = data.xmax

        title = data.title

        new_plot.is_data = False
        if data.is_data:
            data_name = str(data.name)
        else:
            data_name = str(model.__class__.__name__) + '2d'

        if len(title) > 1:
            new_plot.title = "Model2D for %s " % model.name + data_name
        new_plot.name = model.name + " [" + \
                                    data_name + "]"

        return new_plot

    def new1DProductPlots(self, return_data, tab_id):
        """
        If return_data contains separated P(Q) and/or S(Q) data, create 1D plots for each and return as the tuple
        (pq_plot, sq_plot). If either are unavailable, the corresponding plot is None.
        """
        plots = []
        for name, result in return_data['intermediate_results'].items():
            if isinstance(result, tuple) and len(result) > 1:
                result = result[1]
            if not isinstance(result, np.ndarray):
                continue
            plots.append(self._create1DPlot(tab_id, return_data['x'], result,
                         return_data['model'], return_data['data'],
                         component=name))
        return plots

    def getScalarIntermediateResults(self, return_data):
        """
        Returns a dict of scalar-only intermediate results from the return data.
        """
        res = {}
        for name, int_res in return_data["intermediate_results"].items():
            if isinstance(int_res, np.ndarray):
                continue
            res[name] = int_res
        return res

    def computeDataRange(self):
        """
        Wrapper for calculating the data range based on local dataset
        """
        return self.computeRangeFromData(self.data)

    def computeRangeFromData(self, data):
        """
        Compute the minimum and the maximum range of the data
        return the npts contains in data
        """
        qmin, qmax, npts = None, None, None
        if isinstance(data, Data1D):
            try:
                qmin = min(data.x)
                qmax = max(data.x)
                npts = len(data.x)
            except (ValueError, TypeError):
                msg = "Unable to find min/max/length of \n data named %s" % \
                            self.data.filename
                raise ValueError(msg)

        else:
            qmin = 0
            try:
                x = max(np.fabs(data.xmin), np.fabs(data.xmax))
                y = max(np.fabs(data.ymin), np.fabs(data.ymax))
            except (ValueError, TypeError):
                msg = "Unable to find min/max of \n data named %s" % \
                            self.data.filename
                raise ValueError(msg)
            qmax = np.sqrt(x * x + y * y)
            npts = len(data.data)
        return qmin, qmax, npts
