#!/usr/bin/python3

## Copyright (C) 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 Red Hat, Inc.
## Author: Tim Waugh <twaugh@redhat.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.

import cups
from functools import reduce
cups.require ("1.9.42")
import sys
import socket
import getpass
from debug import *

import dbus

# set up global default main loop
from dbus.mainloop.glib import DBusGMainLoop
DBusGMainLoop(set_as_default=True)

import dbus.service
import gi
from gi.repository import GObject
from gi.repository import GLib
import time
import locale
import cupshelpers.installdriver

DOMAIN="system-config-printer"
import gettext
gettext.install(domain=DOMAIN)
try:
    locale.setlocale (locale.LC_ALL, "")
except locale.Error as e:
    import os
    os.environ['LC_ALL'] = 'C'
    locale.setlocale (locale.LC_ALL, "")

gi.require_version('Notify', '0.7')
from gi.repository import Notify

APPDIR="/usr/share/system-config-printer"
ICON="printer"

# We need to call Notify.init before we can check the server for caps
Notify.init('System Config Printer Notification')

# D-Bus APIs of other objects we'll use.
PRINTING_BUS="org.fedoraproject.Config.Printing"
PRINTING_PATH="/org/fedoraproject/Config/Printing"
PRINTING_IFACE="org.fedoraproject.Config.Printing"
NEWPRINTERDIALOG_IFACE=PRINTING_IFACE + ".NewPrinterDialog"
PRINTERPROPERTIESDIALOG_IFACE=PRINTING_IFACE + ".PrinterPropertiesDialog"

