# 
#  Copyright (C) 2010-2012,2014-2020  Smithsonian Astrophysical Observatory
#
#
#  This program is free software; you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation; either version 3 of the License, or
#  (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License along
#  with this program; if not, write to the Free Software Foundation, Inc.,
#  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#

from pycrates import *
from pytransform import *
from numpy import array
import numpy as np
from collections import OrderedDict
import hashlib
import warnings

__all__ = ('add_col',
           'add_comment',
           'add_crate',
           'add_history',
           'add_key',
           'add_piximg',
           'add_record',
           'check_cratedata_modified',
           'check_crate_modified',
           'col_exists',
           'convert_row_to_pha1',
           'copy_colvals',
           'copy_piximgvals',
           'create_vector_column',
           'create_virtual_column',
           'delete_col',
           'delete_comments',
           'delete_crate',
           'delete_history',
           'delete_key',
           'delete_piximg',
           'get_all_records',
           'get_axis_transform',
           'get_col',
           'get_col_names',
           'get_colvals',
           'get_comment_records',
           'get_crate',
           'get_crate_checksum',
           'get_cratedata_checksum',
           'get_crate_type',
           'get_history_records',
           'get_key',
           'get_key_names',
           'get_keyval',
           'get_number_cols',
           'get_number_rows',
           'get_piximg',
           'get_piximg_shape',
           'get_piximgvals',
           'get_transform',
           'get_transform_matrix',
           'is_pha',
           'is_pha_type1',
           'is_pha_type2',
           'is_rmf',
           'is_virtual',
           'key_exists',
           'print_axis_names',
           'print_col_names',
           'print_full_header',
           'print_history',
           'print_key_names',
           'read_dataset',
           'read_file',
           'read_pha',
           'read_rmf',
           'set_colvals',
           'set_key',
           'set_keyval',
           'set_piximgvals',
           'update_all_cratedata_checksums',
           'update_crate_checksum',
           'update_cratedata_checksum',
           'write_dataset',
           'write_file',
           'write_pha',
           'write_pha1_files',
           'write_rmf',
           'write_row_as_pha1'
)


_crate_types = dict ( CrateDataset     = 'CrateDS',
                      TABLECrate       = 'Table',
                      IMAGECrate       = 'Image',
                      PHACrateDataset  = 'PHADS',
                      RMFCrateDataset  = 'RMFDS'
                      )

_crate_item_types = dict( CrateData  ='Data',
                          CrateKey   ='Key'
                          )
# -----------------------------------------------------------------------

def isinstance_int (n):
   """
   This function return true of n is integer of any type , otherwise a false
   """

   s = str(n)
   if s.find('.') != -1:	#check for floating point
      return False
   if s == 'True' or s == 'False':	#check for boolean
      return False
   try:
      if n + 0 == int (n):
         return True
   except:
      pass
   return False

# ----------------------------------------------------------------------

def get_crate_type(crate):
    """
    get_crate_type(<crate>)

    Returns the type of the input Crate.
    """    
    try:
        name = crate.__class__.__name__
    except:
        return None
    try:
        return _crate_types[name]
    except:
        return None


# ----------------------------------------------------------------------

def get_crate_item_type(crate):
   """
   get_crate_item_type( CrateData|CrateKey )
   
   Returns the type of the input crate item.
   """    
   try:
      name = crate.__class__.__name__
   except:
      return None
   try:
      return _crate_item_types[name]
   except:
      return None

# ----------------------------------------------------------------------

def is_pha(dataset):
   """
   is_pha(<dataset>)
   
   Returns 1 if the input dataset is an PHACrateDataset, 0 if it is not.
   """    
   
   if get_crate_type(dataset) != 'PHADS':
      return False
   
   return True


# ----------------------------------------------------------------------

def is_pha_type1(dataset):
   """
   is_pha_type1(<dataset>)
   
   Returns 1 if the input dataset is an PHA Type:I dataset, 0 if it is not.
   
   """
   
   if not is_pha(dataset):
      raise TypeError("Input Crate must be a PHACrateDataset.")
   
   return dataset.is_pha_type1()

# ----------------------------------------------------------------------

def is_pha_type2(dataset):
   """
   is_pha_type2(<dataset>)
   
   Returns 1 if the input dataset is an PHA Type:II dataset, 0 if it is not.
   """
   
   if not is_pha(dataset):
      raise TypeError("Input Crate must be a PHACrateDataset.")
   
   return dataset.is_pha_type2()

# ----------------------------------------------------------------------

def is_rmf(dataset):
   """
   is_rmf(<dataset>)
   
   Returns 1 if the input dataset is an RMFCrateDataset, 0 if it is not.
   """    
   
   if get_crate_type(dataset) != 'RMFDS':
      return False
   
   return True

# ----------------------------------------------------------------------

def is_virtual(crate, colname):
   """
   is_virtual(<crate>, colname)
   
   Return 1 if the input column name is VIRTUAL, 0 if it is not.
   """

   colname = convert_2_str(colname)

   if crate is None:
      raise ValueError("No input Crate specified.")
   
   if colname is None:
      raise ValueError("No input column name specified.")
   
   if get_crate_type(crate) != 'Table':
      raise TypeError("Input Crate must be a TABLE.")
   
   col = crate.get_column(colname)
   if col is None:
      raise RuntimeError("Column not found.")
   
   return col.is_virtual()


# ----------------------------------------------------------------------

def print_col_names(crate, vectors=True, rawonly=True ):
   """
   print_col_names(<crate>, vectors=True, rawonly=True )
   
   Display names of the columns contained in the input Crate.
   
   Arguments:
           vectors   - flag to control format for vector columns.
                        True:  shows vector notation.
                        False: displays individual component columns.
                        default = True
           rawonly   - flag for including/excluding virtual columns
                        True:  exclude virtual columns
                        False: include virtual columns
                        default = False
   """    

   if crate is None:
      raise ValueError("No input Crate specified.")
   
   if get_crate_type(crate) != 'Table':
      raise TypeError("Input Crate must be a TABLE.")
   
   print_str = crate.print_colnames(vectors, rawonly)
   
   return print_str

# ----------------------------------------------------------------------

