#!/usr/bin/env python2.7
# -*- coding: utf-8 -*-
"""Fournit une couche d'abstraction Python pour les fichiers du même
nom"""

import os
import sys
import re
import marshal
import cStringIO

from Bcfg2.Server.Plugin import Debuggable

from .PythonFactories import PythonFileFactory
import PythonEnv
import PythonTools

__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]+=?(.*)")

class PythonFile(Debuggable):
    """Classe représentant un fichier Python"""

    #: Permet de savoir si l'instance a déjà été initialisée
    initialized = False

    def __new__(cls, path, parent=None):
        """Si le fichier a déjà été enregistré dans la Factory, on
        le retourne, et on évite de réinstancier la classe.

        path est le chemin absolu du fichier"""

        path = os.path.normpath(path)

        file_instance = PythonFileFactory.get(path)
        if file_instance is None:
            file_instance = super(PythonFile, cls).__new__(cls)
            PythonFileFactory.record(path, file_instance)

        return file_instance

    def __init__(self, path, parent=None):
        """Initialisation, si non déjà faite"""

        if self.initialized:
            return

        super(self.__class__, self).__init__()

        #: A string containing the raw data in this file
        self.data = None

        #: Le chemin complet du fichier
        self.path = os.path.normpath(path)

        #: Le nom du fichier
        self.name = os.path.basename(self.path)

        #: Un logger
        self.logger = PythonTools.LOGGER

        #: Le plugin parent est pointé pour des raisons pratiques
        self.parent = parent

        #: C'est bon, c'est initialisé
        self.initialized = True

    def exists(self):
        """Teste l'existence du fichier"""
        return os.path.exists(self.path)

    def HandleEvent(self, event=None):
        """ HandleEvent is called whenever the FAM registers an event.

        :param event: The event object
        :type event: Bcfg2.Server.FileMonitor.Event
        :returns: None
        """
        if event and event.code2str() not in ['exists', 'changed', 'created']:
            return

        try:
            self.load()
        except IOError:
            err = sys.exc_info()[1]
            self.logger.error("Failed to read file %s: %s" % (self.name, err))
        except:
            err = sys.exc_info()[1]
            self.logger.error("Failed to parse file %s: %s" % (self.name, err))

    def __repr__(self):
        return "%s: %s" % (self.__class__.__name__, self.name)

    def load(self, refresh=True):
        """Charge le fichier"""
        if self.data is not None and not refresh:
            return

        try:
            directory = os.path.dirname(self.path)
            compiled_file = os.path.join(directory, ".%s.COMPILED" % (self.name,))

            if os.path.exists(compiled_file) and os.stat(self.path).st_mtime <= os.stat(compiled_file).st_mtime:
                self.data = marshal.load(open(compiled_file, 'r'))
            else:
                self.data = compileSource(open(self.path, 'r').read(), self.path, self.logger)
                cfile = open(compiled_file, "w")
                marshal.dump(self.data, cfile)
                cfile.close()
        except Exception as error:
            PythonTools.log_traceback(self.path, 'compilation', error, self.logger)

    def run(self, additionnal=None, environment=None):
        """Exécute le code"""
        if self.data is None:
            self.load(True)

        if additionnal is None:
            additionnal = {}

        if environment is None:
            environment = PythonEnv.SafeEnvironment(additionnal, self)

        # Lors de l'exécution d'un fichier, on inclut
        # toujours common (ie on l'exécute dans l'environnement)
        environment.include("common")

        try:
            exec(self.data, environment)
        except Exception:
            sys.stderr.write('code: %r\n' % (self.data,))
            raise

        return environment.stream.getvalue(), environment['info']

#+---------------------------------------------+
#|        Tools for compilation                |
#+---------------------------------------------+

def compileSource(source, filename="", logger=None):
    '''Compile un script'''
    # On commence par remplacer les lignes de la forme
    # @xxx par out("xxx")
    newsource = cStringIO.StringIO()
    start = 0

    # Parsing de goret : on boucle sur les lignes spéciales,
    # c'est-à-dire celles commençant par un @ ou un % précédé
    # par d'éventuelles espaces/tabs.
    for match in __RE_SPECIAL_LINE.finditer(source):
        # On prend tout ce qui ne nous intéresse pas et on l'ajoute.
        newsource.write(source[start:match.start()])

        # On redéfinit start.
        start = match.end()

        # On écrit le premier groupe (les espaces et cie)
        newsource.write(match.group(1))

        # Le linetype est soit @ soit %
        linetype = match.group(2)

        # @ c'est du print.
        if linetype == "@":
            # On prend ce qui nous intéresse, et on fait quelques remplacements
            # pour éviter les plantages.
            line = match.group(3).replace("\\", "\\\\").replace('"', '\\"')

            # Si la ligne est un commentaire, on la reproduit en remplaçant éventuellement
            # le # par le bon caractère.
            if line and line[0] == "#":
                newsource.write('out(comment_start + "')
                line = line[1:]

            # Sinon bah....
            else:
                newsource.write('out("')

            # On écrit ladite ligne
            newsource.write(line)

            # Et un superbe \n.
            newsource.write('")')

        # %, affectation.
        elif linetype == "%":
            # On récupère le reste.
            line = match.group(3)

            # On fait du matching clef/valeur
            match = __RE_AFFECTATION.match(line)
            if match:
                # Le nom est le premier groupe.
                # Et après c'est weird...
                varname = match.group(1)
                newsource.write(line)
                newsource.write("; defvar('")
                newsource.write(varname)
                newsource.write("', tostring(")
                newsource.write(varname)
                newsource.write("))\n")
            else:
                # Pareil, sauf que cette fois, ce qu'on fait a un sens.
                match = __RE_SPACE_SEP.match(line)
                newsource.write("defvar('")
                newsource.write(match.group(1))
                # Le tostring est facultatif.
                newsource.write("', tostring(")
                newsource.write(match.group(2))
                newsource.write("))\n")
    # On continue.
    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")
