/*
 * Seahorse
 *
 * Copyright (C) 2008 Stefan Walter
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this program; if not, see
 * <http://www.gnu.org/licenses/>.
 */

#include "config.h"

#include "seahorse-gpgme-dialogs.h"
#include "seahorse-pgp-actions.h"
#include "seahorse-pgp-backend.h"
#include "seahorse-server-source.h"
#include "seahorse-transfer.h"
#include "seahorse-unknown-source.h"

#include "seahorse-common.h"

#include "libseahorse/seahorse-progress.h"
#include "libseahorse/seahorse-util.h"

#include <glib/gi18n.h>

#include <string.h>

enum {
	PROP_0,
	PROP_NAME,
	PROP_LABEL,
	PROP_DESCRIPTION,
	PROP_ACTIONS,
	PROP_LOADED
};

static SeahorsePgpBackend *pgp_backend = NULL;

struct _SeahorsePgpBackend {
    GObject parent;

    SeahorseGpgmeKeyring *keyring;
    SeahorseDiscovery *discovery;
    SeahorseUnknownSource *unknown;
    GHashTable *remotes;
    SeahorseActionGroup *actions;
    gboolean loaded;
};

static void         seahorse_pgp_backend_iface            (SeahorseBackendIface *iface);

static void         seahorse_pgp_backend_collection_init  (GcrCollectionIface *iface);

G_DEFINE_TYPE_WITH_CODE (SeahorsePgpBackend, seahorse_pgp_backend, G_TYPE_OBJECT,
                         G_IMPLEMENT_INTERFACE (GCR_TYPE_COLLECTION, seahorse_pgp_backend_collection_init);
                         G_IMPLEMENT_INTERFACE (SEAHORSE_TYPE_BACKEND, seahorse_pgp_backend_iface);
);

static void
seahorse_pgp_backend_init (SeahorsePgpBackend *self)
{
	g_return_if_fail (pgp_backend == NULL);
	pgp_backend = self;

	self->remotes = g_hash_table_new_full (g_str_hash, g_str_equal,
	                                       g_free, g_object_unref);

	self->actions = seahorse_pgp_backend_actions_instance ();
}

#ifdef WITH_KEYSERVER

static void
on_settings_keyservers_changed (GSettings *settings,
                                gchar *key,
                                gpointer user_data)
{
	SeahorsePgpBackend *self = SEAHORSE_PGP_BACKEND (user_data);
	SeahorseServerSource *source;
	gchar **keyservers;
	GHashTable *check;
	const gchar *uri;
	GHashTableIter iter;
	guint i;

	check = g_hash_table_new (g_str_hash, g_str_equal);
	g_hash_table_iter_init (&iter, self->remotes);
	while (g_hash_table_iter_next (&iter, (gpointer*)&uri, (gpointer*)&source))
		g_hash_table_replace (check, (gpointer)uri, source);

	/* Load and strip names from keyserver list */
	keyservers = seahorse_servers_get_uris ();

	for (i = 0; keyservers[i] != NULL; i++) {
		uri = keyservers[i];

		/* If we don't have a keysource then add it */
		if (!g_hash_table_lookup (self->remotes, uri)) {
			source = seahorse_server_source_new (uri);
			if (source != NULL) {
				seahorse_pgp_backend_add_remote (self, uri, source);
				g_object_unref (source);
			}
		}

		/* Mark this one as present */
		g_hash_table_remove (check, uri);
	}

	/* Now remove any extras */
	g_hash_table_iter_init (&iter, check);
	while (g_hash_table_iter_next (&iter, (gpointer*)&uri, NULL))
		seahorse_pgp_backend_remove_remote (self, uri);

	g_hash_table_destroy (check);
	g_strfreev (keyservers);
}

#endif /* WITH_KEYSERVER */

static void
on_place_loaded (GObject       *object,
                 GAsyncResult  *result,
                 gpointer       user_data)
{
	SeahorsePgpBackend *backend = user_data;
	gboolean ok;
	GError *error;

	error = NULL;
	ok = seahorse_place_load_finish (SEAHORSE_PLACE (object), result, &error);

	if (!ok) {
		g_warning ("Failed to initialize PGP backend: %s", error->message);
		g_error_free (error);
	}

	backend->loaded = TRUE;
	g_object_notify (G_OBJECT (backend), "loaded");

	g_object_unref (backend);
}

