# 
#  Copyright (C) 2011-2013,2016-2017,2020-2021  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.
#



# Sept 14, 2010

# caldblib.py
# this file

#------------  Beginning of Template ----------------------
# Support python 2 and 3
from __future__ import print_function

# import the packages
import os, sys
from ctypes import *


"""
a python module to interactivate caldb4-lib C++ functions.
"""
#=============================================================================
#
#   --- Load Library ---
#
#=============================================================================

def loadLibrary(name):
    '''load the shared library and return the handle'''

    DEFAULT_MODE = RTLD_LOCAL
    if os.name == "posix" and sys.platform == "darwin":
        # Helen @ Dec 6 2012
        # On OSX, we use RTLD_GLOBAL as default mode
        # because RTLD_LOCAL does not work at least on
        # the following sequence of importing libraries:
        # 
        # import caldb4, pixlib
        # 
        # where a "Expected flat namespace" error occurs when 
        # both caldb4lib.py and pixlib.py link to the run-time shared library, libcaldb4.dylib
        # similar issues discussed elsewhere, like
        # http://mail.python.org/pipermail/python-bugs-list/2002-February/010167.html
        # http://forums.macrumors.com/archive/index.php/t-639889.html
        # or
        # /proj/xena/ciaod/osx64/ciao_1205/ots/lib/python2.7/ctypes/__init__.py

        DEFAULT_MODE = RTLD_GLOBAL

 
    # simplify the lib-name
    if sys.platform[:3] in ('dar', 'os2'): #Mac
        libname = 'lib%s.dylib' % name
    else: # assume UNIX-like
        libname = 'lib%s.so' % name

    try:
        # throw KeyError exccpetion if the key not defined
        libpath=os.environ['ASCDS_INSTALL'] + '/lib'


        # loooking up the named shared library
        # use 'CDLL' from ctypes.util
        # returns the pathname of a library, or None.
        abslibname=libpath +'/'+ libname

        # throw an EnvironmentError exception if the lib-so not found
        libso = CDLL(abslibname, DEFAULT_MODE)
        return libso
      
    except KeyError as err:
        raise KeyError("failed to retrieve Env. variable {}.".format(str(err)))
    except Exception as err:
        raise ImportError("{}.".format(str(err)))

    
try:
    _libc   =  loadLibrary( 'caldb4' )
except Exception:
    raise


#
# _libds 
# to be loaded in _maphdr_telins with loadLibrary(('ds',))
# "libds.so" is CXCDS libraray. we need it only when
# we execute auto-querying via a input-file
#

#-----------------------------
# 32-bit vs 64-bit machines
#----------------------------
# ctypes defines a number of primitive C compatible data types, see
# http://docs.python.org/library/ctypes.html
# The ctypes interface by default only handles Python integer, long, and string data types.
# If you use float or something else then you must tell ctypes how to convert the C function call arguments and result value
# in prefix of 'argtypes' and 'restype' .
#
# By default, ctypes assumes that all functions return int, which is 32 bits. But, on a 64-bit system,
# a pointer is 64 bits. If you don't tell the ctypes to expect a pointer, you're dropping the upper 32 bits
# from the pointer return value, so you then get a segfault.
#

_libc.calInit.argtypes  = [c_char_p,c_char_p]
_libc.calInit.restype   = c_void_p
_libc.calClose.argtypes = [c_void_p]
_libc.calClose.restype  = None

_libc.calSetProduct.argtypes   = [c_void_p,c_char_p]
_libc.calSetProduct.restype    = c_void_p

_libc.calSearch.argtypes       = [c_void_p]
_libc.calSearch.restype        = c_int
    
_libc.calSetParam.argtypes     = [c_void_p,c_char_p,c_char_p,c_char_p]      
_libc.calSetParam.restype      = c_int
_libc.calSetStartTime.argtypes = [c_void_p,c_char_p]
_libc.calSetStartTime.restype  = c_int
_libc.calSetStopTime.argtypes  = [c_void_p,c_char_p]
_libc.calSetStopTime.restype   = c_int


