import numpy as np
import json
import scipy.io
import pyopencl as cl

from SimNDT.core.scenario import Scenario
from SimNDT.core.material import Material
from SimNDT.core.boundary import Boundary
from SimNDT.core.geometryObjects import Ellipse, Circle, Inclusions, Rectangle, Square
from SimNDT.core.geometryObjects import Concrete2Phase, Concrete2PhaseImmersion, Concrete3Phase, Concrete3PhaseImmersion
from SimNDT.core.inspectionMethods import Source, LinearScan, Transmission, PulseEcho, Tomography
from SimNDT.core.signal import Signals
from SimNDT.core.receivers import Receivers
from SimNDT.core.transducer import Transducer
from SimNDT.core.simulation import Simulation
import SimNDT.core.checkSimulation as CheckSim
from SimNDT.core.simPack import SimPack
import SimNDT.engine.infoCL as infoCL
from SimNDT.run_setup.snapshots import SnapShots
from SimNDT.run_setup.SimulationVideo import create_vector_field_video
from SimNDT.run_setup.engineController import EngineController
from SimNDT.run_setup import HelperMethods
from SimNDT.core.materialLibrary import getMaterialLibrary



def read_scenario(sim_params, SimNDT_geom_objects, SimNDT_Bc_objs, SimNDT_Mat_objs):
    """
    Reads and sets up the scenario based on the provided simulation parameters, geometric objects, and boundary conditions.
    
    Parameters:
    - sim_params: Dictionary containing simulation parameters.
    - SimNDT_geom_objects: List of geometric objects.
    - SimNDT_Bc_objs: List of boundary condition objects.
    
    Returns:
    - SimNDT_Scenario: The scenario object.
    """
    if "Scenario" in sim_params.keys():
        scena_width = sim_params["Scenario"]["Width"]
        scena_height = sim_params["Scenario"]["Height"]
        scena_pixel_mm = sim_params["Scenario"]["Pixel_mm"]
        scena_label = sim_params["Scenario"]["Label"]
        
        SimNDT_Scenario = Scenario(Width=scena_width, Height=scena_height, Pixel_mm=scena_pixel_mm, Label=scena_label)
        
        # adding the geometric objects to the scenario
        for geom_num in range(len(SimNDT_geom_objects)):
            SimNDT_Scenario.addObject(SimNDT_geom_objects[geom_num])   

        SimNDT_Scenario.createBoundaries(SimNDT_Bc_objs) ## TODO after addObjects?

        SimNDT_Scenario.updateScenario()           
    else:
      raise ValueError("read_scenario: No Scenario section found, but required.")
      
    print("Finished reading Scenario.")
    print(SimNDT_Scenario)
    # print("\n")
    return SimNDT_Scenario

def read_geometry_objects(sim_params):
    """
    Reads and sets up geometric objects like ellipses, circles, rectangles, squares, and inclusions from the simulation parameters.
    
    Parameters:
    - sim_params: Dictionary containing simulation parameters.
    
    Returns:
    - SimNDT_geom_objects: List of geometric objects.
    """
    # geometric objects
    # geometric objects
    SimNDT_geom_objects = []
    if "GeometricObjects" in sim_params.keys():
        print("Showing {} GeometricObjects.".format(len(sim_params["GeometricObjects"])))
        for geom_num in range(len(sim_params["GeometricObjects"])):
            geom_name = sim_params["GeometricObjects"][geom_num]["Name"]
            
            if geom_name == "ellipse":
                geom_x0 = sim_params["GeometricObjects"][geom_num]["x0"]
                geom_y0 = sim_params["GeometricObjects"][geom_num]["y0"]
                geom_a = sim_params["GeometricObjects"][geom_num]["a"]
                geom_b = sim_params["GeometricObjects"][geom_num]["b"]
                geom_theta = sim_params["GeometricObjects"][geom_num]["theta"]
                geom_label = sim_params["GeometricObjects"][geom_num]["Label"]
                
                geom_obj = Ellipse(x0=geom_x0, y0=geom_y0, a=geom_a, b=geom_b, theta=geom_theta, Label=geom_label)
                SimNDT_geom_objects.append(geom_obj)
            elif geom_name == "circle":
                geom_x0 = sim_params["GeometricObjects"][geom_num]["x0"]
                geom_y0 = sim_params["GeometricObjects"][geom_num]["y0"]
                geom_r = sim_params["GeometricObjects"][geom_num]["r"]
                geom_label = sim_params["GeometricObjects"][geom_num]["Label"]
                
                geom_obj = Circle(x0=geom_x0, y0=geom_y0, r=geom_r, Label=geom_label)
                SimNDT_geom_objects.append(geom_obj)
            elif geom_name == "rectangle":
                geom_x0 = sim_params["GeometricObjects"][geom_num]["x0"]
                geom_y0 = sim_params["GeometricObjects"][geom_num]["y0"]
                geom_w = sim_params["GeometricObjects"][geom_num]["W"]
                geom_h = sim_params["GeometricObjects"][geom_num]["H"]
                geom_theta = sim_params["GeometricObjects"][geom_num]["theta"]
                geom_label = sim_params["GeometricObjects"][geom_num]["Label"]
                
                geom_obj = Rectangle(x0=geom_x0, y0=geom_y0, W=geom_w, H=geom_h, theta=geom_theta, Label=geom_label)
                SimNDT_geom_objects.append(geom_obj)
            elif geom_name == "square":
                geom_x0 = sim_params["GeometricObjects"][geom_num]["x0"]
                geom_y0 = sim_params["GeometricObjects"][geom_num]["y0"]
                geom_l = sim_params["GeometricObjects"][geom_num]["L"]
                geom_theta = sim_params["GeometricObjects"][geom_num]["theta"]
                geom_label = sim_params["GeometricObjects"][geom_num]["Label"]
                
                geom_obj = Square(x0=geom_x0, y0=geom_y0, L=geom_l, theta=geom_theta, Label=geom_label)
                SimNDT_geom_objects.append(geom_obj)
            elif geom_name == "inclusion":
                geom_diameter = sim_params["GeometricObjects"][geom_num]["Diameter"]
                geom_fraction = sim_params["GeometricObjects"][geom_num]["Fraction"]
                geom_label = sim_params["GeometricObjects"][geom_num]["Label"]
                
                geom_obj = Inclusions(Diameter=geom_diameter, Fraction=geom_fraction, Label=geom_label)
                SimNDT_geom_objects.append(geom_obj)
            # print(geom_obj)
            # print("\n")
        print("Finished reading GeometricObjects.")
        # print(repr(SimNDT_geom_objects))
        # print("\n")
    return SimNDT_geom_objects

