#!/usr/bin/env python
# vim: set fileencoding=utf-8
#
#   sinod - Simple Notifications Daemon
#
#   Based on statnot by Henrik Hallberg (halhen@k2h.se) http://code.k2h.se
#
#   Lightweight notification-deamon intended to be used with scripts in
#   lightweight WMs, like dwm or with non-x environments.
#
#   Receives Desktop Notifications (including libnotify / notify-send)
#   See: http://www.galago-project.org/specs/notification/0.9/index.html
#
#   Note: VERY early prototype, to get feedback.
#
#   Copyright (c) 2009 by Henrik Hallberg (halhen@k2h.se)
#   Copyright (c) 2009 by Paweł Zuzelski (pawelz@pld-linux.org)
#
#   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 3 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, see <http://www.gnu.org/licenses/>.
#

import atexit
import dbus
import dbus.service
import dbus.mainloop.glib
import getopt
import gobject
import sys
import thread
import time

# ===== CONFIGURATION =====

# Default time a notification is show, unless specified in notification
DEFAULT_NOTIFY_TIMEOUT = 3000 # milliseconds

# Maximum time a notification is allowed to show
MAX_NOTIFY_TIMEOUT = 5000 # milliseconds

# Maximum number of characters in a notification. 
NOTIFICATION_MAX_LENGTH = 100 # number of characters

# ===== CONFIGURATION END =====

# ===== SOME USEFUL CONSTANTS =====

VERMAJOR = "0"
VERMINOR = "1"
CURVERSION = VERMAJOR + "." + VERMINOR

try:
    CURVERSION = CURVERSION + "." + VERPATCH
except NameError:
    pass

def version():
    print "sinod " + CURVERSION

def usage():
    print
    version()
    print
    print "Usage: sinod [-V] [-h] [-d DRIVER]"
    print
    print "    -V     display version info and exit"
    print "    -h     display this help and exit"
    print "    -d     use DRIVER as display driver. Default is \"simple\""
    print
    print "Display drivers:"
    print "    simple    - prints each notification in a new line"
    print "    fancy     - split summary and body"
    print "    xml       - XML output"
    print

# ===== SOME USEFUL CONSTANTS END =====

# ===== DISPLAY DRIVERS =====

def display_simple(notification):
    text = notification["summary"] + " " + notification["body"]
    try:
        if len(text):
            print text.decode('UTF-8')
    except UnicodeEncodeError as e:
        print "SINOD ERROR: " + str(e)

def display_fancy(notification):
    print notification["summary"].decode('UTF-8')
    print notification["body"].decode('UTF-8')
    print

def display_xml(notification):
    global first_line

    try:
        first_line
    except NameError:
        print "<?xml version='1.0' encoding='UTF-8'?>"
	print "<notifications>"
	atexit.register(display_xml, False)
	first_line = 1

    if notification:
        print "  <notification id="+str(notification["id"])+">"
        print "    <summary>"+notification["summary"].decode('UTF-8')+"</summary>"
        print "    <body>"+notification["body"].decode('UTF-8')+"</body>"
        print "  </notification>"
    else:
        print "</notifications>"

display_driver = display_simple

driver = {"simple": display_simple,
          "fancy": display_fancy,
	  "xml": display_xml}

# ===== DISPLAY DRIVERS END =====

# List of not shown notifications.
# Array of arrays: [id, text, timeout in s]
# 0th element is being displayed right now, and may change
# Replacements of notification happens att add
# message_thread only checks first element for changes
notification_queue = []
notification_queue_lock = thread.allocate_lock()

def add_notification(notif):
    with notification_queue_lock:
        for index, n in enumerate(notification_queue):
            if n["id"] == notif["id"]: # same id, replace instead of queue
                n = notif
                return

        notification_queue.append(notif)

def next_notification():
    # No need to be thread safe here. Also most common scenario
    if not notification_queue:
        return None

    with notification_queue_lock:
        # If there are several pending messages, discard the first 0-timeouts
        while len(notification_queue) > 1 and notification_queue[0]["timeout"] == 0:
            notification_queue.pop(0)

        return notification_queue.pop(0)

def message_thread(dummy):
    last_notification = 0
    last_notification_update = 0
    current_notification = {}
    
    while 1:
        notif = next_notification()
        current_time = time.time()
        do_notify = False

        if notif:
            if notif != current_notification:
                do_notify = True

        if do_notify:
            if notif:
                current_notification = notif
            else:
                current_notification = {}
                
	    display_driver(current_notification)
            last_notification = current_time
                
        time.sleep(1)		

class NotificationFetcher(dbus.service.Object):
    _id = 0

    @dbus.service.method("org.freedesktop.Notifications",
                         in_signature='susssasa{ss}i',
                         out_signature='u')
    def Notify(self, app_name, notification_id, app_icon,
               summary, body, actions, hints, expire_timeout):
        if (expire_timeout < 0) or (expire_timeout > MAX_NOTIFY_TIMEOUT):
            expire_timeout = DEFAULT_NOTIFY_TIMEOUT

        if not notification_id:
            self._id += 1
            notification_id = self._id
			
	add_notification( {"id": notification_id,
                           "summary": summary,
                           "body": body,
                           "timeout": int(expire_timeout) / 1000.0} )

        return notification_id
		
    @dbus.service.method("org.freedesktop.Notifications", in_signature='', out_signature='as')
    def GetCapabilities(self):
        return ("body")
	
    @dbus.service.signal('org.freedesktop.Notifications', signature='uu')
    def NotificationClosed(self, id_in, reason_in):
        pass

    @dbus.service.method("org.freedesktop.Notifications", in_signature='u', out_signature='')
    def CloseNotification(self, id):
        pass

if __name__ == '__main__':
    try:
        opts, args = getopt.getopt(sys.argv[1:], "hVd:")
    except getopt.GetoptError, err:
        print str(err)
	usage()
	sys.exit(2)

    for o, a in opts:
        if o == "-h":
	    usage()
	    sys.exit()
	elif o == "-V":
	    version()
	    sys.exit()
	elif o == "-d":
	    try:
	        display_driver = driver[a]
            except KeyError:
	        print "ERROR: Unknown display driver."
		sys.exit(1)

    dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
    session_bus = dbus.SessionBus()
    name = dbus.service.BusName("org.freedesktop.Notifications", session_bus)
    nf = NotificationFetcher(session_bus, '/org/freedesktop/Notifications')

    # We must use contexts and iterations to run threads
    # http://www.jejik.com/articles/2007/01/python-gstreamer_threading_and_the_main_loop/
    gobject.threads_init()
    context = gobject.MainLoop().get_context()
    thread.start_new_thread(message_thread, (None,))
    
    try:
        while 1:
            context.iteration(True)
    except:
        exit(0)

