# -*- coding: utf-8 -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8

"""Fonctions de base
"""

__all__ = ('myself', 'mydir', 'myname',
           'Undef',
           'updated', 'updatem', 'odict', 'ncdict',
           'required', 'nlistf', 'snlistf',
           'isnum', 'isflt', 'isbool', 'isbytes', 'isunicode', 'isstr',
           'isseq', 'seqof', 'listof', 'firstof',
           'all_matches', 'one_match',
           'strip_nl', 'norm_nl',
           'make_getter', 'make_setter', 'make_deleter', 'make_prop',
           'getattrs', 'setattrs', 'delattrs', 'make_delegate',
           )

import os, sys, re
from os import path
from types import IntType, LongType, FloatType, BooleanType
from types import StringType, UnicodeType, StringTypes

# Enlever le répertoire courant de sys.path
try: from nutools_config import CLEAN_SYSPATH
except ImportError: CLEAN_SYSPATH = True
if CLEAN_SYSPATH:
    def __clean_syspath():
        cwd = os.getcwd()
        sys.path = filter(lambda p: p not in ('', '.', cwd), sys.path)
    __clean_syspath()
    del __clean_syspath

# emplacement du script courant
myself = path.abspath(sys.argv[0])
mydir, myname = path.split(myself)

# Fonctions diverses

_undef = object()
class Undef(object):
    def sa(self, value, kw, name, default=_undef):
        """si value est Undef, récupérer la valeur avec le nom court name dans kw
        """
        if default is _undef: default = self
        if value is self and name is not None: value = kw.pop(name, self)
        if value is self: value = default
        return value

    def __nonzero__(self):
        return False
    def __len__(self):
        return 0
    def __lt__(self, other):
        if other: return True
        else: return False
    def __le__(self, other):
        return True
    def __eq__(self, other):
        if other: return False
        else: return True
    def __ne__(self, other):
        if other: return True
        else: return False
    def __gt__(self, other):
        if other: return False
        else: return True
    def __ge__(self, other):
        return True
    def __repr__(self):
        return 'Undef'
    def __call__(self):
        """créer une nouvelle instance de Undef, pratique pour un module qui veut
        utiliser sa propre valeur différente de la valeur globale
        """
        return self.__class__()
Undef = Undef()

def updated(dict=None, **kw):
    """Retourner une copie de dict mise à jour avec les éléments de kw
    """
    if dict is None: dict = {}
    else: dict = dict.copy()
    dict.update(kw)
    return dict

def updatem(dict=None, *dicts):
    """Mets à jour dict avec les dictionnaires dicts, et retourner dict
    """
    if dict is None: dict = {}
    for kw in dicts: dict.update(kw)
    return dict

class odict(dict):
    """dictionnaire qui supporte aussi l'accès aux propriétés comme des attributs
    """
    def __init__(self, dict=None, **kw):
        super(odict, self).__init__(**updated(dict, **kw))

    def __getattr__(self, name):
        try: return self[name]
        except KeyError: raise AttributeError(name)

    def __setattr__(self, name, value):
        if name in self.__dict__: self.__dict__[name] = value
        else: self[name] = value

    def __delattr__(self, name):
        try: del self[name]
        except KeyError: raise AttributeError(name)

    def copy(self):
        return self.__class__(super(odict, self).copy())

_none = object()
class ncdict(odict):
    """dictionnaire dont les clés sont insensibles à la casse
    """
    def __init__(self, dict=None, **kw):
        super(ncdict, self).__init__(**updated(dict, **kw))

    def __getitem__(self, key):
        if isstr(key): key = key.lower()
        return super(ncdict, self).__getitem__(key)

    def __setitem__(self, key, value):
        if isstr(key): key = key.lower()
        return super(ncdict, self).__setitem__(key, value)

    def __delitem__(self, key):
        if isstr(key): key = key.lower()
        return super(ncdict, self).__delitem__(key)

    def __getattr__(self, key):
        if isstr(key): key = key.lower()
        return super(ncdict, self).__getattr__(key)

    def __setattr__(self, key, value):
        if isstr(key): key = key.lower()
        return super(ncdict, self).__setattr__(key, value)

    def __delattr__(self, key):
        if isstr(key): key = key.lower()
        return super(ncdict, self).__delattr__(key)

    def has_key(self, key):
        if isstr(key): key = key.lower()
        return super(ncdict, self).has_key(key)

    def get(self, key, default=_none):
        if isstr(key): key = key.lower()
        if default is _none: return super(ncdict, self).get(key)
        else: return super(ncdict, self).get(key, default)

