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

"""Génération de fichiers
"""

__all__=('register_templ', 'get_templ_names',
         'get_templc', 'get_templ',
         'CannotOverwriteError',
         'Templ',
         )

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

from ulib.base.base import isseq, seqof, sorted, odict
from ulib.base.uio import _u, _s
from ulib.base.lines import Lines
from ulib.base.input import is_yes
from ulib.base.output import etitle, ebegin, eend
from ulib.base.env import get_default_encoding
from ulib.base.args import Options

# classes de modèle indexées par nom de modèle
NREGISTRY = {}
# classes de modèle indexées par nom de fichier
FREGISTRY = {}
# classes de modèle indexées par extension de fichier
EREGISTRY = {}
# classes de modèle, par ordre de précédence
OREGISTRY = []

def register_templ(*templcs):
    """Enregistrer une ou plusieurs classes permettant de générer un fichier
    selon un modèle prédéfini.

    Une classe de modèle est identifiée par un nom unique, contenu dans sa
    propriété NAME. En fait, cette propriété peut-être une liste donnant le nom
    unique ainsi que les aliases possibles.

    La classe doit être dérivée de Templ.
    """
    for templc in templcs:
        if templc is None: continue
        elif not issubclass(templc, Templ):
            raise ValueError("%s is not a Templ subclass" % templc.__name__)
        for name in seqof(templc.NAME, ()):
            NREGISTRY[name] = templc
        for filename in seqof(templc.FILENAME, ()):
            FREGISTRY[filename] = templc
        for ext in seqof(templc.EXT, ()):
            EREGISTRY[ext] = templc
        OREGISTRY.insert(0, templc)

def get_templ_names():
    """Obtenir la liste des noms de modèles enregistrés, dans l'ordre de
    précédence.
    """
    names = []
    for templc in REGISTRY_ORDER:
        rnames.extend(seqof(templc.NAME, ()))
    return names

def get_templc(file=None, name=None):
    """Obtenir la classe de modèle qui peut générer le fichier file, ou dont
    le nom est name.
    """
    if name is not None:
        templc = NREGISTRY.get(name, None)
        if templc is not None: return templc
        else: raise ValueError("Invalid template name: %s" % name)

    if file is not None:
        file = path.abspath(file)
        _, filename = path.split(file)
        _, ext = path.splitext(filename)

        if filename is not None:
            templc = FREGISTRY.get(filename, None)
            if templc is not None: return templc

        if ext is not None:
            templc = EREGISTRY.get(ext, None)
            if templc is not None: return templc

        # Si on ne trouve pas par les indexes, essayer de matcher sur le chemin
        # complet, dans l'ordre de précédence.
        for templc in OREGISTRY:
            if templc.MATCHES(file):
                return templc

    # en dernière extrémité, générer un fichier vide
    return EmptyTempl

def get_templ(file=None, name=None):
    templc = get_templc(file, name)
    return templc(file)

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

_none = object()

