from pathlib import Path 
import yaml
from datetime import datetime

from pygarment.data_config import Properties

class PathCofig:
    """Routines for getting paths to various relevant objects with standard names"""
    def __init__(self, 
                 in_element_path, out_path, in_name, out_name=None, 
                 body_name='', samples_name='', default_body=True,
                 smpl_body=False,
                 add_timestamp=False):
        """Specify 
            * in_element_path
            * our_path -- dataset level output path
            * body_name -- specify to indicate use of default bodies 
            * samples_name -- specify to indicate use of body sampling (reading body name from measurments file)
        """

        self._system = Properties('./system.json')  # TODOlOW More stable path?
        self._body_name = body_name
        self._samples_folder_name = samples_name
        self._use_default_body = default_body
        self.use_smpl_seg = smpl_body

        # Tags
        if out_name is None:
            out_name = in_name
        self.in_tag = in_name
        self.out_folder_tag = f'{out_name}_{datetime.now().strftime("%y%m%d-%H-%M-%S")}' if add_timestamp else out_name
        self.sim_tag = out_name 
        self.boxmesh_tag = out_name

        # Base paths
        self.input = Path(in_element_path)
        self.out = out_path
        self.out_el = Path(out_path) / self.out_folder_tag
        self.out_el.mkdir(parents=True, exist_ok=True)
        
        # Individual file paths
        self._update_in_paths()
        self._update_boxmesh_paths()
        self.update_in_copies_paths()
        self.update_sim_paths()
    
    def _update_in_paths(self):

        # Base path
        if not self._samples_folder_name or self._use_default_body:
            self.bodies_path = Path(self._system['bodies_default_path'])   
        else:
            self.bodies_path = Path(self._system['body_samples_path']) / self._samples_folder_name / 'meshes'

        # Body measurements
        if not self._samples_folder_name:
            self.in_body_mes = self.bodies_path / f'{self._body_name}.yaml'
        else:
            self.in_body_mes = self.input / 'body_measurements.yaml'
        
        with open(self.in_body_mes, 'r') as file:
            body_dict = yaml.load(file, Loader=yaml.SafeLoader)
        if 'body_sample' in body_dict['body']:   # Not present in default measurements
            self._body_name = body_dict['body']['body_sample']

        self.in_body_obj = self.bodies_path / f'{self._body_name}.obj'
        self.in_g_spec = self.input / f'{self.in_tag}_specification.json'
        self.body_seg = Path(self._system['bodies_default_path']) / ('ggg_body_segmentation.json' if not self.use_smpl_seg else 'smpl_vert_segmentation.json')
        self.in_design_params = self.input / 'design_params.yaml'

    def _update_boxmesh_paths(self):

        self.g_box_mesh = self.out_el / f'{self.boxmesh_tag}_boxmesh.obj'
        self.g_box_mesh_compressed = self.out_el / f'{self.boxmesh_tag}_boxmesh.ply'
        self.g_mesh_segmentation = self.out_el / f'{self.boxmesh_tag}_sim_segmentation.txt'
        self.g_orig_edge_len = self.out_el / f'{self.boxmesh_tag}_orig_lens.pickle'
        self.g_vert_labels = self.out_el / f'{self.boxmesh_tag}_vertex_labels.yaml'
        self.g_texture_fabric = self.out_el / f'{self.boxmesh_tag}_texture_fabric.png'
        self.g_texture = self.out_el / f'{self.boxmesh_tag}_texture.png'
        self.g_mtl = self.out_el / f'{self.boxmesh_tag}_material.mtl'
        
    def update_in_copies_paths(self):
        self.g_specs = self.out_el / f'{self.in_tag}_specification.json'
        self.element_sim_props = self.out_el / 'sim_props.yaml'
        self.body_mes = self.out_el / f'{self.in_tag}_body_measurements.yaml'
        self.design_params = self.out_el / f'{self.in_tag}_design_params.yaml'
        
    def update_sim_paths(self):
        self.g_sim = self.out_el / f'{self.sim_tag}_sim.obj'
        self.g_sim_glb = self.out_el / f'{self.sim_tag}_sim.glb'
        self.g_sim_compressed = self.out_el / f'{self.sim_tag}_sim.ply'
        self.usd = self.out_el / f'{self.sim_tag}_simulation.usd'


    def render_path(self, camera_name=''):
        
        fname = f'{self.sim_tag}_render_{camera_name}.png' if camera_name else f'{self.sim_tag}_render.png'
        return self.out_el / fname
        

