#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# (c) Copyright 2003-2008 Hewlett-Packard Development Company, L.P.
#
# 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
#
# Author: Don Welch
#

__version__ = '10.1'
__title__ = "Services and Status System Tray dBus Child Process"
__doc__ = "Provides persistent data and event services to HPLIP client applications. Required to be running for PC send fax, optional in all other cases."


# StdLib
import sys
import struct
import os
import time
import getopt
import select
import signal
import tempfile

# Local
from base.g import *
from base.codes import *
from base import utils, device, status

# dBus
try:
    from dbus import lowlevel, SystemBus, SessionBus
    import dbus.service
    from dbus.mainloop.glib import DBusGMainLoop
    from gobject import MainLoop
    dbus_loaded = True
except ImportError:
    log.error("dbus failed to load (python-dbus ver. 0.80+ required). Exiting...")
    dbus_loaded = False
    sys.exit(1)
    
    
# Globals
PIPE_BUF = 4096
dbus_loop = None
system_bus = None
session_bus = None
w = None
devices = {} # { 'device_uri' : DeviceCache, ... }


USAGE = [(__doc__, "", "name", True),
         ("Usage: hpssd.py [OPTIONS]", "", "summary", True),
         utils.USAGE_OPTIONS,
         utils.USAGE_LOGGING1, utils.USAGE_LOGGING2,
         ("Run in debug mode:", "-g (same as options: -ldebug -x)", "option", False),
         utils.USAGE_HELP,
        ]


def usage(typ='text'):
    if typ == 'text':
        utils.log_title(__title__, __version__)

    utils.format_text(USAGE, typ, __title__, 'hpssd.py', __version__)
    sys.exit(0)



class DeviceCache(object):
    def __init__(self, model=''):
        self.history = utils.RingBuffer(prop.history_size) # circular buffer of ServiceEvent
        self.model = device.normalizeModelName(model)
        self.cache = {} # variable name : value
        self.faxes = {} # (username, jobid): FaxEvent



class ServiceEvent(device.Event):
    def __init__(self, device_uri, printer_name, event_code, username, job_id, title):
        device.Event.__init__(self, device_uri, printer_name, event_code, username, job_id, title, time.time())

    def debug(self):
        log.debug("EVENT:")
        device.Event.debug(self)

    def __str__(self):
        return "<ServiceEvent('%s', '%s', %d, '%s', %d, '%s', %f)>" % self.as_tuple()



class FaxEvent(device.Event):
    def __init__(self, temp_file, event):
        device.Event.__init__(self, *event.as_tuple())
        self.temp_file = temp_file

    def debug(self):
        log.debug("FAX:")
        device.Event.debug(self)
        log.debug("    temp_file=%s" % self.temp_file)

    def __str__(self):
        return "<FaxEvent('%s', '%s', %d, '%s', %d, '%s', %f, '%s')>" % self.as_tuple()      

    def as_tuple(self):
        return (self.device_uri, self.printer_name, self.event_code, 
             self.username, self.job_id, self.title, self.timedate,
             self.temp_file)



