# Internal Gildas-Python binding module:
# - defines special classes for importing SIC variables, and creating
#   array objects pointing to memory already allocated by Gildas,
# - defines commands to communicate with Gildas,
# - defines the Sic object in main dictionary

# 'pygildas' module provides 'mkarray' func which creates instances pointing
# to memory allocated by SIC. Also provides SIC type-codes and routines
# which communicate with SIC.
import pygildas
from   pygildas import maindict
from   sicparse import ambigs
import numpy
if (hasattr(numpy,'unicode_')):
   npunicode = numpy.unicode_  # Numpy < 2.0.0
else:
   npunicode = numpy.str_      # Numpy >= 2.0.0

# === PyGildas specific classes ========================================

# --- Pygildas specific errors ---
class PygildasError(Exception):
   """Standard error for general kind of error concerning Pygildas."""

class PygildasAttributeError(AttributeError):
   """AttributeError for Pygildas specific objects."""

class PygildasNameError(NameError):
   """NameError for Pygildas specific objects."""

class PygildasSyntaxError(SyntaxError):
   """SyntaxError for Pygildas specific commands."""

class PygildasTypeError(TypeError):
   """TypeError for Pygildas specific objects."""

class PygildasValueError(ValueError):
   """ValueError for Pygildas specific objects."""


# --- 'SIC STRUCTURE' class definition ---
class SicStructure:
   """A SicStructure instance."""
   def __init__(self,sicname,level=-1):
      self.__sicname__      = sicname
      self.__siclevel__     = level

   def __str__(self):
      if pygildas.isimage(self.__sicname__,self.__siclevel__) == 1:
         vtype = "header"
      else:
         vtype = "structure"
      if len(vars(self)) == 2:
         string = "<empty %s>" % vtype
      else:
         string = "<%s>" % vtype
         string += printattr(self)
      return string

   def __setattr__(self,attname,value):
      if not hasattr(self,attname) and attname in ('__siclevel__','__sicname__'):
         self.__dict__[attname] = value
      elif not hasattr(self,attname):
         # Att does not exist: define it
         fullname = self.__sicname__+"%"+attname.upper()
         if not pygildas.varexist(fullname,self.__siclevel__):
            # 1) Adding an attribute to a SicStructure augments the
            #    structure from the Sic side.
            define_from_this(fullname,value,self.__siclevel__==0)  # Try only?
            # 2) The corresponding SicVar is attached by a recursive call to
            #    this __setattr__ method, during the 'define' process in Sic.
            # 3) Now, set the value
            self.__dict__[attname].assign(value)
         else:
            self.__dict__[attname] = value
      elif attname in ('__siclevel__','__sicname__'):
         # Att exists and is fundamental: overwriting failure
         raise PygildasAttributeError("Cannot overwrite "+attname+" attribute of a SicVar instance.")
      elif isinstance(self.__dict__[attname],(SicVar,SicStructure)):
         # Att exists and is a SicVar or a SicStructure (in structures/images)
         self.__dict__[attname].assign(value)
      else:
         # Att exists but was manually defined: overwriting success
         # Not sure how to end here...
         self.__dict__[attname] = value

   def __delattr__(self,attname):
      # if isinstance(self.__dict__[attname],(SicVar,SicStructure)):
      #    raise PygildasAttributeError, "Cannot delete SicVar/SicStructure attribute of a SicStructure instance."
      if attname in ('__siclevel__','__sicname__'): # Att is fundamental: deletion failure
         raise PygildasAttributeError("Cannot delete "+attname+" attribute of a SicVar instance.")
      else:
         del self.__dict__[attname]

   # Type-as-string function:
   def chartype(self):
       """Returns a nice string from SIC variable type integer."""
       if pygildas.isimage(self.__sicname__,self.__siclevel__) == 1:
          return "<header>"
       else:
          return "<structure>"

   # Array-shape-as-string function:
   def chararray(self):
       """Returns one SIC array dimensions as a nice string."""
       return "0D"

   # RO-or-RW-as-string function:
   def charro(self):
       """Returns 'RO' or 'RW' string from logical value."""
       if pygildas.isro(self.__sicname__,self.__siclevel__): return "RO"
       else:                                                 return "RW"

   # Level-as-string function:
   def charlevel(self):
       """Returns 'GLOBAL' or 'LOCAL' string from logical value."""
       if (self.__siclevel__ == 0): return "GLOBAL"
       else:                        return "LOCAL"+repr(self.__siclevel__)


# --- 'SIC VARIABLE' class definition ---
# Keep a list of the very specific attributes:
SicVar_attributes = ('__sicdata__','__siclevel__','__sicname__')


class SicVarIterator:
   """Special Class for iterating over a SicVar"""
   def __init__(self,parent):
      self.parent = parent  # Back pointer to the parent SicVar
      self.index  = -1

   def __iter__(self):
      self.index = -1
      return self

   def __next__(self):  # This is a Python 2 requirement
                    # 2to3 converts it to __next__(self)
      self.index += 1
      if (self.index >= len(self.parent)):
        raise StopIteration
      else:
        # Should we return the item value:    self.parent.__sicdata__[self.index]
        # or a SicVar around this item value: self.parent[self.index]  ?
        # Choose to do the same as Python when there was no such iterating Class
        return self.parent.__sicdata__[self.index]


