/* ev-metadata.c
 *  this file is part of atril, a mate document viewer
 *
 * Copyright (C) 2009 Carlos Garcia Campos  <carlosgc@gnome.org>
 * Copyright © 2010 Christian Persch
 *
 * Atril 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 2 of the License, or
 * (at your option) any later version.
 *
 * Atril 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, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */

#include "config.h"

#include <glib.h>
#include <glib/gstdio.h>
#include <gio/gio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <unistd.h>

#define EV_DBUS_DAEMON_NAME             "org.mate.atril.Daemon"
#define EV_DBUS_DAEMON_INTERFACE_NAME   "org.mate.atril.Daemon"
#define EV_DBUS_DAEMON_OBJECT_PATH      "/org/mate/atril/Daemon"

#define EV_DBUS_WINDOW_INTERFACE_NAME   "org.mate.atril.Window"

#define DAEMON_TIMEOUT (30) /* seconds */

#define LOG g_printerr

static GList *ev_daemon_docs = NULL;
static guint kill_timer_id;
static GHashTable *pending_invocations = NULL;

typedef struct {
	gchar *dbus_name;
	gchar *uri;
        guint  watch_id;
	guint  loaded_id;
} EvDoc;

static void
ev_doc_free (EvDoc *doc)
{
	if (!doc)
		return;

	g_free (doc->dbus_name);
	g_free (doc->uri);

        g_bus_unwatch_name (doc->watch_id);

	g_free (doc);
}

static EvDoc *
ev_daemon_find_doc (const gchar *uri)
{
	GList *l;

	for (l = ev_daemon_docs; l != NULL; l = l->next) {
		EvDoc *doc = (EvDoc *)l->data;

		if (strcmp (doc->uri, uri) == 0)
			return doc;
	}

	return NULL;
}

static void
ev_daemon_stop_killtimer (void)
{
	if (kill_timer_id != 0)
		g_source_remove (kill_timer_id);
	kill_timer_id = 0;
}

static gboolean
ev_daemon_shutdown (gpointer user_data)
{
        GMainLoop *loop = (GMainLoop *) user_data;

        LOG ("Timeout; exiting daemon.\n");

        if (g_main_loop_is_running (loop))
                g_main_loop_quit (loop);

        return FALSE;
}

static void
ev_daemon_maybe_start_killtimer (gpointer data)
{
	ev_daemon_stop_killtimer ();
        if (ev_daemon_docs != NULL)
                return;

	kill_timer_id = g_timeout_add_seconds (DAEMON_TIMEOUT,
                                               (GSourceFunc) ev_daemon_shutdown,
                                               data);
}

static gboolean
convert_metadata (const gchar *metadata)
{
	GFile   *file;
	char    *argv[3];
	gint     exit_status;
	GFileAttributeInfoList *namespaces;
	gboolean supported = FALSE;
	GError  *error = NULL;
	gboolean retval;

	/* If metadata is not supported for a local file
	 * is likely because and old gvfs version is running.
	 */
	file = g_file_new_for_path (metadata);
	namespaces = g_file_query_writable_namespaces (file, NULL, NULL);
	if (namespaces) {
		gint i;

		for (i = 0; i < namespaces->n_infos; i++) {
			if (strcmp (namespaces->infos[i].name, "metadata") == 0) {
				supported = TRUE;
				break;
			}
		}
		g_file_attribute_info_list_unref (namespaces);
	}
	if (!supported) {
		g_warning ("GVFS metadata not supported. "
			   "Atril will run without metadata support.\n");
		g_object_unref (file);
		return FALSE;
	}
	g_object_unref (file);

	argv[0] = g_build_filename (LIBEXECDIR, "atril-convert-metadata", NULL);
	argv[1] = (char *) metadata;
	argv[2] = NULL;

	retval = g_spawn_sync (NULL /* wd */, argv, NULL /* env */,
			       0, NULL, NULL, NULL, NULL,
			       &exit_status, &error);
	g_free (argv[0]);

	if (!retval) {
		g_printerr ("Error migrating metadata: %s\n", error->message);
		g_error_free (error);
	}

	return retval && WIFEXITED (exit_status) && WEXITSTATUS (exit_status) == 0;
}

