#! /usr/bin/python
#
# (C) Neil Jagdish Patel
#
# GPLv3
#

import sys, os
from gi.repository import GLib, GObject, Gio
from gi.repository import Dee
from gi.repository import Accounts, Signon
# FIXME: Some weird bug in Dee or PyGI makes Dee fail unless we probe
#        it *before* we import the Unity module... ?!
_m = dir(Dee.SequenceModel)
from gi.repository import Unity

import gdata.gauth
import gdata.docs.data
import gdata.docs.client

import urllib

#
# The primary bus name we grab *must* match what we specify in our .lens file
#
BUS_NAME = "net.launchpad.Unity.Lens.GDocs"

class Daemon:

  def __init__ (self):
    # The path for the Lens *must* also match the one in our .lens file
    self._lens = Unity.Lens.new ("/net/launchpad/unity/lens/gdocs", "gdocs")
    self._lens.props.search_hint = "Search Google Docs"
    self._lens.props.visible = True;
    self._lens.props.search_in_global = True;
    
    self.populate_categories();
    self.populate_filters();

    # The Lens infrastructure is capable of having a Lens daemon run independantly
    # of it's Scopes, so you can have the Music Lens that does nothing but
    # add some filters and categories, but then, depending on which music player
    # the user uses, they can install the right scope and it'll all work.
    #
    # However, many times you want to ship something useful with your Lens, which
    # means you'll want to ship a scope to make things work. Instead of having to
    # create a separate daemon and jump through a bunch of hoops to make all that
    # work, you can create a Scope(s) in the same process and use the
    # lens.add_local_scope() method to let the Lens know that it exists.
    self._scopes = []
    self._account_manager = Accounts.Manager.new_for_service_type("documents")
    self._account_manager.connect("enabled-event", self._on_enabled_event);
    for account in self._account_manager.get_enabled_account_services():
      self.add_account_service(account)

    # Now we've set the categories and filters, export the Scope.
    self._lens.export ();

  def _on_enabled_event (self, account_manager, account_id):
    account = self._account_manager.get_account(account_id)
    for service in account.list_services():
      account_service = Accounts.AccountService.new(account, service)
      if account_service.get_enabled():
        self.add_account_service(account_service)

  def add_account_service(self, account_service):
    for scope in self._scopes:
      if scope.get_account_service() == account_service:
        return
    scope = UserScope(account_service);
    self._scopes.append(scope)
    self._lens.add_local_scope (scope.get_scope())

  def populate_filters (self):
    filters = []

    # Radiooption is my favourite filter as it only allows the user to select on
    # option at a time, which does wonders for reducing the complexity of the code!
    # The first argument is the most important, it's an "id" that when a scope
    # queries the filter for the active option, it will receive back one of these
    # ids (i.e. the scope may receive "forms"). If it receives None, then that
    # means the user hasn't selected anything.
    f = Unity.RadioOptionFilter.new ("type", "Type", Gio.ThemedIcon.new(""), False)
    f.add_option ("document", "Text Documents", None)
    f.add_option ("spreadsheet", "Spreadsheets", None)
    f.add_option ("form", "Forms", None)
    f.add_option ("presentation", "Presentations", None)
    f.add_option ("drawing", "Drawings", None)
    f.add_option ("pdf", "PDF Files", None)
    f.add_option ("folder", "Folders", None)
    filters.append (f)

    f = Unity.RadioOptionFilter.new ("ownership", "Ownership", Gio.ThemedIcon.new(""), False)
    f.add_option ("mine", "Owned by me", None)
    filters.append(f)
    
    self._lens.props.filters = filters

  def populate_categories (self):
    # Ideally we'd have more pertinant icons for our Lens, but for now we'll
    # steal Unitys :)
    icon_loc = "/usr/share/icons/unity-icon-theme/places/svg/group-recent.svg"
    cats = []
   
    # You should appent categories in the order you'd like them displayed
    # When you append a result to the model, the third integer argument is
    # actually telling Unity which Category you'd like the result displayed in.
    #
    # For example: To add something to "Modified Today", you would send '0' when
    # adding that result. For "Modified Earilier This Month", you would send '3'.
    #
    # The third, CategoryRenderer, argument allows you to hint to Unity how you
    # would like the results for that Category to be displayed.
    cats.append (Unity.Category.new ("Modified Today",
                                     Gio.ThemedIcon.new(icon_loc),
                                     Unity.CategoryRenderer.VERTICAL_TILE))
    cats.append (Unity.Category.new ("Modified Yesterday",
                                     Gio.ThemedIcon.new(icon_loc),
                                     Unity.CategoryRenderer.VERTICAL_TILE))
    cats.append (Unity.Category.new ("Modified Earlier This Week",
                                     Gio.ThemedIcon.new(icon_loc),
                                     Unity.CategoryRenderer.VERTICAL_TILE))
    cats.append (Unity.Category.new ("Modified Earlier This Month",
                                     Gio.ThemedIcon.new(icon_loc),
                                     Unity.CategoryRenderer.VERTICAL_TILE))
    cats.append (Unity.Category.new ("Modified Earlier This Year",
                                     Gio.ThemedIcon.new(icon_loc),
                                     Unity.CategoryRenderer.VERTICAL_TILE))
    cats.append (Unity.Category.new ("Modified Long Ago",
                                     Gio.ThemedIcon.new(icon_loc),
                                     Unity.CategoryRenderer.VERTICAL_TILE))
    self._lens.props.categories = cats
    