def read_materials(sim_params,Library):
    """
    Reads and sets up materials for the simulation. 
    
    Parameters:
    - sim_params: Dictionary containing simulation parameters.
    
    Returns:
    - SimNDT_Mat_objs: List of material objects.
    """
    # reading materials ######### 
    if "Materials" in sim_params.keys():
        print("Showing {} Materials.".format(len(sim_params["Materials"])))
        SimNDT_Mat_objs = []
        for mat_num in range(len(sim_params["Materials"])):
            mat_name = sim_params["Materials"][mat_num]["Name"]
            if "Rho" in sim_params["Materials"][mat_num]:
              mat_rho = sim_params["Materials"][mat_num]["Rho"]
              mat_vl = sim_params["Materials"][mat_num]["VL"]
              mat_vt = sim_params["Materials"][mat_num]["VT"]
            else:
              # get it from the library
              mat = Library[mat_name]
              # [rho, lam, mu, VL, VT]
              mat_rho = mat[0]
              mat_lambda = mat[1]
              mat_mu = mat[2]
              mat_vl = mat[3]
              mat_vt = mat[4]
              
            mat_label = sim_params["Materials"][mat_num]["Label"]
            if "Eta_v" in sim_params["Materials"][mat_num]:
              mat_eta_v = sim_params["Materials"][mat_num]["Eta_v"]
              mat_eta_s = sim_params["Materials"][mat_num]["Eta_s"]
              mat_damping = True
            else:
              mat_damping = False
              mat_eta_v = 1e-30
              mat_eta_s = 1e-30

            mat_lambda = mat_rho * (mat_vl**2- 2*(mat_vt**2))
            mat_mu = mat_rho * (mat_vt**2)
            mat_c11 = mat_lambda + 2 * mat_mu
            mat_c12 = mat_lambda
            mat_c22 = mat_c11
            mat_c44 = mat_mu

            mat_obj = Material(mat_name, mat_rho, mat_c11, mat_c12, mat_c22, mat_c44, mat_label, mat_damping, mat_eta_v, mat_eta_s)
            
            # print(materials["material_label_{}".format(mat_label)])
            SimNDT_Mat_objs.append(mat_obj)
            print(mat_obj)
            
            # print("\n")
            
        print("Finished reading Materials.")
        # print(repr(materials))
        # print("\n")
        return SimNDT_Mat_objs

def read_boundaries(sim_params):
    """
    Reads and sets up boundary conditions for the simulation.
    
    Parameters:
    - sim_params: Dictionary containing simulation parameters.
    
    Returns:
    - SimNDT_Bc_objs: List of boundary condition objects.
    """
        # boundary conditions
    if "Boundaries" in sim_params.keys():    
        print("Reading {} Boundaries.".format(len(sim_params["Boundaries"])))
        SimNDT_Bc_objs = []
        for bc_num in range(len(sim_params["Boundaries"])):
            bc_name = sim_params["Boundaries"][bc_num]["Name"]
            bc_type = sim_params["Boundaries"][bc_num]["Type"]
            if bc_type == "Absorbing":
              bc_size = sim_params["Boundaries"][bc_num]["Size"]
              bc_type_num = 0
            elif bc_type == "Air":
              bc_size = 0
              bc_type_num = 1
            else:
              raise ValueError("Boundary type "+bc_type+" is invalid [Expecting Absorbing or Air].")
            bc_obj = Boundary(bc_name, bc_type_num, bc_size)
            SimNDT_Bc_objs.append(bc_obj)
            # print(bc_obj)
            # print("\n")
        print("Finished reading Boundaries.")
        # print(repr(bc))
        # print("\n")
        return SimNDT_Bc_objs

