#!/usr/bin/env python
# -*- coding: utf-8 -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8

import os, sys, re, string
from os import path

from ulib.all import *

##################################################
# fonctions diverses

verbosity = 1
verbose = 2
normal = 1
quiet = 0

def _print(level, text, min_verbosity=normal):
    """afficher un texte avec un niveau d'indentation, si le niveau de verbosité
    est correct.

    Retourner 1 si le texte a été affiché, 0 sinon
    """
    global verbosity
    if verbosity < min_verbosity: return 0

    if level < 0: level = 0
    print "%s%s" % ("  " * level, text)
    return 1

debug = 0

def _debug(text):
    if debug: print text

def select_files(p, exclude_names=(), exclude_paths=(), basepath=None):
    """Construire récursivement la liste de tous les fichiers à partir du
    répertoire p, en excluant les répertoires dont le nom est donné dans
    excludes_names, ou dont le chemin relativement à p est donné dans
    exclude_paths

    Note: on exclue automatiquement les fichiers qui sont 'binaires'
    """
    if basepath is None:
        return select_files(p, exclude_names, exclude_paths, p)
    else:
        files = []
        for file in os.listdir(p):
            if fileext_is_binary(file): continue

            pf = path.join(p, file) # chemin absolu
            relpf = pf[len(basepath) + 1:] # chemin relativement à basepath. +1 pour le '/'

            ignore = 0
            for pattern in exclude_names:
                if matches_name(pattern, file, p):
                    ignore = 1
            else:
                for pattern in exclude_paths:
                    if matches_name(pattern, relpf, basepath):
                        ignore = 1
            if not ignore:
                if path.isdir(pf):
                    files.extend(select_files(pf, exclude_names, exclude_paths, basepath))
                else:
                    files.append(pf)
        return files

##################################################
# Gestion des répertoires d'inclusion

incpaths = None

def init_incpaths(ignore_env=0):
    """Initialiser incpaths, la liste globale des répertoires dans lesquels on
    cherche les fichiers d'inclusions.

    Lire la valeur de la variable d'environnement UPDATEINCPATH, sauf si on est
    dans un répertoire non déployé.

    Si on est dans un répertoire non déployé, s'assurer que le répertoire qui
    contient ce script est dans incpaths.
    """
    global incpaths
    if incpaths is None: incpaths = []

    if path.isdir(path.join(scriptdir, "test")):
        # ignorer UPDATEINCPATH si on est dans le répertoire de test
        ignore_env = 1

    if not ignore_env:
        incpaths = filter(None, string.split(os.environ.get('UPDATEINCPATH', ''), ':'))
    else:
        if scriptdir not in incpaths:
            incpaths.insert(0, scriptdir)
    _debug("init_incpaths()\n  incpaths=\n    %s" % string.join(incpaths, "\n    "))

def set_incpaths(p):
    """Soit un répertoire ou une liste de répertoires séparés par ':'

    Si p est un répertoire, l'ajouter à la liste des répertoires de recherche.
    Si p est une liste de répertoires, remplacer la liste actuelle par cette
    nouvelle liste.
    """
    global incpaths
    if incpaths is None: init_incpaths()

    if ':' in p:
        incpaths[:] = map(path.abspath, filter(None, string.split(p, ':')))
    else:
        p = path.abspath(p)
        if p not in incpaths:
            incpaths.append(p)
    _debug("set_incpaths()\n  incpaths=\n    %s" % string.join(incpaths, "\n    "))

def inc_abspath(file, local_incpaths=()):
    """Retourner le chemin absolu vers un fichier.

    Si file est un chemin relatif, il est recherché dans incpaths et dans
    local_incpaths
    """
    file = path.expanduser(path.expandvars(file))

    if path.isabs(file): return file
    if path.exists(file): return path.abspath(file)
    for incpath in incpaths:
        pf = path.join(incpath, file)
        if path.exists(pf): return pf
    else:
        for incpath in local_incpaths:
            pf = path.join(incpath, file)
            if path.exists(pf): return pf
    return path.abspath(file)

##################################################
# Gestion des lignes d'inclusion

