# Copyright 2020 Huawei Technologies Co., Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ============================================================================
"""Gamma Distribution"""
import numpy as np
from mindspore.ops import operations as P
from mindspore.ops import functional as F
from mindspore.ops import composite as C
import mindspore.nn as nn
from mindspore import _checkparam as Validator
from mindspore.common import dtype as mstype
from .distribution import Distribution
from ._utils.utils import check_greater_zero, check_distribution_name
from ._utils.custom_ops import log_generic


class Gamma(Distribution):
    r"""
    Gamma distribution.
    A Gamma distributio is a continuous distribution with the range :math:`(0, \inf)`
    and the probability density function:

    .. math::
        f(x, \alpha, \beta) = \beta^\alpha / \Gamma(\alpha) x^{\alpha - 1} \exp(-\beta x).

    where :math:`G` is the Gamma function,
    and :math:`\alpha` and :math:`\beta` are the concentration and the rate of the distribution respectively.

    Args:
        concentration (int, float, list, numpy.ndarray, Tensor): The concentration,
          also know as alpha of the Gamma distribution. Default: ``None`` .
        rate (int, float, list, numpy.ndarray, Tensor): The rate, also know as
          beta of the Gamma distribution. Default: ``None`` .
        seed (int): The seed used in sampling. The global seed is used if it is None. Default: ``None`` .
        dtype (mindspore.dtype): The type of the event samples. Default: ``mstype.float32`` .
        name (str): The name of the distribution. Default: ``'Gamma'`` .

    Note:
        `concentration` and `rate` must be greater than zero.
        `dist_spec_args` are `concentration` and `rate`.
        `dtype` must be a float type because Gamma distributions are continuous.

    Raises:
        ValueError: When concentration <= 0 or rate <= 0.
        TypeError: When the input `dtype` is not a subclass of float.

    Supported Platforms:
        ``Ascend``

    Examples:
        >>> import mindspore
        >>> import mindspore.nn as nn
        >>> import mindspore.nn.probability.distribution as msd
        >>> from mindspore import Tensor
        >>> # To initialize a Gamma distribution of the concentration 3.0 and the rate 4.0.
        >>> g1 = msd.Gamma([3.0], [4.0], dtype=mindspore.float32)
        >>> # A Gamma distribution can be initialized without arguments.
        >>> # In this case, `concentration` and `rate` must be passed in through arguments.
        >>> g2 = msd.Gamma(dtype=mindspore.float32)
        >>> # Here are some tensors used below for testing
        >>> value = Tensor([1.0, 2.0, 3.0], dtype=mindspore.float32)
        >>> concentration_a = Tensor([2.0], dtype=mindspore.float32)
        >>> rate_a = Tensor([2.0, 2.0, 2.0], dtype=mindspore.float32)
        >>> concentration_b = Tensor([1.0], dtype=mindspore.float32)
        >>> rate_b = Tensor([1.0, 1.5, 2.0], dtype=mindspore.float32)
        >>>
        >>> # Private interfaces of probability functions corresponding to public interfaces, including
        >>> # `prob`, `log_prob`, `cdf`, `log_cdf`, `survival_function`, and `log_survival`,
        >>> # have the same arguments as follows.
        >>> # Args:
        >>> #     value (Tensor): the value to be evaluated.
        >>> #     concentration (Tensor): the concentration of the distribution. Default: self._concentration.
        >>> #     rate (Tensor): the rate of the distribution. Default: self._rate.
        >>> # Examples of `prob`.
        >>> # Similar calls can be made to other probability functions
        >>> # by replacing 'prob' by the name of the function
        >>> ans = g1.prob(value)
        >>> print(ans.shape)
        (3,)
        >>> # Evaluate with respect to the distribution b.
        >>> ans = g1.prob(value, concentration_b, rate_b)
        >>> print(ans.shape)
        (3,)
        >>> # `concentration` and `rate` must be passed in during function calls for g2.
        >>> ans = g2.prob(value, concentration_a, rate_a)
        >>> print(ans.shape)
        (3,)
        >>> # Functions `mean`, `sd`, `mode`, `var`, and `entropy` have the same arguments.
        >>> # Args:
        >>> #     concentration (Tensor): the concentration of the distribution. Default: self._concentration.
        >>> #     rate (Tensor): the rate of the distribution. Default: self._rate.
        >>> # Example of `mean`, `sd`, `mode`, `var`, and `entropy` are similar.
        >>> ans = g1.mean()
        >>> print(ans.shape)
        (1,)
        >>> ans = g1.mean(concentration_b, rate_b)
        >>> print(ans.shape)
        (3,)
        >>> # `concentration` and `rate` must be passed in during function calls.
        >>> ans = g2.mean(concentration_a, rate_a)
        >>> print(ans.shape)
        (3,)
        >>> # Interfaces of 'kl_loss' and 'cross_entropy' are the same:
        >>> # Args:
        >>> #     dist (str): the type of the distributions. Only "Gamma" is supported.
        >>> #     concentration_b (Tensor): the concentration of distribution b.
        >>> #     rate_b (Tensor): the rate of distribution b.
        >>> #     concentration_a (Tensor): the concentration of distribution a. Default: self._concentration.
        >>> #     rate_a (Tensor): the rate of distribution a. Default: self._rate.
        >>> # Examples of `kl_loss`. `cross_entropy` is similar.
        >>> ans = g1.kl_loss('Gamma', concentration_b, rate_b)
        >>> print(ans.shape)
        (3,)
        >>> ans = g1.kl_loss('Gamma', concentration_b, rate_b, concentration_a, rate_a)
        >>> print(ans.shape)
        (3,)
        >>> # Additional `concentration` and `rate` must be passed in.
        >>> ans = g2.kl_loss('Gamma', concentration_b, rate_b, concentration_a, rate_a)
        >>> print(ans.shape)
        (3,)
        >>> # Examples of `sample`.
        >>> # Args:
        >>> #     shape (tuple): the shape of the sample. Default: ()
        >>> #     concentration (Tensor): the concentration of the distribution. Default: self._concentration.
        >>> #     rate (Tensor): the rate of the distribution. Default: self._rate.
        >>> ans = g1.sample()
        >>> print(ans.shape)
        (1,)
        >>> ans = g1.sample((2,3))
        >>> print(ans.shape)
        (2, 3, 1)
        >>> ans = g1.sample((2,3), concentration_b, rate_b)
        >>> print(ans.shape)
        (2, 3, 3)
        >>> ans = g2.sample((2,3), concentration_a, rate_a)
        >>> print(ans.shape)
        (2, 3, 3)
    """

    def __init__(self,
                 concentration=None,
                 rate=None,
                 seed=None,
                 dtype=mstype.float32,
                 name="Gamma"):
        """
        Constructor of Gamma.
        """
        param = dict(locals())
        param['param_dict'] = {'concentration': concentration, 'rate': rate}
        valid_dtype = mstype.float_type
        Validator.check_type_name(
            "dtype", dtype, valid_dtype, type(self).__name__)

        # As some operators can't accept scalar input, check the type here
        if isinstance(concentration, (int, float)):
            raise TypeError("Input concentration can't be scalar")
        if isinstance(rate, (int, float)):
            raise TypeError("Input rate can't be scalar")

        super(Gamma, self).__init__(seed, dtype, name, param)

        self._concentration = self._add_parameter(
            concentration, 'concentration')
        self._rate = self._add_parameter(rate, 'rate')
        if self._concentration is not None:
            check_greater_zero(self._concentration, "concentration")
        if self._rate is not None:
            check_greater_zero(self._rate, "rate")

        # ops needed for the class
        self.log = log_generic
        self.square = P.Square()
        self.sqrt = P.Sqrt()
        self.squeeze = P.Squeeze(0)
        self.cast = P.Cast()
        self.dtypeop = P.DType()
        self.shape = P.Shape()
        self.select = P.Select()
        self.greater = P.Greater()
        self.lgamma = P.Lgamma()
        self.digamma = nn.DiGamma()
        self.igamma = P.Igamma()

    def extend_repr(self):
        """Display instance object as string."""
        if self.is_scalar_batch:
            s = 'concentration = {}, rate = {}'.format(
                self._concentration, self._rate)
        else:
            s = 'batch_shape = {}'.format(self._broadcast_shape)
        return s

    @property
    def concentration(self):
        """
        Return the concentration, also know as the alpha of the Gamma distribution,
        after casting to dtype.

        Output:
            Tensor, the concentration parameter of the distribution.
        """
        return self._concentration

    @property
    def rate(self):
        """
        Return the rate, also know as the beta of the Gamma distribution,
        after casting to dtype.

        Output:
            Tensor, the rate parameter of the distribution.
        """
        return self._rate

    def _get_dist_type(self):
        return "Gamma"

    def _get_dist_args(self, concentration=None, rate=None):
        if concentration is not None:
            self.checktensor(concentration, 'concentration')
        else:
            concentration = self._concentration
        if rate is not None:
            self.checktensor(rate, 'rate')
        else:
            rate = self._rate
        return concentration, rate

    def _mean(self, concentration=None, rate=None):
        """
        The mean of the distribution.
        """
        concentration, rate = self._check_param_type(concentration, rate)
        return concentration / rate

    def _var(self, concentration=None, rate=None):
        """
        The variance of the distribution.
        """
        concentration, rate = self._check_param_type(concentration, rate)
        return concentration / self.square(rate)

    def _sd(self, concentration=None, rate=None):
        """
        The standard deviation of the distribution.
        """
        concentration, rate = self._check_param_type(concentration, rate)
        return self.sqrt(concentration) / rate

    def _mode(self, concentration=None, rate=None):
        """
        The mode of the distribution.
        """
        concentration, rate = self._check_param_type(concentration, rate)
        mode = (concentration - 1.) / rate
        nan = F.fill(self.dtypeop(concentration), self.shape(concentration),
                     np.nan)
        comp = self.greater(concentration, 1.)
        return self.select(comp, mode, nan)

    def _entropy(self, concentration=None, rate=None):
        r"""
        Evaluate entropy.

        .. math::
            H(X) = \alpha - \log(\beta) + \log(\Gamma(\alpha)) + (1 - \alpha) * \digamma(\alpha)
        """
        concentration, rate = self._check_param_type(concentration, rate)
        return concentration - self.log(rate) + self.lgamma(concentration) \
            + (1. - concentration) * self.digamma(concentration)

    def _cross_entropy(self, dist, concentration_b, rate_b, concentration_a=None, rate_a=None):
        r"""
        Evaluate cross entropy between Gamma distributions.

        Args:
            dist (str): Type of the distributions. Should be "Gamma" in this case.
            concentration_b (Tensor): concentration of distribution b.
            rate_b (Tensor): rate of distribution b.
            concentration_a (Tensor): concentration of distribution a. Default: self._concentration.
            rate_a (Tensor): rate of distribution a. Default: self._rate.
        """
        check_distribution_name(dist, 'Gamma')
        return self._entropy(concentration_a, rate_a) +\
            self._kl_loss(dist, concentration_b, rate_b,
                          concentration_a, rate_a)

    def _log_prob(self, value, concentration=None, rate=None):
        r"""
        Evaluate log probability.

        Args:
            value (Tensor): The value to be evaluated.
            concentration (Tensor): The concentration of the distribution. Default: self._concentration.
            rate (Tensor): The rate the distribution. Default: self._rate.

        .. math::
            L(x) = (\alpha - 1) * \log(x) - \beta * x - \log(\gamma(\alpha)) - \alpha * \log(\beta)
        """
        value = self._check_value(value, 'value')
        value = self.cast(value, self.dtype)
        concentration, rate = self._check_param_type(concentration, rate)
        unnormalized_log_prob = (concentration - 1.) * \
            self.log(value) - rate * value
        log_normalization = self.lgamma(
            concentration) - concentration * self.log(rate)
        return unnormalized_log_prob - log_normalization

    def _cdf(self, value, concentration=None, rate=None):
        r"""
        Evaluate the cumulative distribution function on the given value. Note that igamma returns
        the regularized incomplete gamma function, which is what we want for the CDF.

        Args:
            value (Tensor): The value to be evaluated.
            concentration (Tensor): The concentration of the distribution. Default: self._concentration.
            rate (Tensor): The rate the distribution. Default: self._rate.

        .. math::
            cdf(x) = \igamma(\alpha, \beta * x)
        """
        value = self._check_value(value, 'value')
        value = self.cast(value, self.dtype)
        concentration, rate = self._check_param_type(concentration, rate)
        return self.igamma(concentration, rate * value)

    def _kl_loss(self, dist, concentration_b, rate_b, concentration_a=None, rate_a=None):
        r"""
        Evaluate Gamma-Gamma KL divergence, i.e. KL(a||b).

        Args:
            dist (str): The type of the distributions. Should be "Gamma" in this case.
            concentration_b (Tensor): The concentration of distribution b.
            rate_b (Tensor): The rate distribution b.
            concentration_a (Tensor): The concentration of distribution a. Default: self._concentration.
            rate_a (Tensor): The rate distribution a. Default: self._rate.

        .. math::
            KL(a||b) = (\alpha_{a} - \alpha_{b}) * \digamma(\alpha_{a}) + \log(\gamma(\alpha_{b}))
                       - \log(\gamma(\alpha_{a})) + \alpha_{b} * \log(\beta{a}) - \alpha_{b} * \log(\beta{b})
                       + \alpha_{a} * \frac{\beta{b}}{\beta{a} - 1}
        """
        check_distribution_name(dist, 'Gamma')
        concentration_b = self._check_value(concentration_b, 'concentration_b')
        rate_b = self._check_value(rate_b, 'rate_b')
        concentration_b = self.cast(concentration_b, self.parameter_type)
        rate_b = self.cast(rate_b, self.parameter_type)
        concentration_a, rate_a = self._check_param_type(
            concentration_a, rate_a)
        return (concentration_a - concentration_b) * self.digamma(concentration_a) \
            + self.lgamma(concentration_b) - self.lgamma(concentration_a) \
            + concentration_b * self.log(rate_a) - concentration_b * self.log(rate_b) \
            + concentration_a * (rate_b / rate_a - 1.)

    def _sample(self, shape=(), concentration=None, rate=None):
        """
        Sampling.

        Args:
            shape (tuple): The shape of the sample. Default: ().
            concentration (Tensor): The concentration of the samples. Default: self._concentration.
            rate (Tensor): The rate of the samples. Default: self._rate.

        Returns:
            Tensor, with the shape being shape + batch_shape.
        """
        shape = self.checktuple(shape, 'shape')
        concentration, rate = self._check_param_type(concentration, rate)
        batch_shape = self.shape(concentration + rate)
        origin_shape = shape + batch_shape
        if origin_shape == ():
            sample_shape = (1,)
        else:
            sample_shape = origin_shape
        sample_gamma = C.gamma(sample_shape, concentration, rate, self.seed)
        value = self.cast(sample_gamma, self.dtype)
        if origin_shape == ():
            value = self.squeeze(value)
        return value
