import numpy as np
from collections import deque
from hm2_classifiers import classifier


def update_preq_metric(s_prev, n_prev, correct, fading_factor=0.99):
    s = correct + fading_factor * s_prev
    n = 1.0 + fading_factor * n_prev
    metric = s / n

    return s, n, metric


def ogd(data, simulation_time, learning_rate, momentum, hidden_activation_fun, hidden_units):
    # classifier
    cls = classifier(learning_rate=learning_rate, momentum=momentum, hidden_activation_fun=hidden_activation_fun, hidden_units=hidden_units)

    # prequential evaluation
    preq_general_accs = []
    preq_general_acc_n = 0.0
    preq_general_acc_s = 0.0

    # start
    for t in range(simulation_time):

        if t % 1000 == 0:
            print('Time step: ', t)

        # receive example
        xy = data[t, :]

        x = xy[:-1]
        x = np.reshape(x, (1, 2))

        y = xy[-1]
        y = np.reshape(y, (1, 1))

        # predict class label
        y_hat_prob = cls.model.predict(x=x, verbose=0)
        y_hat_class = np.around(y_hat_prob)

        # check if correct
        correct = 1 if y == y_hat_class else 0

        # update classifier
        cls.model.fit(
            x=x,
            y=y.astype('int'),  # cast class to integer
            epochs=1,
            batch_size=1,
            verbose=0
        )

        # update accuracy
        preq_general_acc_s, preq_general_acc_n, preq_general_acc = \
            update_preq_metric(preq_general_acc_s, preq_general_acc_n, correct)
        preq_general_accs.append(preq_general_acc)

    return preq_general_accs


def ogd_sliding(data, simulation_time, learning_rate, momentum, batch_size, hidden_activation_fun, hidden_units):
    # classifier
    cls = classifier(learning_rate=learning_rate, momentum=momentum, hidden_activation_fun=hidden_activation_fun, hidden_units=hidden_units)

    # prequential evaluation
    preq_general_accs = []
    preq_general_acc_n = 0.0
    preq_general_acc_s = 0.0

    # sliding window
    q = deque(maxlen=batch_size)

    # start
    for t in range(simulation_time):

        if t % 1000 == 0:
            print('Time step: ', t)

        # receive example
        xy = data[t, :]

        x = xy[:-1]
        x = np.reshape(x, (1, 2))

        y = xy[-1]
        y = np.reshape(y, (1, 1))

        # predict class label
        y_hat_prob = cls.model.predict(x=x, verbose=0)
        y_hat_class = np.around(y_hat_prob)

        # check if correct
        correct = 1 if y == y_hat_class else 0

        # append example
        q.append(xy)
        arr = np.asarray(q)
        x = arr[:, :-1]
        y = arr[:,-1]
        y = np.reshape(y, (len(y), 1))

        # update classifier
        cls.model.fit(
            x=x,
            y=y.astype('int'),  # cast class to integer
            epochs=1,
            batch_size=batch_size,
            verbose=0
        )

        # update accuracy
        preq_general_acc_s, preq_general_acc_n, preq_general_acc = \
            update_preq_metric(preq_general_acc_s, preq_general_acc_n, correct)
        preq_general_accs.append(preq_general_acc)

    return preq_general_accs
