#!/usr/bin/python3

## Copyright (C) 2010, 2011, 2012, 2013, 2014 Red Hat, Inc.
## Authors:
##  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 asyncconn
import cups
from gi.repository import GLib
from gi.repository import Gdk
from gi.repository import Gtk
import os
from shutil import copyfileobj
from tempfile import NamedTemporaryFile
from debug import *

cups.require ("1.9.50")

class PPDCache:
    def __init__ (self, host=None, port=None, encryption=None):
        self._cups = None
        self._exc = None
        self._cache = dict()
        self._modtimes = dict()
        self._host = host
        self._port = port
        self._encryption = encryption
        self._queued = list()
        self._connecting = False
        debugprint ("+%s" % self)

    def __del__ (self):
        debugprint ("-%s" % self)
        if self._cups:
            self._cups.destroy ()

    def fetch_ppd (self, name, callback, check_uptodate=True):
        if check_uptodate and name in self._modtimes:
            # We have getPPD3 so we can check whether the PPD is up to
            # date.
            debugprint ("%s: check if %s is up to date" % (self, name))
            self._cups.getPPD3 (name,
                                modtime=self._modtimes[name],
                                reply_handler=lambda c, r:
                                    self._got_ppd3 (c, name, r, callback),
                                error_handler=lambda c, r:
                                    self._got_ppd3 (c, name, r, callback))
            return

        try:
            f = self._cache[name]
        except RuntimeError as e:
            self._schedule_callback (callback, name, None, e)
            return
        except KeyError:
            if not self._cups:
                self._queued.append ((name, callback))
                if not self._connecting:
                    self._connect ()

                return

            debugprint ("%s: fetch PPD for %s" % (self, name))
            self._cups.getPPD3 (name,
                                reply_handler=lambda c, r:
                                    self._got_ppd3 (c, name, r, callback),
                                error_handler=lambda c, r:
                                    self._got_ppd3 (c, name, r, callback))
            return

        # Copy from our file object to a new temporary file, create a
        # PPD object from it, then remove the file.  This way we don't
        # leave temporary files around even though we are caching...
        f.seek (0)
        with NamedTemporaryFile () as tmpf:
            copyfileobj (f, tmpf)

            try:
                ppd = cups.PPD (tmpf.file)
                self._schedule_callback (callback, name, ppd, None)
            except Exception as e:
                self._schedule_callback (callback, name, None, e)

    def _connect (self, callback=None):
        self._connecting = True
        asyncconn.Connection (host=self._host, port=self._port,
                              encryption=self._encryption,
                              reply_handler=self._connected,
                              error_handler=self._connected)

    def _got_ppd (self, connection, name, result, callback):
        if isinstance (result, Exception):
            self._schedule_callback (callback, name, none, result)
        else:
            # Store an open file object, then remove the actual file.
            # This way we don't leave temporary files around.
            try:
                self._cache[name] = open (result, "rb")
                debugprint ("%s: caching %s (fd %d)" %
                            (self, result,
                             self._cache[name].fileno()))
                os.unlink (result)
            except IOError as exc:
                self._schedule_callback (callback, name, None, exc)
                return

            self.fetch_ppd (name, callback)

    def _got_ppd3 (self, connection, name, result, callback):
        (status, modtime, filename) = result
        if status in [cups.HTTP_OK, cups.HTTP_NOT_MODIFIED]:
            if status == cups.HTTP_NOT_MODIFIED:
                # The file is no newer than the one we already have.

                # CUPS before 1.5.3 created a temporary file in error
                # in this situation (STR #4018) so remove that.
                try:
                    os.unlink (filename)
                except OSError:
                    pass

            elif status == cups.HTTP_OK:
                # Our version of the file was older.  Cache the new version.

                # Store an open file object, then remove the actual
                # file.  This way we don't leave temporary files
                # around.
                try:
                    self._cache[name] = open (filename, "rb")
                    debugprint ("%s: caching %s (fd %d) "
                                "(%s) - %s" % (self, filename,
                                               self._cache[name].fileno (),
                                               modtime, status))
                    os.unlink (filename)
                    self._modtimes[name] = modtime
                except IOError as exc:
                    # File disappeared?
                    debugprint ("%s: file %s disappeared? Unable to cache it"
                                % (self, filename))
                    self._schedule_callback (callback, name, None, exc)
                    return

            # Now fetch it from our own cache.
            self.fetch_ppd (name, callback, check_uptodate=False)
        else:
            self._schedule_callback (callback, name,
                                     None, cups.HTTPError (status))

    def _connected (self, connection, exc):
        self._connecting = False
        if isinstance (exc, Exception):
            self._cups = None
            self._exc = exc
        else:
            self._cups = connection

        queued = self._queued
        self._queued = list()
        for name, callback in queued:
            self.fetch_ppd (name, callback)

    def _schedule_callback (self, callback, name, result, exc):
        def cb_func (callback, name, result, exc):
            Gdk.threads_enter ()
            callback (name, result, exc)
            Gdk.threads_leave ()
            return False

        GLib.idle_add (cb_func, callback, name, result, exc)

if __name__ == "__main__":
    import sys
    from debug import *
    from gi.repository import GObject
    set_debugging (True)
    Gdk.threads_init ()
    loop = GObject.MainLoop ()

    def signal (name, result, exc):
        debugprint ("**** %s" % name)
        debugprint (repr (result))
        debugprint (repr (exc))

    c = cups.Connection ()
    printers = c.getPrinters ()
    del c

    cache = PPDCache ()
    p = None
    for p in printers:
        cache.fetch_ppd (p, signal)

    if p:
        GLib.timeout_add_seconds (1, cache.fetch_ppd, p, signal)
        GLib.timeout_add_seconds (5, loop.quit)
    loop.run ()
