/*
 * This file is part of GPaste.
 *
 * Copyright (c) 2010-2022, Marc-Antoine Perennou <Marc-Antoine@Perennou.com>
 */

#include <gpaste-gtk3.h>

#include <string.h>

#include <gpaste-clipboard.h>
#include <gpaste-image-item.h>
#include <gpaste-uris-item.h>

struct _GPasteClipboard
{
    GObject parent_instance;
};

enum
{
    C_OWNER_CHANGE,

    C_LAST_SIGNAL
};

typedef struct
{
    GtkClipboard   *real;
    GPasteSettings *settings;
    gchar          *text;
    gchar          *image_checksum;

    guint32         fake_event_source;
    guint64         c_signals[C_LAST_SIGNAL];
} GPasteClipboardPrivate;

G_PASTE_DEFINE_TYPE_WITH_PRIVATE (Clipboard, clipboard, G_TYPE_OBJECT)

enum
{
    OWNER_CHANGE,

    LAST_SIGNAL
};

static guint64 signals[LAST_SIGNAL] = { 0 };

typedef void (*GPasteClipboardTextCallback)  (GPasteClipboard *self,
                                              const gchar     *text,
                                              gpointer         user_data);

typedef void (*GPasteClipboardImageCallback) (GPasteClipboard *self,
                                              GdkPixbuf       *image,
                                              gpointer         user_data);

/**
 * g_paste_clipboard_is_clipboard:
 * @self: a #GPasteClipboard instance
 *
 * Get whether this #GPasteClipboard is a clipboard or not (primary selection)
 *
 * Returns: %TRUE if this #GPasteClipboard is a clipboard
 */
G_PASTE_VISIBLE gboolean
g_paste_clipboard_is_clipboard (const GPasteClipboard *self)
{
    g_return_val_if_fail (_G_PASTE_IS_CLIPBOARD (self), FALSE);

    const GPasteClipboardPrivate *priv = _g_paste_clipboard_get_instance_private (self);

    return gtk_clipboard_get_selection (priv->real) == GDK_SELECTION_CLIPBOARD;
}

/**
 * g_paste_clipboard_get_text:
 * @self: a #GPasteClipboard instance
 *
 * Get the text stored in the #GPasteClipboard
 *
 * Returns: read-only string containing the text or NULL
 */
G_PASTE_VISIBLE const gchar *
g_paste_clipboard_get_text (const GPasteClipboard *self)
{
    g_return_val_if_fail (_G_PASTE_IS_CLIPBOARD (self), NULL);

    const GPasteClipboardPrivate *priv = _g_paste_clipboard_get_instance_private (self);

    return priv->text;
}

static const gchar *
_g_paste_clipboard_private_target_name (const GPasteClipboardPrivate *priv)
{
    GdkAtom target = gtk_clipboard_get_selection (priv->real);

    if (target == GDK_SELECTION_CLIPBOARD)
        return "CLIPBOARD";
    else if (target == GDK_SELECTION_PRIMARY)
        return "PRIMARY";
    else
        return "UNKNOWN";
}

static void
g_paste_clipboard_private_set_text (GPasteClipboardPrivate *priv,
                                    const gchar            *text)
{
    g_free (priv->text);
    g_free (priv->image_checksum);

    g_debug("%s: set text", _g_paste_clipboard_private_target_name (priv));

    priv->text = g_strdup (text);
    priv->image_checksum = NULL;
}

typedef struct {
    GPasteClipboard            *self;
    GPasteClipboardTextCallback callback;
    gpointer                    user_data;
} GPasteClipboardTextCallbackData;

