#!/usr/bin/env python
# -*- mode: python; coding: utf-8 -*-
#
# pygen.py
# --------
#
# Copyright (C) 2007 Jeremie Dimino <jeremie@dimino.org>
#
# This file 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 2 of the License, or
# (at your option) any later version.
#
# This file 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., 59 Temple Street #330, Boston, MA 02111-1307, USA.

'''Module utilisé par le plugin Python de bcfg2 pour générer un
fichier de configuration à partir d'un script python'''

__all__ = [ "Environment", "compileSource", "generate", "load" ]

import re, marshal, os, sys
from cStringIO import StringIO

# Pour l'arrêt d'un script
class Done(Exception):
    pass

def done():
    raise Done()

class Environment(dict):
    '''Environment dans lequel sont éxécuté les scripts'''

    # Le flux de sortie
    stream = None

    def __init__(self):
        # Création de l'environment initial
        dict.__init__(self, {
                # Les variables à "exporter" dans le fichier produit
                "exports": set([]),
                # Export d'une variable
                "export": self.export,
                # Dernière variable exportée à avoir été définie
                "last_definition": None,
                # Écrit: variable keysep tostring(value)
                "defvar": self.defvar,
                # La convertion en chaîne de charactère
                "tostring": self.tostring,
                # Définition des convertions
                "conv": {bool: {True: "yes", False: "no"},
                         list: lambda l: ", ".join([str(x) for x in l]),
                         tuple: lambda l: ", ".join([str(x) for x in l])},
                # Fonction de base pour imprimer quelque chose
                "out": self.out,
                # Arrêt du script
                "done": done,
                # Le séparateur pour la forme: variable keysep valeur
                "keysep": "=",
                # Le charactère de commentaire
                "comment_start": "#"})

    def __setitem__(self, variable, value):
        '''Lorsqu'on définie une variable, on "l'exporte" dans
        le fichier produit'''
        if variable in self["exports"]:
            self["defvar"](variable, value)
            dict.__setitem__(self, "last_definition", variable)
        dict.__setitem__(self, variable, value)

    def defvar(self, variable, value):
        '''Fonction par défaut pour gérée la définition des variables exportée.
        écrit 'variable = tostring(value)' dans le fichier produit'''
        self["out"]("%s%s%s\n" % (variable, self["keysep"], self["tostring"](value)))

    def out(self, string):
        '''Fonction de base pour écrire une chaine dans le fichier produit'''
        self.stream.write(string)

    def tostring(self, value):
        '''Fonction de convertion objet python -> chaine dans un format sympa'''
        convertor = self["conv"].get(type(value))
        if convertor:
            if type(convertor) == dict:
                return convertor[value]
            else:
                return convertor(value)
        else:
            return str(value)

    # Cette fonction pourrait être écrite directement dans les scripts
    # mais elle est pratique donc on la met pour tout le monde
    def export(self, variable):
        '''Exporte une variable'''
        self["exports"].add(variable)

__re_special_line = re.compile(r"^([ \t]*)(@|%)(.*)$", re.MULTILINE)
__re_affectation = re.compile(r"([a-zA-Z_][a-zA-Z_0-9]*)[ \t]*=")
__re_space_sep = re.compile(r"([^ \t]*)[ \t]+=?(.*)")

def compileSource(source, filename="", logger = None):
    '''Compile un script'''
    # On commence par remplacer les lignes de la forme
    # @xxx par out("xxx")
    newsource = StringIO()
    start = 0
    for m in __re_special_line.finditer(source):
        newsource.write(source[start:m.start()])
        start = m.end()
        newsource.write(m.group(1))
        linetype = m.group(2)
        if linetype == "@":
            line = m.group(3).replace("\\", "\\\\").replace('"', '\\"')
            if line and line[0] == "#":
                newsource.write('out(comment_start + "')
                line = line[1:]
            else:
                newsource.write('out("')
            newsource.write(line)
            newsource.write('\\n")')
        elif linetype == "%":
            line = m.group(3)
            m = __re_affectation.match(line)
            if m:
                varname = m.group(1)
                newsource.write(line)
                newsource.write("; defvar('")
                newsource.write(varname)
                newsource.write("', tostring(")
                newsource.write(varname)
                newsource.write("))\n")
            else:
                m = __re_space_sep.match(line)
                newsource.write("defvar('")
                newsource.write(m.group(1))
                newsource.write("', tostring(")
                newsource.write(m.group(2))
                newsource.write("))\n")
    newsource.write(source[start:])
    if logger:
        try:
            logger.info(newsource.getvalue())
        except:
            print "Le logger de BCFG2 c'est de la merde, il refuse le non ascii."
            print "Voici ce que j'ai essayé de logguer."
            print newsource.getvalue()
    return compile(newsource.getvalue(), filename, "exec")

def generate(code, environment=None, logger = None):
    '''Évalue un script'''
    if type(code) == str:
        code = compileSource(code, logger = logger)
    if not environment:
        environment = Environment()
    environment.stream = StringIO()
    save_stdout = sys.stdout
    sys.stdout = environment.stream
    try:
        exec(code, environment)
    except Done, _:
        pass
    except Exception, exn:
        sys.stderr.write('code: %r\n' % code)
        sys.stdout = save_stdout
        raise
    sys.stdout = save_stdout
    return environment.stream.getvalue()

def load(fname, cfname=None, logger=None):
    '''Charge un script et le compile, en créant/utilisant un fichier de
    cache'''
    if not cfname:
        cfname = fname + 'c'
    if os.path.exists(cfname) and os.stat(fname).st_mtime <= os.stat(cfname).st_mtime:
        code = marshal.load(file(cfname))
    else:
        code = compileSource(file(fname).read(), fname, logger)
        cfile = open(cfname, "w")
        marshal.dump(code, cfile)
        cfile.close()
    return code

if __name__ == "__main__":
    result = generate(load(sys.argv[1]))

    print "resultat:"
    sys.stdout.write(result)
