#!/usr/bin/env python

#    matrix.py generate a matrix of all readers characteristics
#    Copyright (C) 2009  Ludovic Rousseau
#
#    This program 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 2 of the License, or
#    (at your option) any later version.
#
#    This program 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 this program; if not, write to the Free Software Foundation, Inc.,
#    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

import glob
import os
import ConfigParser
import pprint
import templayer
import time
import re

pp = pprint.PrettyPrinter(indent=4)

html_escape_table = {
    "&": "&amp;",
    '"': "&quot;",
    ">": "&gt;",
    "<": "&lt;",
    }


def html_escape(text):
    """Produce entities within text."""
    L = []
    for c in text:
        L.append(html_escape_table.get(c, c))
    return "".join(L)


def parse_reader(path, reader):
    """
    parse a reader CCID descriptor and return a dictionnary
    """
    reader_dict = {}
    reader_file = open(path + reader)
    for line in reader_file.readlines():
        line = line[0:-1]
        l = line.strip(" ").split(':')
        if (len(l) > 1):
            reader_dict[l[0]] = l[1].strip(" ")
    reader_file.close()
    return reader_dict


def parse_all(path, reader_list):
    """
    parse each reader from list
    return a dictionnary
    """
    readers = {}
    for reader in reader_list:
        p = parse_reader(path, reader)
        readers[reader] = p

    return readers


def parse_ini(path, section):
    """
    parse a foobar.ini file to extract all informations
    """
    config = ConfigParser.ConfigParser()
    # do not use the default case insensitive transform for key value
    config.optionxform = str
    config.read(section + ".ini")
    reader_list = config.sections()

    readers = parse_all(path, reader_list)
    for r in readers.keys():
        readers[r]['section'] = section
        readers[r]['iManufacturer'] = html_escape(readers[r]['iManufacturer'])
        for o in config.options(r):
            if o == 'features':
                # for the features we use a list
                readers[r][o] = [config.get(r, o)]
            else:
                readers[r][o] = html_escape(config.get(r, o))

        bPINSupport = int(readers[r]['bPINSupport'], 16)
        if bPINSupport != 0 and not 'features' in readers[r]:
            readers[r]['features'] = list()
        if bPINSupport & 1:
            readers[r]['features'].append("PIN Verification")
        if bPINSupport & 2:
            readers[r]['features'].append("PIN Modification")
        if int(readers[r]['dwFeatures'], 16) & 0x0800:
            readers[r].setdefault('features', ["ICCD"])

    #pp.pprint(readers["GemPCPinpad.txt"])
    return readers


def check_list(path, reader_list):
    """
    Check that all .txt files are listed
    """
    cwd = os.getcwd()
    os.chdir(path)
    real_list = glob.glob("*.txt")
    os.chdir(cwd)

    # check that each reader file is listed
    #print real_list
    for r in reader_list:
        #print "remove ", r
        try:
            real_list.remove(r)
        except:
            print "reader %s not yet listed" % r

    # also remove the non-reader supported_readers.txt file
    real_list.remove("supported_readers.txt")

    # some USB descriptor are not listed in readers.txt?
    if len(real_list) > 0:
        print "Reader(s) not listed in any .ini file:"
        print "\n".join(real_list)
        print ""


def check_supported(path, all_readers):
    """
    Check that all .txt files are mentionned in supported_readers.txt
    """
    supported_readers = file(path + "supported_readers.txt").readlines()
    # convert in a long string and in uppercase
    s = "".join(supported_readers).upper()

    unlisted = list()
    for r in all_readers.keys():
        pattern = "%s:%s" % (all_readers[r]['idVendor'], all_readers[r]['idProduct'])
        if not pattern.upper() in s:
            unlisted.append(pattern + " " + r)

    if len(unlisted) > 0:
        print "Reader(s) not in supported_readers.txt"
        print "\n".join(unlisted)
        print ""
    #pp.pprint(supported_readers)