class SicVar:
   """A SicVar instance."""
   def __init__(self,sicname,level,desc=0,subarray=None):
      if subarray is None:
         self.__sicdata__      = pygildas.mkarray(desc)
         self.__sicname__      = sicname
         self.__siclevel__     = level
         self.__sicdata__.setflags(write = not pygildas.isro(sicname,level))
      else:
         self.__sicdata__      = subarray
         self.__sicname__      = sicname
         self.__siclevel__     = level

   def __str__(self):
      string = "<SicVar instance>\n"+repr(self.__sicdata__)
      string += printattr(self)
      return string

   def __repr__(self):
      if pygildas.vartype(self.__sicname__,self.__siclevel__) > 0 and len(self.shape) == 0:
         return repr(self.tounicode())
      elif pygildas.vartype(self.__sicname__,self.__siclevel__) == pygildas.fmtl:
         return repr(bool(self.__sicdata__))
      else:
         return str(self.__sicdata__)

   def assign(self,value):
      """Usage: .assign(value)
      Assign input value. Method will do its best to cast the value to the
      correct type, and to fill the  array  depending  on  the  respective
      dimensions."""
      # Ideally, bound method to assignment (=), but not yet overloadable in
      # Python.
      # No better way to assign value by setting subarrays in order to
      # ensure that memory address is preserved.
      carray = nparray_from_value(value,self.__sicdata__.itemsize)  # ASCII => itemsize==nchar
      if len(self.__sicdata__.shape) == 0:
         self.__sicdata__[()] = carray
      else:
         self.__sicdata__[:] = carray

   def __getitem__(self,index): # Index may be either an int, a tuple, or a slice
      # print "In __getitem__: " + repr(index)
      # Index length:
      if type(index) in (int,type(slice(0))):
         # python 2.2: 'slice' is a method and not a type...
         ilen = 1
         index = (index,)    # A tuple
      else:
         ilen = len(index)
      # Array rank:
      arank = len(self.__sicdata__.shape)
      # Check if there is a slice:
      hasslice = False
      for i in index:
         if type(i) == slice:
            hasslice = True
      # Array is already a 0-d array:
      if arank == 0 and index == (0,):
         return SicVar(self.__sicname__,self.__siclevel__,subarray=self.__sicdata__)
      elif arank == 0:
         raise IndexError("0-d array has only one element at index 0.")
      # This will return a single element as a 0-d array:
      elif arank == ilen and not hasslice:
         # This ugly thing returns a numpy 0-D array AND NOT a numpy scalar!
         # (Numpy scalars do not share their data with the parent array)
         newindex = list(index)
         newindex[0] = slice(index[0],index[0]+1,None)
         newindex = tuple(newindex)
         return SicVar(self.__sicname__,self.__siclevel__,subarray=self.__sicdata__[newindex].squeeze())
      # This will return a n-D subarray (n>=1):
      else:
         return SicVar(self.__sicname__,self.__siclevel__,subarray=self.__sicdata__[index])

   def __setitem__(self,index,value):
      # print "In __setitem__:", index
      if isinstance(value,SicVar):
         # Assigned value is a SicVar: extract the string value
         value = value.__sicdata__[()]
      if pygildas.isro(self.__sicname__,self.__siclevel__):
         raise PygildasValueError("Variable is read-only in SIC.")
      elif pygildas.vartype(self.__sicname__,self.__siclevel__)>0:
         # Auto-blank filling for strings (arrays)
         value += ' '*(self.__sicdata__.itemsize-len(value))
      # Trick for scalar access:
      if self.__sicdata__.shape == () and index == 0:
         index = ()
      # Set value:
      self.__sicdata__[index] = value

   def __getattr__(self,attname):
      # With this redefined method, SicVar instances inherit the attributes
      # of its __sicdata__ (numpy) attribute, e.g. pi.ndim returns
      # pi.__sicdata__.ndim . Nevertheless, avoid recursive calls when you
      # want to access (or check existence of) the real attributes of the
      # instance: this is the purpose of the first test.
      # print "In __getattr__: ", attname
      if attname in SicVar_attributes:
         return self.__dict__[attname]
      else:
         return getattr(self.__sicdata__,attname)

   def __setattr__(self,attname,value):
      # print "In __setattr__: ", attname, " = ", value
      try:
         self.__dict__[attname]
         # Attribute exists
         if hasattr(self.__sicdata__,attname):
            # Attribute exists and is a Numpy.ndarray attribute: can't overwrite it
            warn("\'"+attname+"\' is a ndarray attribute, could not overwrite it.")
         elif isinstance(self.__dict__[attname],(SicVar,SicStructure)):
            # Att exists and is a SicVar or a SicStructure (in structures/images)
            self.__dict__[attname].assign(value)
         elif attname in SicVar_attributes:
            # Attribute exists and is fundamental: overwriting failure
            raise PygildasAttributeError("Cannot overwrite "+attname+" attribute of a SicVar instance.")
         else:
            # Attribute exists but was manually defined: overwriting success
            self.__dict__[attname] = value
      except:
         # Attribute does not exist: define it
         self.__dict__[attname] = value

   def __delattr__(self,attname):
      # if isinstance(self.__dict__[attname],SicVar):
      #    raise PygildasAttributeError, "Cannot delete SicVar attribute."
      if attname in SicVar_attributes: # Att is fundamental: deletion failure
         raise PygildasAttributeError("Cannot delete "+attname+" attribute of a SicVar instance.")
      else:
         # delattr(self,attname)  # Does not work...
         del self.__dict__[attname]

   def __len__(self):
      # print "In __len__"
      if pygildas.vartype(self.__sicname__,self.__siclevel__)>0 and len(self.__sicdata__.shape)==0: # Scalar string length
         return len(self.tounicode())
      else:
         return len(self.__sicdata__)

   def __iter__(self):
      # print "In __iter__"
      return SicVarIterator(self)

   def __eq__(self,other):
      if pygildas.vartype(self.__sicname__,self.__siclevel__)>0 and type(other)==str: # String comparison
         return (self.tounicode().strip() == other.strip())
      else:
         return (self.__sicdata__ == other)

   def __ge__(self,other): return (self.__sicdata__ >= other)

   def __gt__(self,other): return (self.__sicdata__ > other)

   def __le__(self,other): return (self.__sicdata__ <= other)

   def __lt__(self,other): return (self.__sicdata__ < other)

   def __ne__(self,other): return (self.__sicdata__ != other)

   def __bool__(self):  return bool(self.__sicdata__)

   def __int__(self):      return self.__sicdata__.__int__()

   def __long__(self):     return self.__sicdata__.__long__()

   def __float__(self):    return self.__sicdata__.__float__()

   def __neg__(self):      return self.__sicdata__.__neg__()

   def __pos__(self):      return self.__sicdata__.__pos__()

   def __abs__(self):      return self.__sicdata__.__abs__()

   def __invert__(self):   return self.__sicdata__.__invert__()

   def __add__(self,other):
      if pygildas.vartype(self.__sicname__,self.__siclevel__)>0 and type(other)==str: # Strings concatenation
         return self.tounicode() + str(other)
      elif pygildas.vartype(self.__sicname__,self.__siclevel__)>0 and isinstance(other,SicVar): # SicVar Strings concatenation
         return self.tounicode() + other.tounicode()
      elif isinstance(other,SicVar):
         return self.__sicdata__ + other.__sicdata__
      else:
         return self.__sicdata__ + other

   def __sub__(self,other):
      if isinstance(other,SicVar):
         return self.__sicdata__ - other.__sicdata__
      else:
         return self.__sicdata__ - other

   def __mul__(self,other):
      # Multiply a SicVar string by a number
      if pygildas.vartype(self.__sicname__,self.__siclevel__)>0 and type(other)==int: # Strings multiplication
         return self.tounicode() * other
      elif pygildas.vartype(self.__sicname__,self.__siclevel__)>0 and isinstance(other,SicVar): # String multiplication by a SicVar
         return self.tounicode() * other.__sicdata__
      # Multiply a SicVar number by a SicVar
      elif isinstance(other,SicVar):
         if pygildas.vartype(other.__sicname__,other.__siclevel__)>0:
            return self.__sicdata__ * other.tounicode()
         else:
            return self.__sicdata__ * other.__sicdata__
      else:
         return self.__sicdata__ * other

   def __div__(self,other):  # True division in Python 2
      if isinstance(other,SicVar):
         return self.__sicdata__ / other.__sicdata__
      else:
         return self.__sicdata__ / other

   def __truediv__(self,other):  # True division in Python 3
      if isinstance(other,SicVar):
         return self.__sicdata__ / other.__sicdata__
      else:
         return self.__sicdata__ / other

   def __floordiv__(self,other):  # Floor division in Python 3
      if isinstance(other,SicVar):
         return self.__sicdata__ // other.__sicdata__
      else:
         return self.__sicdata__ // other

   def __radd__(self,other):
      if pygildas.vartype(self.__sicname__,self.__siclevel__)>0  and type(other)==str: # Strings concatenation
         return other + self.tounicode()
      else:
         return self.__sicdata__ + other

   def __rsub__(self,other):
      return other - self.__sicdata__

   def __rmul__(self,other):
      if pygildas.vartype(self.__sicname__,self.__siclevel__)>0 and type(other)==int: # Strings multiplication
         return self.tounicode() * other
      elif pygildas.vartype(self.__sicname__,self.__siclevel__)>0 and isinstance(other,SicVar): # String multiplication by a SicVar
         return self.tounicode() * other.__sicdata__
      else:
         return self.__sicdata__ * other

   def __rdiv__(self,other):  # Right-operand true division in Python 2
      return other / self.__sicdata__

   def __rtruediv__(self,other):  # Right-operand true division in Python 3
      return other / self.__sicdata__

   def __rfloordiv__(self,other):  # Right-operand floor division in Python 3
      return other // self.__sicdata__

   def __iadd__(self,other):
      if pygildas.isro(self.__sicname__,self.__siclevel__):
         raise PygildasValueError("Variable is read-only in SIC.")
      if isinstance(other,SicVar):
         other = other.__sicdata__
      if self.__sicdata__.shape == ():
         self.__sicdata__[()] += other
      else: # Vector
         self.__sicdata__[:] += other
      return self # inplace methods: the augmented object is replaced by the 'return' value.
                  # With no 'return', the object would become 'None'.

   def __isub__(self,other):
      if pygildas.isro(self.__sicname__,self.__siclevel__):
         raise PygildasValueError("Variable is read-only in SIC.")
      if isinstance(other,SicVar):
         other = other.__sicdata__
      if self.__sicdata__.shape == ():
         self.__sicdata__[()] -= other
      else: # Vector
         self.__sicdata__[:] -= other
      return self

   def __imul__(self,other):
      if pygildas.isro(self.__sicname__,self.__siclevel__):
         raise PygildasValueError("Variable is read-only in SIC.")
      if isinstance(other,SicVar):
         other = other.__sicdata__
      if self.__sicdata__.shape == ():
         self.__sicdata__[()] *= other
      else: # Vector
         self.__sicdata__[:] *= other
      return self

   def __idiv__(self,other):  # In-place true division in Python 2
      if pygildas.isro(self.__sicname__,self.__siclevel__):
         raise PygildasValueError("Variable is read-only in SIC.")
      if isinstance(other,SicVar):
         other = other.__sicdata__
      if self.__sicdata__.shape == ():
         self.__sicdata__[()] /= other
      else: # Vector
         self.__sicdata__[:] /= other
      return self

   # def __itruediv__(self,other):  # In-place true division in Python 3
   # Undefined => implicitly use __truediv__

   # def __ifloordiv__(self,other):  # In-place floor division in Python 3
   # Undefined => implicitly use __floordiv__

   def __index__(self):  # Evaluate object as an integer (used by range())
      return int(self.__sicdata__.__int__())

   # Fortran ASCII string conversion to Python 3 Unicode (copy!)
   def tounicode(self):
     if pygildas.vartype(self.__sicname__,self.__siclevel__)<=0:
       raise PygildasTypeError("Variable must be a character string!")
     return self.__sicdata__[()].decode()

   # Variable-type-as-string function
   def chartype(self):
       """Returns a nice string from SIC variable type integer."""
       subtype = ""
       if pygildas.isalias(self.__sicname__,self.__siclevel__) == 1:
          subtype += "(alias)"
       if pygildas.isimage(self.__sicname__,self.__siclevel__) == 1:
          subtype += "(image)"
       vtype = pygildas.vartype(self.__sicname__,self.__siclevel__)
       if   vtype>0:               return subtype + "CHARACTER*" + repr(vtype)
       elif vtype==pygildas.fmtr4: return subtype + "REAL*4"
       elif vtype==pygildas.fmtr8: return subtype + "REAL*8"
       elif vtype==pygildas.fmti4: return subtype + "INTEGER*4"
       elif vtype==pygildas.fmti8: return subtype + "INTEGER*8"
       elif vtype==pygildas.fmtl:  return subtype + "LOGICAL*4"
       elif vtype==pygildas.fmtby: return subtype + "BYTE"
       elif vtype==pygildas.fmtc4: return subtype + "COMPLEX"
       else:                       return "Unknown (" + repr(vtype) + ")"

   # Array-shape-as-string function
   def chararray(self):
       """Returns one SIC array dimensions as a nice string."""
       dims = pygildas.vardims(self.__sicname__,self.__siclevel__)
       nd = dims[0]
       if (nd==-1):
          # Error
          string = ""
       elif (nd==0):
          # Scalar
          string = "0D"
       else:
          # Array
          string = ""
          for n in dims[1:nd+1]:
             if (string!=""): string += "x" + repr(n)
             else:            string += repr(n)
          string = repr(nd) + "D (" + string + ")"
       return string

   # RO-or-RW-as-string function
   def charro(self):
       """Returns 'RO' or 'RW' string from logical value."""
       if pygildas.isro(self.__sicname__,self.__siclevel__): return "RO"
       else:                                                 return "RW"

   # Level-as-string function
   def charlevel(self):
       """Returns 'GLOBAL' or 'LOCAL' string from logical value."""
       if (self.__siclevel__ == 0): return "GLOBAL"
       else:                        return "LOCAL"+repr(self.__siclevel__)

   # def __del__(self):
   #    if hasattr(self,"__cfp__"):
   #       error = pygildas.sicdelvar(self.__sicname__,True)
   #       if error:
   #          print "Warning: could not delete " + self.__sicname__ + " in SIC."
   #       else:
   #          print "(deleted " + self.__sicname__ + " in SIC)"