def print_key_names(crate):
   """
   print_key_names(<crate>)
   
   Display names of the keywords contained in the input Crate.
   """    
   
   if crate is None:
      raise ValueError("No input Crate specified.")
   
   crtype = get_crate_type(crate) 
   if crtype != 'Table' and crtype != 'Image':
      raise TypeError("Input Crate must be a TABLE or an IMAGE.")
   
   names = list(crate._get_keylist().keys())
   if names is None:
      raise RuntimeError('This Crate does not contain any keywords.')
   
   print_str = " Index  Keyname\n"
   for ndx in range( len(names) ):
      print_str += ' {0:3d})   {1!s}\n'.format(ndx, names[ndx])
      
   return print_str

# ----------------------------------------------------------------------

def print_axis_names(crate):
   """
   print_axis_names(<crate>)
   
   Display names of the axes defined for the input Image Crate.
   """
   
   if crate is None:
      raise ValueError("No input Crate specified.")
   
   if get_crate_type(crate) != 'Image':
      raise TypeError("Input Crate must be an IMAGE.")
   
   names = crate.get_axisnames()
   if names is None:
      raise RuntimeError('This Crate does not contain any axis definitions.')
      return None
   
   print_str = " Index  Axisname\n"
   for ndx in range( len(names) ):
      print_str += ' {0:3d})   {1!s}\n'.format(ndx, names[ndx])

   return print_str

# ----------------------------------------------------------------------

def get_col(crate, item ):
   """
   get_col(<crate>, name )
   get_col(<crate>, colno )
   
   Locate and return the specified column within the provided Crate.
   Column may be specified by name or column number (0 based).
    """  
   item = convert_2_str(item)
  
   if crate is None:
      raise ValueError("No input Crate specified.")
   
   if get_crate_type(crate) != 'Table':
      raise TypeError("Input Crate must be a TABLE.")
   
   if isinstance_int(item):
      tmp_item = int(item)
   elif type(item) in (np.string_, str):
      tmp_item = item
   else:
      raise IndexError("Please specify column by name or number.")
   
   dat = crate.get_column( tmp_item )
   if dat is None:
      raise RuntimeError('This Crate does not contain column ' + item + '.')
   
   return dat

# ----------------------------------------------------------------------

def get_key(crate, item ):
   """
   get_key(crate, name )
   get_key(crate, keyno )
   
   Locate and return the specified keyword within the provided Crate.
   Keyword may be specified by name or number (0 based).
   """    
   item = convert_2_str(item)

   if crate is None:
      raise ValueError("No input Crate specified.")
   
   crtype = get_crate_type(crate) 
   if crtype != 'Table' and crtype != 'Image':
      raise TypeError("Input Crate must be a TABLE or an IMAGE.")

   if isinstance_int(item):
      tmp_item = int(item)
   elif type(item) in (np.string_, str):
      tmp_item = item
   else:
      raise IndexError("Please specify keyword by name or number.")


   dat = crate.get_key( tmp_item )
   if dat is None:
      raise RuntimeError('This Crate does not contain keyword ' + item + '.')

   return dat

# ----------------------------------------------------------------------

def get_piximg(crate):
   """
   get_piximg(<crate>)
   
   Returns the image associated with the input Image crate.
   """    

   if crate is None:
      raise ValueError("No input Crate specified.")
   
   if get_crate_type(crate) != 'Image':
      raise TypeError("Input Crate must be an IMAGE.")
   
   dat = crate.get_image()
   if dat is None:
      raise RuntimeError('This Crate does not contain an image.')
   
   return dat

# ----------------------------------------------------------------------

def add_col(crate, cratedata):
   """
   add_col(<crate>, CrateData)
   
   Adds input column to the given crate.
   """     
   if crate is None:
      raise ValueError("No input Crate specified.")
   
   if get_crate_type(crate) != 'Table':
      raise TypeError("Input Crate must be a TABLE.")
   
   if cratedata is None:
      raise ValueError("No input CrateData specified.")
   
   if get_crate_item_type(cratedata) != 'Data':
      raise TypeError("Input must be a CrateData object.")
   
   stat = crate.add_column(cratedata)
   
   return stat

# ----------------------------------------------------------------------

def add_key(crate, cratekey):
   """
   add_key(<crate>, CrateKey)
   
   Adds input keyword to the given crate.
   """     
   if crate is None:
      raise ValueError("No input Crate specified.")
   
   crtype = get_crate_type(crate) 
   if crtype != 'Table' and crtype != 'Image':
      raise TypeError("Input Crate must be a TABLE or an IMAGE.")
   
   if cratekey is None:
      raise ValueError("No input CrateKey specified.")
   
   if get_crate_item_type(cratekey) != 'Key':
      raise TypeError("Input must be a CrateKey object.")
   
   crate.add_key(cratekey)
   
   return

# ----------------------------------------------------------------------

def add_piximg(crate, cratedata):
   """
   add_piximg(<crate>, CrateData)
   
   Adds input image to the given crate.
   """     
   if crate is None:
      raise ValueError("No input Crate specified.")
   
   if get_crate_type(crate) != 'Image':
      raise TypeError("Input Crate must be an IMAGE.")
   
   if cratedata is None:
      raise ValueError("No input CrateData specified.")
   
   if get_crate_item_type(cratedata) != 'Data':
      raise TypeError("Input must be a CrateData object.")
   
   crate.add_image(cratedata)
   
   return

# ----------------------------------------------------------------------

def delete_col(crate, item):
   """
   delete_col(<crate>, name)
   delete_col(<crate>, colno)
   
   Removes specified column from the input crate.
   Column may be specified by name or column number (0 based).
   """     
   item = convert_2_str(item)

   if crate is None:
      raise ValueError("No input Crate specified.")
   
   if get_crate_type(crate) != 'Table':
      raise TypeError("Input Crate must be TABLE.")
   
   if isinstance_int(item):
      tmp_item = int(item)
   elif type(item) in (np.string_, str):
      tmp_item = item
   else:
      raise IndexError("Please specify column by name or number.")
   
   stat = crate.delete_column(tmp_item)
   
   return stat

# ----------------------------------------------------------------------

