/* GIO - GLib Output, Output and Streaming Library
 * 
 * Copyright (C) 2017 Red Hat, Inc.
 *
 * This library 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 library 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 library; if not, see <http://www.gnu.org/licenses/>.
 *
 * Author: Benjamin Otte <otte@gnome.org>
 *         Christian Kellner <gicmo@gnome.org> 
 */

#include "config.h"

#include "gdkselectionoutputstream-x11.h"

#include "gdkclipboard-x11.h"
#include "gdkdisplay-x11.h"
#include "gdkintl.h"
#include "gdktextlistconverter-x11.h"
#include "gdkx11display.h"
#include "gdkx11property.h"
#include "gdkx11surface.h"

typedef struct _GdkX11PendingSelectionNotify GdkX11PendingSelectionNotify;
typedef struct _GdkX11SelectionOutputStreamPrivate  GdkX11SelectionOutputStreamPrivate;

struct _GdkX11SelectionOutputStreamPrivate {
  GdkDisplay *display;
  GdkX11PendingSelectionNotify *notify;
  Window xwindow;
  char *selection;
  Atom xselection;
  char *target;
  Atom xtarget;
  char *property;
  Atom xproperty;
  char *type;
  Atom xtype;
  int format;
  gulong timestamp;

  GMutex mutex;
  GCond cond;
  GByteArray *data;
  guint flush_requested : 1;

  GTask *pending_task;

  guint incr : 1;
  guint delete_pending : 1;
};

struct _GdkX11PendingSelectionNotify
{
  gsize n_pending;
  
  XSelectionEvent xevent;
};

G_DEFINE_TYPE_WITH_PRIVATE (GdkX11SelectionOutputStream, gdk_x11_selection_output_stream, G_TYPE_OUTPUT_STREAM);

static GdkX11PendingSelectionNotify *
gdk_x11_pending_selection_notify_new (Window window,
                                      Atom   selection,
                                      Atom   target,
                                      Atom   property,
                                      Time   timestamp)
{
  GdkX11PendingSelectionNotify *pending;

  pending = g_slice_new0 (GdkX11PendingSelectionNotify);
  pending->n_pending = 1;

  pending->xevent.type = SelectionNotify;
  pending->xevent.serial = 0;
  pending->xevent.send_event = True;
  pending->xevent.requestor = window;
  pending->xevent.selection = selection;
  pending->xevent.target = target;
  pending->xevent.property = property;
  pending->xevent.time = timestamp;

  return pending;
}

static void
gdk_x11_pending_selection_notify_free (GdkX11PendingSelectionNotify *notify)
{
  g_slice_free (GdkX11PendingSelectionNotify, notify);
}

static void
gdk_x11_pending_selection_notify_require (GdkX11PendingSelectionNotify *notify,
                                          guint                         n_sends)
{
  notify->n_pending += n_sends;
}

static void
gdk_x11_pending_selection_notify_send (GdkX11PendingSelectionNotify *notify,
                                       GdkDisplay                   *display,
                                       gboolean                      success)
{
  Display *xdisplay;
  int error;

  notify->n_pending--;
  if (notify->n_pending)
    {
      GDK_DISPLAY_NOTE (display, SELECTION, g_printerr ("%s:%s: not sending SelectionNotify yet, %zu streams still pending\n",
                                      gdk_x11_get_xatom_name_for_display (display, notify->xevent.selection),
                                      gdk_x11_get_xatom_name_for_display (display, notify->xevent.target),
                                      notify->n_pending));
      return;
    }

  GDK_DISPLAY_NOTE (display, SELECTION, g_printerr ("%s:%s: sending SelectionNotify reporting %s\n",
                                  gdk_x11_get_xatom_name_for_display (display, notify->xevent.selection),
                                  gdk_x11_get_xatom_name_for_display (display, notify->xevent.target),
                                  success ? "success" : "failure"));
  if (!success)
    notify->xevent.property = None;

  xdisplay = gdk_x11_display_get_xdisplay (display);

  gdk_x11_display_error_trap_push (display);

  if (XSendEvent (xdisplay, notify->xevent.requestor, False, NoEventMask, (XEvent*) &notify->xevent) == 0)
    {
      GDK_DISPLAY_NOTE (display, SELECTION, g_printerr ("%s:%s: failed to XSendEvent()\n",
                                      gdk_x11_get_xatom_name_for_display (display, notify->xevent.selection),
                                      gdk_x11_get_xatom_name_for_display (display, notify->xevent.target)));
    }
  XSync (xdisplay, False);

  error = gdk_x11_display_error_trap_pop (display);
  if (error != Success)
    {
      GDK_DISPLAY_NOTE (display, SELECTION, g_printerr ("%s:%s: X error during write: %d\n",
                                      gdk_x11_get_xatom_name_for_display (display, notify->xevent.selection),
                                      gdk_x11_get_xatom_name_for_display (display, notify->xevent.target),
                                      error));
    }

  gdk_x11_pending_selection_notify_free (notify);
}