# --- 'SIC Local Space' class definition ---
class SicLocalSpace:
   pass


# --- Dummy class used to link commands when SIC calls Python ---
class SicCommands:
   """This object provides:
1) Functions (or aliases to these) which interact with GILDAS processes
2) A 'warnings' boolean flag which can be turned off to hide PygildasWarning's
3) A 'localspaces' array which is used to store upper level variables when a local
   variable imported in Python __main__ conflicts with them."""

   def enter(self):
      return pygildas.enter()

   def __call__(self):
      return pygildas.enter()


# --- Class whose dictionary +always+ point to pygildas.dict ---
class SicDict:
   """Container for imported Gildas variables. Variables are accessed as
   attributes of this object."""

   def __repr__(self):
      return self.__doc__

   # Called each time we look for an attribute
   def __getattr__(self,attname):
      return pygildas.dict[attname]

   def __setattr__(self,attname,value):
      # Define the object in Sic if it does not yet exist
      if attname not in pygildas.dict:
         define_from_this(attname,value)  # Try only?
      # And allways set its new value
      pygildas.dict[attname].assign(value)

   def __delattr__(self,attname):
      delete(pygildas.dict[attname])

   # Override the default dir() result, to get the variable names instead
   def __dir__(self):
    return list(pygildas.dict.keys())

   # Called to see the complete dictionary
   def _dict(self):
      return pygildas.dict

