#!/usr/local/bin/python
# -*- coding: utf-8 -*-
#
# (c) Copyright 2003-2009 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__ = '12.0'
__title__ = "Services and Status System Tray dBus Child/Parent Process"
__mod__ = 'hpssd'
__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
#import threading
#import Queue
from cPickle import loads, HIGHEST_PROTOCOL

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

# dBus
try:
    from dbus import lowlevel, SystemBus, SessionBus
    import dbus.service
    from dbus.mainloop.glib import DBusGMainLoop
    from gobject import MainLoop, timeout_add, threads_init, io_add_watch, IO_IN
    dbus_loaded = True
except ImportError:
    log.error("dbus failed to load (python-dbus ver. 0.80+ required). Exiting...")
    dbus_loaded = False
    sys.exit(1)

import warnings
# Ignore: .../dbus/connection.py:242: DeprecationWarning: object.__init__() takes no parameters
# (occurring on Python 2.6/dBus 0.83/Ubuntu 9.04)
warnings.simplefilter("ignore", DeprecationWarning)


# Globals
PIPE_BUF = 4096
dbus_loop, main_loop = None, None
system_bus = None
session_bus = None
w1, w2, r3 = None, None, None
devices = {} # { 'device_uri' : DeviceCache, ... }


# ***********************************************************************************
#
# DEVICE CACHE
#
# ***********************************************************************************

class DeviceCache(object):
    def __init__(self, model=''):
        self.history = utils.RingBuffer(prop.history_size) # circular buffer of device.Event
        self.model = models.normalizeModelName(model)
        self.cache = {} # variable name : value
        self.faxes = {} # (username, jobid): FaxEvent
        self.dq = {} # last device query results
        #self.backoff = False
        self.backoff_counter = 0  # polling backoff: 0 = none, x = backed off by x intervals
        self.backoff_countdown = 0
        self.polling = False # indicates whether its in the device polling list


#  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='sa(ssisisd)')
    def GetHistory(self, device_uri):
        log.debug("GetHistory('%s')" % device_uri)
        send_systray_blip()
        try:
            devices[device_uri]
        except KeyError:
            #log.warn("Unknown device URI: %s" % device_uri)
            return (device_uri, [])
        else:
            h = devices[device_uri].history.get()
            log.debug("%d events in history:" % len(h))
            [x.debug() for x in h]
            return (device_uri, [x.as_tuple() for x in h])


    @dbus.service.method('com.hplip.StatusService', in_signature='s', out_signature='sa{ss}')
    def GetStatus(self, device_uri):
        log.debug("GetStatus('%s')" % device_uri)
        send_systray_blip()
        try:
            devices[device_uri]
        except KeyError:
            #log.warn("Unknown device URI: %s" % device_uri)
            return (device_uri, {})
        else:
            t = {}
            dq = devices[device_uri].dq
            [t.setdefault(x, str(dq[x])) for x in dq.keys()]
            log.debug(t)
            return (device_uri, t)


    @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):
        log.debug("CheckForWaitingFax('%s', '%s', %d)" % (device_uri, username, job_id))
        send_systray_blip()
        r = (device_uri, '', 0, username, job_id, '', 0.0, '')
        check_device(device_uri)
        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 = device.Event(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, zc, port = \
                device.parseDeviceURI(device_uri)
        except Error:
            log.debug("Invalid device URI: %s" % 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)] = \
            device.FaxEvent(fax_file_name, event)

        show_waiting_faxes(event.device_uri)

        try:
            os.waitpid(-1, os.WNOHANG)
        except OSError:
            pass

        # See if hp-sendfax is already running for this queue
        ok, lock_file = utils.lock_app('hp-sendfax-%s' % event.printer_name, True)

        if ok:
            # able to lock, not running...
            utils.unlock(lock_file)

            path = utils.which('hp-sendfax')
            if path:
                path = os.path.join(path, 'hp-sendfax')
            else:
                log.error("Unable to find hp-sendfax on PATH.")
                return

            log.debug("Running hp-sendfax: %s --printer=%s" % (path, event.printer_name))
            os.spawnlp(os.P_NOWAIT, path, 'hp-sendfax',
                '--printer=%s' % event.printer_name)

        else:
            # cannot lock file - hp-sendfax is running
            # no need to do anything... hp-sendfax is polling
            log.debug("hp-sendfax is running. Waiting for CheckForWaitingFax() call.")

    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]


# Qt4 only
def handle_hpdio_event(event, bytes_written):
    log.debug("Reading %d bytes from hpdio pipe..." % bytes_written)
    total_read, data = 0, ''

    while True:
        r, w, e = select.select([r3], [], [r3], 0.0)
        if not r: break

        x = os.read(r3, PIPE_BUF)
        if not x: break

        data = ''.join([data, x])
        total_read += len(x)

        if total_read == bytes_written: break

    log.debug("Read %d bytes" % total_read)

    if total_read == bytes_written:
        dq = loads(data)

        if check_device(event.device_uri) == ERROR_SUCCESS:
            devices[event.device_uri].dq = dq.copy()

            handle_event(device.Event(event.device_uri, '',
                dq.get('status-code', STATUS_PRINTER_IDLE), prop.username, 0, ''))

            send_toolbox_event(event, EVENT_DEVICE_UPDATE_REPLY)



