from numpy.core.fromnumeric import mean
import pandas as pd 
import numpy as np 

from numpy.core.fromnumeric import var
from numpy.linalg import pinv
import pandas as pd
import numpy as np

"""
Helper functions to help in data manipulation
"""

"""
Functions to generate different plots
"""

import matplotlib.pyplot as plt 
import seaborn as sns 

"""
Basic Scatter Plot
"""
def generateScatterPlot(x,y):
    plt.scatter(x,y)
    plt.show()


"""
Basic Box Plot
"""
def generateBoxPlot(x,figSize):
    plt.figure(figsize=figSize)
    sns.boxplot(x)
    plt.show()

"""
Basic Histogram
"""
def generateHistogram(x,y):
    plt.hist(x,y)
    plt.show()

"""
Basic Correlation Matrix
"""
def generateCorrelationMatrix(data):
    corr = data.corr()
    corr.style.background_gradient()
    return corr

"""
Pandas implementation of train_test_split
Note: Should be split into Features and Labels post split
"""

def getTrainingTestingData(data, trainingRatio = 0.75, seed = 42):

    # Step 1
    trainingData = data.sample(frac = trainingRatio, random_state = seed)

    # Step 2 
    trainingIndex = trainingData.index 

    # Step 3 
    testingData = data.drop(trainingIndex)

    return trainingData,testingData

"""
Sigmoid Function
"""
def sigmoid(x):
    return 1.0/(1.0 + np.exp(-x))

"""
Append a number to the start of every row in a matrix
"""
def appendNumberToEveryRow(array,number):
    return np.insert(array,0,number,axis=1)

"""
Reshaping a Matrix array to newDimensions
"""
def reshapeVector(array,newDimensions):
    return np.reshape(array,newDimensions)

"""
Get Error Metrics of the Model
"""
def getErrorMetrics(x,weights,y):
    predictedY = np.dot(x,weights)
    probabilityValues = sigmoid(predictedY)
    
    tp = 0
    tn = 0
    fp = 0
    fn = 0

    for i in range(len(probabilityValues)):
        if probabilityValues[i] >= 0.5 and y[i] == 1:
            tp += 1
        elif probabilityValues[i] < 0.5 and y[i] == 0:
            tn += 1 
        elif probabilityValues[i] >= 0.5 and y[i] == 0:
            fp += 1 
        else:
            fn += 1 

    accuracyDict = {}
    accuracyDict["accuracy"] = (tp+tn)/(tp+tn+fp+fn)
    accuracyDict["tp"] = tp 
    accuracyDict["tn"] = tn 
    accuracyDict["fp"] = fp
    accuracyDict["fn"] = fn 
    return accuracyDict 

"""
Parse a Pandas DataFrame to a Numpy Array
"""
def dataFrameToNumpyArray(dataFrame):
    return dataFrame.to_numpy()

"""
Compute the Gaussian Probability
"""
def getGaussianLogVector(x,mean,sigma):
    const = -x.shape[1] / 2 * np.log(2 * np.pi) - 0.5 * np.sum(np.log(sigma))
    probs = 0.5 * np.sum(np.power(x - mean, 2) / (sigma), 1)
    return const - probs
    

"""
Get the mean over an array with column Calculation Index colIndex
"""
def getMeanOverArray(array, colIndex):
    return np.mean(array,axis = colIndex)

"""
Get the variance over an array with column Calculation Index colIndex
"""
def getVarianceOverArray(array, colIndex):
    return np.var(array, axis = colIndex)

"""
Return the Index of the Maximum Value of Array along axis colIndex
"""
def getMaximumIndex(array, colIndex):
    return np.argmax(array, axis = colIndex)

"""
Return the Inverse of a Matrix
"""
def getMatrixInverse(x):
    return pinv(x)


"""
Implementation of the Gaussian Naive Bayes Classifier
"""

class NaiveBayes:
    """
    Constructor
    """
    def __init__(self,x,y):
        self._x = np.array(x)
        self._y = np.array(y)
        self._y = reshapeVector(self._y,(self._x.shape[0],1))  

        self._numRecords = self._x.shape[0]
        self._numFeatures = self._x.shape[1]
        self._numClasses = len(np.unique(y))

        self._meanValues = {}
        self._varianceValues = {}
        self._likelihoodValues = {}


    """
    Get Predicted Class Labels 
    """
    def getClassLabels(self):
        x = self._x 
        y = self._y 
        n = self._numRecords
        m = self._numFeatures
        k = self._numClasses

       
        probabilityValues = np.zeros((n,k))

        for classLabel in range(k):
            classId = str(classLabel)

            # Step 1 
            calc1 = []
            for i in range(len(x)):
              if y[i] == classLabel:
                calc1.append(x[i])
            
            calc1 = np.array(calc1)

            # Step 2
            self._meanValues[classId] = getMeanOverArray(calc1,0)

            # Step 3 
            self._varianceValues[classId] = getVarianceOverArray(calc1,0)

            # Step 4 
            self._likelihoodValues[classId] = calc1.shape[0]/x.shape[0]

            # Step 5 
            calc2 = getGaussianLogVector(x,self._meanValues[classId],self._varianceValues[classId])

            # Step 6 
            calc3 = calc2 + np.log(self._likelihoodValues[classId])

            # Step 7 
            probabilityValues[:, classLabel] = calc3 

        return getMaximumIndex(probabilityValues,1)
            



    """
    Get Accuracy
    """
    def getAccuracy(self,labelValues):
        
        totalCorrect = 0
        y = self._y
        
        for i in range(len(y)):
            if labelValues[i] == y[i]:
                totalCorrect += 1 
        
        return totalCorrect/len(labelValues)
    
    """
    Get Accuracy on Testing Data
    """
    def getAccuracyTesting(self,x,y):
        
        x = np.array(x)
        y = np.array(y)
        y = reshapeVector(y,(x.shape[0],1))
        
        n = x.shape[0]
        m = self._numFeatures
        k = self._numClasses

        probabilityValues = np.zeros((n,k))
        
        mean = self._meanValues
        variance = self._varianceValues
        likelihood = self._likelihoodValues

        for classId in range(k):
            classLabel = str(classId)

            # Step 1 
            calc1 = getGaussianLogVector(x,mean[classLabel],variance[classLabel])

            # Step 2
            calc2 = calc1 + np.log(likelihood[classLabel])

            # Step 3 
            probabilityValues[:, classId] = calc2 
        
        predictions = getMaximumIndex(probabilityValues,1)
        
        numCorrect = 0 
        for i in range(len(predictions)):
            if predictions[i] == y[i]:
                numCorrect += 1
        
        return numCorrect/len(predictions)