import numpy as np
import progressbar

from utils import train_test_split, standardize, to_categorical, normalize
from utils import mean_squared_error, accuracy_score
from supervised_learning import XGBoostRegressionTree
from deep_learning.activation_functions import Sigmoid
from utils.misc import bar_widgets
from utils import Plot

class LogisticLoss():
    def __init__(self):
        sigmoid = Sigmoid()
        self.log_func = sigmoid
        self.log_grad = sigmoid.gradient

    def loss(self, y, y_pred):
        y_pred = np.clip(y_pred, 1e-15, 1-1e15)
        p = self.log_func(y_pred)
        return y*np.log(p) + (1-y)*np.log(1-p)

    #gradient w.r.t y_pred
    def gradient(self, y, y_pred):
        p = self.log_func(y_pred)
        return -(y-p)

    #w.r.t y_pred
    def hess(self, y, y_pred):
        p = self.log_func(y_pred)
        return p*(1-p)

class XGBoost(object):
    """
    the xgboost classifier

    parameters:
    -----------
    n_estimators:int
        the number of classification trees that are used
    learning_rate:float
        the step length that will be taken when following the negative gradient during training.
    min_samples_split:int
        the minimum number of samples needed to make a split when building a tree
    min_impurity:float
        the minimum impurity required to split the tree further
    max_depth:int
        the maximum depth of a tree
    """
    def __init__(self, n_estimators=200, learning_rate=0.001, min_samples_split=2,
                 min_impurity=1e-7, max_depth=2):
        self.n_estimators = n_estimators
        self.learning_rate = learning_rate
        self.min_samples_split = min_samples_split
        self.min_impurity = min_impurity
        self.max_depth = max_depth
        self.bar = progressbar.ProgressBar(widgets=bar_widgets)

        #log loss for classification
        self.loss = LogisticLoss()

        #initialize regression trees
        self.trees = []
        for _ in range(n_estimators):
            tree = XGBoostRegressionTree(
                min_samples_split=self.min_samples_split,
                min_impurity=self.min_impurity,
                max_depth=self.max_depth,
                loss = self.loss
            )
            self.trees.append(tree)

    def fit(self, X, y):
        y = to_categorical(y)
        y_pred = np.zeros(np.shape(y))
        for i in self.bar(range(self.n_estimators)):
            tree = self.trees[i]
            y_and_pred = np.concatenate((y, y_pred),axis=1)
            tree.fit(X,y_and_pred)
            update_pred = tree.predict(X)
            y_pred -= np.multiply(self.learning_rate,update_pred)


    def predict(self, X):
        y_pred = None
        # make predictions
        for tree in self.trees:
            #estimate gradient and update prediction
            update_pred = tree.predict(X)
            if y_pred is None:
                y_pred = np.zeros_like(update_pred)
            y_pred -= np.multiply(self.learning_rate,update_pred)

        #turn into probability distribution (softmax)
        y_pred = np.exp(y_pred)/np.sum(np.exp(y_pred),axis=1,keepdims=True)
        #set label to the value that maximizes probability
        y_pred = np.argmax(y_pred, axis=1)
        return y_pred