static gboolean
gdk_x11_selection_output_stream_xevent (GdkDisplay   *display,
                                        const XEvent *xevent,
                                        gpointer      data);

static gboolean
gdk_x11_selection_output_stream_can_flush (GdkX11SelectionOutputStream *stream)
{
  GdkX11SelectionOutputStreamPrivate *priv = gdk_x11_selection_output_stream_get_instance_private (stream);

  if (priv->delete_pending)
    return FALSE;

  return TRUE;
}

static gboolean
gdk_x11_selection_output_stream_needs_flush_unlocked (GdkX11SelectionOutputStream *stream)
{
  GdkX11SelectionOutputStreamPrivate *priv = gdk_x11_selection_output_stream_get_instance_private (stream);

  if (priv->data->len == 0 && priv->notify == NULL)
    return FALSE;

  if (g_output_stream_is_closing (G_OUTPUT_STREAM (stream)))
    return TRUE;

  if (priv->flush_requested)
    return TRUE;

  return priv->data->len >= gdk_x11_display_get_max_request_size (priv->display);
}

static gboolean
gdk_x11_selection_output_stream_needs_flush (GdkX11SelectionOutputStream *stream)
{
  GdkX11SelectionOutputStreamPrivate *priv = gdk_x11_selection_output_stream_get_instance_private (stream);

  gboolean result;

  g_mutex_lock (&priv->mutex);

  result = gdk_x11_selection_output_stream_needs_flush_unlocked (stream);

  g_mutex_unlock (&priv->mutex);

  return result;
}

static gsize
get_element_size (int format)
{
  switch (format)
    {
    case 8:
      return 1;

    case 16:
      return sizeof (short);

    case 32:
      return sizeof (long);

    default:
      g_warning ("Unknown format %u", format);
      return 1;
    }
}

static void
gdk_x11_selection_output_stream_perform_flush (GdkX11SelectionOutputStream *stream)
{
  GdkX11SelectionOutputStreamPrivate *priv = gdk_x11_selection_output_stream_get_instance_private (stream);
  Display *xdisplay;
  gsize element_size, n_elements;
  int error;

  g_assert (!priv->delete_pending);

  xdisplay = gdk_x11_display_get_xdisplay (priv->display);

  /* We operate on a foreign window, better guard against catastrophe */
  gdk_x11_display_error_trap_push (priv->display);

  g_mutex_lock (&priv->mutex);

  element_size = get_element_size (priv->format);
  n_elements = priv->data->len / element_size;

  if (priv->notify && !g_output_stream_is_closing (G_OUTPUT_STREAM (stream)))
    {
      XWindowAttributes attrs;

      priv->incr = TRUE;
      GDK_DISPLAY_NOTE (priv->display, SELECTION, g_printerr ("%s:%s: initiating INCR transfer\n",
                                      priv->selection, priv->target));

      XGetWindowAttributes (xdisplay,
			    priv->xwindow,
			    &attrs);
      if (!(attrs.your_event_mask & PropertyChangeMask))
        {
          XSelectInput (xdisplay, priv->xwindow, attrs.your_event_mask | PropertyChangeMask);
        }

      XChangeProperty (GDK_DISPLAY_XDISPLAY (priv->display),
                       priv->xwindow,
                       priv->xproperty, 
                       gdk_x11_get_xatom_by_name_for_display (priv->display, "INCR"),
                       32,
                       PropModeReplace,
                       (guchar *) &(long) { n_elements },
                       1);
    }
  else
    {
      XChangeProperty (GDK_DISPLAY_XDISPLAY (priv->display),
                       priv->xwindow,
                       priv->xproperty, 
                       priv->xtype,
                       priv->format,
                       PropModeReplace,
                       priv->data->data,
                       n_elements);
      GDK_DISPLAY_NOTE (priv->display, SELECTION, g_printerr ("%s:%s: wrote %zu/%u bytes\n",
                                      priv->selection, priv->target, n_elements * element_size, priv->data->len));
      g_byte_array_remove_range (priv->data, 0, n_elements * element_size);
      if (priv->data->len < element_size)
        priv->flush_requested = FALSE;
    }

  if (priv->notify)
    {
      gdk_x11_pending_selection_notify_send (priv->notify, priv->display, TRUE);
      priv->notify = NULL;
    }

  priv->delete_pending = TRUE;
  g_cond_broadcast (&priv->cond);
  g_mutex_unlock (&priv->mutex);

  /* XXX: handle failure here and report EPIPE for future operations on the stream? */
  error = gdk_x11_display_error_trap_pop (priv->display);
  if (error != Success)
    {
      GDK_DISPLAY_NOTE (priv->display, SELECTION, g_printerr ("%s:%s: X error during write: %d\n",
                                      priv->selection, priv->target, error));
    }

  if (priv->pending_task)
    {
      g_task_return_int (priv->pending_task, GPOINTER_TO_SIZE (g_task_get_task_data (priv->pending_task)));
      g_object_unref (priv->pending_task);
      priv->pending_task = NULL;
    }
}

