#!/bin/env python
import dbus, sys, os, time, signal
import traceback
sys.path.append("/usr/share/system-config-printer")
import cups, cupshelpers, ppds
from syslog import *

def get_hplip_uris_for_usb (fax=False, checkuri=None):
    hpuris = []
    cmd = "LC_ALL=C lsusb 2> /dev/null"
    try:
        lsusboutput = os.popen (cmd, 'r').readlines ()
    except:
        if checkuri:
            return False
        else:
            return hpuris
    for line in lsusboutput:
        if (line.find ("ID 03f0:") < 0): continue
        bus = line[4:7]
        device = line[15:18]
        if fax:
            type="-f"
        else:
            type="-c"
        cmd = \
            "LC_ALL=C hp-makeuri -lnone %s %s:%s 2>/dev/null" % \
            (type, bus, device)
        try:
            uri = os.popen (cmd, 'r').readlines ()[0].strip ()
        except:
            continue
        if (not uri): continue
        if checkuri and checkuri == uri:
             return True
        hpuris.append (uri)
    if checkuri:
        return False
    else:
        return hpuris

class HalPrinter:
    def __init__(self):
        self.get_properties()
        self.uris = None
        self.hp_fax_uris = None
        try:
            self.cups_connection = cups.Connection()
        except RuntimeError, e:
            syslog (LOG_ERR,
                    "Unable to connect to CUPS: '%s'.  Is CUPS running?" % e)
            sys.exit (1)

    def get_properties(self):
        self.properties = {}
        for key, value in os.environ.iteritems():
            if key.startswith("HAL_PROP_"):
                name = key[9:].lower().replace("_", '.')
                self.properties[name] = value
        self.uid = os.getenv("UDI", "")
        self.read()

    def read(self):
        p = self.properties
        self.make = (p.get("printer.vendor", "") or
                     p.get("usb.vendor", "Unknown"))
        self.model = (p.get("printer.product", "") or
                      p.get("usb.product", "Unknown"))
        self.description = p.get("printer.description", "")
        self.name = self.get_name()
        self.faxname = self.name + "_fax"
        self.commandsets = p.get('printer.commandset', '').split('\t')

    def get_name(self):
        # XXX check for unallowed chars
        if self.properties.has_key("usb.port_number"):
            name = "%s-%s" % (self.model,
                              self.properties["usb.port_number"])
        else:
            name = self.model
        name = name.replace(" ", "_")
        name = name.replace("/", "_")
        return name.replace("#", "_")

    def get_cups_uris(self, removed=False):
        if self.uris != None:
            return self.uris
        uris=["hal://%s" % self.uid]
        if self.properties.has_key("printer.vendor"):
            vendor = self.properties["printer.vendor"].lower ()
            if (self.properties.get("linux.subsystem","") == "usb" and
                self.properties.has_key("printer.product")):
                # Use a 'usb:...' URI.  Use the same method the CUPS
                # usb backend uses to construct it.
                make = self.properties["printer.vendor"]
                model = self.properties["printer.product"]
                serial = self.properties.get ("printer.serial", None)
                if vendor == "hewlett-packard":
                    make = "HP"
                elif vendor == "lexmark international":
                    make = "Lexmark"
                if model.lower ().startswith (make.lower ()):
                    model = model[len (make):]
                    model = model.lstrip ()
                model = model.rstrip ()
                
                uri = "usb://%s/%s" % (make, model)
                uri = uri.replace (" ", "%20")
                if serial:
                    uri += "?serial=%s" % serial
                uris.insert (0, uri)
                
            if (not removed and
                (vendor == "hewlett-packard" or vendor == "hp")):
                # Perhaps HPLIP can drive this device.  If so, we
                # should use an 'hp:...' URI for CUPS.
                try:
                    # Try to match serial number (support for having more
                    # than one printer of the same model on one machine)
                    hpuris = get_hplip_uris_for_usb ()
                    matchfound = 0
                    if self.properties.has_key("printer.serial"):
                        for uri in hpuris:
                            s = uri.find ("?serial=")
                            if s == -1: continue
                            s += 8
                            e = uri[s:].find ("?")
                            if e == -1: e = len (uri)
                            serial = uri[s:s+e]
                            if serial == self.properties["printer.serial"]:
                                uris.insert (0, uri)
                                matchfound = 1
                                break
                    # Fall back to printer model
                    if matchfound == 0:
                        for uri in hpuris:
                            s = uri.find ("/usb/")
                            if s == -1: s = uri.find ("/par/")
                            if s == -1: s = uri.find ("/net/")
                            if s == -1: continue
                            s += 5
                            e = uri[s:].find ("?")
                            if e == -1: e = len (uri)
                            prod = uri[s:s+e].lower ().strip ()
                            if prod.startswith ("hp_"):
                                prod = prod[3:]
                            halprod = self.properties["printer.product"].lower ().strip ()
                            halprod = halprod.replace (" ", "_")
                            if halprod.startswith ("hp_"):
                                halprod = halprod[3:]
                            if prod == halprod:
                                matchfound = 1
                                uris.insert (0, uri)
                                break
                except:
                    pass

        self.uris = uris
        return uris

    def get_cups_uri(self):
        return self.get_cups_uris()[0]

    def get_cups_hp_fax_uris(self, removed=False):
        if self.hp_fax_uris != None:
            return self.hp_fax_uris
        faxurisfound = 0
        if self.properties.has_key("printer.vendor"):
            vendor = self.properties["printer.vendor"].lower ()
            if (not removed and
                (vendor == "hewlett-packard" or vendor == "hp")):
                # We only can have a fax URI if we have an HP printer
                # supported by HPLIP
                try:
                    # Try to find a matching HPLIP fax URI for the HPLIP
                    # print URI for this device
                    hpfaxuris = get_hplip_uris_for_usb (True)
                    uris = self.get_cups_uris ()
                    faxuris = []
                    for uri in uris:
                        if not uri.startswith ("hp:"): continue
                        faxuri = uri.replace("hp:/", "hpfax:/")
                        for furi in hpfaxuris:
                            if faxuri == furi:
                                faxurisfound = 1
                                faxuris.append (faxuri)
                except:
                    pass
        
        if faxurisfound == 1:
            self.hp_fax_uris = faxuris
            return faxuris
        else:
            return None

    def get_cups_hp_fax_uri(self):
        faxuris = self.get_cups_hp_fax_uris()
        if faxuris:
            return faxuris[0]
        else:
            return None

    def add(self):
        syslog (LOG_DEBUG, "add")
        printers = cupshelpers.getPrinters(self.cups_connection)
        printers_extra_info = None
        uris = self.get_cups_uris ()
        syslog (LOG_DEBUG, "URIs: %s" % uris)
        faxuris = self.get_cups_hp_fax_uris ()
        syslog (LOG_DEBUG, "HPLIP Fax URIs: %s" % faxuris)
        printer_exists = 0
        fax_exists = 0
        p = None
        for name, printer in printers.iteritems():
            if printer.is_class: continue
            if (name == self.name or
                printer.device_uri in uris):
                syslog (LOG_DEBUG,
                        "Not adding printer: %s already exists" % name)
                printer_exists = 1
                if not printer.enabled:
                    if printers_extra_info == None:
                        printers_extra_info = self.cups_connection.getPrinters()
                    statemsg = printers_extra_info[name]["printer-state-message"]
                    if statemsg.lower ().startswith ("unplugged"):
                        syslog (LOG_INFO,
                                "Re-enabling printer %s" % name)
                        self.cups_connection.enablePrinter(name)
                    else:
                        syslog (LOG_INFO,
                                "Printer %s exists but is disabled, reason: %s; "
                                "use 'cupsenable %s' to enable it" % (name, statemsg, name))
            if (faxuris and
                (name == self.faxname or
                 printer.device_uri in faxuris)):
                syslog (LOG_DEBUG,
                        "Not adding fax printer: %s already exists" % name)
                fax_exists = 1
                if not printer.enabled:
                    if printers_extra_info == None:
                        printers_extra_info = self.cups_connection.getPrinters()
                    statemsg = printers_extra_info[name]["printer-state-message"]
                    if statemsg.lower ().startswith ("unplugged"):
                        syslog (LOG_INFO,
                                "Re-enabling fax printer %s" % name)
                        self.cups_connection.enablePrinter(name)
                    else:
                        syslog (LOG_INFO,
                                "Fax printer %s exists but is disabled, reason: %s; "
                                "use 'cupsenable %s' to enable it" % (name, statemsg, name))

        def wait_child (sig, stack):
            (pid, status) = os.wait ()

        signal.signal (signal.SIGCHLD, wait_child)
        pid = os.fork ()
        if pid == 0:
            # Child.
            if fax_exists == 0:
                # really new fax printer
                faxuri = self.get_cups_hp_fax_uri()
            else:
                faxuri = None

            if printer_exists == 0 or faxuri:
                # really new printer or fax - show tray icon with magnifier
                bus = dbus.SystemBus()
                try:
                    syslog (LOG_DEBUG, "Calling GetReady")
                    obj = bus.get_object("com.redhat.NewPrinterNotification",
                                         "/com/redhat/NewPrinterNotification")
                    notification = dbus.Interface(obj,
                                                  "com.redhat.NewPrinterNotification")
                    notification.GetReady ()
                except dbus.DBusException, e:
                    syslog (LOG_DEBUG, "D-Bus method call failed: %s" % e)
                    notification = None
            else:
                notification = None

            if printer_exists == 0:
                # really new printer - try autodetection
                if p == None:
                    cupsppds = self.cups_connection.getPPDs ()
                    p = ppds.PPDs (cupsppds)
                syslog (LOG_DEBUG, "Device ID: MFG:%s;MDL:%s;DES:%s;CMD:%s; URI:%s" %
                        (self.make, self.model, self.description,
                         reduce(lambda x, y: x + ',' + y, self.commandsets),
                         self.get_cups_uri()))
                (status, ppdname) = p.getPPDNameFromDeviceID (self.make, self.model,
                                                              self.description,
                                                              self.commandsets,
                                                              self.get_cups_uri())
                syslog (LOG_DEBUG, "PPD: %s; Status: %d" % (ppdname, status))

                info = "%s %s" % (self.make, self.model)

                self.cups_connection.addPrinter(self.name,
                                                device=self.get_cups_uri(),
                                                ppdname=ppdname, info=info)
                self.cups_connection.enablePrinter(self.name)
                self.cups_connection.acceptJobs(self.name)
                syslog (LOG_INFO, "Added printer %s" % self.name)

            if faxuri:
                faxname = self.faxname
                if p == None:
                    cupsppds = self.cups_connection.getPPDs ()
                    p = ppds.PPDs (cupsppds)
                (status, faxppd) = p.getPPDNameFromDeviceID ("HP", "Fax",
                                                        "HP Fax", [], faxuri)
                info = "Fax queue for %s %s" % (self.make, self.model)
                self.cups_connection.addPrinter(faxname, device=faxuri,
                                                ppdname=faxppd, info=info)
                self.cups_connection.enablePrinter(faxname)
                self.cups_connection.acceptJobs(faxname)
                syslog (LOG_INFO, "Added fax printer %s" % faxname)

            if notification:
                if faxuri and printer_exists != 0:
                    # Only fax queue
                    n = faxname
                    m = self.model + " (Fax)"
                else:
                    n = self.name
                    m = self.model
                try:
                    notification.NewPrinter (status, n,
                                             self.make, m,
                                             self.description,
                                             reduce(lambda x, y: x + ',' + y,
                                                    self.commandsets))
                except dbus.DBusException:
                    pass

        elif pid == -1:
            pass # should handle error

    def remove(self):
        syslog (LOG_DEBUG, "remove")
        # Disable all print queues which print to the device which
        # we detected as having been removed. This prevents from
        # jobs being retried every 30 seconds. The jobs wait in the
        # queue until the device is reconnected and turned on.
        #
        # We cannot ask CUPS for the HPLIP URIs after having unplugged or
        # turned off the printer. So we take model name and serial number
        # provided by HAL and search the print queues whose URIs contain
        # this model name and derial number. These are then the queues
        # which we will disable.
        printers = cupshelpers.getPrinters(self.cups_connection)
        printers_extra_info = None
        make = self.properties["printer.vendor"]
        makel = make.lower ()
        model = self.properties["printer.product"]
        serial = self.properties.get ("printer.serial", None)
        if makel == "hewlett-packard":
            make = "HP"
        elif makel == "lexmark international":
            make = "Lexmark"
        if model.startswith (make):
            model = model[len (make):]
            model = model.lstrip ()
        for name, printer in printers.iteritems():
            if printer.is_class: continue
            if ((printer.device_uri.find (model.replace (" ", "%20")) != -1 or
                 printer.device_uri.find (model.replace (" ", "_")) != -1) and
                (not serial or
                 (serial and
                  printer.device_uri.find ("serial=" + serial)))):
                syslog (LOG_DEBUG,
                         "Found configured printer: %s" % name)
                if printer.enabled:
                    if ((not printer.device_uri.startswith ("hp:") and
                         not printer.device_uri.startswith ("hpfax:")) or
                        (not get_hplip_uris_for_usb (False, printer.device_uri) and
                         not get_hplip_uris_for_usb (True, printer.device_uri))):
                        self.cups_connection.disablePrinter(name,
                                                        "Unplugged or turned off")
                        syslog (LOG_INFO,
                                "Disabled printer %s, as the corresponding device was unplugged or turned off" % (name))

    def configure(self):
        syslog (LOG_DEBUG, "configure")
        make, model = sys.stdin.readlines()
        if make[-1]=="\n": make = make[:-1]
        if model[-1]=="\n": model = model[:-1]

        cupsppds = self.cups_connection.getPPDs ()
        p = ppds.PPDs (cupsppds)
        (status, ppdname) = p.getPPDNameFromDeviceID (make, model, "", "")

        if not ppdname:
            syslog (LOG_ERR,
                    "User-selected make/model \"%s\" \"%s\" not found" %
                    (make, model))
            return

        # add printer
        self.cups_connection.addPrinter(
            self.name, device=self.get_cups_uri(),
            ppdname=ppdname, info="Added by HAL")
        self.cups_connection.enablePrinter(self.name)
        self.cups_connection.acceptJobs(self.name)
        syslog (LOG_INFO,
                "Added printer %s with user-selected make/model" % self.name)

class HalLpAdmin:

    def __init__(self):
        if len(sys.argv)!=2:
            return self.usage()

        if sys.argv[1]=="--add":
            self.addPrinter()
        elif sys.argv[1]=="--remove":
            self.removePrinter()
        elif sys.argv[1]=="--configure":
            self.configurePrinter()
        else:
            return self.usage()

    def usage(self):
        print "Usage: hal_lpadmin (--add|--remove|--configure)"

    def addPrinter(self):
        printer = HalPrinter()
        printer.add()
        
    def removePrinter(self):
        printer = HalPrinter()
        printer.remove()

    def configurePrinter(self):
        printer = HalPrinter()
        printer.configure()

def main():
    openlog ("hal_lpadmin", 0, LOG_DAEMON)
    time.sleep (1) # Give HPLIP a chance to reconnect
    try:
        h = HalLpAdmin()
    except:
        (type, value, tb) = sys.exc_info ()
        tblast = traceback.extract_tb (tb, limit=None)
        if len (tblast):
            tblast = tblast[:len (tblast) - 1]
        for line in traceback.format_tb (tb):
            syslog (LOG_ERR, line.strip ())
        extxt = traceback.format_exception_only (type, value)
        syslog (LOG_ERR, extxt[0].strip ())

main()