def read_transducers(sim_params):
    """
    Reads and sets up transducers for the simulation, including properties like size, location, and field.
    
    Parameters:
    - sim_params: Dictionary containing simulation parameters.
    
    Returns:
    - SimNDT_Transd_objs: List of transducer objects.
    """
        # transducers 
    if "Transducers" in sim_params.keys():
        SimNDT_Transd_objs = []
        trans_name = sim_params["Transducers"]["Name"]
        trans_size = sim_params["Transducers"]["Size"]
        trans_center_offset = sim_params["Transducers"]["CenterOffset"]
        trans_border_offset = sim_params["Transducers"]["BorderOffset"]
        trans_size_pixel = sim_params["Transducers"]["SizePixel"]
        trans_location = sim_params["Transducers"]["Location"]
        trans_point_source = sim_params["Transducers"]["PointSource"]
        trans_window = sim_params["Transducers"]["Window"]
        trans_field = sim_params["Transducers"]["Field"]
        trans_pzt = sim_params["Transducers"]["PZT"]

        
        SimNDT_Transducers = Transducer(
                                        name=trans_name,
                                        Size=trans_size,
                                        CenterOffset=trans_center_offset,
                                        BorderOffset=trans_border_offset,
                                        Location=trans_location,
                                        PointSource=trans_point_source,
                                        EnableWindow=trans_window,
                                        Field=trans_field,
                                        PZT=trans_pzt
                                        )
        SimNDT_Transd_objs.append(SimNDT_Transducers)
        print("Finished reading Transducers.")
        print(repr(SimNDT_Transd_objs))
        # print("\n")
        return SimNDT_Transd_objs

def check_transducer_size(Scenario, Transducer):
    """
    Checks if the transducer size is appropriate for the given scenario and raises errors if there are issues.
    
    Parameters:
    - Scenario: The scenario object.
    - Transducer: List of transducer objects.
    
    Raises:
    - ValueError: If the transducer size is inappropriate.
    """
    for trans_num in range(len(Transducer)):
        trans = Transducer[trans_num]
        
        if trans.Size >= Scenario.Width:
            raise ValueError("Transducer is larger than Scenario Width!!!!")
                
        elif trans.Size <= 0 and not trans.PointSource:
            raise ValueError("Incorrect Transducer Size!!!!")
            
        if (np.abs(trans.CenterOffset) + trans.Size / 2.0 >= Scenario.Width / 2.0):
            raise ValueError("Transducer is out of Scenario!!!!")
        
        if trans.BorderOffset < 0 or trans.BorderOffset >= Scenario.Height:
            raise ValueError("Transducer is out of Scenario!!!!")
    
def read_inspection(sim_params):
    """
    Reads and sets up the inspection method for the simulation, such as Transmission, PulseEcho, LinearScan, or Tomography.
    
    Parameters:
    - sim_params: Dictionary containing simulation parameters.
    
    Returns:
    - SimNDT_Inspection: The inspection object.
    """
        # setting up the inspection method
    
    if "Inspection" in sim_params.keys():
        
        insp_name = sim_params["Inspection"]["Name"]
        if insp_name == "Transmission":
            insp_location = sim_params["Inspection"]["Location"]
            
            SimNDT_Inspection = Transmission(Location=insp_location)
        
        elif insp_name == "PulseEcho":
            insp_location = sim_params["Inspection"]["Location"]
            
            SimNDT_Inspection = PulseEcho(Location=insp_location)
        
        elif insp_name == "LinearScan":
            insp_ini = sim_params["Inspection"]["ini"]
            insp_end = sim_params["Inspection"]["end"]
            insp_step = sim_params["Inspection"]["step"]
            insp_location = sim_params["Inspection"]["Location"]
            insp_method = sim_params["Inspection"]["Method"]
            insp_theta = sim_params["Inspection"]["Theta"]
            
            SimNDT_Inspection = LinearScan(ini=insp_ini, end=insp_end, step=insp_step, Location=insp_location, Method=insp_method, Theta=insp_theta)
        
        elif insp_name == "Tomography":
            insp_projection_step = sim_params["Inspection"]["ProjectionStep"]
            insp_diameter_ring = sim_params["Inspection"]["DiameterRing"]
            insp_one_projection = sim_params["Inspection"]["OneProjection"]
            
            SimNDT_Inspection = Tomography(ProjectionStep=insp_projection_step, DiameterRing=insp_diameter_ring, OneProjection=insp_one_projection)
        print("Finished reading Inspection.")
        # print(repr(SimNDT_Inspection))
        # print("\n")
        return SimNDT_Inspection
    
