#!/usr/bin/python3

# pylint: disable=no-name-in-module,import-error,no-member,superfluous-parens
# superfluous-parens: Parentheses are valuable for clarity and portability

'''Create HTML tables from CSV inputs'''

import sys

try:
    import urllib.parse
except ImportError:
    import urllib
    HAVE_URLLIB_PARSE = False
else:
    HAVE_URLLIB_PARSE = True

def usage(retval):
    '''Output a usage message and exit'''
    write = sys.stderr.write
    write("Convert a whitespace (or arbitrarily) delimited\n")
    write("CSV table, one row per line, to an HTML table.\n\n")
    write("Usage:\n")
    write("   -d                use a delimiter of |\n")
    write("   --delimiter ch    use a delimiter of ch\n")
    write("   --help            this stuff\n")
    write("   --suppressible    Output javascript code to allow suppressing unwanted rows and columns\n")
    write("   --tooltips        Output CSS styling to allow row/column tooltips\n")
    write('   --selections      Output clickable selections\n')
    write("   --yes-no          Output yes's and no's (yes's and empty spaces really) instead of the\n")
    write("                     actual table values\n")
    write('   --subtables       Build a subtable for each row\n')
    write('   --links           Hyperlink appropriately using 2nd column\n')
    write('   --no-comment      Do not output the input CSV as a series of comments\n')
    sys.exit(retval)

INDENT = '    '
FALSE_VALUES = frozenset(['', '0', 'n', 'no', 'No', 'N', 'false', 'False', 'n/a', 'N/A'])

def supression_functions():
    '''Output javascript code needed for suppressing rows and columns'''

    return '''
<script type="text/javascript">

    function hide_row(row_name)
        {
        var row_element = document.getElementById(row_name);
        row_element.style.display = 'none';
        }

    function hide_colgroup(column_name)
        {
        // Sadly, it appears that columns are not children of their tables - instead, rows are children of the table,
        // and portions of a given column is a child of multiple rows.
        var table_element  = document.getElementById('table');
        td_elements = table_element.getElementsByTagName('td');
        for (td_elementno = 0; td_elementno < td_elements.length; td_elementno ++)
            {
            td_name = td_elements[td_elementno].getAttribute('name');
            // Check if the td name is the ColumnID and change display if needed.
            if (td_name == column_name)
                {
                td_elements[td_elementno].style.display = 'none';
                }
            }
        }
</script>
'''

def tooltip_style():
    '''Output enough CSS that we can bring up tooltips for table elements, describing what row and column they correspond to'''

    return '''
<STYLE>
.popup
{
    COLOR: #8F040A;
    CURSOR: help;
    TEXT-DECORATION: none
}
</STYLE>
'''

def deal_with_special_chars(human_readable_name):
    '''Escape special characters in a row or column name, for use in selecting a row or column by name'''

    spaces_dashed = human_readable_name.replace(' ', '-')
    if HAVE_URLLIB_PARSE:
        browser_happy_name = urllib.parse.quote(spaces_dashed)
    else:
        browser_happy_name = urllib.quote(spaces_dashed)

    return browser_happy_name