static gboolean
gdk_x11_selection_output_stream_invoke_flush (gpointer data)
{
  GdkX11SelectionOutputStream *stream = GDK_X11_SELECTION_OUTPUT_STREAM (data);

  if (gdk_x11_selection_output_stream_needs_flush (stream) &&
      gdk_x11_selection_output_stream_can_flush (stream))
    gdk_x11_selection_output_stream_perform_flush (stream);

  return G_SOURCE_REMOVE;
}

static gssize
gdk_x11_selection_output_stream_write (GOutputStream  *output_stream,
                                       const void     *buffer,
                                       gsize           count,
                                       GCancellable   *cancellable,
                                       GError        **error)
{
  GdkX11SelectionOutputStream *stream = GDK_X11_SELECTION_OUTPUT_STREAM (output_stream);
  GdkX11SelectionOutputStreamPrivate *priv = gdk_x11_selection_output_stream_get_instance_private (stream);

  g_mutex_lock (&priv->mutex);
  g_byte_array_append (priv->data, buffer, count);
  GDK_NOTE (SELECTION, g_printerr ("%s:%s: wrote %zu bytes, %u total now\n",
                                  priv->selection, priv->target, count, priv->data->len));
  g_mutex_unlock (&priv->mutex);

  g_main_context_invoke (NULL, gdk_x11_selection_output_stream_invoke_flush, stream);

  g_mutex_lock (&priv->mutex);
  if (gdk_x11_selection_output_stream_needs_flush_unlocked (stream))
    g_cond_wait (&priv->cond, &priv->mutex);
  g_mutex_unlock (&priv->mutex);

  return count;
}

static void
gdk_x11_selection_output_stream_write_async (GOutputStream        *output_stream,
                                             const void           *buffer,
                                             gsize                 count,
                                             int                   io_priority,
                                             GCancellable         *cancellable,
                                             GAsyncReadyCallback   callback,
                                             gpointer              user_data)
{
  GdkX11SelectionOutputStream *stream = GDK_X11_SELECTION_OUTPUT_STREAM (output_stream);
  GdkX11SelectionOutputStreamPrivate *priv = gdk_x11_selection_output_stream_get_instance_private (stream);
  GTask *task;
  
  task = g_task_new (stream, cancellable, callback, user_data);
  g_task_set_source_tag (task, gdk_x11_selection_output_stream_write_async);
  g_task_set_priority (task, io_priority);

  g_mutex_lock (&priv->mutex);
  g_byte_array_append (priv->data, buffer, count);
  GDK_NOTE (SELECTION, g_printerr ("%s:%s: async wrote %zu bytes, %u total now\n",
                                  priv->selection, priv->target, count, priv->data->len));
  g_mutex_unlock (&priv->mutex);

  if (!gdk_x11_selection_output_stream_needs_flush (stream))
    {
      g_task_return_int (task, count);
      g_object_unref (task);
      return;
    }
  else if (!gdk_x11_selection_output_stream_can_flush (stream))
    {
      g_assert (priv->pending_task == NULL);
      priv->pending_task = task;
      g_task_set_task_data (task, GSIZE_TO_POINTER (count), NULL);
      return;
    }
  else
    {
      gdk_x11_selection_output_stream_perform_flush (stream);
      g_task_return_int (task, count);
      g_object_unref (task);
      return;
    }
}