def delete_key(crate, item):
   """
   delete_key(<crate>, name)
   delete_key(<crate>, keyno)
   
   Removes specified keyword from the input crate.
   Keyword may be specified by name or number (0 based).
   """     
   item = convert_2_str(item)

   if crate is None:
      raise ValueError("No input Crate specified.")
   
   crtype = get_crate_type(crate) 
   if crtype != 'Table' and crtype != 'Image':
      raise TypeError("Input Crate must be a TABLE or an IMAGE.")
   
   if isinstance_int(item):
      tmp_item = int(item)
   elif type(item) in (np.string_, str):
      tmp_item = item
   else:
      raise IndexError("Please specify key by name or number.")
   
   stat = crate.delete_key(tmp_item)
   
   return stat

# ----------------------------------------------------------------------

def delete_piximg(crate):
   """
   delete_piximg(<crate>)
   
   Removes image from specified crate.
   """     
   if crate is None:
      raise ValueError("No input Crate specified.")
   
   if get_crate_type(crate) != 'Image':
      raise TypeError("Input Crate must be an IMAGE.")
   
   stat = crate.delete_image()
   
   return stat

# ----------------------------------------------------------------------

def col_exists(crate, name ):
   """
   col_exists(<crate>, name )
   
   Check if the input Crate contains the specified column.
   Columns are specified by name and the check is case sensitive.
   Returns 1 if the column exists, 0 if it does not.
   """ 
   name = convert_2_str(name)
   
   if crate is None:
      raise ValueError("No input Crate specified.")
   
   if get_crate_type(crate) != 'Table':
      raise TypeError("Input Crate must be a TABLE.")
   
   if type(name) not in (np.string_ , str):
      raise IndexError("Please specify column by name.")
   
   dat = crate.column_exists( name )
   
   return dat

# ----------------------------------------------------------------------

def key_exists(crate, name ):
   """
   key_exists(<crate>, name )
   
   Check if the input Crate contains the specified keyword.
   Keys are specified by name and the check is case sensitive.
   Returns 1 if the keyword exists, 0 if it does not.
   """    
   name = convert_2_str(name)

   if crate is None:
      raise ValueError("No input Crate specified.")
   
   if type(name) not in (np.string_ , str):
      raise IndexError("Please specify key by name.")
   
   crtype = get_crate_type(crate) 
   if crtype != 'Table' and crtype != 'Image':
      raise TypeError("Input Crate must be a TABLE or an IMAGE.")
   
   dat = crate.key_exists( name )
   
   return dat

# ----------------------------------------------------------------------

def get_col_names(crate, vectors=True, rawonly=True ):
   """
   get_col_names(<crate>, vectors=True, rawonly=True )

   Returns names of the columns contained in the input Crate.

   Arguments:
           vectors   - flag to control format for vector columns.
                        True:  shows vector notation.
                        False: displays individual component columns.
                        default = True
           rawonly   - flag for including/excluding virtual columns
                        True:  exclude virtual columns
                        False: include virtual columns
                        default = False
   """    

   if crate is None:
      raise ValueError("No input Crate specified.")
   
   if get_crate_type(crate) != 'Table':
      raise TypeError("Input Crate must be a TABLE.")
   
   return crate.get_colnames(vectors, rawonly)


# ----------------------------------------------------------------------

def get_key_names(crate):
   """
   get_key_names(<crate>)
   
   Returns names of the keywords contained in the input Crate.
   """    
   
   if crate is None:
      raise ValueError("No input Crate specified.")
   
   crtype = get_crate_type(crate) 
   if crtype != 'Table' and crtype != 'Image':
      raise TypeError("Input Crate must be a TABLE or an IMAGE.")
   
   names = crate.get_keynames()
   
   return names

# ----------------------------------------------------------------------

def get_number_cols(crate):
   """
   get_number_cols(<crate>)
   
   Returns number of the columns in the input Crate.
   """    
   
   if crate is None:
      raise ValueError("No input Crate specified.")
   
   crtype = get_crate_type(crate) 
   if crtype != 'Table' and crtype != 'Image':
      raise TypeError("Input Crate must be a TABLE or an IMAGE.")
   
   if crtype == 'Table':
      return crate.get_ncols()
   
   elif crtype == 'Image':
      return 1
   
   
# ----------------------------------------------------------------------

def get_number_rows(crate):
   """
   get_number_rows(<crate>)
   
   Returns number of the rows in the input Crate.
   """    
   if crate is None:
      raise ValueError("No input Crate specified.")
   
   crtype = get_crate_type(crate) 
   if crtype != 'Table' and crtype != 'Image':
      raise TypeError("Input Crate must be a TABLE or an IMAGE.")
   
   if crtype == 'Table':
      return crate.get_nrows()
   
   elif crtype == 'Image':
      return 1


# ----------------------------------------------------------------------

def get_piximg_shape(crate):
   """
   get_piximg_shape(<crate>)
   
   Returns an array providing the image dimensions.
   """     
   if crate is None:
      raise ValueError("No input Crate specified.")
   
   if get_crate_type(crate) != 'Image':
      raise TypeError("Input Crate must be an IMAGE.")
   
   dat = crate.get_shape()
   if dat is None:
      raise RuntimeError("Unable to get dimensional array.")
   
   return dat

# ----------------------------------------------------------------------

def get_colvals(crate, item ):
   """
   get_colvals(<crate>, name)
   get_colvals(<crate>, colno)
   
   Returns an array of column values.
   """    
   item = convert_2_str(item)

   if crate is None:
      raise ValueError("No input Crate specified.")
   
   if get_crate_type(crate) != 'Table':
      raise TypeError("Input Crate must be a TABLE.")
   
   if isinstance_int(item):
      tmp_item = int(item)
   elif type(item) in (np.string_, str):
      tmp_item = item
   else:
      raise IndexError("Please specify column by name or number.")
   
   col = crate.get_column(tmp_item)
   if col is None:
      raise RuntimeError("Column not found.")
   
   return col.values

# ----------------------------------------------------------------------

def copy_colvals(crate, item ):
   """
   copy_colvals(<crate>, name)
   copy_colvals(<crate>, colno)
   
   Returns an array of copied column values.
   """    
   item = convert_2_str(item)

   if crate is None:
      raise ValueError("No input Crate specified.")
   
   if get_crate_type(crate) != 'Table':
      raise TypeError("Input Crate must be a TABLE.")
   
   if isinstance_int(item):
      tmp_item = int(item)
   elif type(item) in (np.string_, str):
      tmp_item = item
   else:
      raise IndexError("Please specify column by name or number.")
   
   col = crate.get_column(tmp_item)
   if col is None:
        raise LookupError("Column not found.")
   
   dat = col.values.copy()
   
   return dat


