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

"""Des fonctions pour gérer des lignes de texte.
"""

__all__ = ('BLines', 'Lines')

import os, re
from types import MethodType, FunctionType

from .base import isstr, isseq
from .uio import _s, _u, UnicodeIO, defaultIO
from .encdetect import detect_line_encoding, guess_line_encoding
from .functions import apply_args

class BLines(list):
    """Une liste de chaines de type str.
    """
    _valid = False
    def is_valid(self): return self._valid
    valid = property(is_valid)

    _nl = None
    def get_nl(self): return self._nl
    def set_nl(self, nl): self._nl = nl
    nl = property(get_nl, set_nl)

    def __strip_nl(self, line):
        nl = ''
        while line[-1:] in ('\r', '\n'):
            nl = line[-1:] + nl
            line = line[:-1]
        if self._nl is None and nl:
            self._nl = nl
        return line

    def __add_nl(self, line):
        if self._nl is None: self._nl = os.linesep
        return line + self._nl

    def __init__(self, lines=None, nl=None):
        if nl is not None: self._nl = nl
        if lines is None:
            pass
        elif isseq(lines):
            self[:] = map(self.__strip_nl, lines)
            self._valid = True
            if self._nl is None: self._nl = os.linesep
        elif isstr(lines):
            self.appendtext(lines)
            if self._nl is None: self._nl = os.linesep
        else:
            raise ValueError("Unsupported type: %s" % type(lines))

    def reset(self):
        """Vider toutes les lignes de cet objet.
        """
        self[:] = []
        self._valid = False
        self._nl = None

    def _new(self, list=None):
        u"""Retourner une instance vide de cette classe, avec les mêmes paramètres pour nl.
        """
        return self.__class__(list, self._nl)

    def copy(self):
        """Faire et retourner une copie de cet objet.
        """
        return self._new(self)

    RE_NLs = re.compile(r'(\r?\n|\r)')
    def appendtext(self, text, strip_last_nl=False):
        """Ajouter un texte éventuellement sur plusieurs lignes.

        Si strip_last_nl, ignorer la dernière ligne vide.

        Equivalent en gros à self.extend(test.split(r'\r?\n|\r'))

        @return: self
        """
        lines = self.RE_NLs.split(text)
        if strip_last_nl and not lines[-1]: del lines[-1]
        i, max = 0, len(lines) - 1
        while i <= max:
            if i == max:
                line = lines[i]
                i += 1
            else:
                line = lines[i] + lines[i + 1]
                i += 2
            self.append(self.__strip_nl(line))
        return self

    def _after_readlines(self, lines, uio):
        return lines

    def readlines(self, inf, raise_exception=True, uio=None, open_func=None, until=None):
        """Lire les lignes du fichier.

        @param until: fonction qui si elle retourne True, arrête la lecture. La
            ligne qui arrête la lecture est incluse dans la liste. La signature
            de la fonction est until(line[, lines]).
        @return: self
        """
        self.reset()
        close_inf = False
        if isstr(inf):
            if open_func is None: open_func = open
            inf = open_func(inf, 'rb')
            close_inf = True
        try:
            try:
                if until:
                    lines = []
                    while True:
                        line = inf.readline()
                        if not line: break
                        line = self.__strip_nl(line)
                        lines.append(line)
                        if apply_args(until, line, lines): break
                else:
                    lines = map(self.__strip_nl, inf.readlines())
                lines = self._after_readlines(lines, uio)
                self.extend(lines)
                self._valid = True
            except IOError:
                if raise_exception: raise
        finally:
            if close_inf: inf.close()
        if self._nl is None: self._nl = os.linesep
        return self

    def _before_writelines(self, lines, uio):
        if uio is None: uio = defaultIO
        return map(uio.s, lines)

    def writelines(self, outf, uio=None, open_func=None):
        """Ecrire les lignes dans le fichier
        """
        close_outf = False
        if isstr(outf):
            if open_func is None: open_func = open
            outf = open_func(outf, 'wb')
            close_outf = True
        try:
            lines = self._before_writelines(self, uio)
            outf.writelines(map(self.__add_nl, lines))
        finally:
            if close_outf: outf.close()

    def _compile(self, forp):
        if isstr(forp): forp = re.compile(forp)
        return forp

    def __apply(self, func, line, index, lines, args):
        f = func
        ac_offset = 0
        if type(f) is MethodType:
            f = f.im_func
            ac_offset = 1
        if type(f) is not FunctionType:
            raise ValueError("func must be a function")
        argcount = f.func_code.co_argcount - ac_offset
        args = (line, index, lines, args)[:argcount]
        return func(*args)

    def _matches(self, forp, line, index=None, lines=None, args=None):
        if callable(forp): return self.__apply(forp, line, index, lines, args)
        else: return forp.match(line) is not None

    def grepi(self, forp, indexes=None, inverse=False, boundaries=False, **args):
        """forp étant une fonction ou une expression régulière, retourner une
        liste d'index de lignes pour lesquelles la fonction retourne True ou
        qui correspondent à l'expression régulière forp.

        Si inverse==True, la logique est inversée (on ne retourne que les
        indexes des lignes qui ne matches pas.)

        Si indexes n'est pas None, la recherche se limite aux lignes dont
        l'index est dans cette liste. Sinon, si boundaries==True, ne rechercher
        qu'au début et à la fin de la liste (i.e. s'arrêter de part et d'autre
        dès que la fonction retourne False ou que le pattern ne matche pas.)

        @return: une liste d'indexes
        @rtype: list
        """
        if indexes is None:
            indexes = range(len(self))
        else:
            # ne pas chercher sur les boundaries si on donne indexes
            boundaries = False

        forp = self._compile(forp)
        min = 0
        matches = []
        for index in indexes:
            line = self[index]
            found = self._matches(forp, line, index, self, args)
            if inverse: found = not found
            if found:
                matches.append(index)
            elif boundaries:
                min = index
                break
        if boundaries:
            index = len(self) - 1
            pos = len(matches)
            while index > min:
                line = self[index]
                found = self._matches(forp, line, index, self, args)
                if inverse: found = not found
                if found:
                    matches.insert(pos, index)
                    index -= 1
                else:
                    break
        return matches

    def grep(self, forp, indexes=None, inverse=False, boundaries=False, **args):
        """forp étant une fonction ou une expression régulière, retourner les
        lignes pour lesquelles la fonction retourne True ou qui correspondent
        à l'expression régulière forp.

        Consulter l'aide de la fonction grepi pour des détails sur les
        paramètres.

        @return: une liste de chaines
        @rtype: list
        """
        indexes = self.grepi(forp, indexes, inverse, boundaries, **args)
        return self._new(map(lambda i: self[i], indexes))

    def replace(self, pattern, repl, count=0, indexes=None):
        """Remplacer dans toute les lignes dont l'index est dans la liste
        indexes (toutes les lignes par défaut), l'expression régulière pattern
        par repl.

        @return: le nombre de remplacements effectués
        @rtype: int
        """
        if indexes is None: indexes = range(len(self))

        pattern = self._compile(pattern)
        nbrepl = 0
        for index in indexes:
            line = self[index]
            line, nb = pattern.subn(repl, line, count)
            self[index] = line
            nbrepl = nbrepl + nb

        return nbrepl

    def map(self, func, copy=False, **args):
        """Pour chacune des lignes, appeler la fonction func avec les arguments
        suivants dans cet ordre:

        line = la ligne
        index = l'index dans la liste
        lines = cet objet (ou sa copie)
        args = le dictionnaire des paramètres supplémentaires

        Si copy==True, opérer sur une copie de cet objet, sinon les lignes sont
        modifiées en place.

        @return: la liste résultat
        @rtype: list
        """
        if copy: lines = self.copy()
        else: lines = self

        for index in range(len(lines)):
            lines[index] = self.__apply(func, lines[index], index, lines, args)
        return lines

    def filter(self, forp, copy=False, inverse=False, boundaries=False, **args):
        """Pour chacune des lignes, essayer de matcher avec l'expression
        régulière forp, ou appeler la fonction forp avec les arguments
        suivants dans cet ordre:

        line = la ligne
        index = l'index dans la liste
        lines = cet objet (ou sa copie)
        args = le dictionnaire des paramètres supplémentaires

        Si l'expression régulière matche, ou si la fonction retourne True,
        la valeur est gardée, sinon elle est supprimée. Cette logique est
        inversée si inverse==True.

        Si copy==True, opérer sur une copie de cet objet, sinon les lignes sont
        modifiées en place. Si boundaries==True, n'opérer qu'au début et à la
        fin de la liste (i.e. s'arrêter de part et d'autre dès que la fonction
        retourne True.)

        @return: la liste résultat
        @rtype: list
        """
        if copy: lines = self.copy()
        else: lines = self

        forp = self._compile(forp)
        index = 0
        min = 0
        max = len(lines)
        while index < max:
            line = lines[index]
            match = self._matches(forp, line, index, lines, args)
            if inverse: match = not match
            if match:
                if boundaries:
                    min = index
                    break
                else:
                    index += 1
            else:
                del lines[index]
                max -= 1
        if boundaries:
            index = len(lines) - 1
            while index > min:
                line = lines[index]
                match = self._matches(forp, line, index, lines, args)
                if inverse: match = not match
                if match:
                    break
                else:
                    del lines[index]
                    index -= 1
        return lines

    def join(self, lastnl=False):
        """Obtenir les lignes jointes avec self.nl

        Si lastnl==True, la dernière ligne se termine par nl. Cela est
        approprié quand on veut par exemple générer un fichier.
        """
        if lastnl:
            return ''.join(map(self.__add_nl, self))
        else:
            nl = self._nl
            if nl is None: nl = os.linesep
            return nl.join(self)

