import argparse
import datetime
import json
import numpy as np
import os
import time
import random
from pathlib import Path

import torch
import torch.nn as nn
import torch.backends.cudnn as cudnn
import torch.nn.functional as F
from torch.utils.tensorboard import SummaryWriter

import timm
from tqdm import *

# assert timm.__version__ == "0.3.2" # version check
from timm.models.layers import trunc_normal_
from timm.data.mixup import Mixup
from timm.loss import LabelSmoothingCrossEntropy, SoftTargetCrossEntropy

import util.lr_decay as lrd
import util.misc as misc
from util.datasets import build_taskonomy
from util.dataset_taskonomy import TaskonomyDataset_test
from util.pos_embed import interpolate_pos_embed
from util.misc import NativeScalerWithGradNormCount as NativeScaler
from util.build_optimizer import param_groups_UniDense_ft

import models_mt

from util.AutomaticWeightedLoss import AutomaticWeightedLoss

from fvcore.nn import FlopCountAnalysis, flop_count_str
from collections import OrderedDict
from util.metric import eval_dense


def get_args_parser():
    parser = argparse.ArgumentParser('MAE fine-tuning for image classification', add_help=False)

    # path
    parser.add_argument('--data_dir', default='./data',
                        help='path where to save dataset')
    parser.add_argument('--output_dir', default='./work_dirs',
                        help='path where to save, empty for no saving')
    parser.add_argument('--log_dir', default='./log_dir',
                        help='path where to tensorboard log')
    parser.add_argument('--pathdir_model', default = None, type = str,
                        help = 'path of directory to save the models.')
    parser.add_argument('--path_ckpt_pretrain', default = None, type = str,
                        help = 'path to save the multi-task pre-trained model.')

    # Model parameters
    parser.add_argument('--model', default='vit_base_patch16', type=str, metavar='MODEL',
                        help='Name of model to train')
    parser.add_argument('--input_size', default=224, type=int,
                        help='images input size')
    parser.add_argument('--drop_path', type=float, default=0.1, metavar='PCT',
                        help='Drop path rate (default: 0.1)')
    parser.add_argument('--replica_factor', type = int, default = 3,
                        help = 'replication factor for the MoE backbone.')

    # Optimizer parameters
    parser.add_argument('--test_episodes', default = 1000, type = int,
                        help = 'total number of episodes for testing.')
    parser.add_argument('--shot_support', default = 10, type = int,
                        help = 'shot number for support set.')
    parser.add_argument('--shot_query', default = 10, type = int,
                        help = 'shot number for query set.')
    parser.add_argument('--steps_FT', default = 30, type = int,
                        help = 'steps for inner fine-tuning.')
    parser.add_argument('--batch_size', default=2, type=int,
                        help='Batch size per GPU (effective batch size is batch_size * accum_iter * # gpus')
    parser.add_argument('--epochs', default=50, type=int)
    parser.add_argument('--accum_iter', default=1, type=int,
                        help='Accumulate gradient iterations (for increasing the effective batch size under memory constraints)')
    parser.add_argument('--clip_grad', type=float, default=None, metavar='NORM',
                        help='Clip gradient norm (default: None, no clipping)')
    parser.add_argument('--weight_decay', type=float, default=0.05,
                        help='weight decay (default: 0.05)')

    parser.add_argument('--lr', type=float, default=None, metavar='LR',
                        help='learning rate (absolute lr)')
    parser.add_argument('--blr', type=float, default=1e-3, metavar='LR',
                        help='base learning rate: absolute_lr = base_lr * total_batch_size / 256')
    parser.add_argument('--layer_decay', type=float, default=1.0,
                        help='layer-wise lr decay from ELECTRA/BEiT')

    parser.add_argument('--min_lr', type=float, default=1e-6, metavar='LR',
                        help='lower lr bound for cyclic schedulers that hit 0')

    parser.add_argument('--warmup_epochs', type=int, default=5, metavar='N',
                        help='epochs to warmup LR')

    # Augmentation parameters
    parser.add_argument('--color_jitter', type=float, default=None, metavar='PCT',
                        help='Color jitter factor (enabled only when not using Auto/RandAug)')
    parser.add_argument('--aa', type=str, default='rand-m9-mstd0.5-inc1', metavar='NAME',
                        help='Use AutoAugment policy. "v0" or "original". " + "(default: rand-m9-mstd0.5-inc1)'),
    parser.add_argument('--smoothing', type=float, default=0.1,
                        help='Label smoothing (default: 0.1)')

    # * Random Erase params
    parser.add_argument('--reprob', type=float, default=0.25, metavar='PCT',
                        help='Random erase prob (default: 0.25)')
    parser.add_argument('--remode', type=str, default='pixel',
                        help='Random erase mode (default: "pixel")')
    parser.add_argument('--recount', type=int, default=1,
                        help='Random erase count (default: 1)')
    parser.add_argument('--resplit', action='store_true', default=False,
                        help='Do not random erase first (clean) augmentation split')

    # * Mixup params
    parser.add_argument('--mixup', type=float, default=0,
                        help='mixup alpha, mixup enabled if > 0.')
    parser.add_argument('--cutmix', type=float, default=0,
                        help='cutmix alpha, cutmix enabled if > 0.')
    parser.add_argument('--cutmix_minmax', type=float, nargs='+', default=None,
                        help='cutmix min/max ratio, overrides alpha and enables cutmix if set (default: None)')
    parser.add_argument('--mixup_prob', type=float, default=1.0,
                        help='Probability of performing mixup or cutmix when either/both is enabled')
    parser.add_argument('--mixup_switch_prob', type=float, default=0.5,
                        help='Probability of switching to cutmix when both mixup and cutmix enabled')
    parser.add_argument('--mixup_mode', type=str, default='batch',
                        help='How to apply mixup/cutmix params. Per "batch", "pair", or "elem"')

    # * Finetuning params
    parser.add_argument('--finetune', default='',
                        help='finetune from checkpoint')
    parser.add_argument('--global_pool', action='store_true')
    parser.set_defaults(global_pool=True)
    parser.add_argument('--cls_token', action='store_false', dest='global_pool',
                        help='Use class token instead of global pool for classification')

    # Dataset parameters
    parser.add_argument('--task_fold', type = int, choices = [1,2,3,4,5], 
                        help = 'task fold')
    parser.add_argument('--nb_classes', default=1000, type=int,
                        help='number of the classification types')
    parser.add_argument('--device', default='cuda',
                        help='device to use for training / testing')
    parser.add_argument('--seed', default=0, type=int)
    parser.add_argument('--resume', default='',
                        help='resume from checkpoint')

    parser.add_argument('--start_epoch', default=0, type=int, metavar='N',
                        help='start epoch')
    parser.add_argument('--eval', action='store_true',
                        help='Perform evaluation only')
    parser.add_argument('--dist_eval', action='store_true', default=False,
                        help='Enabling distributed evaluation (recommended during training for faster monitor')
    parser.add_argument('--num_workers', default=6, type=int)
    parser.add_argument('--pin_mem', action='store_true',
                        help='Pin CPU memory in DataLoader for more efficient (sometimes) transfer to GPU.')
    parser.add_argument('--no_pin_mem', action='store_false', dest='pin_mem')
    parser.set_defaults(pin_mem=True)

    # distributed training parameters
    parser.add_argument('--world_size', default=1, type=int,
                        help='number of distributed processes')
    parser.add_argument('--local_rank', default=-1, type=int)
    parser.add_argument('--dist_on_itp', action='store_true')
    parser.add_argument('--dist_url', default='env://',
                        help='url used to set up distributed training')

    parser.add_argument("--exp-name", type=str, required=True, help="Name for experiment run (used for logging)")

    parser.add_argument('--times', default=1, type=int,
                        help='number of distributed processes')
    parser.add_argument('--tasks', default=14, type=int,
                        help='number of tasks')

    parser.add_argument('--eval_all', action='store_true')
    parser.add_argument('--cycle', action='store_true')
    parser.add_argument('--only_gate', action='store_true')
    parser.add_argument('--dynamic_lr', action='store_true')

    parser.add_argument('--visualize', action='store_true')
    parser.add_argument('--the_task', type=str, default='',
                        help='The only one task')
    parser.add_argument('--visualizeimg', action='store_true')

    parser.add_argument('--scaleup', action='store_true')
    parser.set_defaults(scaleup=False)

    parser.set_defaults(only_gate=False)
    parser.set_defaults(cycle=False)
    parser.set_defaults(eval_all=False)
    parser.set_defaults(dynamic_lr=False)
    parser.set_defaults(visualizeimg=False)

    return parser