# ----------------------------------------------------------------------

def set_colvals(crate, item, data):
   """
   set_colvals(<crate>, name, data)
   set_colvals(<crate>, colno, data)
   
   Assigns the input data array as the value for the specified column
   within the given Crate.
   """     
   item = convert_2_str(item)

   if crate is None:
      raise ValueError("No input Crate specified.")
   
   if get_crate_type(crate) != 'Table':
      raise TypeError("Input Crate must be a TABLE.")
   
   if isinstance_int(item):
      tmp_item = int(item)
   elif type(item) in (np.string_, str):
      tmp_item = item
   else:
      raise IndexError("Please specify column by name or number.")
   
   col = crate.get_column(tmp_item)
   if col is None:
      raise LookupError("Column not found.")
   
   if "array" not in str(type(data)):
      data = array(data)
      
   col.values = data

   return

# ----------------------------------------------------------------------

def get_keyval(crate, item ):
    """
    get_keyval(<crate>, name)
    get_keyval(<crate>, keyno)

    Returns the value of the specified keyword.
    Keyword may be specified by name or number.
    """     
    item = convert_2_str(item)

    if crate is None:
        raise ValueError("No input Crate specified.")

    crtype = get_crate_type(crate) 
    if crtype != 'Table' and crtype != 'Image':
        raise TypeError("Input Crate must be a TABLE or an IMAGE.")

    if isinstance_int(item):
        tmp_item = int(item)
    elif type(item) in (np.string_, str):
        tmp_item = item
    else:
       raise IndexError("Please specify keyword by name or number.")


    key = crate.get_key(tmp_item)
    if key is None:
        raise LookupError("Key not found.")

    dat = key.value

    return dat

# ----------------------------------------------------------------------

def set_key(crate, name, data, unit=None, desc=None):
   """
   set_key(<crate>, name, data, unit=None, desc=None)
   
   Updates the specified keyword with the input data value, unit, and 
   description within the given Crate.  If the keyword does not exist, 
   a new one will be created and added to the Crate.

   Optional Arguments:
       unit    - the keyword units
                 default = None

       desc    - the keyword description
                 default is None
                 
   """  
   name = convert_2_str(name)
   
   if crate is None:
      raise ValueError("No input Crate specified.")
   
   crtype = get_crate_type(crate) 
   if crtype != 'Table' and crtype != 'Image':
      raise TypeError("Input Crate must be a TABLE or an IMAGE.")
   
   if type(name) not in (np.string_, str):
      raise IndexError("<Key|Crate> name must be a string.")
   
   key = crate.get_key(name)
   if key is None:
      key = CrateKey() 
      key.name = name
      crate.add_key(key)
      
   key.value = data

   if unit is not None:
      key.unit = unit
   if desc is not None:
      key.desc = desc

   return

# ----------------------------------------------------------------------

def set_keyval(crate, item, data):
   """
   set_keyval(<crate>, name, data)
   set_keyval(<crate>, keyno, data)
   
   Assigns the input data value as the value for the specified keyword
   within the given Crate.  Keyword may be specified by name or number.
   """     
   item = convert_2_str(item)

   if crate is None:
      raise ValueError("No input Crate specified.")
   
   crtype = get_crate_type(crate) 
   if crtype != 'Table' and crtype != 'Image':
      raise TypeError("Input Crate must be a TABLE or an IMAGE.")

   if isinstance_int(item):
      tmp_item = int(item)
   elif type(item) in (np.string_, str):
      tmp_item = item
   else:
      raise IndexError("Please specify keyword by name or number.")


   key = crate.get_key(tmp_item)
   if key is None:
      raise LookupError("Key not found.")
   
   key.value = data
   
   return

# ----------------------------------------------------------------------


def get_piximgvals(crate):
   """
   get_piximgvals(<crate>)
   
   Returns the image data array.
   """     
   if crate is None:
      raise ValueError("No input Crate specified.")
   
   if get_crate_type(crate) != 'Image':
      raise TypeError("Input Crate must be an IMAGE.")
   
   img = crate.get_image()
   if img is None:
      raise LookupError("Image not found.")
   
   return img.values

# ----------------------------------------------------------------------

def copy_piximgvals(crate):
   """
   copy_piximgvals(<crate>)
   
   Returns a copy of the image data array.
   """     
   if crate is None:
      raise ValueError("No input Crate specified.")
   
   if get_crate_type(crate) != 'Image':
      raise TypeError("Input Crate must be an IMAGE.")
   
   img = crate.get_image()
   if img is None:
      raise LookupError("Image not found.")
   
   dat = img.values.copy()
   
   return dat

# ----------------------------------------------------------------------

def set_piximgvals(crate, data):
   """
   set_piximgvals(<crate>, data)
   
   Sets the image values to input data array.
   """     
   if crate is None:
      raise ValueError("No input Crate specified.")
   
   if get_crate_type(crate) != 'Image':
      raise TypeError("Input Crate must be an IMAGE.")
   
   img = crate.get_image()
   if img is None:
      raise LookupError("Image not found.")
   
   img.values = data
   
   return

# ----------------------------------------------------------------------

def read_file(filename, mode='r'):
   """
   read_file(filename, mode)
   
   Opens the specified file, reads in the file and returns
   a TABLECrate or an IMAGECrate.
   
   Argument:
        mode - indicates which mode the file should be opened in
               'r'  = read-only (default)
               'rw' = readable and writeable
   """
   filename = convert_2_str(filename)

   if filename is None:
      raise ValueError("No filename specified.")
   
   if type(filename) not in (np.string_, str):
      raise TypeError("Please specify file name as string.")
   
   tmp_filename = str(filename)	#low level io accepts str only
   
   try:
      crate = TABLECrate( tmp_filename, mode=mode )
   except:
      try:
         crate = IMAGECrate( tmp_filename, mode=mode )
      except:
         raise
      pass

   return crate
   
# ----------------------------------------------------------------------


def read_pha(filename, mode='r'):
   """
   read_pha(filename, mode)
   
   Load an PHA file and return a PHACrateDataset.
   
   Argument:
        mode - indicates which mode the file should be opened in
               'r'  = read-only (default)
               'rw' = readable and writeable
   """
   filename = convert_2_str(filename)

   if filename is None:
      raise ValueError("No filename specified.")

   if type(filename) not in (np.string_, str):
      raise TypeError("Please specify file name as string.")
   
   tmp_filename = str(filename)    #low level io accepts str only
   
   crate = PHACrateDataset( tmp_filename, mode=mode )
   
   return crate