static void
ev_migrate_metadata (void)
{
	gchar       *updated;
	gchar       *metadata;
	gchar       *dot_dir;
	const gchar *userdir;

	userdir = g_getenv ("MATE22_USER_DIR");
	if (userdir) {
		dot_dir = g_build_filename (userdir, "atril", NULL);
	} else {
		dot_dir = g_build_filename(g_get_user_config_dir(), "atril", NULL);
	}

	updated = g_build_filename (dot_dir, "migrated-to-gvfs", NULL);
	if (g_file_test (updated, G_FILE_TEST_EXISTS)) {
		/* Already migrated */
		g_free (updated);
		g_free (dot_dir);
		return;
	}

	metadata = g_build_filename (dot_dir, "ev-metadata.xml", NULL);
	if (g_file_test (metadata, G_FILE_TEST_EXISTS)) {
		if (convert_metadata (metadata)) {
			gint fd;

			fd = g_creat (updated, 0600);
			if (fd != -1) {
				close (fd);
			}
		}
	}

	g_free (dot_dir);
	g_free (updated);
	g_free (metadata);
}

static gboolean
spawn_atril (const gchar *uri)
{
	gchar   *argv[3];
	gboolean retval;
	GError  *error = NULL;

	/* TODO Check that the uri exists */
	argv[0] = g_build_filename (BINDIR, "atril", NULL);
	argv[1] = (gchar *) uri;
	argv[2] = NULL;

	retval = g_spawn_async (NULL /* wd */, argv, NULL /* env */,
				0, NULL, NULL, NULL, &error);
	if (!retval) {
		g_printerr ("Error spawning atril for uri %s: %s\n", uri, error->message);
		g_error_free (error);
	}
	g_free (argv[0]);

	return retval;
}

static void
name_appeared_cb (GDBusConnection *connection,
                  const gchar     *name,
                  const gchar     *name_owner,
                  gpointer         user_data)
{
        LOG ("Watch name'%s' appeared with owner '%s'\n", name, name_owner);
}

static void
name_vanished_cb (GDBusConnection *connection,
                  const gchar     *name,
                  gpointer         user_data)
{
	GList *l;

        LOG ("Watch name'%s' disappeared\n", name);

        for (l = ev_daemon_docs; l != NULL; l = l->next) {
                EvDoc *doc = (EvDoc *) l->data;

                if (strcmp (doc->dbus_name, name) != 0)
                        continue;

                LOG ("Watch found URI '%s' for name; removing\n", doc->uri);

                ev_daemon_docs = g_list_delete_link (ev_daemon_docs, l);
                ev_doc_free (doc);

                ev_daemon_maybe_start_killtimer (user_data);
                return;
        }
}

static void
process_pending_invocations (const gchar *uri,
			     const gchar *dbus_name)
{
	GList *l;
	GList *uri_invocations;

	LOG ("RegisterDocument process pending invocations for URI %s\n", uri);
	uri_invocations = g_hash_table_lookup (pending_invocations, uri);

	for (l = uri_invocations; l != NULL; l = l->next) {
		GDBusMethodInvocation *invocation;

		invocation = (GDBusMethodInvocation *)l->data;
		g_dbus_method_invocation_return_value (invocation,
						       g_variant_new ("(s)", dbus_name));
	}

	g_list_free (uri_invocations);
	g_hash_table_remove (pending_invocations, uri);
}

static void
document_loaded_cb (GDBusConnection *connection,
		    const gchar     *sender_name,
		    const gchar     *object_path,
		    const gchar     *interface_name,
		    const gchar     *signal_name,
		    GVariant        *parameters,
		    EvDoc           *doc)
{
	const gchar *uri;

	g_variant_get (parameters, "(&s)", &uri);
	if (strcmp (uri, doc->uri) == 0)
		process_pending_invocations (uri, sender_name);
	g_dbus_connection_signal_unsubscribe (connection, doc->loaded_id);
}