static void
seahorse_pgp_backend_constructed (GObject *obj)
{
	SeahorsePgpBackend *self = SEAHORSE_PGP_BACKEND (obj);

	G_OBJECT_CLASS (seahorse_pgp_backend_parent_class)->constructed (obj);

	self->keyring = seahorse_gpgme_keyring_new ();
	seahorse_place_load (SEAHORSE_PLACE (self->keyring), NULL, on_place_loaded,
			     g_object_ref (self));

	self->discovery = seahorse_discovery_new ();
	self->unknown = seahorse_unknown_source_new ();

#ifdef WITH_KEYSERVER
	g_signal_connect (seahorse_pgp_settings_instance (), "changed::keyservers",
	                  G_CALLBACK (on_settings_keyservers_changed), self);

	/* Initial loading */
	on_settings_keyservers_changed (G_SETTINGS (seahorse_pgp_settings_instance ()), "keyservers", self);
#endif
}

static const gchar *
seahorse_pgp_backend_get_name (SeahorseBackend *backend)
{
	return SEAHORSE_PGP_NAME;
}

static const gchar *
seahorse_pgp_backend_get_label (SeahorseBackend *backend)
{
	return _("PGP Keys");
}

static const gchar *
seahorse_pgp_backend_get_description (SeahorseBackend *backend)
{
	return _("PGP keys are for encrypting email or files");
}

static SeahorseActionGroup *
seahorse_pgp_backend_get_actions (SeahorseBackend *backend)
{
	SeahorsePgpBackend *self = SEAHORSE_PGP_BACKEND (backend);
	return g_object_ref (self->actions);
}

static gboolean
seahorse_pgp_backend_get_loaded (SeahorseBackend *backend)
{
	g_return_val_if_fail (SEAHORSE_PGP_IS_BACKEND (backend), FALSE);

	return SEAHORSE_PGP_BACKEND (backend)->loaded;
}

static void
seahorse_pgp_backend_get_property (GObject *obj,
                                   guint prop_id,
                                   GValue *value,
                                   GParamSpec *pspec)
{
	SeahorseBackend *backend = SEAHORSE_BACKEND (obj);

	switch (prop_id) {
	case PROP_NAME:
		g_value_set_string (value,  seahorse_pgp_backend_get_name (backend));
		break;
	case PROP_LABEL:
		g_value_set_string (value,  seahorse_pgp_backend_get_label (backend));
		break;
	case PROP_DESCRIPTION:
		g_value_set_string (value,  seahorse_pgp_backend_get_description (backend));
		break;
	case PROP_ACTIONS:
		g_value_take_object (value, seahorse_pgp_backend_get_actions (backend));
		break;
	case PROP_LOADED:
		g_value_set_boolean (value, seahorse_pgp_backend_get_loaded (backend));
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, prop_id, pspec);
		break;
	}
}

static void
seahorse_pgp_backend_finalize (GObject *obj)
{
	SeahorsePgpBackend *self = SEAHORSE_PGP_BACKEND (obj);

#ifdef WITH_KEYSERVER
	g_signal_handlers_disconnect_by_func (seahorse_pgp_settings_instance (),
	                                      on_settings_keyservers_changed, self);
#endif

	g_clear_object (&self->keyring);
	g_clear_object (&self->discovery);
	g_clear_object (&self->unknown);
	g_hash_table_destroy (self->remotes);
	g_clear_object (&self->actions);
	pgp_backend = NULL;

	G_OBJECT_CLASS (seahorse_pgp_backend_parent_class)->finalize (obj);
}