def check_descriptions(path, all_readers):
    """
    Check that all readers mentionned in supported_readers.txt have a
    .txt descriptor
    """
    supported_readers = file(path + "supported_readers.txt").readlines()

    unlisted = list()
    for line in supported_readers:
        # skip comments
        if line.startswith("#"):
            # but get commented readers
            if line.startswith("#0"):
                # remove the leading #
                line = line[1:]
            else:
                continue

        # remove newline
        line = line.rstrip()

        # skip empty lines
        if line is "":
            continue

        (vendor, product, name) = line.split(":")
        vendor = int(vendor, 16)
        product = int(product, 16)
        found = False
        for r in all_readers.keys():
            reader = all_readers[r]
            if vendor == int(reader['idVendor'], 16) and product == int(reader['idProduct'], 16):
                found = True
        if not found:
            unlisted.append(line)

    if len(unlisted) > 0:
        print "Reader(s) without a .txt description"
        print "\n".join(unlisted)
        print ""
    #pp.pprint(supported_readers)


def get_driver_version(readers):
    """
    set the 'release' field for each reader
    """
    changelog = get_changelog()

    for reader in readers.keys():
        rev = get_driver_revision(reader, changelog)
        readers[reader]['release'] = driver_revision_to_version(rev)


def driver_revision_to_version(rev):
    """
    convert a SVN revision in the first release containing this revision
    """
    history = [
        #[ SVN revision, CCID release ]
        [273, "0.1.0"],
        [342, "0.2.0"],
        [423, "0.3.0"],
        [467, "0.3.1"],
        [552, "0.3.2"],
        [697, "0.4.0"],
        [703, "0.4.1"],
        [1015, "0.9.0"],
        [1018, "0.9.1"],
        [1186, "0.9.2"],
        [1400, "0.9.3"],
        [1761, "0.9.4"],
        [1911, "1.0.0"],
        [2020, "1.0.1"],
        [2135, "1.1.0"],
        [2345, "1.2.0"],
        [2363, "1.2.1"],
        [2522, "1.3.0"],
        [2692, "1.3.1"],
        [2755, "1.3.2"],
        [2796, "1.3.3"],
        [2809, "1.3.4"],
        [2842, "1.3.5"],
        [2924, "1.3.6"],
        [2985, "1.3.7"],
        [3033, "1.3.8"],
        [3208, "1.3.9"],
        [3338, "1.3.10"],
        [4347, "1.3.11"],
        [4931, "1.3.12"],
        [4979, "1.3.13"],
        [5108, "1.4.0"]]
    for h in history:
        if rev <= h[0]:
            return h[1]
    return "SVN"


def get_changelog():
    """
    read a complete svn2cl Changelog file and merge commits on one line
    """
    lines = open("ccid/readers/ChangeLog").readlines()
    changelog = list()
    p = list()

    for line in lines:
        # the lines starts with a year (2000-2099)
        if line.startswith('20'):
            changelog.append("".join(p).replace('\n', ''))
            p = [line]
        else:
            p.append(line)

    # add the last line
    changelog.append("".join(p).replace('\n', ''))

    return changelog


def get_driver_revision(reader, changelog):
    """
    search a log line containing the reader string
    """
    found = None
    for line in changelog:
        if reader in line:
            found = line

    if found:
        result = re.search('\\* \\[r(\d*)\\]', found)
        if result:
            # revision is the inner matching pattern
            return int(result.group(1))
    else:
        print "reader %s not found in ChangeLog" % reader
        # fake SVN revision number high enough to be considered as
        # unreleased
        return 999999999


def get_by_manufacturer(readers):
    """
    return a dict of the readers grouped by manufacturer
    d['manufacturer'] is a list of the manufacturer's readers
    """
    d = {}
    for r in readers.keys():
        d.setdefault(readers[r]['iManufacturer'], []).append(r)
    return d