# --- Message severities ---
class SicMessageSeverities:
  def __init__(self):
    self.f = 1
    self.e = 2
    self.w = 3
    self.r = 4
    self.i = 5
    self.d = 6
    self.t = 7
    self.c = 8
    self.u = 9


# === Allways define the Sic object ====================================

# if not hasattr(maindict,'Sic'): # Not used, import is allways done only once
maindict['Sic'] = SicCommands()


# === PyGildas specific methods ========================================

# Print detailed attributes of SicStructure or SicVar-image
def printattr(var):
   """Usage: FOR INTERNAL USE ONLY!
      printattr(instance)
   Takes a SicVar-image or a SicStructure instance as argument and returns a
   string describing all its SicVar attributes."""
   string = ""
   for k,v in vars(var).items():
      if isinstance(v,SicVar):
         # Beginning:
         beg = "\n%-10s = " % k # 14 chars (minimum)
         # Middle:
         dims = pygildas.vardims(v.__sicname__,v.__siclevel__)
         vtyp = pygildas.vartype(v.__sicname__,v.__siclevel__)
         if vtyp>0:
            s = "\"" + v.tounicode() + "\""
         elif dims[0] > 0 :
            s = str(v.__sicdata__).replace("\n","")
         else:
            s = repr(v)
         lmid = 32 - (len(beg)-14)
         if len(s) > lmid:
           mid = "%-.*s... " % (lmid-3,s)
         else:
           mid = "%-*s " % (lmid,s)
         # End
         end = "%13s %-13s" % (v.chartype(), v.chararray())
         string += beg + mid + end
      if isinstance(v,SicStructure):
         string += "\n%-10s = %-32s" % (k,"<SicStructure>")
   return string


# Examine command
def exa(*var):
    """Usage: exa([instance1,[instance2[,...]]])
    With one or more SicVar or SicStructure instances  as  argument,  displays
    one line per instance with the corresponding SIC  variable  name,  details
    on its type, dimensions,... With no argument, displays this line  for  the
    full list of the SIC variables. Example:
    >>> exa(pi)
    PI      is a        REAL*8, 0D            (GLOBAL,RO) -> pi"""
    if len(var) == 0:
       varlist = list(pygildas.dict.keys())
       varlist.sort()
       var = [pygildas.dict.get(key) for key in varlist if isinstance(pygildas.dict.get(key),(SicVar,SicStructure))]
    for v in var:
       if isinstance(v,(SicVar,SicStructure)):
          sicname = v.__sicname__
          varname = sic2py(sicname)
          str2 = v.chartype()
          str3 = v.chararray()
          str4 = v.charlevel()
          str5 = v.charro()
          print("%-15s is a %14s, %-14s (%6s,%2s) -> %-15s" % (sicname,str2,str3,str4,str5,varname))
       else:
          warn("Could not examine non-SicVar/SicStructure object.")

# Send-command-to-Gildas command
def comm(chain):
    """Usage: comm(string)
    Takes a string as argument and sends it to the SIC interpreter, which will
    execute it."""
    if (pygildas.command(chain)):
       raise PygildasError("Error executing command")

