import sys
import os

sys.path.insert(0, os.path.abspath("."))
pydir = os.path.dirname(__file__)

import warnings
import argparse
from pathlib import Path

import cv2
import torch
import torch.nn as nn

import numpy as np
import val
from models.common import Conv
from tqdm import tqdm

# Disable all warning
warnings.filterwarnings("ignore")
from models.yolo import DetectionModel

from utils.general import non_max_suppression, xywhn2xyxy, xywh2xyxy, scale_boxes, xyxy2xywhn

from utils.metrics import ConfusionMatrix, ap_per_class
import time
import onnxruntime

from trt_utils.trt import TrtModel
import pycuda.driver as cuda

from utils.dataloaders import LoadImagesAndLabels
from torch.utils.data import DataLoader


names = {0: 'person', 1: 'bicycle', 2: 'car', 3: 'motorcycle', 4: 'airplane', 5: 'bus', 6: 'train', 7: 'truck', 8: 'boat', 9: 'traffic light', 
         10: 'fire hydrant', 11: 'stop sign', 12: 'parking meter', 13: 'bench', 14: 'bird', 15: 'cat', 16: 'dog', 17: 'horse', 18: 'sheep', 19: 'cow', 
         20: 'elephant', 21: 'bear', 22: 'zebra', 23: 'giraffe', 24: 'backpack', 25: 'umbrella', 26: 'handbag', 27: 'tie', 28: 'suitcase', 29: 'frisbee', 
         30: 'skis', 31: 'snowboard', 32: 'sports ball', 33: 'kite', 34: 'baseball bat', 35: 'baseball glove', 36: 'skateboard', 37: 'surfboard', 38: 'tennis racket', 39: 'bottle', 
         40: 'wine glass', 41: 'cup', 42: 'fork', 43: 'knife', 44: 'spoon', 45: 'bowl', 46: 'banana', 47: 'apple', 48: 'sandwich', 49: 'orange', 50: 'broccoli', 
         51: 'carrot', 52: 'hot dog', 53: 'pizza', 54: 'donut', 55: 'cake', 56: 'chair', 57: 'couch', 58: 'potted plant', 59: 'bed', 60: 'dining table', 
         61: 'toilet', 62: 'tv', 63: 'laptop', 64: 'mouse', 65: 'remote', 66: 'keyboard', 67: 'cell phone', 68: 'microwave', 69: 'oven', 70: 'toaster', 
         71: 'sink', 72: 'refrigerator', 73: 'book', 74: 'clock', 75: 'vase', 76: 'scissors', 77: 'teddy bear', 78: 'hair drier', 79: 'toothbrush'}


