#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright (C) 2011-2012 Canonical, Ltd.
#
# Authors:
#  Ugo Riboni <ugo.riboni@canonical.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; version 3.
#
# 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/>.

"""
Expose to DBUS a simplified API to display notifications bubbles and
interact with the messaging menu (add shortcuts, indicators, etc).
It also allows interaction with the snap decisions framework. Since
the snap decisions spec hasn't been finalized and no implementation
exists yet, we're using a temporary replacement via zenity dialogs.
"""

from gi.repository import GObject, GLib, Gio, Indicate, Dbusmenu
from gi._gi import variant_type_from_string

from dbus.mainloop.glib import DBusGMainLoop
DBusGMainLoop(set_as_default=True)
import dbus.service

from dbus.exceptions import DBusException

import subprocess
import threading
import sys
import os

from ufa_zenity_dialogs import QuestionDialog

# This should be bumped if/when changes are made to this DBus API!
DBUS_API_VERSION = 1

DBUS_PROPERTIES_INTERFACE = "org.freedesktop.DBus.Properties"
DEFAULT_BUS_NAME = "com.canonical.android.unity"
BASE_PATH = "/com/canonical/android/unity/"
NOTIFICATIONS_OBJECT_PATH = BASE_PATH + "Notifications"
MESSAGING_MENU_OBJECT_PATH = BASE_PATH + "MessagingMenu"
NOTIFICATIONS_INTERFACE = DEFAULT_BUS_NAME + ".Notifications"
MESSAGING_MENU_INTERFACE = DEFAULT_BUS_NAME + ".MessagingMenu"
PROPERTY_VERSION = "Version"

LOCAL_APPS_DIR = os.path.expanduser("~/.cache/share/applications")
LOCAL_ICONS_DIR = os.path.expanduser("~/.icons")


class InvalidArgs(DBusException):
   def __init__(self, message):
      DBusException.__init__(self, message)

class NotSupported(DBusException):
   def __init__(self, message):
      DBusException.__init__(self, message)

class UnknownInterface(DBusException):
   def __init__(self, message):
      DBusException.__init__(self, message)

class Notifications(dbus.service.Object):
   def __init__(self, object_path):
      dbus.service.Object.__init__(self, dbus.SessionBus(), object_path)
      self.decisions = {}

   @dbus.service.method(dbus_interface=NOTIFICATIONS_INTERFACE,
                        in_signature='sss', out_signature='')
   def displayNotification(self, summary, body, icon):
      notify = dbus.SessionBus().get_object('org.freedesktop.Notifications',
                                           '/org/freedesktop/Notifications')
      inotify = dbus.Interface(notify, 'org.freedesktop.Notifications')
      inotify.Notify("", 0, icon, summary, body, [], {}, -1)

   @dbus.service.method(dbus_interface=NOTIFICATIONS_INTERFACE,
                        in_signature='sssassi', out_signature='b')
   def createSnapDecision(self, name, summary, body, buttons, icon, timeout):
      name = str(name)
      if name in self.decisions:
         print >> sys.stderr, "Decision with name %s already exists." % name
         return False

      button_count = len(buttons)
      if button_count < 2:
         print >> sys.stderr, "At least two buttons are required to create a snap decision"
         return False
      elif button_count > 3:
         print >> sys.stderr, "For now only the first two buttons are supported."

      decision = QuestionDialog(summary, body, str(buttons[0]), str(buttons[1]),
                                lambda decision: self.snapDecisionDecided(name, decision))
      self.decisions[name] = decision
      decision.start()
      return True

   @dbus.service.method(dbus_interface=NOTIFICATIONS_INTERFACE,
                        in_signature='s', out_signature='b')
   def clearSnapDecision(self, name):
      name = str(name)
      decision = self.decisions.get(name, None)
      if decision is None:
         print "Can't clear decision with name %s: it doesn't exist." % name
         return False

      decision.stop()
      return True

   @dbus.service.signal(dbus_interface=NOTIFICATIONS_INTERFACE, signature='si')
   def snapDecisionDecided(self, name, decision):
      # Remove to allow garbage collection and reuse of name
      self.decisions.pop(name, None)
      pass

   @dbus.service.method(dbus_interface=DBUS_PROPERTIES_INTERFACE,
                        in_signature='ss', out_signature='v')
   def Get(self, interface_name, property_name):
      if interface_name != NOTIFICATIONS_INTERFACE:
         raise UnknownInterface(interface_name)

      if property_name != PROPERTY_VERSION:
         raise InvalidArgs(property_name)

      return DBUS_API_VERSION

   @dbus.service.method(dbus_interface=DBUS_PROPERTIES_INTERFACE,
                        in_signature='s', out_signature='a{sv}')
   def GetAll(self, interface_name):
      if interface_name != NOTIFICATIONS_INTERFACE:
         raise UnknownInterface(interface_name)

      return {PROPERTY_VERSION: DBUS_API_VERSION}

   @dbus.service.method(dbus_interface=DBUS_PROPERTIES_INTERFACE,
                        in_signature='ssv', out_signature='')
   def Set(self, interface_name, property_name, property_value):
      raise NotSupported("org.freedesktop.DBus.Properties.Set is not supported")