class TemplProperty(object):
    name = None
    aliases = None
    takes_value = None
    varname = None
    initial = None
    value = None

    def __init__(self, propname, propdefs=None):
        if propname.startswith('-'):
            # ne pas tenir compte du - devant le nom de la propriété, qui
            # signifie qu'il faut la supprimer plutot que de l'ajouter.
            propname = propname[1:]
        if propdefs is None: propdefs = {}

        name = propname
        takes_value = False
        if name.endswith("="):
            name = name[:-1]
            takes_value = True
        propdef = propdefs.get(name, {})

        if propdef.has_key('name'):
            # Si on précise un autre nom, prendre celui-là
            name = propdef.get('name')
            if name.endswith("="):
                name = name[:-1]
                takes_value = True

        self.name = name
        self.aliases = seqof(propdef.get('aliases', ()))
        self.takes_value = takes_value
        self.varname = propdef.get('varname', name)
        self.initial = propdef.get('initial', _none)
        self.value = propdef.get('value', _none)

    def __remove_self(self, PROPERTIES):
        i = 0
        while i < len(PROPERTIES):
            if PROPERTIES[i].name == self.name:
                del PROPERTIES[i]
            else:
                i += 1

    def add_to(self, PROPERTIES, ALIASES):
        self.__remove_self(PROPERTIES)
        PROPERTIES.append(self)
        ALIASES[self.name] = self
        for alias in self.aliases:
            ALIASES[alias] = self

    def remove_from(self, PROPERTIES, ALIASES):
        self.__remove_self(PROPERTIES)
        if ALIASES.has_key(self.name):
            del ALIASES[self.name]
        for alias in self.aliases:
            if ALIASES.has_key(alias):
                del ALIASES[alias]

    def add_option(self, OPTIONS):
        options = []
        optnames = [self.name]
        optnames.extend(self.aliases)
        for optname in optnames:
            if self.takes_value:
                if len(optname) == 1: optname = optname + ':'
                else: optname = optname + '='
            options.append(optname)

        actionkw = {'name': self.varname}
        if self.initial is not _none: actionkw['initial'] = self.initial
        if self.value is not _none: actionkw['value'] = self.value
        action = Options.SetValue(**actionkw)

        OPTIONS.add_option(options, action)

