# -*- mode:Python -*-
#
# This file is part of the Coriolis Software.
# Copyright (c) SU 2012-2020, All Rights Reserved
#
# +-----------------------------------------------------------------+ 
# |                   C O R I O L I S                               |
# |          Alliance / Hurricane  Interface                        |
# |                                                                 |
# |  Author      :                    Jean-Paul Chaput              |
# |  E-mail      :            Jean-Paul.Chaput@lip6.fr              |
# | =============================================================== |
# |  Python      :   "./crlcore/helpers/overlay.py"                 |
# +-----------------------------------------------------------------+
#
# Those classes are based on the work of Jock Tanner from Libre-SOC.


"""
Overlay to make some C++ objects provide a more Pythonic interface.
Contains:

* ``overlay.UpdateSession`` : to be used in ``with`` construct.
* ``overlay.Configuration`` : to be used in ``with`` construct.
* ``overlay.CfgCache``      : A cache for Cfg parameters.
"""

from   __future__ import print_function
import Cfg
import Hurricane


class UpdateSession ( object ):
    """
    Context manager for a GO update session. See Hurricane reference manual
    for an info on Hurricane::UpdateSession class.
    """

    def __enter__ ( self ):
        Hurricane.UpdateSession.open()

    def __exit__( self, *args ):
        Hurricane.UpdateSession.close()


class Configuration:
    """
    Allow access to Cfg parameter as attributes. For attribute syntax,
    the dot (.) used in C++ or raw access is replaced by an underscore (_)
    in Python mode.

    Also provides a context manager.
    """

    PRIORITY_USE_DEFAULT         = Cfg.Parameter.Priority.UseDefault
    PRIORITY_APPLICATION_BUILTIN = Cfg.Parameter.Priority.ApplicationBuiltin
    PRIORITY_CONFIGURATION_FILE  = Cfg.Parameter.Priority.ConfigurationFile
    PRIORITY_USER_FILE           = Cfg.Parameter.Priority.UserFile
    PRIORITY_COMMAND_LINE        = Cfg.Parameter.Priority.CommandLine
    PRIORITY_INTERACTIVE         = Cfg.Parameter.Priority.Interactive

    def __init__ ( self, priority=None ):
        self._priority = priority

    def __enter__( self ):
        if self._priority is not None:
            Cfg.Configuration.pushDefaultPriority( self._priority )
        return self

    def __setattr__( self, attr, val ):
        if attr.startswith("_"):
            self.__dict__[attr] = val
            return
        attr = attr.replace("_", ".")
        if isinstance(val, bool):
            Cfg.getParamBool(attr).setBool( val )
        elif isinstance(val, int):
            p = Cfg.getParamInt( attr ) # all params have a type
            if p.type == 'Enumerate':
                Cfg.getParamEnumerate(attr).setInt( val )
            else:
                Cfg.getParamInt(attr).setInt( val )
        elif isinstance(val, long):
            p = Cfg.getParamInt( attr ) # all params have a type
            p.setInt( val )
        elif isinstance(val, float):
            p = Cfg.getParamDouble( attr ).setDouble( val )
        elif '%' in val:
            Cfg.getParamPercentage(attr).setPercentage( float(val[:-1]) )
        else:
            Cfg.getParamString(attr).setString( val )

    def __exit__( self, *args ):
        if self._priority is not None:
            Cfg.Configuration.popDefaultPriority()