static void
g_paste_clipboard_on_text_ready (GtkClipboard *clipboard G_GNUC_UNUSED,
                                 const gchar  *text,
                                 gpointer      user_data)
{
    g_autofree GPasteClipboardTextCallbackData *data = user_data;
    GPasteClipboard *self = data->self;

    if (!text)
    {
        if (data->callback)
            data->callback (self, NULL, data->user_data);
        return;
    }

    GPasteClipboardPrivate *priv = g_paste_clipboard_get_instance_private (self);
    GPasteSettings *settings = priv->settings;
    g_autofree gchar *stripped = g_strstrip (g_strdup (text));
    gboolean trim_items = g_paste_settings_get_trim_items (settings);
    const gchar *to_add = trim_items ? stripped : text;
    guint64 length = strlen (to_add);

    if (length < g_paste_settings_get_min_text_item_size (settings) ||
        length > g_paste_settings_get_max_text_item_size (settings))
    {
        if (data->callback)
            data->callback (self, NULL, data->user_data);
        return;
    }
    if (priv->text && g_paste_str_equal (priv->text, to_add))
    {
        if (data->callback)
            data->callback (self, NULL, data->user_data);
        return;
    }

    if (trim_items &&
        gtk_clipboard_get_selection (priv->real) == GDK_SELECTION_CLIPBOARD &&
        !g_paste_str_equal (text, stripped))
            g_paste_clipboard_select_text (self, stripped);
    else
        g_paste_clipboard_private_set_text (priv, to_add);

    if (data->callback)
        data->callback (self, priv->text, data->user_data);
}

static void
g_paste_clipboard_set_text (GPasteClipboard            *self,
                            GPasteClipboardTextCallback callback,
                            gpointer                    user_data)
{
    g_return_if_fail (_G_PASTE_IS_CLIPBOARD (self));

    const GPasteClipboardPrivate *priv = _g_paste_clipboard_get_instance_private (self);
    GPasteClipboardTextCallbackData *data = g_new (GPasteClipboardTextCallbackData, 1);

    data->self = self;
    data->callback = callback;
    data->user_data = user_data;

    gtk_clipboard_request_text (priv->real,
                                g_paste_clipboard_on_text_ready,
                                data);
}

/**
 * g_paste_clipboard_select_text:
 * @self: a #GPasteClipboard instance
 * @text: the text to select
 *
 * Put the text into the #GPasteClipbaord and the intern GtkClipboard
 */
G_PASTE_VISIBLE void
g_paste_clipboard_select_text (GPasteClipboard *self,
                               const gchar     *text)
{
    g_return_if_fail (_G_PASTE_IS_CLIPBOARD (self));
    g_return_if_fail (text);
    g_return_if_fail (g_utf8_validate (text, -1, NULL));

    GPasteClipboardPrivate *priv = g_paste_clipboard_get_instance_private (self);

    g_debug("%s: select text", _g_paste_clipboard_private_target_name (priv));

    /* Avoid cycling twice as gtk_clipboard_set_text will make the clipboards manager react */
    g_paste_clipboard_private_set_text (priv, text);
    gtk_clipboard_set_text (priv->real, text, -1);
}

static void
g_paste_clipboard_sync_ready (GtkClipboard *clipboard G_GNUC_UNUSED,
                              const gchar  *text,
                              gpointer      user_data)
{
    if (text)
        g_paste_clipboard_select_text (user_data, text);
}

/**
 * g_paste_clipboard_sync_text:
 * @self: the source #GPasteClipboard instance
 * @other: the target #GPasteClipboard instance
 *
 * Synchronise the text between two clipboards
 */
G_PASTE_VISIBLE void
g_paste_clipboard_sync_text (const GPasteClipboard *self,
                             GPasteClipboard       *other)
{
    g_return_if_fail (_G_PASTE_IS_CLIPBOARD (self));
    g_return_if_fail (_G_PASTE_IS_CLIPBOARD (other));

    const GPasteClipboardPrivate *priv = _g_paste_clipboard_get_instance_private (self);

    gtk_clipboard_request_text (priv->real, g_paste_clipboard_sync_ready, other);
}

static guchar *
copy_str_as_uchars (const gchar *str,
                    guint64      length)
{
    guchar *data = g_new (guchar, length);
    for (guint64 i = 0; i < length; ++i)
        data[i] = (guchar) str[i];
    return data;
}

static void
_get_clipboard_data_from_special_atom (GtkSelectionData *selection_data,
                                       const GPasteItem *item,
                                       GPasteSpecialAtom atom)
{
    if (atom >= G_PASTE_SPECIAL_ATOM_FIRST && atom < G_PASTE_SPECIAL_ATOM_LAST)
    {
        g_autofree guchar *data = NULL;
        gsize length = 0;
        const gchar *str = g_paste_item_get_special_value (item, atom);

        if (str)
        {
            data = g_base64_decode (str, &length);
        }
        else
        {
            str = g_paste_item_get_value (item);
            length = strlen (str);
            data = copy_str_as_uchars (str, length);
        }

        gtk_selection_data_set (selection_data, g_paste_special_atom_get (atom), 8, data, length);
    }
}