def read_source(sim_params):
    """
    Reads and sets up the source for the simulation, including properties like longitudinal and shear wave sources.
    
    Parameters:
    - sim_params: Dictionary containing simulation parameters.
    
    Returns:
    - SimNDT_Source: The source object.
    """
        # setting up the source
    
    if "Source" in sim_params.keys():
        source_longitudinal = sim_params["Source"]["Longitudinal"]
        source_shear = sim_params["Source"]["Shear"]
        source_pressure = sim_params["Source"]["Pressure"]
        source_displacement = sim_params["Source"]["Displacement"]
        source_hide_receiver = 0
        
        # check if Source attributes should be changed by passins as arguments
        SimNDT_Source = Source()
        print("Finished reading Source section.")
        # print(repr(SimNDT_Source))
        # print("\n")
        return SimNDT_Source

def read_signal(sim_params):
    """
    Reads and sets up the signal for the simulation, including properties like amplitude, frequency, and number of cycles.
    
    Parameters:
    - sim_params: Dictionary containing simulation parameters.
    
    Returns:
    - SimNDT_Signal: The signal object.
    """
        # setting up the signal
      
    if "Signal" in sim_params.keys():
        signal_name = sim_params["Signal"]["Name"]
        signal_amplitude = sim_params["Signal"]["Amplitude"]
        signal_frequency = sim_params["Signal"]["Frequency"]
        signal_n_cycles = sim_params["Signal"]["N_Cycles"]
        
        SimNDT_Signal = Signals(Name=signal_name, Amplitud=signal_amplitude, Frequency=signal_frequency, N_Cycles=signal_n_cycles)  
        print("Finished reading Signal section.")
        # print(repr(SimNDT_Signal))
        # print("\n")
        return SimNDT_Signal
    
def read_simulation(sim_params):
    """
    Reads and sets up the simulation parameters, including time scale, 
            maximum frequency, and simulation time.
    
    Parameters:
    - sim_params: Dictionary containing simulation parameters.
    
    Returns:
    - SimNDT_Simulation: The simulation object.
    """
        # setting up the simulation
    
    if "Simulation" in sim_params.keys():
        sim_time_scale = sim_params["Simulation"]["TimeScale"]
        sim_max_freq = sim_params["Simulation"]["MaxFreq"]
        sim_point_cycle = sim_params["Simulation"]["PointCycle"]
        sim_simulation_time = sim_params["Simulation"]["SimulationTime"]
        sim_order = sim_params["Simulation"]["Order"]
        # sim_device = sim_params["Simulation"]["Device"]
        
        if "Device" in sim_params["Simulation"]:
          sim_device=sim_params["Simulation"]["Device"]
        else:
          sim_device="CPU"
          
        SimNDT_Simulation = Simulation(TimeScale=sim_time_scale, MaxFreq=sim_max_freq, PointCycle=sim_point_cycle, SimTime=sim_simulation_time, Order=sim_order)
        
        SimNDT_Simulation.preferredDeviceType = sim_device
        
        if "dx" in sim_params["Simulation"]:
          SimNDT_Simulation.preferredDX=sim_params["Simulation"]["dx"]
        else:
          SimNDT_Simulation.preferredDX=None
        if "dt" in sim_params["Simulation"]:
          SimNDT_Simulation.preferredDT=sim_params["Simulation"]["dt"]
        else:
          SimNDT_Simulation.preferredDT=None
        
        print("Finished reading Simulation section.")
        print(repr(SimNDT_Simulation))
        #print("\n")
        return SimNDT_Simulation

def read_receivers(sim_params):
    """
    Reads and sets up the receivers for the simulation.
    
    Parameters:
    - sim_params: Dictionary containing simulation parameters.
    
    Returns:
    - SimNDT_Receivers: The receivers object.
    """
    
    # setting up the receivers
    
    if "Receivers" in sim_params.keys():
        receiver_method = sim_params["Receivers"]["Method"]
        SimNDT_Receivers = Receivers(method=receiver_method)
        print("Finished reading Receivers section.")
        # print(repr(SimNDT_Receivers))
        # print("\n")
        return SimNDT_Receivers
    
def SingleLaunchSetup(SimNDT_Scenario, SimNDT_Source, SimNDT_Inspection, SimNDT_Transd_objs, SimNDT_Signal):
    """
    Sets up the single launch inspection for the simulation by performing the following steps:
    1. Calculates XL and YL for the inspection based on the scenario and transducer.
    2. Sets up and checks the signal values, ensuring they are within valid ranges.
    
    Parameters:
    - SimNDT_Scenario: The simulation scenario object containing scenario details.
    - SimNDT_Source: The source object for the simulation.
    - SimNDT_Inspection: The inspection object for the simulation.
    - SimNDT_Transd_objs: The list of transducer objects for the simulation.
    - SimNDT_Signal: The signal object for the simulation.
    
    Raises:
    - ValueError: If the signal frequency is not within valid ranges or if the signal type is not implemented.
    """
    
    # calculating XL, YL for inspection
    SimNDT_Inspection.XL, SimNDT_Inspection.YL = SimNDT_Inspection.view(SimNDT_Scenario.M, SimNDT_Scenario.N, SimNDT_Scenario.Pixel_mm, SimNDT_Inspection.Theta, SimNDT_Transd_objs[0])
    
    # setting up signal
    # checking signal values
    if SimNDT_Signal.Name == "RaisedCosine":
        if SimNDT_Signal.Frequency > 1e8:
            raise ValueError("SingleLaunchSetup: Frequency out of range (MHz)")
        cycles = 1
    elif SimNDT_Signal.Name == "GaussianSine":
        if SimNDT_Signal.Frequency > 1e8:
            raise ValueError("SingleLaunchSetup: Frequency out of range (MHz)")          
    else:
        ValueError("Invalid Signal Function Name "+SimNDT_Signal.Name)
        

