import argparse
import torch as th
import torch.optim as optim
import torch.nn.functional as F
import torch.nn as nn

import dgl.function as fn

from utils import in_out_norm

from models import CompGCN_ConvE
from data_loader import Data
import numpy as np
from time import time


#predict the tail for (head, rel, -1) or head for (-1, rel, tail)
def predict(model, graph, device, data_iter, split='valid', mode='tail'):
    model.eval()
    with th.no_grad():
        results = {}
        train_iter = iter(data_iter['{}_{}'.format(split, mode)])
        
        for step, batch in enumerate(train_iter):
            triple, label = batch[0].to(device), batch[1].to(device)
            sub, rel, obj, label = triple[:, 0], triple[:, 1], triple[:, 2], label
            pred = model(graph, sub, rel)
            b_range = th.arange(pred.size()[0], device = device)
            target_pred = pred[b_range, obj]
            pred = th.where(label.byte(), -th.ones_like(pred) * 10000000, pred)
            pred[b_range, obj] = target_pred

            #compute metrics
            ranks = 1 + th.argsort(th.argsort(pred, dim=1, descending=True), dim =1, descending=False)[b_range, obj]
            ranks = ranks.float()
            results['count'] = th.numel(ranks) + results.get('count', 0.0)
            results['mr'] = th.sum(ranks).item() + results.get('mr', 0.0)
            results['mrr'] = th.sum(1.0/ranks).item() + results.get('mrr', 0.0)
            for k in [1,3,10]:
                results['hits@{}'.format(k)] = th.numel(ranks[ranks <= (k)]) + results.get('hits@{}'.format(k), 0.0)
            
    return results

#evaluation function, evaluate the head and tail prediction and then combine the results
def evaluate(model, graph, device, data_iter, split='valid'):
    #predict for head and tail
    left_results = predict(model, graph, device, data_iter, split, mode='tail')
    right_results = predict(model, graph, device, data_iter, split, mode='head')
    results = {}
    count = float(left_results['count'])

    #combine the head and tail prediction results
    #Metrics: MRR, MR, and Hit@k
    results['left_mr'] = round(left_results['mr']/count, 5)
    results['left_mrr'] = round(left_results['mrr']/count, 5)
    results['right_mr'] = round(right_results['mr']/count, 5)
    results['right_mrr'] = round(right_results['mrr']/count, 5)
    results['mr'] = round((left_results['mr'] + right_results['mr']) /(2*count), 5)
    results['mrr'] = round((left_results['mrr'] + right_results['mrr']) /(2*count), 5)
    for k in [1,3,10]:
        results['left_hits@{}'.format(k)] = round(left_results['hits@{}'.format(k)]/count, 5)
        results['right_hits@{}'.format(k)] = round(right_results['hits@{}'.format(k)]/count, 5)
        results['hits@{}'.format(k)] = round((left_results['hits@{}'.format(k)] + right_results['hits@{}'.format(k)])/(2*count), 5)
    return results 
    