static void
g_paste_clipboard_get_clipboard_data (GtkClipboard     *clipboard G_GNUC_UNUSED,
                                      GtkSelectionData *selection_data,
                                      guint32           info      G_GNUC_UNUSED,
                                      gpointer          user_data_or_owner)
{
    g_return_if_fail (_G_PASTE_IS_ITEM (user_data_or_owner));

    GPasteItem *item = G_PASTE_ITEM (user_data_or_owner);

    GdkAtom target = gtk_selection_data_get_target (selection_data);
    GdkAtom targets[1] = { target };

    if (_G_PASTE_IS_IMAGE_ITEM (item))
    {
        if (gtk_targets_include_image (targets, 1, TRUE))
            gtk_selection_data_set_pixbuf (selection_data, g_paste_image_item_get_image (G_PASTE_IMAGE_ITEM (item)));
        return;
    }
    else if (_G_PASTE_IS_URIS_ITEM (item))
    {
        if (gtk_targets_include_uri (targets, 1))
        {
            const gchar * const *uris = g_paste_uris_item_get_uris (G_PASTE_URIS_ITEM (item));

            gtk_selection_data_set_uris (selection_data, (GStrv) uris);
            return;
        }
    }

    for (GPasteSpecialAtom a = G_PASTE_SPECIAL_ATOM_FIRST; a < G_PASTE_SPECIAL_ATOM_LAST; ++a)
    {
        if (target == g_paste_special_atom_get (a))
        {
            _get_clipboard_data_from_special_atom (selection_data, item, a);
            return;
        }
    }

    /* The content is requested as text */
    if (gtk_targets_include_text (targets, 1))
        gtk_selection_data_set_text (selection_data, g_paste_item_get_real_value (item), -1);
}

static void
g_paste_clipboard_clear_clipboard_data (GtkClipboard *clipboard G_GNUC_UNUSED,
                                        gpointer      user_data_or_owner)
{
    g_object_unref (user_data_or_owner);
}

/**
 * g_paste_clipboard_clear:
 * @self: a #GPasteClipboard instance
 *
 * Clears the content of the clipboard
 */
G_PASTE_VISIBLE void
g_paste_clipboard_clear (GPasteClipboard *self)
{
    g_return_if_fail (_G_PASTE_IS_CLIPBOARD (self));

    GPasteClipboardPrivate *priv = g_paste_clipboard_get_instance_private (self);

    /* We're already clear, don't enter an infinite event loop */
    if (!priv->text && !priv->image_checksum)
        return;

    g_debug("%s: clear", _g_paste_clipboard_private_target_name (priv));

    g_clear_pointer (&priv->text, g_free);
    g_clear_pointer (&priv->image_checksum, g_free);

    gtk_clipboard_clear (priv->real);
}

/**
 * g_paste_clipboard_store:
 * @self: a #GPasteClipboard instance
 *
 * Store the contents of the clipboard before exiting
 */
G_PASTE_VISIBLE void
g_paste_clipboard_store (GPasteClipboard *self)
{
    g_return_if_fail (_G_PASTE_IS_CLIPBOARD (self));

    GPasteClipboardPrivate *priv = g_paste_clipboard_get_instance_private (self);

    g_debug("%s: store", _g_paste_clipboard_private_target_name (priv));

    gtk_clipboard_store (priv->real);
}

/**
 * g_paste_clipboard_get_image_checksum:
 * @self: a #GPasteClipboard instance
 *
 * Get the checksum of the image stored in the #GPasteClipboard
 *
 * Returns: read-only string containing the checksum of the image stored in the #GPasteClipboard or NULL
 */
G_PASTE_VISIBLE const gchar *
g_paste_clipboard_get_image_checksum (const GPasteClipboard *self)
{
    g_return_val_if_fail (_G_PASTE_IS_CLIPBOARD (self), NULL);

    const GPasteClipboardPrivate *priv = _g_paste_clipboard_get_instance_private (self);

    return priv->image_checksum;
}