static void
seahorse_pgp_backend_class_init (SeahorsePgpBackendClass *klass)
{
	GObjectClass *gobject_class = G_OBJECT_CLASS (klass);

	gobject_class->constructed = seahorse_pgp_backend_constructed;
	gobject_class->finalize = seahorse_pgp_backend_finalize;
	gobject_class->get_property = seahorse_pgp_backend_get_property;

	g_object_class_override_property (gobject_class, PROP_NAME, "name");
	g_object_class_override_property (gobject_class, PROP_LABEL, "label");
	g_object_class_override_property (gobject_class, PROP_DESCRIPTION, "description");
	g_object_class_override_property (gobject_class, PROP_ACTIONS, "actions");
	g_object_class_override_property (gobject_class, PROP_LOADED, "loaded");
}

static guint
seahorse_pgp_backend_get_length (GcrCollection *collection)
{
	return 1;
}

static GList *
seahorse_pgp_backend_get_objects (GcrCollection *collection)
{
	SeahorsePgpBackend *self = SEAHORSE_PGP_BACKEND (collection);
	return g_list_append (NULL, self->keyring);
}

static gboolean
seahorse_pgp_backend_contains (GcrCollection *collection,
                               GObject *object)
{
	SeahorsePgpBackend *self = SEAHORSE_PGP_BACKEND (collection);
	return G_OBJECT (self->keyring) == object;
}

static void
seahorse_pgp_backend_collection_init (GcrCollectionIface *iface)
{
	iface->contains = seahorse_pgp_backend_contains;
	iface->get_length = seahorse_pgp_backend_get_length;
	iface->get_objects = seahorse_pgp_backend_get_objects;
}

static SeahorsePlace *
seahorse_pgp_backend_lookup_place (SeahorseBackend *backend,
                                   const gchar *uri)
{
	SeahorsePgpBackend *self = SEAHORSE_PGP_BACKEND (backend);
	if (g_str_equal (uri, "gnupg://"))
		return SEAHORSE_PLACE (seahorse_pgp_backend_get_default_keyring (self));
	return NULL;
}

static void
seahorse_pgp_backend_iface (SeahorseBackendIface *iface)
{
	iface->lookup_place = seahorse_pgp_backend_lookup_place;
	iface->get_actions = seahorse_pgp_backend_get_actions;
	iface->get_description = seahorse_pgp_backend_get_description;
	iface->get_label = seahorse_pgp_backend_get_label;
	iface->get_name = seahorse_pgp_backend_get_name;
	iface->get_loaded = seahorse_pgp_backend_get_loaded;
}

SeahorsePgpBackend *
seahorse_pgp_backend_get (void)
{
	g_return_val_if_fail (pgp_backend, NULL);
	return pgp_backend;
}

void
seahorse_pgp_backend_initialize (void)
{
	SeahorsePgpBackend *self;

	g_return_if_fail (pgp_backend == NULL);
	self = g_object_new (SEAHORSE_PGP_TYPE_BACKEND, NULL);

	seahorse_backend_register (SEAHORSE_BACKEND (self));
	g_object_unref (self);

	g_return_if_fail (pgp_backend != NULL);

	gpgme_set_engine_info (GPGME_PROTOCOL_OpenPGP, NULL, NULL);
}

SeahorseGpgmeKeyring *
seahorse_pgp_backend_get_default_keyring (SeahorsePgpBackend *self)
{
	self = self ? self : seahorse_pgp_backend_get ();
	g_return_val_if_fail (SEAHORSE_PGP_IS_BACKEND (self), NULL);
	g_return_val_if_fail (self->keyring, NULL);
	return self->keyring;
}

SeahorsePgpKey *
seahorse_pgp_backend_get_default_key (SeahorsePgpBackend *self)
{
	SeahorsePgpKey *key = NULL;
	SeahorsePgpSettings *settings;
	const gchar *keyid;
	gchar *value;

	self = self ? self : seahorse_pgp_backend_get ();
	g_return_val_if_fail (SEAHORSE_PGP_IS_BACKEND (self), NULL);

	settings = seahorse_pgp_settings_instance ();
	if (settings != NULL) {
		value = seahorse_pgp_settings_get_default_key (settings);
		if (value != NULL && value[0]) {
			if (g_str_has_prefix (value, "openpgp:"))
				keyid = value + strlen ("openpgp:");
			else
				keyid = value;
			key = SEAHORSE_PGP_KEY (seahorse_gpgme_keyring_lookup (self->keyring, keyid));
		}
		g_free (value);
	}

	return key;
}