class Field(object):
    '''Store one field in the table, and perform operations on it'''

    row_urls = []

    def __init__(self, options, row, col, string):
        self.options = options
        self.row = row
        self.col = col
        self.string = string

    def is_true(self):
        '''Return true if this field has a value'''

        if self.string in FALSE_VALUES:
            return False
        return True

    def is_left(self):
        '''Is this field on the left edge of the table?'''

        return self.col == 0

    def is_right(self):
        '''Is this field on the right edge of the table?'''
        return self.col == self.options.width - 1

    def is_top(self):
        '''Is this field on the top edge of the table?'''
        return self.row == 0

    def is_bottom(self):
        '''Is this field on the bottom edge of the table?'''
        return self.row == self.options.height - 1

    def column_supression_button(self):
        '''Return the HTML for a column supression button'''

        if self.is_top() and not self.is_left():
            return '<button onclick="hide_colgroup(\'col-%d\')">Hide column</button><br>' % (self.col + 1)
        return ''

    def row_supression_button(self):
        '''Return the HTML for a row supression button'''

        if not self.is_top() and self.is_left():
            result = '<button onclick="hide_row(\'row-%d\')">Hide row</button>' % (self.row + 1)
        else:
            result = ''
        return result

    def column_selection_button(self):
        '''Return the HTML for a column selection button'''

        if self.is_top() and not self.is_left():
            result = '<button onclick="window.open(\'#column-name-select-%s\')">Select column</button>' % (
                deal_with_special_chars(self.string), )
        else:
            result = ''
        return result

    def row_selection_button(self):
        '''Return the HTML for a row selection button'''

        if not self.is_top() and self.is_left():
            result = '<button onclick="window.open(\'#row-name-select-%s\')">Select row</button>' % (
                deal_with_special_chars(self.string), )
        else:
            result = ''
        return result

    def id_row(self):
        '''Output a row id for supression'''
        if self.options.suppressible:
            result = ' id="row-%d"' % (self.row + 1)
        else:
            result = ''
        return result

    def id_col(self):
        '''Output a column id for supression'''
        if self.options.suppressible:
            result = ' name="col-%d"' % (self.col + 1)
        else:
            result = ''
        return result

    def id_table(self):
        '''Output a table id for supression'''
        if self.options.suppressible:
            result = 'id="table" '
        else:
            result = ''
        return result

    def span_start(self, row_description, col_description):
        '''Output just the start of the CSS span'''
        if self.options.tooltips and not self.is_left() and  not self.is_top():
            result = '<span title="row: %s, column: %s " class="popup">' % (
                row_description,
                col_description,
                )
        else:
            result = ''
        return result

    def span_end(self):
        '''Output just the end of the CSS span'''
        if self.options.tooltips and not self.is_left() and  not self.is_top():
            result = '</span>'
        else:
            result = ''
        return result

    def stringer(self):
        '''Convert a field to a yes/no value, except for the left and top edges'''
        if self.is_left() or self.is_top() or not self.options.yesno:
            if self.is_left() and not self.is_top() and self.options.links:
                return '<a href="%s">%s</a>' % (Field.row_urls[self.row], self.string)
            return self.string
        else:
            if self.string in FALSE_VALUES:
                return ''
            return '<font color="green">Yes</font>'

    def to_string(self, row_description, col_description):
        '''Convert this field to a string, with appropriate HTML/CSS/Javascript added'''
        stuff = []
        if self.is_left():
            if self.is_top():
                if self.options.tooltips:
                    stuff.append(tooltip_style())
                if self.options.suppressible:
                    stuff.append(supression_functions())
                stuff.append('<table %swidth="100%%" border="1">\n' % self.id_table())
            stuff.append('%s<tr%s>\n' % (INDENT, self.id_row()))
        stuff.append('%s<td%s>' % (INDENT*2, self.id_col()))
        if self.options.suppressible:
            stuff.append(self.column_supression_button())
            stuff.append(self.row_supression_button())
        if self.options.selections:
            stuff.append(self.column_selection_button())
            stuff.append(self.row_selection_button())
        if self.options.tooltips:
            stuff.append(self.span_start(row_description, col_description))
        if self.is_top():
            stuff.append('<br>')
        stuff.append(self.stringer())
        if self.options.tooltips:
            stuff.append(self.span_end())
        stuff.append('</td>\n')
        if self.is_right():
            stuff.append('%s</tr>\n' % INDENT)
            if self.is_bottom():
                stuff.append('</table>')
        return ''.join(stuff)


def read_table_as_strings(options):
    '''Read our table as a series of strings from stdin'''
    rows = []
    row_urls = []

    # Read the table from stdin, into a list of lists containing strings, sans padding - that comes later
    unprocessed_table = []
    for line in sys.stdin:
        line = line.rstrip('\n')
        line = line.replace('""', '"')
        unprocessed_table.append(line)
        if options.delimiter:
            field_strings = line.split(options.delimiter)
        else:
            field_strings = line.split()

        if options.links:
            # We're skipping the second - it's a URL that we don't want to display.  Instead we hyperlink to it
            modified_rows = [field_strings[0]] + field_strings[2:]
            rows.append(modified_rows)
            row_urls.append(field_strings[1])
        else:
            rows.append(field_strings)

    if options.comment:
        for unprocessed_line in unprocessed_table:
            greater_line = unprocessed_line.replace('>', '&gt;')
            print('<!-- %s -->' % greater_line)

    # Compute the height of the table
    height = len(rows)

    # Compute the maximum width of all rows
    width = 0
    for row in rows:
        width = max(width, len(row))

    # Pad out short rows with empty fields
    for row in rows:
        while len(row) < width:
            row.append('')

    Field.row_urls = row_urls

    return width, height, rows