def SimulationSetup(Scenario, SimNDT_Mat_objs, SimNDT_Transd_objs, Simulation):
    """
    This function sets up the simulation environment by performing the following steps:
    1. Checks if the boundary conditions are defined in the scenario.
    2. Sets up the platform and device for the simulation, using OpenCL if available.
    3. Calculates the time step (dt) and spatial step (dx) for the simulation based on the materials and transducers.
    4. Creates the numerical model for the simulation, including Tapgrid, Rgrid, MRI, NRI, Im, and Mp.
    
    Parameters:
    - Scenario: The scenario object.
    - SimNDT_Mat_objs: List of material objects.
    - SimNDT_Transd_objs: List of transducer objects.
    - Simulation: The simulation object.
    """
    
    if np.size(Scenario.Iabs) == 1:
            ValueError("Please define the Boundaries Conditions!!!!")
    # setting up the platform and device
    if infoCL.importCL():
        Platforms = infoCL.getPlatforms()
        print('Platforms:', Platforms)
    else:
        Platforms = None
        
    if Platforms is not None:
        PlatformAndDevices = infoCL.getPlatformsAndDevices()    
        print('PlatformAndDevices:', PlatformAndDevices)
        
        preferred_platform = None
        preferred_device = None
        
        # First pass: Look for non-Intel GPU
        for PlatformAndDevice in PlatformAndDevices:
            platform_name = PlatformAndDevice[0].name
            device_name = PlatformAndDevice[1].name
            if 'Intel' not in platform_name and 'GPU' in device_name:
                preferred_platform = platform_name
                preferred_device = cl.device_type.to_string(PlatformAndDevice[1].type)
                break
        
        # Second pass: If no non-Intel GPU found, use any available platform
        if preferred_platform is None:
            for PlatformAndDevice in PlatformAndDevices:
                platform_name = PlatformAndDevice[0].name
                device_name = PlatformAndDevice[1].name
                device_type = cl.device_type.to_string(PlatformAndDevice[1].type)
                if device_type == Simulation.preferredDeviceType :
                  preferred_platform = platform_name
                  preferred_device = device_type
                  break
        
        Simulation.Platform = preferred_platform
        Simulation.Device = preferred_device
    else:
        Simulation.Platform = "Serial"
        Simulation.Device = "CPU"
        
    print("Assigned Platform='"+Simulation.Platform+"'")
    print("Assig. Device='"+Simulation.Device+"'")
    # calculating dt, dx in simulation
    Simulation.job_parameters(SimNDT_Mat_objs, SimNDT_Transd_objs[0])

    # calculating the numerical model (Tapgrid, Rgrid, MRI, NRI, Im, Mp )
    Simulation.create_numericalModel(Scenario)
    print(repr(Simulation))
    

def checkSimulation(Scenario, Materials, Boundaries, Transducers, Inspection, Signal, Simulation):
    """
    This function performs a series of checks to ensure that the simulation setup is correct and ready to run. 
    It checks the following:
    
    1. Material Labels: Ensures that the number of labels in the scenario matches the number of defined materials, 
       and that there are no repeated labels in the materials.
    2. Boundaries: Ensures that the boundaries are correctly defined and reloads them if necessary.
    3. Transducers: Sets the inspection on the numerical model.
    4. Signal: Generates the signal and ensures it fits within the simulation time.
    
    Parameters:
    - Scenario: The simulation scenario object containing scenario details.
    - Materials: The list of material objects for the simulation.
    - Boundaries: The list of boundary objects for the simulation.
    - Transducers: The list of transducer objects for the simulation.
    - Inspection: The inspection object for the simulation.
    - Signal: The signal object for the simulation.
    - Simulation: The simulation object containing simulation parameters.
    
    Raises:
    - ValueError: If any of the checks fail, a ValueError is raised with an appropriate error message.
    """
    # material label check
    case = CheckSim.labels(Scenario, Materials)

    if case == 1:
        ValueError("Number of labels in scenario is higher than the number of the defined materials. ")
    elif case == 2:
        ValueError("Number of labels in scenario is lower than the number of the defined materials. ")
    
    case = CheckSim.materials(Materials)
    if case == 1:
        raise ValueError("Repeated labels in Materials!!!.")
    print("Materials checked.")

    case = CheckSim.isLabelsEquals(Scenario, Materials)
    if case == 1:
        raise ValueError("Scenario Labels do not coincide with Material Labels!!!.")
    print("Labels checked.")

    # checking boundaries
    Scenario, Boundaries, isChange = CheckSim.boundariesReLoad(Scenario, Materials, Boundaries, Transducers[0], Inspection, Simulation)

    if isChange:
        Boundaries = Boundaries
        Scenario = Scenario
    print("Found error, but fixed: All boundaries with Air Layers")
    print("Boundaries checked.")

    # checking transducers
    # Resetting XL, YL, IR using x2, y2 (not sure why this is done! what are x2, y2?)
    Inspection.setInspection(Scenario, Transducers[0], Simulation)
    print("Inspection set.")

    # checking signal
    t = Simulation.t
    try:
        source = Signal.generate(t)
    except Exception as e:
        raise ValueError("Signal does not fit in the Simulation Time, Increase the Simulation Time to solve this issue")
    print("Signal generated.")

    # If no valueerror is raised, then the simulation is ready to run
    print("Simulation is ready to run!!!") 