static void
method_call_cb (GDBusConnection       *connection,
                const gchar           *sender,
                const gchar           *object_path,
                const gchar           *interface_name,
                const gchar           *method_name,
                GVariant              *parameters,
                GDBusMethodInvocation *invocation,
                gpointer               user_data)
{
        if (g_strcmp0 (interface_name, EV_DBUS_DAEMON_INTERFACE_NAME) != 0)
                return;

        if (g_strcmp0 (method_name, "RegisterDocument") == 0) {
                EvDoc       *doc;
                const gchar *uri;

                g_variant_get (parameters, "(&s)", &uri);

                doc = ev_daemon_find_doc (uri);
                if (doc != NULL) {
                        LOG ("RegisterDocument found owner '%s' for URI '%s'\n", doc->dbus_name, uri);
                        g_dbus_method_invocation_return_value (invocation,
                                                               g_variant_new ("(s)", doc->dbus_name));
                        return;
                }

                ev_daemon_stop_killtimer ();

                doc = g_new (EvDoc, 1);
                doc->dbus_name = g_strdup (sender);
                doc->uri = g_strdup (uri);

		doc->loaded_id = g_dbus_connection_signal_subscribe (connection,
								     doc->dbus_name,
								     EV_DBUS_WINDOW_INTERFACE_NAME,
								     "DocumentLoaded",
								     NULL,
								     NULL,
								     0,
								     (GDBusSignalCallback) document_loaded_cb,
								     doc,
								     NULL);
                doc->watch_id = g_bus_watch_name_on_connection (connection,
                                                                sender,
                                                                G_BUS_NAME_WATCHER_FLAGS_NONE,
                                                                name_appeared_cb,
                                                                name_vanished_cb,
                                                                user_data, NULL);

                LOG ("RegisterDocument registered owner '%s' for URI '%s'\n", doc->dbus_name, uri);
                ev_daemon_docs = g_list_prepend (ev_daemon_docs, doc);

                g_dbus_method_invocation_return_value (invocation, g_variant_new ("(s)", ""));
        } else if (g_strcmp0 (method_name, "UnregisterDocument") == 0) {
                EvDoc *doc;
                const gchar *uri;

                g_variant_get (parameters, "(&s)", &uri);

                LOG ("UnregisterDocument URI '%s'\n", uri);

                doc = ev_daemon_find_doc (uri);
                if (doc == NULL) {
                        LOG ("UnregisterDocument URI was not registered!\n");
                        g_dbus_method_invocation_return_error_literal (invocation,
                                                                       G_DBUS_ERROR,
                                                                       G_DBUS_ERROR_INVALID_ARGS,
                                                                       "URI not registered");
                        return;
                }

                if (strcmp (doc->dbus_name, sender) != 0) {
                        LOG ("UnregisterDocument called by non-owner (owner '%s' sender '%s')\n",
                             doc->dbus_name, sender);

                        g_dbus_method_invocation_return_error_literal (invocation,
                                                                       G_DBUS_ERROR,
                                                                       G_DBUS_ERROR_BAD_ADDRESS,
                                                                       "Only owner can call this method");
                        return;
                }

                ev_daemon_docs = g_list_remove (ev_daemon_docs, doc);
                ev_doc_free (doc);
                ev_daemon_maybe_start_killtimer (user_data);

                g_dbus_method_invocation_return_value (invocation, g_variant_new ("()"));
	} else if (g_strcmp0 (method_name, "FindDocument") == 0) {
		EvDoc *doc;
		const gchar *uri;
		gboolean spawn;

		g_variant_get (parameters, "(&sb)",  &uri, &spawn);

		LOG ("FindDocument URI '%s' \n", uri);

		doc = ev_daemon_find_doc (uri);
		if (doc != NULL) {
			g_dbus_method_invocation_return_value (invocation,
							       g_variant_new ("(s)", doc->dbus_name));
			return;
		}

		if (spawn) {
			GList *uri_invocations;
			gboolean ret_val = TRUE;

			uri_invocations = g_hash_table_lookup (pending_invocations, uri);

			if (uri_invocations == NULL) {
				/* Only spawn once. */
				ret_val = spawn_atril (uri);
			}

			if (ret_val) {
				/* Only defer DBUS answer if atril was succesfully spawned */
				uri_invocations = g_list_prepend (uri_invocations, invocation);
				g_hash_table_insert (pending_invocations,
						     g_strdup (uri),
						     uri_invocations);
				return;
			}
		}

		LOG ("FindDocument URI '%s' was not registered!\n", uri);
		g_dbus_method_invocation_return_value (invocation,
						       g_variant_new ("(s)",""));
	}
}