class Table(object):
    # pylint: disable=R0903
    # R0903: We don't need a lot of public methods
    '''Store all the fields of a table and perform operations on that table'''

    def __init__(self, options):
        self.options = options
        del options

        self.options.width, self.options.height, table_as_strings = read_table_as_strings(self.options)

        # create a list of lists of Fields from the list of lists of strings
        self.rows = []
        for row in range(self.options.height):
            self.rows.append([])
            for col in range(self.options.width):
                field = Field(self.options, row, col, table_as_strings[row][col])
                self.rows[-1].append(field)

    def get_field(self, row, col):
        '''Look up one field by row and column'''
        return self.rows[row][col]

    def __str__(self):
        '''Output one cell in the table'''
        list_of_str = []
        for row in range(self.options.height):
            row_field = self.get_field(row, 0)
            row_string = row_field.string
            for col in range(self.options.width):
                col_field = self.get_field(0, col)
                col_string = col_field.string
                field = self.get_field(row, col)
                list_of_str.append(field.to_string(row_string, col_string))
        return ''.join(list_of_str)

    def row_selections(self):
        '''Print data for rows'''
        list_ = []
        list_.append('<br>\n' * 5)
        list_.append('<li>Rows</li>\n')
        list_.append('%s<ul>\n' % INDENT)
        for row in range(1, self.options.height):
            list_.append('%s<a name="row-select-%d">\n' % (INDENT, row + 1))
            list_.append('%s<a name="row-name-select-%s">\n' % (INDENT, deal_with_special_chars(self.get_field(row, 0).string)))
            string = self.get_field(row, 0).stringer()
            list_.append('%s<li>%s</li>\n' % (INDENT, string))
            list_.append('%s<ol>\n' % (INDENT*2))
            for col in range(1, self.options.width):
                field = self.get_field(row, col)
                if field.is_true():
                    list_.append('%s<li>%s</li>\n' % (INDENT*2, self.get_field(0, col).stringer()))
            list_.append('%s</ol>\n\n' % (INDENT*2))
        list_.append('%s</ul>' % INDENT)
        return ''.join(list_)

    def column_selections(self):
        '''Print data for columns'''
        list_ = []
        list_.append('<br>\n' * 5)
        list_.append('<li>Columns</li>\n')
        list_.append('%s<ul>\n' % INDENT)
        for col in range(1, self.options.width):
            list_.append('%s<a name="column-select-%d">\n' % (INDENT, col + 1))
            list_.append('%s<a name="column-name-select-%s">\n' % (INDENT, deal_with_special_chars(self.get_field(0, col).string)))
            string = self.get_field(0, col).stringer()
            list_.append('%s<li>%s</li>\n' % (INDENT, string))
            list_.append('%s<ol>\n' % (INDENT*2))
            for row in range(1, self.options.height):
                field = self.get_field(row, col)
                if field.is_true():
                    list_.append('%s<li>%s</li>\n' % (INDENT*2, self.get_field(row, 0).stringer()))
            list_.append('%s</ol>\n\n' % (INDENT*2))
        list_.append('%s</ul>' % INDENT)
        return ''.join(list_)

    def row_subtable(self):
        '''Print data for row subtables'''
        list_ = []
        list_.append('<br>\n' * 5)
        list_.append('<li>Row subtables</li>\n')
        list_.append('%s<ul>\n' % INDENT)
        for row in range(1, self.options.height):
            list_.append('%s<li>%s</li>\n' % (INDENT, self.get_field(row, 0).stringer()))
            list_.append('%s<table width="100%%" border="1">\n' % (INDENT*2))
            for col in range(1, self.options.width):
                list_.append('%s<tr>\n' % (INDENT*3))
                list_.append(
                    '%s<td>%s</td> <td>%s</td>\n' % (INDENT*4, self.get_field(0, col).string, self.get_field(row, col).string),
                    )
                list_.append('%s</tr>\n' % (INDENT*3))
            list_.append('%s</table>\n\n' % (INDENT*2))
        list_.append('%s</ul>' % INDENT)
        return ''.join(list_)

class Options(object):
    # pylint: disable=R0903,R0902
    # R0903: We don't need a lot of public methods
    # R0902: We need a bunch of instance attributes because we're mostly a container
    '''Hold options related to command line arguments, plus the width and height of a table'''

    def __init__(self):
        self.delimiter = None
        self.suppressible = False
        self.tooltips = False
        self.selections = False
        self.width = None
        self.height = None
        self.yesno = False
        self.subtables = False
        self.links = False
        self.comment = True

        while sys.argv[1:]:
            if sys.argv[1] == '-d':
                self.delimiter = '|'
            elif sys.argv[1] == '--delimiter' and sys.argv[2:]:
                self.delimiter = sys.argv[2]
                del sys.argv[2]
            elif sys.argv[1] in ['-h', '--help']:
                usage(0)
            elif sys.argv[1] == '--suppressible':
                self.suppressible = True
            elif sys.argv[1] == '--tooltips':
                self.tooltips = True
            elif sys.argv[1] == '--selections':
                self.selections = True
            elif sys.argv[1] == '--yes-no':
                self.yesno = True
            elif sys.argv[1] == '--subtables':
                self.subtables = True
            elif sys.argv[1] == '--links':
                self.links = True
            elif sys.argv[1] == '--no-comment':
                self.comment = False
            else:
                sys.stderr.write('%s: Unrecognized option: %s\n' % (sys.argv[0], sys.argv[1]))
                usage(1)
            del sys.argv[1]


def main():
    '''Main function'''

    options = Options()

    table = Table(options)

    print(str(table))

    if options.selections:
        print('\n')
        print(table.row_selections())
        print('\n')
        print(table.column_selections())
        print('<br>\n' * 100)

    if options.subtables:
        print('\n')
        print(table.row_subtable())
        print('<br>\n' * 100)
main()