_libc.calGetParam.argtypes     = [c_void_p,c_int, c_int]
_libc.calGetParam.restype      = c_char_p
_libc.calGetParamUnit.argtypes = [c_void_p,c_int]
_libc.calGetParamUnit.restype  = c_char_p

_libc.calGetPValue.argtypes      = [c_void_p, c_char_p]
_libc.calGetPValue.restype       = c_char_p
_libc.calGetPUnit.argtypes       = [c_void_p, c_char_p]
_libc.calGetPUnit.restype        = c_char_p

_libc.calGetWhichParams.argtypes = [c_void_p]
_libc.calGetWhichParams.restype  = c_int


_libc.calGetFile.argtypes      = [c_void_p,c_int]
_libc.calGetFile.restype       = c_char_p
_libc.calGetFileExtno.argtypes = [c_void_p,c_int]
_libc.calGetFileExtno.restype  = c_int

_libc.calGetErrNum.argtypes   = [c_void_p]
_libc.calGetErrNum.restype    = c_int
_libc.calGetErrCur.argtypes   = [c_void_p]
_libc.calGetErrCur.restype    = c_char_p
_libc.calGetWarnCur.argtypes  = [c_void_p]
_libc.calGetWarnCur.restype   = c_char_p
_libc.calPrintErrCur.argtypes = [c_void_p]
_libc.calPrintErrCur.restype  = None


_libc.calGetVersion.argtypes  = []
_libc.calGetVersion.restype   = c_char_p

_libc.calPrintInfo.argtypes   = [c_void_p]
_libc.calPrintInfo.restype    = None
_libc.calSetVerbose.argtypes  = [c_void_p, c_int]




#=============================================================================
#
#   handle fs-string, bytes-string, and unicode-string for Py3 migration, incl
#    convert_to_ascii()
#    isstring()
#
#=============================================================================

def isstring( val ):
    """ return true if it is string in fs-string or unicode-str or bytes-strin """

    if sys.version_info.major >= 3:     # for Python 3, unicode and fs-string are same, bytes not recognizable
        if isinstance(val, str) or isinstance(val, bytes):
            return True
    else:                               # for Python 2, bytes and fs-string are same, unicode detecable
        if isinstance(val, str) or isinstance(val, unicode)  or isinstance(val, bytes):
            return True

    return False

def convert_to_ascii( val, check_str=False ):
    """  convert bytes-string and unicode-string to ascii-string and return the ascii.   """
    if check_str:
        if not isstring(val):
            return val #raise caldbError("Error: not strings  at '" + str(val)+"'.")

    if sys.version_info.major >= 3:     # for Python 3, unicode and sf-string are same, bytes not recognizable
        if isinstance(val, bytes) :
            val=val.decode()
    val=val.strip()
    return str(val)

  

def convert_to_ascii_dictval( A ):
    for ak in A.keys():
        av=A[ak]
        if isinstance(av, tuple):
            v1,v2=av
            if isstring(v1):
                v1 = convert_to_ascii(v1, False)
            if isstring(v2):
                v2 = convert_to_ascii(v2, False)
            A[ak] = v1,v2
        elif isstring(av):
            A[ak] = convert_to_ascii(av, False)  
     
    return A
  


#=============================================================================
#
#   --- The Exception/Error handling Class ---
#
#=============================================================================

class caldbError(Exception):
  """
  caldb exception and error-handling class
  """
  def __init__(self, value):
    self.value = convert_to_ascii(value, True)
  def __str__(self):
    return repr(str(self.value))


