#==============================================================================
# Copyright (C) 2011 by Loïc Grobol
#
# This file is part of CTESLO.
# 
#    CTESLO is free software: you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation, either version 3 of the License, or
#    (at your option) any later version.
# 
#    CTESLO is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
# 
#    You should have received a copy of the GNU General Public License
#    along with CTESLO.  If not, see <http://www.gnu.org/licenses/>.
#==============================================================================

"""Text data files handling base library

Created on 18 mai 2011
@author: Loïc Grobol <loic.grobol@gmail.com>

This module is a base library for mid level 
text data files handling libraries. That is
handling data at the level of lines of data
instead of the level of text files.

a "text data file" means here a file encoding
tags of words (or lexemes) of a text in a column fashion.
An example should be more obvious.

    oui        oui        ADV    ADV      ADV
    en_effet   en_effet   ADV    ADV      ADV
    on         on         P      P3MS     P3MSPERSUJ
    peut       pouvoir    V      V3SINDP  V3SINDP
    commencer  commencer  V      VINF     VINF
    
    à          à          PREP   PREP     PREP
    ...
    
so the global syntax is

    word <coldel> data <coldel> data <coldel> tag <newline>
    ...
    
with a variable number of columns, <coldel> standing for
column delimiter. Usually <coldel> is the <TAB> character.

So it is basically a csv file with occasional blank
line containing data about texts.
    
The module contains the following classes

    - DataLine -- The base class for all the library.
    it represents a line in the data file. Actually
    a wrapper for a list that contains the actual data,
    but with a is_blank flag that mark blank lines in the
    file. Useful MutableSequence operations are available,
    though they will raise LookupError if the line is flagged
    as blank. They are build using a list only, build_x
    functions ensuring the building from a data string.
    
    - DataFileStream -- A stream-like object for dummy access
    to a data file. It is meant to be as similar to io module's I/O
    as possible, ie support iteration, read, write except that
    it returns DataLine objects instead of strings. It is also
    it's self content manager, to allow with statement constructions.
    
And it provides the following functions

    - build_DataLine/build_DataString -- Are inverse functions
    the former builds a DataLine from a string (typically a line
    in a data file) while the latter build such a string from
    a DataLine.
        usage example:
            >>> DL = build_DataLine("oui oui ADV ADV ADV", delimiter=" ")
            >>> st = build_DataString(DL, delimiter=" ")
            >>> print(st)
            oui oui ADV ADV ADV
    
    - datafile_open -- Meant to be akin to io.open. It
    returns a DataFileStream context manager.
        usage example:
            >>> with datafile_open("spam", mode="r") as spam:
                    for line in spam:
                        print(build_DataString(line))
                        
And, last but not least the following module-level constants

    - DEFAULT_DELIMITER -- The default column delimiter, ie
    the character that separates distinct columns in the same line.
    <TAB> character (\t) by default.
    
Note that the option to use another column delimiter is open everywhere
in this module, but that for typical applications (that is to say 
Wapiti or CRF++ data files) the default delimiter should suffice.
Still, modules using this library should allow the user to chose a different
column delimiter.
 """

from collections import MutableSequence
import io

DEFAULT_DELIMITER = "\t"
STRIPABLES = " \n"

class DataLine(MutableSequence):
    """Abstraction for a line in a data file.
    
    Essentially a wraper for a list with a
    self.is_blank boolean flag to mark blank lines
    (block separators).
    
    Perhaps an empty non-blank line doesn't make
    much sense, though it is possible here.
    """
    def __init__(self, data=None, is_blank=False):
        """Constructor. Ignore %data if %is_blank."""
        self.is_blank = is_blank
        if is_blank:
            self.data = []
        else:
            if data:
                self.data = list(data)
            else:
                self.data = []

    # Token getters and setters. Typically redirects
    # to self.data ones. Dumb work below unavoidable
    # see docs.python.org/py3k/reference/datamodel.html
    # section 3.3.9 (special method lookup).
    # Methods inconsistent with a blank line expected
    # behavior raise LookupError if one try to use them.
    def __len__(self):
        return self.data.__len__()

    def __getitem__(self, key):
        """Return column #key.
        
        Raise LookupError if self.is_blank.
        """
        if self.is_blank:
            raise LookupError("Cannot get item from blank line")
        sliced_data = self.data[key]
        if isinstance(key, slice):
            return DataLine(sliced_data)
        return sliced_data

    def __setitem__(self, key, value):
        """Set column #key value.
        
        Raise LookupError if self.is_blank.
        """
        if self.is_blank:
            raise LookupError("Cannot set item from blank line")
        self.data[key] = value

    def __delitem__(self, key):
        """Delete column #key
        
        Raise LookupError if self.is_blank."""
        if self.is_blank:
            raise LookupError("Cannot delete item from blank line")
        del self.data[key]

    def __iter__(self):
        """Return an iterator for self.
        
        Raise LookupError if self.is_blank."""
        if self.is_blank:
                raise LookupError("Cannot iter over blank line")
        return self.data.__iter__()

    def __str__(self):
        if self.is_blank:
            return "DataLine(is_blank=True)"
        return "DataLine(" + str(self.data) + ")"

    def __repr__(self):
        if self.is_blank:
            return "<BLANK LINE>"
        if not self:
            return "<EMPTY LINE>"
        return repr(self.data)

    def __bool__(self):
        """A DataLine is False iff self.data is empty and
        self.is_blank is False."""
        return (len(self) != 0) or self.is_blank

    def insert(self, index, value):
        """Insert %value @ column #key.
        
        Raise LookupError if self.is_blank."""
        if self.is_blank:
            raise LookupError("Cannot insert item in a blank line")
        self.data.insert(index, value)

    def sort(self, key=None, reverse=False):
        self.data.sort(key=key, reverse=reverse)

