import os.path
import numpy as np
import keras.backend as K
from keras import callbacks

np.random.seed(5)


def unison_shuffled_copies(arrays):
    """shuffle multiple arrays keeping
    corresponding items in same relative positions
    between arrays"""
    # print(len(arrays))
    # print(len(arrays[0]))
    length = len(arrays[1])
    # print(length)

    for x in arrays:
        print(x)
        print(len(x))
        assert len(x) == length
    p = np.random.permutation(length)
    return tuple([x[p] for x in arrays])


def to_numpy_tensors(x, y):
    """ return x and y as numpy tensors,
    x as a 3-D tensor, y as a 2-D tensor"""

    #x_shape = (len(x), len(x[0]), len(x[0][0]))
    #y_shape = (len(y), 1)

    if type(x) is list:
        x = np.array(x)
    if type(y) is list:
        y = np.array(y)

    #x = x.reshape(x_shape)
    #y = y.reshape(y_shape)
    x = x.reshape(x.shape[0], x.shape[1], 1)
    return x, y


def get_mean_and_stdv(dataset):
    """return means and standard
    deviations along 0th axis of tensor"""
    means = dataset.mean(0)
    stdvs = dataset.std(0)
    return means, stdvs


def scale_array(dataset, means, stdvs):
    """normalise by means and standard
    deviations along 0th axis of tensor"""
    return (dataset - means) / (stdvs + K.epsilon())


def check_filename(originalName):
    """ add number to filename to make
    unique if not already - returns new name"""
    count = 1
    exists = True
    fileName = originalName
    while(exists):
        exists = os.path.exists(fileName)
        if exists:
            if "." in originalName:
                fileName = originalName.split(".")
                fileName[0] += "_" + str(count)
                fileName = ".".join(fileName)
            else:
                fileName = originalName + "_" + str(count)
            count += 1
    return fileName


def truncate_and_tensor(dataX, dataY, length):
    """crop any longer than length,
    return new X and Y tensors"""
    new_X = []
    new_Y = []

    for x in list(range(len(dataX))):
        new_X.append(dataX[x][:length])
        new_Y.append(dataY[x][:length])

    x, y = to_numpy_tensors(new_X, new_Y)
    return x, y


def remove_short(dataX, dataY, length):
    """Remove any seqences in dataX shorter than length,
    crop any longer than length, return new X and Y tensors"""
    new_X = []
    new_Y = []
    '''
    for x in list(range(len(dataX))):
        if len(dataX[x]) >= length:
            new_X.append(dataX[x][:length])
            new_Y.append(dataY[x][:length])

    x, y = to_numpy_tensors(new_X, new_Y)
    '''
    x, y = to_numpy_tensors(dataX, dataY)
    return x, y


def remove_short_idx(dataX, dataY, idxs, length):
    """ Remove any seqences in dataX shorter than length,
    crop any longer than length, return new X and Y tensors
    and indicies of the returned samples with refernce to
    their position in the original array"""
    new_X = []
    new_Y = []
    #new_idxs = []
    # dataX[x] will kinda wil always be greater than length
    '''
    for x in list(range(len(dataX))):
        if len(dataX[x]) >= length:
            new_X.append(dataX[x][:length])
            new_Y.append(dataY[x][:length])
            new_idxs.append(idxs[x])

    x, y = to_numpy_tensors(new_X, new_Y)
    '''
    # new_idxs.append(idxs)
    x, y = to_numpy_tensors(dataX, dataY)
    # print(new_idxs)
    # print(len(new_idxs))
    # print(np.array(new_idxs))
    # print(len(np.array(new_idxs[0])))
    # return x, y, np.array(new_idxs)
    return x, y, idxs