static void
g_paste_clipboard_private_set_image_checksum (GPasteClipboardPrivate *priv,
                                              const gchar            *image_checksum)
{
    g_free (priv->text);
    g_free (priv->image_checksum);

    priv->text = NULL;
    priv->image_checksum = g_strdup (image_checksum);
}

static void
g_paste_clipboard_private_select_image (GPasteClipboardPrivate *priv,
                                        GdkPixbuf              *image,
                                        const gchar            *checksum)
{
    g_return_if_fail (GDK_IS_PIXBUF (image));

    GtkClipboard *real = priv->real;

    g_debug("%s: select image", _g_paste_clipboard_private_target_name (priv));

    g_paste_clipboard_private_set_image_checksum (priv, checksum);
    gtk_clipboard_set_image (real, image);
}

typedef struct {
    GPasteClipboard             *self;
    GPasteClipboardImageCallback callback;
    gpointer                     user_data;
} GPasteClipboardImageCallbackData;

static void
g_paste_clipboard_on_image_ready (GtkClipboard *clipboard G_GNUC_UNUSED,
                                  GdkPixbuf    *image,
                                  gpointer      user_data)
{
    g_autofree GPasteClipboardImageCallbackData *data = user_data;
    GPasteClipboard *self = data->self;

    if (!image)
    {
        if (data->callback)
            data->callback (self, NULL, data->user_data);
        return;
    }

    GPasteClipboardPrivate *priv = g_paste_clipboard_get_instance_private (self);
    g_autofree gchar *checksum = g_paste_gtk_util_compute_checksum (image);

    if (g_paste_str_equal (checksum, priv->image_checksum))
        image = NULL;
    else
        g_paste_clipboard_private_select_image (priv, image, checksum);

    if (data->callback)
        data->callback (self, image, data->user_data);
}

static void
g_paste_clipboard_set_image (GPasteClipboard             *self,
                             GPasteClipboardImageCallback callback,
                             gpointer                     user_data)
{
    g_return_if_fail (_G_PASTE_IS_CLIPBOARD (self));

    const GPasteClipboardPrivate *priv = _g_paste_clipboard_get_instance_private (self);
    GPasteClipboardImageCallbackData *data = g_new (GPasteClipboardImageCallbackData, 1);

    data->self = self;
    data->callback = callback;
    data->user_data = user_data;

    gtk_clipboard_request_image (priv->real,
                                 g_paste_clipboard_on_image_ready,
                                 data);
}

typedef void (*GPasteClipboardSpecialAtomCallback) (GPasteClipboard  *self,
                                                    GPasteSpecialAtom atom,
                                                    const guchar     *data,
                                                    gsize             length,
                                                    gpointer          user_data);

typedef struct {
    GPasteClipboard                   *self;
    GPasteSpecialAtom                  atom;
    GPasteClipboardSpecialAtomCallback callback;
    gpointer                           user_data;
} GPasteClipboardSpecialAtomData;

static void
g_paste_clipboard_on_special_atom_ready (GtkClipboard     *clipboard G_GNUC_UNUSED,
                                         GtkSelectionData *selection_data,
                                         gpointer          user_data)
{
    g_autofree GPasteClipboardSpecialAtomData *data = user_data;
    gint length;
    const guchar *raw = gtk_selection_data_get_data_with_length (selection_data, &length);

    if (data->callback)
        data->callback (data->self, data->atom, raw, (gsize) (length > 0 ? length : 0), data->user_data);
}

static void
g_paste_clipboard_fetch_special_atom (GPasteClipboard                   *self,
                                      GPasteSpecialAtom                  atom,
                                      GPasteClipboardSpecialAtomCallback callback,
                                      gpointer                           user_data)
{
    g_return_if_fail (_G_PASTE_IS_CLIPBOARD (self));

    const GPasteClipboardPrivate *priv = _g_paste_clipboard_get_instance_private (self);
    GPasteClipboardSpecialAtomData *data = g_new (GPasteClipboardSpecialAtomData, 1);

    data->self = self;
    data->atom = atom;
    data->callback = callback;
    data->user_data = user_data;

    gtk_clipboard_request_contents (priv->real,
                                    g_paste_special_atom_get (atom),
                                    g_paste_clipboard_on_special_atom_ready,
                                    data);
}