# Generate-parents-in-case-of-(nested)-structures command
def mkparents(sicname,level,dict):
    """Usage: FOR INTERNAL USE ONLY!
       mkparents(string,integer,dictionary)
    Takes a SIC name as argument  and  instantiate  recursively  in  Python
    every parent structure  at  given  level  into  the  given  dictionary.
    Dictionary may be the __main__ one, or a Sic.localspaces[level] one. It
    returns the dictionary of the parent structure that  will  contain  the
    object that caused the routine call."""
    # 'sicname' is the full name of the object that caused the 'mkparents' call:
    sicname    = sicname.upper().strip() # "A%B%C%D"
    posp       = sicname.rfind("%")      # 5
    structname = sicname[:posp]          # "A%B%C"
    fclassname = sic2py(structname)      # "a.b.c"
    posp       = structname.rfind("%")   # 3
    if posp != -1:
       parentdict = mkparents(structname,level,dict)
       childname  = fclassname[posp+1:]
    else:
       parentdict = dict
       childname  = fclassname
    if childname not in parentdict:
       parentdict[childname] = SicStructure(structname,level)
    return parentdict[childname].__dict__

# Get-parent command
def getparentdict(varname,dict): # Varname = "a.b.c"
    """Usage: FOR INTERNAL USE ONLY!
       getparentdict(string,dictionary)
    Takes a SicVar name as argument and a dictionary to look in,  and  returns
    the parent dictionary that  contains  it.  Returns  'None'  if  the  input
    dictionary does not contains an object with the given name."""
    # If name does not have a dot:
    if varname.rfind(".") == -1:
       if dict is None:
          return None
       elif varname in dict:
          return dict
       else:
          return None
    # If name has a dot
    else:
       parent      = varname[:varname.rfind(".")]
       child       = varname.split('.')[-1]
       gparentdict = getparentdict(parent,dict)
       if gparentdict is None:
          return None
       elif child in gparentdict[parent.split('.')[-1]].__dict__:
          return gparentdict[parent.split('.')[-1]].__dict__ # This is a dict
       else:
          return None

# Get-a-standard-variable internal routine:
def getvar(sicname,level,dict):
    """Usage: FOR INTERNAL USE ONLY!
       getvar(string,integer,dictionary)
    Takes a SIC name as argument and instantiate in Python the corresponding
    SicVar at given level into the given dictionary."""
    sicname = sicname.upper().strip()
    varname = sic2py(sicname)
    address = pygildas.descptr(sicname,level)
    if address == -1:
       raise PygildasError("An error occured while importing variable '" + sicname +"'.")
    elif (sicname.find("%") != -1):
       parentdict            = mkparents(sicname,level,dict)
       childname             = varname[varname.rfind(".")+1:]
       parentdict[childname] = SicVar(sicname,level,address)
    else:
       dict[varname] = SicVar(sicname,level,address)

# Get-a-structure internal routine:
def getstruct(structname,level,dict):
    """Usage: FOR INTERNAL USE ONLY!
       getstruct(string,integer,dictionary)
    Takes a SIC name as argument and instantiate in Python the corresponding
    SicStructure at given level into the given dictionary. All the structure
    elements are linked to the SicStructure as attributes of the instance."""
    structname = structname.upper().strip() # "A%B%C" is a (nested) structure
    classname  = sic2py(structname)         # "a.b.c"
    address = pygildas.descptr(structname,level)
    if address == -1:
       raise PygildasError("An error occured while importing structure '" + structname +"'.")
    elif (structname.find("%") != -1):
       parentdict = mkparents(structname,level,dict)
       childname  = classname[classname.rfind(".")+1:]
    else:
       childname  = classname
       parentdict = dict
    # Add the structure in its parent object:
    if (childname not in parentdict):
       # Attach it only if it was not attached before. Because of the
       # 'remark' in the loop hereafter, we would loose the nested
       # structures already attached (there is no specific order in the
       # SIC dictionary, i.e. we cannot know if we will encounter A%B%C
       # and then A%B%C%D, or reverse).
       parentdict[childname] = SicStructure(structname,level)
    # Get all attributes of the structure:
    for i in range(1,pygildas.dictsize()+1):
       (sicname,varlevel) = pygildas.getname(i)
       if ( sicname.find(structname+"%")==0 and varlevel==level):
          suffix  = sicname[len(structname)+1:]
          if suffix.find("%") != -1:
             # Remark:
             # If we found "A%B%C%D%E", suffix is "D%E". We should getstruct
             # D and then D%E, but this will be done when encountering
             # "A%B%C%D" itself. I.e. nothing to do.
             return
          attname = sic2py(suffix)
          if (hasattr(parentdict[childname],attname)):
             # 'getstruct' is currently getting the whole A%B%C, but we may
             # already have attached A%B%C%D in another call, since there is
             # no specific order in the SIC dictionary. Do nothing, because
             # some objects may be not writeable and would raise an error.
             return
          if pygildas.vartype(sicname,level) == 0:   # Structure or header
             getstruct(sicname,level,parentdict)
          elif pygildas.isimage(sicname,level) == 1: # Image
             getimage(sicname,level,parentdict)
          else:                                      # Scalar or array
             address = pygildas.descptr(sicname,level)
             # # Must call the __setattr__ method which protects some attributes
             # # vars(eval(classname,maindict))[attname] = SicVar(sicname,level,address)
             # eval(classname,dict).__setattr__(attname,SicVar(sicname,level,address))
             parentdict[childname].__setattr__(attname,SicVar(sicname,level,address))

# Get-an-image internal routine:
def getimage(imagename,level,dict):
    """Usage: FOR INTERNAL USE ONLY!
       getimage(string,integer,dictionary)
    Takes a SIC name as argument and instantiate in Python the corresponding
    SicVar-image at given level into the given  dictionary.  All  the  image
    header components are linked to the SicVar-image as  attributes  of  the
    instance."""
    imagename = imagename.upper().strip()
    classname = sic2py(imagename)
    address   = pygildas.descptr(imagename,level)
    if address == -1:
       raise PygildasError("An error occured while importing image '" + imagename +"'.")
    elif (imagename.find("%") != -1):
       parentdict = mkparents(imagename,level,dict)
       childname  = classname[classname.rfind(".")+1:]
    else:
       childname  = classname
       parentdict = dict
    # Add the image in its parent object
    parentdict[childname] = SicVar(imagename,level,address)
    for i in range(1,pygildas.dictsize()+1):
       (sicname,varlevel) = pygildas.getname(i)
       if ( sicname.find(imagename+"%")==0 and varlevel==level):
          address = pygildas.descptr(sicname,level)
          suffix  = sicname.split('%')[-1]
          if pygildas.vartype(sicname,level) == 0: # Structure or header
             getstruct(sicname,level,parentdict)
          elif suffix.find("%") == -1:             # Scalar or array
             attname = sic2py(suffix)
             # # Must call the __setattr__ method which protects some attributes
             # # vars(eval(classname,indict))[attname] = SicVar(sicname,level,address)
             # eval(classname,indict).__setattr__(attname,SicVar(sicname,level,address))
             parentdict[childname].__setattr__(attname,SicVar(sicname,level,address))