def _itemprop(i, name):
    def getter(self):
        return self._values[i]
    def setter(self, value):
        validator = self.VALIDATORS.get(name, None)
        if validator is not None: value = validator(value)
        self._values[i] = value
    return property(getter, setter)

def _fix_module(cls):
    try: cls.__module__ = sys._getframe(1).f_globals.get('__name__', '__main__')
    except (AttributeError, ValueError): pass
    return cls

def required(validator, name=None):
    if name is None: name = "The value"
    def wrapper(value):
        value = validator(value)
        if value is None: raise ValueError("%s is required" % name)
        else: return value
    return wrapper

def nlistf(name, *attrs, **kw):
    name = str(name)
    # attributs
    if len(attrs) == 1 and isstr(attrs[0]):
        attrs = attrs[0].replace(',', ' ').split()
    attrs = tuple(map(str, attrs))
    # validateurs
    validators = {}
    for attr, validator in kw.iteritems():
        if attr not in attrs:
            raise ValueError("Invalid validator attribute: %s" % attr)
        validators[attr] = validator

    template = ["""class %(name)s(object):
    __slots__ = ('_values')
    ATTRS = None
    VALIDATORS = None
    def reset(self):
        "Reinitialiser toutes les valeurs a None"
        self._values = [None] * len(self.ATTRS)
        return self
    def replace(self, *values, **kw):
        "Modifier des valeurs specifiques"
        for i, attr in enumerate(self.ATTRS[:len(values)]): setattr(self, attr, values[i])
        for attr, value in kw.iteritems(): setattr(self, attr, value)
        return self
    def init(self, *values, **kw):
        "Modifier toutes les valeurs de cet objet. Les valeurs non specifiees recoivent None."
        return self.reset().replace(*values, **kw)
    def __init__(self, *values, **kw): self.init(*values, **kw)
    def inito(self, o):
        "Modifier toutes les valeurs de cet objet en les prenant depuis les attributs de l'objet o."
        for attr in self.ATTRS: setattr(self, attr, getattr(o, attr, None))
    def update(self, d):
        "Mettre a jour le dictionnaire d avec les valeurs de cet objet"
        for attr in self.ATTRS: d[attr] = getattr(self, attr)
    def updateo(self, o):
        "Mettre a jour les attributs de l'objet o avec les valeurs de cet objet."
        for attr in self.ATTRS: setattr(o, attr, getattr(self, attr))
    def asdict(self): return dict(zip(self.ATTRS, self._values))
    def __repr__(self): return repr(self.asdict())
    def __len__(self): return len(self._values)
    def __getitem__(self, key): return self._values.__getitem__(key)
    def __setitem__(self, key, value): self._values.__setitem__(key, value)
    def __iter__(self): return self._values.__iter__()
    def __contains__(self, item): return self._values.__contains__(item)"""]
    for i, attr in enumerate(attrs):
        template.append("    %s = itemprop(%i, '%s')" % (attr, i, attr))
    template = "\n".join(template) % locals()
    namespace = dict(itemprop=_itemprop)
    try: exec template in namespace
    except SyntaxError, e: raise SyntaxError('%s:\n%s' % (e.message, template))

    cls = namespace[name]
    cls.ATTRS = attrs
    cls.VALIDATORS = validators
    return _fix_module(cls)