#  dbus interface on session bus
class StatusService(dbus.service.Object):
    def __init__(self, name, object_path):
        dbus.service.Object.__init__(self, name, object_path)


    @dbus.service.method('com.hplip.StatusService', in_signature='s', out_signature='a(ssisisd)')
    def GetHistory(self, device_uri):
        log.debug("GetHistory('%s')" % device_uri)
        try:
            devices[device_uri]
        except KeyError:
            #log.warn("Unknown device URI: %s" % device_uri)
            return []
        else:
            h = devices[device_uri].history.get()
            log.debug("%d events in history:" % len(h))
            [x.debug() for x in h]
            return [x.as_tuple() for x in h]



    @dbus.service.method('com.hplip.StatusService', in_signature='ssi', out_signature='i')
    def SetCachedIntValue(self, device_uri, key, value):
        log.debug("SetCachedIntValue('%s', '%s', %d)" % (device_uri, key, value))
        if check_device(device_uri) == ERROR_SUCCESS: 
            devices[device_uri].cache[key] = value
            return value

        return -1


    @dbus.service.method('com.hplip.StatusService', in_signature='ss', out_signature='i')
    def GetCachedIntValue(self, device_uri, key):
        try:
            ret = devices[device_uri].cache[key]
        except KeyError:
            ret = -1

        log.debug("GetCachedIntValue('%s', '%s') --> %d" % (device_uri, key, ret))
        return ret


    @dbus.service.method('com.hplip.StatusService', in_signature='sss', out_signature='s')
    def SetCachedStrValue(self, device_uri, key, value):
        log.debug("SetCachedStrValue('%s', '%s', '%s')" % (device_uri, key, value))
        if check_device(device_uri) == ERROR_SUCCESS: 
            devices[device_uri].cache[key] = value
            return value

        return ''


    @dbus.service.method('com.hplip.StatusService', in_signature='ss', out_signature='s')
    def GetCachedStrValue(self, device_uri, key):
        try:
            ret = devices[device_uri].cache[key]
        except KeyError:
            ret = ''

        log.debug("GetCachedStrValue('%s', '%s') --> %s" % (device_uri, key, ret))
        return ret


    # Pass a non-zero job_id to retrieve a specific fax
    # Pass zero for job_id to retrieve any avail. fax
    @dbus.service.method('com.hplip.StatusService', in_signature='ssi', out_signature='ssisisds')
    def CheckForWaitingFax(self, device_uri, username, job_id=0):
        device_uri = device_uri.replace('hp:', 'hpfax:')
        log.debug("CheckForWaitingFax('%s', '%s', %d)" % (device_uri, username, job_id))
        r = (device_uri, '', 0, username, job_id, '', 0.0, '')
        try:
            devices[device_uri]
        except KeyError:
            log.warn("Unknown device URI: %s" % device_uri)
            return r
        else:
            show_waiting_faxes(device_uri)

            if job_id: # check for specific job_id
                try:
                    devices[device_uri].faxes[(username, job_id)]
                except KeyError:
                    return r
                else:
                    return self.check_for_waiting_fax_return(device_uri, username, job_id)

            else: # return any matching one from cache. call mult. times to get all.
                for u, j in devices[device_uri].faxes.keys():
                    if u == username:
                        return self.check_for_waiting_fax_return(device_uri, u, j)

                return r


    # if CheckForWaitingFax returns a fax job, that job is removed from the cache
    def check_for_waiting_fax_return(self, d, u, j):
        log.debug("Fax (username=%s, jobid=%d) removed from faxes and returned to caller." % (u, j))
        r = devices[d].faxes[(u, j)].as_tuple()
        del devices[d].faxes[(u, j)]
        show_waiting_faxes(d)
        return r 


    # Alternate way to "send" an event rather than using a signal message
    @dbus.service.method('com.hplip.StatusService', in_signature='ssisis', out_signature='')
    def SendEvent(self, device_uri, printer_name, event_code, username, job_id, title):
        event = ServiceEvent(device_uri, printer_name, event_code, username, job_id, title)
        handle_event(event)



def check_device(device_uri):
    try:
        devices[device_uri]
    except KeyError:
        log.debug("New device: %s" % device_uri)
        try:
            back_end, is_hp, bus, model, serial, dev_file, host, port = \
                device.parseDeviceURI(device_uri)
        except Error:
            log.error("Invalid device URI")
            return ERROR_INVALID_DEVICE_URI

        devices[device_uri] = DeviceCache(model)

    return ERROR_SUCCESS   


def create_history(event):
    history = devices[event.device_uri].history.get()

    if history and history[-1].event_code == event.event_code:
        log.debug("Duplicate event. Replacing previous event.")
        devices[event.device_uri].history.replace(event)
        return True
    else:
        devices[event.device_uri].history.append(event)
        return False


def handle_fax_event(event, pipe_name):
    if event.event_code == EVENT_FAX_RENDER_COMPLETE and \
        event.username == prop.username:
        
        fax_file_fd, fax_file_name = tempfile.mkstemp(prefix="hpfax-")
        pipe = os.open(pipe_name, os.O_RDONLY) 
        bytes_read = 0
        while True:
            data = os.read(pipe, PIPE_BUF)
            if not data: 
                break

            os.write(fax_file_fd, data)
            bytes_read += len(data)

        log.debug("Saved %d bytes to file %s" % (bytes_read, fax_file_name))

        os.close(pipe)
        os.close(fax_file_fd)

        devices[event.device_uri].faxes[(event.username, event.job_id)] = \
            FaxEvent(fax_file_name, event)

        show_waiting_faxes(event.device_uri)

    else:
        log.warn("Not handled!")
        pass


