#!/usr/bin/env python

import codecs
import optparse
import sys


o = optparse.OptionParser()
o.add_option("-p", "--page", metavar="<number of rows to align per page>",
             dest='rows',
             default=100, type="int",
             help="use 0 to align entire file")
o.add_option("-P", "--paragraph",
             action="store_true",
             help="use paragraph style")
o.add_option("-s", "--spacing", metavar="<column spacing>",
             default=2, type="int")
o.add_option("-w", "--maxwidth", metavar="<# chars>",
             default=float("inf"), type="int")
o.add_option("-d", "--delim", metavar="<delimiter>",
             default="\t")
o.add_option("-H", "--header", metavar="<# rows>",
             type="int", default=0)
o.add_option("-c", "--num-columns",
             action="store_true")
o.add_option("-r", "--num-rows",
             action="store_true")
o.add_option("", "--raw",
             action="store_true",
             help='output a raw delimited table (no whitespace padding)')
o.add_option("", "--cut",
             help='show only some columns of the table (comma separated)')


#stdout = codecs.getwriter('utf-8')(sys.stdout)
#stdin = codecs.getreader('utf-8')(sys.stdin)
stdin = sys.stdin
stdout = sys.stdout


def printcols(data, width=None, spacing=1, out=stdout,
              colwidth=100000, overflow="!"):
    """Prints a list or matrix in aligned columns

       data    - a list or matrix
       width   - maxium number of characters per line (default: 75 for lists)
       spacing - number of spaces between columns (default: 1)
       out     - stream to print to (default: sys.stdout)
    """

    if len(data) == 0:
        return

    # turn all entries into strings
    matstr = [[str(x) for x in row] for row in data]

    # overflow
    for row in matstr:
        for j in xrange(len(row)):
            if len(row[j]) > colwidth:
                row[j] = row[j][:colwidth-len(overflow)] + overflow

    # ensure every row has same number of columns
    maxcols = max(map(len, matstr))
    for row in matstr:
        if len(row) < maxcols:
            row.extend([""] * (maxcols - len(row)))

    # find the maximum width char in each column
    maxwidths = [max(len(matstr[i][j]) for i in xrange(len(matstr)))
                 for j in xrange(maxcols)]

    # print out matrix with whitespace padding
    for i in xrange(len(data)):
        fields = []
        ncols = len(data[i])
        for j in xrange(ncols):
            padding = (maxwidths[j] - len(matstr[i][j]) + spacing
                       if j < ncols - 1 else 0)
            fields.append(matstr[i][j] + " " * padding)
        out.write("".join(fields)[:width] + "\n")


def write_delim(mat, delim='\t', out=stdout):
    """
    Write delimited table.
    """
    for row in mat:
        out.write(delim.join(map(str, row)))
        out.write('\n')


def write_table(mat, conf, out=stdout):
    """
    Write tabular data.
    """
    if conf.raw:
        write_delim(mat, delim=conf.delim, out=out)
    else:
        printcols(mat, spacing=conf.spacing, colwidth=conf.maxwidth, out=out)


def read_table(infile, conf):
    """
    Read a delimited tabular data.

    Return headers and an iterator of data rows.
    """
    infile = iter(infile)
    headers = []

    cut_cols = map(int, conf.cut.split(',')) if conf.cut else None

    def process_row(row):
        if cut_cols:
            return [row[i-1] for i in cut_cols if i <= len(row)]
        else:
            return row

    # Read headers
    try:
        for i in range(conf.header):
            row = process_row(infile.next().rstrip('\r\n').split(conf.delim))
            headers.append(row)
    except StopIteration:
        pass

    def read_data():
        for line in infile:
            row = process_row(line.rstrip('\r\n').split(conf.delim))
            yield row

    return headers, read_data()


def annotate_table(mat, headers, conf, lineno=1):
    """
    Add headers and other annotations to a data matrix.
    """
    mat2 = []

    # Add column numbers.
    headers2 = []
    if conf.num_columns:
        ncols = max(map(len, mat))
        headers2.append(range(1, ncols+1))

    # Collect headers.
    headers2.extend(headers)

    # Add row number header.
    if conf.num_rows:
        headers2 = [[""] + header for header in headers2]
        if headers2:
            headers2[-1][0] = 'ROW'

    # Add headers.
    mat2.extend(headers2)

    # Add data.
    if conf.num_rows:
        mat2.extend([i] + row for i, row in enumerate(mat, lineno))
    else:
        mat2.extend(mat)
    return mat2


def paginate(data, pagesize=0):
    """
    Paginate an interable.
    """
    data = iter(data)
    if pagesize == 0:
        pagesize = 1e1000

    page = []
    for row in data:
        page.append(row)
        if len(page) == pagesize:
            yield page
            page = []

    yield page


def print_table(infile, conf):
    """
    Print tabular data with aligned columns.
    """
    headers, data = read_table(infile, conf)

    lineno = 1
    for mat in paginate(data, conf.rows):
        mat2 = annotate_table(mat, headers, conf, lineno)
        write_table(mat2, conf)
        lineno += len(mat)

        # Add spacing between pages if using headers.
        if conf.header:
            print


def print_paragraphs(infile, conf):
    """
    Print tabular data in paragraph mode.
    """
    headers, data = read_table(infile, conf)

    for i, row in enumerate(data, 1):

        # Build paragraph
        mat = []
        if conf.num_rows:
            mat.append(['ROW', i])
        for j, item in enumerate(row):
            row2 = []
            if conf.num_columns:
                row2.append(j+1)
            for header in headers:
                row2.append(header[j] if j < len(header) else '')
            row2.append(item)
            mat.append(row2)

        write_table(mat, conf)
        print


if __name__ == '__main__':
    # parse arguments
    conf, args = o.parse_args()

    # determine input stream
    if len(args) > 0 and args[0] != "-":
        infile = file(args[0])
    else:
        infile = stdin

    if conf.paragraph:
        print_paragraphs(infile, conf)
    else:
        print_table(infile, conf)
