import os
import torch
import pickle

from MeLU import MeLU
from options import config
from model_training import training
from data_generation import generate
from evidence_candidate import selection
from model_test import test
from embedding_module import EmbeddingModule

import learn2learn as l2l
from embeddings import item, user
import random
import numpy as np
from learnToLearnTest import test
from fast_adapt import fast_adapt




# DATA GENERATION
print("DATA GENERATION PHASE")
os.environ["CUDA_DEVICE_ORDER"] = "PCI_BUS_ID"
os.environ["CUDA_VISIBLE_DEVICES"] = "1"
master_path= "/media/external_10TB/10TB/maheri/melu_data5"
if not os.path.exists("{}/".format(master_path)):
    os.mkdir("{}/".format(master_path))
    # preparing dataset. It needs about 22GB of your hard disk space.
    generate(master_path)


# TRAINING
print("TRAINING PHASE")
embedding_dim = config['embedding_dim']
fc1_in_dim = config['embedding_dim'] * 8
fc2_in_dim = config['first_fc_hidden_dim']
fc2_out_dim = config['second_fc_hidden_dim']
use_cuda = config['use_cuda']

emb = EmbeddingModule(config).cuda()

fc1 = torch.nn.Linear(fc1_in_dim, fc2_in_dim)
fc2 = torch.nn.Linear(fc2_in_dim, fc2_out_dim)
linear_out = torch.nn.Linear(fc2_out_dim, 1)
head = torch.nn.Sequential(fc1,fc2,linear_out)


# META LEARNING
print("META LEARNING PHASE")
head = l2l.algorithms.MetaSGD(head, lr=config['local_lr'])
head.cuda()

# Setup optimization
print("SETUP OPTIMIZATION PHASE")
all_parameters =  list(emb.parameters()) + list(head.parameters())
optimizer = torch.optim.Adam(all_parameters, lr=config['lr'])
# loss = torch.nn.MSELoss(reduction='mean')

# Load training dataset.
print("LOAD DATASET PHASE")
training_set_size = int(len(os.listdir("{}/warm_state".format(master_path))) / 4)
supp_xs_s = []
supp_ys_s = []
query_xs_s = []
query_ys_s = []
for idx in range(training_set_size):
    supp_xs_s.append(pickle.load(open("{}/warm_state/supp_x_{}.pkl".format(master_path, idx), "rb")))
    supp_ys_s.append(pickle.load(open("{}/warm_state/supp_y_{}.pkl".format(master_path, idx), "rb")))
    query_xs_s.append(pickle.load(open("{}/warm_state/query_x_{}.pkl".format(master_path, idx), "rb")))
    query_ys_s.append(pickle.load(open("{}/warm_state/query_y_{}.pkl".format(master_path, idx), "rb")))
total_dataset = list(zip(supp_xs_s, supp_ys_s, query_xs_s, query_ys_s))
del(supp_xs_s, supp_ys_s, query_xs_s, query_ys_s)
training_set_size = len(total_dataset)
batch_size = config['batch_size']

print("\n\n\n")
for iteration in range(config['num_epoch']):
    random.shuffle(total_dataset)
    num_batch = int(training_set_size / batch_size)
    a, b, c, d = zip(*total_dataset)

    for i in range(num_batch):
        optimizer.zero_grad()
        meta_train_error = 0.0
        meta_train_accuracy = 0.0
        meta_valid_error = 0.0
        meta_valid_accuracy = 0.0
        meta_test_error = 0.0
        meta_test_accuracy = 0.0

        print("EPOCH: ", iteration, " BATCH: ", i)
        supp_xs = list(a[batch_size * i:batch_size * (i + 1)])
        supp_ys = list(b[batch_size * i:batch_size * (i + 1)])
        query_xs = list(c[batch_size * i:batch_size * (i + 1)])
        query_ys = list(d[batch_size * i:batch_size * (i + 1)])
        batch_sz = len(supp_xs)

        for j in range(batch_size):
            supp_xs[j] = supp_xs[j].cuda()
            supp_ys[j] = supp_ys[j].cuda()
            query_xs[j] = query_xs[j].cuda()
            query_ys[j] = query_ys[j].cuda()

        for task in range(batch_sz):
            # print("EPOCH: ", iteration," BATCH: ",i, "TASK: ",task)

            # Compute meta-training loss
            learner = head.clone()
            temp_sxs = emb(supp_xs[task])
            temp_qxs = emb(query_xs[task])

            evaluation_error = fast_adapt(learner,
                                           temp_sxs,
                                           temp_qxs,
                                           supp_ys[task],
                                           query_ys[task],
                                           config['inner']
                                           )

            evaluation_error.backward()
            meta_train_error += evaluation_error.item()


        # Print some metrics
        print('Iteration', iteration)
        print('Meta Train Error', meta_train_error / batch_sz)
        # print('Meta Train Accuracy', meta_train_accuracy / batch_sz)
        # print('Meta Valid Error', meta_valid_error / batch_sz)
        # print('Meta Valid Accuracy', meta_valid_accuracy / batch_sz)
        # print('Meta Test Error', meta_test_error / batch_sz)
        # print('Meta Test Accuracy', meta_test_accuracy / batch_sz)

        # Average the accumulated gradients and optimize
        for p in all_parameters:
            p.grad.data.mul_(1.0 / batch_sz)
        optimizer.step()

        print("===============================================\n")


# save model
final_model = torch.nn.Sequential(emb,head)
torch.save(final_model.state_dict(), master_path + "/models_sgd.pkl")


# testing
print("start of test phase")
for test_state in ['warm_state', 'user_cold_state', 'item_cold_state', 'user_and_item_cold_state']:
    test_dataset = None
    test_set_size = int(len(os.listdir("{}/{}".format(master_path, test_state))) / 4)
    supp_xs_s = []
    supp_ys_s = []
    query_xs_s = []
    query_ys_s = []
    for idx in range(test_set_size):
        supp_xs_s.append(pickle.load(open("{}/{}/supp_x_{}.pkl".format(master_path, test_state, idx), "rb")))
        supp_ys_s.append(pickle.load(open("{}/{}/supp_y_{}.pkl".format(master_path, test_state, idx), "rb")))
        query_xs_s.append(pickle.load(open("{}/{}/query_x_{}.pkl".format(master_path, test_state, idx), "rb")))
        query_ys_s.append(pickle.load(open("{}/{}/query_y_{}.pkl".format(master_path, test_state, idx), "rb")))
    test_dataset = list(zip(supp_xs_s, supp_ys_s, query_xs_s, query_ys_s))
    del (supp_xs_s, supp_ys_s, query_xs_s, query_ys_s)

    print("===================== " + test_state + " =====================")
    test(emb,head, test_dataset, batch_size=config['batch_size'], num_epoch=config['num_epoch'])
    print("===================================================\n\n\n")






