from argparse import ArgumentParser, Namespace
import sys
import os

class GroupParams:
    pass

class ParamGroup:
    def __init__(self, parser: ArgumentParser, name : str, fill_none = False):
        group = parser.add_argument_group(name)
        for key, value in vars(self).items():
            shorthand = False
            if key.startswith("_"):
                shorthand = True
                key = key[1:]
            t = type(value)
            value = value if not fill_none else None 
            if shorthand:
                if t == bool:
                    group.add_argument("--" + key, ("-" + key[0:1]), default=value, action="store_true")
                else:
                    group.add_argument("--" + key, ("-" + key[0:1]), default=value, type=t)
            else:
                if t == bool:
                    group.add_argument("--" + key, default=value, action="store_true")
                else:
                    group.add_argument("--" + key, default=value, type=t)

    def extract(self, args):
        group = GroupParams()
        for arg in vars(args).items():
            if arg[0] in vars(self) or ("_" + arg[0]) in vars(self):
                setattr(group, arg[0], arg[1])
        return group
    
    def load_yaml(self, opts=None):
        if opts is None:
            return
        else:
            for key, value in opts.items():
                try:
                    setattr(self, key, value)
                except:
                    raise Exception(f'Unknown attribute {key}')

class GuidanceParams(ParamGroup):
    def __init__(self, parser, opts=None):
        self.guidance = "SD"        
        self.g_device = "cuda"

        self.model_key = None
        self.is_safe_tensor = False
        self.base_model_key = None

        self.controlnet_model_key = None

        self.perpneg =  True
        self.negative_w = -2.
        self.front_decay_factor = 2.
        self.side_decay_factor = 10.   
        
        self.vram_O = False
        self.fp16 = True
        self.hf_key = None
        self.t_range = [0.02, 0.5]     
        self.max_t_range = 0.98
        
        self.scheduler_type = 'DDIM'
        self.num_train_timesteps = None 

        self.sds = False
        self.fix_noise = False
        self.noise_seed = 0

        self.ddim_inv = False
        self.delta_t = 80
        self.delta_t_start = 100
        self.annealing_intervals = True
        self.text = ''
        self.inverse_text = ''
        self.textual_inversion_path = None
        self.LoRA_path = None
        self.controlnet_ratio = 0.5
        self.negative = ""
        self.guidance_scale = 7.5
        self.denoise_guidance_scale = 1.0
        self.lambda_guidance = 1.

        self.xs_delta_t = 200
        self.xs_inv_steps = 5
        self.xs_eta = 0.0

        # multi-batch
        self.C_batch_size = 1

        self.vis_interval = 100

        super().__init__(parser, "Guidance Model Parameters")


class ModelParams(ParamGroup): 
    def __init__(self, parser, sentinel=False, opts=None):
        self.sh_degree = 0
        self._source_path = ""
        self._model_path = ""
        self.pretrained_model_path = None
        self._images = "images"
        self.workspace = "debug"
        self.batch = 10  
        self._resolution = -1
        self._white_background = True
        self.data_device = "cuda"
        self.eval = False
        self.opt_path = None
        
        # augmentation
        self.sh_deg_aug_ratio = 0.1
        self.bg_aug_ratio = 0.5
        self.shs_aug_ratio = 0.0
        self.scale_aug_ratio = 1.0
        super().__init__(parser, "Loading Parameters", sentinel)

    def extract(self, args):
        g = super().extract(args)
        g.source_path = os.path.abspath(g.source_path)
        return g


class PipelineParams(ParamGroup):
    def __init__(self, parser, opts=None):
        self.convert_SHs_python = False
        self.compute_cov3D_python = False
        self.debug = False
        super().__init__(parser, "Pipeline Parameters")


class OptimizationParams(ParamGroup):
    def __init__(self, parser, opts=None):
        self.iterations = 5000# 10_000
        self.position_lr_init = 0.00016
        self.position_lr_final = 0.0000016
        self.position_lr_delay_mult = 0.01
        self.position_lr_max_steps = 30_000
        self.feature_lr = 0.0050
        self.feature_lr_final = 0.0030

        self.opacity_lr = 0.05
        self.scaling_lr = 0.005
        self.rotation_lr = 0.001


        self.geo_iter = 0
        self.as_latent_ratio = 0.2
        # dense

        self.resnet_lr = 1e-4
        self.resnet_lr_init = 2e-3
        self.resnet_lr_final = 5e-5


        self.scaling_lr_final = 0.001
        self.rotation_lr_final = 0.0002

        self.percent_dense = 0.003
        self.densify_grad_threshold = 0.00075

        self.lambda_tv = 1.0 # 0.1
        self.lambda_bin = 10.0
        self.lambda_scale = 1.0
        self.lambda_sat = 1.0
        self.lambda_radius = 1.0
        self.densification_interval = 100
        self.opacity_reset_interval = 300
        self.densify_from_iter = 100
        self.densify_until_iter = 30_00 
        
        self.use_control_net_iter = 10000000 
        self.warmup_iter = 1500 
        
        self.use_progressive = False
        self.save_process = True
        self.pro_frames_num = 600
        self.pro_render_45 = False
        self.progressive_view_iter = 500
        self.progressive_view_init_ratio = 0.2

        self.scale_up_cameras_iter = 500
        self.scale_up_factor = 0.95
        self.fovy_scale_up_factor = [0.75, 1.1]
        self.phi_scale_up_factor = 1.5
        super().__init__(parser, "Optimization Parameters")


class GenerateCamParams(ParamGroup):
    def __init__(self, parser):
        self.init_shape = 'sphere'
        self.init_prompt = ''      
        self.use_pointe_rgb  = False
        self.radius_range = [5.2, 5.5] #[3.8, 4.5] #[3.0, 3.5]
        self.max_radius_range = [3.5, 5.0]
        self.default_radius = 3.5
        self.theta_range = [45, 105]
        self.max_theta_range = [45, 105]
        self.phi_range = [-180, 180]
        self.max_phi_range = [-180, 180]
        self.fovy_range = [0.32, 0.60] #[0.3, 1.5] #[0.5, 0.8]  #[10, 30]
        self.max_fovy_range = [0.16, 0.60]
        self.rand_cam_gamma = 1.0
        self.angle_overhead = 30
        self.angle_front =60
        self.render_45 = True
        self.uniform_sphere_rate = 0
        self.image_w = 512
        self.image_h = 512 # 512
        self.SSAA = 1
        self.init_num_pts = 100_000
        self.default_polar = 90
        self.default_azimuth = 0
        self.default_fovy = 0.55 #20
        self.jitter_pose = True
        self.jitter_center = 0.05
        self.jitter_target = 0.05
        self.jitter_up = 0.01
        self.device = "cuda"
        super().__init__(parser, "Generate Cameras Parameters")

def get_combined_args(parser : ArgumentParser):
    cmdlne_string = sys.argv[1:]
    cfgfile_string = "Namespace()"
    args_cmdline = parser.parse_args(cmdlne_string)

    try:
        cfgfilepath = os.path.join(args_cmdline.model_path, "cfg_args")
        print("Looking for config file in", cfgfilepath)
        with open(cfgfilepath) as cfg_file:
            print("Config file found: {}".format(cfgfilepath))
            cfgfile_string = cfg_file.read()
    except TypeError:
        print("Config file not found at")
        pass
    args_cfgfile = eval(cfgfile_string)

    merged_dict = vars(args_cfgfile).copy()
    for k,v in vars(args_cmdline).items():
        if v != None:
            merged_dict[k] = v
    return Namespace(**merged_dict)