# Get-anything command
def get(sicname=None,level=None,verbose=True):
    """Usage: get([string[,integer[,boolean]]])
    Takes a string as argument, which must be a valid SIC name (in SIC format,
    thus not case sensitive). Variable can be a scalar, an array, a structure,
    or an image. The second argument may be the level of the  variable  to  be
    imported (0 = global, 1 = local-1, and so on). It defaults to the  current
    execution level. A third argument indicates  if  the  function  should  be
    verbose (True, default), or not (False).
    'get()' creates a SicVar or a SicStructure instance sharing its data  with
    the corresponding variable in SIC. Its name is the original SIC name,  but
    lowercased, and converted in respect with the Python name conventions: '$'
    can not appear in Python names: it is converted to '_'. '%' (structures in
    SIC) has a special treatment and  is  converted  to  '.'.  All  structures
    elements are linked  to  the  corresponding  SicStructure  as  SicVar  (or
    SicStructure) attributes, and all header  components  of  the  images  are
    linked to the corresponding SicVar as SicVar attributes.
    'get()' fails to import variable into Python when its converted name is  a
    Python reserved keyword (such as 'def', 'del', 'import', 'as', ...), or  a
    built-in function name (such as 'type', 'range', 'min', 'max', ...).
    With no argument, 'get()' imports all SIC global variables by  iteratively
    calling 'get()' itself with their names as argument."""
    # The next line is here because default value for level (and thus
    # pygildas.varlevel()) is evaluated at first import and not at calling time.
    if level is None:
       level = pygildas.varlevel()
    if sicname is not None:
       sicname  = sicname.upper().strip()
       varname  = sic2py(sicname)
       posp     = varname.find(".")
       if posp<0:
          rootname = varname
       else:
          rootname = varname[:posp]
    if sicname is None:
       if (verbose):
         print("Importing all SIC variables into Python...")
       for i in range(1,pygildas.dictsize()+1):
          (sicname,level) = pygildas.getname(i)
          get(sicname,level,verbose)
       if (verbose):
         print("... done.")
    elif pygildas.descptr(sicname,level) == -1:
       raise PygildasNameError("Variable '%s' at level %d is not defined in SIC." % (sicname,level))
    else:
       try:
          indict = pygildas.dict
          oldvar = eval(rootname,indict)
          # Evaluation can occur: var already exists in Python name area.
          if (isinstance(oldvar,(SicVar,SicStructure))):
             # It is a SicVar/SicStructure.
             # Check if it is not the same level, and and save it.
             if (oldvar.__siclevel__ < level):
                # Save the OLD var in its maindict['Sic'].localspaces[?]
                setattr(maindict['Sic'].localspaces[oldvar.__siclevel__],rootname,oldvar)
             elif (oldvar.__siclevel__ > level):
                # Will link this NEW global var to maindict['Sic'].localspaces[0]
                indict = maindict['Sic'].localspaces[level].__dict__
          elif (isinstance(oldvar,types.BuiltinFunctionType)):
             # It is a known keyword but non-reserved (e.g. "all"). We can safely
             # create a variable with same name
             raise NameError
          else:
             # It is a standalone Python-side variable. Save it.
             setattr(maindict['Sic'].localspaces[0],rootname,oldvar)
          # And then get the NEW var into its __dict__
          if pygildas.vartype(sicname,level) == 0:   # Structure or header
             getstruct(sicname,level,indict)
          elif pygildas.isimage(sicname,level) == 1: # Image
             getimage(sicname,level,indict)
          else:                                      # Scalar or array
             getvar(sicname,level,indict)
       except SyntaxError:
          # Name is one of 'lambda', 'def', 'del', 'import' ... reserved keywords
          if (verbose):
            warn("'"+rootname+"' is a Python command (could not assign).")
       except (NameError,AttributeError):
          # Variable does not already exist on Python-side in __main__
          if pygildas.vartype(sicname,level) == 0:   # Structure or header
             getstruct(sicname,level,pygildas.dict)
          elif pygildas.isimage(sicname,level) == 1: # Image
             getimage(sicname,level,pygildas.dict)
          else:                                      # Scalar or array
             getvar(sicname,level,pygildas.dict)
       except:
          print("An unexpected error occured while 'getting' '%s' variable:" % sicname)
          import sys
          print(sys.exc_info())

# Define-the-importing-area internal routine:
def setgdict(dico):
    """setgdict(dictionary)
    Define input dictionary as the area where Gildas variables will  be
    imported, and automatically import these into the input dictionary.
    If a dictionary was already defined, it  is  first  cleaned  before
    filling the new one given in input."""
    if type(dico) is not dict:
       raise PygildasTypeError("Input object must be a dictionary")
    # First, clean the old dictionary
    for key in list(pygildas.dict.keys()):
       if isinstance(pygildas.dict.get(key),(SicVar,SicStructure)):
          pygildas.dict.pop(key)
    # Then define the new one and fill it
    pygildas.dict = dico
    get()

# Check if dictionary has already been imported:
def gotgdict():
    """gotgdict()
    Check if Gildas dictionary has already been imported."""
    #
    # return len(pygildas.dict)!=0  # This test is stupid since we start
    # importing variables one by one as soon as python is started. This
    # does not mean a whole import has been done.
    #
    return ingdict("SIC")  # Check that the "SIC" structure has been
    # imported. Not perfect since it could have been imported explicitely
    # out of a whole import. Can we think of a cleverer test?

