#! /usr/bin/python3
#
# Copyright 2012 Canonical Ltd.
#
# Contact: Alberto Mardegan <alberto.mardegan@canonical.com>
#
# GPLv3
#

import sys
from gi.repository import GLib, GObject, Gio
from gi.repository import Dee
from gi.repository import Accounts, Signon
from gi.repository import GData

# 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

from datetime import datetime, timedelta
import time

#
# The primary bus name we grab *must* match what we specify in our .scope file
#
BUS_NAME = "net.launchpad.Unity.Scope.GDrive"
# Map Google Docs types to the values of the "type" filter
TYPE_MAP = {
    "document": "documents",
    "pdf": "documents",
    "folder": "folders",
    "drawing": "images",
    "presentation": "presentations"
}


class Daemon:

  def __init__ (self):
    self._scope = Unity.Scope.new ("/net/launchpad/unity/scope/gdrive")
    self._scope.search_in_global = True;
    self._preferences = Unity.PreferencesManager.get_default()
    self._preferences.connect ("notify::remote-content-search",
            self._on_filters_or_preferences_changed);

    self._gdocs_accounts = []
    try:
      self._account_manager = Accounts.Manager.new_for_service_type("documents")
    except TypeError as e:
      print ("Couldn't start account manager, not initialising: %s" % e)
      sys.exit(0)
    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)

    # 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_or_preferences_changed);
    self._scope.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 gdocs_account in self._gdocs_accounts:
      if gdocs_account.get_account_service() == account_service:
        return
    gdocs_account = GDocsAccount(self._scope, account_service);
    self._gdocs_accounts.append(gdocs_account)

  def _on_search_changed (self, scope, search, search_type, cancellable):
    search_string = search.props.search_string
    results = search.props.results_model
    results.clear()
    # Flush the results
    results.flush_revision_queue()

    if self._preferences.props.remote_content_search != Unity.PreferencesManagerRemoteContent.ALL:
      search.emit("finished")
      return

    if search_type == Unity.SearchType.GLOBAL:
      is_global = True
    else:
      is_global = False

    print("Search changed to: '%s'" % search_string)

    for gdocs_account in self._gdocs_accounts:
      gdocs_account.update_results_model (search_string, results, is_global)
    search.emit("finished")
  
  def _on_filters_or_preferences_changed (self, *_):
    self._scope.queue_search_changed(Unity.SearchType.DEFAULT)
  

class SignOnAuthorizer(GObject.Object, GData.Authorizer):
  __g_type_name__ = "SignOnAuthorizer"
  def __init__(self, account_service):
    GObject.Object.__init__(self)
    self._account_service = account_service
    self._main_loop = None
    self._token = None

  def do_process_request(self, domain, message):
    message.props.request_headers.replace('Authorization', 'OAuth %s' % (self._token, ))

  def do_is_authorized_for_domain(self, domain):
    return True if self._token else False

  def do_refresh_authorization(self, cancellable):
    if self._main_loop:
      print("Authorization already in progress")
      return False

    old_token = self._token
    # 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._main_loop = GObject.MainLoop()
    self._auth_session.process(session_data,
            auth_data.get_mechanism(),
            self.login_cb, None)
    if self._main_loop:
      self._main_loop.run()
    if self._token == old_token:
      print("Got the same token")
      return False
    else:
      print("Got token: %s" % (self._token, ))
      return True

  def login_cb(self, session, reply, error, user_data):
    print("login finished")
    self._main_loop.quit()
    self._main_loop = None
    if error:
      print("Got authentication error:", error.message)
      return
    if "AuthToken" in reply:
      self._token = reply["AuthToken"]
    elif "AccessToken" in reply:
      self._token = reply["AccessToken"]
    else:
      print("Didn't find token in session:", reply)


# Encapsulates searching a single user's GDocs
class GDocsAccount:
  def __init__ (self, scope, account_service):
    self._scope = scope
    self._account_service = account_service
    self._account_service.connect("enabled", self._on_account_enabled)
    self._enabled = self._account_service.get_enabled()
    self._authenticating = False
    authorizer = SignOnAuthorizer(self._account_service)
    authorizer.refresh_authorization(None)
    self._client = GData.DocumentsService(authorizer=authorizer)

  def get_account_service (self):
    return self._account_service

  def _on_account_enabled (self, account, enabled):
    print("account %s, enabled %s" % (account, enabled))
    self._enabled = enabled

  def update_results_model (self, search, model, is_global=False):
    if not self._enabled:
      return

    # Get the list of documents
    feed = self.get_doc_list(search, is_global);
    for entry in feed:
      rtype = entry.get_resource_id().split(":")[0]

      if is_global:
        category = 1
      else:
        if rtype == "folder":
          category = 3
        else:
          category = 0

      model.append(entry.look_up_link(GData.LINK_ALTERNATE).get_uri(),
                   self.icon_for_type(rtype),
                   category,
                   "text/html",
                   entry.props.title,
                   rtype,
                   entry.props.content_uri);

  # This is where we do the actual search for documents
  def get_doc_list (self, search, is_global):
    query = GData.DocumentsQuery(q=search)

    # We do not want filters to effect global results
    if not is_global:
      self.apply_filters(query)

    print("Searching for: " + query.props.q)

    if not self._client.is_authorized():
      if not self._client.props.authorizer.refresh_authorization(None):
        return []
    try:
      feed = self._client.query_documents(query, None, None, None).get_entries()
    except GObject.GError as e:
      print(e.message)
      return []

    if not is_global:
      feed = filter(self.filter_results, feed)
    return feed

  def apply_filters (self, query):
    f = self._scope.get_filter("modified")
    if f != None:
      o = f.get_active_option()
      if o != None:
        age = 0
        if o.props.id == "last-year":
          age = 365
        elif o.props.id == "last-30-days":
          age = 30
        elif o.props.id == "last-7-days":
          age = 7
        if age:
          last_time = datetime.now() - timedelta(age)
          query.set_updated_min(time.mktime(last_time.timetuple()))

  def filter_results (self, entry):
    f = self._scope.get_filter("type")
    if not f: return True

    if not f.props.filtering:
      return True

    rtype = entry.get_resource_id().split(":")[0]

    filter_type = TYPE_MAP.get(rtype, "other")
    return f.get_option(filter_type).props.active

  # 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"
    elif doc_type == "file":
      ret = "gnome-fs-regular"
    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("Failed to own name %s. Bailing out." % BUS_NAME)
    raise SystemExit (1)
  
  daemon = Daemon()
  GObject.MainLoop().run()