class OAuth2Token:
  def __init__(self, token):
    self._token = token

  def modify_request(self, http_request):
    http_request.headers['Authorization'] = 'OAuth %s' % (self._token, )
    return http_request

  def __eq__(self, other):
    return other and self._token == other._token

# Encapsulates searching a single user's GDocs
class UserScope:
  def __init__ (self, account_service):
    self._account_service = account_service
    self._account_service.connect("enabled", self._on_account_enabled)
    self._enabled = self._account_service.get_enabled()
    self._authenticating = False
    self._client = gdata.docs.client.DocsClient(source='njpatel-UnityLensGDocs-0.1')
    self._client.ssl = True
    self._client.http_client.debug = False
    self._queued_search = None

    self._scope = Unity.Scope.new ("/net/launchpad/unity/scope/gdocs")

    # Listen for changes and requests
    self._scope.connect ("search-changed", self._on_search_changed)

    # This allows us to re-do the search if any parameter on a filter has changed
    # Though it's possible to connect to a more-specific changed signal on each
    # Fitler, as we re-do the search anyway, this catch-all signal is perfect for
    # us.
    self._scope.connect ("filters-changed", self._on_filters_changed);

    # Initiate the login
    self.login()

  def login(self):
    if self._authenticating:
      return
    print "logging in"
    self._authenticating = True
    # Get the global account settings
    auth_data = self._account_service.get_auth_data()
    identity = auth_data.get_credentials_id()
    session_data = auth_data.get_parameters()
    self.auth_session = Signon.AuthSession.new(identity, auth_data.get_method())
    self.auth_session.process(session_data,
            auth_data.get_mechanism(),
            self.login_cb, None)

  def login_cb(self, session, reply, error, user_data):
    print "login finished"
    self._authenticating = False
    if error:
      print >> sys.stderr, "Got authentication error:", error.message
      return
    old_token = self._client.auth_token
    if reply.has_key("AuthToken"):
      self._client.auth_token = gdata.gauth.ClientLoginToken(reply["AuthToken"])
    elif reply.has_key("AccessToken"):
      # OAuth2Token is included in GData 2.0.15 only
      #self._client.auth_token = gdata.gauth.OAuth2Token(None, None, None, None,
      #    access_token=reply["AccessToken"])
      self._client.auth_token = OAuth2Token(reply["AccessToken"])
    else:
      print >> sys.stderr, "Didn't find token in session:", reply

    if self._client.auth_token == old_token:
      print "Got the same token"
      return

    if self._queued_search:
      print "Performing queued search"
      self._on_search_changed(*self._queued_search)

  def get_account_service (self):
    return self._account_service

  def get_scope (self):
    return self._scope;

  def _on_account_enabled (self, account, enabled):
    self._enabled = enabled

  def _on_search_changed (self, scope, search, search_type, cancellable):
    self._queued_search = (scope, search, search_type, cancellable)
    if self._authenticating:
      print "authenticating, queuing search"
      return

    search_string = search.props.search_string
    results = search.props.results_model
    
    print "Search changed to: '%s'" % search_string
    
    if self._enabled:
      try:
        self._update_results_model (search_string, results)
      except gdata.client.Unauthorized:
        self.login()
        return
    else:
      results.clear()
    self._queued_search = None
    search.emit("finished")
  
  def _on_filters_changed (self, scope, param_spec=None):
    scope.queue_search_changed(Unity.SearchType.DEFAULT)
  
  def _update_results_model (self, search, model, is_global=False):
    # Clear out the current results in the model as we'll get a new list
    # NOTE: We could be clever and only remove/add things entries that have
    # changed, however the cost of doing that for a small result set probably
    # outweighs the gains. Especially if you consider the complexity,
    model.clear ()

    # Get the list of documents
    feed = self.get_doc_list(search, is_global);
    for entry in feed:
      if hasattr(entry, "GetResourceType"):
        rtype = entry.GetResourceType()
      else:
        rtype = entry.GetDocumentType()
      model.append(entry.link[0].href,
                   self.icon_for_type(rtype),
                   0,
                   "text/html",
                   entry.title.text.encode("UTF-8"),
                   rtype,
                   entry.link[0].href);

  # This is where we do the actual search for documents
  def get_doc_list (self, search, is_global):
    uri = "/feeds/default/private/full"

    # We do not want filters to effect global results
    if not is_global:
      uri = self.apply_filters(uri)
  
    uri += "?showfolders=true"

    if search != None and search != "":
      uri += "&q=" + urllib.quote_plus(search)

    print "Searching for: " + uri;

    if hasattr(self._client, "GetAllResources"):
      return self._client.GetAllResources(uri)
    else:
      return self._client.GetDocList(uri).entry

  def apply_filters (self, uri):
    # Try and grab a known filter and check whether or not it has an active
    # option. We've been clever and updated our filter option id's to match
    # what google expect in the request, so we can just use the id's when
    # constructing the uri :)
    f = self._scope.get_filter("type")
    if f != None:
      o = f.get_active_option()
      if o != None:
        uri +="/-/" + o.props.id

    f = self._scope.get_filter("ownership")
    if f != None:
      o = f.get_active_option()
      if o != None:
        uri +="/mine"

    return uri

  # Send back a useful icon depending on the document type
  def icon_for_type (self, doc_type):
    ret = "text-x-preview"

    if doc_type == "pdf":
      ret = "gnome-mime-application-pdf"
    elif doc_type == "drawing":
      ret = "x-office-drawing"
    elif doc_type == "document":
      ret = "x-office-document"
    elif doc_type == "presentation":
      ret = "libreoffice-oasis-presentation"
    elif doc_type == "spreadsheet" or doc_type == "text/xml":
      ret = "x-office-spreadsheet"
    elif doc_type == "folder":
      ret = "folder"
    else:
      print "Unhandled icon type: ", doc_type

    return ret;

if __name__ == "__main__":
  # NOTE: If we used the normal 'dbus' module for Python we'll get
  #       slightly odd results because it uses a default connection
  #       to the session bus that is different from the default connection
  #       GDBus (hence libunity) will use. Meaning that the daemon name
  #       will be owned by a connection different from the one all our
  #       Dee + Unity magic is working on...
  #       Still waiting for nice GDBus bindings to land:
  #                        http://www.piware.de/2011/01/na-zdravi-pygi/  
  session_bus_connection = Gio.bus_get_sync (Gio.BusType.SESSION, None)
  session_bus = Gio.DBusProxy.new_sync (session_bus_connection, 0, None,
                                        'org.freedesktop.DBus',
                                        '/org/freedesktop/DBus',
                                        'org.freedesktop.DBus', None)
  result = session_bus.call_sync('RequestName',
                                 GLib.Variant ("(su)", (BUS_NAME, 0x4)),
                                 0, -1, None)
                                 
  # Unpack variant response with signature "(u)". 1 means we got it.
  result = result.unpack()[0]
  
  if result != 1 :
    print >> sys.stderr, "Failed to own name %s. Bailing out." % BUS_NAME
    raise SystemExit (1)
  
  daemon = Daemon()
  GObject.MainLoop().run()