class InterestingLine:
    def __init__(self, c=None):
        """Initialiser l'objet

        re_inc matche une ligne d'inclusion repliée

        re_start_inc matche une ligne d'inclusion dépliée de type 'start'

        re_end_inc match une ligne d'inclusion dépliée de type 'end'

        re_require match une ligne d'inclusion de type 'require'
        """
        self.re_comments = [
            re.compile(r'[ \t]*(?:r|R)(?:e|E)(?:m|M)'),
            re.compile(r'[ \t]*##'),
            re.compile(r'[ \t]*;;'),
            re.compile(r'[ \t]*//'),
            re.compile(r"[ \t]*''"),
            ]
        if c is None:
            self.re_inc = re.compile(r'(.*)@include[ \t]+(.+)')
            self.re_start_inc = re.compile(r'(.*)@inc\[(.+)')
            self.re_end_inc = re.compile(r'(.*)@inc\](.+)')
            self.re_require = re.compile(r'(.*)@require[ \t]+(.+)')
            self.re_provide = re.compile(r'(.*)@provide[ \t]+(.+)')
            self.c = '@'
        elif c in ('*', '@'):
            self.re_inc = re.compile(r'(.*)(?:@|\*)include[ \t]+(.+)')
            self.re_start_inc = re.compile(r'(.*)(?:@|\*)inc\[(.+)')
            self.re_end_inc = re.compile(r'(.*)(?:@|\*)inc\](.+)')
            self.re_require = re.compile(r'(.*)(?:@|\*)require[ \t]+(.+)')
            self.re_provide = re.compile(r'(.*)(?:@|\*)provide[ \t]+(.+)')
            self.c = c

        self.inc_templ = "%s%sinclude %s"
        self.start_inc_templ = "%s%sinc[%s"
        self.end_inc_templ = "%s%sinc]%s"
        self.require_templ = "%s%srequire %s"
        self.provide_templ = "%s%sprovide %s"

        self.top_level = True

    def copy(self):
        """faire une copie de cet objet
        """
        il = self.__class__(self.c)
        il.top_level = False
        return il

    def is_comment(self, s):
        """retourner vrai si s est un commentaire (matche l'une des expressions
        régulières de re_comment)
        """
        for re_comment in self.re_comments:
            if re_comment.match(s):
                return True
        return False

    def matches(self, line):
        """retourner vrai si line est une ligne 'intéressante': ligne
        d'inclusion repliée, dépliée de type 'start', dépliée de type 'end', de
        type 'require', ou de type 'provide'

        initialiser les informations sur cette ligne: mo, spaces, file
        """
        self.__is_inc = 0
        self.__is_start_inc = 0
        self.__is_end_inc = 0
        self.__is_require = 0
        self.__is_provide = 0

        mo = self.re_inc.match(line)
        if mo is not None:
            self.__is_inc = 1
        else:
            mo = self.re_start_inc.match(line)
            if mo is not None:
                self.__is_start_inc = 1
            else:
                mo = self.re_end_inc.match(line)
                if mo is not None:
                    self.__is_end_inc = 1
                else:
                    mo = self.re_require.match(line)
                    if mo is not None:
                        self.__is_require = 1
                    else:
                        mo = self.re_provide.match(line)
                        if mo is not None:
                            self.__is_provide = 1
        if mo is not None:
            before = mo.group(1)
            if self.is_comment(before):
                self.before = before
                self.file = mo.group(2)
                self.pf = inc_abspath(self.file)
            else:
                self.__is_inc = 0
                self.__is_start_inc = 0
                self.__is_end_inc = 0
                self.__is_require = 0
                self.__is_provide = 0
                mo = None
        self.mo = mo

        return mo is not None

    def is_inc(self, line=None):
        """Si line==None, retourner vrai si la ligne matchée par matches() est
        une ligne d'inclusion repliée.

        si line!=None, retourner vrai si la ligne est une ligne d'inclusion
        repliée dont le nom de fichier correspond à la ligne matchée par
        matches()
        """
        if line is None:
            return self.__is_inc
        else:
            mo = self.re_inc.match(line)
            if mo is not None:
                if mo.group(2) == self.file:
                    return 1
            return 0

    def inc(self):
        """retourner une ligne d'inclusion repliée construite avec self.before
        et self.file
        """
        return self.inc_templ % (self.before, self.c, self.file)

    def is_start_inc(self, line=None):
        """Si line==None, retourner vrai si la ligne matchée par matches() est
        une ligne d'inclusion dépliée de type 'start'.

        si line!=None, retourner vrai si la ligne est une ligne d'inclusion
        dépliée de type 'start' dont le nom de fichier correspond à la ligne
        matchée par matches()
        """
        if line is None:
            return self.__is_start_inc
        else:
            mo = self.re_start_inc.match(line)
            if mo is not None:
                if mo.group(2) == self.file:
                    return 1
            return 0

    def start_inc(self):
        """retourner une ligne d'inclusion dépliée de type 'start' construite
        avec self.before et self.file
        """
        return self.start_inc_templ % (self.before, self.c, self.file)

    def is_end_inc(self, line=None):
        """Si line==None, retourner vrai si la ligne matchée par matches() est
        une ligne d'inclusion dépliée de type 'end'.

        si line!=None, retourner vrai si la ligne est une ligne d'inclusion
        dépliée de type 'end' dont le nom de fichier correspond à la ligne
        matchée par matches()
        """
        if line is None:
            return self.__is_end_inc
        else:
            mo = self.re_end_inc.match(line)
            if mo is not None:
                if mo.group(2) == self.file:
                    return 1
            return 0

    def end_inc(self):
        """retourner une ligne d'inclusion dépliée de type 'end' construite
        avec self.before et self.file
        """
        return self.end_inc_templ % (self.before, self.c, self.file)

    def is_require(self, line=None):
        """Si line==None, retourner vrai si la ligne matchée par matches() est
        une ligne d'inclusion de type 'require'.

        si line!=None, retourner vrai si la ligne est une ligne d'inclusion de
        type 'require' dont le nom de fichier correspond à la ligne matchée par
        matches()
        """
        if line is None:
            return self.__is_require
        else:
            mo = self.re_require.match(line)
            if mo is not None:
                if mo.group(2) == self.file:
                    return 1
            return 0

    def require(self):
        """retourner une ligne d'inclusion de type 'require' construite avec
        self.before et self.file
        """
        return self.require_templ % (self.before, self.c, self.file)

    def is_provide(self, line=None):
        """Si line==None, retourner vrai si la ligne matchée par matches() est
        une ligne d'inclusion de type 'provide'.

        si line!=None, retourner vrai si la ligne est une ligne d'inclusion de
        type 'provide' dont le nom de fichier correspond à la ligne matchée par
        matches()
        """
        if line is None:
            return self.__is_provide
        else:
            mo = self.re_provide.match(line)
            if mo is not None:
                if mo.group(2) == self.file:
                    return 1
            return 0

    def provide(self):
        """retourner une ligne d'inclusion de type 'provide' construite avec
        self.before et self.file
        """
        return self.provide_templ % (self.before, self.c, self.file)

