#==============================================================================
# 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 column handling library

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

Note : this implementation uses dflib.abstractions to
parse and write text data files, for general considerations
on text data files, see the docstring of dflib.abstractions

This module is a library for processing text data files
column-wise, i.e. to perform re-ordering (inverse-sort,
permutations), selection and insertion of data columns.

The functionalities are basically split in two parts
    - A function that handles a single line and
perform the desired operation
    - A function that applies said single-line
function to each line of a file
"""
from . import abstractions as dfa
import io, functools
from itertools import chain

DEFAULT_DELIMITER = dfa.DEFAULT_DELIMITER

def _open_stream(stream, mode="r"):
    """Return an open stream to %streamif if it
    is not open and not None, else %stream itself.

    To gracefully bypass io.open(None) raising TypError and
    allow "with io.open(None) as smthg:" statements.
    """
    try:
        return io.open(stream, mode)
    except TypeError:
        return stream

def list_select(lst, order):
    """Select and order elements of a list.

    >>> list1 = [2, 7, 1, 3]
    >>> list = list_select(list1, [2, 1, 3])
    >>> assert list == [7, 2, 1]
    """
    # Dirty, dirty hack, but oh so quick.
    return list(map(lst.__getitem__, order))

def list_delete(lst, indices):
    """Remove columns from a list."""

    keep = [index for index in range(len(lst)) if index not in indices]
    return list(map(lst.__getitem__, keep))

def list_insert(list_from, list_to, order):
    """Select and insert elements of a list into another.

    /!\ %order should be a to -> from dict for
    by-item insertion or an integer for by-list insertion
    >>> list1 = ["a", "e", "i", "o", "u", "y"]
    >>> list2 = ["c", "m", "l", "t"]
    >>> list = list_insert(list1, list2, {1:0, 3:1, 5:3})
    >>> assert list == ["c", "a", "m", "e", "l", "o", "t"]
    >>> list = list_insert(list1, list2, 1)
    >>> assert list == ["c", "a", "e", "i", "o", "u", "y", "m", "l", "t"]
    """
    try:
        order = {_normalise(key, len(list_to) + 1):_normalise(value, len(list_from))
                  for (key, value) in order.items()}
        # An idea to list-comprehension this, anyone?
        length = len(list_to) + len(order)
        inserted = 0
        result = []
        for index in range(length):
            if index in order:
                result.append(list_from[order[index]])
                inserted += 1
            else:
                result.append(list_to[index - inserted])
    # If order has no length, e.g. an integer
    except (TypeError, AttributeError):
        result = [list_to[:]] # safe copy : no side-effect
        _consistent_insert(result, order, list_from)
        # Un-nest inserted list
        result = list(chain(*result))
    return result

def stream_select(stream_in, stream_out, order,
                  column_delimiter=dfa.DEFAULT_DELIMITER):
    """Select and sort columns in a text data file.

        The columns in %stream_out will be those of %stream_in
        with indices in %order.
        ie ["a","b","c","d"], [0,3,2] -> ["a", "c", "b"].
         """
    def foo(line):
        if line.is_blank:
            return dfa.BLANK_LINE
        return dfa.DataLine(list_select(line, order))
    dfa.compose(stream_in, stream_out, foo, column_delimiter, column_delimiter)

def stream_insert(stream_in_from, stream_in_to, stream_out,
                  order_from=None, order_to= -1,
                  column_delimiter=dfa.DEFAULT_DELIMITER,
                  strict=False):
    """Select columns from a data file and insert them in another.

    If %order_from is None, select all columns.
    """
    try:
        order = dict(zip(order_to, order_from))
    except TypeError:
        # by-list insertion a index %order_to
        # if %order_to is an integer, of course
        order = order_to
    with dfa.datafile_open(stream_in_from, mode="r", in_delimiter=column_delimiter) as s_in_from, \
         dfa.datafile_open(stream_in_to, mode="r", in_delimiter=column_delimiter) as s_in_to, \
         dfa.datafile_open(stream_out, mode="w", out_delimiter=column_delimiter) as s_out:

        for line_to in s_in_to:
            if line_to.is_blank:
                s_out.write(line_to)
                if strict:
                    line_from = s_in_from.read()
                    if not line_from:
                        raise Exception("EOF reached, too few lines in {0}".format(s_in_from.name))
                    if not line_from.is_blank:
                        raise Exception("Unmatching blank line pattern.")
            else:
                line_from = s_in_from.read()
                if not line_from:
                        raise Exception("EOF reached, too few lines in {0}".format(s_in_from.name))
                if line_from.is_blank:
                    # For more efficiency this could be handled
                    # in an internal function which would be
                    # different if strict.
                    if strict:
                        raise Exception("Unmatching blank line pattern.")
                    else:
                        try:
                            while line_from.is_blank:
                                line_from = s_in_from.read()
                        except AttributeError: # EOF
                            line_from = dfa.DataLine()
                line_to.data = list_insert(line_from.data, line_to.data, order)
                s_out.write(line_to)

def stream_delete(stream_in, stream_out, indices,
                  column_delimiter=dfa.DEFAULT_DELIMITER):

    delete = functools.partial(list_delete, indices=indices)
    with dfa.datafile_open(stream_in, mode="r", in_delimiter=column_delimiter) as s_in, \
         dfa.datafile_open(stream_out, mode="w", out_delimiter=column_delimiter) as s_out:
        for line in s_in:
            if line.is_blank:
                s_out.write(line)
            else:
                line.data = delete(line.data)
                s_out.write(line)

def check_stream_consistency(stream_in, stream_log,
                             column_delimiter=dfa.DEFAULT_DELIMITER,
                             column_number=None,
                             stream_repair=None):
    """Check the consistency of the columns of a data file, log errors and try
    to repair them."""
    with dfa.datafile_open(stream_in, mode="r", in_delimiter=column_delimiter) as s_in, \
         dfa.datafile_open(stream_repair, mode="w", out_delimiter=column_delimiter) as s_rep, \
         _open_stream(stream_log, "w") as s_log:
        index = 0
        if not column_number:
            index += 1
            line = s_in.read()
            if s_rep:
                s_rep.write(line)
            while line.is_blank:
                index += 1
                line = s_in.read()
                if s_rep:
                    s_rep.write(line)
            column_number = len(line)
            del line
        for line in s_in:
            index += 1
            if len(line) != column_number and not line.is_blank:
                if s_rep:
                    line = repair_line(line, column_number)
                    if len(line) != column_number:
                        s_log.write("Unrepaired error @ line {0} : {1} columns missing : ".format(index, column_number - len(line)))
                        s_log.write("line = {0}\n".format(line.data))
                    else:
                        s_log.write("Repaired error @ line {0}.\n".format(index))
                else:
                    s_log.write("Error @ line {0} : {1} columns missing : ".format(index, column_number - len(line)))
                    s_log.write("line = {0}\n".format(line.data))
            if s_rep:
                s_rep.write(line)

def repair_line(line, expected_columns):
    """Repair a corrupted (i.e. with bad number of columns) DataLine.

    Remove trailing spaces, replace spaces between columns with <TAB> and
    delete empty columns if there is too many columns."""
    cleansed_data = "\t".join(line.data).strip().split()
    if len(cleansed_data) > expected_columns:
        cleansed_data = [column for column in cleansed_data if column]
    return dfa.DataLine(cleansed_data)

def _consistent_insert(l, index, value):
    """Insert an item at a position such that
    a lookup at this position will return said item,
    even for negative indices.

    >>> l = [2,1]
    >>> _consistent_inster(l, -1, 3)
    >>> _consistent_insert(l, 1, 7)
    >>> assert l[-1] == 3
    >>> assert l[1] == 7"""
    insert_index = index if index > 0 else (len(l) + index + 1)
    l.insert(insert_index, value)

def _normalise(index, length):
    """Return a positive index from a total length and a relative index."""
    return index if index >= 0 else (length + index)