class CachedParameter ( object ):

    def __init__ ( self, path, v ):
        self.path   = path
        self._v     = None
        self.v      = v
        self.vRange = [ None, None ]
        self.vEnum  = []
        self.create = True
        self.cacheRead()

    @property
    def v ( self ): return self._v

    @v.setter
    def v ( self, value ):
        if value is not None: self._v = value

    def __str__ ( self ):
        if isinstance(self.v,str): s = '"{}"'.format(self.v)
        else: s = '{}'.format(self.v)
        if self.vRange[0] is not None or self.vRange[1] is not None:
            s += ' [{}:{}]'.format(self.vRange[0],self.vRange[1])
        if self.vEnum:
            s += ' ('
            for i in range(len(self.vEnum)):
                if i: s += ', '
                s += '{}:"{}"'.format(self.vEnum[i][1],self.vEnum[i][0])
            s += ')'
        return s

    def cacheWrite ( self ):
        """"
        Commit the value of parameter ``self.path`` to ``self.v`` in Cfg.
        Percentage are set as Double and Enumerate as Int.
        """
        if Cfg.hasParameter(self.path):
            confDb = Cfg.Configuration.get()
            p      = confDb.getParameter( self.path )
        else:
            if   len(self.vEnum):          p = Cfg.getParamEnumerate( self.path )
            elif isinstance(self.v,bool ): p = Cfg.getParamBool     ( self.path )
            elif isinstance(self.v,int  ): p = Cfg.getParamInt      ( self.path )
            elif isinstance(self.v,long ): p = Cfg.getParamInt      ( self.path )
            elif isinstance(self.v,float): p = Cfg.getParamDouble   ( self.path )
            else:                          p = Cfg.getParamString   ( self.path )
        if   p.type == Cfg.Parameter.Type.Enumerate:  p.setInt      ( self.v )
        elif p.type == Cfg.Parameter.Type.Int:        p.setInt      ( self.v )
        elif p.type == Cfg.Parameter.Type.Bool:       p.setBool     ( self.v )
        elif p.type == Cfg.Parameter.Type.Double:     p.setDouble   ( self.v )
        elif p.type == Cfg.Parameter.Type.Percentage: p.setDouble   ( self.v*100.0 )
        else:                                         p.setString   ( str(self.v) )
        if self.create:
            if len(self.vEnum):
                for item in self.vEnum:
                    p.addValue( item[0], item[1] )
        if self.vRange[0] is not None: p.setMin( self.vRange[0] )
        if self.vRange[1] is not None: p.setMax( self.vRange[1] )

    def cacheRead ( self ):
        """"Get the value of parameter ``self.path`` from Cfg."""
        if not Cfg.hasParameter(self.path):
            self.create = True
            return
        if self.v is not None: return
        confDb = Cfg.Configuration.get()
        p      = confDb.getParameter( self.path )
        if p:
            if   p.type == Cfg.Parameter.Type.Enumerate:  self.v = p.asInt()
            elif p.type == Cfg.Parameter.Type.Int:
                self.v = p.asInt()
            elif p.type == Cfg.Parameter.Type.Bool:       self.v = p.asBool()
            elif p.type == Cfg.Parameter.Type.String:     self.v = p.asString()
            elif p.type == Cfg.Parameter.Type.Double:     self.v = p.asDouble()
            elif p.type == Cfg.Parameter.Type.Percentage: self.v = p.asDouble()/100.0
            else: self.v = p.asString()

            
