#!/usr/bin/python

# Extract remote descriptions from annotated source code
# Copyright (C) 2008 Openismus GmbH (www.openismus.com)
#
# 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
#
# Authors:
#   Mathias Hasselmann <mathias@openismus.com>
#
# Usage:
#   build-receiver-list [SRCDIR]
#
import logging, os, re, sys
from datetime import datetime

def find_srcdir():
    srcdir = len(sys.argv) > 1 and sys.argv[1] or ''
    filename = os.path.join(srcdir, 'daemons', 'lircd.c')

    if not os.path.isfile(filename):
        raise SystemExit, 'No LIRC code found at %s.' % (srcdir and srcdir or os.getcwd())

    return srcdir

def drop_hidden_names(filenames):
    enumeration = list(enumerate(filenames))
    enumeration.reverse()

    for i, name in enumeration:
        if name.startswith('.'):
            filenames.pop(i)

def parse_comments(text):
    comments  = re_comments.findall(text)
    receivers = []

    # parse comments to build list of property dictionaries:
    for details in filter(lambda s: s.count(':'), comments):
        details = [re_properties.match(p) for p in details.splitlines()]
        details = [p and p.groups() for p in details]
        details = dict(filter(None, details))

        receivers.append(details)

    return receivers

def scan_sources(path, scanner):
    daemons_path = os.path.join(srcdir, 'daemons')

    for path, subdirs, files in os.walk(path):
        drop_hidden_names(subdirs)

        for name in files:
            if not name.endswith('.c'): continue
            if name.startswith('.'): continue

            scanner(os.path.join(path, name))

def scan_userspace_driver(filename):
    driver_code = open(filename).read()

    for declaration in re_hardware.finditer(driver_code):
        declaration = declaration.group(1)

        # convert decaration text into list of C expressions:
        expressions = re_comments.sub('', declaration).split(',')
        expressions = [value.strip() for value in expressions]

        # last expression is the driver name:
        driver = expressions[-1].strip('"')
        if not driver: continue

        # extract receiver information from comments:
        receivers = parse_comments(declaration)

        if not receivers:
            logging.warning(
                'No receivers declared for userspace driver %s.',
                driver)

            continue

        # print receiver information for current driver:
        print '# receivers supported by %s userspace driver' % driver
        print '# %s' % ('-' * 70)

        for receiver in receivers:
            receiver['lirc-driver'] = driver
            print_receiver_details(receiver)

def expand_symbols(symbols, text):
    def replace_symbol(match):
        # lookup word in symbol table:
        expansion = symbols.get(match.group(0))

        if expansion:
            # expand symbol recursively when found:
            return expand_symbols(symbols, expansion)

        return match.group(0)

    return re.sub(r'\b\w+\b', replace_symbol, text)

def scan_kernel_driver(filename):
    driver_code = open(filename).read()

    # naively parse preprocessor symbols:
    symbols = dict()

    for declaration in re_define.finditer(driver_code):
        name, value = declaration.groups()
        symbols[name] = value

    # resolve driver name, from symbol table or filename:
    driver_name = symbols.get('DRIVER_NAME')

    if not driver_name:
        dirname     = os.path.dirname(filename)
        driver_name = os.path.basename(dirname)

    else:
        driver_name = driver_name.strip('"')

    # interpret USB device declarations:
    for declaration in re_usb_device.finditer(driver_code):
        vendor_id, product_id, comments = declaration.groups()
        receivers = parse_comments(comments)

        # expand arguments of USB device declaration:
        vendor_id = int(expand_symbols(symbols, vendor_id), 0)
        product_id = int(expand_symbols(symbols, product_id), 0)
        device_ids = 'usb:%04x:%04x' % (vendor_id, product_id)

        if not receivers:
            logging.warning(
                'No receivers declared for USB device %04x:%04x in %s.',
                vendor_id, product_id, driver_name)

            continue

        # dump receiver information found in comments:
        print '# receivers supported by %s kernel module' % driver_name
        print '# %s' % ('-' * 70)

        for receiver in receivers:
            receiver['kernel-modules'] = 'linux:%s' % driver_name
            receiver['device-ids'] = device_ids
            print_receiver_details(receiver)

def print_database_header():
    print '# LIRC Receiver Database'
    print '# Generated on %s' % datetime.now().strftime('%c')
    print '# from %s' % srcdir
    print '# %s' % ('=' * 70)
    print

def print_receiver_details(properties):
    main_properties = (
        'compatible-remotes', 'device-ids',
        'kernel-modules', 'lirc-driver')

    vendor, product = map(properties.pop, ('vendor-name', 'product-name', ))
    indent = max(map(len, properties.keys()))

    print '[%s: %s]' % (vendor, product)

    for key in main_properties:
        value = properties.pop(key, None)

        if not value:
            continue

        print '%*s = %s' % (-indent, key, value)

    for key, value in properties.items():
        print '%*s = %s' % (-indent, key, value)

    print

if '__main__' == __name__:
    # initialize logging facilities:
    logging.BASIC_FORMAT = '%(levelname)s: %(message)s'

    # find lirc sources:
    srcdir = find_srcdir()

    # declare some frequenty used regular expressions:
    re_hardware = r'struct\s+hardware\s+hw_\w+\s*=\s*{(.*?)};'
    re_hardware = re.compile(re_hardware, re.DOTALL)

    re_comments = r'/\*\s*(.*?)\s*\*/'
    re_comments = re.compile(re_comments, re.DOTALL)

    re_properties = r'^(?:\s|\*)*(\S+)\s*:\s*(.*?)(?:\s|\*)*$'
    re_properties = re.compile(re_properties)

    re_define = r'^#\s*define\s+(\w+)\s+(.*?)\s*$'
    re_define = re.compile(re_define, re.MULTILINE)

    re_usb_device = r'{\s*USB_DEVICE\s*\(\s*([^,]*),\s*(.*?)\s*\)\s*(.*?)\s*}'
    re_usb_device = re.compile(re_usb_device, re.DOTALL)

    # scan source code for receiver information,
    # and dump this information immediatly:
    print_database_header()
    scan_sources(os.path.join(srcdir, 'daemons'), scan_userspace_driver)
    scan_sources(os.path.join(srcdir, 'drivers'), scan_kernel_driver)