def ingdict(varname):
    """ingdict(varname)
    Check if Gildas dictionary contains the input variable name."""
    return sic2py(varname) in pygildas.dict

# SIC-to-Python-names-conversion command
def sic2py(sicname):
    """Usage: sic2py(string)
    Takes a SIC name as argument and returns the name in which the corresponding
    object is imported in Python."""
    # What about underscores in SIC?
    return sicname.lower().replace("%",".").replace("$","_").strip()

# Define-in-both-SIC-and-Python command
def define(string,glob=False):
    """Usage: define(string[,boolean])
    Takes one string as argument and defines one or more variables in both SIC
    and Python. First keyword must be  the  type  (one  of  'real',  'double',
    'integer', 'character' or 'structure', or a non-ambiguous  truncated  form
    of them), followed by one or more valid variable-creator  SIC  name  (e.g.
    'A', 'B*8', 'C[2]', 'D%E' or even 'F%G*8[2,3]').  Newly-created  variables
    are automatically imported in the corrresponding  SicVar  or  SicStructure
    instance(s) by  internally  calling  the  get()  function.  All  variables
    created this way are read-and-write. By default  they  are  local  to  the
    current execution level, but you can provide a  second  optional  argument
    set to 'True' to make the variables global. For images, give  the  command
    line you would give to the  SIC  interpreter,  but  without  the  'define'
    keyword. 'exa()' is finally  called  on  the  newly  created  instance(s).
    Examples:
    >>> define('real a')
    A               is a        REAL*4, 0D            (GLOBAL,RW) -> a
    >>> define('double b c[2] d[2,3]')
    B               is a        REAL*8, 0D            (GLOBAL,RW) -> b
    C               is a        REAL*8, 1D (2x0x0x0)  (GLOBAL,RW) -> c
    D               is a        REAL*8, 2D (2x3x0x0)  (GLOBAL,RW) -> d
    >>> define('character e*8')
    E               is a   CHARACTER*8, 0D            (GLOBAL,RW) -> e
    >>> define('structure f')
    F               is a   <structure>, 0D            (GLOBAL,RW) -> f
    >>> define('integer f%g f%h*6[2,3]')
    F%G             is a     INTEGER*4, 0D            (GLOBAL,RW) -> f.g
    F%H             is a   CHARACTER*6, 2D (2x3x0x0)  (GLOBAL,RW) -> f.h
    >>> define('image i centaurus.gdf read')
    I               is a        REAL*4, 2D (512x512x1x1) (GLOBAL,RO) -> i"""
    #
    args    = string.split()
    typestr = args[0].lower()
    names   = list(args[1:])
    #
    typelist = ['real','double','integer','logical','character', \
                'structure','image','table','header','alias']
    typevals = [pygildas.fmtr4,pygildas.fmtr8,pygildas.fmti4,pygildas.fmtl,1]
    itype = ambigs(typestr,typelist)
    #
    # Standard variables
    if itype<=4:
       for i in range(len(names)):
          name    = names[i]
          typeval = typevals[itype]
          posb    = name.find("[")
          if posb != -1:
             shape    = name[posb:]
             name     = name[:posb]
             names[i] = name
          else:
             shape = ""
          poss = name.find("*")
          if poss != -1:
             nchar    = int(name[poss+1:])
             typeval  = nchar
             name     = name[:poss]
             names[i] = name
          else:
             nchar = 1
          isglobal = int(glob or pygildas.varlevel()==0) # 1 == Global
          error = pygildas.defvar(typeval,name+shape,isglobal)
          if error:
             raise PygildasError("Could not define variable '%s'." % name)
    # Structures
    elif itype==5:
       for i in range(len(names)):
          name     = names[i]
          isglobal = int(glob or pygildas.varlevel()==0) # 1 == Global
          error    = pygildas.defstruct(name,isglobal)
          if error:
             raise PygildasError("Could not define structure '%s'." % name)
    # Images
    elif itype==6:
       if glob or pygildas.varlevel()==0:
          string += " /GLOBAL"
       comm('define ' + string)
       posb = names[0].find("[")
       if posb != -1:
          names[0] = names[0][:posb]
       names = [names[0]] # Only keep image name to call exa()
    # Tables
    elif itype==7:
       if glob or pygildas.varlevel()==0:
          string += " /GLOBAL"
       comm('define ' + string)
       names = [names[0]] # Only keep table name to call exa()
    # Headers
    elif itype==8:
       if glob or pygildas.varlevel()==0:
          string += " /GLOBAL"
       comm('define ' + string)
       names = [names[0]] # Only keep header name to call exa()
    # Aliases
    elif itype==9:
       if glob or pygildas.varlevel()==0:
          string += " /GLOBAL"
       comm('define ' + string)
       names = [names[0]] # Only keep alias name to call exa()
    # Else invalid keyword:
    else:
       raise PygildasSyntaxError("Invalid type as first argument.")
    # Call exa() whatever is the variable(s) type:
    for name in names:
       varname = sic2py(name)
       # Working in current name area:
       if pygildas.varlevel()==0 or not glob:
          indict = pygildas.dict
       # Working in local but defining in global:
       else:
          parentdict = getparentdict(varname,pygildas.dict)
          if parentdict is None:
             # Object was not found in pygildas.dict
             indict = maindict['Sic'].localspaces[0].__dict__
          elif parentdict[varname.split('.')[-1]].__siclevel__ == 0:
             # Object was found in pygildas.dict and is global
             indict = pygildas.dict
          else:
             # Object was found in main but is not global
             indict = maindict['Sic'].localspaces[0].__dict__
       parentdict = getparentdict(varname,indict)
       # Get suffix name
       if varname.rfind(".") == -1:
          child = varname
       else:
          child = varname.split('.')[-1]
       # Exa it if it was imported
       # if parentdict is not None:
       #    exa(parentdict[child])

