#!/usr/bin/python
#
# Based on inf2mondb.py from RedHat
#
# originally by Matt Wilson <msw@redhat.com>
# option parsing and database comparison by Fred New
# ini parsing completely rewritten by Matt Domsch <Matt_Domsch@dell.com> 2006
#
# Copyright 2002 Red Hat, Inc.
# Copyright 2006 Dell, Inc.
# Copyright 2007 Sebastian Heinlein
#
# This software may be freely redistributed under the terms of the GNU
# library public license.
#
# You should have received a copy of the GNU Library Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
"""
Provides an importer for Microsoft Windows monitor descriptions

The code can be used as a python module for or as a script to add new monitor
definitions to a monitor database.

In code example: Read the list of monitors from an inf file.

import infimport
monitors = infimport.get_monitors_from_inf(PATH)

Script example: To check for monitors of an inf file that are not yet in the database.

./infimport.py MONITORS.inf /usr/share/hwdata/MonitorsDB
"""

import sys
import string
import re
import ConfigParser
import os

import logging

logging.basicConfig()
log = logging.getLogger("infimport")
#log.setLevel(logging.DEBUG)
log.setLevel(logging.INFO)

# this is a class to deal with various file line endings and leading whitespace
# converts all \r line endings to \n.
# It also strips leading whitespace.
# NOTE: be sure to always return _something_, even if it is just "\n", or we 
# break the file API.  (nothing == eof)
class myFile(object):
    def __init__(self, *args):
        self.fd = open(*args)

    def close(self):
        return self.fd.close()
    
    def readline(self, *args):
        line = self.fd.readline(*args)
        line = line.replace('\r', '\n')
        line = line.replace('\n\n', '\n')
        line = line.lstrip(" \t")
        return line


# we will use this to override default option parsing in ConfigParser to handle
# Microsoft-style "INI" files. (Which do not necessarily have " = value " after
# the option name
OPTCRE = re.compile(
        r'(?P<option>[^:=\s][^:=]*)'          # very permissive!
        r'\s*(?P<vi>[:=]{0,1})\s*'            # any number of space/tab,
                                              # optionally followed by
                                              # separator (either : or =)
                                              # optionally followed
                                              # by any # space/tab
        r'(?P<value>.*)$'                     # everything up to eol
        )

percentSplit = re.compile(r'%(?P<field>.*)%')
def _percent_to_string(ini, strings, name):
    mo = percentSplit.match(name)
    if (mo):
        field = mo.group('field')
        try:
            val = strings[field.lower()]
        except KeyError:
            return ""
        return val.strip(" '\"")
    return ""

def get_monitors_from_database(path):
    """Returns a dictonary of the found monitor models in the given 
       monitor models database"""
    monitors = {}
    try:
        mdb = open(path, 'r')
    except IOError, (errno, str):
        log.error("Unable to open %s: %s" % (path, str))
        return {}
    for line in mdb.readlines():
        if len(line.strip()) == 0 or line.startswith('#'):
            continue
        line_split = line.split(";")
        vendor = line_split[0].strip()
        name = line_split[1].strip()
        id = line_split[2].strip()
        if monitors.has_key((vendor, name, id)):
            log.warn("Duplicated entry: %s" % line)
        else:
            monitors[(vendor, name, id)] = line
    mdb.close()
    return monitors

