import os
import random

import torch
import torch.distributed as dist
import torch.nn as nn

# Dynamically import BaseExp from fixed path
import importlib.util
import sys
base_exp_path = '/home/kitraining/Yolox/YOLOX-main/yolox/exp/base_exp.py'
spec = importlib.util.spec_from_file_location('base_exp', base_exp_path)
base_exp = importlib.util.module_from_spec(spec)
sys.modules['base_exp'] = base_exp
spec.loader.exec_module(base_exp)
BaseExp = base_exp.BaseExp

__all__ = ["Exp", "check_exp_value"]

class Exp(BaseExp):
    def __init__(self):
        super().__init__()
        self.seed = None
        self.data_dir = r'/home/kitraining/To_Annotate/'
        self.train_ann = 'coco_project_37_train.json'
        self.val_ann = 'coco_project_37_valid.json'
        self.test_ann = 'coco_project_37_test.json'
        self.num_classes = 80
        self.pretrained_ckpt = r'/home/kitraining/Yolox/YOLOX-main/pretrained/YOLOX-l.pth'
        self.depth = 1.00
        self.width = 1.00
        self.act = 'silu'
        self.data_num_workers = 4
        self.input_size = (640, 640)
        self.multiscale_range = 5
        self.mosaic_prob = 1.0
        self.mixup_prob = 1.0
        self.hsv_prob = 1.0
        self.flip_prob = 0.5
        self.degrees = (10.0, 10.0)
        self.translate = (0.1, 0.1)
        self.mosaic_scale = (0.1, 2)
        self.enable_mixup = True
        self.mixup_scale = (0.5, 1.5)
        self.shear = (2.0, 2.0)
        self.warmup_epochs = 5
        self.max_epoch = 300
        self.warmup_lr = 0
        self.min_lr_ratio = 0.05
        self.basic_lr_per_img = 0.01 / 64.0
        self.scheduler = 'yoloxwarmcos'
        self.no_aug_epochs = 15
        self.ema = True
        self.weight_decay = 5e-4
        self.momentum = 0.9
        self.print_interval = 10
        self.eval_interval = 10
        self.save_history_ckpt = True
        self.exp_name = os.path.split(os.path.realpath(__file__))[1].split('.')[0]
        self.test_size = (640, 640)
        self.test_conf = 0.01
        self.nmsthre = 0.65
        self.exp_name = 'custom_exp123'
        self.max_epoch = 300
        self.depth = 1
        self.width = 1
        self.activation = 'silu'
        self.warmup_epochs = 5
        self.warmup_lr = 0
        self.scheduler = 'yoloxwarmcos'
        self.no_aug_epochs = 15
        self.min_lr_ratio = 0.05
        self.ema = True
        self.weight_decay = 0.0005
        self.momentum = 0.9
        self.input_size = (640, 640)
        self.print_interval = 10
        self.eval_interval = 10
        self.save_history_ckpt = True
        self.test_size = (640, 640)
        self.test_conf = 0.01
        self.multiscale_range = 5
        self.enable_mixup = True
        self.mosaic_prob = 1
        self.mixup_prob = 1
        self.hsv_prob = 1
        self.flip_prob = 0.5
        self.degrees = (10, 10)
        self.mosaic_scale = (0.1, 2)
        self.mixup_scale = (0.5, 1.5)
        self.translate = (0.1, 0.1)
        self.shear = (2, 2)
        self.project_details_id = 37
        self.selected_model = 'YOLOX-l'
        self.transfer_learning = 'coco'

    def get_model(self):
        from yolox.models import YOLOX, YOLOPAFPN, YOLOXHead
        def init_yolo(M):
            for m in M.modules():
                if isinstance(m, nn.BatchNorm2d):
                    m.eps = 1e-3
                    m.momentum = 0.03
        if getattr(self, 'model', None) is None:
            in_channels = [256, 512, 1024]
            backbone = YOLOPAFPN(self.depth, self.width, in_channels=in_channels, act=self.act)
            head = YOLOXHead(self.num_classes, self.width, in_channels=in_channels, act=self.act)
            self.model = YOLOX(backbone, head)
        self.model.apply(init_yolo)
        self.model.head.initialize_biases(1e-2)
        self.model.train()
        return self.model

    def get_dataset(self, cache=False, cache_type='ram'):
        from yolox.data import COCODataset, TrainTransform
        return COCODataset(
            data_dir=self.data_dir,
            json_file=self.train_ann,
            img_size=self.input_size,
            preproc=TrainTransform(
                max_labels=50,
                flip_prob=self.flip_prob,
                hsv_prob=self.hsv_prob
            ),
            cache=cache,
            cache_type=cache_type,
        )

    def get_data_loader(self, batch_size, is_distributed, no_aug=False, cache_img=None):
        from yolox.data import (
            TrainTransform,
            YoloBatchSampler,
            DataLoader,
            InfiniteSampler,
            MosaicDetection,
            worker_init_reset_seed,
        )
        from yolox.utils import wait_for_the_master
        if self.dataset is None:
            with wait_for_the_master():
                assert cache_img is None, 'cache_img must be None if you did not create self.dataset before launch'
                self.dataset = self.get_dataset(cache=False, cache_type=cache_img)
        self.dataset = MosaicDetection(
            dataset=self.dataset,
            mosaic=not no_aug,
            img_size=self.input_size,
            preproc=TrainTransform(
                max_labels=120,
                flip_prob=self.flip_prob,
                hsv_prob=self.hsv_prob),
            degrees=self.degrees,
            translate=self.translate,
            mosaic_scale=self.mosaic_scale,
            mixup_scale=self.mixup_scale,
            shear=self.shear,
            enable_mixup=self.enable_mixup,
            mosaic_prob=self.mosaic_prob,
            mixup_prob=self.mixup_prob,
        )
        if is_distributed:
            batch_size = batch_size // dist.get_world_size()
        sampler = InfiniteSampler(len(self.dataset), seed=self.seed if self.seed else 0)
        batch_sampler = YoloBatchSampler(
            sampler=sampler,
            batch_size=batch_size,
            drop_last=False,
            mosaic=not no_aug,
        )
        dataloader_kwargs = {'num_workers': self.data_num_workers, 'pin_memory': True}
        dataloader_kwargs['batch_sampler'] = batch_sampler
        dataloader_kwargs['worker_init_fn'] = worker_init_reset_seed
        train_loader = DataLoader(self.dataset, **dataloader_kwargs)
        return train_loader

    def random_resize(self, data_loader, epoch, rank, is_distributed):
        tensor = torch.LongTensor(2).cuda()
        if rank == 0:
            size_factor = self.input_size[1] * 1.0 / self.input_size[0]
            if not hasattr(self, 'random_size'):
                min_size = int(self.input_size[0] / 32) - self.multiscale_range
                max_size = int(self.input_size[0] / 32) + self.multiscale_range
                self.random_size = (min_size, max_size)
            size = random.randint(*self.random_size)
            size = (int(32 * size), 32 * int(size * size_factor))
            tensor[0] = size[0]
            tensor[1] = size[1]
        if is_distributed:
            dist.barrier()
            dist.broadcast(tensor, 0)
        input_size = (tensor[0].item(), tensor[1].item())
        return input_size

    def preprocess(self, inputs, targets, tsize):
        scale_y = tsize[0] / self.input_size[0]
        scale_x = tsize[1] / self.input_size[1]
        if scale_x != 1 or scale_y != 1:
            inputs = nn.functional.interpolate(
                inputs, size=tsize, mode='bilinear', align_corners=False
            )
            targets[..., 1::2] = targets[..., 1::2] * scale_x
            targets[..., 2::2] = targets[..., 2::2] * scale_y
        return inputs, targets

    def get_optimizer(self, batch_size):
        if 'optimizer' not in self.__dict__:
            if self.warmup_epochs > 0:
                lr = self.warmup_lr
            else:
                lr = self.basic_lr_per_img * batch_size
            pg0, pg1, pg2 = [], [], []
            for k, v in self.model.named_modules():
                if hasattr(v, 'bias') and isinstance(v.bias, nn.Parameter):
                    pg2.append(v.bias)
                if isinstance(v, nn.BatchNorm2d) or 'bn' in k:
                    pg0.append(v.weight)
                elif hasattr(v, 'weight') and isinstance(v.weight, nn.Parameter):
                    pg1.append(v.weight)
            optimizer = torch.optim.SGD(
                pg0, lr=lr, momentum=self.momentum, nesterov=True
            )
            optimizer.add_param_group({'params': pg1, 'weight_decay': self.weight_decay})
            optimizer.add_param_group({'params': pg2})
            self.optimizer = optimizer
        return self.optimizer

    def get_lr_scheduler(self, lr, iters_per_epoch):
        from yolox.utils import LRScheduler
        scheduler = LRScheduler(
            self.scheduler,
            lr,
            iters_per_epoch,
            self.max_epoch,
            warmup_epochs=self.warmup_epochs,
            warmup_lr_start=self.warmup_lr,
            no_aug_epochs=self.no_aug_epochs,
            min_lr_ratio=self.min_lr_ratio,
        )
        return scheduler

    def get_eval_dataset(self, **kwargs):
        from yolox.data import COCODataset, ValTransform
        testdev = kwargs.get('testdev', False)
        legacy = kwargs.get('legacy', False)
        return COCODataset(
            data_dir=self.data_dir,
            json_file=self.val_ann if not testdev else self.test_ann,
            name='' if not testdev else 'test2017',
            img_size=self.test_size,
            preproc=ValTransform(legacy=legacy),
        )

    def get_eval_loader(self, batch_size, is_distributed, **kwargs):
        valdataset = self.get_eval_dataset(**kwargs)
        if is_distributed:
            batch_size = batch_size // dist.get_world_size()
            sampler = torch.utils.data.distributed.DistributedSampler(
                valdataset, shuffle=False
            )
        else:
            sampler = torch.utils.data.SequentialSampler(valdataset)
        dataloader_kwargs = {
            'num_workers': self.data_num_workers,
            'pin_memory': True,
            'sampler': sampler,
        }
        dataloader_kwargs['batch_size'] = batch_size
        val_loader = torch.utils.data.DataLoader(valdataset, **dataloader_kwargs)
        return val_loader

    def get_evaluator(self, batch_size, is_distributed, testdev=False, legacy=False):
        from yolox.evaluators import COCOEvaluator
        return COCOEvaluator(
            dataloader=self.get_eval_loader(batch_size, is_distributed,
                                            testdev=testdev, legacy=legacy),
            img_size=self.test_size,
            confthre=self.test_conf,
            nmsthre=self.nmsthre,
            num_classes=self.num_classes,
            testdev=testdev,
        )

    def get_trainer(self, args):
        from yolox.core import Trainer
        trainer = Trainer(self, args)
        return trainer

    def eval(self, model, evaluator, is_distributed, half=False, return_outputs=False):
        return evaluator.evaluate(model, is_distributed, half, return_outputs=return_outputs)

def check_exp_value(exp):
    h, w = exp.input_size
    assert h % 32 == 0 and w % 32 == 0, 'input size must be multiples of 32'