#ifdef WITH_KEYSERVER

SeahorseDiscovery *
seahorse_pgp_backend_get_discovery (SeahorsePgpBackend *self)
{
	self = self ? self : seahorse_pgp_backend_get ();
	g_return_val_if_fail (SEAHORSE_PGP_IS_BACKEND (self), NULL);
	g_return_val_if_fail (self->discovery, NULL);
	return self->discovery;
}

SeahorseServerSource *
seahorse_pgp_backend_lookup_remote (SeahorsePgpBackend *self,
                                    const gchar *uri)
{
	self = self ? self : seahorse_pgp_backend_get ();
	g_return_val_if_fail (SEAHORSE_PGP_IS_BACKEND (self), NULL);

	return g_hash_table_lookup (self->remotes, uri);
}

void
seahorse_pgp_backend_add_remote (SeahorsePgpBackend *self,
                                 const gchar *uri,
                                 SeahorseServerSource *source)
{
	self = self ? self : seahorse_pgp_backend_get ();
	g_return_if_fail (SEAHORSE_PGP_IS_BACKEND (self));
	g_return_if_fail (uri != NULL);
	g_return_if_fail (SEAHORSE_IS_SERVER_SOURCE (source));
	g_return_if_fail (g_hash_table_lookup (self->remotes, uri) == NULL);

	g_hash_table_insert (self->remotes, g_strdup (uri), g_object_ref (source));
}

void
seahorse_pgp_backend_remove_remote (SeahorsePgpBackend *self,
                                    const gchar *uri)
{
	self = self ? self : seahorse_pgp_backend_get ();
	g_return_if_fail (SEAHORSE_PGP_IS_BACKEND (self));
	g_return_if_fail (uri != NULL);

	g_hash_table_remove (self->remotes, uri);
}

typedef struct {
    gint num_searches;
} search_remote_closure;

static void
search_remote_closure_free (gpointer user_data)
{
    search_remote_closure *closure = user_data;
    g_free (closure);
}

static void
on_source_search_ready (GObject *source,
                        GAsyncResult *result,
                        gpointer user_data)
{
    g_autoptr(GTask) task = G_TASK (user_data);
    search_remote_closure *closure = g_task_get_task_data (task);
    g_autoptr(GError) error = NULL;

    g_return_if_fail (closure->num_searches > 0);

    if (!seahorse_server_source_search_finish (SEAHORSE_SERVER_SOURCE (source),
                                               result, &error)) {
        g_task_return_error (task, g_steal_pointer (&error));
        return;
    }

    closure->num_searches--;
    seahorse_progress_end (g_task_get_cancellable (task),
                           GINT_TO_POINTER (closure->num_searches));

    if (closure->num_searches == 0)
        g_task_return_boolean (task, TRUE);
}

void
seahorse_pgp_backend_search_remote_async (SeahorsePgpBackend *self,
                                          const gchar *search,
                                          GcrSimpleCollection *results,
                                          GCancellable *cancellable,
                                          GAsyncReadyCallback callback,
                                          gpointer user_data)
{
    search_remote_closure *closure;
    g_autoptr(GTask) task = NULL;
    SeahorseServerSource *source;
    g_autoptr(GHashTable) servers = NULL;
    GHashTableIter iter;
    g_auto(GStrv) names = NULL;
    guint i;

    self = self ? self : seahorse_pgp_backend_get ();
    g_return_if_fail (SEAHORSE_PGP_IS_BACKEND (self));

    /* Get a list of all selected key servers */
    names = g_settings_get_strv (G_SETTINGS (seahorse_app_settings_instance ()), "last-search-servers");
    if (names != NULL && names[0] != NULL) {
        servers = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
        for (i = 0; names[i] != NULL; i++)
            g_hash_table_insert (servers, g_strdup (names[i]), GINT_TO_POINTER (TRUE));
    }

    task = g_task_new (self, cancellable, callback, user_data);
    closure = g_new0 (search_remote_closure, 1);
    g_task_set_task_data (task, closure, search_remote_closure_free);

    g_hash_table_iter_init (&iter, self->remotes);
    while (g_hash_table_iter_next (&iter, NULL, (gpointer *)&source)) {
        if (servers) {
            g_autofree gchar *uri = NULL;
            g_object_get (source, "uri", &uri, NULL);
            if (!g_hash_table_lookup (servers, uri))
                continue;
        }

        seahorse_progress_prep_and_begin (cancellable, GINT_TO_POINTER (closure->num_searches), NULL);
        seahorse_server_source_search_async (source, search, results, cancellable,
                                             on_source_search_ready, g_object_ref (task));
        closure->num_searches++;
    }

    if (closure->num_searches == 0)
        g_task_return_boolean (task, FALSE);
}