# Define-and-assign internal routine:
def define_from_this(name,value,glob=False):
    """Usage: FOR INTERNAL USE ONLY!
       define_from_this(name,value)
    Given a Python name and an object, (try to) create in Gildas the variable
    which will store the input value. Value is  not  set  here. WARNING:  not
    protected against redefinition (which is an error)."""
    # Cast input value into a numpy array
    carray = nparray_from_value(value)
    # Handle rank of input object:
    if numpy.ndim(carray) == 0:
      shapestr = ""
    elif numpy.ndim(carray) <= 7:  # SIC_MAXDIMS
      shapestr = str(list(carray.shape[::-1])).replace(" ","")  # Ensure Fortran memory arrangment
    else:
      raise PygildasValueError("Too many dimensions, maximum is 7")
    # Handle type of input object:
    if carray.dtype.type in (int,numpy.int0,numpy.int16,numpy.int32,numpy.uint,numpy.uint0,numpy.uint16,numpy.uint32):
      typestr = "INTEGER"
      charstr = ""
    elif carray.dtype.type is numpy.float32:
      typestr = "REAL"
      charstr = ""
    elif carray.dtype.type in (float,numpy.float64):
      typestr = "DOUBLE"
      charstr = ""
    elif carray.dtype.type is numpy.bool_:
      typestr = "LOGICAL"
      charstr = ""
    elif carray.dtype.type is numpy.string_:
      typestr = "CHARACTER"
      charstr = "*"+str(carray.itemsize)
    else:
      raise PygildasTypeError("Unsupported input type")
    # Define the object in Gildas
    define("%s %s%s%s" % (typestr,name,charstr,shapestr),glob)

def nparray_from_value(value,nchar=None):
    """Usage: FOR INTERNAL USE ONLY!
       nparray_from_value(name,value)
    Transform a Python value (whatever it is) in a Numpy array. If such
    a conversion is impossible, obviously it can not be copied to the
    corresponding GILDAS variable. In particular, transform Unicode to
    ASCII!"""
    try:
      carray = numpy.array(value)
      if (carray.dtype.type is npunicode):
         # Sic variables can not store Unicode strings. Need to convert
         # to ASCII strings, taking care of trailing blanks.
         #   numpy.str_==numpy.unicode_==Unicode, while
         #   numpy.string_==numpy.bytes_==bytes==ASCII (Gildas strings)
         if (nchar):
           # Resize to proper size
           carray = numpy.array(carray,dtype='U'+repr(nchar))
         # Pad all values with blanks (instead of trailing zeroes)
         with numpy.nditer(carray,op_flags=['readwrite']) as it:
           for x in it:
             x[...] = x[()].ljust(nchar,' ')
         # From Unicode to ASCII
         carray = carray.astype(numpy.string_)
    except:
      raise PygildasTypeError("Value can not be cast to a scalar or array object")
    return carray

# Delete-in-SIC command
def delete(*var):
    """Usage: delete(instance[,...])
    Takes one or more SicVar or SicStructure instances as argument. Asks SIC
    to delete its corresponding variables. SIC will also  delete  the  input
    instances."""
    for v in var:
       if isinstance(v,(SicVar,SicStructure)):
          # del globals()[sic2py(var.__sicname__)]
          error = pygildas.delvar(v.__sicname__)
          if error:
             raise PygildasError("Could not delete '" + v.__sicname__ + "' in SIC.")
       else:
          raise PygildasError("Could not delete non-SicVar/SicStructure object. Use 'del' instead.")

# Warning command
def warn(string):
    """Usage: warn(string)
    Warns user in case of non-critical error."""
    if maindict['Sic'].warnings:
       print("PygildasWarning: %s" % string)

# Print-and-exec-file command
def pexecfile(filename,globdict=pygildas.dict):
    """Usage: pexecfile(string[,dict])
    Takes a Python file name as argument. Does the same as the  `execfile()'
    standard Python function (which executes the file in current name area),
    but also  prints  the  command  lines  or  full  command  blocks  before
    execution. The global dictionary used as name  area  can  be  passed  as
    second argument. __main__ dictionary is used by default (NOT the current
    `globals()' one)."""
    # Based on the fact that 'execfile' is a shortcut for
    # 'exec open(filename).read() in globals()'
    lines  = open(filename).readlines()
    # First usefull line
    for index in range(0,len(lines)):
       string = lines[index]
       if string[0] not in ("#","\n"):
          break
    # And loop over next lines
    for line in lines[index+1:]:
       if line.rstrip() == "" or line.startswith("#"):
          # At this stage, should check for two consecutive blank lines,
          # because they end all blocks in standard Python syntax. But
          # 'execfile' does not raise any error and seems to ignore all
          # blanklines, thus I do the same.
          pass
       elif line[0]==" " or line[0]=="\t" or line[0:4]=="elif" or \
            line[0:4]=="else" or line[0:6]=="except" or line[0:7]=="finally":
          string += line
       else:
          print(">>>", string.rstrip().replace("\n","\n... "))
          exec(string, globdict)
          string = line
    print(">>>", string.rstrip().replace("\n","\n... "))
    exec(string, globdict)

# Execute welcome procedure:
def welcome():
   comm('@ welcome')

# === Define clean Gildas-exit function ================================

import atexit
atexit.register(pygildas.sicexit)
del atexit
# 'enter_program' does not work, 'play_program' does.
# def sicexit():
#     comm('EXIT')
# atexit.register(sicexit)
# del atexit, sicexit


# === Define some more objects =========================================

warnings = True

maxlev = 127  # Same as Sic MAXLEV (maximum nesting of levels)
localspaces = ( SicLocalSpace(), )*maxlev

seve = SicMessageSeverities()

# === Define the exportable objects ====================================
# Keys define the front-end name of the method exposed to the user,
# values the reference to the method.

sic_exports = { 'exa':         exa,
                'comm':        comm,
                'get':         get,
                'getlogical':  pygildas.getlogical,
                'parsefile':   pygildas.parsefile,
                'sicerror':    pygildas.sicerror,
                'message':     pygildas.message,
                'setgdict':    setgdict,
                'gotgdict':    gotgdict,
                'ingdict':     ingdict,
                'sic2py':      sic2py,
                'define':      define,
                'delete':      delete,
                'warn':        warn,
                'pexecfile':   pexecfile,
                'welcome':     welcome,
                'warnings':    warnings,
                'localspaces': localspaces,
                'seve':        seve }

# Optionally add them to the Sic object
for key, value in sic_exports.items():
   maindict['Sic'].__dict__[key] = value


# === Clean the module =================================================

del key,value
