#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Created on Wed May 15 23:34:23 2019

@author: george
"""
import sys

sys.path.append('../SGMM')
sys.path.append('../metrics')
sys.path.append('../loaders')
sys.path.append('../oldCode')
sys.path.append('../visual')
sys.path.append('../testingCodes')
sys.path.append('../otherModels')

import numpy as np
#import scipy as sc
#from scipy.stats import multivariate_normal


def genxG(pi, means, covs):
    """ Generates a point from a mixture of Gaussians with parameters
    pi : list --> mixing weights
    means : list --> gaussian means
    covs : list --> list of the variances
    """
    m = len(pi) #number of mixture elements
    ind = np.random.choice( np.arange(m), size = 1, p = pi )[0] #to be index not array
    mk = means[ ind ]
    covk = covs[ ind ]
   
    #Generate from the component chosen
    x = np.random.normal( loc = mk, scale = covk, size = 1 )[0] # returns x as scalar and not array
    
    return x, ind 

def genXG( pi, means, covs, size):
    """ generates a matrix of data from the distribution
    of the MoG"""
    
    X = np.zeros( size )
    
    for point in np.arange( size ):
        x = genxG(pi, means, covs ) 
        X[point] = x[0]
    
    return X



def genyG(x, means, covs, w):
    """
    given a point x  generates a label for x according to the given distribution
    
    """
    
    m  = len( means )
    #initialize the probability matrix
    p = np.zeros( m )
    
    #find probabilities of y belonging to each of the gaussian
    for i in np.arange( len(means) ):
        mi = means[i]
        covi = covs[i]
        #g = multivariate_normal( mean = mi, cov = covi**2)
        g = gauss( x, mi, covi )
        p[i] = g
    
    #normalize probabilities to chose one of them
    pnorm = p/np.sum( p )
    
    ind = np.random.choice( np.arange( m ), size = 1, p = pnorm)[0] #returns index as a scalar
    
    #select the classifier of the Gaussian used
    ws = w[ ind ]  
    #compute probabilities to take 1 and 0
    p1 = sigm(x, ws)
    p0 = 1-p1
    
    lab = np.random.choice( [0, 1], size = 1, p = [p0, p1] )[0] #returns choice as scalar
    #print(p0, p1, lab)
    y = lab
    
    return y, ind

def genYG( X, means, covs, w):
    """Generate a matrix of Y given  a Matrix X according to the Y 
    distribution """
    
    N = X.shape[0]
    Y = np.zeros( shape = N)
    
    for i in np.arange( N ):
        Y[i], _ = genyG( X[i], means, covs, w)
        
    return Y

def gauss(x, m, cov):
     #give x mean m and standard deviation cov returns gaussian pdf of x
    
     covS = cov**2
     den1 = 1/np.sqrt( 2*np.pi*covS )
     den2 = np.exp( -(x - m)**2/( 2*covS ))
     px = den1*den2
     
     return px       
           
def probxG(x, pi, means, covs):
    """ Given x predicts the probability of being generated by  the Mixture
        distribution 
        x: scalar or array
        pi : list
        means: list
        covs: list
        """
    px = 0
    
    for k in np.arange( len( pi ) ): #for each mixture add the probability 
        pk = pi[k]
        mk = means[k]
        covk = covs[k]
        #g = multivariate_normal( mean  =  mk, cov = covk**2)
        g = gauss(x, mk, covk)
        #px +=  g.pdf(x) * pik
        px += g*pk
        
    return px

def probyG( x, means, covs, w ):
    """computes the probability of Y to be 1"""
    
    den = 0
    nom = 0
    for k in np.arange( len(means) ):
        mk = means[k]
        covk = covs[k]
        wk = w[k]
        #g = multivariate_normal( mean = mk, cov = covk**2)
        #gx = g.pdf( x )
        gx = gauss( x, mk, covk)
        nom += gx * sigm(x, wk)
        den += gx
        
    py = nom/den
    
    return py #, nom, den
    

def genData1D(pi, means, covs,  meansY,  covsY, w, N):
    """generates synthetic data in the following fashion
    1) pick a Gaussian according to mixing weights p1, p2
    2) generate a data point according to the gaussian picked
    3) find the probability of the point belonging to one of the 
    Gaussians 3, 4
    4) classify the point in one of them 
    5) use the classification Rule  of the sepcific gaussian
    6) apply a label to the point 0 or 1
    """
    
    
    #initialize data
    data = np.zeros( [N, 5] )
    
    for point in np.arange( N ):
    
        x, indx = genxG(pi, means, covs)
            
        one = [1, x ]
       
        x = np.array( one )        
        
        data[point, 0:2]  = x
        data[point, 3] = indx
    
        #choose second layer of gaussians for x
    
        y, indy = genyG( x[1], meansY, covsY, w )
    
        data[point, 2] = y
        data[point, 4] = indy
        
        #END OF LOOP FOR DATA GENERATION

    return data
    

 
def bayesO(pi, means, covs,  meansY,  covsY, w, L, U, N):
    """ Given A 1D mixtures of Gaussians Model compute the optimal error
        of the bayesian optimal classifier """
        
    Dx = (U - L)/N
    point  = L
    integral = 0
    
    for i in np.arange( N ):
       #print(i)
        #print(point)
        px = probxG( point, pi, means, covs)
        py1 = probyG( point, meansY, covsY, w)
        py0 = 1-py1
        pymin = min(py1, py0)
        p = px*pymin
        integral += p*Dx
        point += Dx
    
    return integral, point, Dx
    

def MC(pi, means, covs,  meansY,  covsY, w, N):
    """Compute the optimal Eout with monte carlo simulation """
    
    MCsum = 0
    
    for i in np.arange( N ):
        x = genxG(pi, means, covs)
        py1 = probyG(x[0], meansY, covsY, w)
        py0 = 1-py1
        pymin = min(py1, py0)
        MCsum += pymin[0]/N
        
    return MCsum
        
def bayesOF( pi, means, covs,  meansY,  covsY, w, L, U, N ): 
    """ Fast attempt for bayesOptimal """
    
    points = np.linspace(L, U, N)
    Dx = (U - L)/N
    
    px = probxG( points, pi, means, covs)
    py1 = probyG( points, meansY, covsY, w)
    py0 = 1 - py1
    py01 = np.zeros( shape = [N, 2] ) 
    py01[:, 0] = py0
    py01[:, 1] = py1
    pymin = np.min( py01, axis = 1)
    integral = np.sum( px*pymin*Dx )
    
    return integral
    
    
    
def MCF(pi, means, covs,  meansY,  covsY, w, N):
    
    x = genXG( pi, means, covs, N)
    py1 = probyG( x, meansY, covsY, w )
    py0 = 1-py1
    py01 = np.zeros( shape = [N, 2] )
    py01[:, 0] = py0
    py01[:, 1] = py1
    pymin = np.min( py01, axis = 1)
    MCsum = np.sum( pymin )/N
    
    return MCsum
       
     
def sigm(X, w):
    
      p = 1/ ( 1 + np.exp( -(X*w[1] + w[0] )  ))
      
      return p
        