gboolean
seahorse_pgp_backend_search_remote_finish (SeahorsePgpBackend *self,
                                           GAsyncResult *result,
                                           GError **error)
{
    g_return_val_if_fail (SEAHORSE_PGP_IS_BACKEND (self), FALSE);
    g_return_val_if_fail (g_task_is_valid (result, self), FALSE);

    return g_task_propagate_boolean (G_TASK (result), error);
}

typedef struct {
    gint num_transfers;
} transfer_closure;

static void
transfer_closure_free (gpointer user_data)
{
    transfer_closure *closure = user_data;
    g_free (closure);
}

static void
on_source_transfer_ready (GObject *source,
                          GAsyncResult *result,
                          gpointer user_data)
{
    g_autoptr(GTask) task = G_TASK (user_data);
    transfer_closure *closure = g_task_get_task_data (task);
    g_autoptr(GError) error = NULL;

    g_return_if_fail (closure->num_transfers > 0);

    seahorse_transfer_finish (result, &error);
    if (error != NULL) {
        g_task_return_error (task, g_steal_pointer (&error));
        return;
    }

    closure->num_transfers--;
    seahorse_progress_end (g_task_get_cancellable (task),
                           GINT_TO_POINTER (closure->num_transfers));

    if (closure->num_transfers == 0)
        g_task_return_boolean (task, TRUE);
}

void
seahorse_pgp_backend_transfer_async (SeahorsePgpBackend *self,
                                     GList *keys,
                                     SeahorsePlace *to,
                                     GCancellable *cancellable,
                                     GAsyncReadyCallback callback,
                                     gpointer user_data)
{
    transfer_closure *closure;
    SeahorseObject *object;
    g_autoptr(GTask) task = NULL;
    SeahorsePlace *from;

    self = self ? self : seahorse_pgp_backend_get ();
    g_return_if_fail (SEAHORSE_PGP_IS_BACKEND (self));
    g_return_if_fail (SEAHORSE_IS_PLACE (to));

    task = g_task_new (self, cancellable, callback, user_data);
    closure = g_new0 (transfer_closure, 1);
    g_task_set_task_data (task, closure, transfer_closure_free);

    keys = g_list_copy (keys);
    /* Sort by key place */
    keys = seahorse_util_objects_sort_by_place (keys);

    while (keys) {
        GList *next;

        /* break off one set (same keysource) */
        next = seahorse_util_objects_splice_by_place (keys);

        g_assert (SEAHORSE_IS_OBJECT (keys->data));
        object = SEAHORSE_OBJECT (keys->data);

        /* Export from this key place */
        from = seahorse_object_get_place (object);
        g_return_if_fail (SEAHORSE_IS_PLACE (from));

        if (from != to) {
            /* Start a new transfer operation between the two places */
            seahorse_progress_prep_and_begin (cancellable, GINT_TO_POINTER (closure->num_transfers), NULL);
            seahorse_transfer_keys_async (from, to, keys, cancellable,
                                          on_source_transfer_ready, g_object_ref (task));
            closure->num_transfers++;
        }

        g_list_free (keys);
        keys = next;
    }

    if (closure->num_transfers == 0)
        g_task_return_boolean (task, TRUE);
}