# ----------------------------------------------------------------------

def read_rmf(filename, mode='r'):
   """
   read_rmf(filename, mode)
   
   Load an RMF file and return an RMFCrateDataset.

   Argument:
        mode - indicates which mode the file should be opened in
               'r'  = read-only (default)
               'rw' = readable and writeable
   """     
   filename = convert_2_str(filename)

   if filename is None:
      raise ValueError("No filename specified.")
   
   if type(filename) not in (np.string_, str):
      raise TypeError("Please specify file name as string.")

   tmp_filename = str(filename)    #low level io accepts str only
   
   crate = RMFCrateDataset( tmp_filename, mode=mode )
   
   return crate

# ----------------------------------------------------------------------

def read_dataset(filename):
   """
   read_dataset(filename)
   
   Opens the specified file, reads in the entire dataset and returns
   the CrateDataset.
   """
   filename = convert_2_str(filename)

   if filename is None:
       raise ValueError("No filename specified.")

   if type(filename) not in (np.string_, str):
      raise TypeError("Please specify file name as string.")
   
   tmp_filename = str(filename)    #low level io accepts str only
   
   crateds = CrateDataset(tmp_filename)
   
   return crateds

# ----------------------------------------------------------------------

def write_file(crate, filename, clobber=False, history=True):
   """
   write_file(<crate>, filename, clobber=False, history=True)
   
   Writes specified crate to output file.
   """     
   filename = convert_2_str(filename)

   if crate is None:
      raise ValueError("No input Crate specified.")
   
   if get_crate_type(crate) is None :
      raise TypeError("Input Crate must be a Dataset, TABLE, PHA, RMF, or IMAGE.")
   
   if filename is None:
      raise ValueError("No filename specified.")

   if type(filename) not in (np.string_, str):
      raise TypeError("Please specify file name as string.")
   
   tmp_filename = str(filename)    #low level io accepts str only
   
   crate.write(outfile=tmp_filename, clobber=clobber, history=history)

# ----------------------------------------------------------------------

def write_pha(crate, filename, clobber=False, history=True):
   """
   write_pha(<dataset>, filename, clobber=False, history=True)
   
   Writes a PHACrateDataset to an output file.
   """      
   filename = convert_2_str(filename)

   if is_pha(crate):
      if type(filename) not in (np.string_, str):
         raise TypeError("Please specify file name as string.")
       
      tmp_filename = str(filename)    #low level io accepts str only
      write_file(crate, tmp_filename, clobber=clobber, history=history)
   else:
      raise TypeError("Input Crate must be a PHACrateDataset.")
    
    
# ----------------------------------------------------------------------

def write_pha1_files(pha_crateds, root_filename, clobber=False, history=True):
   """
   write_pha1_files(<dataset>, root_filename, clobber=False, history=True)
   
   Writes each row in a PHA Type:II file as a PHA Type:I file.
   Using the input root filename, files will be named in the format:
         <root_filename>_<row_number>.fits
   """      
   root_filename = convert_2_str(root_filename)

   if is_pha(pha_crateds):
      if type(root_filename) not in (np.string_, str):
         raise TypeError("Please specify file name as string.")
       
      tmp_root_filename = str(root_filename)    #low level io accepts str only
      
      pha_crateds.write_pha1_files(tmp_root_filename, clobber=clobber, history=history)
   else:
      raise TypeError("Input Crate must be a PHACrateDataset.")
    
# ----------------------------------------------------------------------
    
def write_row_as_pha1( pha_crateds, rowno, filename, clobber=False, history=True ):
   """
   write_row_as_pha1( <dataset>, rowno, filename, clobber=False, history=True ):
   
   Writes a PHA Type:II row as a PHA:Type:I dataset to filename specified.
   """
   filename = convert_2_str(filename)

   if is_pha(pha_crateds):
      if type(filename) not in (np.string_, str):
         raise TypeError("Please specify file name as string.")
      
      tmp_filename = str(filename)    #low level io accepts str only
      
      if not isinstance_int(rowno):
         raise TypeError("Please specify row as integer.")
      
      pha_crateds.write_pha1_dataset(rowno, tmp_filename, clobber=clobber, history=history)
   else:
      raise TypeError("Input Crate must be a PHACrateDataset.")


# ----------------------------------------------------------------------

def convert_row_to_pha1( pha_crateds, rowno ):
   """
   convert_row_to_pha1( <dataset>, rowno )
   
   Returns a PHA Type:II row as a PHA Type:I dataset
   """
   
   if not is_pha(pha_crateds):
      raise TypeError("Input Crate must be a PHACrateDataset.")
   
   if not isinstance_int(rowno):
      raise TypeError("Input row number must be numeric.")
   
   pha = pha_crateds.convert_row_to_pha1(rowno)

   return pha

# ----------------------------------------------------------------------

def write_rmf(crate, filename, clobber=False, history=True):
   """
   write_rmf(<crate>, filename, clobber=False, history=True)
   
   Writes an RMFCrate to an output file.
   """        
   filename = convert_2_str(filename)

   if is_rmf(crate):
      write_file(crate, filename, clobber=clobber, history=history)
   else:
      raise TypeError("Input Crate must be an RMFCrateDataset.")
   
# ----------------------------------------------------------------------
   
def write_dataset(crate, filename, clobber=False, history=True):
   """
   write_dataset(<crate>, filename, clobber=False, history=True)
   
   Writes an entire dataset to an output file.
   """        
   filename = convert_2_str(filename)

   if crate is None:
      raise ValueError("No input Crate specified.")
   
   crtype = get_crate_type( crate )
   if crtype.find("DS") == -1:
      raise TypeError("Input must be a CrateDataset.")
   
   write_file(crate, filename, clobber=clobber, history=history)

# ----------------------------------------------------------------------