def snlistf(base, name, *attrs, **kw):
    name = str(name)
    # attributs
    if len(attrs) == 1 and isstr(attrs[0]):
        attrs = attrs[0].replace(',', ' ').split()
    attrs = tuple(map(str, attrs))
    allattrs = base.ATTRS + attrs
    # validateurs
    validators = base.VALIDATORS.copy()
    for attr, validator in kw.iteritems():
        if attr not in allattrs:
            raise ValueError("Invalid validator attribute: %s" % attr)
        validators[attr] = validator

    template = ["""class %(name)s(base):
    __slots__ = ()
    ATTRS = None
    VALIDATORS = None"""]
    basei = len(base.ATTRS)
    for i, attr in enumerate(attrs):
        template.append("    %s = itemprop(%i, '%s')" % (attr, basei + i, attr))
    template = "\n".join(template) % locals()
    namespace = dict(base=base, itemprop=_itemprop)
    try: exec template in namespace
    except SyntaxError, e: raise SyntaxError('%s:\n%s' % (e.message, template))

    cls = namespace[name]
    cls.ATTRS = allattrs
    cls.VALIDATORS = validators
    return _fix_module(cls)

def isnum(i):
    """Tester si i est une valeur numérique (int ou long)
    """
    return type(i) in (IntType, LongType)
def isflt(f):
    """Tester si f est une valeur numérique flottante (float)
    """
    return type(f) is FloatType
def isbool(b):
    """Tester si b est une valeur booléenne
    """
    return type(b) is BooleanType
def isseq(t):
    """Tester si t est une séquence (list ou tuple)
    """
    return isinstance(t, list) or isinstance(t, tuple)
def seqof(seq, ifNone=Undef, nocopy=False):
    """Retourner une séquence.
    Si seq est une séquence, retourner une copie de l'objet si nocopy==False,
    sinon l'objet lui-même.
    Si seq==None: si ifNone est défini, retourner ifNone, sinon un tuple vide.
    Sinon, retourner le tuple (seq,)
    """
    if isseq(seq):
        if nocopy: return seq
        else: return seq[:]
    elif seq is None:
        if ifNone is Undef: return ()
        else: return ifNone
    else: return (seq,)
def listof(seq, ifNone=Undef):
    """Retourner une liste.
    Si seq est une séquence, retourner la liste correspondante
    Si seq==None: si ifNone est défini, retourner ifNone, sinon une liste vide.
    Sinon, retourner la liste [seq]
    """
    if seq is None:
        if ifNone is Undef: return []
        else: return ifNone
    elif isseq(seq): return list(seq)
    else: return [seq]
def firstof(seq):
    """Retourner le premier élément de la séquence.
    Si seq n'est pas une séquence, retourner l'objet lui-même.
    Si seq est une séquence vide, retourner None.
    """
    if isseq(seq): return seq[0:1] and seq[0] or None
    else: return seq
def isbytes(s):
    """Tester si s est une valeur chaine (str)
    """
    return type(s) is StringType
def isunicode(s):
    """Tester si s est une valeur chaine (unicode)
    """
    return type(s) is UnicodeType
def isstr(s):
    """Tester si s est une valeur chaine (str ou unicode)
    """
    return type(s) in StringTypes

def all_matches(func, seq):
    """Tester si tous les éléments de seq sont matchés par la fonction func.
    """
    for item in seqof(seq):
        if not func(item): return False
    return True

def one_match(func, seq):
    """Tester si au moins un des éléments de seq est matché par la fonction
    func.
    """
    for item in seqof(seq):
        if func(item): return True
    return False

def strip_nl(s):
    """Enlever le caractère de fin de ligne de s: soit \\n, soit \\r, soit \\r\\n
    """
    if s is None: return None
    elif s.endswith("\r\n"): s = s[: - 2]
    elif s.endswith("\n"): s = s[: - 1]
    elif s.endswith("\r"): s = s[: - 1]
    return s

RE_NL = re.compile(r'(?:\r?\n|\r)')
def norm_nl(s, nl="\\n"):
    """Transformer tous les caractères de fin de ligne en \\n
    """
    if s is None: return None
    else: return RE_NL.sub(nl, s)

def make_getter(name):
    return lambda self: getattr(self, name)
def make_setter(name, validator=None):
    if validator is None:
        return lambda self, value: setattr(self, name, value)
    else:
        return lambda self, value: setattr(self, name, validator(value))

def make_deleter(name):
    return lambda self: delattr(self, name)