static gssize
gdk_x11_selection_output_stream_write_finish (GOutputStream  *stream,
                                              GAsyncResult   *result,
                                              GError        **error)
{
  g_return_val_if_fail (g_task_is_valid (result, stream), -1);
  g_return_val_if_fail (g_task_get_source_tag (G_TASK (result)) == gdk_x11_selection_output_stream_write_async, -1);

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

static gboolean
gdk_x11_selection_output_request_flush (GdkX11SelectionOutputStream *stream)
{
  GdkX11SelectionOutputStreamPrivate *priv = gdk_x11_selection_output_stream_get_instance_private (stream);
  gboolean needs_flush;

  g_mutex_lock (&priv->mutex);

  if (priv->data->len >= get_element_size (priv->format))
    priv->flush_requested = TRUE;

  needs_flush = gdk_x11_selection_output_stream_needs_flush_unlocked (stream);
  g_mutex_unlock (&priv->mutex);

   GDK_NOTE (SELECTION, g_printerr ("%s:%s: requested flush%s\n",
                                  priv->selection, priv->target, needs_flush ?"" : ", but not needed"));
  return needs_flush;
}

static gboolean
gdk_x11_selection_output_stream_flush (GOutputStream  *output_stream,
                                       GCancellable   *cancellable,
                                       GError        **error)
{
  GdkX11SelectionOutputStream *stream = GDK_X11_SELECTION_OUTPUT_STREAM (output_stream);
  GdkX11SelectionOutputStreamPrivate *priv = gdk_x11_selection_output_stream_get_instance_private (stream);

  if (!gdk_x11_selection_output_request_flush (stream))
    return TRUE;

  g_main_context_invoke (NULL, gdk_x11_selection_output_stream_invoke_flush, stream);

  g_mutex_lock (&priv->mutex);
  if (gdk_x11_selection_output_stream_needs_flush_unlocked (stream))
    g_cond_wait (&priv->cond, &priv->mutex);
  g_mutex_unlock (&priv->mutex);

  return TRUE;
}

static void
gdk_x11_selection_output_stream_flush_async (GOutputStream       *output_stream,
                                             int                  io_priority,
                                             GCancellable        *cancellable,
                                             GAsyncReadyCallback  callback,
                                             gpointer             user_data)
{
  GdkX11SelectionOutputStream *stream = GDK_X11_SELECTION_OUTPUT_STREAM (output_stream);
  GdkX11SelectionOutputStreamPrivate *priv = gdk_x11_selection_output_stream_get_instance_private (stream);
  GTask *task;
  
  task = g_task_new (stream, cancellable, callback, user_data);
  g_task_set_source_tag (task, gdk_x11_selection_output_stream_flush_async);
  g_task_set_priority (task, io_priority);

  if (!gdk_x11_selection_output_stream_can_flush (stream))
    {
      if (gdk_x11_selection_output_request_flush (stream))
        {
          g_assert (priv->pending_task == NULL);
          priv->pending_task = task;
          return;
        }
      else
        {
          g_task_return_boolean (task, TRUE);
          g_object_unref (task);
          return;
        }
    }

  gdk_x11_selection_output_stream_perform_flush (stream);
  g_task_return_boolean (task, TRUE);
  g_object_unref (task);
  return;
}

static gboolean
gdk_x11_selection_output_stream_flush_finish (GOutputStream  *stream,
                                              GAsyncResult   *result,
                                              GError        **error)
{
  g_return_val_if_fail (g_task_is_valid (result, stream), FALSE);
  g_return_val_if_fail (g_async_result_is_tagged (result, gdk_x11_selection_output_stream_flush_async), FALSE);

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

static gboolean
gdk_x11_selection_output_stream_invoke_close (gpointer stream)
{
  GdkX11SelectionOutputStreamPrivate *priv = gdk_x11_selection_output_stream_get_instance_private (stream);

  GDK_X11_DISPLAY (priv->display)->streams = g_slist_remove (GDK_X11_DISPLAY (priv->display)->streams, stream);
  g_signal_handlers_disconnect_by_func (priv->display,
                                        gdk_x11_selection_output_stream_xevent,
                                        stream);
  g_object_unref (stream);

  return G_SOURCE_REMOVE;
}

static gboolean
gdk_x11_selection_output_stream_close (GOutputStream  *stream,
                                       GCancellable   *cancellable,
                                       GError        **error)
{
  g_main_context_invoke (NULL, gdk_x11_selection_output_stream_invoke_close, g_object_ref (stream));

  return TRUE;
}

static void
gdk_x11_selection_output_stream_close_async (GOutputStream       *stream,
                                             int                  io_priority,
                                             GCancellable        *cancellable,
                                             GAsyncReadyCallback  callback,
                                             gpointer             user_data)
{
  GTask *task;

  task = g_task_new (stream, cancellable, callback, user_data);
  g_task_set_source_tag (task, gdk_x11_selection_output_stream_close_async);
  g_task_set_priority (task, io_priority);

  gdk_x11_selection_output_stream_invoke_close (stream);
  g_task_return_boolean (task, TRUE);

  g_object_unref (task);
}

static gboolean
gdk_x11_selection_output_stream_close_finish (GOutputStream  *stream,
                                              GAsyncResult   *result,
                                              GError        **error)
{
  g_return_val_if_fail (g_task_is_valid (result, stream), FALSE);
  g_return_val_if_fail (g_async_result_is_tagged (result, gdk_x11_selection_output_stream_close_async), FALSE);

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

static void
gdk_x11_selection_output_stream_finalize (GObject *object)
{
  GdkX11SelectionOutputStream *stream = GDK_X11_SELECTION_OUTPUT_STREAM (object);
  GdkX11SelectionOutputStreamPrivate *priv = gdk_x11_selection_output_stream_get_instance_private (stream);

  /* not sending a notify is terrible */
  g_assert (priv->notify == NULL);

  g_byte_array_unref (priv->data);
  g_cond_clear (&priv->cond);
  g_mutex_clear (&priv->mutex);

  g_free (priv->selection);
  g_free (priv->target);
  g_free (priv->property);
  g_free (priv->type);

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

static void
gdk_x11_selection_output_stream_class_init (GdkX11SelectionOutputStreamClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);
  GOutputStreamClass *output_stream_class = G_OUTPUT_STREAM_CLASS (klass);

  object_class->finalize = gdk_x11_selection_output_stream_finalize;
  
  output_stream_class->write_fn = gdk_x11_selection_output_stream_write;
  output_stream_class->flush = gdk_x11_selection_output_stream_flush;
  output_stream_class->close_fn = gdk_x11_selection_output_stream_close;

  output_stream_class->write_async = gdk_x11_selection_output_stream_write_async;
  output_stream_class->write_finish = gdk_x11_selection_output_stream_write_finish;
  output_stream_class->flush_async = gdk_x11_selection_output_stream_flush_async;
  output_stream_class->flush_finish = gdk_x11_selection_output_stream_flush_finish;
  output_stream_class->close_async = gdk_x11_selection_output_stream_close_async;
  output_stream_class->close_finish = gdk_x11_selection_output_stream_close_finish;
}

static void
gdk_x11_selection_output_stream_init (GdkX11SelectionOutputStream *stream)
{
  GdkX11SelectionOutputStreamPrivate *priv = gdk_x11_selection_output_stream_get_instance_private (stream);

  g_mutex_init (&priv->mutex);
  g_cond_init (&priv->cond);
  priv->data = g_byte_array_new ();
}

static gboolean
gdk_x11_selection_output_stream_xevent (GdkDisplay   *display,
                                        const XEvent *xevent,
                                        gpointer      data)
{
  GdkX11SelectionOutputStream *stream = GDK_X11_SELECTION_OUTPUT_STREAM (data);
  GdkX11SelectionOutputStreamPrivate *priv = gdk_x11_selection_output_stream_get_instance_private (stream);
  Display *xdisplay;

  xdisplay = gdk_x11_display_get_xdisplay (priv->display);

  if (xevent->xany.display != xdisplay ||
      xevent->xany.window != priv->xwindow)
    return FALSE;

  switch (xevent->type)
  {
    case PropertyNotify:
      if (!priv->incr ||
          xevent->xproperty.atom != priv->xproperty ||
          xevent->xproperty.state != PropertyDelete)
        return FALSE;

      GDK_DISPLAY_NOTE (display, SELECTION, g_printerr ("%s:%s: got PropertyNotify Delete during INCR\n",
                                      priv->selection, priv->target));
      priv->delete_pending = FALSE;
      if (gdk_x11_selection_output_stream_needs_flush (stream) &&
          gdk_x11_selection_output_stream_can_flush (stream))
        gdk_x11_selection_output_stream_perform_flush (stream);
      return FALSE;

    default:
      return FALSE;
    }
}

static GOutputStream *
gdk_x11_selection_output_stream_new (GdkDisplay                   *display,
                                     GdkX11PendingSelectionNotify *notify,
                                     Window                        window,
                                     const char                   *selection,
                                     const char                   *target,
                                     const char                   *property,
                                     const char                   *type,
                                     int                           format,
                                     gulong                        timestamp)
{
  GdkX11SelectionOutputStream *stream;
  GdkX11SelectionOutputStreamPrivate *priv;

  stream = g_object_new (GDK_TYPE_X11_SELECTION_OUTPUT_STREAM, NULL);
  priv = gdk_x11_selection_output_stream_get_instance_private (stream);

  priv->display = display;
  GDK_X11_DISPLAY (display)->streams = g_slist_prepend (GDK_X11_DISPLAY (display)->streams, stream);
  priv->notify = notify;
  priv->xwindow = window;
  priv->selection = g_strdup (selection);
  priv->xselection = gdk_x11_get_xatom_by_name_for_display (display, priv->selection);
  priv->target = g_strdup (target);
  priv->xtarget = gdk_x11_get_xatom_by_name_for_display (display, priv->target);
  priv->property = g_strdup (property);
  priv->xproperty = gdk_x11_get_xatom_by_name_for_display (display, priv->property);
  priv->type = g_strdup (type);
  priv->xtype = gdk_x11_get_xatom_by_name_for_display (display, priv->type);
  priv->format = format;
  priv->timestamp = timestamp;

  g_signal_connect (display,
                    "xevent",
                    G_CALLBACK (gdk_x11_selection_output_stream_xevent),
                    stream);
  
  return G_OUTPUT_STREAM (stream);
}

static void
print_atoms (GdkDisplay *display,
             const char *selection,
             const char *prefix,
             const Atom *atoms,
             gsize       n_atoms)
{
  GDK_DISPLAY_NOTE (display, CLIPBOARD,
           gsize i;
            
           g_printerr ("%s: %s [ ", selection, prefix);
           for (i = 0; i < n_atoms; i++)
             {
               g_printerr ("%s%s", i > 0 ? ", " : "", gdk_x11_get_xatom_name_for_display (display , atoms[i]));
             }
           g_printerr (" ]\n");
          ); 
}

static void
handle_targets_done (GObject      *stream,
                     GAsyncResult *result,
                     gpointer      user_data)
{
  GError *error = NULL;
  gsize bytes_written;

  if (!g_output_stream_write_all_finish (G_OUTPUT_STREAM (stream), result, &bytes_written, &error))
    {
      GDK_NOTE (CLIPBOARD, g_printerr ("---: failed to send targets after %zu bytes: %s\n",
                                      bytes_written, error->message));
      g_error_free (error);
    }

  g_free (user_data);
}

static void
handle_targets (GOutputStream                *stream,
                GdkDisplay                   *display,
                GdkContentFormats            *formats,
                const char                   *target,
                const char                   *encoding,
                int                           format,
                gulong                        timestamp,
                GdkX11SelectionOutputHandler  handler,
                gpointer                      user_data)
{
  Atom *atoms;
  gsize n_atoms;

  atoms = gdk_x11_clipboard_formats_to_atoms (display,
                                              TRUE,
                                              formats,
                                              &n_atoms);
  print_atoms (display, "---", "sending targets", atoms, n_atoms);
  g_output_stream_write_all_async (stream,
                                   atoms,
                                   n_atoms * sizeof (Atom),
                                   G_PRIORITY_DEFAULT,
                                   NULL,
                                   handle_targets_done,
                                   atoms);
  g_object_unref (stream);
}

static void
handle_timestamp_done (GObject      *stream,
                       GAsyncResult *result,
                       gpointer      user_data)
{
  GError *error = NULL;
  gsize bytes_written;

  if (!g_output_stream_write_all_finish (G_OUTPUT_STREAM (stream), result, &bytes_written, &error))
    {
      GDK_NOTE (CLIPBOARD, g_printerr ("---: failed to send timestamp after %zu bytes: %s\n",
                                      bytes_written, error->message));
      g_error_free (error);
    }

  g_slice_free (gulong, user_data);
}

static void
handle_timestamp (GOutputStream                *stream,
                  GdkDisplay                   *display,
                  GdkContentFormats            *formats,
                  const char                   *target,
                  const char                   *encoding,
                  int                           format,
                  gulong                        timestamp,
                  GdkX11SelectionOutputHandler  handler,
                  gpointer                      user_data)
{
  gulong *time_;

  time_ = g_slice_new (gulong);
  *time_ = timestamp;

  g_output_stream_write_all_async (stream,
                                   time_,
                                   sizeof (gulong),
                                   G_PRIORITY_DEFAULT,
                                   NULL,
                                   handle_timestamp_done,
                                   time_);
  g_object_unref (stream);
}

static void
handle_save_targets (GOutputStream                *stream,
                     GdkDisplay                   *display,
                     GdkContentFormats            *formats,
                     const char                   *target,
                     const char                   *encoding,
                     int                           format,
                     gulong                        timestamp,
                     GdkX11SelectionOutputHandler  handler,
                     gpointer                      user_data)
{
  /* Don't do anything */

  g_object_unref (stream);
}

static void
handle_text_list (GOutputStream                *stream,
                  GdkDisplay                   *display,
                  GdkContentFormats            *formats,
                  const char                   *target,
                  const char                   *encoding,
                  int                           format,
                  gulong                        timestamp,
                  GdkX11SelectionOutputHandler  handler,
                  gpointer                      user_data)
{
  GOutputStream *converter_stream;
  GConverter *converter;

  converter = gdk_x11_text_list_converter_to_utf8_new (display,
                                                       encoding,
                                                       format);
  converter_stream = g_converter_output_stream_new (stream, converter);

  g_object_unref (converter);
  g_object_unref (stream);

  handler (converter_stream, gdk_intern_mime_type ("text/plain;charset=utf-8"), user_data);
}

static void
handle_utf8 (GOutputStream                *stream,
             GdkDisplay                   *display,
             GdkContentFormats            *formats,
             const char                   *target,
             const char                   *encoding,
             int                           format,
             gulong                        timestamp,
             GdkX11SelectionOutputHandler  handler,
             gpointer                      user_data)
{
  handler (stream, gdk_intern_mime_type ("text/plain;charset=utf-8"), user_data);
}

typedef void (* MimeTypeHandleFunc) (GOutputStream *, GdkDisplay *, GdkContentFormats *, const char *, const char *, int, gulong, GdkX11SelectionOutputHandler, gpointer);

static const struct {
  const char *x_target;
  const char *mime_type;
  const char *type;
  int format;
  MimeTypeHandleFunc handler;
} special_targets[] = {
  { "UTF8_STRING",   "text/plain;charset=utf-8", "UTF8_STRING",   8,  handle_utf8 },
  { "COMPOUND_TEXT", "text/plain;charset=utf-8", "COMPOUND_TEXT", 8,  handle_text_list },
  { "TEXT",          "text/plain;charset=utf-8", "STRING",        8,  handle_text_list },
  { "STRING",        "text/plain;charset=utf-8", "STRING",        8,  handle_text_list },
  { "TARGETS",       NULL,                       "ATOM",          32, handle_targets },
  { "TIMESTAMP",     NULL,                       "INTEGER",       32, handle_timestamp },
  { "SAVE_TARGETS",  NULL,                       "NULL",          32, handle_save_targets }
};

static gboolean
gdk_x11_selection_output_streams_request (GdkDisplay                   *display,
                                          GdkX11PendingSelectionNotify *notify,
                                          GdkContentFormats            *formats,
                                          Window                        requestor,
                                          Atom                          xselection,
                                          Atom                          xtarget,
                                          Atom                          xproperty,
                                          gulong                        timestamp,
                                          GdkX11SelectionOutputHandler  handler,
                                          gpointer                      user_data)
{
  const char *mime_type, *selection, *target, *property;

  selection = gdk_x11_get_xatom_name_for_display (display, xselection);
  target = gdk_x11_get_xatom_name_for_display (display, xtarget);
  property = gdk_x11_get_xatom_name_for_display (display, xproperty);
  mime_type = gdk_intern_mime_type (target);

  if (mime_type)
    {
      if (gdk_content_formats_contain_mime_type (formats, mime_type))
        {
          GOutputStream *stream;

          stream = gdk_x11_selection_output_stream_new (display,
                                                        notify,
                                                        requestor,
                                                        selection,
                                                        target,
                                                        property,
                                                        target,
                                                        8,
                                                        timestamp);
          handler (stream, mime_type, user_data);
          return TRUE;
        }
    }
  else if (g_str_equal (target, "MULTIPLE"))
    {
      gulong n_atoms;
      gulong nbytes;
      Atom prop_type;
      int prop_format;
      Atom *atoms = NULL;
      int error;

      error = XGetWindowProperty (gdk_x11_display_get_xdisplay (display),
                                  requestor,
                                  xproperty,
                                  0, 0x1FFFFFFF, False,
                                  AnyPropertyType,
                                  &prop_type, &prop_format,
                                  &n_atoms, &nbytes, (guchar **) &atoms);
      if (error != Success)
        {
          GDK_DISPLAY_NOTE (display, SELECTION, g_printerr ("%s: XGetProperty() during MULTIPLE failed with %d\n",
                                          selection, error));
        }
      else if (prop_format != 32 ||
               prop_type != gdk_x11_get_xatom_by_name_for_display (display, "ATOM_PAIR"))
        {
          GDK_DISPLAY_NOTE (display, SELECTION, g_printerr ("%s: XGetProperty() type/format should be ATOM_PAIR/32 but is %s/%d\n",
                                          selection, gdk_x11_get_xatom_name_for_display (display, prop_type), prop_format));
        }
      else if (n_atoms < 2)
        {
          print_atoms (display, selection, "ignoring MULTIPLE request with too little elements", atoms, n_atoms);
        }
      else
        {
          gulong i;

          print_atoms (display, selection, "MULTIPLE request", atoms, n_atoms);
          if (n_atoms % 2)
            {
              GDK_DISPLAY_NOTE (display, SELECTION, g_printerr ("%s: Number of atoms is uneven at %lu, ignoring last element\n",
                                              selection, n_atoms));
              n_atoms &= ~1;
            }

          gdk_x11_pending_selection_notify_require (notify, n_atoms / 2);

          for (i = 0; i < n_atoms / 2; i++)
            {
              gboolean success;

              if (atoms[2 * i] == None || atoms[2 * i + 1] == None)
                {
                  success = FALSE;
                  GDK_DISPLAY_NOTE (display, SELECTION, g_printerr ("%s: None not allowed as atom in MULTIPLE request\n",
                                                  selection));
                  gdk_x11_pending_selection_notify_send (notify, display, FALSE);
                }
              else if (atoms[2 * i] == gdk_x11_get_xatom_by_name_for_display (display, "MULTIPLE"))
                {
                  success = FALSE;
                  GDK_DISPLAY_NOTE (display, SELECTION, g_printerr ("%s: MULTIPLE as target in MULTIPLE request would cause recursion\n",
                                                  selection));
                  gdk_x11_pending_selection_notify_send (notify, display, FALSE);
                }
              else
                {
                  success = gdk_x11_selection_output_streams_request (display,
                                                                      notify,
                                                                      formats,
                                                                      requestor,
                                                                      xselection,
                                                                      atoms[2 * i],
                                                                      atoms[2 * i + 1],
                                                                      timestamp,
                                                                      handler,
                                                                      user_data);
                }

              if (!success)
                atoms[2 * i + 1] = None;
            }
        }

      XChangeProperty (gdk_x11_display_get_xdisplay (display),
                       requestor,
                       xproperty,
                       prop_type, 32,
                       PropModeReplace, (guchar *)atoms, n_atoms);

      if (atoms)
        XFree (atoms);

      gdk_x11_pending_selection_notify_send (notify, display, TRUE);
      return TRUE;
    }
  else
    {
      gsize i;

      for (i = 0; i < G_N_ELEMENTS (special_targets); i++)
        {
          if (g_str_equal (target, special_targets[i].x_target) &&
              special_targets[i].handler)
            {
              GOutputStream *stream;

              if (special_targets[i].mime_type)
                mime_type = gdk_intern_mime_type (special_targets[i].mime_type);
              stream = gdk_x11_selection_output_stream_new (display,
                                                            notify,
                                                            requestor,
                                                            selection,
                                                            target,
                                                            property,
                                                            special_targets[i].type,
                                                            special_targets[i].format,
                                                            timestamp);
              special_targets[i].handler (stream,
                                          display,
                                          formats,
                                          target,
                                          special_targets[i].type,
                                          special_targets[i].format,
                                          timestamp,
                                          handler,
                                          user_data);
              return TRUE;
            }
        }
    }

  gdk_x11_pending_selection_notify_send (notify, display, FALSE);
  return FALSE;
}

void
gdk_x11_selection_output_streams_create (GdkDisplay                   *display,
                                         GdkContentFormats            *formats,
                                         Window                        requestor,
                                         Atom                          selection,
                                         Atom                          target,
                                         Atom                          property,
                                         gulong                        timestamp,
                                         GdkX11SelectionOutputHandler  handler,
                                         gpointer                      user_data)
{
  GdkX11PendingSelectionNotify *notify;

  notify = gdk_x11_pending_selection_notify_new (requestor,
                                                 selection,
                                                 target,
                                                 property,
                                                 timestamp);
  gdk_x11_selection_output_streams_request (display,
                                            notify,
                                            formats,
                                            requestor,
                                            selection,
                                            target,
                                            property,
                                            timestamp,
                                            handler,
                                            user_data);
}