def generate_page(section, title, comment, readers):
    """
    generate a web page for the corresponding section
    """
    # sort the readers by manufacturers
    manufacturer_readers = get_by_manufacturer(readers)
    manufacturers = list(manufacturer_readers)
    # sort the manufacturers list alphabetically
    manufacturers.sort(key=str.lower)

    template = templayer.HTMLTemplate("webpage.template")
    file_writer = template.start_file(file=file("ccid/" + section + ".html", "w"))
    main_layer = file_writer.open(date=time.asctime(),
        title=title, comment=comment, section=section)

    # for each manufacturer
    for m in manufacturers:
        main_layer.write_layer('manufacturer', manufacturer=m)

        # for each reader
        for r in sorted(manufacturer_readers[m]):
            note_layer = main_layer.open_layer('reader',
                manufacturer=m,
                product=readers[r]['iProduct'],
                idVendor=readers[r]['idVendor'],
                idProduct=readers[r]['idProduct'],
                image="img/" + readers[r].get('image', "no_image.png"))

            note_layer.write_layer('descriptor', descriptor="readers/%s" % r)

            url = readers[r].get('url', "")
            if url:
                note_layer.write_layer('url',
                    url=url,
                    manufacturer=m,
                    product=readers[r]['iProduct'])

            features = ", ".join(readers[r].get('features', ""))
            if features:
                note_layer.write_layer('features', features=features)

            note = readers[r].get('note', "").split('\n')
            for n in note:
                note_layer.write_layer('note', contents=n)

            note_layer.write_layer('release',
                release=readers[r]['release'])

    file_writer.close()


def generate_table(readers, field, index, fields):
    """
    generate a web page with all the reader attributes
    readers are in the order given by index
    """

    header = """<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
  <head>
    <title>%s</title>
    <link rel="stylesheet" type="text/css" href="default.css">
    <link rel="stylesheet" type="text/css" href="matrix.css">
<script type="text/javascript">
/* <![CDATA[ */
    (function() {
        var s = document.createElement('script'), t = document.getElementsByTagName('script')[0];
        
        s.type = 'text/javascript';
        s.async = true;
        s.src = 'http://api.flattr.com/js/0.5.0/load.js?mode=auto';
        
        t.parentNode.insertBefore(s, t);
    })();
/* ]]> */
</script>
  </head>
  <body>"""

    footer = """
<p>
<a href="http://validator.w3.org/check/referer"><img src="http://www.w3.org/Icons/valid-html401" alt="Valid HTML 4.01!"></a> 
<a href="http://jigsaw.w3.org/css-validator/"><img src="http://jigsaw.w3.org/css-validator/images/vcss" alt="Valid CSS!"></a> 
</p>

<script type="text/javascript">
var gaJsHost = (("https:" == document.location.protocol) ? "https://ssl." : "http://www.");
document.write(unescape("%3Cscript src='" + gaJsHost + "google-analytics.com/ga.js' type='text/javascript'%3E%3C/script%3E"));
</script>
<script type="text/javascript">
try {
var pageTracker = _gat._getTracker("UA-2404298-2");
pageTracker._trackPageview();
} catch(err) {}</script>
    <p>Ludovic Rousseau</p>
  </body>
</html>"""

    documentation = """
    <ul>
    <li>Click on the column header to sort by that column.</li>
    <li>The background color indicates the section of the reader:
    <table border="1" summary="color codes">
    <tr>
    <td class="supported">supported</td>
    <td class="shouldwork">should work</td>
    <td class="unsupported">unsupported</td>
    <td class="disabled">disabled</td>
    </tr></table></li>
    <li><a class="FlattrButton" style="display:none;"
            href="http://pcsclite.alioth.debian.org/ccid.html"></a></li>
    </ul>
    """

    file = open("ccid/" + field + ".html", "w")
    title = "Readers sorted by '%s' field" % field
    file.write(header % title)

    file.write("<h1>" + title + "</h1>")
    file.write(documentation)

    file.write('<table border="1" summary="">\n')

    file.write('<tr>')
    file.write("<th>#</th>")
    for f in fields:
        file.write("<th><a href='%s'>%s</a></th>" % (f + ".html", f))
    file.write('</tr>\n')

    num = 0
    for r in index:
        num += 1
        # define color of the line
        if num % 2:
            # even line number
            color = readers[r]['section']
        else:
            # odd line number
            color = readers[r]['section'] + '_odd'
        file.write('<tr class="%s">' % color)

        file.write("<td>%d</td>" % num)
        for f in fields:
            if f == 'iProduct':
                file.write("<td><a href='%s.html#%s%s'>%s</a></td>" % (readers[r]['section'], readers[r]['idVendor'], readers[r]['idProduct'], readers[r][f]))
            elif f == 'image':
                file.write('<td><img src="%s" height="100" alt="image"></td>' % ("img/" + readers[r].get('image', "no_image.png")))
            else:
                file.write("<td>%s</td>" % readers[r].get(f, ""))
        file.write('</tr>\n')

    file.write('</table>\n')

    file.write("<hr><p>Generated: %s</p>" % time.asctime())

    file.write(footer)
    file.close()


