from __future__ import division
import sys
import gobject
import gtk
import gtk.gdk as gdk
import pango
import bonobo
import gosd
import gconfsync
import gc
import os.path
import gnome
import gnome_osd_conf
import warnings
import xml.dom.minidom as dom
import gtasklet
import traceback

try:
    import dbus
    import dbus.service
    import dbus.glib
except ImportError:
    HAVE_DBUS = False
else:
    HAVE_DBUS = True


bgcolor = "#000000"
fgcolor = "#80ff80"
gosd.BORDER_WIDTH = 1
ANIMATION = True
ANIMATION_STEPS = 10
ANIMATION_TIME = 200

gnome.program_init("gnome-osd-server", gnome_osd_conf.VERSION)
prefs = gconfsync.GConfSync("/apps/gnome-osd",
                            dict(osd_halignment=dict(left=pango.ALIGN_LEFT,
                                                     right=pango.ALIGN_RIGHT,
                                                     center=pango.ALIGN_CENTER)))

# Note: interning atoms avoids X server roundtrips
_NET_CURRENT_DESKTOP = gtk.gdk.atom_intern("_NET_CURRENT_DESKTOP")
_NET_WORKAREA = gtk.gdk.atom_intern("_NET_WORKAREA")
def get_current_workarea():
    root = gtk.gdk.screen_get_default().get_root_window()
    prop = root.property_get(_NET_CURRENT_DESKTOP)
    if prop is None:
        return None
    propname, proptype, propvalue = prop
    current = propvalue[0]
    prop = root.property_get(_NET_WORKAREA)
    if prop is None:
        return None
    propname, proptype, propvalue = prop
    return propvalue[current*4 : (current + 1)*4]

def _map_ellipsize_val(val):
    return {
        'none': pango.ELLIPSIZE_NONE,
        'start': pango.ELLIPSIZE_START,
        'middle': pango.ELLIPSIZE_MIDDLE,
        'end': pango.ELLIPSIZE_END,
        } [val.lower()]

    