def get_monitors_from_inf(path):
    """Returns a dictonary of the found monitor models in the given .inf file"""
    monitors = {}
    ini = ConfigParser.ConfigParser()
    # FIXME: perhaps could be done in a nicer way, but __builtins__ is a dict
    #        for imported modules
    #ini.optionxform = __builtins__.str
    ini.optionxform = type("")
    ini.OPTCRE = OPTCRE
    try:
        f = myFile(path)
        ini.readfp(f)
        f.close()
    except IOError, (errno, str):
        log.error("Unable to open %s: %s" % (path, str))
        sys.exit(1)

    # a dictionary of manufacturers we're looking at
    manufacturers = {}
    # a big fat dictionary of strings to use later on.
    strings = {}

    # This RE is for EISA info lines
    # %D5259A%=D5259A, Monitor\HWP0487
    monitor1Re = re.compile(r'.*,.*Monitor\\(?P<id>[^\s]*)')
    # This one is for legacy entries
    # %3020%     =PB3020,   MonID_PB3020
    monitor2Re = re.compile(r'.*,.*MonID_(?P<id>[^\s]*)')

    for section in ini.sections():
        if section.lower() == "manufacturer":
            for mfr in ini.options(section):
                # generate the vendor.arch funny entries
                manufacturer_values = string.split(ini.get(section, mfr), 
                                                       ',')
                manufacturers[manufacturer_values[0]] = mfr
                while len(manufacturer_values) > 1:
                    manufacturers["%s.%s" % (manufacturer_values[0], 
                                             manufacturer_values[-1])] = mfr
                    manufacturer_values = manufacturer_values[0:-1]

        elif section.lower() == "strings":
            for key in ini.options(section):
                strings[key.lower()] = string.strip(ini.get(section, key))

    for mfr in manufacturers.keys():
        if ini.has_section(mfr):
            monitor_vendor_name = manufacturers[mfr]
            for monitor_name in ini.options(mfr):
                v = ini.get(mfr, monitor_name)
                v = v.split(',')
                install_key = v[0].strip()

                line = ini.get(mfr, monitor_name)
                # Find monitor inf IDs and EISA ids

                edid = "0"
                mo = monitor1Re.match(line)
                if mo:
                    edid = mo.group('id')
                else:
                    mo = monitor2Re.match(line)
                    if mo:
                        edid = mo.group('id').strip()

                #if self.monitors.has_key(edid.lower()):
                #   continue

                if ini.has_section(install_key):
                    line = ini.get(install_key, "AddReg")
                    if line:
                        sline = line.split(',')
                        registry = sline[0]
                        try:
                            resolution = sline[1]
                        except IndexError:
                            resolution = ""
                        try:
                            dpms = sline[2]
                        except IndexError:
                            dpms = ""

                        if ini.has_section(registry):
                            for line in ini.options(registry):
                                if string.find(line, 'HKR,"MODES') >= 0:
                                    sline = line.split('"')
                                    try:
                                        syncline = sline[3]
                                    except IndexError:
                                        syncline = ","
                                    syncline = syncline.split(',')
                                    hsync = syncline[0].strip()
                                    vsync = syncline[1].strip()

                                    vendor_clear = _percent_to_string(ini,
                                            strings, monitor_vendor_name)
                                    monitor_clear = _percent_to_string(ini,
                                            strings, monitor_name)

                                    output = "%s; %s; %s; %s; %s" % \
                                             (vendor_clear, monitor_clear,
                                              edid, hsync, vsync)
                                    if dpms.lower().strip() == "dpms":
                                        output = output + "; 1"

                                    if not monitors.has_key((vendor_clear,
                                            monitor_clear, edid.lower())):
                                        log.debug("added %s" % output)
                                        monitors[(vendor_clear,
                                                  monitor_clear,
                                                  edid.lower())] = output
                                    else:
                                        log.warn("duplicated entry %s" % output)
    return monitors

def write_monitors_to_file(monitors, path):
    """Writes monitors as a monitor models database"""
    try:
        if os.path.exists(path):
            os.remove(path)
        mdb = open(path, 'w')
        mdb.writelines(map(lambda l: "%s\n" % l, monitors.values()))
        mdb.close()
    except IOError, (errno, str):
        log.error("Unable to write %s: %s" % (path, str))
        return False

def append_monitors_to_file(monitors, path):
    """Appends monitors to a monitor models database"""
    try:
        if os.path.exists(path):
            os.remove(path)
        mdb = open(path, 'a')
        mdb.writelines(map(lambda l: "%s\n" % l, monitors.values()))
        mdb.close()
    except IOError, (errno, str):
        log.error("Unable to write %s: %s" % (path, str))
        return False

if __name__ == "__main__":
    from optparse import OptionParser
    import sys

    parser = OptionParser()
    parser.add_option("-a", "--append",
                      action="store_true", dest="append",
                      help="Append new models to the database")
    parser.add_option("-o", "--output",
                      default=None,
                      action="store", type="string", dest="output",
                      help="Write changes to an alternative file")
    parser.usage = "%prog [options] INF_FILE [MONITOR_DATABASE]"
    (options, args) = parser.parse_args()

    if len(args) == 2:
        # continue with normal operation
        pass
    elif len(args) == 1:
        # jsut print the monitors from the given inf file
        monitors_inf = get_monitors_from_inf(args[0])
        for mon in monitors_inf.values():
            print "%s" % mon
        sys.exit()
    else:
        parser.error("You have to specify an .inf file that contains the "
                     "monitor models that you want to add and a "
                     "monitor model database")

    monitors_inf = get_monitors_from_inf(args[0])
    monitors_db = get_monitors_from_database(args[1])

    monitors_new = {}
    for mon in monitors_inf.keys():
        if not monitors_db.has_key(mon):
            log.info("New monitor: %s" % monitors_inf[mon])
            monitors_new[mon] = monitors_inf[mon]

    if options.append:
        if options.output:
            append_monitors_to_file(monitors_new, options.output)
        else:
            append_monitors_to_file(new_monitors, args[1])