gboolean
seahorse_pgp_backend_transfer_finish (SeahorsePgpBackend *self,
                                      GAsyncResult *result,
                                      GError **error)
{
    g_return_val_if_fail (SEAHORSE_PGP_IS_BACKEND (self), FALSE);
    g_return_val_if_fail (g_task_is_valid (result, self), FALSE);

    return g_task_propagate_boolean (G_TASK (result), error);
}

void
seahorse_pgp_backend_retrieve_async (SeahorsePgpBackend *self,
                                     const gchar **keyids,
                                     SeahorsePlace *to,
                                     GCancellable *cancellable,
                                     GAsyncReadyCallback callback,
                                     gpointer user_data)
{
    transfer_closure *closure;
    g_autoptr(GTask) task = NULL;
    SeahorsePlace *place;
    GHashTableIter iter;

    g_return_if_fail (SEAHORSE_PGP_IS_BACKEND (self));
    g_return_if_fail (SEAHORSE_IS_PLACE (to));

    task = g_task_new (self, cancellable, callback, user_data);
    closure = g_new0 (transfer_closure, 1);
    g_task_set_task_data (task, closure, transfer_closure_free);

    g_hash_table_iter_init (&iter, self->remotes);
    while (g_hash_table_iter_next (&iter, NULL, (gpointer *)&place)) {
        /* Start a new transfer operation between the two places */
        seahorse_progress_prep_and_begin (cancellable,
                                          GINT_TO_POINTER (closure->num_transfers), NULL);
        seahorse_transfer_keyids_async (SEAHORSE_SERVER_SOURCE (place),
                                        to, keyids, cancellable,
                                        on_source_transfer_ready, g_object_ref (task));
        closure->num_transfers++;
    }

    if (closure->num_transfers == 0)
        g_task_return_boolean (task, TRUE);
}

gboolean
seahorse_pgp_backend_retrieve_finish (SeahorsePgpBackend *self,
                                      GAsyncResult *result,
                                      GError **error)
{
    g_return_val_if_fail (SEAHORSE_PGP_IS_BACKEND (self), FALSE);
    g_return_val_if_fail (g_task_is_valid (result, self), FALSE);

    return g_task_propagate_boolean (G_TASK (result), error);
}

#endif /* WITH_KEYSERVER */

GList *
seahorse_pgp_backend_discover_keys (SeahorsePgpBackend *self,
                                    const gchar **keyids,
                                    GCancellable *cancellable)
{
    GList *robjects = NULL;
    SeahorseGpgmeKey *key;
    SeahorseObject *object;
    g_autoptr(GPtrArray) todiscover = NULL;
    gint i;

    self = self ? self : seahorse_pgp_backend_get ();
    g_return_val_if_fail (SEAHORSE_PGP_IS_BACKEND (self), NULL);

    todiscover = g_ptr_array_new ();

    /* Check all the ids */
    for (i = 0; keyids[i] != NULL; i++) {
        const gchar *keyid = keyids[i];

        /* Do we know about this object? */
        key = seahorse_gpgme_keyring_lookup (self->keyring, keyid);

        /* No such object anywhere, discover it */
        if (key == NULL) {
            g_ptr_array_add (todiscover, (gchar *)keyid);
            continue;
        }

        /* Our return value */
        robjects = g_list_prepend (robjects, key);
    }

    if (todiscover->len > 0) {
        g_ptr_array_add (todiscover, NULL);
        keyids = (const gchar **)todiscover->pdata;

#ifdef WITH_KEYSERVER
        /* Start a discover process on all todiscover */
        if (seahorse_app_settings_get_server_auto_retrieve (seahorse_app_settings_instance ()))
            seahorse_pgp_backend_retrieve_async (self, keyids, SEAHORSE_PLACE (self->keyring),
                                                 cancellable, NULL, NULL);
#endif

        /* Add unknown objects for all these */
        for (i = 0; keyids[i] != NULL; i++) {
            object = seahorse_unknown_source_add_object (self->unknown, keyids[i], cancellable);
            robjects = g_list_prepend (robjects, object);
        }
    }

    return robjects;
}