#=============================================================================
#
#   --- The caldb4-lib interface Class ---
#
#=============================================================================
class CDBlib:
  """
  Interface with caldb c-library functions
  """

  #
  # override the default __str__ and __repr__
  #
  
  def __str__( self): 
      return "CDBlib:\n\ttelescope = "+self.telescope +"\n\tinstrume  = "+self.instrume +"\n\tproduct   = "+self.product +"\n\tinfile    = "+self.infile
  def __repr__(self): return "CDBlib instance at address of " + hex(id(self))
  

  def __init__(self, telescope="chandra", instrume="", infile=""):
    

    self.libcptr  =None
    self.srchptr  =None
    self.maphdr   =None
    self.infile   =infile
    self.errmsg   =None

    try:
        #-------------------------------------------------
        # infile overwrites telescope and instrume inputs
        #--------------------------------------------------
        if isstring(infile): 
            infile = convert_to_ascii(infile)      # convert to ascii string
            self.infile  = infile
        else:
            raise caldbError("Error: infile type should be string.")

        if not infile=="":  # have an effective 'infile' input
            (telescope, instrume) = self._maphdr_telins(infile)
            if telescope == "":
                raise caldbError(self.errmsg)
        else:                                   # have no  effective 'infile' input and 
            if isstring(telescope):             # look up telescope
                telescope=convert_to_ascii(telescope)            
            else:                               # is error if 'telescope' is not effective input
                telescope=""
            
            if isstring(instrume):              # look up instrume
                instrume=convert_to_ascii(instrume)
            else:                               # assign empty string for other input types
                instrume=""

        if telescope == "":                    # error out
            raise caldbError("Error: cannot launch caldb without specifying telecope or infile.\n")    
    
        if instrume == "":                     # okay
            self.libcptr = _libc.calInit(telescope.encode(), None)  
        else:
            self.libcptr = _libc.calInit(telescope.encode(), instrume.encode())  

        if not self.libcptr:
            raise caldbError( _libc.calGetErrCur(None)  ) # None is passed as a C NULL pointer

        _libc.calSetVerbose( self.libcptr, 0)

        self.telescope = telescope
        self.instrume  = instrume 
        return 1                    # no error
    except caldbError as e:
        self.errmsg=e.value
        return 0

  
  def _close(self):
    "Close the caldb module"
    _libc.calClose(self.libcptr)
    
    self.libcptr = None
    self.srchptr = None

   
  # immutable keywords/parameters List
  def _notUpdatable(self, name):
    keywords_imtbl = ["telescope", "instrument", "instrume"]  #immutable keywords list
    lname=name.lower()
    if lname in keywords_imtbl:
      return True
    return False

  def _setProduct(self, product):
    """Set data for querying"""

    try:
        if product is None:
            raise caldbError("Error: 'product' in Null/None.")
        if not isstring(product):
            raise caldbError("Error: 'product' in non-string.")
       
        product=convert_to_ascii(product)
        if product=="" or len(product)==0:
            raise caldbError("Error: empty 'product' string.")

        self.srchptr = _libc.calSetProduct(self.libcptr, product.encode())
        enum =_libc.calGetErrNum(self.libcptr)
        if enum != 0 or not self.srchptr:
            raise caldbError( _libc.calGetErrCur(self.libcptr) )

        self.product = product
        return 1  # okay
    except  caldbError as e:
        self.errmsg = e.value   # eror catched in _setProd() of caldb4.py
        return 0 # not okay
    except:
        self.errmsg = "Error: unexpected failure in calSetProduct() ."
        return 0
 
  def _setParam(self, name,val,unit=""):
 
      if not name or not isstring(name) or name.strip()=="":
          return 1  # do nothing
      if isstring(val) and val.strip()=="":
          return 1  # do nothing
      
      try:
          if isstring(val) and val.strip() != "":
              val=convert_to_ascii(val)  # return ascii string
          else:
              val=str(val)

          if isstring(unit) and unit.strip !="":
              unit=convert_to_ascii(unit)
          else:
              unit=str(unit)

          estat = _libc.calSetParam(self.srchptr,name.encode(),val.encode(),unit.encode())
          enum  = _libc.calGetErrNum(self.libcptr)
          if enum != 0 or estat != 0 :
              raise caldbError( _libc.calGetErrCur(self.libcptr))
              
          return 1   # okay
      except caldbError as e:
          self.errmsg = e.value   # eror catched in _setProd() of caldb4.py
          return 0      # not-okay
      except:
          self.errmsg="Error: unexpected failure in calSetParam()."
          return 0      # not-okay

  def _setStartTime(self, val):
    try:
        if isstring(val) and val.strip() != "":
            val=convert_to_ascii(val)  # return ascii string
        else:
            return 1 # do nothing  
        estat =_libc.calSetStartTime(self.srchptr,val.encode())
        if estat != 0 :
            raise caldbError( _libc.calGetErrCur(self.libcptr) )
    except caldbError as e:
        self.errmsg = e.value  # not-okay
        return 0

    return 1 # okay

  def _setStopTime(self, val):
    try:
        if isstring(val) and val.strip() != "":
            val=convert_to_ascii(val)  # return ascii string
        else:
            return 1 # do nothing  
        estat=_libc.calSetStopTime(self.srchptr,val.encode())
        if estat != 0:
            raise caldbError( _libc.calGetErrCur(self.libcptr) )
    except caldbError as e:
        self.errmsg = e.value   # eror catched in _setProd() of caldb4.py
        return 0 # not okay
 
    return 1 # okay


  def _search(self):
    """
    Start searching for the products of the data
    Return the findings or error message
    """
    
    
    if not self.srchptr:
        self.errmsg="Error: caldb-lib null search pointer."
        return -1

    try:
      #------------------
      # do auto-querying
      #------------------
        if self.maphdr:
            stat = self._maphdr_autopars(self.srchptr)
            if stat == 0:
                return -1

        nfile = _libc.calSearch(self.srchptr)
 
        enum =_libc.calGetErrNum(self.libcptr)
        if enum != 0 :
            raise caldbError( _libc.calGetErrCur(self.libcptr) )          
        return nfile   # number of files in int

    except caldbError as e:
        self.errmsg=e.value
        return -1
    except:
        self.errmsg="Error: unexpected failure in calSearch() ."
        return -1     # not-okay


  def _whichParams(self):
    "Return number of params free from queried"

    
    if not self.srchptr:
        self.errmsg="Error: caldb-lib Null search pointer."
        return []

    try:
        num=_libc.calGetWhichParams(self.srchptr)
        pars=[]
        for indx in range(num):
            _par=_libc.calGetParam(self.srchptr, indx, 0)
            if _par==None or _par.strip()=="":
                raise caldbError( _libc.calGetErrCur(self.libcptr) )
        
            _unit=_libc.calGetParamUnit(self.srchptr, indx)
            n=convert_to_ascii(_par) 
            u=convert_to_ascii(_unit) #u.decode()   # convert to str
            par=n,"",u
            pars.append(par)
        return pars
    except caldbError as e:
        self.errmsg=e.value
        return []
    except :
        self.errmsg="Error: unexpected failure in whichParams() ."
        return []


  def _getFile(self, indx):

    afile=_libc.calGetFile(self.srchptr, indx)
    if not afile or afile.strip()=="":
        return str("")
    extn=_libc.calGetFileExtno(self.srchptr, indx)

    afile=convert_to_ascii(afile)
    fullname=afile+"["+str(extn)+"]"

    return str(fullname)   # return pure ascii string

   
  def extno(self, indx=0):
    """Get resulting FITS file extension number.

    Argument:
    indx -- the index of file in the results queue, default to 0
            or the first file in the stack.

    Return extension number.
    
    """
    
    return _libc.calGetFileExtno(self.srchptr, indx)

  
  def libinfo(self):
    "Print out the current caldb-lib configuration and set-inputs information."

    try:
        if not self.srchptr:
            raise caldbError("Error: caldb-lib Null search pointer.")
        else:
            _libc.calPrintInfo(self.srchptr)

    except caldbError as err:
        self.errmsg = err.value
    except:
        self.errmsg = "Error: unexpected failure in calPrintInfo() ."

  
  def libversion(self):
      """ Return the current caldb-lib software version string."""

      return convert_to_ascii(_libc.calGetVersion(), True)
 
  def _maphdr_telins(self, infile):    

      tele=(c_char*10)()
      inst=(c_char*10)()
        
      global _libds  
      _libds = loadLibrary('ds')
      try:
          if not _libds:
              raise caldbError("Error: failed to get libds CDLL.\n")
  
      #--------------------------------------- 
      # open file and get the file pointer
      # return (telescope, instrume, err)
      #----------------------------------------

          _libds.ds_get_telins.argtypes     = [c_char_p,  c_char_p, c_char_p, c_int ]
          _libds.ds_get_telins.restype      = c_void_p
          self.maphdr=_libds.ds_get_telins(infile.encode(), tele, inst,0)
          if not self.maphdr:  
              raise caldbError("Error: failed to get header or parameter info.\n")

      except caldbError as e:
          self.errmsg = e.value
          return ("","")
          
      return (tele.value.decode(), inst.value.decode())  # return byte to str type


  def _maphdr_autopars(self, calsearch):
      """ 
      map the remained parameters.
      """ 
 
      try:
          _libds.ds_map_hdr_to_caldb4.argtypes = [ c_char_p, c_void_p, c_void_p, c_int ]
          _libds.ds_map_hdr_to_caldb4.restype  = c_int
 
          estat =_libds.ds_map_hdr_to_caldb4( "CALDB".encode(), self.maphdr, calsearch, 0)
          if estat == -1:
              raise caldbError("Error: failed to auto-querying.")
          return 1 # okay
      except Exception as e:
          self.errmsg = e.value
          return 0 # not-okay