def create_vector_column(parent_name, component_names, source=None):
   """
   create_vector_column(parent_name, cpts_list, source=None)
   
   Creates a vector column along with its component columns.  If source 
   is given, the column becomes a Virtual column.  Function returns the 
   resulting vector column.

   
   Arguments:
       parent_name      = the name of the parent column
       component_names  = python list of component names
       source           = source CrateData object if vector is virtual;
                          default is None
       
   """
   parent_name = convert_2_str(parent_name)

   # check name of parent column
   if len(parent_name) == 0:
      raise ValueError("Please specify name of parent column.")
   
   if type(parent_name) not in (np.string_, str):
      raise TypeError("Please specify parent name as string.")
   
   if source is not None and get_crate_item_type(source) != 'Data':
      raise TypeError("Input source must be a CrateData object.")

   vdim=2

   if len(component_names) == vdim:
      for ii in range(0, vdim):
         if component_names is not None and type(component_names[ii]) not in (np.string_, str):
            raise TypeError("Please specify component names as strings.")
   else:
      raise ValueError("Vector columns require " + str(vdim) + " component names.")

   # create parent column
   cd = CrateData()
   cd.name = parent_name
   cd.vdim = vdim

   # change parent column to Virtual
   if source:
      cd.source = source
      cd._set_eltype(VIRTUAL)

   # create component list
   cptslist = OrderedDict()
   
   # create vector components and add to list
   for ii in range(0, vdim):
      cpt = CrateData()
      cpt.name = component_names[ii].upper()
      cpt.parent = cd
      cpt.vdim = 1

      if source:
         cpt._set_eltype(VIRTUAL)

      cptslist[cpt.name] = cpt

   # add components list to parent column
   cd._set_cptslist(cptslist)

   # return vector column
   return cd

# ----------------------------------------------------------------------

def create_virtual_column(source_cd, virtual_name, component_names, transform ):
   """
   create_virtual_column(source_cd, virtual_name, component_names, transform)
   
   Creates a Virtual column or Virtual vector column and attaches the given 
   transform and source data.  Function returns the resulting Virtual column.

   Arguments:
       source_cd        = source CrateData object
       virtual_name     = the name of the Virtual column
       component_names  = python list of component names if Virtual 
                          column is also a vector
       transform        = either LINEAR, LINEAR2D, or WCS Transform object

   """
   virtual_name = convert_2_str(virtual_name)

   # check CrateData type
   if source_cd is None:
      raise ValueError("No input CrateData specified.")

   if get_crate_item_type(source_cd) != 'Data':
      raise TypeError("Input source must be a CrateData object.")

   # check name of virtual column
   if len(virtual_name) == 0:
      raise ValueError("Please specify name of Virtual column.")

   if type(virtual_name) not in (np.string_, str):
      raise TypeError("Please specify parent name as string.")

   # check Transform type
   if type(transform) not in [LINEAR2DTransform, LINEARTransform, WCSTransform]:
      raise TypeError("Input Transform must be of type LINEAR2DTransform, LINEARTransform, or WCSTransform.")

   # if LINEAR   
   if isinstance(transform, LINEARTransform):
      #create VIRTUAL Scalar
      virtual_cd = CrateData()
      virtual_cd.name = virtual_name
      virtual_cd._set_eltype(VIRTUAL)
      virtual_cd.source = source_cd
      
   # if LINEAR2D
   # if WCS
   if type(transform) in [LINEAR2DTransform, WCSTransform]:

      if len(component_names) != 2:
         raise TypeError("LINEAR2DTransforms and WCSTransforms require two components.")

      # create VIRTUAL Vector
      virtual_cd = create_vector_column(virtual_name, component_names, source_cd)


   # set transform
   virtual_cd.set_transform(transform)

   # return virtual column
   return virtual_cd

# ----------------------------------------------------------------------


def get_transform(crate, name):
   """
   get_transform(<crate>, name)
   
   Scans the input crate for a data transform of the specified name.
   The name is provided as a string and checks are case sensitive.
   Returns the requested transform if available.
   
   """     
   name = convert_2_str(name)

   if crate is None:
      raise ValueError("No input Crate specified.")
   
   crtype = get_crate_type(crate) 
   if crtype != 'Table' and crtype != 'Image':
      raise TypeError("Input Crate must be a TABLE or an IMAGE.")
   
   if type(name) not in (np.string_, str):
      raise IndexError("Please specify transform by name.")
   
   trans = crate.get_transform( name )
   
   return trans

# ----------------------------------------------------------------------

def get_axis_transform(crate, category):
   """
   get_axis_transform(<crate>, category)
   
   Scans input image crate for the 'default' axis transform of the
   specified category and returns it.  Category is provided as a
   string with allowed values of 'WORLD', 'PHYSICAL', and 'LOGICAL'
   (not case sensitive).
   """
   category = convert_2_str(category)

   if crate is None:
      raise ValueError("No input Crate specified.")
   
   if get_crate_type(crate) != 'Image':
      raise TypeError("Input Crate must be an IMAGE.")
   
   if type(category) not in (np.string_, str):
      raise ValueError("Please specify transform category.")
   
   tstr = category.upper()

   if tstr not in ['WORLD', 'PHYSICAL', 'LOGICAL']:
      raise LookupError("Invalid transform category.")
   
   trans = None
   name  = None
   
    # Get list of axis names from image.
   axisList = crate.get_axisnames()
   
   if axisList is None:
      raise RuntimeError('Input Crate does not contain any axis definitions.')
   else:
      # match 'default' name to list based on category.
      if tstr == "WORLD":
         trans = crate.get_transform("EQPOS")
         
      elif tstr == "PHYSICAL":
         try:
            trans = crate.get_transform("POS")
         except KeyError:
            trans = crate.get_transform("SKY")
            pass
         except:
            raise LookupError("Unable to find physical coordinates.")
         
   return trans

# ----------------------------------------------------------------------

def get_transform_matrix(crate, name):
   """
   get_transform_matrix(<crate>, name)
   
   Returns transform matrix.
   """     
   name = convert_2_str(name)

   if crate is None:
      raise ValueError("No input Crate specified.")
   
   crtype = get_crate_type(crate) 
   if crtype != 'Table' and crtype != 'Image':
      raise TypeError("Input Crate must be a TABLE or an IMAGE.")
   
   if type(name) not in (np.string_, str):
      raise ValueError("Please specify transform name.")
   
   trans = get_transform( crate, name )
   if trans is None:
      raise LookupError("Unable to find transform.")
   
   matrix = trans.get_transform_matrix()
   
   return matrix


