# ==================================================================
# === Make Line Probes =============================================
# ==================================================================
# Make a series of lines probes at various azimuthal angles, and
# write them as separate zones to a single plot3d file

# === Import =======================================================
import numpy as np
import yaml
import argparse
# ==================================================================

# === Generator ====================================================
class LineProbeGenerator:
   """
   Line Probe Generator Class
   """

   def __init__(self,opts):
      """
      Constructor for LAVA line probe generator

      Parameters:
      -----------
      opts: dict
         Dictionary containing user input parameters.
      """
      # Set member data from options dictionary
      self._set_params(opts)

   def _set_params(self,opts):
      """
      Define parameters from dicitionary.

      Parameters:
      -----------
      opts, dict:
        Dictionary containing user input parameters.    
      """
      # Ensure all dictionary keys are lowercase
      opts = self._lower_dict_keys(opts)
      
      # Get keys from user input dictionary
      user_keys = self._get_keys(opts)

      # Get required keys
      required_keys = self._get_required_keys()

      # Missing keys
      missing_keys = list(set(required_keys).difference(set(user_keys)))

      if (missing_keys != []):
         raise ValueError("Invalid input provided. The follwing entries are missing: ", missing_keys)

      # Retrieve options from dictionary
      self.number_points = int(opts["probes"]["number_points"])
      self.phi_start = float(opts["probes"]["phi_start"])
      self.phi_end = float(opts["probes"]["phi_end"])
      self.number_angles = int(opts["probes"]["number_angles"])
      self.mach = opts["probes"]["mach"]
      self.tau_start = opts["probes"]["tau_start"]
      self.tau_end = opts["probes"]["tau_end"]
      self.body_length = opts["probes"]["body_length"]
      self.height_over_length = opts["probes"]["height_over_length"]
      self.output_file = opts["output"]["probe_filename"]

   @staticmethod
   def _get_required_keys():
      """
      Return the required keys for this utility.
      """

      return ["probes", "number_points", "phi_start", "phi_end", \
              "number_angles", "mach", "tau_start", \
              "tau_end", "body_length", "height_over_length", \
              "output", "probe_filename"]
      
   def _get_keys(self, d, keys=None):
      """
      Returns all keys in a dictionary.

      Parameters
      ----------
      d: dict
         Dictionary with possibly recursive keys
      
      Returns
      -------
      keys: list
         List of keys belonging to dictionary d.
      """
      if keys is None: keys = []

      if isinstance(d, list):
         for item in d:
            self._get_keys(item, keys)
      elif isinstance(d, dict):
         for key,value in d.items():
            keys.append(key)
            if isinstance(value, dict):
               self._get_keys(value, keys)

      return keys

   def _lower_dict_keys(self, d) :
      """
      Returns the dictionary d, but with all keys in lower case

      Parameters
      ----------
      d: dict
         Dictionary with keys in any case

      Returns
      -------
      d: dict
         Dictionary with keys in lower case

      """        
      if isinstance(d, list):
         return [self._lower_dict_keys(v) for v in d]
      elif isinstance(d, dict):
         return dict((k.lower(), self._lower_dict_keys(v)) for k, v in d.items())
      else:
         return d
   
   def generate(self):
      """
      Generates the line probes.

      Returns:
      --------
      xyz: numpy array
       The line probe coordinates.
      """
      self._make_line_probes(self.number_points, self.phi_start, \
                             self.phi_end, self.number_angles, self.mach, \
                             self.tau_start, self.tau_end, self.body_length, \
                             self.height_over_length)
   
   def _make_line_probes(self, npts, phi_start, phi_end, nangle, mach, 
                     tau_start, tau_end,
                     bodylength, heightoverlength):
      """
      Generate line probes for off-body grid waveform evaluations

      Parameters
      ----------
      npts: int
        Number of points per line probe.

      phi_start: float
        Starting azimuthal angle.

      phi_end: float
        Ending azimuthal angle.

      nangle: int
        Number of azimuthal angles, which corresponds to the numb
        of line probes that will be generated.

      mach: float
        Freestream Mach number.

      tau_start: float
        Start of the line probe relative to the body origin.

      tau_end: float
        End of the line probe relative to the body origin.

      body_length: float
        Length of the geometric body.

      height_over_length: float
        Non-dimensional distance from the body to the probe.

      Returns:
      ---------
      xyz: numpy array
        The line probe coordinates.
      """
      # Get array of discrete angles
      phis = np.linspace(phi_start, phi_end, nangle)
 
      # Mach cone angle
      line_mu = np.arcsin(1.0/mach)
      line_beta = 1.0 / np.tan(line_mu)
 
      self.xyz = []
      for i in range(nangle) :
         self.xyz.append(np.empty((1, 1, npts, 3)))
         xmin = (tau_start+line_beta*heightoverlength)*bodylength
         xmax = (tau_end+line_beta*heightoverlength)*bodylength
 
         self.xyz[i][0, 0, :, 0] = np.linspace(xmin, xmax, npts)
         self.xyz[i][0, 0, :, 1] = -heightoverlength * np.sin(phis[i]/180.0*np.pi)*bodylength
         self.xyz[i][0, 0, :, 2] = -heightoverlength * np.cos(phis[i]/180.0*np.pi)*bodylength
   
   @staticmethod
   def _writeIntsRM(f, val):

      val = np.asarray(val, dtype='i4').flatten(order='F')
      length = np.asarray(val.nbytes, dtype='i4')

      length.tofile(f)
      val.tofile(f)
      length.tofile(f)
   
   @staticmethod
   def _writeRealsRM(f, val):

      val = np.asarray(val, dtype='f8').flatten(order='F')
      length = np.asarray(val.nbytes, dtype='i4')

      length.tofile(f)
      val.tofile(f)
      length.tofile(f) 

   @staticmethod
   def _writePlot3DGrid(filename, xyz):
      """
      Write a plot3d file in unformatted Fortran style with record markers.
 
      Parameters
      ----------
      filename : str
         Name of the file
 
      xyz : list of ndarrays
         A list of arrays that contain the xyz points for the grid.
         Each array in the list must be of size: (jmax, kmax, lmax, 3)
      """
 
      if not isinstance(xyz, list) :
         raise ValueError("Input into writePlot3DGrid must be a list")
 
      nBlks = len(xyz)
 
      sizes = np.zeros((3, nBlks))
      for i in range(nBlks):
         sizes[:,i] = np.asarray(xyz[i].shape[0:3])
 
      with open(filename, mode='wb') as f:
         # Write the number of zones
         LineProbeGenerator._writeIntsRM(f, nBlks)
 
         # Write the size
         LineProbeGenerator._writeIntsRM(f, sizes)
 
         # Write the coordinates
         for i in range(nBlks) :
            LineProbeGenerator._writeRealsRM(f, xyz[i][:,:,:,:])

   def writeProbes(self):
      """
      Writes the line probes to a Plot3D file with RMs.
      """

      self._writePlot3DGrid(self.output_file, self.xyz)

# =================================================================
#                             Main
# =================================================================

if __name__ == '__main__' :

   #===================================
   # Preprocessing
   #===================================

   # Define argument parser
   parser = argparse.ArgumentParser(description="LAVA Probe Extraction Utility", \
                                    formatter_class=argparse.ArgumentDefaultsHelpFormatter)
   parser.add_argument('input_file', type=str, help='Path to the YAML configuration file')

   # Parse user provided arguments
   args = parser.parse_args()

   # Read YAML input file
   try:
      with open(args.input_file,'r') as f:
         opts = yaml.safe_load(f)
   except FileNotFoundError:
      print(f"Error: The file '{args.input_file}' was not found.")
      exit(1)
   except yaml.YAMLError as e:
      print(f"Error parsing YAML file: {e}")
      exit(1)
   
   #===================================
   # Generate probes
   #===================================
   generator = LineProbeGenerator(opts)
   generator.generate()
   generator.writeProbes()