BLANK_LINE = DataLine(is_blank=True)


def build_DataLine(data, delimiter=DEFAULT_DELIMITER):
    """Build a DataLine from a data string, stripping
    occasional trailing spaces and <NEWLINE>.
    
    Typically it should be a text data file line.
    >>> DL = build_DataLine("We\tPro\tPro1P\n", "\t"))
    >>> print(DL)
    """
    if not data:  # Empty string, empty list...
        return DataLine()
    data_string = data.strip(STRIPABLES)
    if not data_string:
        # %data was only whitespaces and \n's. 'tis a blank line!
        return BLANK_LINE
    # It's a proper data string! Let's split it!
    data_list = data_string.split(delimiter)
    return DataLine(data_list)


def build_DataString(line, delimiter=DEFAULT_DELIMITER):
    """Build a string representation of a DataLine.
    
    Typically to be printed into a data file.
    Inverse function of build_DataLine.
    >>> s = "We\tPro\tPro1P\n"
    >>> DL = build_DataLine("We\tPro\tPro1P\n", "\t")
    >>> t = build_Datatring(DL, "\t"))
    >>> assert t == s.strip(STRIPABLES)
    """
    if line.is_blank:
        return "\n"
    return delimiter.join(line)


class DataFileStream:
    """Both context manager and stream iterator wrapper for data files
    easy access. Meant to mimic io.TextIO behavior. Only on DataLines
    instead of strings.
    
    As for everything in this module you can rely on tokenizers defaults
    almost everywhere.
    """
    def __init__(self, stream, mode="r",
                 in_delimiter=DEFAULT_DELIMITER,
                 out_delimiter=DEFAULT_DELIMITER):
        self.stream = stream
        self.name = stream
        self.mode = mode
        self.in_delimiter = in_delimiter
        self.out_delimiter = out_delimiter
        self.close_flag = False

    # Context manager special functions
    def __enter__(self):
        """Invoked @ context manager's block beginning,
        open self.stream IF it's not already an open
        stream."""

        if not self.stream:
            return None
        try:
            self.stream = io.open(self.stream, self.mode)
            self.close_flag = True
        except TypeError:  # If already an open stream
            pass
        self.name = self.stream.name
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        """Invoked @ context manager's block end, close
        self.stream IF it was not opened @ context manager's
        block beginning. (self.close flag is True)"""
        if self.close_flag:
            self.stream.close()

    # Iterator special functions
    # (Relies on I/O functions below)
    def __iter__(self):
        """Return a iterator for self.stream lines.
        
        Actually return self, since self is already
        an iterator.
        """
        # self IS the iterator
        return self

    def __next__(self):
        """Return next line as a DataLine."""
        next_line = self.read()
        if not next_line:
            raise StopIteration("EOF")
        return next_line

    # I/O base functions
    # (filter file I/O with build_Data* functions)
    # /!\ I/O mode consistency rely on io mod's one
    def read(self):
        """Read next line of self.stream and return a corresponding
        DataLine."""
        line = self.stream.readline()
        # EOF
        if not line:
            return None
        return build_DataLine(line, self.in_delimiter)

    def write(self, datas):
        """Append a string representation of a DataLine to self.stream.
        and a new line"""
        data_string = build_DataString(datas, self.out_delimiter)
        self.stream.write(data_string)
        if not datas.is_blank:
            self.stream.write("\n")


def datafile_open(stream,
                  mode="r",
                  in_delimiter=DEFAULT_DELIMITER,
                  out_delimiter=DEFAULT_DELIMITER):
    """Return a DataFileStream as an iterator and context manager for %stream."""
    return DataFileStream(stream, mode, in_delimiter, out_delimiter)


def compose(in_stream, out_stream, foo,
            in_delimiter=DEFAULT_DELIMITER,
            out_delimiter=DEFAULT_DELIMITER):
    """Apply a function line-wise to a data-file."""
    with datafile_open(in_stream, "r", in_delimiter) as ins, \
        datafile_open(out_stream, "w", out_delimiter) as outs:
        for line in ins:
            outs.write(foo(line))


def main():
    print(__doc__)

if __name__ == "__main__":
    main()