def handle_event(event, more_args=None):
    #global polling_blocked
    #global request_queue

    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)

    # regular user/device status event
    if event.event_code < EVENT_MIN_USER_EVENT:
        pass

    elif EVENT_MIN_USER_EVENT <= event.event_code <= EVENT_MAX_USER_EVENT:

        if event.device_uri:
            #event.device_uri = event.device_uri.replace('hpfax:', 'hp:')
            dup_event = create_history(event)

            if event.event_code in (EVENT_DEVICE_STOP_POLLING,
                                    EVENT_START_MAINT_JOB,
                                    EVENT_START_COPY_JOB,
                                    EVENT_START_FAX_JOB,
                                    EVENT_START_PRINT_JOB):
                pass # stop polling (increment counter)

            elif event.event_code in (EVENT_DEVICE_START_POLLING, # should this event force counter to 0?
                                      EVENT_END_MAINT_JOB,
                                      EVENT_END_COPY_JOB,
                                      EVENT_END_FAX_JOB,
                                      EVENT_END_PRINT_JOB,
                                      EVENT_PRINT_FAILED_MISSING_PLUGIN,
                                      EVENT_SCANNER_FAIL,
                                      EVENT_END_SCAN_JOB,
                                      EVENT_SCAN_FAILED_MISSING_PLUGIN,
                                      EVENT_FAX_JOB_FAIL,
                                      EVENT_FAX_JOB_CANCELED,
                                      EVENT_FAX_FAILED_MISSING_PLUGIN,
                                      EVENT_COPY_JOB_FAIL,
                                      EVENT_COPY_JOB_CANCELED):
                pass # start polling if counter <= 0
                # TODO: Do tools send END event if canceled or failed? Should they?
                # TODO: What to do if counter doesn't hit 0 after a period? Timeout?
                # TODO: Also, need to deal with the backoff setting (or it completely sep?)

        # Send to system tray icon if available
        if not dup_event: # and event.event_code != STATUS_PRINTER_IDLE:
            send_event_to_systray_ui(event)

        # send EVENT_HISTORY_UPDATE signal to hp-toolbox
        send_toolbox_event(event, EVENT_HISTORY_UPDATE)

    # 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)

    elif event.event_code == EVENT_USER_CONFIGURATION_CHANGED:
        # Sent if polling, hiding, etc. configuration has changed
    #    send_event_to_hpdio(event)
        send_event_to_systray_ui(event)

    elif event.event_code == EVENT_SYS_CONFIGURATION_CHANGED: # Not implemented
        #send_event_to_hpdio(event)
        send_event_to_systray_ui(event)

    # Qt4 only
    elif event.event_code in (EVENT_DEVICE_UPDATE_REQUESTED,):
                              #EVENT_DEVICE_START_POLLING,  # ?  Who handles polling? hpssd? probably...
                              #EVENT_DEVICE_STOP_POLLING):  # ?
        send_event_to_hpdio(event)

    # Qt4 only
    elif event.event_code in (EVENT_DEVICE_UPDATE_ACTIVE,
                              EVENT_DEVICE_UPDATE_INACTIVE):
        send_event_to_systray_ui(event)

    # Qt4 only
    elif event.event_code == EVENT_DEVICE_UPDATE_REPLY:
        bytes_written = int(more_args[1])
        handle_hpdio_event(event, bytes_written)

    # Qt4 only
    elif event.event_code == EVENT_SYSTEMTRAY_EXIT:
        send_event_to_hpdio(event)
        send_toolbox_event(event)
        send_event_to_systray_ui(event)
        log.debug("Exiting")
        main_loop.quit()

    elif event.event_code in (EVENT_DEVICE_START_POLLING,
                              EVENT_DEVICE_STOP_POLLING):
        pass

    else:
        log.error("Unhandled event: %d" % event.event_code)



def send_systray_blip():
    send_event_to_systray_ui(device.Event('', '', EVENT_DEVICE_UPDATE_BLIP))


def send_event_to_systray_ui(event, event_code=None):
    e = event.copy()

    if event_code is not None:
        e.event_code = event_code

    e.send_via_pipe(w1, 'systemtray')


def send_event_to_hpdio(event):
    event.send_via_pipe(w2, 'hpdio')


def send_toolbox_event(event, event_code=None):
    global session_bus

    e = event.copy()

    if event_code is not None:
        e.event_code = event_code

    e.send_via_dbus(session_bus, 'com.hplip.Toolbox')



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

        event = device.Event(*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)



def run(write_pipe1=None,  # write pipe to systemtray
        write_pipe2=None,  # write pipe to hpdio
        read_pipe3=None):  # read pipe from hpdio

    global dbus_loop, main_loop
    global system_bus, session_bus
    global w1, w2, r3

    log.set_module("hp-systray(hpssd)")
    log.debug("PID=%d" % os.getpid())
    w1, w2, r3 = write_pipe1, write_pipe2, read_pipe3

    dbus_loop = DBusGMainLoop(set_as_default=True)
    main_loop = MainLoop()

    try:
        system_bus = SystemBus(mainloop=dbus_loop)
    except dbus.exceptions.DBusException, e:
        log.error("Unable to connect to dbus system bus. Exiting.")
        sys.exit(1)

    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)

    # 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 dbus loop...")
    try:
        main_loop.run()
    except KeyboardInterrupt:
        log.debug("Ctrl-C: Exiting...")