def draw_boxes(model,
               image_path,
               mtype):
    device = torch.device("cuda")
    image = cv2.imread(image_path)
    w, h, c = image.shape
    
    image_input = cv2.resize(image, (640, 640))
    image_input = image_input[:, :, ::-1].transpose(2, 1, 0).astype(np.float32)[np.newaxis, ...] / 255.
    
    if mtype == 'ori' or mtype == 'qat':
        image_input = torch.from_numpy(image_input).to(device)
    
    pred = model(image_input)
    
    if mtype == "trt":
        pred = pred[-1].reshape(1, -1, 85)
    
    if mtype == "trt" or mtype == "onnx":
        preds = non_max_suppression(torch.from_numpy(pred).to(torch.device("cuda")), conf_thres=0.1, iou_thres=0.65, max_det=1000, agnostic=False)
    else:
        preds = non_max_suppression(pred, conf_thres=0.1, iou_thres=0.65, max_det=1000, agnostic=False)

    for bboxes in preds:
        for bbox in bboxes:
            bbox = xyxy2xywhn(bbox)
            bbox = xywhn2xyxy(bbox, w, h)
            bbox = bbox.cpu().numpy()
            y1,x1,y2,x2,conf,cid = bbox
            cv2.rectangle(image, (int(x1),int(y1)), (int(x2),int(y2)), (0, 255, 0), 2)
            label = f'Class: {names[int(cid)]}, Confidence: {conf:.2f}'
            cv2.putText(image, label, (int(x1), int(y1) - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1, cv2.LINE_AA)
    
    cv2.imwrite(f"{mtype}.png", image)
    
    del model
    torch.cuda.empty_cache()
    

def compute_metric(model,
                   dataloader,
                   mtype):
    
    device = torch.device("cuda")
    iouv = torch.linspace(0.5, 0.95, 10, device=device)
    niou = iouv.numel()
    stats = []
    seen = 0
    total_model_time = 0.
    
    for i in range(2):
        desc = "warmup"

        if i == 1:
            desc = "val"

        progress_bar = tqdm(dataloader, total=len(dataloader), desc=desc)
        
        for data, targets, paths, shapes in progress_bar:
            data = data.float() / 255.
            
            nb, _, height, width = data.shape
            
            if mtype == 'ori' or mtype == 'qat':
                data, targets = data.to(device), targets.to(device)
            elif mtype == 'onnx' or mtype == 'trt':
                data, targets = data.numpy(), targets.to(device)
            else:
                raise NotImplemented
            
            start_time = time.time()
            pred = model(data)
            end_time = time.time()
            
            if i == 0:
                continue
            
            total_model_time += end_time - start_time
            
            if mtype == 'trt':
                pred = torch.from_numpy(pred[-1].reshape(1, -1, 85)).to(device)
            elif mtype == "onnx":
                pred = torch.from_numpy(pred).to(device)
            else:
                pass
            
            preds = non_max_suppression(pred, conf_thres=0.001, iou_thres=0.6, max_det=300, multi_label=True, agnostic=False)
            targets[:, 2:] *= torch.tensor((width, height, width, height), device=device)
            
            for si, pred in enumerate(preds):
                seen += 1
                labels = targets[targets[:, 0] == si, 1:]
                nl, npr = labels.shape[0], pred.shape[0]  # number of labels, predictions
                path, shape = Path(paths[si]), shapes[si][0]
                correct = torch.zeros(npr, niou, dtype=torch.bool, device=device)  # init

                if npr == 0:
                    if nl:
                        stats.append((correct, *torch.zeros((2, 0), device=device), labels[:, 0]))
                    continue

                # Predictions
                predn = pred.clone()
                scale_boxes(data[si].shape[1:], predn[:, :4], shape, shapes[si][1])  # native-space pred

                # Evaluate
                if nl:
                    tbox = xywh2xyxy(labels[:, 1:5])  # target boxes
                    scale_boxes(data[si].shape[1:], tbox, shape, shapes[si][1])  # native-space labels
                    labelsn = torch.cat((labels[:, 0:1], tbox), 1).to(device)  # native-space labels
                    correct = val.process_batch(predn, labelsn, iouv)
                stats.append((correct, pred[:, 4], pred[:, 5], labels[:, 0]))  # (correct, conf, pcls, tcls)
                
    # Compute metrics
    stats = [torch.cat(x, 0).cpu().numpy() for x in zip(*stats)]  # to numpy
    if len(stats) and stats[0].any():
        tp, fp, p, r, f1, ap, ap_class = ap_per_class(*stats, names=names)
        ap50, ap = ap[:, 0], ap.mean(1)  # AP@0.5, AP@0.5:0.95
        mp, mr, map50, map = p.mean(), r.mean(), ap50.mean(), ap.mean()
    
    s = ("%22s" + "%11s" * 6) % ("Class", "Images", "P", "R", "mAP50", "mAP50-95", "Time")
    pf = "%22s" + "%11i" * 1 + "%11.3g" * 5  # print format
    print(s)
    print(pf % ("all", seen, mp, mr, map50, map, total_model_time / seen))

class ONNX:
    
    def __init__(self, 
                 onnx_path, 
                 device):
        
        sess_options = onnxruntime.SessionOptions()

        if onnxruntime.get_device() == "GPU":
            providers = ['CUDAExecutionProvider']
        else:
            providers = ['CUDAExecutionProvider', 'CPUExecutionProvider']
        
        sess_options.graph_optimization_level = onnxruntime.GraphOptimizationLevel.ORT_ENABLE_EXTENDED
        
        self.session = onnxruntime.InferenceSession(onnx_path, sess_options, providers=providers, provider_options=[{"device_id": 0}]*len(providers))

        self.input_name = self.get_input_name()
        self.output_name = self.get_output_name()
    
    def get_input_name(self):
        input_name = []
        for node in self.session.get_inputs():
            input_name.append(node.name)
        return input_name
    
    def get_output_name(self):
        output_name = []
        for node in self.session.get_outputs():
            output_name.append(node.name)
        return output_name

    def get_input_feed(self, image):
        input_feed = {}
        for name in self.input_name:
            input_feed[name] = image
        
        return input_feed

    def inference(self, img):
        input_feed = self.get_input_feed(img)
        pred = self.session.run(None, input_feed)[0]
        
        return pred


class TorchModel:
    
    def __init__(self, 
                 weight,
                 device):
        self.device = device
        self.model = self.load_model(weight)
    
    def load_model(self, weight):
        # def load_yolov5_model(model: str, weight, device) -> Model:
        if 'yolov5l' in weight:
            cfg = "models/yolov5l.yaml"
        elif 'yolov5m' in weight:
            cfg = "models/yolov5m.yaml"
        elif 'yolov5n' in weight:
            cfg = "models/yolov5n.yaml"
        elif 'yolov5s' in weight:
            cfg = "models/yolov5s.yaml"
        elif "yolov5x" in weight:
            cfg = "models/yolov5x.yaml"
        else:
            raise NotImplementedError("Only support yolov5[l, m, n, s, x]")
        
        model = DetectionModel(cfg=cfg).to(self.device)
        weight = torch.load(weight, map_location="cpu")["model"].state_dict()
        model.load_state_dict(weight,strict=False)
        for m in model.modules():
            if type(m) is nn.Upsample:
                m.recompute_scale_factor = None  # torch 1.11.0 compatibility
            elif type(m) is Conv:
                m._non_persistent_buffers_set = set()  # pytorch 1.6.0 compatibility
                
        model.float()
        model.eval()

        with torch.no_grad():
            model.fuse()
        return model

    @torch.no_grad()
    def inference(self, img):
        return self.model(img)
        

def main_eval(args):
    dataloader =  DataLoader(LoadImagesAndLabels(args.data_list,
                                                 img_size=640, augment=False, rect=False, cache_images=True, single_cls=False),
                             shuffle=False, batch_size=1, collate_fn=LoadImagesAndLabels.collate_fn)
    
    if args.mtype == "ori":
    # # ================ 原始模型 =======================        
        device = torch.device(f"cuda")
        model = TorchModel(args.weight, device).inference
    
    elif args.mtype=='qat':
        # =============== qat 模型 ==========================
        device = torch.device(f"cuda")
        model = torch.load(args.weight, map_location="cpu")['model']
        model.to(device)
    
    elif args.mtype=='onnx':
        # =============== onnx 模型 ======================
        device = torch.device(f"cuda")
        model = ONNX(args.weight, 0).inference
    
    # ================ trt 模型 ======================
    elif args.mtype=='trt':
        cuda.init()
        model = TrtModel(args.weight)
        
    compute_metric(model, dataloader, args.mtype)
    del model
    torch.cuda.empty_cache()


def main_draw(args):
    if args.mtype == "ori":
    # # ================ 原始模型 =======================        
        device = torch.device(f"cuda")
        model = TorchModel(args.weight, device).inference
    
    elif args.mtype=='qat':
        # =============== qat 模型 ==========================
        device = torch.device(f"cuda")
        model = torch.load(args.weight, map_location="cpu")['model']
        model.to(device)
    
    elif args.mtype=='onnx':
        # =============== onnx 模型 ======================
        device = torch.device(f"cuda")
        model = ONNX(args.weight, 0).inference
    
    # ================ trt 模型 ======================
    elif args.mtype=='trt':
        cuda.init()
        model = TrtModel(args.weight)
        
    draw_boxes(model, args.image_path, args.mtype)
    del model
    torch.cuda.empty_cache()
    

if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    
    subps = parser.add_subparsers(dest="cmd")
    
    eval_m = subps.add_parser("eval")
    eval_m.add_argument("--data_list", default="/home/temp/coco2017/val2017.txt", type=str, help="测试数据列表文件路径")
    eval_m.add_argument("--weight", required=True, type=str, help="权重路径")
    eval_m.add_argument("--mtype", required=True, type=str, choices=['onnx', 'qat', 'trt', 'ori'],
                        help="模型类型，ori原始模型，qat带有量化节点的模型，onnx模型，tensorrt模型.")
    
    draw = subps.add_parser("draw")
    draw.add_argument("--weight", required=True, type=str, help="输入trt权重路径")
    draw.add_argument("--image_path", required=True, type=str, help="待检测图片")
    draw.add_argument("--mtype", required=True, type=str)
    
    args = parser.parse_args()
    
    if args.cmd == "eval":
        print(args)
        main_eval(args)
    elif args.cmd == "draw":
        print(args)
        main_draw(args)