def generate_tables(readers):
    """
    generate all the web page tables with all the fields values
    """
    fields = ['section', 'iManufacturer', 'iProduct', 'image', 'idVendor',
            'idProduct', 'bNumEndpoints', 'bInterfaceClass', 'bcdCCID',
            'bMaxSlotIndex', 'bVoltageSupport', 'dwProtocols',
            'dwDefaultClock', 'dwMaximumClock', 'dwDataRate',
            'dwMaxDataRate', 'dwMaxIFSD', 'dwSynchProtocols',
            'dwMechanical', 'dwFeatures', 'dwMaxCCIDMessageLength',
            'bClassGetResponse', 'bClassEnveloppe', 'wLcdLayout',
            'bPINSupport', 'bMaxCCIDBusySlots', 'features', 'note',
            'release']

    for f in fields:
        index = list()
        for r in readers.keys():
            index.append([readers[r].get(f, ""), r])
        if f == 'section':
            # hack to sort in the order supported, shouldwork, unsupported, disabled
            index = [(s.replace('supported', 'asupported'), r) for s, r in index]
            index = [(s.replace('disabled', 'zdisabled'), r) for s, r in index]
        if f in ['dwDefaultClock', 'dwMaximumClock', 'dwDataRate',
                'dwMaxDataRate', 'dwMaxIFSD', 'dwMaxCCIDMessageLength']:
            # convert from text to decimal
            index = [(float(s.split(' ')[0]) * 1000, r) for s, r in index]
        if f == 'release':
            index = [(release2int(s), r) for s, r in index]
        index.sort()
        sorted_fields = list(fields)
        sorted_fields.remove(f)
        sorted_fields.insert(0, f)
        generate_table(readers, f, [r for f, r in index], sorted_fields)


def release2int(r):
    rr = r.split('.')
    if len(rr) > 1:
        return int(rr[2]) + int(rr[1]) * 1000 + int(rr[0]) * 1000 * 1000
    else:
        # SVN version
        return 99*1000*1000

if __name__ == "__main__":
    path = "../trunk/Drivers/ccid/readers/"

    supported_readers = parse_ini(path, "supported")
    shouldwork_readers = parse_ini(path, "shouldwork")
    unsupported_readers = parse_ini(path, "unsupported")
    disabled_readers = parse_ini(path, "disabled")

    # all_readers contain the union of the 3 lists
    all_readers = dict(supported_readers)
    all_readers.update(shouldwork_readers)
    all_readers.update(unsupported_readers)
    all_readers.update(disabled_readers)

    check_list(path, all_readers.keys())
    check_supported(path, all_readers)
    check_descriptions(path, all_readers)

    get_driver_version(supported_readers)
    get_driver_version(shouldwork_readers)
    get_driver_version(unsupported_readers)
    get_driver_version(disabled_readers)

    generate_page("supported", "Supported CCID readers/ICCD tokens", "If you are a reader manufacturer and your reader is not listed here then contact me at ludovic.rousseau@free.fr", supported_readers)
    generate_page("shouldwork", "Should work but untested by me", "The CCID readers and ICCD tokens listed bellow should work with the driver but have not be validated by me. I would like to get these readers to perform test and validation and move them in the supported list above. If you are one of the manufacturers, please, contact me at ludovic.rousseau@free.fr.", shouldwork_readers)
    generate_page("unsupported", "Unsupported or partly supported CCID readers", "These readers have problems or serious limitations.", unsupported_readers)
    generate_page("disabled", "Disabled CCID readers", "These readers are not even recognized by the driver because they are too bogus or because they are supported by another driver.", disabled_readers)

    generate_tables(all_readers)