# ----------------------------------------------------------------------

def get_crate_checksum(crate):
   """
   get_crate_checksum(<crate>)
   
   Calculates and returns the input Crate's checksum. 
   """     
  
   if crate is None:
      raise ValueError("No input Crate specified.")
   
   crtype = get_crate_type(crate) 
   if crtype != 'Table' and crtype != 'Image':
      raise TypeError("Input Crate must be a TABLE or an IMAGE.")
   
   return crate.get_signature()

# ----------------------------------------------------------------------

def update_crate_checksum(crate):
   """
   update_crate_checksum(<crate>)
   
   Calculates the checksum of the input Crate and updates the Crate's
   checksum signature.
   """     
   
   if crate is None:
      raise ValueError("No input Crate specified.")
   
   crtype = get_crate_type(crate) 
   if crtype != 'Table' and crtype != 'Image':
      raise TypeError("Input Crate must be a TABLE or an IMAGE.")
   
   crate.update_signature()
   
   return


# ----------------------------------------------------------------------

def check_crate_modified(crate):
   """
   check_crate_modified(<crate>)
   
   Compares the input Crate's current checksum with its stored checksum.
   Returns 1 if the Crate has been modified, 0 if it has not.
   """     
   
   if crate is None:
      raise ValueError("No input Crate specified.")
   
   crtype = get_crate_type(crate) 
   if crtype != 'Table' and crtype != 'Image':
      raise TypeError("Input Crate must be a TABLE or an IMAGE.")
   
   return crate.is_modified()

# ----------------------------------------------------------------------

def get_cratedata_checksum(cratedata):
   """
   get_cratedata_checksum(<cratedata>)
   
   Calculates and returns the input CrateData's checksum. 
   """     
   
   if cratedata is None:
      raise ValueError("No input CrateData specified.")
   
   if get_crate_item_type(cratedata) != 'Data':
      raise TypeError("Input must be a CrateData.")
   
   return cratedata.get_signature()

# ----------------------------------------------------------------------

def update_cratedata_checksum(cratedata):
   """
   update_cratedata_checksum(<cratedata>)
   
   Calculates the checksum of the input CrateData and updates the 
   CrateData's checksum signature.
   """     
   
   if cratedata is None:
      raise ValueError("No input CrateData specified.")
   
   if get_crate_item_type(cratedata) != 'Data':
      raise TypeError("Input must be a CrateData.")
   
   cratedata.update_signature()
   
   return

# ----------------------------------------------------------------------

def update_all_cratedata_checksums(crate):
   """
   update_all_cratedata_checksums(<crate>)
   
   Calculates the checksums of all the CrateData objects in the input Crate 
   and updates their respective checksum signatures.
   """     
   
   if crate is None:
      raise ValueError("No input Crate specified.")
   
   crtype = get_crate_type(crate) 
   if crtype != 'Table' and crtype != 'Image':
      raise TypeError("Input Crate must be a TABLE or an IMAGE.")
   
   datalist = crate._get_datalist()
   
   for item_name in list(datalist.keys()):
      datalist[ item_name ].update_signature() 
      
   return

# ----------------------------------------------------------------------

def check_cratedata_modified(cratedata):
   """
   check_cratedata_modified(<cratedata>)
   
   Compares the input CrateData's current checksum with its stored checksum.
   Returns 1 if the CrateData has been modified, 0 if it has not.
   """     
   
   if cratedata is None:
      raise ValueError("No input CrateData specified.")
   
   if get_crate_item_type(cratedata) != 'Data':
      raise TypeError("Input must be a CrateData.")
   
   return cratedata.is_modified()

# ----------------------------------------------------------------------

def get_crate(crateds, item):
   """
   get_crate(<cratedataset>, cratename)
   get_crate(<cratedataset>, crateno)
   
   Locate and return the specified crate within the provided CrateDataset.
   Crate may be specified by name or number (1 based).
   """
   item = convert_2_str(item)

   if crateds is None:
      raise ValueError("No input CrateDataset specified.")
   
   crtype = get_crate_type( crateds )
   if crtype.find("DS") == -1:
      raise TypeError("Input must be a CrateDataset.")
   
   if isinstance_int(item):
      tmp_item = int(item)
   elif type(item) in (np.string_, str):
      tmp_item = item
   else:
      raise IndexError("Please specify crate by name or number.")
   
   return crateds.get_crate(tmp_item)


# ----------------------------------------------------------------------

def add_crate(crateds, crate):
   """
   add_crate(<cratedataset>, <crate>)
   
   Adds input Crate to the given CrateDataset.
   """
   
   if crateds is None:
      raise ValueError("No input CrateDataset specified.")
   
   dstype = get_crate_type( crateds )
   if dstype.find("DS") == -1:
      raise TypeError("Input must be a CrateDataset.")
   
   crtype = get_crate_type( crate )
   if crtype != 'Table' and crtype != 'Image':
      raise TypeError("Input Crate must be a TABLE or an IMAGE.")
   
   crateds.add_crate(crate)
   
   return


# ----------------------------------------------------------------------

def delete_crate(crateds, cratename):
   """
   delete_crate(<cratedataset>, cratename)
   
   Removes input Crate to the given CrateDataset.
   """
   cratename = convert_2_str(cratename)

   if crateds is None:
      raise ValueError("No input CrateDataset specified.")
   
   dstype = get_crate_type( crateds )
   if dstype.find("DS") == -1:
      raise TypeError("Input must be a CrateDataset.")
   
   if isinstance_int(cratename):
      tmp_cratename = int(cratename)
   elif type(cratename) in (np.string_, str):
      tmp_cratename = cratename
   else:
      raise IndexError("<Key|Crate> name must be a string or number.")
   
   crateds.delete_crate(tmp_cratename)
   
   return


# ----------------------------------------------------------------------

def add_comment(crate, content):
   """
   add_comment(<crate>, content)

   Adds a Comment record to the end of the CXCHistory list.

   Arguments
       content     - python string or list of strings

   """
   if crate is None:
      raise ValueError("No input Crate specified.")

   crtype = get_crate_type(crate) 
   if crtype != 'Table' and crtype != 'Image':
      raise TypeError("Input Crate must be a TABLE or an IMAGE.")


   crate.add_record("COMMENT", content)
   return

# ----------------------------------------------------------------------