####
#### NewPrinterNotification DBus server (the 'new' way).
####
PDS_PATH="/com/redhat/NewPrinterNotification"
PDS_IFACE="com.redhat.NewPrinterNotification"
PDS_OBJ="com.redhat.NewPrinterNotification"
class NewPrinterNotification(dbus.service.Object):
    STATUS_SUCCESS = 0
    STATUS_MODEL_MISMATCH = 1
    STATUS_GENERIC_DRIVER = 2
    STATUS_NO_DRIVER = 3

    def __init__ (self, system_bus, session_bus):
        self.system_bus = system_bus
        self.session_bus = session_bus
        self.getting_ready = 0
        bus_name = dbus.service.BusName (PDS_OBJ, bus=system_bus)
        dbus.service.Object.__init__ (self, bus_name, PDS_PATH)
        self.notification = None

    @dbus.service.method(PDS_IFACE, in_signature='', out_signature='')
    def GetReady (self):
        TIMEOUT=1200000
        if self.getting_ready == 0:
            n = Notify.Notification.new (_("Configuring new printer"),
                                         _("Please wait..."),
                                        'printer')
            n.set_timeout (TIMEOUT + 5000)
            n.closed = False
            n.connect ('closed', self.on_notification_closed)
            n.show ()
            self.notification = n

        self.getting_ready += 1
        GLib.timeout_add_seconds (TIMEOUT, self.timeout_ready)

    def on_notification_closed (self, notification):
        notification.closed = True

    def timeout_ready (self):
        if self.getting_ready > 0:
            self.getting_ready -= 1
        if (self.getting_ready == 0 and
            self.notification and
            not getattr (self.notification, 'closed', None)):
            self.notification.close ()

        return False

    @dbus.service.method(PDS_IFACE, in_signature='isssss', out_signature='')
    def NewPrinter (self, status, name, mfg, mdl, des, cmd):
        if name.find("/") >= 0:
            # name is a URI, no queue was generated, because no suitable
            # driver was found
            title = _("Missing printer driver")
            devid = "MFG:%s;MDL:%s;DES:%s;CMD:%s;" % (mfg, mdl, des, cmd)
            if (mfg and mdl) or des:
                if (mfg and mdl):
                    device = "%s %s" % (mfg, mdl)
                else:
                    device = des
                text = _("No printer driver for %s.") % device
            else:
                text = _("No driver for this printer.")
            n = Notify.Notification.new (title, text, 'printer')
            if "actions" in Notify.get_server_caps():
                n.set_urgency (Notify.Urgency.CRITICAL)
                n.set_timeout (Notify.EXPIRES_NEVER)
                n.add_action ("setup-printer", _("Search"),
                              lambda x, y:
                                  self.setup_printer (x, y, name, devid))
            else:
                self.setup_printer (None, None, name, devid)

        else:
            # name is the name of the queue which hal_lpadmin has set up
            # automatically.
            c = cups.Connection ()
            try:
                printer = c.getPrinters ()[name]
            except KeyError:
                return

            try:
                filename = c.getPPD (name)
            except cups.IPPError:
                return

            del c

            # Check for missing packages
            cups.ppdSetConformance (cups.PPD_CONFORM_RELAXED)
            ppd = cups.PPD (filename)
            import os
            os.unlink (filename)
            import sys
            sys.path.append (APPDIR)
            import cupshelpers
            (missing_pkgs,
             missing_exes) = cupshelpers.missingPackagesAndExecutables (ppd)

            from cupshelpers.ppds import ppdMakeModelSplit
            (make, model) = ppdMakeModelSplit (printer['printer-make-and-model'])
            driver = make + " " + model
            if status < self.STATUS_GENERIC_DRIVER:
                title = _("Printer added")
            else:
                title = _("Missing printer driver")

            if len (missing_pkgs) > 0:
                pkgs = reduce (lambda x,y: x + ", " + y, missing_pkgs)
                title = _("Install printer driver")
                text = (_("`%s' requires driver installation: %s.") %
                        (name, pkgs))
                n = Notify.Notification.new (title, text, 'printer')
                import installpackage
                if "actions" in Notify.get_server_caps():
                    try:
                        self.packagekit = installpackage.PackageKit ()
                        n.set_timeout (Notify.EXPIRES_NEVER)
                        n.add_action ("install-driver", _("Install"),
                                      lambda x, y:
                                          self.install_driver (x, y,
                                                               missing_pkgs))
                    except:
                        pass
                else:
                    try:
                        self.packagekit = installpackage.PackageKit ()
                        self.packagekit.InstallPackageName (0, 0,
                                                            missing_pkgs[0])
                    except:
                        pass

            elif status == self.STATUS_SUCCESS:
                devid = "MFG:%s;MDL:%s;DES:%s;CMD:%s;" % (mfg, mdl, des, cmd)
                text = _("`%s' is ready for printing.") % name
                n = Notify.Notification.new (title, text, 'printer')
                if "actions" in Notify.get_server_caps():
                    n.set_urgency (Notify.Urgency.NORMAL)
                    n.add_action ("test-page", _("Print test page"),
                                  lambda x, y:
                                      self.print_test_page (x, y, name))
                    n.add_action ("configure", _("Configure"),
                                  lambda x, y: self.configure (x, y, name))
            else: # Model mismatch
                devid = "MFG:%s;MDL:%s;DES:%s;CMD:%s;" % (mfg, mdl, des, cmd)
                text = (_("`%s' has been added, using the `%s' driver.") %
                        (name, driver))
                n = Notify.Notification.new (title, text, 'printer')
                if "actions" in Notify.get_server_caps():
                    n.set_urgency (Notify.Urgency.CRITICAL)
                    n.add_action ("test-page", _("Print test page"),
                                  lambda x, y:
                                      self.print_test_page (x, y, name, devid))
                    n.add_action ("find-driver", _("Find driver"),
                                  lambda x, y: 
                                  self.find_driver (x, y, name, devid))
                    n.set_timeout (Notify.EXPIRES_NEVER)
                else:
                    self.configure (None, None, name)

        self.timeout_ready ()
        n.show ()
        self.notification = n

    def print_test_page (self, notification, action, name):
        path = self.configure (None, None, name)
        obj = self.session_bus.get_object (PRINTING_BUS, path)
        iface = dbus.Interface (obj, PRINTERPROPERTIESDIALOG_IFACE)
        iface.PrintTestPage ()

    def configure (self, notification, action, name):
        obj = self.session_bus.get_object (PRINTING_BUS, PRINTING_PATH)
        iface = dbus.Interface (obj, PRINTING_IFACE)
        return iface.PrinterPropertiesDialog (dbus.UInt32(0), name)

    def get_newprinterdialog_interface (self):
        obj = self.session_bus.get_object (PRINTING_BUS, PRINTING_PATH)
        iface = dbus.Interface (obj, PRINTING_IFACE)
        path = iface.NewPrinterDialog ()
        obj = self.session_bus.get_object (PRINTING_BUS, path)
        iface = dbus.Interface (obj, NEWPRINTERDIALOG_IFACE)
        return iface

    def ignore_dbus_replies (self, *args):
        pass

    def find_driver (self, notification, action, name, devid = ""):
        try:
            iface = self.get_newprinterdialog_interface ()
            iface.ChangePPD (dbus.UInt32(0), name, devid,
                             reply_handler=self.ignore_dbus_replies,
                             error_handler=self.ignore_dbus_replies)
        except dbus.DBusException:
            pass

    def setup_printer (self, notification, action, uri, devid = ""):
        try:
            iface = self.get_newprinterdialog_interface ()
            iface.NewPrinterFromDevice (dbus.UInt32(0), uri, devid,
                                        reply_handler=self.ignore_dbus_replies,
                                        error_handler=self.ignore_dbus_replies)
        except dbus.DBusException:
            pass

    def install_driver (self, notification, action, missing_pkgs):
        try:
            self.packagekit.InstallPackageName (0, 0, missing_pkgs[0])
        except:
            pass

    def collect_exit_code (self, pid):
        # We do this with timers instead of signals because we already
        # have GLib imported, but don't (yet) import signal;
        # let's try not to inflate the process size.
        import os
        try:
            print("Waiting for child %d" % pid)
            (pid, status) = os.waitpid (pid, os.WNOHANG)
            if pid == 0:
                # Run this timer again.
                return True
        except OSError:
            pass

        return False

PROGRAM_NAME="system-config-printer-applet"
def show_help ():
    print("usage: %s [--help|--version|--debug]" % PROGRAM_NAME)