class SimConfig:
    def __init__(self, sim_props):
        # ---- Paths ----
        # Sim props sections
        self.props = sim_props
        sim_props_option = sim_props['options']
        sim_props_material = sim_props['material']

        # Basic setup
        self.sim_fps = 60.0
        self.sim_substeps = 10 #increase?
        self.sim_wo_gravity_percentage = 0
        self.zero_gravity_steps = self.get_sim_props_value(sim_props, 'zero_gravity_steps', 5)
        self.resolution_scale = self.get_sim_props_value(sim_props, 'resolution_scale', 1.0)
        self.ground = self.get_sim_props_value(sim_props, 'ground', True)

        # Stopping criteria 
        self.static_threshold = self.get_sim_props_value(sim_props, 'static_threshold', 0.01)
        self.max_sim_steps = self.get_sim_props_value(sim_props, 'max_sim_steps', 1000)
        self.max_frame_time = int(self.get_sim_props_value(sim_props, 'max_frame_time', 15))
        self.max_sim_time = int(self.get_sim_props_value(sim_props, 'max_sim_time', 25 * 60))
        self.non_static_percent = self.get_sim_props_value(sim_props, 'non_static_percent', 5)
        # Quality filter
        self.max_body_collisions = self.get_sim_props_value(sim_props, 'max_body_collisions', 0)
        self.max_self_collisions = self.get_sim_props_value(sim_props, 'max_self_collisions', 0)

        
        # Self-collision prevention properties
        self.enable_particle_particle_collisions = self.get_sim_props_value(
            sim_props_option,
            'enable_particle_particle_collisions', False)
        self.enable_triangle_particle_collisions = self.get_sim_props_value(
            sim_props_option,
            'enable_triangle_particle_collisions', False)
        self.enable_edge_edge_collisions = self.get_sim_props_value(
            sim_props_option,
            'enable_edge_edge_collisions', False)
        self.enable_body_collision_filters = self.get_sim_props_value(
            sim_props_option, 
            'enable_body_collision_filters', 
            False
        )

        # Attachment constraints
        self.enable_attachment_constraint = self.get_sim_props_value(
            sim_props_option, 
            'enable_attachment_constraint', 
            False
        )
        self.attachment_labels = self.get_sim_props_value(
            sim_props_option, 
            'attachment_label_names', 
            []
        )
        self.attachment_frames = self.get_sim_props_value(
            sim_props_option, 
            'attachment_frames', 
            100
        )
        self.attachment_stiffness = self.get_sim_props_value(
            sim_props_option,
            'attachment_stiffness',
            []
        )
        self.attachment_damping = self.get_sim_props_value(
            sim_props_option,
            'attachment_damping',
            []
        )
        if not self.attachment_frames or not self.attachment_labels:
            self.enable_attachment_constraint = False

        # Global damping properties
        self.global_damping_factor = self.get_sim_props_value(
            sim_props_option,'global_damping_factor', 1.) 
        self.global_damping_effective_velocity = self.get_sim_props_value(
            sim_props_option,
            'global_damping_effective_velocity', 0.0) 
        self.global_max_velocity = self.get_sim_props_value(
            sim_props_option,'global_max_velocity', 50.0)  

        # Cloth global collision resolution (reference drag) options
        self.enable_global_collision_filter = self.get_sim_props_value(
            sim_props_option, 
            'enable_global_collision_filter',
            False
        )
        self.enable_cloth_reference_drag = self.get_sim_props_value(
            sim_props_option,
            'enable_cloth_reference_drag', False)
        self.cloth_reference_margin = self.get_sim_props_value(
            sim_props_option,'cloth_reference_margin', 0.1)
        self.cloth_reference_k = self.get_sim_props_value(
            sim_props_option,'cloth_reference_k', 1.0e7)

        # Body smoothing options
        self.enable_body_smoothing = self.get_sim_props_value(
            sim_props_option,'enable_body_smoothing', True)
        self.smoothing_total_smoothing_factor = self.get_sim_props_value(
            sim_props_option,
            'smoothing_total_smoothing_factor', 1)
        self.smoothing_recover_start_frame = self.get_sim_props_value(
            sim_props_option,
            'smoothing_recover_start_frame', 0)
        self.smoothing_frame_gap_between_steps = self.get_sim_props_value(
            sim_props_option,
            'smoothing_frame_gap_between_steps', 5)
        self.smoothing_num_steps = self.get_sim_props_value(
            sim_props_option, 'smoothing_num_steps', 100)
        self.smoothing_num_steps = max(min(
            self.smoothing_num_steps, self.max_sim_steps - self.smoothing_recover_start_frame),
            0)
        if self.smoothing_num_steps == 0:
            self.enable_body_smoothing = False

        # ----- Fabric material properties ----- 
        # Bending 
        self.garment_edge_ke = self.get_sim_props_value(
            sim_props_material,'garment_edge_ke', 50000.0) #default = 100.0
        self.garment_edge_kd = self.get_sim_props_value(
            sim_props_material,'garment_edge_kd',10.0) #default = 0.0
        
        # Area preservation
        self.garment_tri_ke = self.get_sim_props_value(
            sim_props_material,'garment_tri_ke', 10000.0) #default = 100.0, small number = more elasticity
        self.garment_tri_kd = self.get_sim_props_value(
            sim_props_material,'garment_tri_kd', 1.0) #default = 10.0
        self.garment_tri_ka = self.get_sim_props_value(
            sim_props_material, 'garment_tri_ka', 10000.0)  # default = 100.0
        self.garment_tri_drag = 0.0  # default = 0.0
        self.garment_tri_lift = 0.0 #default = 0.0

        # Thickness
        self.garment_density = self.get_sim_props_value(
            sim_props_material,'fabric_density', 1.0)  
        self.garment_radius = self.get_sim_props_value(
            sim_props_material,'fabric_thickness', 0.1)  

        # Spring properties (Distance constraints)
        self.spring_ke = self.get_sim_props_value(
            sim_props_material,'spring_ke', 50000)
        self.spring_kd = self.get_sim_props_value(
            sim_props_material,'spring_kd', 10.0)

        # Soft contact properties (contact between cloth and body)
        self.soft_contact_margin = 0.2 
        self.soft_contact_ke = 1000.0 
        self.soft_contact_kd = 10.0 
        self.soft_contact_kf = 1000.0 
        self.soft_contact_mu = self.get_sim_props_value(
            sim_props_material, 'fabric_friction', 0.5
        )

        # Body material
        self.body_thickness = self.get_sim_props_value(sim_props_option,'body_collision_thickness', 0.0)
        self.body_friction = self.get_sim_props_value(sim_props_option,'body_friction', 0.5)

        # particle properties  
        # Some default values -- not used in cloth sim
        self.particle_ke = 1.0e3 
        self.particle_kd = 1.0e2 
        self.particle_kf = 100.0  
        self.particle_mu = 0.5 
        self.particle_cohesion = 0.0 
        self.particle_adhesion = 0.0 

        # After the initialization
        self.update_min_steps()

    def update_min_steps(self):
        self.min_sim_steps = 0
        if self.enable_body_smoothing: 
            self.min_sim_steps = self.smoothing_recover_start_frame + self.smoothing_num_steps
        if self.enable_attachment_constraint:
            # NOTE: Adding a small number of frames 
            # to allow clothing movement to restart after attachment is released
            self.min_sim_steps = max(self.min_sim_steps, self.attachment_frames + 5)

    def get_sim_props_value(self, sim_props, name, default_value):
        if name in sim_props:
            return sim_props[name]
        return default_value