class CfgCache ( object ):
    """
    CgfCache cache a set of configuration parameters. The values of the
    parameters are not set in the system *until* the ``apply()`` function
    is called.

    If a parameter do not exists in the ``Cfg`` module, it is created
    when ``apply()`` is called. Be aware that it is not able to guess
    the right type between Double and Percentage or Int and Enumerate.
    It will, by default, only create Double or Int. So, when setting
    Percentage or Enumerate, be sure that they exists beforehand in
    the ``Cfg`` module.

    The attributes of CfgCache exactly mimic the behavior of the
    ``Cfg`` parameter string identifiers. For example:

    .. code-block:: python

       # Direct access to a Cfg parameter.
       p = Cfg.getParamInt('katana.eventsLimit').setInt( 4000000 )

       # Setup of a CfgCache parameter.
       cache = CfgCache('')
       cache.katana.eventsLimit = 4000000

       # ...
       # Effective setting of the Cfg parameter.
       cache.apply()

    If a cache parameter is assigned to ``None``, it triggers the
    loading of the value from the disk, it it exists.

    .. code-block:: python

       # Setup of a CfgCache parameter.
       cache = CfgCache('')
       cache.katana.eventsLimit = None
       # The parameter will read it's value from the disk (4000000).


    This is done by overloading ``__setattr__()`` and ``__getattr__()``
    which recursively create CfgCache objects for intermediate levels
    attributes (in the previous example, a CfgCache for ``katana``
    will automatically be created). To separate between attributes
    that are part of configuration parameters and attributes belonging
    to CfgCache itself, we prepend a '_' to the laters.

    .. note:: It is important to understand the difference of behavior
              with ``Configuration``, the former set the parameters
              at once, it directly act on the ``Cfg`` settings.
              The later keep a state and set the ``Cfg`` parameters
              *only* when ``apply()`` is called.
    """

    def __enter__( self ):
        return self

    def __exit__( self, *args ):
        self.apply()
        self.display()

    def __init__ ( self, path='', priority=None ):
        """Create a new CfgCache with a ``path`` as parent path."""
        self._priority = priority
        self._path     = path
        self._rattr    = {}

    def __setattr__ ( self, attr, v ):
        """
        Recursively set an attribute. Attributes names starting by an '_' are
        treated as belonging to *this* object (self).

        How does the recursive attributes/CfgCache works? Assumes that we
        are doing:

        .. code-block:: python

           # Setup of a CfgCache parameter.
           cache = CfgCache('')
           cache.katana.eventsLimit = 4000000

        The explicit call sequence will be:

        .. code-block:: python

           cache.__getattr__('katana').__setattr__( 'eventsLimit', 4000000 )

        1. For the intermediate hierarchy level ``katana``, it is __getattr__()
           which is called, if the attribute do not exists, we create a new
           CfgCache().

        2. Second, and only then, __setattr__() is called, which will create a
           parameter entry named ``eventsLimit``.

        The decision of whether create a parameter entry *or* a CfgCache
        intermediate level will always be correctly handled because prior
        to any access, an attribute needs to be set. So we always have
        first a call chain of __getattr__() with one final __setattr__().
        For any subsequent access to ``cache.katana.eventsLimit``, as
        the attribute already exists, there is no type creation problem.
        """
        if attr[0] == '_':
            object.__setattr__( self, attr, v )
            return
        vRange = None
        vEnum  = None
        if isinstance(v,list ): vRange = v; v = None
        if isinstance(v,tuple): vEnum  = v; v = None
        if not self._rattr.has_key(attr):
            self._rattr[ attr ] = CachedParameter( self._path+'.'+attr, v )
        if   vRange is not None: self._rattr[ attr ].vRange = vRange
        elif vEnum  is not None: self._rattr[ attr ].vEnum  = vEnum
        else: self._rattr[ attr ].v = v

    def __getattr__ ( self, attr ):
        """
        Get an attribute, if it doesn't exists, then we are in an intermediate
        level like ``katana``, so create a new sub CfgCache for that attribute.
        """
        if not self._rattr.has_key(attr):
            path = self._path+'.'+attr if len(self._path) else attr
            self._rattr[attr] = CfgCache( path, self._priority )
        if isinstance(self._rattr[attr],CachedParameter):
            return self._rattr[attr].v
        return self._rattr[attr]

    def _hasCachedParam ( self, elements ):
        if not self._rattr.has_key(elements[0]):
            return False
        if len(elements) == 1:
            return True
        rattr = self._rattr[ elements[0] ]
        if not isinstance(rattr,CfgCache):
            return False
        return rattr._hasCachedParam( elements[1:] )

    def hasCachedParam ( self, attr ):
        return self._hasCachedParam( attr.split('.') )

    def apply ( self, priority=None ):
        """Apply the parameters values stored in the cache to the ``Cfg`` database."""
        if priority is None: priority = self._priority
        if not len(self._path) and priority is not None:
            Cfg.Configuration.pushDefaultPriority( priority )
        for attrName in self._rattr.keys():
            if isinstance(self._rattr[attrName],CfgCache):
                self._rattr[attrName].apply()
                continue
            self._rattr[attrName].cacheWrite()
        if not len(self._path) and priority is not None:
            Cfg.Configuration.popDefaultPriority()
       #self.display()

    def display ( self ):
        """Print all the parameters stored in that CfgCache."""
        if not len(self._path):
            print( '  o  Applying configuration (CfgCache):' )
        for attrName in self._rattr.keys():
            if isinstance(self._rattr[attrName],CfgCache):
                self._rattr[attrName].display()
                continue
            print( '     - {}.{} = {}'.format(self._path,attrName,self._rattr[attrName]) )