import scipy.stats.distributions as sc_dst
import inspect
import numpy as np
from pymc import Stochastic
from copy import copy
from .distributions import *

"""
Wraps a SciPy rv object in a PyMC random variable object.
Needs to wrap the following methods:

generic.rvs(<shape(s)>,loc=0,scale=1)
    - random variates

generic.pdf(x,<shape(s)>,loc=0,scale=1)
    - probability density function

generic.cdf(x,<shape(s)>,loc=0,scale=1)
    - cumulative density function

generic.sf(x,<shape(s)>,loc=0,scale=1)
    - survival function (1-cdf --- sometimes more accurate)

generic.ppf(q,<shape(s)>,loc=0,scale=1)
    - percent point function (inverse of cdf --- percentiles)

generic.isf(q,<shape(s)>,loc=0,scale=1)
    - inverse survival function (inverse of sf)

generic.stats(<shape(s)>,loc=0,scale=1,moments='mv')
    - mean('m',axis=0), variance('v'), skew('s'), and/or kurtosis('k')

generic.entropy(<shape(s)>,loc=0,scale=1)
    - (differential) entropy of the RV.
"""
__all__ = ['stochastic_from_scipy_dist']
def separate_shape_args(kwds, shape_args):
    
    new_kwds = {}
    for name in kwds:
        new_kwds[name] = kwds[name]
    
    args = [new_kwds.pop(name) for name in shape_args]
    
    return args, new_kwds

def remove_size_arg(fun):
    def wrapper(size=1,*args, **kwds):
        return fun(*args, **kwds)


def stochastic_from_scipy_dist(scipy_dist):
    """
    Return a Stochastic subclass made from a particular SciPy distribution.
    """

    name = scipy_dist.__class__.__name__.replace('_gen','').capitalize()

    
    (args, varargs, varkw, defaults) = inspect.getargspec(scipy_dist._cdf)
    shape_args = args[2:]
    if isinstance(scipy_dist, sc_dst.rv_continuous):
        dtype=float
        
        def logp(value, **kwds):
            args, kwds = separate_shape_args(kwds, shape_args)
            return np.sum(scipy_dist.logpdf(value,*args,**kwds))
        
        parent_names = shape_args + ['loc', 'scale']
        defaults = [None] * (len(parent_names)-2) + [0., 1.]
    
    elif isinstance(scipy_dist, sc_dst.rv_discrete):
        dtype=int
        
        def logp(value, **kwds):
            args, kwds = separate_shape_args(kwds, shape_args)
            return np.sum(scipy_dist.logpmf(value,*args,**kwds))
        
        parent_names = shape_args + ['loc']
        defaults = [None] * (len(parent_names)-1) + [0]
    else:
        return None

    
    parents_default = dict(zip(parent_names, defaults))

    
    def random(shape=None,**kwds):
        args, kwds = separate_shape_args(kwds, shape_args)
        
        if shape is None:
            return scipy_dist.rvs(*args,**kwds)
        else:
            return np.reshape(scipy_dist.rvs(*args,**kwds), shape)

    # Build docstring from distribution
    docstr = name[0]+' = '+name + '(name, '+', '.join(parent_names)+', value=None, shape=None, trace=True, rseed=True, doc=None)\n\n'
    docstr += 'Stochastic variable with '+name+' distribution.\nParents are: '+', '.join(parent_names) + '.\n\n'
    docstr += """
Methods:
    
    random()
        - draws random value
          sets value to return value
    
    ppf(q)
        - percent point function (inverse of cdf --- percentiles)
          sets value to return value
    
    isf(q)
        - inverse survival function (inverse of sf)
          sets value to return value
    
    stats(moments='mv')
        - mean('m',axis=0), variance('v'), skew('s'), and/or kurtosis('k')


Attributes:
    
    logp
        - sum(log(pdf())) or sum(log(pmf()))
    
    cdf
        - cumulative distribution function
    
    sf
        - survival function (1-cdf --- sometimes more accurate)
    
    entropy
        - (differential) entropy of the RV.


NOTE: If you encounter difficulties with this object, please try the analogous
computation using the rv objects in scipy.stats.distributions directly before
reporting the bug.
    """
    
    new_class = new_dist_class(dtype, name, parent_names, parents_default, docstr, logp, random, True, None)
    class newer_class(new_class):
        __doc__ = docstr
        rv = scipy_dist
        def __init__(self, *args, **kwds):
            new_class.__init__(self, *args, **kwds)
            self.args, self.kwds = separate_shape_args(self.parents, shape_args)
            self.frozen_rv = self.rv(self.args, self.kwds)
        
        def _pymc_dists_to_value(self, args):
            """Replace arguments that are a pymc.Node with their value."""
            # This is needed because the scipy rv function transforms
            # every input argument which causes new pymc lambda
            # functions to be generated. Thus, when calling this many
            # many times, excessive amounts of RAM are used.
            new_args = []
            for arg in args:
                if isinstance(arg, pm.Node):
                    new_args.append(arg.value)
                else:
                    new_args.append(arg)
            return new_args
        
        def _cdf(self):
            """
            The cumulative distribution function of self conditional on parents
            evaluated at self's current value
            """
            return self.rv.cdf(self.value, *self._pymc_dists_to_value(self.args), **self.kwds)
        cdf = property(_cdf, doc=_cdf.__doc__)
        
        def _sf(self):
            """
            The survival function of self conditional on parents
            evaluated at self's current value
            """
            return self.rv.sf(self.value, *self._pymc_dists_to_value(self.args), **self.kwds)
        sf = property(_sf, doc=_sf.__doc__)
        
        def ppf(self, q):
            """
            The percentile point function (inverse cdf) of self conditional on parents.
            Self's value will be set to the return value.
            """
            self.value = self.rv.ppf(q, *self._pymc_dists_to_value(self.args), **self.kwds)
            return self.value

        def isf(self, q):
            """
            The inverse survival function of self conditional on parents.
            Self's value will be set to the return value.
            """
            self.value = self.rv.isf(q, *self._pymc_dists_to_value(self.args), **self.kwds)
            return self.value

        def stats(self, moments='mv'):
            """The first few moments of self's distribution conditional on parents"""
            return self.rv.stats(moments=moments, *self._pymc_dists_to_value(self.args), **self.kwds)
        
        def _entropy(self):
            """The entropy of self's distribution conditional on its parents"""
            return self.rv.entropy(*self._pymc_dists_to_value(self.args), **self.kwds)
        entropy = property(_entropy, doc=_entropy.__doc__)
    
    newer_class.__name__ = new_class.__name__
    return newer_class

for scipy_dist_name in sc_dst.__all__:
    scipy_dist = sc_dst.__dict__[scipy_dist_name]
    if isinstance(scipy_dist, sc_dst.rv_continuous) or isinstance(scipy_dist, sc_dst.rv_discrete):
        new_dist = stochastic_from_scipy_dist(scipy_dist)
        locals()[new_dist.__name__] = new_dist
        __all__.append(new_dist.__name__)