class Loss_L1(nn.Module):
    def __init__(self):
        super().__init__()

    def forward(self, pred, target):
        pred = pred.squeeze()
        target = target.squeeze()

        loss = F.l1_loss(pred, target, reduction='none').mean()

        return loss


class Loss_SS(nn.Module):
    def __init__(self):
        super().__init__()
        self.loss_CE = nn.CrossEntropyLoss()

    def forward(self, pred, target):
        target = target.squeeze(1)
        loss = self.loss_CE(pred, target.to(torch.long))
        
        return loss


def get_loss(outputs, targets, task):
    if (task == 'segment_semantic'):
        criterion_d = Loss_SS()
    else:
        criterion_d = Loss_L1()
    
    task_loss = criterion_d(outputs, targets)

    return task_loss

def main(args):
    args.all_tasks = ['segment_semantic', 'normal', 'depth_euclidean', 'depth_zbuffer', 'edge_texture', 'edge_occlusion', 'keypoints2d', 'keypoints3d', 'reshading', 'principal_curvature']
    args.img_types = []
    args.img_types_test = []
    for num_task in range(len(args.all_tasks)):
        if not (num_task >= (args.task_fold-1)*2 and num_task <= (args.task_fold-1)*2+1):
            args.img_types.append(args.all_tasks[num_task])
        else:
            args.img_types_test.append(args.all_tasks[num_task])
    args.img_types.append('rgb')

    # make dir
    args.output_dir = os.path.join(args.output_dir, str(args.exp_name))
    args.log_dir = os.path.join(args.log_dir, str(args.exp_name))
    Path(args.output_dir).mkdir(parents=True, exist_ok=True)
    Path(args.log_dir).mkdir(parents=True, exist_ok=True)

    # check if there is already 
    files = os.listdir(args.output_dir)
    for file in files:
        if file[:10] == 'checkpoint': #
            print('resume', os.path.join(args.output_dir, file))
            args.resume = os.path.join(args.output_dir, file)
        
    misc.init_distributed_mode(args)

    print('job dir: {}'.format(os.path.dirname(os.path.realpath(__file__))))
    print("{}".format(args).replace(', ', ',\n'))

    device = torch.device(args.device)

    # fix the seed for reproducibility
    seed = args.seed + misc.get_rank()
    torch.manual_seed(seed)
    np.random.seed(seed)

    cudnn.benchmark = True

    args.img_types_test_rgb = args.img_types_test[:]
    args.img_types_test_rgb.append('rgb')

    if args.scaleup: # use the whole taskonomy
        print('Caution:: You are scaling up!!')
        dataset_test = TaskonomyDataset_test(args.img_types_test_rgb, data_dir = args.data_dir, split='fullplus', partition='test', resize_scale=256, crop_size=224, fliplr=True)
    else: # use the medium set of taskonomy
        dataset_test = TaskonomyDataset_test(args.img_types_test_rgb, data_dir = args.data_dir, split='muleshoe', partition='test', resize_scale=256, crop_size=224, fliplr=True)

    if True:  # args.distributed:
        num_tasks = misc.get_world_size()
        global_rank = misc.get_rank()
        sampler_test = torch.utils.data.DistributedSampler(
            dataset_test, num_replicas=num_tasks, rank=global_rank, shuffle=True
        )
        print("Sampler_test = %s" % str(sampler_test))
    else:
        sampler_test = torch.utils.data.RandomSampler(dataset_test)

    if global_rank == 0 and args.log_dir is not None and not args.eval:
        os.makedirs(args.log_dir, exist_ok=True)
        log_writer = SummaryWriter(log_dir=args.log_dir)
    else:
        log_writer = None

    data_loader_test = torch.utils.data.DataLoader(
        dataset_test, 
        sampler=sampler_test,
        batch_size=args.shot_support+args.shot_query,
        num_workers=args.num_workers,
        pin_memory=False,
        drop_last=True,
    )

    if (args.model == 'UniDense'):
        print('initialize UniDense...')
        args.list_task = []
        for task in args.img_types:
            if not ('rgb' in task):
                args.list_task.append(task)

        model = models_mt.UniDense(
            replica_factor = args.replica_factor,
            list_task = args.list_task,
            args = args
        )
        print('initialize UniDense successfully!')
    else:
        model = models_mt.__dict__[args.model](
            args.img_types,
            num_classes=args.nb_classes,
            drop_path_rate=args.drop_path,
            global_pool=args.global_pool,
        )

    # load pre-trained model
    checkpoint = torch.load(args.path_ckpt_pretrain, map_location='cpu')

    print("Load pre-trained checkpoint from: %s" % args.path_ckpt_pretrain)
    checkpoint_model = checkpoint['model']

    model.load_state_dict(checkpoint_model, strict=False)

    model.to(device)

    model_without_ddp = model
    n_parameters = sum(p.numel() for p in model.parameters() if p.requires_grad)

    eff_batch_size = args.batch_size * args.accum_iter * misc.get_world_size()
    
    if args.lr is None:  # only base_lr is specified
        args.lr = args.blr * eff_batch_size / 16

    if misc.is_main_process():
        print("Model = %s" % str(model_without_ddp))
        print('model_name:', args.model)
        
        if model_without_ddp.moe_type == 'FLOP' or model_without_ddp.ismoe == False:
            t_mg_types = [type_ for type_ in args.img_types if type_ != 'rgb']
            flops = FlopCountAnalysis(model, (torch.randn(1,3,224,224).to(device), 'normal', True))
            print('Model total flops: ', flops.total()/1000000000, 'G ', t_mg_types[0])

        print('number of params (M): %.2f' % (n_parameters / 1.e6))
        print("base lr: %.2e" % (args.lr * 256 / eff_batch_size))
        print("actual lr: %.2e" % args.lr)

        print("accumulate grad iterations: %d" % args.accum_iter)
        print("effective batch size: %d" % eff_batch_size)

        print('len test: ', len(dataset_test))

    args.distributed = True
    if args.distributed:
        model = torch.nn.parallel.DistributedDataParallel(model, device_ids=[args.gpu], find_unused_parameters=True)
        model_without_ddp = model.module

    # build optimizer with layer-wise lr decay (lrd)
    AWL = AutomaticWeightedLoss(args.tasks-1)
    AWL.to(device)

    if (args.model == 'UniDense'):
        param_groups = param_groups_UniDense_ft(
            model_without_ddp, 
            args.weight_decay,
            args.blr
        )
    else:
        param_groups = lrd.param_groups_lrd(
            model_without_ddp, 
            args.weight_decay,
            no_weight_decay_list=model_without_ddp.no_weight_decay(),
            layer_decay=args.layer_decay,
            AWL=AWL
        )

    optimizer = torch.optim.AdamW(param_groups, lr=args.lr, eps=1e-4)
    loss_scaler = NativeScaler()

    start_time = time.time()
    max_accuracy = 0.0

    result_metrics = {}
    for id_task in range(len(args.img_types_test)):
        task = args.img_types_test[id_task]
        result_metrics[task] = 0.0

    for id_task in range(8, 10):
        model.module.init_conv_layer(id_task)
    original_weights = OrderedDict(model.named_parameters())

    for num_episode, (data, valid_mask_all) in enumerate(data_loader_test):
        
        image_support = data['rgb'][:args.shot_support].to(device)
        image_query = data['rgb'][args.shot_support:].to(device)

        for id_task in range(len(args.img_types_test)):
            task = args.img_types_test[id_task]            

            label_support = data[task][:args.shot_support].to(device)
            label_query = data[task][args.shot_support:].to(device)

            for n_p, np_orig in zip(model.named_parameters(), original_weights.items()):
                n_p[1].data.copy_(np_orig[1])

            # fine-tune
            for step in range(args.steps_FT):
                pred_support, _ = model(image_support, 8 + id_task, image_support, label_support)

                task_loss = get_loss(pred_support, label_support, task)

                optimizer.zero_grad()

                task_loss.backward()
                optimizer.step()

            # test
            pred_query, _ = model(image_query, 8 + id_task, image_support, label_support)
            
            valid_mask = valid_mask_all[task]

            computed_result = eval_dense(pred_query, label_query, mask = valid_mask, task = task)

            if (args.dataset in ['depth_zbuffer','keypoints3d', 'keypoints2d', 'depth_euclidean', 'edge_texture', 'edge_occlusion', 'reshading', 'principal_curvature']):
                metric_name = 'rmse'
            elif (args.dataset == 'normal'):
                metric_name = 'mErr'
            elif (args.dataset == 'segment_semantic'):
                metric_name = 'mIoU'
            else:
                exit('error: unknown dataset.')

            result_metrics[task] += computed_result[metric_name]

        if (num_episode == args.test_episodes):
            break
    
    for id_task in range(len(args.img_types_test)):
        task = args.img_types_test[id_task]
        result_metrics[task] = result_metrics[task] / args.test_episodes
        print(f'{task}: {result_metrics[task]}')

    total_time = time.time() - start_time
    total_time_str = str(datetime.timedelta(seconds=int(total_time)))
    print('Testing time {}'.format(total_time_str))

if __name__ == '__main__':
    args = get_args_parser()
    args = args.parse_args()

    if args.output_dir:
        Path(args.output_dir).mkdir(parents=True, exist_ok=True)
    
    main(args)