static const char introspection_xml[] =
  "<node>"
    "<interface name='org.mate.atril.Daemon'>"
      "<method name='RegisterDocument'>"
        "<arg type='s' name='uri' direction='in'/>"
        "<arg type='s' name='owner' direction='out'/>"
      "</method>"
      "<method name='UnregisterDocument'>"
        "<arg type='s' name='uri' direction='in'/>"
      "</method>"
      "<method name='FindDocument'>"
        "<arg type='s' name='uri' direction='in'/>"
        "<arg type='b' name='spawn' direction='in'/>"
        "<arg type='s' name='owner' direction='out'/>"
      "</method>"
    "</interface>"
  "</node>";

static const GDBusInterfaceVTable interface_vtable = {
  method_call_cb,
  NULL,
  NULL
};

static GDBusNodeInfo *introspection_data;

static void
bus_acquired_cb (GDBusConnection *connection,
		 const gchar     *name,
		 gpointer         user_data)
{
	GMainLoop *loop = (GMainLoop *) user_data;
	guint      registration_id;
	GError    *error = NULL;

	if (!introspection_data)
		introspection_data = g_dbus_node_info_new_for_xml (introspection_xml, NULL);

	registration_id = g_dbus_connection_register_object (connection,
							     EV_DBUS_DAEMON_OBJECT_PATH,
							     introspection_data->interfaces[0],
							     &interface_vtable,
							     g_main_loop_ref (loop),
							     (GDestroyNotify) g_main_loop_unref,
							     &error);
	if (registration_id == 0) {
		g_printerr ("Failed to register object: %s\n", error->message);
		g_error_free (error);

		if (g_main_loop_is_running (loop))
			g_main_loop_quit (loop);
	}
}

static void
name_acquired_cb (GDBusConnection *connection,
                  const gchar     *name,
                  gpointer         user_data)
{
	ev_migrate_metadata ();

	ev_daemon_maybe_start_killtimer (user_data);
}

static void
name_lost_cb (GDBusConnection *connection,
              const gchar     *name,
              gpointer         user_data)
{
          GMainLoop *loop = (GMainLoop *) user_data;

          /* Failed to acquire the name; exit daemon */
          if (g_main_loop_is_running (loop))
                  g_main_loop_quit (loop);
}

gint
main (gint argc, gchar **argv)
{
	GMainLoop *loop;
        guint owner_id;

        g_set_prgname ("atril-daemon");

	loop = g_main_loop_new (NULL, FALSE);

	pending_invocations = g_hash_table_new_full (g_str_hash,
						     g_str_equal,
						     (GDestroyNotify)g_free,
						     NULL);

        owner_id = g_bus_own_name (G_BUS_TYPE_SESSION,
				   EV_DBUS_DAEMON_NAME,
				   G_BUS_NAME_OWNER_FLAGS_NONE,
				   bus_acquired_cb,
				   name_acquired_cb,
				   name_lost_cb,
				   g_main_loop_ref (loop),
				   (GDestroyNotify) g_main_loop_unref);

        g_main_loop_run (loop);

        g_bus_unown_name (owner_id);

        g_main_loop_unref (loop);
	if (introspection_data)
		g_dbus_node_info_unref (introspection_data);
        g_list_foreach (ev_daemon_docs, (GFunc)ev_doc_free, NULL);
        g_list_free (ev_daemon_docs);
        g_hash_table_destroy (pending_invocations);

	return 0;
}