"""
adesklets configfile module.
"""
from commands import set_charset

from pprint import PrettyPrinter
from itertools import chain
from fcntl import lockf, LOCK_UN, LOCK_EX
import compiler
import mmap
import re

#-------------------------------------------------------------------------------
class ConfigImport:
    """
    This class makes use of the AST (Abstract Syntax Tree) generated
    by the compiler package parsing an arbitrary string of
    python code to 'walk' through it until it finishes retrieving
    all the global variables completely determined at compile time.

    For instance, one could call:
    
    	ConfigImport.load('i=1; j={2:True,3:False}')

    To get back, as a dictionary:

    	{'i':1, 'j': {2:True,3:False}}

    This was written as a safe alternative to calls such as:

    	exec file('some_File','r') in some_dict

    It is indeed much safer, since no arbitrary code ever
    get executed (only parsed), and variadic symbols
    (i.e everything that is not a known constant at compile time)
    are all discarded.
    	
    """
    class _StaticConfigImport:
        def __call__(self,buf):
            return dict(self._group(
                self.start_walk(compiler.parse(buf).getChildNodes()[0])))
    
        def start_walk(self, node):
            assert(node.__class__==compiler.ast.Stmt)
            for child in node.getChildNodes():
                for result in self.walk(child):
                    if result:
                        yield result
                    
        def walk(self, node):
            return getattr(self,
                           '_%s' % node.__class__.__name__,
                           self._Discard)(node)

        def _Assign(self,node):
            return (self.walk(node.getChildNodes()[0]),\
                    self.walk(node.getChildNodes()[1]))
        
        def _AssName(self, node):
            return node.name
        
        def _Const(self, node):
            return node.value

        def _UnarySub(self, node):
            return -(node.getChildNodes()[0].value)
        
        def _Name(self, node):
            if node.name=='None':
                return None
            else:
                return [False,True][node.name=='True']
        
        def _Dict(self,node):
            return dict([(self.walk(k),self.walk(v))
                         for k,v in node.items])

        def _Tuple(self,node):
            return tuple([self.walk(i)
                         for i in node.nodes])

        def _List(self,node):
            return [self.walk(i)
                    for i in node.nodes]

        def _Discard(self,node):
            return []

        def _group(self,iterable):
            group=[]
            for item in iterable:
                group.append(item)
                if len(group)==2:
                    yield group
                    group=[]

    load=_StaticConfigImport()
    
#-------------------------------------------------------------------------------
class ConfigFile(dict):
    """
    This is a configuration dictionary base class; it takes
    care of reading and writing the configuration to a file
    transparently as needed.

    Thanks to Mike Pirnat <exilejedi@users.sourceforge.net>
    for the original idea and code this was copied from
    as well as a number of useful comments and tips.

    It has to be subclassed, so that cfg_default contains the
    default option mapping, and __doc__ the comment to be printed out
    at the beginning of the configuration file.

    For instance, one could define:

    class MyConfig(adesklets.ConfigFile):
    	\"\"\"
    	This is my configuration file description
    	\"\"\"
        cfg_default = { 'my_one_parameter' : 'some_value' }
        
        def __init__(self, id, filename):
            adesklets.ConfigFile.__init__(self, id, filename)

    And then call, from somewhere in its code:
        config = MyConfig(adesklets.get_id(),'/path/to/config_file')

    From there, all configuration read and write would be taken care of,
    and the parameter value could be accessed through
    config['my_one_parameter'].

    Parameter resolution works this way:
        - Parameters are first looked up in a textual representation of
        a dictionnary from the 'config_file' file named id##,
        ## being the ID number.
        - All undefined parameters are set to their default values from
        the cfg_default dictionary
        - Whenever a parameter is changed using something such as:
            config['my_one_parameter']='new_value'
        or anytime a load phase is over, all the parameters initially
        present in 'config_file' (NOT default ones) are pretty printed
        back to the file.
        - In the special case where no id## entry did exist in
        'config_file', the dictionary of default values is copied
        instead.

    This behavior ensure desklets writers being able to add or remove
    parameters on upgrade while preserving backward compatibility.

    """
    cfg_default = {}
    
    def __init__(self, id, filename):
        dict.__init__(self)
        if self.__class__==ConfigFile:
            raise TypeError, 'ConfigFile should always be subclassed'
        if len(self.cfg_default)==0:
            raise ValueError, 'cfg_default should not be empty'
        self._filename=filename
        self._id='id'+str(id)
        self._load_and_save()

    def __setitem__(self, key, value):
        """
        This was 'overloaded' since we wanted to make
        sure the configuration file was kept in sync
        with the live configuration data
        """
        if self.has_key(key):
            dict.__setitem__(self,key,value)
            self._load_and_save()
        else:
            raise KeyError,'Key does not already exist'
            
    def _load_and_save(self):
        # Get configuration file and size
        #
        try:
            f = file(self._filename,'r+')
        except IOError:
            f = file(self._filename,'w+')
        lockf(f,LOCK_EX)
        f.seek(0,2)
        f_size = f.tell()
        f.seek(0)

        # Get encoding, and set interpreter state accordingly
        #
        # See http://www.python.org/peps/pep-0263.html for details on this
        #
        self.charset='ASCII'
        for line in [f.readline(),f.readline()]:
            match=re.match('(.*\s)?coding[:=]\s*([-\w.]+)(\s.*)?',line)
            if match:
                self.charset=match.group(2);
        if self.charset!='ASCII':
            set_charset(self.charset)
        f.seek(0)

        # Now, get all the data from file
        #
        if f_size>0:
            buf=mmap.mmap(f.fileno(),f_size,mmap.MAP_PRIVATE)
            all= ConfigImport.load(buf[:])
            buf.close()
        else:
            all={}

        # Fill the dictionary as needed
        #
        if len(self)==0:
            result=self.cfg_default
            for key, value in all.iteritems():
                if key==self._id:
                    result=value
                    break
            for key, value in chain(self.cfg_default.iteritems(),
                                    result.iteritems()):
                dict.__setitem__(self,key,value)
            all[self._id]=result
        else:
            all[self._id]=dict(self)

        # Save it back
        #
        f.seek(0)
        print >> f, '# -*- coding: %s -*-' % self.charset
        pp = PrettyPrinter(width=80)
        if self.__doc__:
            print >> f , '\n'.join(['#'+x for x in self.__doc__.split('\n')])
        keys=all.keys(); keys.sort()
        for key in keys:
            print >> f, '%s = %s' % (str(key),
                                     pp.pformat(all[key]))
        f.truncate() 

        # Release the file
        #
        lockf(f,LOCK_UN)
        f.close()