def add_history(crate, content):
   """
   add_history(<crate>, content)

   Adds a History record to the pycrates history list.

   Arguments
       content    - python newline-delimited string, list of strings, or a HistoryRecord


   """
   if crate is None:
      raise ValueError("No input Crate specified.")

   crtype = get_crate_type(crate) 
   if crtype != 'Table' and crtype != 'Image':
      raise TypeError("Input Crate must be a TABLE or an IMAGE.")

   if type(content) in (np.string_, str, list):
      input = content
 
   elif isinstance(content, HistoryRecord):
      try:
         input = content.as_ASC_FITS(with_tag=False, stk_expand=True)
      except:
         input = content.as_FITS(with_tag=False)

   else:
      raise TypeError("Invalid input; content must be newline-delimited string, a list of strings, or a HistoryRecord.")

   crate.add_record("HISTORY", input)
   return

# ----------------------------------------------------------------------

def add_record(crate, tag, content):
   """
   add_record(<crate>, tag, content)

   Adds a record to the end of the CXCHistory list.  

   Arguments
       tag      - valid options are "HISTORY" and "COMMENT"
       content  - python string or list of strings

   Example:
       1) add_record("HISTORY", ["This is a non-ASCFITS compliant history record"])

       2) add_record("COMMENT", "blah blah blah")

       3) histlist = ["HISTORY  TOOL  :dmcopy  2016-08-11T20:27:22                             ASC00041",        
                      "HISTORY  PARM  :infile=/export/proc/tmp.ascdsl3/prs_run/tmp//L3AMRF____5ASC00042",
                      "HISTORY  CONT  :87231988n944/output/acisf05017_000N022_r0001b_rayevt3.fiASC00043",
                      "HISTORY  CONT  :ts[sky=bounds(region(/export/proc/tmp.ascdsl3/prs_run/tmASC00044",
                      "HISTORY  CONT  :p//L3AMRF____587231988n944/input/acisfJ0331514m274138_00ASC00045",
                      "HISTORY  CONT  :1N021_r0001_srbreg3.fits[bkgreg]))][bin sky=1][opt type=ASC00046",
                      "HISTORY  CONT  :i4]                                                     ASC00047",
                      "HISTORY  PARM  :outfile=/export/proc/tmp.ascdsl3/prs_run/tmp//L3AMRF____ASC00048",
                      "HISTORY  CONT  :587231988n944/output/acisf05017_000N022_r0001b_fbinpsf3.ASC00049",
                      "HISTORY  CONT  :fits                                                    ASC00050",
                      "HISTORY  PARM  :kernel=default                                          ASC00051",
                      "HISTORY  PARM  :option=image                                            ASC00052",
                      "HISTORY  PARM  :verbose=0                                               ASC00053",
                      "HISTORY  PARM  :clobber=yes                                             ASC00054"]
          add_record("HISTORY", histlist) 

       4) histstr = "First History line \nSecond History line \nThird History line \nFourth History line \nFifth History line"
          add_record("HISTORY", histstr)             

   """


   if tag == "COMMENT":
      add_comment(crate, content)
   elif tag == "HISTORY":
      add_history(crate, content)
   else:
      raise ValueError("Invalid input; tag must be 'HISTORY' or 'COMMENT'.")

   return

# ----------------------------------------------------------------------

def delete_comments(crate):
   """
   delete_comments(<crate>)

   Removes all Comment records from the CXCHistory list.
   """
   if crate is None:
      raise ValueError("No input Crate specified.")

   crtype = get_crate_type(crate) 
   if crtype != 'Table' and crtype != 'Image':
      raise TypeError("Input Crate must be a TABLE or an IMAGE.")

   crate.delete_comment_records()
   return

# ----------------------------------------------------------------------

def delete_history(crate):
   """
   delete_history(<crate>)

   Removes all History records from the CXCHistory list.
   """
   if crate is None:
      raise ValueError("No input Crate specified.")

   crtype = get_crate_type(crate) 
   if crtype != 'Table' and crtype != 'Image':
      raise TypeError("Input Crate must be a TABLE or an IMAGE.")

   crate.delete_history_records()
   return

# ----------------------------------------------------------------------

def get_comment_records(crate):
   """
   get_comment_records(<crate>)

   Returns the CommentRecords stored in the CXCHistory list.
   """
   if crate is None:
      raise ValueError("No input Crate specified.")

   crtype = get_crate_type(crate) 
   if crtype != 'Table' and crtype != 'Image':
      raise TypeError("Input Crate must be a TABLE or an IMAGE.")

   return crate.get_comment_records()

# ----------------------------------------------------------------------

def get_history_records(crate):
   """
   get_history_records(<crate>)

   Returns the HistoryRecords stored in the CXCHistory list.
   """
   if crate is None:
      raise ValueError("No input Crate specified.")

   crtype = get_crate_type(crate) 
   if crtype != 'Table' and crtype != 'Image':
      raise TypeError("Input Crate must be a TABLE or an IMAGE.")

   return crate.get_history_records()

# ----------------------------------------------------------------------

def get_all_records(crate):
   """
   get_all_records(<crate>)

   Returns all records, both Comments and History, stored in the CXCHistory list.
   """
   if crate is None:
      raise ValueError("No input Crate specified.")

   crtype = get_crate_type(crate) 
   if crtype != 'Table' and crtype != 'Image':
      raise TypeError("Input Crate must be a TABLE or an IMAGE.")

   return crate.get_all_records()

# ----------------------------------------------------------------------

def print_full_header(crate):
   """
   print_full_header(<crate>)

   Prints the entire header.  This includes standard keywords interleaved with 
   History and Comment keywords.
   """
   if crate is None:
      raise ValueError("No input Crate specified.")

   crtype = get_crate_type(crate) 
   if crtype != 'Table' and crtype != 'Image':
      raise TypeError("Input Crate must be a TABLE or an IMAGE.")

   return crate.print_full_header()

# ----------------------------------------------------------------------

def print_history(crate):
   """
   print_history(<crate>)

   Prints the string representation of the CXCHistory list.
   """
   if crate is None:
      raise ValueError("No input Crate specified.")

   crtype = get_crate_type(crate) 
   if crtype != 'Table' and crtype != 'Image':
      raise TypeError("Input Crate must be a TABLE or an IMAGE.")

   return crate.print_history()

# ----------------------------------------------------------------------