typedef struct {
    GPasteClipboard              *self;
    GPasteClipboardUpdateCallback callback;
    gpointer                      user_data;
    gint                          pending;
    const gchar                  *text;
    GdkPixbuf                    *image;
    gboolean                      uris_available;
    gboolean                      fallback;
    guchar                       *special_atom_data[G_PASTE_SPECIAL_ATOM_LAST];
    gsize                         special_atom_size[G_PASTE_SPECIAL_ATOM_LAST];
} GPasteClipboardUpdateData;

static void
g_paste_clipboard_update_maybe_done (GPasteClipboardUpdateData *data)
{
    if (--data->pending > 0)
        return;

    GPasteItem *item = NULL;

    if (data->text)
    {
        if (data->uris_available)
            item = G_PASTE_ITEM (g_paste_uris_item_new (data->text));
        else
            item = G_PASTE_ITEM (g_paste_text_item_new (data->text));

        for (GPasteSpecialAtom atom = G_PASTE_SPECIAL_ATOM_FIRST; atom < G_PASTE_SPECIAL_ATOM_LAST; ++atom)
        {
            if (data->special_atom_data[atom])
            {
                g_autofree gchar *val = g_base64_encode (data->special_atom_data[atom], data->special_atom_size[atom]);
                g_autofree GPasteSpecialValue *v = g_new (GPasteSpecialValue, 1);
                v->mime = atom;
                v->data = val;
                g_paste_item_add_special_value (item, v);
            }
        }
    }
    else if (data->image)
    {
        item = G_PASTE_ITEM (g_paste_image_item_new (data->image));
    }

    if (data->callback)
        data->callback (data->self, item, data->fallback, data->user_data);

    for (GPasteSpecialAtom atom = G_PASTE_SPECIAL_ATOM_FIRST; atom < G_PASTE_SPECIAL_ATOM_LAST; ++atom)
        g_free (data->special_atom_data[atom]);
    g_free (data);
}

static void
g_paste_clipboard_update_on_text_ready (GPasteClipboard *self G_GNUC_UNUSED,
                                        const gchar     *text,
                                        gpointer         user_data)
{
    GPasteClipboardUpdateData *data = user_data;
    data->text = text;
    g_paste_clipboard_update_maybe_done (data);
}

static void
g_paste_clipboard_update_on_image_ready (GPasteClipboard *self G_GNUC_UNUSED,
                                         GdkPixbuf       *image,
                                         gpointer         user_data)
{
    GPasteClipboardUpdateData *data = user_data;
    data->image = image;
    g_paste_clipboard_update_maybe_done (data);
}

static void
g_paste_clipboard_update_on_special_atom_ready (GPasteClipboard  *self G_GNUC_UNUSED,
                                                GPasteSpecialAtom atom,
                                                const guchar     *raw,
                                                gsize             length,
                                                gpointer          user_data)
{
    GPasteClipboardUpdateData *data = user_data;

    if (raw && length > 0)
    {
        data->special_atom_data[atom] = g_memdup2 (raw, length);
        data->special_atom_size[atom] = length;
    }

    g_paste_clipboard_update_maybe_done (data);
}