class OSDMessage(object):

    class Message(object):
        __slots__ = ('text', 'timeout')

    class Animation(gtasklet.Tasklet):
        def __init__(self, win, x0, y0, xf, yf):
            self.x = float(x0)
            self.y = float(y0)
            self.dx = (xf - x0) / ANIMATION_STEPS
            self.dy = (yf - y0) / ANIMATION_STEPS
            self.win = win
            self.counter = ANIMATION_STEPS
            self.timeout = gtasklet.WaitForTimeout(ANIMATION_TIME//ANIMATION_STEPS)
            win.move(int(x0), int(y0))
            win.show()
            gtasklet.Tasklet.__init__(self)

        def run(self):
            counter = ANIMATION_STEPS
            while counter:
                counter -= 1

                yield self.timeout
                gtasklet.get_event()

                self.x += self.dx
                self.y += self.dy
                self.win.move(int(self.x), int(self.y))

    
    def __init__(self, id, inherit=None):
        self.hide_timeout = None
        self.message_queue = []
        self.__show_lock = False
        self.animation_vector = None
        self.inherit = inherit
        self.props = {}
        self.id = id
        self.occupied_area = None
        self.__show_task = None

    def getprop(self, name):
        try:
            return self.props[name]
        except KeyError:
            if self.inherit is None:
                return prefs[name]
            else:
                return self.inherit.getprop(name)

    def show(self, text, timeout=None):
        msg = self.Message()
        msg.text = text
        if timeout is None:
            msg.timeout = self.getprop('hide_timeout')
        else:
            msg.timeout = timeout
        self.message_queue.append(msg)
        if self.__show_task is None:
            self.__show_task = gtasklet.Tasklet(self._show_messages())
        else:
            ## speed up hiding existing message
            self.__show_task.send_message(gtasklet.Message("continue"))

    def _show_messages(self):
        while self.message_queue:
            msg = self.message_queue.pop(0)
            halign = self.getprop('osd_halignment')
            vpos = self.getprop('osd_vposition')

            if self.getprop('avoid_panels'):
                workarea = get_current_workarea()
                if workarea is not None:
                    desk_x, desk_y, desk_w, desk_h = workarea
                else:
                    warnings.warn("get_current_workarea failed, somehow")
                    desk_x, desk_y = 0, 0
                    desk_w = gdk.screen_width()
                    desk_h = gdk.screen_height()
            else:
                desk_x, desk_y = 0, 0
                desk_w = gdk.screen_width()
                desk_h = gdk.screen_height()

            w = gosd.osd(msg.text, bgcolor, fgcolor,
                         pango.FontDescription(self.getprop('osd_font')),
                         use_markup=True,
                         alignment=halign,
                         fake_translucent_bg=self.getprop("osd_fake_translucent_bg"),
                         drop_shadow=self.getprop("drop_shadow"),
                         max_width=desk_w,
                         debug_frame=self.getprop('debug_frame'),
                         ellipsize=_map_ellipsize_val(self.getprop('ellipsize')))
            if self.getprop('enable_sound'):
                gnome.sound_play(self.__get_sound_sample_file())

            y, x, y_final = self.reserve_display_area(vpos, halign, w,
                                                      desk_x, desk_y, desk_w, desk_h)
            if self.getprop('animations'):
                anim = self.Animation(w, x, y, x, y_final)
                yield gtasklet.WaitForTasklet(anim), gtasklet.WaitForMessages(defer='continue')
                gtasklet.get_event()
            else:
                w.move(x, y_final)
                w.show()

            wait_list = [gtasklet.WaitForMessages(accept='continue')]
            
            if not self.message_queue:
                ## if there are messages waiting, don't pause
                wait_list.append(gtasklet.WaitForTimeout(msg.timeout))
            if self.getprop('hide_on_hover'):
                wait_list.append(gtasklet.WaitForSignal(w, "enter-notify-event"))

            yield wait_list ## during this yield the message stays on screen, very still
            event = gtasklet.get_event()

            ## Now hide it
            if self.getprop('animations'):
                anim = self.Animation(w, x, y_final, x, y)
                yield gtasklet.WaitForTasklet(anim), gtasklet.WaitForMessages(defer='continue')
                gtasklet.get_event()

            w.destroy()
            self.occupied_area = None
            while gc.collect():
                pass

        self.__show_task = None
        return
        
    def __get_sound_sample_file(self):
        sound_sample = self.getprop('sound_sample')
        if sound_sample[0] == '/':
            return sound_sample
        else:
            return os.path.join(gnome_osd_conf.datadir, "sounds", sound_sample)

    instances = {}

    def get(cls, attrs={}):
        global prefs
        id_ = attrs.get('id', 'default')
        inherit = attrs.get('inherit', 'default')
        try:
            osd_msg = cls.instances[id_]
        except KeyError:
            if inherit == 'default':
                inherit_osd = None
            else:
                inherit_osd = cls.instances[inherit]
            osd_msg = OSDMessage(id=id_, inherit=inherit_osd)
            cls.instances[id_] = osd_msg
        for key, str_val in attrs.iteritems():
            if key in ('id', 'inherit'): # special properties, no gconf schema
                continue
            if id_ == 'default':
                raise ValueError, "changing message properties"\
                      " for 'default' instance not allowed"
            val = prefs.parse_string(key, str_val)
            osd_msg.props[key] = val
        return osd_msg
    get = classmethod(get)

    def reserve_display_area(self, vpos, halign, w, desk_x, desk_y, desk_w, desk_h):
        if halign == pango.ALIGN_LEFT:
            x = 2 + desk_x
        elif halign == pango.ALIGN_RIGHT:
            x = desk_x + desk_w - w.width - 2
        else: # center
            x = desk_x + desk_w/2 - w.width/2

        occupied_areas = [inst.occupied_area for inst in self.instances.itervalues()
                          if not (inst is self or inst.occupied_area is None)]

        def intersection(area, x, y):
            x1, y1 = x, y
            x2, y2 = x + w.width, y + w.height
            ox1, oy1, ox2, oy2 = area
            result = ((x1 >= ox1 and x1 <= ox2
                       or x2 <= ox2 and x2 >= ox1
                       or x1 <= ox1 and x2 >= ox2)
                      and
                      (y1 >= oy1 and y1 <= oy2
                       or y2 <= oy2 and y2 >= oy1
                       or y1 <= oy1 and y2 >= oy2))
            return result
            
        if vpos == 'top':
            y = -w.height
            def sort_func(a, b):
                return cmp(a[3], b[3])
            occupied_areas.sort(sort_func)
            y_final = desk_y + 2
            for area in occupied_areas:
                if intersection(area, x, y_final):
                    y_final = area[3] + 2
                else:
                    break
        elif vpos == 'center':
            y = -w.height
            y_ideal = desk_y + desk_h//2  - w.height//2
            def sort_func(a, b):
                return cmp(abs(y_ideal - a), abs(y_ideal - b))
            y_final = y_ideal
            possible_targets = [y_ideal]
            for area in occupied_areas:
                possible_targets.append(area[3] + 2)
                possible_targets.append(area[1] - 2 - w.height)
            possible_targets.sort(sort_func)
            for target in possible_targets:
                for area in occupied_areas:
                    if intersection(area, x, target):
                        break
                else:
                    y_final = target
                    break
            else:
                y_final = None
            assert y_final is not None
        elif vpos == 'bottom':
            y = gdk.screen_height()
            y_final = desk_y + desk_h - w.height - 2
            def sort_func(a, b):
                return cmp(b[1], a[1])
            occupied_areas.sort(sort_func)
            for area in occupied_areas:
                if intersection(area, x, y_final):
                    y_final = area[1] - 2 - w.height
                else:
                    break
        else:
            raise ValueError, "invalid vposition '%s'" % vpos
        self.occupied_area = (x, y_final, x + w.width, y_final + w.height)
        return y, x, y_final
        
    
def message_quit_cb(app_client, message, app):
    print "received quit message"
    app.unref()
    bonobo.main_quit()

def message_show_cb(_, message, timeout=-1):
    try:
        attrs = {'id': 'default'}
        if timeout == -1:
            timeout = None
        osd_msg = OSDMessage.get(attrs)
        osd_msg.show(message, timeout)
        return ''
    except Exception, ex:
        exc_info = sys.exc_info()
        try:
            error = "".join(traceback.format_exception(*exc_info))
        finally:
            del exc_info
        print >> sys.stderr, error
        return error

def message_parse(message):
    '''message_parse(msg) -> (attrs, pango_markup)'''
    doc = dom.parseString(message.decode("utf-8"))
    root = doc.documentElement
    if root.tagName != "message":
        raise RuntimeError("Protocol error: expecting <message>, got <%s>" % root.tagName)
    pango_markup = "".join(map(dom.Node.toxml, root.childNodes))
    attrs = {}
    for i in xrange(root.attributes.length):
        name = root.attributes.item(i).name
        attrs[name] = root.getAttribute(name)
    return attrs, pango_markup

def message_show_full_cb(_, message):
    try:
        attrs, pango_s = message_parse(message)
        osd_msg = OSDMessage.get(attrs)
        osd_msg.show(pango_s)
        return ''
    except Exception, ex:
        print >> sys.stderr, ex
        return "%s: %s" % (str(ex.__class__), str(ex))

(ACTIVATION_REG_SUCCESS, 
 ACTIVATION_REG_NOT_LISTED, 
 ACTIVATION_REG_ALREADY_ACTIVE, 
 ACTIVATION_REG_ERROR) = range(4)


def start_bonobo_interface():
    os.putenv("DISPLAY", gtk.gdk.get_display())
    bonobo.activate()
    app = bonobo.Application("GNOME_OSD")
    def last_unref_cb(*args):
        print "Last Unref, quitting"
        bonobo.main_quit()
    reg = bonobo.activation.active_server_register("OAFIID:GNOME_OSD", app.corba_objref())
    if reg != ACTIVATION_REG_SUCCESS:
        raise SystemExit(3)
    app.connect("message::quit", message_quit_cb, app)

    app.register_message("show",
                         "Show a message in pango markup using default options",
                         str, (str, int),
                         message_show_cb)

    app.register_message("show-full",
                         "Show a message in Gnome OSD protocol format"
                         " (XML <message ...> ... </message>)",
                         str, (str,),
                         message_show_full_cb)

    bonobo.context_running_get().connect("last-unref", last_unref_cb)

if HAVE_DBUS:
    class DBusInterface(dbus.service.Object):
        def __init__(self, bus_name, object_path="/Server"):
            dbus.service.Object.__init__(self, bus_name, object_path)

        #@dbus.service.method("pt.inescporto.telecom.GnomeOSD")
        def showMessage(self, message, timeout=-1):
            return message_show_cb(None, message, timeout)
        showMessage = dbus.service.method("pt.inescporto.telecom.GnomeOSD")(showMessage)

        #@dbus.service.method("pt.inescporto.telecom.GnomeOSD")
        def showMessageFull(self, message):
            return message_show_full_cb(None, message)
        showMessageFull = dbus.service.method("pt.inescporto.telecom.GnomeOSD")(showMessageFull)

    def start_dbus_interface():
        session_bus = dbus.SessionBus()
        name = dbus.service.BusName("pt.inescporto.telecom.GnomeOSD",
                                    bus=session_bus)
        obj = DBusInterface(name)
    

def main():
    print "starting Bonobo interface..."
    start_bonobo_interface()
    if HAVE_DBUS:
        print "starting DBUS interface..."
        start_dbus_interface()
    print "entering main loop..."
    bonobo.main()