##################################################
# replier un fichier

def fold(file, level=0, il=None, recursive_update=0, parent_print_processing_maybe=None, **ignored):
    """Replier un fichier: c'est l'opération inverse de unfold_or_update
    """
    if il is None: il = InterestingLine()

    pf = inc_abspath(file)

    # vérifier la présence du fichier
    if not path.exists(pf):
        _print(level, "not found: %s" % pf)
        return

    # Il ne faut pas changer le répertoire courant en sortant de cette
    # fonction. Faire une copie d'abord
    cwd = os.getcwd()

    # Se placer dans le répertoire du fichier
    p, f = path.split(pf)
    os.chdir(p)

    try:
        tf = TextFile(pf, lines=BLines())
        try:
            old = tf.readlines()
        except:
            # impossible de lire le fichier
            _print(level, "unable to read: %s" % pf)
            return
        new = []

        printed = [0]
        def print_processing_maybe(level=level, file=file, printed=printed, parent_print_processing_maybe=parent_print_processing_maybe):
            if parent_print_processing_maybe is not None:
                parent_print_processing_maybe()
            if not printed[0]:
                _print(level, "processing: %s" % file)
                printed[0] = 1

        folding = 0 # est-on en train de replier?
        was_modified = 0
        for line in old:
            if not folding:
                if il.matches(line):
                    if il.is_require():
                        if recursive_update:
                            fold(il.file, level=level + 1,
                                 il=il.copy(), recursive_update=recursive_update,
                                 parent_print_processing_maybe=print_processing_maybe)
                        new.append(il.require())
                        if il.require() != line: was_modified = 1
                    elif il.is_provide():
                        if recursive_update:
                            fold(il.file, level=level + 1,
                                 il=il.copy(), recursive_update=recursive_update,
                                 parent_print_processing_maybe=print_processing_maybe)
                        new.append(il.provide())
                        if il.provide() != line: was_modified = 1
                    elif il.is_inc():
                        if recursive_update:
                            fold(il.file, level=level + 1,
                                 il=il.copy(), recursive_update=recursive_update,
                                 parent_print_processing_maybe=print_processing_maybe)
                        new.append(il.inc())
                        if il.inc() != line: was_modified = 1
                    elif il.is_start_inc():
                        if il.start_inc() != line: was_modified = 1
                        folding = 1

                    if was_modified: print_processing_maybe()
                else:
                    new.append(line)
            else:
                if il.is_end_inc(line):
                    if recursive_update:
                        fold(il.file, level=level + 1,
                             il=il.copy(), recursive_update=recursive_update,
                             parent_print_processing_maybe=print_processing_maybe)
                    print_processing_maybe()
                    _print(level, "f %s" % il.file, verbose)
                    new.append(il.inc())
                    was_modified = 1
                    folding = 0

        if folding:
            # on a trouvé une inclusion non terminée. ne pas modifier le fichier
            _print(level, "warning: %s include not properly ended, ignored" % il.file)
            was_modified = 0

        if was_modified:
            tf.writelines(new)
    finally:
        os.chdir(cwd)