static void
g_paste_clipboard_update_targets_ready (GtkClipboard     *real G_GNUC_UNUSED,
                                        GtkSelectionData *_targets,
                                        gpointer          user_data)
{
    GPasteClipboardUpdateData *data = user_data;
    GPasteClipboard *self = data->self;
    const GPasteClipboardPrivate *priv = _g_paste_clipboard_get_instance_private (self);

    if (gtk_selection_data_get_length (_targets) >= 0)
    {
        g_autofree GdkAtom *targets = NULL;
        gint n_targets;

        gtk_selection_data_get_targets (_targets, &targets, &n_targets);
        data->uris_available = gtk_targets_include_uri (targets, n_targets);

        gboolean has_text = data->uris_available || gtk_targets_include_text (targets, n_targets);
        gboolean has_image = !has_text &&
                             g_paste_settings_get_images_support (priv->settings) &&
                             gtk_targets_include_image (targets, n_targets, FALSE);
        gboolean atom_available[G_PASTE_SPECIAL_ATOM_LAST] = { FALSE };

        if (has_text && g_paste_settings_get_rich_text_support (priv->settings))
        {
            for (GPasteSpecialAtom atom = G_PASTE_SPECIAL_ATOM_FIRST; atom < G_PASTE_SPECIAL_ATOM_LAST; ++atom)
            {
                for (gint i = 0; i < n_targets; ++i)
                {
                    if (targets[i] == g_paste_special_atom_get (atom))
                    {
                        atom_available[atom] = TRUE;
                        ++data->pending;
                        break;
                    }
                }
            }
        }

        if (has_text || has_image)
            ++data->pending;

        if (has_text)
            g_paste_clipboard_set_text (self, g_paste_clipboard_update_on_text_ready, data);
        else if (has_image)
            g_paste_clipboard_set_image (self, g_paste_clipboard_update_on_image_ready, data);

        for (GPasteSpecialAtom atom = G_PASTE_SPECIAL_ATOM_FIRST; atom < G_PASTE_SPECIAL_ATOM_LAST; ++atom)
        {
            if (atom_available[atom])
                g_paste_clipboard_fetch_special_atom (self, atom, g_paste_clipboard_update_on_special_atom_ready, data);
        }
    }
    else
    {
        data->fallback = TRUE;
        ++data->pending;
        g_paste_clipboard_set_text (self, g_paste_clipboard_update_on_text_ready, data);
    }

    g_paste_clipboard_update_maybe_done (data);
}

/**
 * g_paste_clipboard_update:
 * @self: a #GPasteClipboard instance
 * @callback: (scope async): the callback to be called when the clipboard content is ready
 * @user_data: user data to pass to @callback
 *
 * Read the current clipboard content and update the internal cache.
 * The callback receives a newly created #GPasteItem (or NULL if unchanged or empty)
 * and a fallback flag that is TRUE when the clipboard was empty and no content could be retrieved.
 */
G_PASTE_VISIBLE void
g_paste_clipboard_update (GPasteClipboard              *self,
                          GPasteClipboardUpdateCallback callback,
                          gpointer                      user_data)
{
    g_return_if_fail (_G_PASTE_IS_CLIPBOARD (self));

    const GPasteClipboardPrivate *priv = _g_paste_clipboard_get_instance_private (self);
    GPasteClipboardUpdateData *data = g_new0 (GPasteClipboardUpdateData, 1);

    data->self = self;
    data->callback = callback;
    data->user_data = user_data;
    data->pending = 1;

    gtk_clipboard_request_contents (priv->real,
                                    gdk_atom_intern_static_string ("TARGETS"),
                                    g_paste_clipboard_update_targets_ready,
                                    data);
}

/**
 * g_paste_clipboard_select_item:
 * @self: a #GPasteClipboard instance
 * @item: the item to select
 *
 * Put the value of the item into the #GPasteClipbaord and the intern GtkClipboard
 *
 * Returns: %FALSE if the item was invalid, %TRUE otherwise
 */
G_PASTE_VISIBLE gboolean
g_paste_clipboard_select_item (GPasteClipboard *self,
                               GPasteItem      *item)
{
    g_return_val_if_fail (_G_PASTE_IS_CLIPBOARD (self), FALSE);
    g_return_val_if_fail (_G_PASTE_IS_ITEM (item), FALSE);

    GPasteClipboardPrivate *priv = g_paste_clipboard_get_instance_private (self);
    GtkClipboard *real = priv->real;
    g_autoptr (GtkTargetList) target_list = gtk_target_list_new (NULL, 0);

    g_debug("%s: select item", _g_paste_clipboard_private_target_name (priv));

    if (_G_PASTE_IS_IMAGE_ITEM (item))
    {
        gtk_target_list_add_image_targets (target_list, 0, FALSE);
        g_paste_clipboard_private_set_image_checksum (priv, g_paste_image_item_get_checksum (G_PASTE_IMAGE_ITEM (item)));
    }
    else
    {
        gtk_target_list_add_text_targets (target_list, 0);
        if (_G_PASTE_IS_URIS_ITEM (item))
            gtk_target_list_add_uri_targets (target_list, 0);
        g_paste_clipboard_private_set_text (priv, g_paste_item_get_real_value (item));
    }

    for (const GSList *sv = g_paste_item_get_special_values (item); sv; sv = sv->next)
    {
        const GPasteSpecialValue *v = sv->data;
        gtk_target_list_add (target_list, g_paste_special_atom_get (v->mime), 0, 0);
    }

    gint32 n_targets;
    GtkTargetEntry *targets = gtk_target_table_new_from_list (target_list, &n_targets);
    gtk_clipboard_set_with_owner (real,
                                  targets,
                                  n_targets,
                                  g_paste_clipboard_get_clipboard_data,
                                  g_paste_clipboard_clear_clipboard_data,
                                  g_object_ref ((GObject *) item));
    gtk_target_table_free (targets, n_targets);

    return TRUE;
}

