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

__all__ = ('FileSpec', 'FileSpecs')

import os, re
from os import path
from ulib.base import seqof, make_prop

class FileSpec(object):
    """une spécification de fichier.

    Les caractères * et ? ont la même signification que pour fnmatch.
    XXX supporter aussi les spécifications de classes de caractères [...]
    et [!...]

    .../**/... signifie n'importe quel nombre de répertoires intermédiaire.

    .../ est équivalent à .../** mais signifie aussi que l'on veut matcher le
    répertoire, auquel cas l'attribut dirmatch vaut True.

    Une chaine vide ou contenant uniquement la chaine ** signifie que l'on veut
    matcher TOUS les fichiers.
    """
    _spec, spec = make_prop('_spec', setter=False)[:2]
    _pattern, pattern = make_prop('_pattern', setter=False)[:2]
    _dirmatch, dirmatch = make_prop('_dirmatch', setter=False)[:2]

    def __init__(self, spec, normcase=True, normpath=True):
        spec, pattern, dirmatch = self.build_pattern(spec, normcase, normpath)
        self._spec = spec
        self._pattern = pattern
        self._dirmatch = dirmatch

    RE_INVALID = re.compile(r'(?:[^/]\*\*|\*\*[^/])')
    RE_DUP_WCS = re.compile(r'\*\*(?:/\*\*)+')
    RE_LEADING_WC = re.compile(r'^\*\*/')
    RE_TRAILING_WC = re.compile(r'/\*\*$')
    def build_pattern(cls, spec, normcase=True, normpath=True):
        dirmatch = False
        if cls.RE_INVALID.search(spec) is not None:
            raise ValueError("Invalid file specification: %s" % spec)
        if spec in ('', '**'):
            # cas particulier: on matche tous les fichiers sans exception
            return '**', None, dirmatch
        if spec.endswith("/"):
            spec += '**'
            dirmatch = True
        if normcase: spec = path.normcase(spec)
        if normpath: spec = path.normpath(spec)
        pattern = spec
        # traiter '.', '[', ']'
        pattern = pattern.replace('.', r'\.')
        pattern = pattern.replace('[', r'\[')
        pattern = pattern.replace(']', r'\]')
        # traiter '?'
        pattern = pattern.replace('?', r'[^/]')
        # traiter '**'
        pattern = cls.RE_DUP_WCS.sub(r'/**/', pattern)
        pattern = pattern.replace('/**/', r'(?:/.+/)?')
        pattern = cls.RE_LEADING_WC.sub(r'^(?:.+/)?', pattern)
        pattern = cls.RE_TRAILING_WC.sub(r'(?:/.*)?$', pattern)
        # traiter '*'
        pattern = pattern.replace('*', r'[^/]*')
        # ajouter '$' si ce n'est déjà fait
        if not pattern.endswith('$'): pattern += '$'

        return spec, re.compile(pattern), dirmatch
    build_pattern = classmethod(build_pattern)

    def fix_path(cls, p, basedir=None):
        """Si basedir n'est pas None, et que c'est un préfixe de p, alors ce
        préfixe est enlevé de p.
        """
        if basedir is not None:
            if not basedir.endswith('/'): basedir += '/'
            if p.startswith(basedir): p = p[len(basedir):]
        return p
    fix_path = classmethod(fix_path)

    def matches(self, p, basedir=None):
        """Tester si le chemin p correspond à cette spécification.

        Si basedir n'est pas None, et que c'est un préfixe de p, alors ce
        préfixe est enlevé de p avant de tester la correspondance.
        """
        pattern = self._pattern
        if pattern is None: return True
        p = self.fix_path(p, basedir)
        return pattern.match(p) is not None

class FileSpecs(object):
    """Un ensemble de spécifications, d'inclusion et d'exclusion.

    par défaut, exclure les fichiers spéciaux de la recherche.
    """
    _includes, includes = make_prop('_includes', setter=False)[:2]
    _excludes, excludes = make_prop('_excludes', setter=False)[:2]
    _exclude_special, exclude_special = make_prop('_exclude_special')[:2]

    def ensure_spec(cls, spec):
        if not isinstance(spec, FileSpec): spec = FileSpec(spec)
        return spec
    ensure_spec = classmethod(ensure_spec)

    def __init__(self, includes=None, excludes=None, exclude_special=True):
        self._includes = [self.ensure_spec(include) for include in seqof(includes, [])]
        self._excludes = [self.ensure_spec(include) for include in seqof(excludes, [])]
        self._exclude_special = exclude_special

    def is_included(self, p, basedir=None):
        for include in self.includes:
            if include.matches(p, basedir):
                return True
        return False

    def is_excluded(self, p, basedir=None, dirmatch=None):
        for exclude in self.excludes:
            if exclude.matches(p, basedir):
                if dirmatch is None or exclude.dirmatch == dirmatch:
                    return True
        return False

    def matches(self, p, basedir=None):
        """Tester si le chemin p correspond à cette spécification.

        Si basedir n'est pas None, et que c'est un préfixe de p, alors ce
        préfixe est enlevé de p avant de tester la correspondance.
        """
        if self._exclude_special and not path.isfile(p) and not path.isdir(p):
            return False
        return self.is_included(p, basedir) and not self.is_excluded(p, basedir)

    def walk(self, p='.', files=True, dirs=False, topdown=True, onerror=None):
        """Parcourir le répertoire p comme os.walk()

        Seuls les fichiers ou les répertoires qui correspondent à la
        spécifiations sont retournés. Si dirs==True, inclure les répertoires
        dans le résultat. Si files==True, inclure les fichiers dans le
        résultat. dirs et files ne peuvent être tous les deux False.
        """
        if not dirs and not files:
            raise ValueError("dirs and files should not be both False")
        basedir = p
        for dp, dns, fns in os.walk(p, topdown, onerror):
            for dn in dns:
                p = path.join(dp, dn)
                if self.is_excluded(p, basedir, True):
                    dns.remove(dn)
                elif self.matches(p, basedir):
                    if dirs: yield p
            if files:
                for fn in fns:
                    p = path.join(dp, fn)
                    if self.matches(p, basedir): yield p