##################################################
# déplier un fichier

MODIFIED = 2
UNMODIFIED = 1
ERROR = 0

def unfold_or_update(file, level=0, il=None, recursive_update=0, files=None, provided=None, parent_print_processing_maybe=None, **ignored):
    """déplier un fichier. Seul le fichier file est modifié le cas échéant si
    files!=None

    retourner MODIFIED si le fichier a été lu depuis le disque ou s'il a été
    modifié, UNMODIFIED s'il faut utiliser le cache de lecture files, ERROR si
    erreur (aucune modification effectuée)
    """
    if il is None: il = InterestingLine()

    pf = inc_abspath(file)

    # Si le fichier a déja été traité, nous pouvons retourner
    if files is not None and files.has_key(pf):
        return UNMODIFIED

    # liste des fichiers déjà traités.
    can_write = recursive_update # doit-on mettre à jour le fichier sur disque?
    if files is None:
        can_write = 1
        files = {}
    # liste des fichiers déjà inclus
    if provided is None:
        provided = {}

    printed = [0]
    def print_processing_maybe(level=level, file=file, printed=printed, parent_print_processing_maybe=parent_print_processing_maybe):
        if parent_print_processing_maybe is not None:
            parent_print_processing_maybe()
        if not printed[0]:
            _print(level, "processing: %s" % file)
            printed[0] = 1

    # vérifier la présence du fichier
    if not path.exists(pf):
        if level == 0:
            _print(level, "not found: %s" % pf)
        else:
            print_processing_maybe()
            _print(level - 1, "X %s (not found)" % file)
        return ERROR

    # Il ne faut pas changer le répertoire courant en sortant de cette
    # fonction. Faire une copie d'abord
    cwd = os.getcwd()

    # Se placer dans le répertoire du fichier
    p, f = path.split(pf)
    os.chdir(p)

    try:
        tf = TextFile(pf, lines=BLines())
        try:
            old = tf.readlines()
        except:
            # impossible de lire le fichier
            if level == 0:
                _print(level, "unable to read: %s" % pf)
            else:
                _print(level - 1, "X %s (unable to read)" % file)
            return ERROR
        new = []
        files[pf] = tf
        provided[pf] = 1

        unfolding = None # est-on en train de déplier?
        was_modified = 0
        for line in old:
            if unfolding is None:
                if il.matches(line):
                    if il.is_require():
                        # traiter le cas @require
                        _debug("require\n  file=%s\n  provided=\n    %s" % (il.file, string.join(provided.keys(), '\n    ')))
                        if not il.top_level and not provided.has_key(il.pf):
                            print_processing_maybe()
                            _print(level, "R %s (is required)" % il.file)
                        new.append(il.require())
                        if il.require() != line: was_modified = 1
                    elif il.is_provide():
                        # traiter le cas @provide
                        _debug("provide\n  file=%s\n  provided=\n    %s" % (il.file, string.join(provided.keys(), '\n    ')))
                        provided[il.pf] = 1
                        new.append(il.provide())
                        if il.provide() != line: was_modified = 1
                    elif il.is_inc():
                        # traiter le cas @include
                        if unfold_or_update(il.file, level=level + 1,
                                            il=il.copy(), recursive_update=recursive_update,
                                            files=files, provided=provided,
                                            parent_print_processing_maybe=print_processing_maybe):
                            print_processing_maybe()
                            _print(level, "U %s" % il.file, verbose)
                            new.append(il.start_inc())
                            new.extend(files[il.pf].lines)
                            new.append(il.end_inc())
                            was_modified = 1
                        else:
                            new.append(il.inc())
                            if il.inc() != line: was_modified = 1
                    elif il.is_start_inc():
                        unfolding = []
                        if il.start_inc() != line: was_modified = 1
                else:
                    new.append(line)
            else:
                if il.is_end_inc(line):
                    status = unfold_or_update(il.file, level=level + 1,
                                              il=il.copy(), recursive_update=recursive_update,
                                              files=files, provided=provided,
                                              parent_print_processing_maybe=print_processing_maybe)
                    if status == MODIFIED:
                        if unfolding != files[il.pf].lines:
                            print_processing_maybe()
                            _print(level, "u %s" % il.file, verbose)
                            was_modified = 1
                        new.append(il.start_inc())
                        new.extend(files[il.pf].lines)
                        new.append(il.end_inc())
                        if il.end_inc() != line: was_modified = 1
                    elif status == UNMODIFIED:
                        new.append(il.start_inc())
                        new.extend(files[il.pf].lines)
                        new.append(il.end_inc())
                        if il.end_inc() != line: was_modified = 1
                    elif status == ERROR:
                        print_processing_maybe()
                        _print(level, "X %s (not found)" % il.file)
                        new.append(il.start_inc())
                        new.extend(unfolding)
                        new.append(il.end_inc())
                        if il.end_inc() != line: was_modified = 1
                    unfolding = None
                else:
                    unfolding.append(line)

        if unfolding is not None:
            # on a trouvé une inclusion non terminée. ne pas modifier le fichier
            print_processing_maybe()
            _print(level, "warning: %s include not properly ended, ignored" % il.file)
            was_modified = 0

        if was_modified:
            if new == tf.lines:
                return UNMODIFIED
            print_processing_maybe()
            tf.lines[:] = new
            if can_write:
                tf.writelines()

        return MODIFIED
    finally:
        os.chdir(cwd)

