import scipy.stats as ST
import numpy as NP

logit_sigmoid = lambda x: 1.0 / (1.0 + NP.exp(-x))
probit_sigmoid = lambda x: ST.norm.cdf(x)


class Likelihood:
    def __init__(self):
        pass

    # likelihood
    def plain(self, f, y):
        raise NotImplementedError

    # log likelihood
    def log(self, f, y):
        raise NotImplementedError

    # gradient of the log likelihood
    def gradient_log(self, f, y):
        raise NotImplementedError

    # hessian of the log likelihood
    def hessian_log(self, f):
        raise NotImplementedError

    def third_derivative_log(self, f):
        raise NotImplementedError


class ProbitLikelihood(Likelihood):
    def __init__(self):
        Likelihood.__init__(self)

    # likelihood
    def plain(self, f, y):
        return ST.norm._cdf(y * f).prod()

    # log likelihood
    def log(self, f, y):
        return ST.norm._logcdf(y * f).sum()


# Implements p(y|F) = prod_{i=1}^n p(y_i|f_i)
#                   = prod_{i=1}^n logistic(y_i*f_i).
class LogitLikelihood(Likelihood):
    def __init__(self):
        Likelihood.__init__(self)
        # self.sigmoid = lambda x: 1.0 / (1.0 + NP.exp(-x))
        self.sigmoid = logit_sigmoid

    # likelihood
    def plain(self, f, y):
        return self.sigmoid(y * f).prod()

    # log likelihood
    def log(self, f, y):
        yf = y * f
        r = self.sigmoid(yf)
        ok = r > 0.0
        r[ok] = NP.log(r[ok])
        r[~ok] = yf[~ok]
        return r.sum()

    # gradient of the log likelihood
    def gradient_log(self, f, y):
        return y * self.sigmoid(-y * f)

    # hessian of the log likelihood
    def hessian_log(self, f):
        r = self.sigmoid(f) * self.sigmoid(-f)
        ok = r >= 1e-16
        r[~ok] = 1e-16
        return -r
        # maybe faster, but maybe less precise version
        # pi = self.sigmoid(f)
        # return -pi*(1.0-pi)

    def third_derivative_log(self, f):
        pi = self.sigmoid(f)
        return -pi * self.sigmoid(-f) * (1.0 - 2.0 * pi)
        # maybe faster, but maybe less precise version
        # return -pi*(1.0-pi)*(1.0-2.0*pi)

    def intOverGauss(self, mu, sig2):
        # horta201306 we approximate \int logistic(x) N(x, mu, sig2) dx
        # by observing that logistic(x) is cdf of the standard logistic distribution
        # and that this distribution is similar in shape with the normal one
        # so we approximate the logistic pdf by a mixture of five normal
        # distributions, and then calculate the integral
        # we adopted the coefficients c and lambd from Rasmussen and Williams' GP toolbox
        # for Matlab.

        c = NP.array(
            [
                1.146480988574439e02,
                -1.508871030070582e03,
                2.676085036831241e03,
                -1.356294962039222e03,
                7.543285642111850e01,
            ]
        )
        lambd = NP.array([0.44, 0.41, 0.40, 0.39, 0.36])

        sigs = 1.0 / (lambd * NP.sqrt(2))

        z = mu / (sigs * NP.sqrt(1.0 + sig2 / sigs**2))

        return NP.sum(c * ST.norm.cdf(z))