def main(args):

    # Step 1: Prepare graph data and retrieve train/validation/test index ============================= #
    # check cuda
    if args.gpu >= 0 and th.cuda.is_available():
        device = 'cuda:{}'.format(args.gpu)
    else:
        device = 'cpu'
    
    #construct graph, split in/out edges and prepare train/validation/test data_loader
    data = Data(args.dataset, args.lbl_smooth, args.num_workers, args.batch_size)
    data_iter = data.data_iter #train/validation/test data_loader
    graph = data.g.to(device)
    num_rel = th.max(graph.edata['etype']).item() + 1

    #Compute in/out edge norms and store in edata
    graph = in_out_norm(graph)

    # Step 2: Create model =================================================================== #
    compgcn_model=CompGCN_ConvE(num_bases=args.num_bases,
                                num_rel=num_rel,
                                num_ent=graph.num_nodes(),
                                in_dim=args.init_dim,
                                layer_size=args.layer_size,
                                comp_fn=args.opn,
                                batchnorm=True,
                                dropout=args.dropout,
                                layer_dropout=args.layer_dropout,
                                num_filt=args.num_filt,
                                hid_drop=args.hid_drop,
                                feat_drop=args.feat_drop,
                                ker_sz=args.ker_sz,
                                k_w=args.k_w,
                                k_h=args.k_h
                                )
    compgcn_model = compgcn_model.to(device)

    # Step 3: Create training components ===================================================== #
    loss_fn = th.nn.BCELoss()
    optimizer = optim.Adam(compgcn_model.parameters(), lr=args.lr, weight_decay=args.l2)
    
    # Step 4: training epoches =============================================================== #
    best_mrr = 0.0
    kill_cnt = 0
    for epoch in range(args.max_epochs):
        # Training and validation using a full graph
        compgcn_model.train()
        train_loss=[]
        t0 = time()
        for step, batch in enumerate(data_iter['train']):
            triple, label = batch[0].to(device), batch[1].to(device)
            sub, rel, obj, label = triple[:, 0], triple[:, 1], triple[:, 2], label
            logits = compgcn_model(graph, sub, rel)
		
            # compute loss
            tr_loss = loss_fn(logits, label)
            train_loss.append(tr_loss)

            # backward
            optimizer.zero_grad()
            tr_loss.backward()
            optimizer.step()

        train_loss = np.sum(train_loss)

        t1 = time()  
        val_results = evaluate(compgcn_model, graph, device, data_iter, split='valid')
        t2 = time()

        #validate
        if val_results['mrr']>best_mrr:
            best_mrr = val_results['mrr']
            best_epoch = epoch
            th.save(compgcn_model.state_dict(), 'comp_link'+'_'+args.dataset)
            kill_cnt = 0
            print("saving model...")
        else:
            kill_cnt += 1
            if kill_cnt > 25:
                print('early stop.')
                break
        print("In epoch {}, Train Loss: {:.4f}, Valid MRR: {:.5}\n, Train time: {}, Valid time: {}"\
                .format(epoch, train_loss, val_results['mrr'], t1-t0, t2-t1))
    
    #test use the best model
    compgcn_model.eval()
    compgcn_model.load_state_dict(th.load('comp_link'+'_'+args.dataset))
    test_results = evaluate(compgcn_model, graph, device, data_iter, split='test')
    print("Test MRR: {:.5}\n, MR: {:.10}\n, H@10: {:.5}\n, H@3: {:.5}\n, H@1: {:.5}\n"\
            .format(test_results['mrr'], test_results['mr'], test_results['hits@10'], test_results['hits@3'], test_results['hits@1']))
    

if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='Parser For Arguments', formatter_class=argparse.ArgumentDefaultsHelpFormatter)

    parser.add_argument('--data', dest='dataset', default='FB15k-237', help='Dataset to use, default: FB15k-237')
    parser.add_argument('--model', dest='model', default='compgcn', help='Model Name')
    parser.add_argument('--score_func', dest='score_func', default='conve', help='Score Function for Link prediction')
    parser.add_argument('--opn', dest='opn', default='ccorr', help='Composition Operation to be used in CompGCN')

    parser.add_argument('--batch', dest='batch_size', default=128, type=int, help='Batch size')
    parser.add_argument('--gpu', type=int, default='0', help='Set GPU Ids : Eg: For CPU = -1, For Single GPU = 0')
    parser.add_argument('--epoch', dest='max_epochs', type=int, default=500, help='Number of epochs')
    parser.add_argument('--l2', type=float, default=0.0, help='L2 Regularization for Optimizer')
    parser.add_argument('--lr', type=float, default=0.001, help='Starting Learning Rate')
    parser.add_argument('--lbl_smooth', dest='lbl_smooth', type=float, default=0.1, help='Label Smoothing')
    parser.add_argument('--num_workers', type=int, default=10, help='Number of processes to construct batches')
    parser.add_argument('--seed', dest='seed', default=41504, type=int, help='Seed for randomization')

    parser.add_argument('--num_bases', dest='num_bases', default=-1, type=int, help='Number of basis relation vectors to use')
    parser.add_argument('--init_dim', dest='init_dim', default=100, type=int, help='Initial dimension size for entities and relations')
    parser.add_argument('--layer_size', nargs='?', default='[200]', help='List of output size for each compGCN layer')
    parser.add_argument('--gcn_drop', dest='dropout', default=0.1, type=float, help='Dropout to use in GCN Layer')
    parser.add_argument('--layer_dropout', nargs='?', default='[0.3]', help='List of dropout value after each compGCN layer')

    # ConvE specific hyperparameters
    parser.add_argument('--hid_drop', dest='hid_drop', default=0.3, type=float, help='ConvE: Hidden dropout')
    parser.add_argument('--feat_drop', dest='feat_drop', default=0.3, type=float, help='ConvE: Feature Dropout')
    parser.add_argument('--k_w', dest='k_w', default=10, type=int, help='ConvE: k_w')
    parser.add_argument('--k_h', dest='k_h', default=20, type=int, help='ConvE: k_h')
    parser.add_argument('--num_filt', dest='num_filt', default=200, type=int, help='ConvE: Number of filters in convolution')
    parser.add_argument('--ker_sz', dest='ker_sz', default=7, type=int, help='ConvE: Kernel size to use')

    args = parser.parse_args()
    
    np.random.seed(args.seed)
    th.manual_seed(args.seed)

    print(args)

    args.layer_size = eval(args.layer_size)
    args.layer_dropout = eval(args.layer_dropout)

    main(args)
    