def make_prop(name, value=None, getter=True, setter=True, deleter=False, validator=None):
    """Retourne un tuple facilitant la création d'une propriété protégée par
    des accesseurs.

    Voici un exemple d'usage:

        class C:
            _name, name, get_name, set_name = make_prop('_name', 'Default value')

    @return: (value, property, getter_func, setter_func, deleter_func)
    """
    accessors = {}
    if getter in (False, None): pass
    elif getter is True: getter = make_getter(name)
    if getter: accessors['fget'] = getter
    if setter in (False, None): pass
    elif setter is True: setter = make_setter(name, validator)
    elif validator is not None:
        _setter = setter
        setter = lambda self, value: _setter(self, validator(value))
    if setter: accessors['fset'] = setter
    if deleter in (False, None): pass
    elif deleter is True: deleter = make_deleter(name)
    if deleter: accessors['fdel'] = deleter
    result = [value, property(**accessors)]
    if getter: result.append(accessors['fget'])
    if setter: result.append(accessors['fset'])
    if deleter: result.append(accessors['fdel'])
    return tuple(result)

def __check_names(names):
    if not names: raise AttributeError("The attribute name is required")

def getattrs(obj, names, strict=False):
    u"""Soit un objet obj, et un nom de la forme "attr0.attr1....",
    retourner l'objet obtenu avec l'expression obj.attr0.attr1....

    @param strict: on requière que toute l'expression soit parcouru jusqu'à la
        fin. Sinon, arrêter dès que le résultat de l'expression est None.
    """
    if not names: return obj
    if not isseq(names): names = names.split(".")
    __check_names(names)
    value = obj
    for i in range(len(names)):
        name = names[i]
        if value is None:
            if strict:
                if i > 0: path = "obj." + ".".join(names[:i])
                else: path = "None"
                raise AttributeError("%s instance has no value '%s'" % (path, name))
            else: break
        value = getattr(value, name)
    return value

def setattrs(obj, names, value):
    u"""Soit un objet obj, et un nom de la forme "attr0.attr1....",
    effectuer l'équivalent de l'opération:

        obj.attr0.attr1.... = value
    """
    if not isseq(names): names = names.split(".")
    __check_names(names)
    obj = getattrs(obj, names[:-1], True)
    setattr(obj, names[-1], value)

def delattrs(obj, names):
    u"""Soit un objet obj, et un nom de la forme "attr0.attr1....",
    effectuer l'équivalent de l'opération:

        del obj.attr0.attr1....
    """
    if not isseq(names): names = names.split(".")
    __check_names(names)
    obj = getattrs(obj, names[:-1], True)
    delattr(obj, names[-1])

def make_delegate(names, getter=True, setter=True, deleter=False):
    if getter is True:
        def getter(self):
            return getattrs(self, names, True)
    if setter is True:
        def setter(self, value):
            setattrs(self, names, value)
    if deleter is True:
        def deleter(self):
            delattrs(self, names)

    accessors = {}
    if getter: accessors['fget'] = getter
    if setter: accessors['fset'] = setter
    if deleter: accessors['fdel'] = deleter
    return property(**accessors)

def get__all__(module):
    """Retourner la valeur __all__ d'un module, ou la construire si cette
    valeur n'est pas définie.

    @rtype: tuple
    """
    all = getattr(module, '__all__', None)
    if all is None:
        all = []
        for key in module.__dict__.keys():
            if key[0] != '_': all.append(key)
    return tuple(all)

def import__module__(module_name, globals, locals=None, name=None):
    """Importer dans globals le module nommé module_name, en le nommant name.

    Par défaut, name est le nom de base du module. par exemple, le module
    "a.b.c" sera importé sous le nom "c"
    """
    module = __import__(module_name, globals, locals)
    basenames = module_name.split('.')
    for basename in basenames[1:]:
        module = getattr(module, basename)

    if name is None: name = basenames[-1]
    globals[name] = module
    return [name]

def import__all__(module_name, globals, locals=None, *names):
    """Importer dans globals tous les objets du module nommé module_name
    mentionnés dans names. Si names est vides, tous les objets sont importés
    comme avec 'from module import *'
    """
    module = __import__(module_name, globals, locals)
    basenames = module_name.split('.')
    for basename in basenames[1:]:
        module = getattr(module, basename)

    if not names: names = get__all__(module)
    __all__ = []
    for name in names:
        globals[name] = getattr(module, name, None)
        __all__.append(name)
    return __all__
