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

u"""Afficher les lignes d'un fichier en mettant en surbrillance certains patterns"""

import sys, subprocess, re
from collections import OrderedDict

def bluef(line, *ignored): return '\x1B[34m%s\x1B[0m' % line
def greenf(line, *ignored): return '\x1B[32m%s\x1B[0m' % line
def yellowf(line, *ignored): return '\x1B[33m%s\x1B[0m' % line
def redf(line, *ignored): return '\x1B[31m%s\x1B[0m' % line
def nonef(line, *ignored): return re.sub('\x1B\[.*?m', '', line)

DEFAULT_PATTERNS = OrderedDict([
    (r'(?i)error', redf),
    (r'(?i)warn(ing)?', yellowf),
    (r'(?i)info', bluef),
    (None, nonef),
])
FORMATS = OrderedDict([
    ('blue', bluef),
    ('green', greenf),
    ('yellow', yellowf),
    ('red', redf),
    ('none', nonef),
])
FORMAT_ALIASES = {
    'b': 'blue',
    'g': 'green',
    'y': 'yellow',
    'r': 'red',
    '': 'none',
}

def strip_nl(s):
    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

def run_tailor(inputfile=None, follow=False, patterns=None):
    if inputfile is None or not follow:
        if inputfile is None:
            inf = sys.stdin
            close = False
        else:
            inf = open(inputfile, 'rb')
            close = True
        def next_line():
            try:
                while True:
                    try: line = inf.readline()
                    except: break
                    if line == '': break
                    yield line
            finally:
                if close: inf.close()
    else:
        def next_line():
            p = subprocess.Popen(
                ['tail', '-f', inputfile],
                stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
            while True:
                try: line = p.stdout.readline()
                except: break
                if line == '': break
                yield line
    if patterns is None: patterns = DEFAULT_PATTERNS

    for line in next_line():
        line = nonef(strip_nl(line))
        func = False
        mo = None
        for p, f in patterns.items():
            if p is None:
                func = f
                mo = None
                break
            mo = re.search(p, line)
            if mo is not None:
                func = f
                break
        if func is not False:
            line = func(line, mo)
            sys.stdout.write(line)
            sys.stdout.write('\n')

if __name__ == '__main__':
    from argparse import ArgumentParser, HelpFormatter
    if sys.argv[1:2] == ['--compat']:
        # Avec l'argument --compat, désactiver la classe FancyHelpFormatter qui
        # se base sur une API non documentée
        sys.argv = sys.argv[0:1] + sys.argv[2:]
        FancyHelpFormatter = HelpFormatter
    else:
        class FancyHelpFormatter(HelpFormatter):
            """Comme HelpFormatter, mais ne touche pas aux lignes qui commencent par les
            caractères '>>>'. Cela permet de mixer du texte formaté et du texte non
            formaté.
            """
            def _fill_text(self, text, width, indent):
                return ''.join([indent + line for line in text.splitlines(True)])
            def _split_lines(self, text, width):
                lines = ['']
                for line in text.splitlines():
                    if line.startswith('>>>'):
                        lines.append(line)
                        lines.append('')
                    else:
                        lines[-1] += '\n' + line
                lines = filter(None, lines)
                texts = []
                for line in lines:
                    if line.startswith('>>>'):
                        texts.append(line[3:])
                    else:
                        texts.extend(super(FancyHelpFormatter, self)._split_lines(line, width))
                return texts
    AP = ArgumentParser(
        usage=u"%(prog)s [-f] [INPUTFILE]",
        description=__doc__,
        formatter_class=FancyHelpFormatter,
    )
    AP.set_defaults(inputfile=None, follow=False, patterns=None, defaults=True)
    pattern_help = u"""\
Ajouter une spécification de pattern et le format dans lequel il doit être affiché.
Le format par défaut est red. Les formats valides sont:
>>>    %(formats)s""" % {
    'formats': ', '.join(FORMATS.keys()),
}
    AP.add_argument('-e', '--pattern', action='append', dest='patterns', metavar='PATTERN:FORMAT',
                    help=pattern_help)
    default_patterns = [u"%s:%s" % (p or '', f.__name__[:-1]) for (p, f) in DEFAULT_PATTERNS.items()]
    no_defaults_help = u"""\
Ne pas ajouter les patterns par défaut. Sans cette option, les patterns par défaut sont:
%(default_patterns)s""" % {
    'default_patterns': '\n'.join([u">>>    %s" % pattern for pattern in default_patterns]),
}
    AP.add_argument('-z', '--no-defaults', action='store_false', dest='defaults',
                    help=no_defaults_help)
    AP.add_argument('-f', '--follow', action='store_true', dest='follow',
                    help=u"Suivre le contenu du fichier spécifié")
    AP.add_argument('inputfile', metavar='INPUTFILE', nargs='?',
                    help=u"Fichier qu'il faut afficher ou dont il faut suivre le contenu")
    o = AP.parse_args()

    if o.patterns is None:
        patterns = DEFAULT_PATTERNS
    else:
        patterns = OrderedDict()
        for pf in o.patterns:
            mo = re.match('(.*):([a-zA-Z]*)$', pf)
            if mo is not None:
                p = mo.group(1)
                of = mo.group(2)
            else:
                p = pf
                of = 'red'
            if p == '': p = None
            f = of.lower()
            f = FORMAT_ALIASES.get(f, f)
            if f not in FORMATS:
                raise ValueError("%s: format invalide" % of)
            patterns[p] = FORMATS[f]
        if o.defaults:
            for p, f in DEFAULT_PATTERNS.items():
                patterns.setdefault(p, f)

    run_tailor(o.inputfile, o.follow, patterns)
