import argparse
import os
import sys
import tempfile
import csv

# Max sure that CSV files are as wide as possible
csv.field_size_limit(sys.maxsize)


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def main():
    # Initialise and parse the command line arguments
    parser = _init_cmd_args()
    args = _parse_cmd_args(parser)
    try:
        run_mdtable(args)
    except (BrokenPipeError, KeyboardInterrupt):
        # Python flushes standard streams on exit; redirect remaining
        # output to devnull to avoid another BrokenPipeError at shutdown
        devnull = os.open(os.devnull, os.O_WRONLY)
        os.dup2(devnull, sys.stdout.fileno())


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def _init_cmd_args():
    """Initialise the command line arguments and return the parser.

    Returns
    -------
    args : `argparse.ArgumentParser`
        The argparse parser object with arguments added
    """
    # Read in the command line arguments
    parser = argparse.ArgumentParser(
        description='Takes tabular STDIN and FORMATS markdown tables in '
        'to STDOUT'
    )
    parser.add_argument('-d', '--delimiter', type=str,
                        default="\t", help="The delimiter of STDIN")
    parser.add_argument('-q', '--quote', type=str,
                        default="\"", help="The Quoting character")
    parser.add_argument('-b', '--buffer', type=int, default=100,
                        help="Output lines to buffer before writing a temp "
                        "file/or output")
    parser.add_argument('-r', '--round', type=int, default=2,
                        help="Round floats to --round dp")
    parser.add_argument('-f', '--file', action="store_true",
                        help="When the buffer is full write output to a temp "
                        "file (memory saver)")
    parser.add_argument('-c', '--cut', action="store_true",
                        help="Truncate any strings that are longer then the "
                        "column width")
    parser.add_argument('-u', '--unicode', action="store_true",
                        help="Have nice unicode table dividers")
    parser.add_argument('--all-left', action="store_true",
                        help="Do not right justify the final column.")
    parser.add_argument('--npad', type=int, default=1,
                        help="Add --npad number of spaces to the start and "
                        "end of the string")
    return parser


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def _parse_cmd_args(parser):
    """Parse the command line arguments.

    Parameters
    ----------
    parser : :obj:`argparse.ArgumentParser`
        The argparse parser object with arguments added.

    Returns
    -------
    args : `argparse.Namespace`
        The argparse namespace object containing the arguments
    """
    args = parser.parse_args()
    return args


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def round_extra(n, dp):
    """Will round a float to the required decimal places but will leave ints
    alone all values are then returned as strings.
    """

    # Generate a string to round the floats to the correct dp
    format_string = "%%.%if" % dp

    # first try to cast to a float, if this fails then we don't have a
    # number at all
    try:
        n = float(n)

        # If we get to here then the cast has worked. However, ints can be
        # cast to floats and that is not always desirable fort he aesthetics
        # so we test if the number is actually an int, if so we cast to int
        # and then to string before returning
        if n % 1 == 0:
            n = int(n)
            return str(n)
        # If we get to here then we return our float rounded and represented
        # as a string
        return format_string % n
    except ValueError:
        return n


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def format_string(string, pad, just, args, cutchr=">>"):
    """Formats a string for output.
    """
    juststr = "-"
    if just == "right":
        juststr = ""
    elif just == "centre":
        left_pad = round((pad-len(string))/2, 0)
        if left_pad > 1:
            string = "%s%s" % ((" "*left_pad), string)

    # If the string is grater that out column width and we want to
    # cut
    if args.cut is True and len(string) > pad:
        # Limit the cut chr to the size of the pad so
        # we don't end up with negatives
        cutchr = cutchr[0:pad]
        string = "%s%s" % (string[0:(pad-len(cutchr))], cutchr)

    # Generate a formatted string
    fs = "%s%%%s%ds%s" % ((" "*args.npad), juststr, pad, (" "*args.npad))
    return fs % string


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def format_div(pad, just, args):
    """Formats a dividing line for output.
    """

    if args.unicode is False:
        if just == "left":
            return ":%s" % ("-"*(pad-1+(args.npad*2)))
        elif just == "right":
            return "%s:" % ("-"*(pad-1+(args.npad*2)))
        else:
            return ":%s:" % ("-"*(pad-2+(args.npad*2)))

    # If we get to here then we have selected unicode output
    # so we return the unicode horizontal table divider
    return "%s" % (u"\u2500"*(pad+(args.npad*2)))


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def write_data_row(row, widths, args):
    """Writes a data row to STDOUT.
    """
    for nc, col in enumerate(row):
        just = "left"
        if args.all_left is False and nc == len(row)-1:
            just = "right"
        row[nc] = format_string(col, widths[nc], just, args)

    if args.unicode is False:
        print("%s" % "|".join(row))
    else:
        # For the unicode we will join with the vertical long unicode line.
        # Note that the unicode is encoded to utf-8 before output otherwise we
        # can get an ordinal not in range error in some circumstances, i.e.
        # when we go through a pipe
        try:
            # print ("%s" % u'\u2502'.join(row)).encode('utf-8')
            print("%s" % u'\u2502'.join(row))
        except AttributeError:
            print(row)
            raise


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def write_div_row(row, widths, args):
    """Writes a div row to SDTOUT.
    """
    div = []
    for nc in range(len(row)):
        just = "left"
        if nc == len(row)-1:
            just = "right"

        div.append(format_div(widths[nc], just, args))
    if args.unicode is False:
        print("%s" % "|".join(div))
    else:
        # When we join the div rows we ise the unicode horizontal and vertical
        # divider. Again, encoding before output
        print("%s" % u'\u253C'.join(div))
        # print ("%s" % u'\u253C'.join(div)).encode('utf-8')


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def run_mdtable(args):
    """Run the mdtable program, this will need some more refactoring to make in
    a bit more pythonic.

    Raises
    ------
    BrokenPipeError
        If piped into less and then exited before the end of the file
    """
    # Will hold the maximum string width in each column
    field_width = []

    # Will hold the stdinput until --buffer is reached
    stdin_buffer = []
    w = None
    fh = None
    field_width_freeze = []

    # Read in stdinput
    stdout_lines = 0
    with sys.stdin as csvfile:
        reader = csv.reader(
            csvfile,
            delimiter=args.delimiter,
            quotechar=args.quote
        )
        filewriter = csv.writer

        # Now loop through each of the rows and then each of the columns
        for nr, row in enumerate(reader):
            for nc, col in enumerate(row):
                row[nc] = round_extra(col, args.round)

                # Attempt to test/set the string width for the current column,
                # if this fails then the column has not been seen before so we
                # add it to the list of column lengths
                try:
                    field_width[nc] = max(field_width[nc], len(row[nc]))
                except IndexError:
                    field_width.append(len(row[nc]))

            # If the designated buffer size is full we will do one of two
            # things. If we are using a temp file then we will write the buffer
            # to a temp file and carry on reading STDIN and calculating the
            # column widths otherwise we write the buffer according to the args
            # and then write the contents of STDIN according to the args and we
            # use the string widths that were determined during buffering
            if nr > (args.buffer-1) and args.file is True:
                try:
                    # Write the current row to a file
                    w.writerow(row)
                except (AttributeError, NameError):
                    # If we get here then no writer is open yet so we make a
                    # tmp filename open it and create a writer
                    n, fn = tempfile.mkstemp()
                    fh = open(fn, "w")
                    w = filewriter(fh)

                    # Flush the buffer and write the current line
                    for i in stdin_buffer:
                        w.writerow(i)
                    stdin_buffer = []
                    w.writerow(row)
            elif nr > (args.buffer-1):
                #            pp.pprint(stdin_buffer)
                # If we get to here then the buffer is full and we just want to
                # use what we have read in so far to base our column string
                # widths
                # on
                # First flush the buffer
                #            pass
                #            for nc,col in enumerate(row):
                try:
                    field_width_freeze[nc]
                except IndexError:
                    field_width_freeze = list(field_width)

                # Next flush the buffer
                for buffrow in stdin_buffer:
                    write_data_row(buffrow, field_width_freeze, args)
                    stdout_lines += 1

                    if stdout_lines == 1:
                        write_div_row(buffrow, field_width_freeze, args)
                        stdout_lines += 1
                stdin_buffer = []

                # Now we can write the unbuffered line
                if stdout_lines == 1:
                    write_div_row(row, field_width_freeze, args)
                    stdout_lines += 1
                    continue

                write_data_row(row, field_width_freeze, args)
                stdout_lines += 1
            else:
                # Otherwise the buffer is not full so we add the line to the
                # buffer
                stdin_buffer.append(row)

    # When we get here the buffer may have data in it so flush it:
    for buffrow in stdin_buffer:
        if stdout_lines == 1:
            write_div_row(buffrow, field_width, args)

        write_data_row(buffrow, field_width, args)
        stdout_lines += 1
    stdin_buffer = []

    # When we get here we may have data in a file
    try:
        fn = fh.name
        fh.close()

        with open(fn, "r") as csvfile:
            reader = csv.reader(csvfile)

            # Now loop through each of the rows and then each of the columns
            for nr, row in enumerate(reader):
                if stdout_lines == 1:
                    write_div_row(row, field_width, args)

                write_data_row(row, field_width, args)
                stdout_lines += 1

        csvfile.close()
        os.remove(fn)
    except AttributeError:
        pass


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
if __name__ == '__main__':
    main()
