# -*- coding: utf-8 -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
import ulib.base.i_need_py23

"""Modèles de fichiers
"""

__all__ = ('register_templates', 'get_registered_names', 'Templ', 'FileTempl',
           'CannotOverwriteError', 'UpdateNotSupportedError')

import os
from os import path
from stat import ST_MODE, S_IMODE, S_IXUSR, S_IXGRP, S_IXOTH

from ulib.base.base import updated, isseq
from ulib.base.uio import _s, _u
from ulib.base.env import get_default_encoding
from ulib.base.input import is_yes, check_interaction, I_INTER, I_NORMAL, I_AUTO
from ulib.base.output import etitle, estep, eend
from ulib.base.lines import Lines

TEMPL_REGISTRY = []

def register_templates(*templ_clss):
    for templ_cls in templ_clss:
        if templ_cls is not None and templ_cls not in TEMPL_REGISTRY:
            TEMPL_REGISTRY.append(templ_cls)
    if len(templ_clss) == 0: return None
    elif len(templ_clss) == 1: return templ_clss[0]
    else: return templ_clss

def get_registered_names():
    return [templ.TEMPLATE for templ in TEMPL_REGISTRY]

class CannotOverwriteError(StandardError):
    def __init__(self, file=None):
        self.file = file

class UpdateNotSupportedError(NotImplementedError):
    pass

def _PS(cls, *vps, **kw):
    u"""Créer et retourner VPS, ACPS, NCPS et ICPS pour la classe cls.

    Utilisation dans une classe dérivée:

        VPS, ACPS, NCPS, ICPS = Templ.PS('prop0', ncps=dict(prop0=parser))

    - VPS est la liste des propriétés valides (celles qui peuvent être
    initialisées lors de la création du template.)
    - ACPS est un dictionnaire qui associe à des propriétés la fonction à
    utiliser pour valider la saisie de l'utilisateur (None si pas de
    validation). La valeur de ces propriétés doit être demandée à
    l'utilisateur si le I_LEVEL courant est au moins I_AUTO.
    - NCPS, comme ACPS, pour le I_LEVEL I_NORMAL.
    - ICPS, comme ACPS, pour le I_LEVEL I_INTER.

    Dans la liste vps, si une propriété a un nom commençant par "-", la
    propriété est enlevée de la liste des propriétés valides.
    """
    if cls is None:
        VPS, ACPS, NCPS, ICPS = (), {}, {}, {}
    else:
        VPS, ACPS, NCPS, ICPS = cls.VPS, cls.ACPS, cls.NCPS, cls.ICPS
    if kw.has_key('acps'): ACPS = updated(ACPS, **kw['acps'])
    if kw.has_key('ncps'): NCPS = updated(NCPS, **kw['ncps'])
    if kw.has_key('icps'): ICPS = updated(ICPS, **kw['icps'])
    VPS = list(VPS)
    # construire la liste des propriétés valides
    for vp in vps:
        if vp.startswith("-"):
            # si vps contient un nom commençant par "-", enlever ce nom de
            # la liste des popriétés valides
            vp = vp[1:]
            if vp in VPS: VPS.remove(vp)
            if ACPS.has_key(vp): del ACPS[vp]
            if NCPS.has_key(vp): del NCPS[vp]
            if ICPS.has_key(vp): del ICPS[vp]
        else:
            # n'ajouter la propriété que si elle n'existe pas déjà
            if vp not in VPS: VPS.append(vp)
    # Les propriétés mentionnées dans ACPS, NCPS et ICPS sont aussi valides
    # et doivent être insérées dans VPS si elles n'y sont pas déjà.
    for cps in ACPS, NCPS, ICPS:
        for name in cps.keys():
            if name not in VPS:
                VPS.append(name)
    return tuple(VPS), ACPS, NCPS, ICPS

class Templ(object):
    u"""Un générateur de fichier ou répertoire suivant un modèle défini.

    Cette classe doit être dérivée et les méthodes create() et (éventuellement)
    update() doivent être implémentées.

    @since: python2.2
    """

    TEMPLATE = None
    FILENAME = None
    EXT = None

    def MATCHES(file): return False
    MATCHES = staticmethod(MATCHES)

    VPS, ACPS, NCPS, ICPS = _PS(None, 'nl', 'overwrite', 'encoding', 'executable',
                                 icps=dict(encoding=None, executable=is_yes))
    PS = classmethod(_PS)

    file = None
    dir, filename, basename, ext = None, None, None, None
    nl = u'\n'
    overwrite = False
    encoding = None
    executable = False

    def __init__(self, file, **properties):
        self.file = file
        self.dir, self.filename = path.split(self.file)
        self.basename, self.ext = path.splitext(self.filename)
        self.set_properties(properties)

    def _check_file(self, check_overwrite=False):
        if self.file is None:
            raise ValueError("file must be set")
        if check_overwrite and not self.overwrite and path.exists(self.file):
            raise CannotOverwriteError(self.file)

    def create(self):
        u"""Créer le fichier ou le répertoire destination selon le modèle.
        """
        raise NotImplementedError

    def update(self):
        u"""Mettre à jour le fichier ou le répertoire destination.

        Ce qui est mis à jour dépend du modèle.
        """
        raise UpdateNotSupportedError

    def configurable_properties(self):
        u"""Retourner la liste des propriétés à configurer pour le I_LEVEL courant
        """
        auto = check_interaction(I_AUTO)
        normal = check_interaction(I_NORMAL)
        inter = check_interaction(I_INTER)
        cps = []
        for vp in self.VPS:
            if auto and self.ACPS.has_key(vp): cps.append(vp)
            elif normal and self.NCPS.has_key(vp): cps.append(vp)
            elif inter and self.ICPS.has_key(vp): cps.append(vp)
        return [(name, getattr(self, name)) for name in cps]

    def is_valid_property(self, name):
        u"""Tester la validité d'un nom de propriété.
        """
        return name in self.VPS

    def set_properties(self, properties):
        u"""Initialiser les valeurs des propriétés

        @type properties: dict
        """
        for name, value in properties.items():
            if self.is_valid_property(name) and value is not None:
                self.set_property(name, value)
    def get_properties(self, *keys):
        u"""Retourner les valeurs des propriétés spécifiées

        @rtype: dict
        """
        properties = {}
        for key in self.VPS:
            if keys and not key in keys: continue
            properties[key] = getattr(self, key)
        return properties

    def __parse_value(self, name, value, cps):
        parsed = False
        if name in cps.keys():
            parser = cps[name]
            if parser is not None: value = parser(value)
            parsed = True
        return parsed, value

    def set_property(self, name, value):
        u"""Changer la valeur d'une propriété
        """
        if not self.is_valid_property(name):
            raise ValueError(_s(u"Propriété non supportée: %s" % _u(name)))
        for cps in (self.ICPS, self.NCPS, self.ACPS):
            parsed, value = self.__parse_value(name, value, cps)
            if parsed: break
        setattr(self, name, value)