def read_import(sim_params,SimNDT_Scenario,SimNDT_Simulation):
    print("Searching Import section...")
    importIm = 0
    importIabs = 0
    importI = 0
    if "Import" in sim_params.keys():
      print("Found Import section.")
      if "I" in sim_params["Import"].keys():
        importI     = sim_params["Import"]["I"]
      if "Iabs" in sim_params["Import"].keys():
        importIabs  = sim_params["Import"]["Iabs"]
      if "Im" in sim_params["Import"].keys():
        importIm    = sim_params["Import"]["Im"]
      if "Rho" in sim_params["Import"].keys():
        # must be imported later by the efit2d engine ...
        SimNDT_Simulation.importRho    = sim_params["Import"]["Rho"]
      if "VT" in sim_params["Import"].keys():
        # must be imported later by the efit2d engine ...
        SimNDT_Simulation.importVT    = sim_params["Import"]["VT"]
      if "VL" in sim_params["Import"].keys():
        # must be imported later by the efit2d engine ...
        SimNDT_Simulation.importVL    = sim_params["Import"]["VL"]
      if importIm:
        print("Reading Im from "+importIm)
        importIm    = np.load(importIm)
        print (np.shape(importIm))
        SimNDT_Simulation.update_numericalModel(SimNDT_Scenario,importIm,0)
  
def read_export(sim_params):
    """
    Enables and sets up snapshots for the simulation, including properties like step size, file path, and fields to save.
    
    Parameters:
    - sim_params: Dictionary containing simulation parameters.
    
    Returns:
    - SimNDT_SnapShots: The snapshots object.
    """
    print("Searching Export (Snapshot) section...")
    if "Snapshot" in sim_params.keys():
      sim_params["Export"]["Export"] = sim_params["Export"]["Snapshot"]
    if "Export" in sim_params.keys():
        print("Found Export section.")
        step            = sim_params["Export"]["Step"]
        save_filepath   = sim_params["Export"]["Save_filepath"]
        filename        = sim_params["Export"]["Filename"]
        if "enableFields" in sim_params["Export"].keys():
          enableFields  = sim_params["Export"]["enableFields"]
        else:
          enableFields  = False
        if "enableNumPy" in sim_params["Export"].keys():
          enableNumPy   = sim_params["Export"]["enableNumPy"]
        else:
          enableNumPy   = False
        if "enableCsv" in sim_params["Export"].keys():
          enableCsv   = sim_params["Export"]["enableCsv"]
        else:
          enableCsv   = False
        if "Extension" in sim_params["Export"].keys():
          extension       = sim_params["Export"]["Extension"]
        else:
          extension       = ".png"
        if "dB" in sim_params["Export"].keys():
          db            = sim_params["Export"]["dB"]
        else:
          db            = 50
        if "Color" in sim_params["Export"].keys():
          color         = sim_params["Export"]["Color"]
        else:
          color         = 0
        if "Field" in sim_params["Export"].keys():
          field           = sim_params["Export"]["Field"]
        else:
          field           = 3
        
        if "Material" in sim_params["Export"].keys():
          material      = sim_params["Export"]["Material"]
        else:
          material      = 0
        if "enableSignals" in sim_params["Export"].keys():
          enableSignals   = sim_params["Export"]["enableSignals"]
        else:
           enableSignals  = False
        if "enableImages" in sim_params["Export"].keys():
          enableImages  = sim_params["Export"]["enableImages"]
        else:
          enableImages  = False
        if "enableMaterial" in sim_params["Export"].keys():
          enableMaterial = sim_params["Export"]["enableMaterial"]
        else:
          enableMaterial = False
        if "enableVolume" in sim_params["Export"].keys():
          enableVolume  = sim_params["Export"]["enableVolume"]
        else:
          enableVolume  = False
        if "enableView" in sim_params["Export"].keys():        
          enableView    = sim_params["Export"]["enableView"]
        else:
          enableView    = False
        if "enableSummary" in sim_params["Export"].keys():        
          enableSummary    = sim_params["Export"]["enableSummary"]
        else:
          enableSummary    = False
        if "sensorShape" in sim_params["Export"].keys():        
          sensorShape  = sim_params["Export"]["sensorShape"]
        else:
          sensorShape  = []
        if "sensorPlacement" in sim_params["Export"].keys():        
          sensorPlacement = sim_params["Export"]["sensorPlacement"]
        else:
          sensorPlacement = []
        if "sensorSize" in sim_params["Export"].keys():        
          sensorSize   = sim_params["Export"]["sensorSize"]
        else:
          sensorSize   = 0
        print("Steps for Export="+str(step))
        # RunSimulation(self.filename, self.SimNDT_Simulation)
        SimNDT_SnapShots = SnapShots(
            Enable=True,
            Step=step,
            Filename=filename,
            File_path=save_filepath,
            enableFields=enableFields,
            enableNumPy=enableNumPy,
            enableCsv=enableCsv,
            Extension=extension,
            dB=db,
            Color=color,
            Field=field,
            Material=material,
            enableSignals=enableSignals,
            enableImages=enableImages,
            enableMaterial=enableMaterial,
            enableVolume=enableVolume,
            enableView=enableView,
            enableSummary=enableSummary,
            sensorShape=sensorShape,
            sensorPlacement=sensorPlacement,
            sensorSize=sensorSize
            )
        return SimNDT_SnapShots
    else:
      print ("No Export/Snapshot section found. Fatal.")
    

