#!/usr/bin/env python

"""
Provide base functionality for all model components
"""

# imports
import copy
from collections import OrderedDict

import numpy as np

#TO DO: that about a way to make the parameter
#is self return if it is fittable or not

class BaseComponent:
    """
    Basic model component

    Since version 0.5.0, basic operations are no longer supported.
    """

    def __init__(self):
        """ Initialization"""

        ## Name of the model
        self.name = "BaseComponent"

        ## Parameters to be accessed by client
        self.params = {}
        self.details = {}
        ## Dictionary used to store the dispersity/averaging
        #  parameters of dispersed/averaged parameters.
        self.dispersion = {}
        # string containing information about the model such as the equation
        #of the given model, exception or possible use
        self.description = ''
        #list of parameter that can be fitted
        self.fixed = []
        #list of non-fittable parameter
        self.non_fittable = []
        ## parameters with orientation
        self.orientation_params = []
        ## magnetic parameters
        self.magnetic_params = []
        ## store dispersity reference
        self._persistency_dict = {}
        ## independent parameter name and unit [string]
        self.input_name = "Q"
        self.input_unit = "A^{-1}"
        ## output name and unit  [string]
        self.output_name = "Intensity"
        self.output_unit = "cm^{-1}"

        self.is_multiplicity_model = False
        self.is_structure_factor = False
        self.is_form_factor = False

    def __str__(self):
        """
        :return: string representatio
        """
        return self.name

    def is_fittable(self, par_name):
        """
        Check if a given parameter is fittable or not

        :param par_name: the parameter name to check

        """
        return par_name.lower() in self.fixed
        #For the future
        #return self.params[str(par_name)].is_fittable()

    def run(self, x):
        """
        run 1d
        """
        return NotImplemented

    def runXY(self, x):
        """
        run 2d
        """
        return NotImplemented

    def calculate_ER(self):
        """
        Calculate effective radius
        """
        return NotImplemented

    def calculate_VR(self):
        """
        Calculate volume fraction ratio
        """
        return NotImplemented

    def evalDistribution(self, qdist):
        """
        Evaluate a distribution of q-values.

        * For 1D, a numpy array is expected as input: ::

            evalDistribution(q)

          where q is a numpy array.


        * For 2D, a list of numpy arrays are expected: [qx_prime,qy_prime],
          where 1D arrays, ::

              qx_prime = [ qx[0], qx[1], qx[2], ....]

          and ::

              qy_prime = [ qy[0], qy[1], qy[2], ....]

        Then get ::

            q = np.sqrt(qx_prime^2+qy_prime^2)

        that is a qr in 1D array; ::

            q = [q[0], q[1], q[2], ....]

        .. note:: Due to 2D speed issue, no anisotropic scattering
                  is supported for python models, thus C-models should have
                  their own evalDistribution methods.

        The method is then called the following way: ::

            evalDistribution(q)

        where q is a numpy array.

        :param qdist: ndarray of scalar q-values or list [qx,qy] where qx,qy are 1D ndarrays
        """
        if qdist.__class__.__name__ == 'list':
            # Check whether we have a list of ndarrays [qx,qy]
            if len(qdist)!=2 or \
                qdist[0].__class__.__name__ != 'ndarray' or \
                qdist[1].__class__.__name__ != 'ndarray':
                msg = "evalDistribution expects a list of 2 ndarrays"
                raise RuntimeError(msg)

            # Extract qx and qy for code clarity
            qx = qdist[0]
            qy = qdist[1]

            # calculate q_r component for 2D isotropic
            q = np.sqrt(qx**2+qy**2)
            # vectorize the model function runXY
            v_model = np.vectorize(self.runXY, otypes=[float])
            # calculate the scattering
            iq_array = v_model(q)

            return iq_array

        elif qdist.__class__.__name__ == 'ndarray':
            # We have a simple 1D distribution of q-values
            v_model = np.vectorize(self.runXY, otypes=[float])
            iq_array = v_model(qdist)
            return iq_array

        else:
            mesg = "evalDistribution is expecting an ndarray of scalar q-values"
            mesg += " or a list [qx,qy] where qx,qy are 2D ndarrays."
            raise RuntimeError(mesg)



    def clone(self):
        """ Returns a new object identical to the current object """
        obj = copy.deepcopy(self)
        return self._clone(obj)

    def _clone(self, obj):
        """
        Internal utility function to copy the internal
        data members to a fresh copy.
        """
        obj.params     = copy.deepcopy(self.params)
        obj.details    = copy.deepcopy(self.details)
        obj.dispersion = copy.deepcopy(self.dispersion)
        obj._persistency_dict = copy.deepcopy( self._persistency_dict)
        return obj

    def set_dispersion(self, parameter, dispersion):
        """
        model dispersions
        """
        ##Not Implemented
        return None

    def getProfile(self):
        """
        Get SLD profile

        : return: (z, beta) where z is a list of depth of the transition points
                beta is a list of the corresponding SLD values
        """
        #Not Implemented
        return None, None

    def setParam(self, name, value):
        """
        Set the value of a model parameter

        :param name: name of the parameter
        :param value: value of the parameter

        """
        # Look for dispersion parameters
        toks = name.split('.')
        if len(toks)==2:
            for item in self.dispersion.keys():
                if item.lower()==toks[0].lower():
                    for par in self.dispersion[item]:
                        if par.lower() == toks[1].lower():
                            self.dispersion[item][par] = value
                            return
        else:
            # Look for standard parameter
            for item in self.params.keys():
                if item.lower()==name.lower():
                    self.params[item] = value
                    return

        raise ValueError("Model does not contain parameter %s" % name)

    def getParam(self, name):
        """
        Set the value of a model parameter
        :param name: name of the parameter

        """
        # Look for dispersion parameters
        toks = name.split('.')
        if len(toks)==2:
            for item in self.dispersion.keys():
                if item.lower()==toks[0].lower():
                    for par in self.dispersion[item]:
                        if par.lower() == toks[1].lower():
                            return self.dispersion[item][par]
        else:
            # Look for standard parameter
            for item in self.params.keys():
                if item.lower()==name.lower():
                    return self.params[item]

        raise ValueError("Model does not contain parameter %s" % name)

    def getParamList(self):
        """
        Return a list of all available parameters for the model
        """
        list = _ordered_keys(self.params)
        # WARNING: Extending the list with the dispersion parameters
        list.extend(self.getDispParamList())
        return list

    def getDispParamList(self):
        """
        Return a list of all available parameters for the model
        """
        list = []
        for item in _ordered_keys(self.dispersion):
            for p in _ordered_keys(self.dispersion[item]):
                if p not in ['type']:
                    list.append('%s.%s' % (item.lower(), p.lower()))

        return list

    # Old-style methods that are no longer used
    def setParamWithToken(self, name, value, token, member):
        """
        set Param With Token
        """
        return NotImplemented
    def getParamWithToken(self, name, token, member):
        """
        get Param With Token
        """
        return NotImplemented

    def getParamListWithToken(self, token, member):
        """
        get Param List With Token
        """
        return NotImplemented
    def __add__(self, other):
        """
        add
        """
        raise ValueError("Model operation are no longer supported")
    def __sub__(self, other):
        """
        sub
        """
        raise ValueError("Model operation are no longer supported")
    def __mul__(self, other):
        """
        mul
        """
        raise ValueError("Model operation are no longer supported")
    def __div__(self, other):
        """
        div
        """
        raise ValueError("Model operation are no longer supported")


def _ordered_keys(d):
    keys = list(d.keys())
    if not isinstance(d, OrderedDict):
        keys.sort()
    return keys