class ObjectDict:
    def __init__(self, object, **kw):
        self.object = object
        self.dict = kw

    def __getitem__(self, key):
        if self.dict.has_key(key): return self.dict[key]
        else: return getattr(self.object, key)

class FileTempl(Templ):
    u"""Un générateur de fichier orienté ligne.

    Si le contenu du fichier est fixe, il est possible de le mettre dans un
    fichier, et initialiser TEMPLATE au nom de ce fichier. Cette classe est
    alors prête à l'emploi.

    Sinon, il faut surcharger get_lines() pour retourner les lignes du fichier
    à générer.
    """

    def readlines(self, template):
        u"""Lire et retourner les lignes du fichier template, dont le nom est
        exprimé relativement à ce package.
        """
        if not path.isabs(template):
            template = path.join(path.split(__file__)[0], template)
        return Lines().readlines(template)

    def gettext(self, text=None, **kw):
        u"""Remplacer dans text les occurences des variables avec leurs valeur
        prises dans cet objet.

        Les variables doivent avoir la syntaxe "%(nom)m" où m est le mode ("s",
        "i", etc.)
        """
        if text is None: text = u""
        return text % ObjectDict(self, **kw)

    def appendtext(self, lines=None, text=None, bnl=False, nl=False, **kw):
        u"""Ajouter la(les) ligne(s) de text à lines, en y remplaçant les
        occurences des variables par leur valeur.
        """
        if lines is None: lines = Lines()
        if text is not None:
            if bnl: lines.append("")
            lines.appendtext(text % ObjectDict(self, **kw))
            if nl: lines.append("")
        return lines

    MODE = None
    MODELINE_PREFIX = u"# "
    MODELINE = u"-*- coding: %(encoding)s%(mode)s -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=%(encoding)s"
    MODELINE_SUFFIX = u""

    _mode = None
    def get_mode(self):
        if self._mode is None: return u""
        else: return u" mode: %s" % self._mode
    mode = property(get_mode)

    def get_modeline(self, mode=None, nl=False):
        if mode is not None: self._mode = mode
        modeline = self.MODELINE_PREFIX + self.MODELINE + self.MODELINE_SUFFIX
        if nl: modeline += u"\n"
        return self.gettext(modeline)
    modeline = property(get_modeline)

    def get_lines(self):
        u"""Retourner les lignes du fichier à générer.
        """
        template = self.TEMPLATE
        if isseq(template): template = template[0]
        if template is not None: return self.readlines(template)
        else: raise NotImplementedError

    def fix_properties(self):
        u"""S'assurer de la valeurs des propriétés de cet objet avant leur
        utilisation par les méthodes gettext(), appendtext().
        """
        self._mode = self.MODE
        self.encoding = get_default_encoding(self.encoding)

    def get_content(self):
        u"""Retourner le contenu du fichier à générer.

        Par défaut, joindre puis retourner les lignes de get_lines().
        """
        return u''.join([line + self.nl for line in self.get_lines()])

    def create(self):
        def gen_templ():
            u"""générer le contenu du fichier dans self._content
            """
            self.fix_properties()
            self._content = self.get_content()
        def write_file():
            u"""écrire self._content dans le fichier self.file
            """
            outf = open(self.file, 'wb')
            try:
                outf.write(_s(self._content, self.encoding))
            finally:
                outf.close()
        def set_mode():
            u"""rendre exécutable le fichier
            """
            mode = S_IMODE(os.stat(self.file)[ST_MODE])
            os.chmod(self.file, mode | (S_IXUSR + S_IXGRP + S_IXOTH))

        etitle(self.file)
        try:
            self._check_file(True)

            estep(u"Génération du modèle", gen_templ)
            estep(u"Enregistrement du fichier", write_file)
            if self.executable:
                estep(u"Configuration du mode", set_mode)
        finally:
            eend()