def timestamped_to_vector(data, classification_col=1):
    """Return tuple of inputs and outputs)
    Removes timestamp - not desirable if irregular inputs
    """
    x = []
    y = []
    temp_inputs = []
    temp_outputs = []
    #malware = []
    #benign = []
    #malware_value = []
    #benign_value = []
    for input_row in data:
        keep = np.array([x for x in range(len(input_row)) if x not in [classification_col]])
        # print(keep)

        x.append(input_row[keep])  # Advanced indexing. Only select/keep elements at indexes 0 to 54. columns(0 - 54). 55 is left out since it is malware_col
        assert int(input_row[classification_col]) in [0, 1]
        y.append([int(input_row[classification_col])])

       # assert int(input_row[classification_col]) in [0, 1]
        # if int(input_row[classification_col]) == 0:
        #    benign.append(input_row[keep])
        # if int(input_row[classification_col]) not in benign_value:
        #    benign_value.append(0)
    # else:
    #    malware.append(input_row[keep])
    #    if int(input_row[classification_col]) not in malware_value:
    #        malware_value.append(1)
    '''
    if (temp_inputs != [] and temp_outputs != []):
        x.append(temp_inputs)
        y.append(temp_outputs)
        temp_inputs = []
        temp_outputs = []
    '''
    # if benign != [] and malware != []:
    #   x.append(benign)
    #  x.append(malware)
    # y.append(benign_value)
    # y.append(malware_value)

    x = np.asarray(x)  # convert existing python sequence (list, list of tuples etc) into ndarray
    y = np.asarray(y)
    return x, y  # return tuple


def array_to_list(arr):
    try:
        return arr.tolist()
    except AttributeError:
        return arr


def into_sliding_chunk_arrays(data_x, chunk_size):
    """return array of data cut into chunks of size chunk_size sliding along array
    and array of tuples denoting starting index and end index for chunk"""
    big_X = []
    data_temp = array_to_list(data_x)
    idx_len_tuples = list(range(len(data_temp[0]) - chunk_size + 1)) if ids == [] else ids

    for i in list(range(len(data_temp[0]) - chunk_size + 1)):
        x = []
        for s, sequence in enumerate(data_temp):
            x.append(sequence[i:i + chunk_size])
        big_X.append(np.array(x))

    return big_X, idx_len_tuples


def sliding_window_data(data_x, chunk_size):
    """return array of data cut into chunks of size chunk_size sliding along array
    and array of tuples denoting starting index and end index for chunk"""
    big_X = []
    data_temp = array_to_list(data_x)
    idx_len_tuples = list(range(len(data_temp[0]) - chunk_size + 1)) if ids == [] else ids

    for i in list(range(len(data_temp[0]) - chunk_size + 1)):
        x = []
        for s, sequence in enumerate(data_temp):
            x.append(sequence[i:i + chunk_size])
        big_X.append(np.array(x))

    return big_X, idx_len_tuples


def to_chunks(l, num_chunks):
    last_one = 0
    chunks = []
    n = len(l) // num_chunks

    # Most samples spread out
    for i in range(0, n * num_chunks, n):
        chunks.append(l[i:i + n])
    # Extras distributed evenly:
    for i in range(n * num_chunks, len(l)):
        chunks[last_one].append(l[i])
        last_one = (last_one + 1) % num_chunks
    return chunks


def merge_two_dicts(x, y):
    """Given two dicts, merge them into a new dict as a shallow copy. Credit: http://stackoverflow.com/questions/38987/how-to-merge-two-python-dictionaries-in-a-single-expression"""
    z = x.copy()
    z.update(y)
    return z


class ResetStatesCallback(callbacks.Callback):
    def __init__(self):
        self.current_epoch = 0

    def on_batch_begin(self, batch, logs={}):
        self.model.reset_states()


def extract_val_set_binary(input_data, targets, val_percentage):
    change_every = 1 / val_percentage  # add to val indicies every 1/percentage
    pos = 0
    neg = 0
    classes = [pos, neg]
    train_long_tensor = []
    val_long_tensor = []
    for t, target in enumerate(targets):
        int_targ = int(target[0])
        classes[int_targ] += 1
        if classes[int_targ] % change_every == 0:
            val_long_tensor.append(t)
        else:
            train_long_tensor.append(t)

    # return reduced training set and validation set
    train_data = input_data[train_long_tensor]
    train_targets = targets[train_long_tensor]
    val_data = input_data[val_long_tensor]
    val_targets = targets[val_long_tensor]
    return train_data, train_targets, val_data, val_targets


def extract_neg(input_data, targets, val_percentage):
    change_every = 1 / val_percentage  # add to val indicies every 1/percentage
    pos = []
    neg = []
    classes = [pos, neg]
    train_long_tensor = []
    val_long_tensor = []
    for t, target in enumerate(targets):
        int_targ = int(target[0])
        classes[int_targ].append(t)

    pos = pos[-len(neg):]
    train_long_tensor = pos + neg

    # return reduced training set and validation set
    train_data = input_data[train_long_tensor]
    train_targets = targets[train_long_tensor]
    return train_data, train_targets