class Templ(object):
    """Un générateur de fichier ou de répertoire.

    Cette classe doit être dérivée et sa méthode create() doit être implémentée.
    """

    # nom unique de la classe de modèle. En fait, cette propriété peut-être une
    # liste donnant le nom unique ainsi que les aliases possibles.
    NAME = None
    # Noms des fichiers qui sont gérés par cette classe.
    FILENAME = None
    # Extensions des fichiers qui sont gérés par cette classe.
    EXT = None

    # fonction permettant de matcher sur un nom absolu de fichier. Ceci permet
    # de faire un générateur pour un fichier en particulier.
    def MATCHES(absfile): return False
    MATCHES = staticmethod(MATCHES)

    file = None
    dir, filename, basename, ext = None, None, None, None
    editable = None

    optvalues, args = None, None

    def __init__(self, file, args=None, **kw):
        self.file = file
        self.dir, self.filename = path.split(self.file)
        self.basename, self.ext = path.splitext(self.filename)
        self.editable = file
        for name, value in kw.items():
            self.set_property(name, value)
        if args is not None: self.parse_args(args)

    # convertisseurs pour les propriétés
    def to_str(v, emptyIsNone=False):
        if v is None: return None
        elif emptyIsNone and not v: return None
        else: return _u(v)
    to_str = staticmethod(to_str)
    def to_bstr(v, emptyIsNone=True):
        if v is None: return None
        elif emptyIsNone and not v: return None
        else: return _s(v)
    to_bstr = staticmethod(to_bstr)
    def to_bool(v):
        if v is None: return None
        else: return is_yes(v)
    to_bool = staticmethod(to_bool)

    ##################################################################
    # Gestion des propriétés

    # caractère de saut de ligne à utiliser pour les fichiers générés
    _nl = u'\n'
    def get_nl(self): return self._nl
    def set_nl(self, nl): self._nl = self.to_str(nl)
    nl = property(get_nl, set_nl)

    # est-il permis d'écraser un fichier existant?
    _overwrite = False
    def get_overwrite(self): return self._overwrite
    def set_overwrite(self, overwrite): self._overwrite = self.to_bool(overwrite)
    overwrite = property(get_overwrite, set_overwrite)

    # encoding à utiliser pour écrire le fichier, et éventuellement configurer
    # son modeline
    _encoding = None
    def get_encoding(self): return self._encoding
    def set_encoding(self, encoding): self._encoding = self.to_bstr(encoding)
    encoding = property(get_encoding, set_encoding)

    # le fichier généré doit-il être exécutable?
    _executable = False
    def get_executable(self): return self._executable
    def set_executable(self, executable): self._executable = self.to_bool(executable)
    executable = property(get_executable, set_executable)

    # fonction pour générer PROPERTIES, ALIASES et OPTIONS à partir d'une classe
    # de base
    def DECL_P(baseclass=None, *propnames, **propdefs):
        PROPERTIES = []
        ALIASES = {}
        if baseclass is not None:
            PROPERTIES.extend(baseclass.PROPERTIES)
            ALIASES.update(baseclass.ALIASES)

        for propname in filter(None, propnames):
            property = TemplProperty(propname, propdefs)
            if propname.startswith('-'):
                # supprimer la propriété
                property.remove_from(PROPERTIES, ALIASES)
            else:
                # ajouter la propriété
                property.add_to(PROPERTIES, ALIASES)

        OPTIONS = Options()
        for property in PROPERTIES:
            property.add_option(OPTIONS)

        return PROPERTIES, ALIASES, OPTIONS

    # définitions des propriété valides pour cette classe de modèle
    PROPERTIES, ALIASES, OPTIONS = \
        DECL_P(None,
               'nl=', 'overwrite', 'encoding=', 'executable', 'noexec',
               nl=dict(initial=_nl),
               overwrite=dict(initial=_overwrite, value=True, aliases='f'),
               encoding=dict(initial=_encoding, aliases=('enc', 'E')),
               executable=dict(initial=_executable, value=True, aliases=('exec', 'x')),
               noexec=dict(varname='executable', value=False, aliases='n'),
               )

    DECL_P = staticmethod(DECL_P)

    def __get_property(self, name):
        property = self.ALIASES.get(name, None)
        if property is None:
            raise ValueError("Invalid property: %s" % name)
        return property

    def get_property(self, name):
        property = self.__get_property(name)
        return getattr(self, property.name)

    def set_property(self, name, value):
        property = self.__get_property(name)
        setattr(self, property.name, value)

    def get_properties(self, *names):
        properties = {}
        for name in names:
            property = self.__get_property(name)
            properties[name] = getattr(self, property.name)
        return 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 parse_args(self, args=None):
        if args is None: return
        self.optvalues, self.args = self.OPTIONS.parse(args)
        for property in self.PROPERTIES:
            name = property.name
            value = self.OPTIONS.get_value(property.varname)
            self.set_property(name, value)

    def create(self):
        """Créer le fichier ou le répertoire, selon les règles du modèle.
        """
        raise NotImplementedError

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):
    """Un générateur de fichier orienté lignes.

    Si le contenu du fichier est fixe, il suffit de dériver cette classe, de
    mettre le contenu dans un fichier nommé selon la première valeur de NAME,
    dans le même répertoire que son module. La classe est alors prête à
    l'emploi. Ne pas oublier de l'enregister avec register_templ() !

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

    def readlines(self, template):
        """Lire et retourner les lignes du fichier template, dont le chemin,
        s'il n'est pas absolu, est exprimé relativement au package de cette
        classe.
        """
        if not path.isabs(template):
            modulep = sys.modules[self.__class__.__module__]
            modulep = getattr(modulep, '__file__', None)
            if modulep is None: modulep = '.'
            else: modulep = path.split(modulep)[0]
            templcname = self.__class__.__name__
            template = path.join(modulep, templcname)
        return Lines().readlines(template)

    def gettext(self, text=None, **kw):
        """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):
        """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):
        """Retourner les lignes du fichier à générer.
        """
        template = self.NAME
        if isseq(template): template = template[0]
        if template is not None: return self.readlines(template)
        else: raise NotImplementedError

    def fix_properties(self):
        """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):
        """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 gen_templ(self):
        """générer le contenu du fichier dans self._content
        """
        self.fix_properties()
        self._content = self.get_content()
    def write_file(self):
        """é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(self):
        """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))

    def create(self):
        etitle(self.file)
        try:
            self._check_file(True)
            ebegin(u"Génération du modèle", self.gen_templ)
            ebegin(u"Enregistrement du fichier", self.write_file)
            if self.executable: ebegin(u"Configuration du mode", self.set_mode)
        finally:
            eend()

class EmptyTempl(FileTempl):
    """Un modèle qui génère un fichier vide
    """
    NAME = 'empty'

    def get_lines(self): return []

register_templ(EmptyTempl)