/**
 * g_paste_clipboard_ensure_not_empty:
 * @self: a #GPasteClipboard instance
 * @history: a #GPasteHistory instance
 *
 * Ensure the clipboard has some contents (as long as the history's not empty)
 */
G_PASTE_VISIBLE void
g_paste_clipboard_ensure_not_empty (GPasteClipboard *self,
                                    GPasteHistory   *history)
{
    g_return_if_fail (_G_PASTE_IS_CLIPBOARD (self));
    g_return_if_fail (_G_PASTE_IS_HISTORY (history));

    const GPasteClipboardPrivate *priv = _g_paste_clipboard_get_instance_private (self);

    if (priv->text || priv->image_checksum)
        return;

    const GList *hist = g_paste_history_get_history (history);

    if (hist)
    {
        GPasteItem *item = hist->data;

        if (!g_paste_clipboard_select_item (self, item))
            g_paste_history_remove (history, 0);
    }
}

static void
g_paste_clipboard_owner_change (GtkClipboard        *clipboard G_GNUC_UNUSED,
                                GdkEventOwnerChange *event,
                                gpointer             user_data)
{
    GPasteClipboard *self = user_data;
    GPasteClipboardPrivate *priv = g_paste_clipboard_get_instance_private (self);

    g_debug("%s: owner change", _g_paste_clipboard_private_target_name (priv));

    g_signal_emit (self,
		   signals[OWNER_CHANGE],
                   0, /* detail */
                   event,
                   NULL);
}

static void
g_paste_clipboard_fake_event_finish_text (GtkClipboard *clipboard G_GNUC_UNUSED,
                                          const gchar  *text,
                                          gpointer      user_data)
{
    GPasteClipboard *self = user_data;
    const GPasteClipboardPrivate *priv = _g_paste_clipboard_get_instance_private (self);

    if (!g_paste_str_equal (text, priv->text))
        g_paste_clipboard_owner_change (NULL, NULL, self);
}

static void
g_paste_clipboard_fake_event_finish_image (GtkClipboard *clipboard G_GNUC_UNUSED,
                                           GdkPixbuf    *image,
                                           gpointer      user_data)
{
    if (!image)
        return;

    GPasteClipboard *self = user_data;
    const GPasteClipboardPrivate *priv = _g_paste_clipboard_get_instance_private (self);
    g_autofree gchar *checksum = g_paste_gtk_util_compute_checksum (image);

    if (!g_paste_str_equal (checksum, priv->image_checksum))
        g_paste_clipboard_owner_change (NULL, NULL, self);
}

static gboolean
g_paste_clipboard_fake_event (gpointer user_data)
{
    GPasteClipboard *self = user_data;
    const GPasteClipboardPrivate *priv = _g_paste_clipboard_get_instance_private (self);

    if (priv->text)
        gtk_clipboard_request_text (priv->real, g_paste_clipboard_fake_event_finish_text, self);
    else if (priv->image_checksum)
        gtk_clipboard_request_image (priv->real, g_paste_clipboard_fake_event_finish_image, self);
    else
        g_paste_clipboard_owner_change (NULL, NULL, self);

    return G_SOURCE_CONTINUE;
}