def openSim(sim_params):
    """
    Reads all simulation parameters and sets up the entire simulation environment, 
    including scenario, materials, boundaries, transducers, inspection, source, 
    signal, simulation, receivers, and snapshots.
    
    Parameters:
    - sim_params: Dictionary containing simulation parameters.
    
    Returns:
    - SimNDT_Scenario: The scenario object.
    - SimNDT_Mat_objs: List of material objects.
    - SimNDT_Bc_objs: List of boundary condition objects.
    - SimNDT_Transd_objs: List of transducer objects.
    - SimNDT_geom_objects: List of geometric objects.
    - SimNDT_Inspection: The inspection object.
    - SimNDT_Source: The source object.
    - SimNDT_Signal: The signal object.
    - SimNDT_Simulation: The simulation object.
    - SimNDT_Receivers: The receivers object.
    - SimNDT_SnapShots: The snapshots object.
    """
    SimNDT_Library = getMaterialLibrary()

    SimNDT_Scenario = None
    SimNDT_Mat_objs = None
    SimNDT_Bc_objs = None
    SimNDT_Transd_objs = None
    SimNDT_geom_objects = None
    SimNDT_Inspection = None
    SimNDT_Source = None
    SimNDT_Signal = None
    SimNDT_Simulation = None
    SimNDT_Receivers = None
    
    # reading geometric objects
    SimNDT_geom_objects = read_geometry_objects(sim_params)
    SimNDT_Mat_objs = read_materials(sim_params,SimNDT_Library)
    SimNDT_Bc_objs = read_boundaries(sim_params)
    SimNDT_Scenario = read_scenario(sim_params, SimNDT_geom_objects, SimNDT_Bc_objs, SimNDT_Mat_objs)

    SimNDT_Source = read_source(sim_params)
    SimNDT_Signal = read_signal(sim_params)
    SimNDT_Transd_objs = read_transducers(sim_params)
    check_transducer_size(SimNDT_Scenario, SimNDT_Transd_objs)
    
    SimNDT_Signal = read_signal(sim_params)
    
    SimNDT_Inspection = read_inspection(sim_params)
    if SimNDT_Inspection.Name == "Transmission":
        SingleLaunchSetup(SimNDT_Scenario, SimNDT_Source, SimNDT_Inspection, SimNDT_Transd_objs, SimNDT_Signal)
    
    SimNDT_Simulation = read_simulation(sim_params)
    
    # sets up simulation env
    SimulationSetup(SimNDT_Scenario, SimNDT_Mat_objs, SimNDT_Transd_objs, SimNDT_Simulation)  
    # checking labels, setting inspection and fixing boundary if necessary 
    checkSimulation(SimNDT_Scenario, SimNDT_Mat_objs, SimNDT_Bc_objs, SimNDT_Transd_objs, SimNDT_Inspection, SimNDT_Signal, SimNDT_Simulation)
    
    
    SimNDT_Receivers = read_receivers(sim_params)

    read_import(sim_params,SimNDT_Scenario,SimNDT_Simulation)
    
    SimNDT_SnapShots = read_export(sim_params)
    
    print("steps: ", SimNDT_SnapShots.Step)
    print("Finished reading all parameters.")

    simPack = SimPack(SimNDT_Scenario, SimNDT_Mat_objs, \
                        SimNDT_Bc_objs, SimNDT_Inspection, SimNDT_Source, \
                        SimNDT_Transd_objs, SimNDT_Signal, SimNDT_Simulation, \
                        SimNDT_geom_objects, SimNDT_Receivers, SimNDT_SnapShots, SimNDT_Library)
    
    return simPack