##################################################
# lecture des paramètres

def update_inc_params(args=None, basedir=None):
    if args is None: args = sys.argv[1:]

    global verbosity, debug, incpaths
    pr = {'action': unfold_or_update,
          'exclude_names':  ['CVS/', '.svn/'],
          'exclude_paths':  [],
          'il':  None,
          'recursive_update':  0,
          }

    args_read = False
    while not args_read:
        config_file = None

        opts, args = get_args(args, 'qvDuRfx:X:I:*@C:',
                              ['quiet', 'verbose', 'debug',
                               'update', 'unfold', 'recursive-update',
                               'fold',
                               'exclude-names=', 'exclude-paths', 'include-paths',
                               'config-file=', 'basedir=',
                               ])

        for opt, value in opts:
            if opt in ('-q', '--quiet'):
                verbosity = quiet
            elif opt in ('-v', '--verbose'):
                verbosity = verbose
            elif opt in ('-D', '--debug'):
                debug = 1
            elif opt in ('-u', '--update', '--unfold'):
                pr['action'] = unfold_or_update
            elif opt in ('-R', '--recursive-update'):
                pr['recursive_update'] = 1
            elif opt in ('-f', '--fold'):
                pr['action'] = fold
            elif opt in ('-x', '--exclude-names'):
                if ':' in value:
                    pr['exclude_names'] = filter(None, string.split(value, ':'))
                else:
                    pr['exclude_names'].append(value)
            elif opt in ('-X', '--exclude-paths'):
                if ':' in value:
                    pr['exclude_paths'] = filter(None, string.split(value, ':'))
                else:
                    pr['exclude_paths'].append(value)
            elif opt in ('-I', '--include-paths'):
                if incpaths is None: init_incpaths()
                set_incpaths(value)
            elif opt in ('-*',):
                pr['il'] = InterestingLine('*')
            elif opt in ('-@',):
                pr['il'] = InterestingLine('@')
            elif opt in ('-C', '--config-file'):
                config_file = value
            elif opt in ('--basedir',):
                basedir = path.abspath(value)

        if config_file is not None:
            cf = ShConfigFile(config_file, raise_exception=False)
            if cf.is_valid():
                args = split_args(cf.get('update_inc_options', '')) + split_args(cf.get('update_inc_args', '')) + args
            else:
                ewarn("Impossible de lire le fichier de configuration: %s" % config_file)

        args_read = config_file is None

    # transformer tous les chemins de args en chemins absolus.
    if basedir is None: basedir = os.getcwd()
    for i in range(len(args)):
        args[i] = path.abspath(path.join(basedir, args[i]))

    return pr, args

def update_inc(pr, args=None, action=None):
    global incpaths
    if incpaths is None: init_incpaths()

    if action is not None:
        action = {'fold': fold,
                  'unfold': unfold_or_update,
                  'update': unfold_or_update,
                  'unfold_or_update': unfold_or_update,
                  }.get(action, None)
    if action is None: action = pr['action']

    if args is None: args = []
    for arg in args:
        if path.isdir(arg):
            for file in select_files(arg, pr['exclude_names'], pr['exclude_paths']):
                action(file, il=pr['il'], recursive_update=pr['recursive_update'])
        else:
            action(arg, il=pr['il'], recursive_update=pr['recursive_update'])

if __name__ == '__main__':
    pr, args = update_inc_params()
    update_inc(pr, args)
