text: """Convenience wrapper for looping and clipping identically gridded in inputs
  
  Everything here can be customized using the classes in the subset package
  
  """
  import sys
  import argparse
  import os
  from pathlib import Path
  import logging
  from datetime import datetime
  from parflow.subset.clipper import MaskClipper, BoxClipper
  from parflow.subset.utils.arguments import is_valid_file, is_valid_path
  from parflow.subset import TIF_NO_DATA_VALUE_OUT as NO_DATA
  from parflow.subset.mask import SubsetMask
  import parflow.subset.utils.io as file_io_tools
  
  
  def parse_args(args) -> argparse.Namespace:
      """Parse the command line arguments
  
      Parameters
      ----------
      args : list
          list of arguments from sys.argv
  
      Returns
      -------
      Namespace
          populated Namespace object from the parsed command line options
      """
      parser = argparse.ArgumentParser('Clip a list of identically gridded files and extract the data within the mask')
  
      exclusive_group = parser.add_mutually_exclusive_group(required=True)
  
      exclusive_group.add_argument("--maskfile", "-m", dest="mask_file", required=False,
                          type=lambda x: is_valid_file(parser, x),
                          help="gridded full_dim_mask file to full extent of files to be clipped")
  
      exclusive_group.add_argument("--bboxfile", "-b", dest="bbox_file", required=False,
                          type=lambda x: is_valid_file(parser, x),
                          help="a tab separated text file indicating x1,y1,nx,ny of the files to be clipped")
  
      exclusive_group.add_argument("--inline-bbox", "-l", dest="bbox_def", nargs=4, metavar=('X1', 'Y1', 'NX', 'NY'),
                                   required=False, type=int, help="bbox defined by x1 y1 nx ny")
  
      exclusive_group2 = parser.add_mutually_exclusive_group(required=True)
  
      exclusive_group2.add_argument("--datafiles", "-d", dest="data_files", required=False,
                                  nargs='+',
                                  type=lambda x: is_valid_file(parser, x),
                                  help="the list of gridded data files (.pfb or .tif) to clip from")
  
      exclusive_group2.add_argument("--glob", "-g", dest="glob_pattern", required=False, type=str,
                                    help="a filter string for filtering files to clip")
  
      parser.add_argument("--input_path", "-i", dest="input_path", required=False, default='.',
                          type=lambda x: is_valid_path(parser, x),
                          help="the folder to look for input files")
  
      parser.add_argument("--ref_file", "-r", dest="ref_file", required=False,
                          type=lambda x: is_valid_file(parser, x),
                          help="the reference tif, if writing TIF outputs")
  
      parser.add_argument("--out_dir", "-o", dest="out_dir", required=False,
                          default='.',
                          help="the directory to write outputs to",
                          type=lambda x: is_valid_path(parser, x))
  
      parser.add_argument("--pfb_outs", "-p", dest="write_pfbs", required=False,
                          action='store_true', default=True, help="write pfb output files")
  
      parser.add_argument("--tif_outs", "-t", dest="write_tifs", required=False,
                          action='store_true', help="write tif output files")
  
      return parser.parse_args(args)
  
  
  def mask_clip(mask_file, data_files, out_dir='.', pfb_outs=1, tif_outs=0) -> None:
      """clip a list of files using a full_dim_mask and a domain reference tif
  
      Parameters
      ----------
      mask_file : str
          full_dim_mask file generated from shapefile to mask utility no_data,0's=bbox,1's=mask
      data_files : list
          list of data files (tif, pfb) to clip from
      out_dir : str, optional
          output directory (optional) (Default value = '.')
      pfb_outs : int, optional
          write pfb files as outputs (optional) (Default value = 1)
      tif_outs : int, optional
          write tif files as outputs (optional) (Default value = 0)
  
      Returns
      -------
      None
      """
      # read the full_dim_mask file
      mask = SubsetMask(mask_file)
  
      # create clipper with full_dim_mask
      clipper = MaskClipper(subset_mask=mask, no_data_threshold=-1)
      # clip all inputs and write outputs
      clip_inputs(clipper, input_list=data_files, out_dir=out_dir, pfb_outs=pfb_outs,
                  tif_outs=tif_outs)
  
  
  def box_clip(bbox, data_files, out_dir='.', pfb_outs=1, tif_outs=0) -> None:
      """clip a list of files using a bounding box
  
      Parameters
      ----------
      bbox : tuple
          tuple of x, y, nx, ny to specifies the bounding region
      data_files : list
          list of data files (tif, pfb) to clip from
      out_dir : str, optional
          output directory (optional) (Default value = '.')
      pfb_outs : int, optional
          write pfb files as outputs (optional) (Default value = 1)
      tif_outs : int, optional
          write tif files as outputs (optional) (Default value = 0)
  
      Returns
      -------
      None
      
      """
  
      # create clipper with bbox
      clipper = BoxClipper(ref_array=file_io_tools.read_file(data_files[0]), x=bbox[0], y=bbox[1], nx=bbox[2], ny=bbox[3])
      # clip all inputs and write outputs
      clip_inputs(clipper, input_list=data_files, out_dir=out_dir, pfb_outs=pfb_outs,
                  tif_outs=tif_outs)
  
  
  def locate_tifs(file_list) -> list:
      """identify the .tif files in a list of files
  
      Parameters
      ----------
      file_list : list
          list of files to parse
  
      Returns
      -------
      list
          list of files where the extension is .tif
  
      """
      return list([s for s in file_list if '.tif' in s.lower()])
  
  
  def clip_inputs(clipper, input_list, out_dir='.', pfb_outs=1, tif_outs=0, no_data=NO_DATA) -> None:
      """clip a list of files using a clipper object
  
      Parameters
      ----------
      clipper : Clipper
          clipper object prepared with full_dim_mask and reference dataset
      input_list : list
          list of data files (tif, pfb) to clip from
      out_dir : str, optional
          output directory (optional) (Default value = '.')
      pfb_outs : int, optional
          write pfb files as outputs (optional) (Default value = 1)
      tif_outs : int, optional
          write tif files as outputs (optional) (Default value = 0)
      no_data : int, optional
          no_data value for tifs (optional) (Default value = NO_DATA)
  
      Returns
      -------
      None
      """
      ref_proj = None
      if tif_outs:
          # identify projection
          ref_proj = clipper.subset_mask.mask_tif.GetProjection()
  
      # loop over and clip
      for data_file in input_list:
          filename = Path(data_file).stem
          return_arr, new_geom, _, _ = clipper.subset(file_io_tools.read_file(data_file))
          if pfb_outs:
              file_io_tools.write_pfb(return_arr, os.path.join(out_dir, f'{filename}_clip.pfb'))
          if tif_outs and new_geom is not None and ref_proj is not None:
              file_io_tools.write_array_to_geotiff(os.path.join(out_dir, f'{filename}_clip.tif'),
                                                   return_arr, new_geom, ref_proj, no_data=no_data)
  
  
  def get_file_list(input_dir, glob_pattern=None, files=None) -> list:
      """get a list of proper paths for files either in the list or matching the glob pattern
  
      Parameters
      ----------
      input_dir : Path
          directory where input files are located
      glob_pattern : str
          filename pattern to match
      files : list
          list of input filenames to use
      Returns
      -------
      file_list : list of Paths
          the assembled list of filenames with paths
      """
      file_list = []
      if glob_pattern is not None:
          file_list = input_dir.glob(glob_pattern)
      elif files is not None:
          file_list = [input_dir / filename for filename in files]
      return list(file_list)
  
  
  def main():
      # setup logging
      logging.basicConfig(filename='bulk_clipper.log', filemode='w', level=logging.INFO)
      start_date = datetime.utcnow()
      logging.info(f'start process at {start_date} from command {" ".join(sys.argv[:])}')
      args = parse_args(sys.argv[1:])
      # get the input path and data file list
      input_path = Path(args.input_path)
      data_files = get_file_list(input_dir=input_path, files=args.data_files, glob_pattern=args.glob_pattern)
      # If tif out specified, look for a reference tif
      if args.write_tifs and not args.ref_file:
          if 'tif' not in args.mask_file.lower():
              input_tifs = locate_tifs(data_files)
              if len(input_tifs) < 1:
                  raise Exception('Must include at least one geotif input or a ref_file when tif_outs is selected')
      if args.mask_file:
          mask_clip(args.mask_file, data_files, args.out_dir, args.write_pfbs, args.write_tifs)
      elif args.bbox_file:
          box_clip(file_io_tools.read_bbox(args.bbox_file), data_files, args.out_dir, args.write_pfbs,
                   args.write_tifs)
      elif args.bbox_def:
          box_clip(args.bbox_def, data_files, args.out_dir, args.write_pfbs, args.write_tifs)
      end_date = datetime.utcnow()
      logging.info(f'completed process at {end_date} for a runtime of {end_date-start_date}')
  
  
  if __name__ == '__main__':
      main()
  