class MessagingMenu(dbus.service.Object):
   def __init__(self, object_path):
      dbus.service.Object.__init__(self, dbus.SessionBus(), object_path)
      self.applications = {}
      self.default_server = None

   @dbus.service.method(dbus_interface=MESSAGING_MENU_INTERFACE,
                        in_signature='sss', out_signature='b')
   def registerApplication(self, application_id, title, icon):
      if application_id in self.applications:
         print >> sys.stderr, "An application with id %s is already registered." % application_id
         return False

      if not os.path.exists(LOCAL_APPS_DIR):
         try:
            os.makedirs(LOCAL_APPS_DIR)
         except OSError as e:
            print >> sys.stderr, "Failed to create local apps dir: %s" % e
            return False

      # TODO: handle base-64 icons by creating them into LOCAL_ICONS_DIR

      # Always overwrite the desktop file and the icon. They are cached
      # and it's possible the user has specified new values.
      desktopfile = os.path.join(LOCAL_APPS_DIR, application_id + ".desktop")
      try:
         with open(desktopfile, "w") as desktopfile_handle:
            desktopfile_handle.writelines([
               "[Desktop Entry]\n",
               "Type=Application\n"
               "Name=%s\n" % title,
               "Icon=%s\n" % icon,
               "Exec=%s\n" % application_id #FIXME: this should be hardcoded to the app launcher on android or something similar
            ])
      except IOError as e:
         print >> sys.stderr, "Failed to create local desktop file %s." % desktopfile
         return False

      server = Indicate.Server(type="message", desktop=desktopfile,
                               path="/com/canonical/indicate/%s" % application_id)

      menuserver = Dbusmenu.Server.new('/%s/shortcuts' % application_id)
      root = Dbusmenu.Menuitem.new()
      menuserver.set_root(root)
      server.set_menu(menuserver)

      if self.default_server is None:
         # We need to keep a reference to this alive for as long as we want indicators
         # to exist, otherwise when we create new servers and use them to create
         # indicators, libindicate will segfault. This is due to a bug in libindicate:
         # https://bugs.launchpad.net/libindicate/+bug/973480
         self.default_server = Indicate.Server.ref_default()

      server.show()
      self.applications[application_id] = {
         "server": server,
         "menu": root,
         "shortcuts": {},
         "indicators": {}
      }

      return True

   @dbus.service.method(dbus_interface=MESSAGING_MENU_INTERFACE,
                        in_signature='s', out_signature='b')
   def clearApplication(self, application_id):
      application = self.applications.pop(application_id, None)
      if application is not None:
         server = application.pop("server", None)
         if server is not None:
            server.hide()

         # Destroy the default server when there are no more applications
         if len(self.applications) == 0:
            self.default_server = None

         return True

      return False

   def get_application(self, application_id):
      application = self.applications.get(application_id, None)
      if application is None:
         print >> sys.stderr, "Application %s was not registered." % application_id
         return None

      return application

   @dbus.service.method(dbus_interface=MESSAGING_MENU_INTERFACE,
                        in_signature='ssbuts', out_signature='b')
   def createIndicator(self, application_id, name, attention, count, timestamp, icon):
      application = self.get_application(application_id)
      if application is None:
         return False
      server = application["server"]

      if name in application["indicators"]:
         indicator = application["indicators"][name]
      else:
         indicator = Indicate.Indicator.new_with_server(server)
         indicator.set_property(Indicate.INDICATOR_MESSAGES_PROP_NAME, name)

      indicator.set_property(Indicate.INDICATOR_MESSAGES_PROP_COUNT, str(count))
      if timestamp > 0:
         indicator.set_property(Indicate.INDICATOR_MESSAGES_PROP_TIME, str(timestamp))

      if attention:
         indicator.set_property(Indicate.INDICATOR_MESSAGES_PROP_ATTENTION,
                                Indicate.INDICATOR_VALUE_TRUE)
      else:
         indicator.set_property(Indicate.INDICATOR_MESSAGES_PROP_ATTENTION,
                                Indicate.INDICATOR_VALUE_FALSE)

      indicator.set_property(Indicate.INDICATOR_MESSAGES_PROP_ICON, icon)

      if name not in application["indicators"]:
         indicator.connect('user-display',
                           lambda shortcut, *args: self.indicatorActivated(application_id, name))

         application["indicators"][name] = indicator
         server.add_indicator(indicator)
         indicator.show()

      return True

   @dbus.service.method(dbus_interface=MESSAGING_MENU_INTERFACE,
                        in_signature='ss', out_signature='b')
   def clearIndicator(self, application_id, name):
      application = self.get_application(application_id)
      if application is None:
         return False

      server = application["server"]

      indicator = application["indicators"].pop(name, None)
      if indicator is None:
         print >> sys.stderr, "Can't find indicator with name %s." % name
         return False

      indicator.hide()
      server.remove_indicator(indicator)

      return True

   @dbus.service.method(dbus_interface=MESSAGING_MENU_INTERFACE,
                        in_signature='ss', out_signature='b')
   def createShortcut(self, application_id, name):
      application = self.get_application(application_id)
      if application is None:
         return False

      shortcuts = application["shortcuts"]
      if name in shortcuts:
         print >> sys.stderr, "Shortcut %s already exists. Ignoring request to create." % name
         return False

      shortcut = Dbusmenu.Menuitem.new()
      shortcut.property_set (Dbusmenu.MENUITEM_PROP_LABEL, name)
      shortcut.property_set_bool (Dbusmenu.MENUITEM_PROP_VISIBLE, True)
      shortcut.connect('item-activated',
                       lambda shortcut, *args: self.shortcutActivated(application_id, name))
      application["menu"].child_append(shortcut)
      shortcuts[name] = shortcut

      return True

   @dbus.service.method(dbus_interface=MESSAGING_MENU_INTERFACE,
                  in_signature='ss', out_signature='b')
   def clearShortcut(self, application_id, name):
      application = self.get_application(application_id)
      if application is None:
         return False

      shortcut = application["shortcuts"].pop(name, None)
      if shortcut is None:
         print >> sys.stderr, "Shortcut %s didn't exist. Ignoring request to clear." % name
         return False

      application["menu"].child_delete(shortcut)

      return True

   @dbus.service.signal(dbus_interface=MESSAGING_MENU_INTERFACE, signature='ss')
   def indicatorActivated(self, application_id, name):
      pass

   @dbus.service.signal(dbus_interface=MESSAGING_MENU_INTERFACE, signature='ss')
   def shortcutActivated(self, application_id, name):
      pass

   @dbus.service.method(dbus_interface=DBUS_PROPERTIES_INTERFACE,
                        in_signature='ss', out_signature='v')
   def Get(self, interface_name, property_name):
      if interface_name != MESSAGING_MENU_INTERFACE:
         raise UnknownInterface(interface_name)

      if property_name != PROPERTY_VERSION:
         raise InvalidArgs(property_name)

      return DBUS_API_VERSION

   @dbus.service.method(dbus_interface=DBUS_PROPERTIES_INTERFACE,
                        in_signature='s', out_signature='a{sv}')
   def GetAll(self, interface_name):
      if interface_name != MESSAGING_MENU_INTERFACE:
         raise UnknownInterface(interface_name)

      return {PROPERTY_VERSION: DBUS_API_VERSION}

   @dbus.service.method(dbus_interface=DBUS_PROPERTIES_INTERFACE,
                        in_signature='ssv', out_signature='')
   def Set(self, interface_name, property_name, property_value):
      raise NotSupported("org.freedesktop.DBus.Properties.Set is not supported")

if __name__ == '__main__':
   # Ensure only one instance of self is running at any given time.
   # See http://dbus.freedesktop.org/doc/dbus-specification.html#message-bus-names
   answer = dbus.SessionBus().request_name(DEFAULT_BUS_NAME,
                                           dbus.bus.NAME_FLAG_DO_NOT_QUEUE)
   if answer == dbus.bus.REQUEST_NAME_REPLY_PRIMARY_OWNER:
      notifications_server = Notifications(NOTIFICATIONS_OBJECT_PATH)
      messaging_menu_server = MessagingMenu(MESSAGING_MENU_OBJECT_PATH)

      # TODO: this is only needed for as long as we're using classes from
      # ufa-zenity-dialogs. It should be removed when we switch to real snap
      # decisions.
      GObject.threads_init()
      GObject.MainLoop().run()
   else:
      print >> sys.stderr, "Failed to acquire DBUS name %s. Exiting." % DEFAULT_BUS_NAME