def main():
  print ("\nDo test1 ...")
  test1();
  print ("\nDo test2 ...")  
  test2();
  print ("\nDo test3 ...")  
  test3();
  print ("\nDo test3a ...")  
  test3a();
  

#=======================================
# test 1: test returns of 
# __str__  and  __repr__
#========================================
def test1():
  c = CDBlib()
  print (c )               # __str__, return "CDBlib libc at 0x7f93d1ade950"
  print (c.__repr__())     # __repr__, return "CDBlib instance at address of 0x7f93cbbc2488"
  print ("caldb-lib version-%-s" %( (c.libversion()).value ) )
  c._close()

  c = CDBlib("chandra", "acis", "badpix")
  print ("\n",c )
#=======================================
# test 2
#========================================
def test2():
  c = CDBlib("chandra", "acis", "badpix")
  c._setStartTime("2000-01-29T20:00:00")
  c._setStopTime("2000-11-28")
  c.libinfo()
 
  #create some warns
  c._setProduct("badpix")
  c._setParam("telescope","mytel")
  c._setParam("instrument","myinst")
  c.libinfo()
  print ("-- params to be queried:\n", c._whichParams() )
 
  c._close()

#=======================================
# test 3a: the outut shoule be same as the test3's
# 
#========================================
def test3a():
  c = CDBlib("chandra","acis","det_gain")
  c._setStartTime("1999-08-13T01:43:58")
  c._setStopTime("1999-08-13T05:45:44")
  c._setParam("grating","None")
  c.libinfo()
  num = c._search()
  
  if num == 0:
    print ("\nnone file is found")
  else:
    print ("\n%d files are found: " %(num))
    ii=1
    while ii <= num:
      fname = c._getFile(ii-1)#.value
      extno = c.extno(ii-1)
      print ("   %d>  %s  extno=%d" %(ii, fname, extno))
      ii += 1
    print ("\n")
  c._close()

#=======================================
# test 3: get file header keywords of
# telescop and instrume
#========================================
def test3():
  infile="/data/regression_test/development/indata/calquiz/acism62566_000N000_bpix1.fits"
  print ("infile=", infile)
  c = CDBlib(None,None,"det_gain", infile)

  
  c.libinfo()
  num = c._search()
  c.libinfo()
  if num == 0:
    print ("\nnone file is found")
  else:
    print ("\n%d files are found: " %(num))
    ii=1
    while ii <= num:
      fname = c._getFile(ii-1) #.value
      extno = c.extno(ii-1)
      print ("   %d>  %s  extno=%d" %(ii, fname, extno))
      ii += 1
    print ("\n")
  c._close()

 

if __name__ == "__main__":
  main()