def show_waiting_faxes(d):
    f = devices[d].faxes
    if not len(f):
        log.debug("No faxes waiting for %s" % d)
    else:
        if len(f) == 1:
            log.debug("1 fax waiting for %s:" % d)
        else:
            log.debug("%d faxes waiting for %s:" % (len(f), d))

        [f[x].debug() for x in f]


def handle_event(event, more_args=None):
    log.debug("Handling event...")
    if more_args is None: 
        more_args = []

    event.debug()

    if event.device_uri and check_device(event.device_uri) != ERROR_SUCCESS:
        return

    # If event-code > 10001, its a PJL error code, so convert it
    if event.event_code > EVENT_MAX_EVENT:
        event.event_code = status.MapPJLErrorCode(event.event_code)
        #event_code = event.event_code

    # regular user/device status event
    if EVENT_MIN_USER_EVENT <= event.event_code <= EVENT_MAX_USER_EVENT:
        if event.device_uri:
            dup_event = create_history(event)

        # Send to system tray icon if available
        if not dup_event and event.event_code != STATUS_PRINTER_IDLE:
            if w is not None:
                log.debug("Sending event to system tray icon UI...")
                try:
                    os.write(w, event.pack())
                except OSError:
                    log.debug("Failed.")

    # Handle fax signals
    elif EVENT_FAX_MIN <= event.event_code <= EVENT_FAX_MAX and more_args:
        log.debug("Fax event")
        pipe_name = str(more_args[0])
        handle_fax_event(event, pipe_name)



def handle_signal(typ, *args, **kwds):
    if kwds['interface'] == 'com.hplip.Service' and \
        kwds['member'] == 'Event':

        event = ServiceEvent(*args[:6])
        return handle_event(event, args[6:])


def handle_system_signal(*args,**kwds):
    return handle_signal('system', *args, **kwds)


def handle_session_signal(*args, **kwds):
    return handle_signal('session', *args, **kwds)


# Entry point for hp-systray
def run(write_pipe=None, parent_pid=0):
    global dbus_loop
    global system_bus
    global session_bus
    global w

    log.set_module("hp-systray(hpssd)")
    w = write_pipe

    dbus_loop = DBusGMainLoop(set_as_default=True)
    system_bus = SystemBus(mainloop=dbus_loop)
    
    try:
        session_bus = dbus.SessionBus()
    except dbus.exceptions.DBusException, e:
        if os.getuid() != 0:
            log.error("Unable to connect to dbus session bus. Exiting.")
            sys.exit(1)
        else:
            log.error("Unable to connect to dbus session bus (running as root?)")            
            sys.exit(1)    
    
    #session_bus = SessionBus(mainloop=dbus_loop)

    # Receive events from the system bus
    system_bus.add_signal_receiver(handle_system_signal, sender_keyword='sender',
        destination_keyword='dest', interface_keyword='interface',
        member_keyword='member', path_keyword='path')

    # Receive events from the session bus
    session_bus.add_signal_receiver(handle_session_signal, sender_keyword='sender',
        destination_keyword='dest', interface_keyword='interface',
        member_keyword='member', path_keyword='path')

    # Export an object on the session bus
    session_name = dbus.service.BusName("com.hplip.StatusService", session_bus)
    status_service = StatusService(session_name, "/com/hplip/StatusService")

    log.debug("Entering main loop...")
    try:
        MainLoop().run()
    except KeyboardInterrupt:
        log.debug("Ctrl-C: Exiting...")



if __name__ == '__main__':
    log.set_module('hpssd')

    try:
        opts, args = getopt.getopt(sys.argv[1:], 'l:hg', 
            ['level=', 'help', 'help-man', 'help-rest', 'help-desc'])

    except getopt.GetoptError, e:
        log.error(e.msg)
        usage()

    if os.getenv("HPLIP_DEBUG"):
        log.set_level('debug')

    for o, a in opts:
        if o in ('-l', '--logging'):
            log_level = a.lower().strip()
            if not log.set_level(log_level):
                usage()

        elif o == '-g':
            log.set_level('debug')

        elif o in ('-h', '--help'):
            usage()

        elif o == '--help-rest':
            usage('rest')

        elif o == '--help-man':
            usage('man')

        elif o == '--help-desc':
            print __doc__,
            sys.exit(0)


    utils.log_title(__title__, __version__)    
    sys.exit(run())