def show_version ():
    import config
    print("%s %s" % (PROGRAM_NAME, config.VERSION))
    
####
#### Main program entry
####

def monitor_session (*args):
    pass

def any_jobs ():
    try:
        c = cups.Connection ()
        jobs = c.getJobs (my_jobs=True, limit=1)
        if len (jobs):
            return True
    except:
        pass

    return False

class RunLoop:
    DBUS_PATH="/com/redhat/PrinterSpooler"
    DBUS_IFACE="com.redhat.PrinterSpooler"

    def __init__ (self, session_bus, system_bus, loop):
        self.system_bus = system_bus
        self.session_bus = session_bus
        self.loop = loop
        self.timer = None
        try:
            system_bus.add_signal_receiver (self.handle_dbus_signal,
                                            path=self.DBUS_PATH,
                                            dbus_interface=self.DBUS_IFACE)
        except dbus.DBusException as e:
            try:
                print ("%s: failed to add D-Bus signal receiver: %s" \
                        % (PROGRAM_NAME, e), file=sys.stderr)
            finally:
                sys.exit (1)

        self.check_for_jobs ()

    def remove_signal_receiver (self):
        try:
            self.system_bus.remove_signal_receiver (self.handle_dbus_signal,
                                                    path=self.DBUS_PATH,
                                                    dbus_interface=self.DBUS_IFACE)
        except dbus.DBusException as e:
            try:
                print ("%s: failed to remove D-Bus signal receiver: %s" \
                        % (PROGRAM_NAME, e), file=sys.stderr)
            except:
                pass

    def run (self):
        self.loop.run ()

    def __del__ (self):
        self.remove_signal_receiver ()
        if self.timer:
            GLib.source_remove (self.timer)

    def handle_dbus_signal (self, *args):
        if self.timer:
            GLib.source_remove (self.timer)
        self.timer = GLib.timeout_add (200, self.check_for_jobs)

    def check_for_jobs (self, *args):
        debugprint ("checking for jobs")
        if any_jobs ():
            if self.timer is not None:
                GLib.source_remove (self.timer)

            self.remove_signal_receiver ()

            # Start the job applet.
            debugprint ("Starting job applet")
            try:
                obj = self.session_bus.get_object (PRINTING_BUS, PRINTING_PATH)
                iface = dbus.Interface (obj, PRINTING_IFACE)
                path = iface.JobApplet ()
                debugprint ("Job applet is %s" % path)
            except dbus.DBusException as e:
                try:
                    print(e)
                except:
                    pass

        # Don't run this timer again.
        return False

if __name__ == '__main__':
    import sys, getopt
    try:
        opts, args = getopt.gnu_getopt (sys.argv[1:], '',
                                        ['debug',
                                         'help',
                                         'version'])
    except getopt.GetoptError:
        show_help ()
        sys.exit (1)

    for opt, optarg in opts:
        if opt == "--help":
            show_help ()
            sys.exit (0)
        if opt == "--version":
            show_version ()
            sys.exit (0)
        elif opt == "--debug":
            set_debugging (True)

    # Must be done before connecting to D-Bus (for some reason).
    if not Notify.init (PROGRAM_NAME):
        try:
            print(("%s: unable to initialize pynotify" %
                                  PROGRAM_NAME), file=sys.stderr)
        except:
            pass

    system_bus = session_bus = None
    try:
        system_bus = dbus.SystemBus()
    except:
        try:
            print(("%s: failed to connect to system D-Bus" %
                                  PROGRAM_NAME), file=sys.stderr)
        finally:
            sys.exit (1)

    try:
        session_bus = dbus.SessionBus()
        # Stop running when the session ends.
        session_bus.add_signal_receiver (monitor_session)
    except:
        try:
            print(("%s: failed to connect to "
                                  "session D-Bus" % PROGRAM_NAME), file=sys.stderr)
        finally:
            sys.exit (1)

    try:
        NewPrinterNotification(system_bus, session_bus)
    except:
        try:
            print(("%s: failed to start "
                                  "NewPrinterNotification service" %
                                  PROGRAM_NAME), file=sys.stderr)
        except:
            pass

    try:
        cupshelpers.installdriver.set_debugprint_fn (debugprint)
        cupshelpers.installdriver.PrinterDriversInstaller(system_bus)
    except Exception as e:
        try:
            print(("%s: failed to start "
                                  "PrinterDriversInstaller service: "
                                  "%s" % (PROGRAM_NAME, e)), file=sys.stderr)
        except:
            pass

    try:
        sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
        # Create an abstract socket, by prefixing it with null.
        sock.bind("\0printer-applet-lock-user-" + getpass.getuser()) 
    except socket.error as err:
        error_code = err.args[0]
        error_string = err.args[1]
        print("Process already running ({0}:{1}). Exiting.".format(error_code, error_string))
        sys.exit(0)

    loop = GLib.MainLoop ()
    runloop = RunLoop (session_bus, system_bus, loop)
    try:
        runloop.run ()
    except KeyboardInterrupt:
        pass