class Lines(BLines):
    """Une liste de chaines de type unicode.
    """
    __use_defaultIO = []
    __dont_convert = []
    def detect_encoding(lines):
        encoding = detect_line_encoding(lines)
        if encoding is None:
            encoding = guess_line_encoding(lines,
                                           Lines.__use_defaultIO,
                                           Lines.__dont_convert)
        return encoding
    detect_encoding = staticmethod(detect_encoding)

    _uio = None
    def get_uio(self): return self._uio
    def set_uio(self, uio): self._uio = uio
    uio = property(get_uio, set_uio)

    _encoding_detector = detect_encoding
    def get_encoding_detector(self): return self._encoding_detector
    def set_encoding_detector(self, encoding_detector): self._encoding_detector = encoding_detector
    encoding_detector = property(get_encoding_detector, set_encoding_detector)

    def __init__(self, lines=None, nl=None, uio=None, encoding_detector=None):
        if uio is not None: self._uio = uio
        if encoding_detector is not None:
            self._encoding_detector = encoding_detector
        BLines.__init__(self, lines, nl)

    def _after_readlines(self, lines, uio):
        set_uio = uio is None
        if uio is None: uio = self._uio
        if uio is None:
            encoding_detector = self._encoding_detector
            if encoding_detector is not None:
                encoding = encoding_detector(lines)
                if encoding is self.__use_defaultIO:
                    uio = defaultIO
                    if set_uio: self._uio = uio
                elif encoding is self.__dont_convert:
                    pass
                else:
                    uio = UnicodeIO(encoding)
                    if set_uio: self._uio = uio
        if uio is not None:
            lines = map(uio.u, lines)
        return lines

    def _before_writelines(self, lines, uio):
        if uio is None: uio = self._uio
        if uio is None: uio = defaultIO
        return map(uio.s, lines)