def runEngine(simPack):
    """
    Runs the simulation engine with the provided simulation components and 
    saves the simulation video.
    
    Parameters:
    - SimNDT_Scenario: The scenario object.
    - SimNDT_Materials: List of material objects.
    - SimNDT_Boundaries: List of boundary condition objects.
    - SimNDT_Inspection: The inspection object.
    - SimNDT_Source: The source object.
    - SimNDT_Transducers: List of transducer objects.
    - SimNDT_Signal: The signal object.
    - SimNDT_Simulation: The simulation object.
    - SimNDT_Receivers: The receivers object.
    - SimNDT_SnapShots: The snapshots object.
    """
    
    engine = EngineController(simPack, simPack.SnapShots)
    
    simPack.SimNDT_Engine = engine
    
    state = engine.run()

    if state == "Stop":
        print("Stop by User!!!!!")
    else:
        print("Simulation Done.")
    
    
        
    # SimNDT_Receivers = Receivers(self.SimNDT_Inspection.Name)
    # SimNDT_Receivers.setReceivers(engine)


def openSim_mat_file(mat_filename):
    """
    Loads simulation data from a MATLAB file and sets up the simulation environment based on the loaded data.
    
    Parameters:
    - mat_filename: The path to the MATLAB file.
    
    Returns:
    - SimNDT_Scenario: The scenario object.
    - SimNDT_Materials: List of material objects.
    - SimNDT_Boundaries: List of boundary condition objects.
    - SimNDT_Transducers: List of transducer objects.
    - SimNDT_ObjectList: List of geometric objects.
    - SimNDT_Inspection: The inspection object.
    - SimNDT_Source: The source object.
    - SimNDT_Signal: The signal object.
    - SimNDT_Simulation: The simulation object.
    - SimNDT_Receivers: The receivers object.
    """
    data2load = {}
    data2load = scipy.io.loadmat(mat_filename, squeeze_me=True, struct_as_record=False)


    if "Scenario" in data2load:
        Width = getattr(data2load["Scenario"], 'Width')
        Height = getattr(data2load["Scenario"], 'Height')
        Pixel_mm = getattr(data2load["Scenario"], 'Pixel_mm')
        Label = getattr(data2load["Scenario"], 'Label')
        SimNDT_Scenario = Scenario(Width=Width, Height=Height, Pixel_mm=Pixel_mm, Label=Label)
        SimNDT_Scenario = HelperMethods.mat2Obj(data2load["Scenario"], SimNDT_Scenario)

    SimNDT_Materials = HelperMethods.loadDataFromList(data2load, 'Materials', Material())
    SimNDT_Boundaries = HelperMethods.loadDataFromList(data2load, "Boundaries", Boundary())
    SimNDT_Transducers = HelperMethods.loadDataFromList(data2load, "Transducers", Transducer())

    geoLabels = ["ellipse", "circle", "square", "rectangle"]
    geoObjects = [Ellipse(), Circle(), Square(), Rectangle()]
    SimNDT_ObjectList = HelperMethods.loadDataFromListWithLabels(data2load, 'GeometricObjects', geoLabels,
                                                                        geoObjects)

    ConcreteLabels = ["Concrete2Phase", "Concrete2PhaseImmersion", "Concrete3Phase", "Concrete3PhaseImmersion"]
    ConcreteObjects = [Concrete2Phase(), Concrete2PhaseImmersion(), Concrete3Phase(), Concrete3PhaseImmersion()]

    if "ConcreteMicrostructure" in data2load:
        SimNDT_ConcreteMicrostructure = HelperMethods.loadDataWithLabels(data2load, 'ConcreteMicrostructure',
                                                                                ConcreteLabels, ConcreteObjects)
    if "Simulation" in data2load:
        SimNDT_Simulation = HelperMethods.mat2Obj(data2load["Simulation"], Simulation())
        
    if "Inspection" in data2load:
        inspLabels = ['Transmission', 'PulseEcho', 'LinearScan', 'Tomography']
        inspObjects = [Transmission(), PulseEcho(), LinearScan(), Tomography()]
        SimNDT_Inspection = HelperMethods.loadDataWithLabels(data2load, "Inspection", inspLabels, inspObjects)
        SimNDT_Inspection.setInspection(SimNDT_Scenario, SimNDT_Transducers[0], SimNDT_Simulation)
        
    if "Source" in data2load:
        SimNDT_Source = HelperMethods.mat2Obj(data2load["Source"], Source())

    if "Signal" in data2load:
        SimNDT_Signal = HelperMethods.mat2Obj(data2load["Signal"], Signals())



    if "Receivers" in data2load:
        SimNDT_Receivers = HelperMethods.mat2Obj(data2load["Receivers"], Receivers())



    return SimNDT_Scenario, SimNDT_Materials, SimNDT_Boundaries, SimNDT_Transducers, SimNDT_ObjectList, SimNDT_Inspection, SimNDT_Source, SimNDT_Signal, SimNDT_Simulation, SimNDT_Receivers