static void
g_paste_clipboard_dispose (GObject *object)
{
    GPasteClipboardPrivate *priv = g_paste_clipboard_get_instance_private (G_PASTE_CLIPBOARD (object));

    if (priv->settings)
    {
        if (priv->fake_event_source)
        {
            g_source_remove (priv->fake_event_source);
            priv->fake_event_source = 0;
        }
        g_signal_handler_disconnect (priv->real, priv->c_signals[C_OWNER_CHANGE]);
        g_clear_object (&priv->settings);
    }

    G_OBJECT_CLASS (g_paste_clipboard_parent_class)->dispose (object);
}

static void
g_paste_clipboard_finalize (GObject *object)
{
    const GPasteClipboardPrivate *priv = _g_paste_clipboard_get_instance_private (G_PASTE_CLIPBOARD (object));

    g_free (priv->text);
    g_free (priv->image_checksum);

    G_OBJECT_CLASS (g_paste_clipboard_parent_class)->finalize (object);
}

static void
g_paste_clipboard_class_init (GPasteClipboardClass *klass)
{
    GObjectClass *object_class = G_OBJECT_CLASS (klass);

    object_class->dispose = g_paste_clipboard_dispose;
    object_class->finalize = g_paste_clipboard_finalize;

    /**
     * GPasteClipboard::owner-change:
     * @clipboard: the object on which the signal was emitted
     * @event: (type Gdk.EventOwnerChange): the @GdkEventOwnerChange event
     *
     * The "owner-change" signal is emitted when GPaste receives an
     * event that indicates that the ownership of the selection
     * associated with @clipboard has changed.
     */
    signals[OWNER_CHANGE] = g_signal_new ("owner-change",
                                          G_PASTE_TYPE_CLIPBOARD,
                                          G_SIGNAL_RUN_FIRST,
                                          0,    /* class offset     */
                                          NULL, /* accumulator      */
                                          NULL, /* accumulator data */
                                          g_cclosure_marshal_VOID__BOXED,
                                          G_TYPE_NONE,
                                          1,
                                          GDK_TYPE_EVENT | G_SIGNAL_TYPE_STATIC_SCOPE);
}

static void
g_paste_clipboard_init (GPasteClipboard *self G_GNUC_UNUSED)
{
}

static GPasteClipboard *
_g_paste_clipboard_new (GPasteSettings *settings,
                        GdkAtom         target)
{
    GPasteClipboard *self = g_object_new (G_PASTE_TYPE_CLIPBOARD, NULL);
    GPasteClipboardPrivate *priv = g_paste_clipboard_get_instance_private (self);
    GtkClipboard *real = priv->real = gtk_clipboard_get (target);

    priv->settings = g_object_ref (settings);
    priv->c_signals[C_OWNER_CHANGE] = g_signal_connect (real,
                                                        "owner-change",
                                                        G_CALLBACK (g_paste_clipboard_owner_change),
                                                        self);

    if (!gdk_display_request_selection_notification (gdk_display_get_default (), target))
    {
        g_warning ("Selection notification not supported, using active poll");
        priv->fake_event_source = g_timeout_add_seconds (1, g_paste_clipboard_fake_event, self);
        g_source_set_name_by_id (priv->fake_event_source, "[GPaste] clipboard fake events");
    }

    return self;
}

/**
 * g_paste_clipboard_new_clipboard:
 * @settings: a #GPasteSettings instance
 *
 * Create a new instance of #GPasteClipboard
 *
 * Returns: a newly allocated #GPasteClipboard
 *          free it with g_object_unref
 */
G_PASTE_VISIBLE GPasteClipboard *
g_paste_clipboard_new_clipboard (GPasteSettings *settings)
{
    g_return_val_if_fail (_G_PASTE_IS_SETTINGS (settings), NULL);

    return _g_paste_clipboard_new (settings, GDK_SELECTION_CLIPBOARD);
}

/**
 * g_paste_clipboard_new_primary:
 * @settings: a #GPasteSettings instance
 *
 * Create a new instance of #GPasteClipboard
 *
 * Returns: a newly allocated #GPasteClipboard
 *          free it with g_object_unref
 */
G_PASTE_VISIBLE GPasteClipboard *
g_paste_clipboard_new_primary (GPasteSettings *settings)
{
    g_return_val_if_fail (_G_PASTE_IS_SETTINGS (settings), NULL);

    return _g_paste_clipboard_new (settings, GDK_SELECTION_PRIMARY);
}
