/* GDK - The GIMP Drawing Kit
 * Copyright (C) 1995-1997 Peter Mattis, Spencer Kimball and Josh MacDonald
 * Copyright (C) 1998-2002 Tor Lillqvist
 *
 * 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 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/>.
 */

/*
 * Modified by the GTK+ Team and others 1997-2000.  See the AUTHORS
 * file for a list of people on the GTK+ Team.  See the ChangeLog
 * files for a list of changes.  These files are distributed with
 * GTK+ at ftp://ftp.gtk.org/pub/gtk/.
 */

/*
GTK has two clipboards - normal clipboard and primary clipboard
Primary clipboard is only handled
internally by GTK (it's not portable to Windows).

("C:" means clipboard client (requester), "S:" means clipboard server (provider))
("transmute" here means "change the format of some data"; this term is used here
 instead of "convert" to avoid clashing with the old g(t|d)k_selection_convert() APIs,
 which are completely unrelated)

For Clipboard:
GTK calls one of the gdk_clipboard_set* () functions (either supplying
its own content provider, or giving a GTyped data for which GDK will
create a content provider automatically).
That function associates the content provider with the clipboard and calls
S: gdk_clipboard_claim(),
to claim ownership. GDK first calls the backend implementation of
that function, then the
S: gdk_clipboard_real_claim()
implementation.
The "real" function does some mundane bookkeeping, whereas the backend
implementation advertises the formats supported by the clipboard,
if the call says that the claim is local. Non-local (remote) claims
are there just to tell GDK that some other process owns the clipboard
and claims to provide data in particular formats.
No data is sent anywhere.

The content provider has a callback, which will be invoked every time
the data from this provider is needed.

GTK might also call gdk_clipboard_store_async(), which instructs
the backend to put the data into the OS clipboard manager (if
supported and available) so that it remains available for other
processes after the clipboard owner terminates.

When something needs to be obtained from clipboard, GTK calls
C: gdk_clipboard_read_async () -> gdk_clipboard_read_internal (),
providing it with a string-array of mime/types, which is internally
converted into a GdkContentFormats object.
That function creates a task.

Then, if the clipboard is local, it calls
C: gdk_clipboard_read_local_async(),
which matches given formats to the content provider formats and, if there's a match,
creates a pipe, calls
C: gdk_clipboard_write_async()
on the write end, and sets the read end as a return value of the task, which will
later be given to the caller-specified callback.

If the clipboard isn't local, it calls
C: read_async()
of the backend clipboard stream class.
The backend starts creating a stream (somehow) and sets up the callback to return that
stream via the task, once the stream is created.
Either way, the caller-specified callback is invoked, and it gets the read end
of a stream by calling
C: gdk_clipboard_read_finish(),
then reads the data from the stream and unrefs the stream once it is done.
IRL applications use wrappers, which create an extra task that gets the
stream, reads from it asynchronously and turns the bytes that it reads into
some kind of application-specific object type. GDK comes pre-equipped with
functions that read arbitrary GTypes (as long as they are serializable),
texts (strings) or textures (GdkPixbufs) this way.

On Windows:
Clipboard is opened by OpenClipboard(), emptied by EmptyClipboard() (which also
makes the window the clipboard owner), data is put into it by SetClipboardData().
Clipboard is closed with CloseClipboard().
If SetClipboardData() is given a NULL data value, the owner will later
receive WM_RENDERFORMAT message, in response to which it must call
SetClipboardData() with the provided handle and the actual data this time.
This way applications can avoid storing everything in the clipboard
all the time, only putting the data there as it is requested by other applications.
At some undefined points of time an application might get WM_RENDERALLFORMATS
message, it should respond by opening the clipboard and rendering
into it all the data that it offers, as if responding to multiple WM_RENDERFORMAT
messages.

On GDK-Win32:

Any operations that require OpenClipboard()/CloseClipboard() combo (i.e.
almost everything, except for WM_RENDERFORMAT handling) is offloaded into
separate thread, which tries to complete any operations in the queue.
Each operation routine usually starts with a timeout check (all operations
time out after 30 seconds), then a check for clipboard status (to abort
any operations that became obsolete due to clipboard status being changed - 
i.e. retrieving clipboard contents is aborted if clipboard contents change
before the operation can be completed), then an attempt to OpenClipboard().
Failure to OpenClipboard() leads to the queue processing stopping, and
resuming from the beginning after another 1-second delay.
A success in OpenClipboard() allows the operation to continue.
The thread remembers the fact that it has clipboard open, and does
not try to close & reopen it, unless that is strictly necessary.
The clipboard is closed after each queue processing run.

GTK calls one of the gdk_clipboard_set* () functions (either supplying
its own content provider, or giving a GTyped data for which GDK will
create a content provider automatically).
That function associates the content provider with the clipboard and calls
S: gdk_clipboard_claim(),
to claim ownership. GDK first calls the backend implementation of
that function,
S: gdk_win32_clipboard_claim(),
which maps the supported GDK contentformats to W32 data formats and
caches this mapping, then the
S: gdk_clipboard_real_claim()
implementation.
The "real" function does some mundane bookkeeping, whereas the backend
implementation advertises the formats supported by the clipboard,
if the call says that the claim is local. Non-local (remote) claims
are there just to tell GDK that some other process owns the clipboard
and claims to provide data in particular formats.
For the local claims gdk_win32_clipboard_claim() queues a clipboard
advertise operation (see above).
That operation will call EmptyClipboard() to claim the ownership,
then call SetClipboardData() with NULL value for each W32 data format
supported, advertising the W32 data formats to other processes.
No data is sent anywhere.

The content provider has a callback, which will be invoked every time
the data from this provider is needed.

GTK might also call gdk_clipboard_store_async(), which instructs
the W32 backend to put the data into the OS clipboard manager by
sending WM_RENDERALLFORMATS to itself and then handling it normally.

Every time W32 backend gets WM_CLIPBOARDUPDATE,
it calls GetUpdatedClipboardFormats() and GetClipboardSequenceNumber()
and caches the results of both. These calls do not require the clipboard
to be opened.
After that it would call
C: gdk_win32_clipboard_claim_remote()
to indicate that some other process owns the clipboard and supports
the formats from the cached list. If the process is the owner,
the remote claim is not performed (it's assumed that a local claim
was already made when a clipboard content provider is set, so no need
to do that either).
Note: clipboard sequence number changes with each SetClipboardData() call.
Specifically, a process that uses delayed rendering (like GDK does)
must call SetClipboardData() with NULL value every time the data changes,
even if its format remains the same.
The cached remote formats are then mapped into GDK contentformats.
This map is separate from the one that maps supported GDK contentformats
to W32 formats for locally-claimed clipboards.

When something needs to be obtained from clipboard, GTK calls
C: gdk_clipboard_read_async () -> gdk_clipboard_read_internal (),
providing it with a string-array of mime/types, which is internally
converted into a GdkContentFormats object.
That function creates a task.

Then, if the clipboard is local, it calls
C: gdk_clipboard_read_local_async(),
which matches given formats to the content provider formats and, if there's a match,
creates a pipe, calls
C: gdk_clipboard_write_async()
on the write end, and sets the read end as a return value of the task, which will
later be given to the caller-specified callback.

If the clipboard isn't local, it calls
C: read_async()
of the W32 backend clipboard stream class.
It then queues a retrieve operation (see above).
The retrieve operation goes over formats available on the clipboard,
and picks the first one that matches the list supplied with the retrieve
operation (that is, it gives priority to formats at the top of the clipboard
format list, even if such formats are at the bottom of the list of formats
supported by the application; this is due to the fact that formats at the
top of the clipboard format list are usually "raw" or "native" and assumed
to not to be transmuted by the clipboard owner from some other format,
and thus it is better to use these, if the requesting application can handle
them). It then calls GetClipboardData(), which either causes
a WM_RENDERFORMAT to be sent to the server (for delayed rendering),
or it just grabs the data from the OS.

Server-side GDK catches WM_RENDERFORMAT, figures out a contentformat
to request (it has an earlier advertisement cached in the thread, so
there's no need to ask the main thread for anything), and
creates a render request, then sends it to the main thread.
After that it keeps polling the queue until the request comes back.
The main thread render handler creates an output stream that
writes the data into a global memory buffer, then calls
S: gdk_clipboard_write_async()
to write the data.
The callback finishes that up with
S: gdk_clipboard_write_finish(),
which sends the render request back to the clipborad thread,
along with the data. The clipboard thread then calls
S: SetClipboardData()
with the clipboard handle provided by the OS on behalf of the client.

Once the data handle is available, the clipboard thread creates a stream
that reads from a copy of that data (after transmutation, if necessary),
and sends that stream back to the main thread. The data is kept in a client-side
memory buffer (owned by the stream), the HGLOBAL given by the OS is not held
around for this to happen.
The stream is then returned through the task to the caller.

Either way, the caller-specified callback is invoked, and it gets the read end
of a stream by calling
C: gdk_clipboard_read_finish(),
then reads the data from the stream and unrefs the stream once it is done.
The local buffer that backed the stream is freed with the stream.
IRL applications use wrappers, which create an extra task that gets the
stream, reads from it asynchronously and turns the bytes that it reads into
some kind of application-specific object type. GDK comes pre-equipped with
functions that read arbitrary GTypes (as long as they are serializable),
texts (strings) or textures (GdkPixbufs) this way.

If data must be stored on the clipboard, because the application is quitting,
GTK will call
S: gdk_clipboard_store_async()
on all the clipboards it owns. This creates multiple write stream (one for each
format being stored), each backed by a HGLOBAL memory object. Once all memory
objects are written, the backend queues a store operation, passing along
all these HGLOBAL objects. The clipboard thread processes that by sending
WM_RENDERALLFORMATS to the window, then signals the task that it's done.

When clipboard owner changes, the old owner receives WM_DESTROYCLIPBOARD message,
the clipboard thread schedules a call to gdk_clipboard_claim_remote()
in the main thread, with an empty list of formats,
to indicate that the clipboard is now owned by a remote process.
Later the OS will send WM_CLIPBOARDUPDATE to indicate
the new clipboard contents (see above).

DND:
GDK-Win32:
DnD server runs in a separate thread, and schedules calls to be
made in the main thread in response to the DnD thread being invoked
by the OS (using OLE2 mechanism).
The DnD thread normally just idles, until the main thread tells it
to call DoDragDrop(), at which point it enters the DoDragDrop() call
(which means that its OLE2 DnD callbacks get invoked repeatedly by the OS
 in response to user actions), and doesn't leave it until the DnD
operation is finished.

Otherwise it's similar to how the clipboard works. Only the DnD server
(drag source) works in a thread. DnD client (drop target) works normally.
*/

#include "config.h"
#include <string.h>
#include <stdlib.h>

/* For C-style COM wrapper macros */
#define COBJMACROS

/* for CIDA */
#include <shlobj.h>

#include "gdkdebugprivate.h"
#include "gdkdisplay.h"
#include "gdkprivate-win32.h"
#include "gdkclipboardprivate.h"
#include "gdkclipboard-win32.h"
#include "gdkclipdrop-win32.h"
#include "gdkhdataoutputstream-win32.h"
#include "gdkwin32dnd.h"
#include "gdkwin32dnd-private.h"
#include "gdkwin32.h"

#include "gdk/gdkdebugprivate.h"
#include "gdk/gdkdragprivate.h"
#include <glib/gi18n-lib.h>
#include "gdk/gdkprivate.h"

#define HIDA_GetPIDLFolder(pida) (LPCITEMIDLIST)(((LPBYTE)pida)+(pida)->aoffset[0])
#define HIDA_GetPIDLItem(pida, i) (LPCITEMIDLIST)(((LPBYTE)pida)+(pida)->aoffset[i+1])

#define CLIPBOARD_OPERATION_TIMEOUT (G_USEC_PER_SEC * 30)

/* GetClipboardData() times out after 30 seconds.
 * Try to reply (even if it's a no-action reply due to a timeout)
 * before that happens.
 */
#define CLIPBOARD_RENDER_TIMEOUT (G_USEC_PER_SEC * 29)

gboolean _gdk_win32_transmute_windows_data (UINT          from_w32format,
                                            const char   *to_contentformat,
                                            HANDLE        hdata,
                                            guchar      **set_data,
                                            gsize        *set_data_length);

/* Just to avoid calling RegisterWindowMessage() every time */
static UINT thread_wakeup_message;

typedef enum _GdkWin32ClipboardThreadQueueItemType GdkWin32ClipboardThreadQueueItemType;

enum _GdkWin32ClipboardThreadQueueItemType
{
  GDK_WIN32_CLIPBOARD_THREAD_QUEUE_ITEM_ADVERTISE = 1,
  GDK_WIN32_CLIPBOARD_THREAD_QUEUE_ITEM_RETRIEVE = 2,
  GDK_WIN32_CLIPBOARD_THREAD_QUEUE_ITEM_STORE = 3,
};

typedef struct _GdkWin32ClipboardThreadQueueItem GdkWin32ClipboardThreadQueueItem;

struct _GdkWin32ClipboardThreadQueueItem
{
  GdkWin32ClipboardThreadQueueItemType  item_type;
  gint64                                start_time;
  gint64                                end_time;
  gpointer                              opaque_task;
};

typedef struct _GdkWin32ClipboardThreadAdvertise GdkWin32ClipboardThreadAdvertise;

struct _GdkWin32ClipboardThreadAdvertise
{
  GdkWin32ClipboardThreadQueueItem  parent;
  GArray                      *pairs; /* of GdkWin32ContentFormatPair */
  gboolean                     unset;
};

typedef struct _GdkWin32ClipboardThreadRetrieve GdkWin32ClipboardThreadRetrieve;

struct _GdkWin32ClipboardThreadRetrieve
{
  GdkWin32ClipboardThreadQueueItem  parent;
  GArray                      *pairs; /* of GdkWin32ContentFormatPair */
  gint64                       sequence_number;
};

typedef struct _GdkWin32ClipboardStorePrepElement GdkWin32ClipboardStorePrepElement;

struct _GdkWin32ClipboardStorePrepElement
{
  UINT           w32format;
  const char    *contentformat;
  HANDLE         handle;
  GOutputStream *stream;
};

typedef struct _GdkWin32ClipboardStorePrep GdkWin32ClipboardStorePrep;

struct _GdkWin32ClipboardStorePrep
{
  GTask             *store_task;
  GArray            *elements; /* of GdkWin32ClipboardStorePrepElement */
};

typedef struct _GdkWin32ClipboardThreadStore GdkWin32ClipboardThreadStore;

struct _GdkWin32ClipboardThreadStore
{
  GdkWin32ClipboardThreadQueueItem  parent;
  GArray                      *elements; /* of GdkWin32ClipboardStorePrepElement */
};

typedef struct _GdkWin32ClipboardThreadRender GdkWin32ClipboardThreadRender;

struct _GdkWin32ClipboardThreadRender
{
  /* The handle that the main thread prepares for us.
   * We just give it to SetClipboardData ().
   * NULL means that the rendering failed.
   */
  HANDLE                                main_thread_data_handle;

  /* The format that is being requested of us */
  GdkWin32ContentFormatPair             pair;
};

typedef struct _GdkWin32ClipboardThread GdkWin32ClipboardThread;

struct _GdkWin32ClipboardThread
{
  /* A hidden window that owns our clipboard
   * and receives clipboard-related messages.
   */
  HWND         clipboard_window;

  /* We receive instructions from the main thread in this queue */
  GAsyncQueue *input_queue;

  /* Last observer owner of the clipboard, as reported by the OS.
   * This is compared to GetClipboardOwner() return value to see
   * whether the owner changed.
   */
  HWND         stored_hwnd_owner;

  /* The last time we saw an owner change event.
   * Any requests made before this time are invalid and
   * fail automatically.
   */
  gint64       owner_change_time;

  /* The handle that was given to OpenClipboard().
   * NULL is a valid handle,
   * INVALID_HANDLE_VALUE means that the clipboard is closed.
   */
  HWND         clipboard_opened_for;

  /* We can't peek the queue or "unpop" queue items,
   * so the items that we can't act upon (yet) got
   * to be stored *somewhere*.
   */
  GList       *dequeued_items;

  /* Wakeup timer id (1 if timer is set, 0 otherwise) */
  UINT         wakeup_timer;

  /* The formats that the main thread claims to provide */
  GArray      *cached_advertisement; /* of GdkWin32ContentFormatPair */

  /* We receive rendered clipboard data in this queue.
   * Contains GdkWin32ClipboardThreadRender structs.
   */
  GAsyncQueue *render_queue; 

  /* Set to TRUE when we're calling EmptyClipboard () */
  gboolean     ignore_destroy_clipboard;
};

/* The code is much more secure if we don't rely on the OS to keep
 * this around for us.
 */
static GdkWin32ClipboardThread *clipboard_thread_data = NULL;

typedef struct _GdkWin32ClipboardThreadResponse GdkWin32ClipboardThreadResponse;

struct _GdkWin32ClipboardThreadResponse
{
  GdkWin32ClipboardThreadQueueItemType  item_type;
  GError                               *error;
  gpointer                              opaque_task;
  GInputStream                         *input_stream;
};

gboolean
_gdk_win32_format_uses_hdata (UINT w32format)
{
  switch (w32format)
    {
    case CF_DIB:
    case CF_DIBV5:
    case CF_DIF:
    case CF_DSPBITMAP:
    case CF_DSPENHMETAFILE:
    case CF_DSPMETAFILEPICT:
    case CF_DSPTEXT:
    case CF_OEMTEXT:
    case CF_RIFF:
    case CF_SYLK:
    case CF_TEXT:
    case CF_TIFF:
    case CF_UNICODETEXT:
    case CF_WAVE:
      return TRUE;
    default:
      if (w32format >= 0xC000)
        return TRUE;
      else
        return FALSE;
    }
}


/* This function is called in the main thread */
static gboolean
clipboard_window_created (gpointer user_data)
{
  GdkWin32Clipdrop *clipdrop = _gdk_win32_clipdrop_get ();

  clipdrop->clipboard_window = (HWND) user_data;

  return G_SOURCE_REMOVE;
}

/* This function is called in the main thread */
static gboolean
clipboard_owner_changed (gpointer user_data)
{
  GdkDisplay *display = gdk_display_get_default ();
  GdkClipboard *clipboard = gdk_display_get_clipboard (display);
  gdk_win32_clipboard_claim_remote (GDK_WIN32_CLIPBOARD (clipboard));

  return G_SOURCE_REMOVE;
}

typedef struct _GdkWin32ClipboardRenderAndStream GdkWin32ClipboardRenderAndStream;

struct _GdkWin32ClipboardRenderAndStream
{
  GdkWin32ClipboardThreadRender *render;
  GdkWin32HDataOutputStream *stream;
};

static void
clipboard_render_hdata_ready (GObject      *clipboard,
                              GAsyncResult *result,
                              gpointer      user_data)
{
  GError *error = NULL;
  GdkWin32ClipboardRenderAndStream render_and_stream = *(GdkWin32ClipboardRenderAndStream *) user_data;
  GdkWin32Clipdrop *clipdrop = _gdk_win32_clipdrop_get ();

  g_free (user_data);

  if (!gdk_clipboard_write_finish (GDK_CLIPBOARD (clipboard), result, &error))
    {
      HANDLE handle;
      gboolean is_hdata;
      GDK_NOTE(CLIPBOARD, g_printerr ("%p: failed to write HData-backed stream: %s\n", clipboard, error->message));
      g_error_free (error);
      g_output_stream_close (G_OUTPUT_STREAM (render_and_stream.stream), NULL, NULL);
      handle = gdk_win32_hdata_output_stream_get_handle (render_and_stream.stream, &is_hdata);

      if (is_hdata)
        API_CALL (GlobalFree, (handle));
      else
        API_CALL (CloseHandle, (handle));

      render_and_stream.render->main_thread_data_handle = NULL;
    }
  else
    {
      g_output_stream_close (G_OUTPUT_STREAM (render_and_stream.stream), NULL, NULL);
      render_and_stream.render->main_thread_data_handle = gdk_win32_hdata_output_stream_get_handle (render_and_stream.stream, NULL);
    }

  g_async_queue_push (clipdrop->clipboard_render_queue, render_and_stream.render);
  g_object_unref (render_and_stream.stream);
}

/* This function is called in the main thread */
static gboolean
clipboard_render (gpointer user_data)
{
  GdkWin32ClipboardThreadRender *render = (GdkWin32ClipboardThreadRender *) user_data;
  GdkWin32Clipdrop *clipdrop = _gdk_win32_clipdrop_get ();
  GdkDisplay *display = gdk_display_get_default ();
  GdkClipboard *clipboard = gdk_display_get_clipboard (display);
  GError *error = NULL;
  GOutputStream *stream = gdk_win32_hdata_output_stream_new (&render->pair, &error);
  GdkWin32ClipboardRenderAndStream *render_and_stream;

  if (stream == NULL)
    {
      GDK_NOTE (SELECTION, g_printerr ("%p: failed create a HData-backed stream: %s\n", clipboard, error->message));
      g_error_free (error);
      render->main_thread_data_handle = NULL;
      g_async_queue_push (clipdrop->clipboard_render_queue, render);

      return G_SOURCE_REMOVE;
    }

  render_and_stream = g_new0 (GdkWin32ClipboardRenderAndStream, 1);
  render_and_stream->render = render;
  render_and_stream->stream = GDK_WIN32_HDATA_OUTPUT_STREAM (stream);

  gdk_clipboard_write_async (GDK_CLIPBOARD (clipboard),
                             render->pair.contentformat,
                             stream,
                             G_PRIORITY_DEFAULT,
                             NULL,
                             clipboard_render_hdata_ready,
                             render_and_stream);

  /* Keep our reference to the stream, don't unref it */

  return G_SOURCE_REMOVE;
}

/* This function is called in the main thread */
static gboolean
clipboard_thread_response (gpointer user_data)
{
  GdkWin32ClipboardThreadResponse *response = (GdkWin32ClipboardThreadResponse *) user_data;
  GTask *task = (GTask *) response->opaque_task;

  if (task != NULL)
    {
      if (response->error)
        g_task_return_error (task, response->error);
      else if (response->input_stream)
        g_task_return_pointer (task, response->input_stream, g_object_unref);
      else
        g_task_return_boolean (task, TRUE);

      g_object_unref (task);
    }

  g_free (response);

  return G_SOURCE_REMOVE;
}

static void
free_prep_element (GdkWin32ClipboardStorePrepElement *el)
{
  if (el->handle)
    {
      if (_gdk_win32_format_uses_hdata (el->w32format))
        GlobalFree (el->handle);
      else
        CloseHandle (el->handle);
    }

  if (el->stream)
    g_object_unref (el->stream);
}

static void
free_queue_item (GdkWin32ClipboardThreadQueueItem *item)
{
  GdkWin32ClipboardThreadAdvertise *adv;
  GdkWin32ClipboardThreadRetrieve *retr;
  GdkWin32ClipboardThreadStore    *store;
  int i;

  switch (item->item_type)
    {
    case GDK_WIN32_CLIPBOARD_THREAD_QUEUE_ITEM_ADVERTISE:
      adv = (GdkWin32ClipboardThreadAdvertise *) item;
      if (adv->pairs)
        g_array_free (adv->pairs, TRUE);
      break;
    case GDK_WIN32_CLIPBOARD_THREAD_QUEUE_ITEM_RETRIEVE:
      retr = (GdkWin32ClipboardThreadRetrieve *) item;
      if (retr->pairs)
        g_array_free (retr->pairs, TRUE);
      break;
    case GDK_WIN32_CLIPBOARD_THREAD_QUEUE_ITEM_STORE:
      store = (GdkWin32ClipboardThreadStore *) item;
      for (i = 0; i < store->elements->len; i++)
        {
          GdkWin32ClipboardStorePrepElement *el = &g_array_index (store->elements, GdkWin32ClipboardStorePrepElement, i);
          free_prep_element (el);
        }
      g_array_free (store->elements, TRUE);
      break;
    }

  g_free (item);
}

static void
send_response (GdkWin32ClipboardThreadQueueItemType  request_type,
               gpointer                              opaque_task,
               GError                               *error)
{
  GdkWin32ClipboardThreadResponse *response = g_new0 (GdkWin32ClipboardThreadResponse, 1);
  response->error = error;
  response->opaque_task = opaque_task;
  response->item_type = request_type;
  g_idle_add_full (G_PRIORITY_DEFAULT, clipboard_thread_response, response, NULL);
}

static void
send_input_stream (GdkWin32ClipboardThreadQueueItemType  request_type,
                   gpointer                              opaque_task,
                   GInputStream                         *stream)
{
  GdkWin32ClipboardThreadResponse *response = g_new0 (GdkWin32ClipboardThreadResponse, 1);
  response->input_stream = stream;
  response->opaque_task = opaque_task;
  response->item_type = request_type;
  g_idle_add_full (G_PRIORITY_DEFAULT, clipboard_thread_response, response, NULL);
}

static DWORD
try_open_clipboard (HWND hwnd)
{
  if (clipboard_thread_data->clipboard_opened_for == hwnd)
    return NO_ERROR;

  if (clipboard_thread_data->clipboard_opened_for != INVALID_HANDLE_VALUE)
    {
      API_CALL (CloseClipboard, ());
      clipboard_thread_data->clipboard_opened_for = INVALID_HANDLE_VALUE;
    }

  if (!OpenClipboard (hwnd))
    return GetLastError ();

  clipboard_thread_data->clipboard_opened_for = hwnd;

  return NO_ERROR;
}

static gboolean
process_advertise (GdkWin32ClipboardThreadAdvertise *adv)
{
  DWORD error_code;
  int i;

  if (g_get_monotonic_time () > adv->parent.end_time)
    {
      GDK_NOTE (CLIPBOARD, g_printerr ("An advertise task timed out\n"));
      send_response (adv->parent.item_type,
                     adv->parent.opaque_task,
                     g_error_new (G_IO_ERROR, G_IO_ERROR_FAILED,
                                  _("Cannot claim clipboard ownership. OpenClipboard() timed out.")));
      return FALSE;
    }

  if (clipboard_thread_data->owner_change_time > adv->parent.start_time)
    {
      GDK_NOTE (CLIPBOARD, g_printerr ("An advertise task timed out due to ownership change\n"));
      send_response (adv->parent.item_type,
                     adv->parent.opaque_task,
                     g_error_new (G_IO_ERROR, G_IO_ERROR_FAILED,
                                  _("Cannot claim clipboard ownership. Another process claimed it before us.")));
      return FALSE;
    }

  error_code = try_open_clipboard (adv->unset ? NULL : clipboard_thread_data->clipboard_window);

  if (error_code == ERROR_ACCESS_DENIED)
    return TRUE;

  if (G_UNLIKELY (error_code != NO_ERROR))
    {
      send_response (adv->parent.item_type,
                     adv->parent.opaque_task,
                     g_error_new (G_IO_ERROR, G_IO_ERROR_FAILED,
                                  _("Cannot claim clipboard ownership. OpenClipboard() failed: 0x%lx."), error_code));
      return FALSE;
    }

  clipboard_thread_data->ignore_destroy_clipboard = TRUE;
  if (!EmptyClipboard ())
    {
      clipboard_thread_data->ignore_destroy_clipboard = FALSE;
      error_code = GetLastError ();
      send_response (adv->parent.item_type,
                     adv->parent.opaque_task,
                     g_error_new (G_IO_ERROR, G_IO_ERROR_FAILED,
                                  _("Cannot claim clipboard ownership. EmptyClipboard() failed: 0x%lx."), error_code));
      return FALSE;
    }

  clipboard_thread_data->ignore_destroy_clipboard = FALSE;

  if (adv->unset)
    return FALSE;

  for (i = 0; i < adv->pairs->len; i++)
    {
      GdkWin32ContentFormatPair *pair = &g_array_index (adv->pairs, GdkWin32ContentFormatPair, i);

      SetClipboardData (pair->w32format, NULL);
    }

  if (clipboard_thread_data->cached_advertisement)
    g_array_free (clipboard_thread_data->cached_advertisement, TRUE);

  clipboard_thread_data->cached_advertisement = adv->pairs;

  /* To enure that we don't free it later on */
  adv->pairs = NULL;

  send_response (adv->parent.item_type,
                 adv->parent.opaque_task,
                 NULL);

  return FALSE;
}

static gboolean
process_store (GdkWin32ClipboardThreadStore *store)
{
  DWORD error_code;
  int i;

  if (g_get_monotonic_time () > store->parent.end_time)
    {
      GDK_NOTE (CLIPBOARD, g_printerr ("A store task timed out\n"));
      send_response (store->parent.item_type,
                     store->parent.opaque_task,
                     g_error_new (G_IO_ERROR, G_IO_ERROR_FAILED,
                                  _("Cannot set clipboard data. OpenClipboard() timed out.")));
      return FALSE;
    }

  if (clipboard_thread_data->owner_change_time > store->parent.start_time)
    {
      GDK_NOTE (CLIPBOARD, g_printerr ("A store task timed out due to ownership change\n"));
      send_response (store->parent.item_type,
                     store->parent.opaque_task,
                     g_error_new (G_IO_ERROR, G_IO_ERROR_FAILED,
                                  _("Cannot set clipboard data. Another process claimed clipboard ownership.")));
      return FALSE;
    }

  error_code = try_open_clipboard (clipboard_thread_data->clipboard_window);

  if (error_code == ERROR_ACCESS_DENIED)
    return TRUE;

  if (G_UNLIKELY (error_code != NO_ERROR))
    {
      send_response (store->parent.item_type,
                     store->parent.opaque_task,
                     g_error_new (G_IO_ERROR, G_IO_ERROR_FAILED,
                                  _("Cannot set clipboard data. OpenClipboard() failed: 0x%lx."), error_code));
      return FALSE;
    }

  /* It's possible for another process to claim ownership
   * between between us entering this function and us opening the clipboard.
   * So check the ownership one last time.
   * Unlike the advertisement routine above, here we don't want to
   * claim clipboard ownership - we want to store stuff in the clipboard
   * that we already own, otherwise we're just killing stuff that some other
   * process put in there, which is not nice.
   */
  if (GetClipboardOwner () != clipboard_thread_data->clipboard_window)
    {
      send_response (store->parent.item_type,
                     store->parent.opaque_task,
                     g_error_new (G_IO_ERROR, G_IO_ERROR_FAILED,
                                  _("Cannot set clipboard data. Another process claimed clipboard ownership.")));
      return FALSE;
    }

  for (i = 0; i < store->elements->len; i++)
    {
      GdkWin32ClipboardStorePrepElement *el = &g_array_index (store->elements, GdkWin32ClipboardStorePrepElement, i);
      if (el->handle != NULL && el->w32format != 0)
        if (SetClipboardData (el->w32format, el->handle))
          el->handle = NULL; /* the OS now owns the handle, don't free it later on */
    }

  send_response (store->parent.item_type,
                 store->parent.opaque_task,
                 NULL);

  return FALSE;
}

static gpointer
grab_data_from_hdata (GdkWin32ClipboardThreadRetrieve *retr,
                      HANDLE                           hdata,
                      gsize                           *data_len)
{
  gpointer ptr;
  SIZE_T length;
  guchar *data;

  ptr = GlobalLock (hdata);
  if (ptr == NULL)
    {
      DWORD error_code = GetLastError ();
      send_response (retr->parent.item_type,
                     retr->parent.opaque_task,
                     g_error_new (G_IO_ERROR, G_IO_ERROR_FAILED,
                                  _("Cannot get clipboard data. GlobalLock(0x%p) failed: 0x%lx."), hdata, error_code));
      return NULL;
    }

  length = GlobalSize (hdata);
  if (length == 0 && GetLastError () != NO_ERROR)
    {
      DWORD error_code = GetLastError ();
      send_response (retr->parent.item_type,
                     retr->parent.opaque_task,
                     g_error_new (G_IO_ERROR, G_IO_ERROR_FAILED,
                                  _("Cannot get clipboard data. GlobalSize(0x%p) failed: 0x%lx."), hdata, error_code));
      GlobalUnlock (hdata);
      return NULL;
    }

  data = g_try_malloc (length);

  if (data == NULL)
    {
      char *length_str = g_strdup_printf ("%" G_GSIZE_FORMAT, length);
      send_response (retr->parent.item_type,
                     retr->parent.opaque_task,
                     g_error_new (G_IO_ERROR, G_IO_ERROR_FAILED,
                                  _("Cannot get clipboard data. Failed to allocate %s bytes to store the data."), length_str));
      g_free (length_str);
      GlobalUnlock (hdata);
      return NULL;
    }

  memcpy (data, ptr, length);
  *data_len = length;

  GlobalUnlock (hdata);

  return data;
}

static gboolean
process_retrieve (GdkWin32ClipboardThreadRetrieve *retr)
{
  DWORD error_code;
  int i;
  UINT fmt, fmt_to_use;
  HANDLE hdata;
  GdkWin32ContentFormatPair *pair;
  guchar *data;
  gsize   data_len;
  GInputStream *stream;

  if (g_get_monotonic_time () > retr->parent.end_time)
    {
      GDK_NOTE (CLIPBOARD, g_printerr ("A retrieve task timed out\n"));
      send_response (retr->parent.item_type,
                     retr->parent.opaque_task,
                     g_error_new (G_IO_ERROR, G_IO_ERROR_FAILED,
                                  _("Cannot get clipboard data. OpenClipboard() timed out.")));
      return FALSE;
    }

  if (clipboard_thread_data->owner_change_time > retr->parent.start_time)
    {
      GDK_NOTE (CLIPBOARD, g_printerr ("A retrieve task timed out due to ownership change\n"));
      send_response (retr->parent.item_type,
                     retr->parent.opaque_task,
                     g_error_new (G_IO_ERROR, G_IO_ERROR_FAILED,
                                  _("Cannot get clipboard data. Clipboard ownership changed.")));
      return FALSE;
    }

  if (GetClipboardSequenceNumber () > retr->sequence_number)
    {
      GDK_NOTE (CLIPBOARD, g_printerr ("A retrieve task timed out due to data change\n"));
      send_response (retr->parent.item_type,
                     retr->parent.opaque_task,
                     g_error_new (G_IO_ERROR, G_IO_ERROR_FAILED,
                                  _("Cannot get clipboard data. Clipboard data changed before we could get it.")));
      return FALSE;
    }

  if (clipboard_thread_data->clipboard_opened_for == INVALID_HANDLE_VALUE)
    error_code = try_open_clipboard (clipboard_thread_data->clipboard_window);
  else
    error_code = try_open_clipboard (clipboard_thread_data->clipboard_opened_for);

  if (error_code == ERROR_ACCESS_DENIED)
    return TRUE;

  if (G_UNLIKELY (error_code != NO_ERROR))
    {
      send_response (retr->parent.item_type,
                     retr->parent.opaque_task,
                     g_error_new (G_IO_ERROR, G_IO_ERROR_FAILED,
                                  _("Cannot get clipboard data. OpenClipboard() failed: 0x%lx."), error_code));
      return FALSE;
    }

  for (fmt_to_use = 0, pair = NULL, fmt = 0;
       fmt_to_use == 0 && 0 != (fmt = EnumClipboardFormats (fmt));
      )
    {
      for (i = 0; i < retr->pairs->len; i++)
        {
          pair = &g_array_index (retr->pairs, GdkWin32ContentFormatPair, i);

          if (pair->w32format != fmt)
            continue;

          fmt_to_use = fmt;
          break;
        }
    }

  if (!fmt_to_use)
    {
      send_response (retr->parent.item_type,
                     retr->parent.opaque_task,
                     g_error_new (G_IO_ERROR, G_IO_ERROR_FAILED,
                                  _("Cannot get clipboard data. No compatible transfer format found.")));
      return FALSE;
    }

  if ((hdata = GetClipboardData (fmt_to_use)) == NULL)
    {
      error_code = GetLastError ();
      send_response (retr->parent.item_type,
                     retr->parent.opaque_task,
                     g_error_new (G_IO_ERROR, G_IO_ERROR_FAILED,
                                  _("Cannot get clipboard data. GetClipboardData() failed: 0x%lx."), error_code));
      return FALSE;
    }

  if (!pair->transmute)
    {
      if (_gdk_win32_format_uses_hdata (pair->w32format))
        {
          data = grab_data_from_hdata (retr, hdata, &data_len);

          if (data == NULL)
            return FALSE;
        }
      else
        {
          data_len = sizeof (HANDLE);
          data = g_malloc (data_len);
          memcpy (data, &hdata, data_len);
        }
    }
  else
    {
      _gdk_win32_transmute_windows_data (pair->w32format, pair->contentformat, hdata, &data, &data_len);

      if (data == NULL)
        return FALSE;
    }

  stream = g_memory_input_stream_new_from_data (data, data_len, g_free);
  g_object_set_data (G_OBJECT (stream), "gdk-clipboard-stream-contenttype", (gpointer) pair->contentformat);

  GDK_NOTE (CLIPBOARD, g_printerr ("reading clipboard data from a %" G_GSIZE_FORMAT "-byte buffer\n",
                                   data_len));
  send_input_stream (retr->parent.item_type,
                     retr->parent.opaque_task,
                     stream);

  return FALSE;
}

static gboolean
process_clipboard_queue ()
{
  GdkWin32ClipboardThreadQueueItem *placeholder;
  GList *p;
  gboolean try_again;
  GList *p_next;

  for (p = clipboard_thread_data->dequeued_items, p_next = NULL; p; p = p_next)
    {
      placeholder = (GdkWin32ClipboardThreadQueueItem *) p->data;
      p_next = p->next;

      switch (placeholder->item_type)
        {
        case GDK_WIN32_CLIPBOARD_THREAD_QUEUE_ITEM_ADVERTISE:
          try_again = process_advertise ((GdkWin32ClipboardThreadAdvertise *) placeholder);
          break;
        case GDK_WIN32_CLIPBOARD_THREAD_QUEUE_ITEM_RETRIEVE:
          try_again = process_retrieve ((GdkWin32ClipboardThreadRetrieve *) placeholder);
          break;
        case GDK_WIN32_CLIPBOARD_THREAD_QUEUE_ITEM_STORE:
          try_again = process_store ((GdkWin32ClipboardThreadStore *) placeholder);
          break;
        }

      if (try_again)
        return FALSE;

      clipboard_thread_data->dequeued_items = g_list_delete_link (clipboard_thread_data->dequeued_items, p);
      free_queue_item (placeholder);
    }

  while ((placeholder = g_async_queue_try_pop (clipboard_thread_data->input_queue)) != NULL)
    {
      switch (placeholder->item_type)
        {
        case GDK_WIN32_CLIPBOARD_THREAD_QUEUE_ITEM_ADVERTISE:
          try_again = process_advertise ((GdkWin32ClipboardThreadAdvertise *) placeholder);
          break;
        case GDK_WIN32_CLIPBOARD_THREAD_QUEUE_ITEM_RETRIEVE:
          try_again = process_retrieve ((GdkWin32ClipboardThreadRetrieve *) placeholder);
          break;
        case GDK_WIN32_CLIPBOARD_THREAD_QUEUE_ITEM_STORE:
          try_again = process_store ((GdkWin32ClipboardThreadStore *) placeholder);
          break;
        }

      if (!try_again)
        {
          free_queue_item (placeholder);
          continue;
        }

      clipboard_thread_data->dequeued_items = g_list_append (clipboard_thread_data->dequeued_items, placeholder);

      return FALSE;
    }

  return TRUE;
}

static void
discard_render (GdkWin32ClipboardThreadRender *render,
                gboolean                       dont_touch_the_handle)
{
  GdkWin32ClipboardThreadRender render_copy = *render;

  g_free (render);

  if (dont_touch_the_handle || render_copy.main_thread_data_handle == NULL)
    return;

  if (_gdk_win32_format_uses_hdata (render_copy.pair.w32format))
    API_CALL (GlobalFree, (render_copy.main_thread_data_handle));
  else
    API_CALL (CloseHandle, (render_copy.main_thread_data_handle));
}

static LRESULT
inner_clipboard_window_procedure (HWND   hwnd,
                                  UINT   message,
                                  WPARAM wparam,
                                  LPARAM lparam)
{
  if (message == thread_wakeup_message ||
      message == WM_TIMER)
    {
      gboolean queue_is_empty = FALSE;

      if (clipboard_thread_data == NULL)
        {
          g_warning ("Clipboard thread got an actionable message with no thread data");
          return DefWindowProcW (hwnd, message, wparam, lparam);
        }

      queue_is_empty = process_clipboard_queue ();

      if (queue_is_empty && clipboard_thread_data->wakeup_timer)
        {
          API_CALL (KillTimer, (clipboard_thread_data->clipboard_window, clipboard_thread_data->wakeup_timer));
          clipboard_thread_data->wakeup_timer = 0;
        }

      /* Close the clipboard after each queue run, if it's open.
       * It would be wrong to keep it open, even if we would
       * need it again a second later.
       * queue_is_empty == FALSE implies that the clipboard
       * is closed already, but it's better to be sure.
       */
      if (clipboard_thread_data->clipboard_opened_for != INVALID_HANDLE_VALUE)
        {
          API_CALL (CloseClipboard, ());
          clipboard_thread_data->clipboard_opened_for = INVALID_HANDLE_VALUE;
        }

      if (queue_is_empty ||
          clipboard_thread_data->wakeup_timer != 0)
        return 0;

      if (SetTimer (clipboard_thread_data->clipboard_window, 1, 1000, NULL))
        clipboard_thread_data->wakeup_timer = 1;
      else
        g_critical ("Failed to set a timer for the clipboard window 0x%p: %lu",
                    clipboard_thread_data->clipboard_window,
                    GetLastError ());
    }

  switch (message)
    {
    case WM_DESTROY: /* unregister the clipboard listener */
      {
        if (clipboard_thread_data == NULL)
          {
            g_warning ("Clipboard thread got an actionable message with no thread data");
            return DefWindowProcW (hwnd, message, wparam, lparam);
          }

        RemoveClipboardFormatListener (hwnd);
        PostQuitMessage (0);
        return 0;
      }
    case WM_DESTROYCLIPBOARD:
      {
        return 0;
      }
    case WM_CLIPBOARDUPDATE:
      {
        HWND hwnd_owner;
        HWND hwnd_opener;
/*
        GdkEvent *event;
*/
        if (clipboard_thread_data == NULL)
          {
            g_warning ("Clipboard thread got an actionable message with no thread data");
            return DefWindowProcW (hwnd, message, wparam, lparam);
          }

        SetLastError (0);
        hwnd_owner = GetClipboardOwner ();
        if (hwnd_owner == NULL && GetLastError () != 0)
          WIN32_API_FAILED ("GetClipboardOwner");

        hwnd_opener = GetOpenClipboardWindow ();

        GDK_NOTE (DND, g_print (" drawclipboard owner: %p; opener %p ", hwnd_owner, hwnd_opener));

        if (GDK_DEBUG_CHECK (DND))
          {
            /* FIXME: grab and print clipboard formats without opening the clipboard
            if (clipboard_thread_data->clipboard_opened_for != INVALID_HANDLE_VALUE ||
                OpenClipboard (hwnd))
              {
                UINT nFormat = 0;

                while ((nFormat = EnumClipboardFormats (nFormat)) != 0)
                  g_print ("%s ", _gdk_win32_cf_to_string (nFormat));

                if (clipboard_thread_data->clipboard_opened_for == INVALID_HANDLE_VALUE)
                  CloseClipboard ();
              }
            else
              {
                WIN32_API_FAILED ("OpenClipboard");
              }
             */
          }

        GDK_NOTE (DND, g_print (" \n"));

        if (clipboard_thread_data->stored_hwnd_owner != hwnd_owner)
          {
            clipboard_thread_data->stored_hwnd_owner = hwnd_owner;
            clipboard_thread_data->owner_change_time = g_get_monotonic_time ();

            if (hwnd_owner != clipboard_thread_data->clipboard_window)
              {
                if (clipboard_thread_data->cached_advertisement)
                  g_array_free (clipboard_thread_data->cached_advertisement, TRUE);

                clipboard_thread_data->cached_advertisement = NULL;
              }

            API_CALL (PostMessage, (clipboard_thread_data->clipboard_window, thread_wakeup_message, 0, 0));

            if (hwnd_owner != clipboard_thread_data->clipboard_window)
              g_idle_add_full (G_PRIORITY_DEFAULT, clipboard_owner_changed, NULL, NULL);
          }

        /* clear error to avoid confusing SetClipboardViewer() return */
        SetLastError (0);

        return 0;
      }
    case WM_RENDERALLFORMATS:
      {
        if (clipboard_thread_data == NULL)
          {
            g_warning ("Clipboard thread got an actionable message with no thread data");
            return DefWindowProcW (hwnd, message, wparam, lparam);
          }

        if (clipboard_thread_data->cached_advertisement == NULL)
          return DefWindowProcW (hwnd, message, wparam, lparam);

        if (API_CALL (OpenClipboard, (hwnd)))
          {
            int i;
            GdkWin32ContentFormatPair *pair;

            for (pair = NULL, i = 0;
                 i < clipboard_thread_data->cached_advertisement->len;
                 i++)
              {
                pair = &g_array_index (clipboard_thread_data->cached_advertisement, GdkWin32ContentFormatPair, i);

                if (pair->w32format != 0)
                  SendMessage (hwnd, WM_RENDERFORMAT, pair->w32format, 0);
              }

            API_CALL (CloseClipboard, ());
          }

        return 0;
      }
    case WM_RENDERFORMAT:
      {
        int i;
        GdkWin32ClipboardThreadRender *render;
        GdkWin32ClipboardThreadRender *returned_render;
        GdkWin32ContentFormatPair *pair;


        GDK_NOTE (EVENTS, g_print (" %s", _gdk_win32_cf_to_string (wparam)));

        if (clipboard_thread_data == NULL)
          {
            g_warning ("Clipboard thread got an actionable message with no thread data");
            return DefWindowProcW (hwnd, message, wparam, lparam);
          }

        if (clipboard_thread_data->cached_advertisement == NULL)
          return DefWindowProcW (hwnd, message, wparam, lparam);

        for (pair = NULL, i = 0;
             i < clipboard_thread_data->cached_advertisement->len;
             i++)
          {
            pair = &g_array_index (clipboard_thread_data->cached_advertisement, GdkWin32ContentFormatPair, i);

            if (pair->w32format == wparam)
              break;

            pair = NULL;
          }

        if (pair == NULL)
          {
            GDK_NOTE (EVENTS, g_print (" (contentformat not found)"));
            return DefWindowProcW (hwnd, message, wparam, lparam);
          }

        /* Clear the queue */
        while ((render = g_async_queue_try_pop (clipboard_thread_data->render_queue)) != NULL)
          discard_render (render, FALSE);

        render = g_new0 (GdkWin32ClipboardThreadRender, 1);
        render->pair = *pair;
        g_idle_add_full (G_PRIORITY_DEFAULT, clipboard_render, render, NULL);
        returned_render = g_async_queue_timeout_pop (clipboard_thread_data->render_queue, CLIPBOARD_RENDER_TIMEOUT);

        /* We should get back the same pointer, ignore everything else. */
        while (returned_render != NULL && returned_render != render)
        {
          discard_render (returned_render, FALSE);
          /* Technically, we should use timed pop here as well,
           * as it's *possible* for a late render struct
           * to come down the queue just after we cleared
           * the queue, but before our idle function
           * triggered the actual render to be pushed.
           * If you get many "Clipboard rendering timed out" warnings,
           * this is probably why.
           */
          returned_render = g_async_queue_try_pop (clipboard_thread_data->render_queue);
        }

        /* Just in case */
        render = NULL;

        if (returned_render == NULL)
          {
            g_warning ("Clipboard rendering timed out");
          }
        else if (returned_render->main_thread_data_handle)
          {
            BOOL set_data_succeeded;
            /* The requester is holding the clipboard, no
             * OpenClipboard() is required/possible
             */
            GDK_NOTE (DND,
                      g_print (" SetClipboardData (%s, %p)\n",
                               _gdk_win32_cf_to_string (wparam),
                               returned_render->main_thread_data_handle));

            SetLastError (0);
            set_data_succeeded = (SetClipboardData (wparam, returned_render->main_thread_data_handle) != NULL);

            if (!set_data_succeeded)
              WIN32_API_FAILED ("SetClipboardData");

            discard_render (returned_render, set_data_succeeded);
          }

        return 0;
      }
    default:
      /* Otherwise call DefWindowProcW(). */
      GDK_NOTE (EVENTS, g_print (" DefWindowProcW"));
      return DefWindowProcW (hwnd, message, wparam, lparam);
    }
}

LRESULT CALLBACK
_clipboard_window_procedure (HWND   hwnd,
                             UINT   message,
                             WPARAM wparam,
                             LPARAM lparam)
{
  LRESULT retval;

  GDK_NOTE (EVENTS, g_print ("clipboard thread %s %p",
			     _gdk_win32_message_to_string (message), hwnd));
  retval = inner_clipboard_window_procedure (hwnd, message, wparam, lparam);

  GDK_NOTE (EVENTS, g_print (" => %" G_GINT64_FORMAT "\n", (gint64) retval));

  return retval;
}

/*
 * Creates a hidden window and add a clipboard listener
 */
static gboolean
register_clipboard_notification ()
{
  WNDCLASS wclass = { 0, };
  ATOM klass;

  wclass.lpszClassName = L"GdkClipboardNotification";
  wclass.lpfnWndProc = _clipboard_window_procedure;
  wclass.hInstance = this_module ();
  wclass.cbWndExtra = sizeof (GdkWin32ClipboardThread *);

  klass = RegisterClass (&wclass);
  if (!klass)
    return FALSE;

  clipboard_thread_data->clipboard_window = CreateWindow (MAKEINTRESOURCE (klass),
                                                NULL, WS_POPUP,
                                                0, 0, 0, 0, NULL, NULL,
                                                this_module (), NULL);

  if (clipboard_thread_data->clipboard_window == NULL)
    goto failed;

  SetLastError (0);

  if (AddClipboardFormatListener (clipboard_thread_data->clipboard_window) == FALSE)
    {
      DestroyWindow (clipboard_thread_data->clipboard_window);
      goto failed;
    }

  g_idle_add_full (G_PRIORITY_DEFAULT, clipboard_window_created, (gpointer) clipboard_thread_data->clipboard_window, NULL);

  return TRUE;

failed:
  g_critical ("Failed to install clipboard viewer");
  UnregisterClass (MAKEINTRESOURCE (klass), this_module ());
  return FALSE;
}

static gpointer
_gdk_win32_clipboard_thread_main (gpointer data)
{
  MSG msg;
  GAsyncQueue *queue = (GAsyncQueue *) data;
  GAsyncQueue *render_queue = (GAsyncQueue *) g_async_queue_pop (queue);

  g_assert (clipboard_thread_data == NULL);

  clipboard_thread_data = g_new0 (GdkWin32ClipboardThread, 1);
  clipboard_thread_data->input_queue = queue;
  clipboard_thread_data->render_queue = render_queue;
  clipboard_thread_data->clipboard_opened_for = INVALID_HANDLE_VALUE;

  if (!register_clipboard_notification ())
    {
      g_async_queue_unref (queue);
      g_clear_pointer (&clipboard_thread_data, g_free);

      return NULL;
    }

  while (GetMessage (&msg, NULL, 0, 0))
    {
      TranslateMessage (&msg); 
      DispatchMessage (&msg); 
    }

  /* Just in case, as this should only happen when we shut down */
  DestroyWindow (clipboard_thread_data->clipboard_window);
  CloseHandle (clipboard_thread_data->clipboard_window);
  g_async_queue_unref (queue);
  g_clear_pointer (&clipboard_thread_data, g_free);

  return NULL;
}

G_DEFINE_TYPE (GdkWin32Clipdrop, gdk_win32_clipdrop, G_TYPE_OBJECT)

static void
gdk_win32_clipdrop_class_init (GdkWin32ClipdropClass *klass)
{
}

void
_gdk_win32_clipdrop_init (void)
{
  _win32_main_thread = g_thread_self ();
  _win32_clipdrop = GDK_WIN32_CLIPDROP (g_object_new (GDK_TYPE_WIN32_CLIPDROP, NULL));
}

static void
gdk_win32_clipdrop_init (GdkWin32Clipdrop *win32_clipdrop)
{
  GArray             *atoms;
  GArray             *cfs;
  GSList             *pixbuf_formats;
  GSList             *rover;
  int                 i;
  GArray             *comp;
  GdkWin32ContentFormatPair fmt;
  HMODULE                   user32;

  thread_wakeup_message = RegisterWindowMessage (L"GDK_WORKER_THREAD_WEAKEUP");

  user32 = LoadLibrary (L"user32.dll");
  win32_clipdrop->GetUpdatedClipboardFormats = (GetUpdatedClipboardFormatsFunc) GetProcAddress (user32, "GetUpdatedClipboardFormats");
  FreeLibrary (user32);

  atoms = g_array_sized_new (FALSE, TRUE, sizeof (const char *), GDK_WIN32_ATOM_INDEX_LAST);
  g_array_set_size (atoms, GDK_WIN32_ATOM_INDEX_LAST);
  cfs = g_array_sized_new (FALSE, TRUE, sizeof (UINT), GDK_WIN32_CF_INDEX_LAST);
  g_array_set_size (cfs, GDK_WIN32_CF_INDEX_LAST);

  win32_clipdrop->known_atoms = atoms;
  win32_clipdrop->known_clipboard_formats = cfs;

  _gdk_atom_array_index (atoms, GDK_WIN32_ATOM_INDEX_GDK_SELECTION) = g_intern_static_string ("GDK_SELECTION");
  _gdk_atom_array_index (atoms, GDK_WIN32_ATOM_INDEX_CLIPBOARD_MANAGER) = g_intern_static_string ("CLIPBOARD_MANAGER");
  _gdk_atom_array_index (atoms, GDK_WIN32_ATOM_INDEX_WM_TRANSIENT_FOR) = g_intern_static_string ("WM_TRANSIENT_FOR");
  _gdk_atom_array_index (atoms, GDK_WIN32_ATOM_INDEX_TARGETS) = g_intern_static_string ("TARGETS");
  _gdk_atom_array_index (atoms, GDK_WIN32_ATOM_INDEX_DELETE) = g_intern_static_string ("DELETE");
  _gdk_atom_array_index (atoms, GDK_WIN32_ATOM_INDEX_SAVE_TARGETS) = g_intern_static_string ("SAVE_TARGETS");
  _gdk_atom_array_index (atoms, GDK_WIN32_ATOM_INDEX_TEXT_PLAIN_UTF8) = g_intern_static_string ("text/plain;charset=utf-8");
  _gdk_atom_array_index (atoms, GDK_WIN32_ATOM_INDEX_TEXT_PLAIN) = g_intern_static_string ("text/plain");
  _gdk_atom_array_index (atoms, GDK_WIN32_ATOM_INDEX_TEXT_URI_LIST) = g_intern_static_string ("text/uri-list");
  _gdk_atom_array_index (atoms, GDK_WIN32_ATOM_INDEX_TEXT_HTML) = g_intern_static_string ("text/html");
  _gdk_atom_array_index (atoms, GDK_WIN32_ATOM_INDEX_IMAGE_PNG) = g_intern_static_string ("image/png");
  _gdk_atom_array_index (atoms, GDK_WIN32_ATOM_INDEX_IMAGE_JPEG) = g_intern_static_string ("image/jpeg");
  _gdk_atom_array_index (atoms, GDK_WIN32_ATOM_INDEX_IMAGE_BMP) = g_intern_static_string ("image/bmp");
  _gdk_atom_array_index (atoms, GDK_WIN32_ATOM_INDEX_IMAGE_GIF) = g_intern_static_string ("image/gif");

  _gdk_atom_array_index (atoms, GDK_WIN32_ATOM_INDEX_LOCAL_DND_SELECTION) = g_intern_static_string ("LocalDndSelection");
  _gdk_atom_array_index (atoms, GDK_WIN32_ATOM_INDEX_DROPFILES_DND) = g_intern_static_string ("DROPFILES_DND");
  _gdk_atom_array_index (atoms, GDK_WIN32_ATOM_INDEX_OLE2_DND) = g_intern_static_string ("OLE2_DND");

  _gdk_atom_array_index (atoms, GDK_WIN32_ATOM_INDEX_PNG)= g_intern_static_string ("PNG");
  _gdk_atom_array_index (atoms, GDK_WIN32_ATOM_INDEX_JFIF) = g_intern_static_string ("JFIF");
  _gdk_atom_array_index (atoms, GDK_WIN32_ATOM_INDEX_GIF) = g_intern_static_string ("GIF");

  /* These are a bit unusual. It's here to allow GTK applications
   * to actually support CF_DIB and Shell ID List clipboard formats on their own,
   * instead of allowing GDK to use them internally for interoperability.
   */
  _gdk_atom_array_index (atoms, GDK_WIN32_ATOM_INDEX_CF_DIB) = g_intern_static_string ("application/x.windows.CF_DIB");
  _gdk_atom_array_index (atoms, GDK_WIN32_ATOM_INDEX_CFSTR_SHELLIDLIST) = g_intern_static_string ("application/x.windows.Shell IDList Array");
  _gdk_atom_array_index (atoms, GDK_WIN32_ATOM_INDEX_CF_UNICODETEXT) = g_intern_static_string ("application/x.windows.CF_UNICODETEXT");
  _gdk_atom_array_index (atoms, GDK_WIN32_ATOM_INDEX_CF_TEXT) = g_intern_static_string ("application/x.windows.CF_TEXT");

  /* MS Office 2007, at least, offers images in common file formats
   * using clipboard format names like "PNG" and "JFIF". So we follow
   * the lead and map the GDK contentformat "image/png" to the clipboard
   * format name "PNG" etc.
   */
  _gdk_cf_array_index (cfs, GDK_WIN32_CF_INDEX_PNG) = RegisterClipboardFormat (L"PNG");
  _gdk_cf_array_index (cfs, GDK_WIN32_CF_INDEX_JFIF) = RegisterClipboardFormat (L"JFIF");
  _gdk_cf_array_index (cfs, GDK_WIN32_CF_INDEX_GIF) = RegisterClipboardFormat (L"GIF");

  _gdk_cf_array_index (cfs, GDK_WIN32_CF_INDEX_UNIFORMRESOURCELOCATORW) = RegisterClipboardFormat (L"UniformResourceLocatorW");
  _gdk_cf_array_index (cfs, GDK_WIN32_CF_INDEX_CFSTR_SHELLIDLIST) = RegisterClipboardFormat (CFSTR_SHELLIDLIST);
  _gdk_cf_array_index (cfs, GDK_WIN32_CF_INDEX_HTML_FORMAT) = RegisterClipboardFormat (L"HTML Format");
  _gdk_cf_array_index (cfs, GDK_WIN32_CF_INDEX_TEXT_HTML) = RegisterClipboardFormat (L"text/html");

  _gdk_cf_array_index (cfs, GDK_WIN32_CF_INDEX_IMAGE_PNG) = RegisterClipboardFormat (L"image/png");
  _gdk_cf_array_index (cfs, GDK_WIN32_CF_INDEX_IMAGE_JPEG) = RegisterClipboardFormat (L"image/jpeg");
  _gdk_cf_array_index (cfs, GDK_WIN32_CF_INDEX_IMAGE_BMP) = RegisterClipboardFormat (L"image/bmp");
  _gdk_cf_array_index (cfs, GDK_WIN32_CF_INDEX_IMAGE_GIF) = RegisterClipboardFormat (L"image/gif");
  _gdk_cf_array_index (cfs, GDK_WIN32_CF_INDEX_TEXT_URI_LIST) = RegisterClipboardFormat (L"text/uri-list");
  _gdk_cf_array_index (cfs, GDK_WIN32_CF_INDEX_TEXT_PLAIN_UTF8) = RegisterClipboardFormat (L"text/plain;charset=utf-8");

  win32_clipdrop->active_source_drags = g_hash_table_new_full (NULL, NULL, (GDestroyNotify) g_object_unref, NULL);

  pixbuf_formats = gdk_pixbuf_get_formats ();

  win32_clipdrop->n_known_pixbuf_formats = 0;
  for (rover = pixbuf_formats; rover != NULL; rover = rover->next)
    {
      char **mime_types =
	gdk_pixbuf_format_get_mime_types ((GdkPixbufFormat *) rover->data);
      char **mime_type;

      for (mime_type = mime_types; *mime_type != NULL; mime_type++)
	win32_clipdrop->n_known_pixbuf_formats++;
    }

  win32_clipdrop->known_pixbuf_formats = g_new (const char *, win32_clipdrop->n_known_pixbuf_formats);

  i = 0;
  for (rover = pixbuf_formats; rover != NULL; rover = rover->next)
    {
      char **mime_types =
	gdk_pixbuf_format_get_mime_types ((GdkPixbufFormat *) rover->data);
      char **mime_type;

      for (mime_type = mime_types; *mime_type != NULL; mime_type++)
	win32_clipdrop->known_pixbuf_formats[i++] = g_intern_string (*mime_type);
    }

  g_slist_free (pixbuf_formats);

  win32_clipdrop->compatibility_w32formats = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify) g_array_unref);

  /* GTK actually has more text formats, but it's unlikely that we'd
   * get anything other than UTF8_STRING these days.
   * GTKTEXTBUFFERCONTENTS format can potentially be converted to
   * W32-compatible rich text format, but that's too complex to address right now.
   */
  comp = g_array_sized_new (FALSE, FALSE, sizeof (GdkWin32ContentFormatPair), 3);
  fmt.contentformat = _gdk_atom_array_index (atoms, GDK_WIN32_ATOM_INDEX_TEXT_PLAIN_UTF8);

  fmt.w32format = _gdk_cf_array_index (cfs, GDK_WIN32_CF_INDEX_TEXT_PLAIN_UTF8);
  fmt.transmute = FALSE;
  g_array_append_val (comp, fmt);

  fmt.w32format = CF_UNICODETEXT;
  fmt.transmute = TRUE;
  g_array_append_val (comp, fmt);

  fmt.w32format = CF_TEXT;
  g_array_append_val (comp, fmt);

  g_hash_table_replace (win32_clipdrop->compatibility_w32formats, (gpointer) fmt.contentformat, comp);


  comp = g_array_sized_new (FALSE, FALSE, sizeof (GdkWin32ContentFormatPair), 2);
  fmt.contentformat = _gdk_atom_array_index (atoms, GDK_WIN32_ATOM_INDEX_IMAGE_PNG);

  fmt.w32format = _gdk_cf_array_index (cfs, GDK_WIN32_CF_INDEX_IMAGE_PNG);
  fmt.transmute = FALSE;
  g_array_append_val (comp, fmt);

  fmt.w32format = _gdk_cf_array_index (cfs, GDK_WIN32_CF_INDEX_PNG);
  g_array_append_val (comp, fmt);

  g_hash_table_replace (win32_clipdrop->compatibility_w32formats, (gpointer) fmt.contentformat, comp);


  comp = g_array_sized_new (FALSE, FALSE, sizeof (GdkWin32ContentFormatPair), 2);
  fmt.contentformat = _gdk_atom_array_index (atoms, GDK_WIN32_ATOM_INDEX_IMAGE_JPEG);

  fmt.w32format = _gdk_cf_array_index (cfs, GDK_WIN32_CF_INDEX_IMAGE_JPEG);
  fmt.transmute = FALSE;
  g_array_append_val (comp, fmt);

  fmt.w32format = _gdk_cf_array_index (cfs, GDK_WIN32_CF_INDEX_JFIF);
  g_array_append_val (comp, fmt);

  g_hash_table_replace (win32_clipdrop->compatibility_w32formats, (gpointer) fmt.contentformat, comp);


  comp = g_array_sized_new (FALSE, FALSE, sizeof (GdkWin32ContentFormatPair), 2);
  fmt.contentformat = _gdk_atom_array_index (atoms, GDK_WIN32_ATOM_INDEX_IMAGE_GIF);

  fmt.w32format = _gdk_cf_array_index (cfs, GDK_WIN32_CF_INDEX_IMAGE_GIF);
  fmt.transmute = FALSE;
  g_array_append_val (comp, fmt);

  fmt.w32format = _gdk_cf_array_index (cfs, GDK_WIN32_CF_INDEX_GIF);
  g_array_append_val (comp, fmt);

  g_hash_table_replace (win32_clipdrop->compatibility_w32formats, (gpointer) fmt.contentformat, comp);


  comp = g_array_sized_new (FALSE, FALSE, sizeof (GdkWin32ContentFormatPair), 2);
  fmt.contentformat = _gdk_atom_array_index (atoms, GDK_WIN32_ATOM_INDEX_IMAGE_BMP);

  fmt.w32format = _gdk_cf_array_index (cfs, GDK_WIN32_CF_INDEX_IMAGE_BMP);
  fmt.transmute = FALSE;
  g_array_append_val (comp, fmt);

  fmt.w32format = CF_DIB;
  fmt.transmute = TRUE;
  g_array_append_val (comp, fmt);

  g_hash_table_replace (win32_clipdrop->compatibility_w32formats, (gpointer) fmt.contentformat, comp);


/* Not implemented, but definitely possible
  comp = g_array_sized_new (FALSE, FALSE, sizeof (GdkWin32ContentFormatPair), 2);
  fmt.contentformat = _gdk_atom_array_index (atoms, GDK_WIN32_ATOM_INDEX_TEXT_URI_LIST);

  fmt.w32format = _gdk_cf_array_index (cfs, GDK_WIN32_CF_INDEX_TEXT_URI_LIST);
  fmt.transmute = FALSE;
  g_array_append_val (comp, fmt);

  fmt.w32format = _gdk_cf_array_index (cfs, GDK_WIN32_CF_INDEX_CFSTR_SHELLIDLIST);
  fmt.transmute = TRUE;
  g_array_append_val (comp, fmt);

  g_hash_table_replace (win32_clipdrop->compatibility_w32formats, fmt.contentformat, comp);
*/

  win32_clipdrop->compatibility_contentformats = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify) g_array_unref);

  comp = g_array_sized_new (FALSE, FALSE, sizeof (GdkWin32ContentFormatPair), 2);
  fmt.w32format = CF_TEXT;
  fmt.transmute = FALSE;

  fmt.contentformat = _gdk_atom_array_index (atoms, GDK_WIN32_ATOM_INDEX_CF_TEXT);
  g_array_append_val (comp, fmt);

  fmt.contentformat = _gdk_atom_array_index (atoms, GDK_WIN32_ATOM_INDEX_TEXT_PLAIN_UTF8);
  fmt.transmute = TRUE;
  g_array_append_val (comp, fmt);

  g_hash_table_replace (win32_clipdrop->compatibility_contentformats, GINT_TO_POINTER (CF_TEXT), comp);


  comp = g_array_sized_new (FALSE, FALSE, sizeof (GdkWin32ContentFormatPair), 2);
  fmt.w32format = CF_UNICODETEXT;
  fmt.transmute = FALSE;

  fmt.contentformat = _gdk_atom_array_index (atoms, GDK_WIN32_ATOM_INDEX_CF_UNICODETEXT);
  g_array_append_val (comp, fmt);

  fmt.contentformat = _gdk_atom_array_index (atoms, GDK_WIN32_ATOM_INDEX_TEXT_PLAIN_UTF8);
  fmt.transmute = TRUE;
  g_array_append_val (comp, fmt);

  g_hash_table_replace (win32_clipdrop->compatibility_contentformats, GINT_TO_POINTER (CF_UNICODETEXT), comp);


  comp = g_array_sized_new (FALSE, FALSE, sizeof (GdkWin32ContentFormatPair), 2);
  fmt.w32format = _gdk_cf_array_index (cfs, GDK_WIN32_CF_INDEX_PNG);
  fmt.transmute = FALSE;

  fmt.contentformat = _gdk_atom_array_index (atoms, GDK_WIN32_ATOM_INDEX_PNG);
  g_array_append_val (comp, fmt);

  fmt.contentformat = _gdk_atom_array_index (atoms, GDK_WIN32_ATOM_INDEX_IMAGE_PNG);
  g_array_append_val (comp, fmt);

  g_hash_table_replace (win32_clipdrop->compatibility_contentformats, GINT_TO_POINTER (_gdk_cf_array_index (cfs, GDK_WIN32_CF_INDEX_PNG)), comp);


  comp = g_array_sized_new (FALSE, FALSE, sizeof (GdkWin32ContentFormatPair), 2);
  fmt.w32format = _gdk_cf_array_index (cfs, GDK_WIN32_CF_INDEX_JFIF);
  fmt.transmute = FALSE;

  fmt.contentformat = _gdk_atom_array_index (atoms, GDK_WIN32_ATOM_INDEX_JFIF);
  g_array_append_val (comp, fmt);

  fmt.contentformat = _gdk_atom_array_index (atoms, GDK_WIN32_ATOM_INDEX_IMAGE_JPEG);
  g_array_append_val (comp, fmt);

  g_hash_table_replace (win32_clipdrop->compatibility_contentformats, GINT_TO_POINTER (_gdk_cf_array_index (cfs, GDK_WIN32_CF_INDEX_JFIF)), comp);


  comp = g_array_sized_new (FALSE, FALSE, sizeof (GdkWin32ContentFormatPair), 2);
  fmt.w32format = _gdk_cf_array_index (cfs, GDK_WIN32_CF_INDEX_GIF);
  fmt.transmute = FALSE;

  fmt.contentformat = _gdk_atom_array_index (atoms, GDK_WIN32_ATOM_INDEX_GIF);
  g_array_append_val (comp, fmt);

  fmt.contentformat = _gdk_atom_array_index (atoms, GDK_WIN32_ATOM_INDEX_IMAGE_GIF);
  g_array_append_val (comp, fmt);

  g_hash_table_replace (win32_clipdrop->compatibility_contentformats, GINT_TO_POINTER (_gdk_cf_array_index (cfs, GDK_WIN32_CF_INDEX_GIF)), comp);


  comp = g_array_sized_new (FALSE, FALSE, sizeof (GdkWin32ContentFormatPair), 2);
  fmt.w32format = CF_DIB;
  fmt.transmute = FALSE;

  fmt.contentformat = _gdk_atom_array_index (atoms, GDK_WIN32_ATOM_INDEX_CF_DIB);
  g_array_append_val (comp, fmt);

  fmt.contentformat = _gdk_atom_array_index (atoms, GDK_WIN32_ATOM_INDEX_IMAGE_BMP);
  fmt.transmute = TRUE;
  g_array_append_val (comp, fmt);

  g_hash_table_replace (win32_clipdrop->compatibility_contentformats, GINT_TO_POINTER (CF_DIB), comp);


  comp = g_array_sized_new (FALSE, FALSE, sizeof (GdkWin32ContentFormatPair), 2);
  fmt.w32format = _gdk_cf_array_index (cfs, GDK_WIN32_CF_INDEX_CFSTR_SHELLIDLIST);
  fmt.transmute = FALSE;

  fmt.contentformat = _gdk_atom_array_index (atoms, GDK_WIN32_ATOM_INDEX_CFSTR_SHELLIDLIST);
  g_array_append_val (comp, fmt);

  fmt.contentformat = _gdk_atom_array_index (atoms, GDK_WIN32_ATOM_INDEX_TEXT_URI_LIST);
  fmt.transmute = TRUE;
  g_array_append_val (comp, fmt);

  g_hash_table_replace (win32_clipdrop->compatibility_contentformats, GINT_TO_POINTER (_gdk_cf_array_index (cfs, GDK_WIN32_CF_INDEX_CFSTR_SHELLIDLIST)), comp);

  win32_clipdrop->clipboard_open_thread_queue = g_async_queue_new ();
  win32_clipdrop->clipboard_render_queue = g_async_queue_new ();
  /* Out of sheer laziness, we just push the extra queue through the
   * main queue, instead of allocating a struct with two queue
   * pointers and then passing *that* to the thread.
   */
  g_async_queue_push (win32_clipdrop->clipboard_open_thread_queue, g_async_queue_ref (win32_clipdrop->clipboard_render_queue));
  win32_clipdrop->clipboard_open_thread = g_thread_new ("GDK Win32 Clipboard Thread",
                                                        _gdk_win32_clipboard_thread_main,
                                                        g_async_queue_ref (win32_clipdrop->clipboard_open_thread_queue));

  win32_clipdrop->dnd_queue = g_async_queue_new ();
  win32_clipdrop->dnd_thread = g_thread_new ("GDK Win32 DnD Thread",
                                             _gdk_win32_dnd_thread_main,
                                             g_async_queue_ref (win32_clipdrop->dnd_queue));
  win32_clipdrop->dnd_thread_id = GPOINTER_TO_UINT (g_async_queue_pop (win32_clipdrop->dnd_queue));
}

#define CLIPBOARD_IDLE_ABORT_TIME 30

static const char *
predefined_name (UINT fmt)
{
#define CASE(fmt) case fmt: return #fmt
  switch (fmt)
    {
    CASE (CF_TEXT);
    CASE (CF_BITMAP);
    CASE (CF_METAFILEPICT);
    CASE (CF_SYLK);
    CASE (CF_DIF);
    CASE (CF_TIFF);
    CASE (CF_OEMTEXT);
    CASE (CF_DIB);
    CASE (CF_PALETTE);
    CASE (CF_PENDATA);
    CASE (CF_RIFF);
    CASE (CF_WAVE);
    CASE (CF_UNICODETEXT);
    CASE (CF_ENHMETAFILE);
    CASE (CF_HDROP);
    CASE (CF_LOCALE);
    CASE (CF_DIBV5);
    CASE (CF_MAX);

    CASE (CF_OWNERDISPLAY);
    CASE (CF_DSPTEXT);
    CASE (CF_DSPBITMAP);
    CASE (CF_DSPMETAFILEPICT);
    CASE (CF_DSPENHMETAFILE);
#undef CASE
    default:
      return NULL;
    }
}

char *
_gdk_win32_get_clipboard_format_name (UINT      fmt,
                                      gboolean *is_predefined)
{
  int registered_name_w_len = 1024;
  wchar_t *registered_name_w = g_malloc (registered_name_w_len);
  char *registered_name = NULL;
  int gcfn_result;
  const char *predef = predefined_name (fmt);

  /* TODO: cache the result in a hash table */

  do
    {
      gcfn_result = GetClipboardFormatNameW (fmt, registered_name_w, registered_name_w_len);

      if (gcfn_result > 0 && gcfn_result < registered_name_w_len)
        {
          registered_name = g_utf16_to_utf8 (registered_name_w, -1, NULL, NULL, NULL);
          g_clear_pointer (&registered_name_w, g_free);
          if (!registered_name)
            gcfn_result = 0;
          else
            *is_predefined = FALSE;
          break;
        }

      /* If GetClipboardFormatNameW() used up all the space, it means that
       * we probably need a bigger buffer, but cap this at 1 megabyte.
       */
      if (gcfn_result == 0 || registered_name_w_len > 1024 * 1024)
        {
          gcfn_result = 0;
          g_clear_pointer (&registered_name_w, g_free);
          break;
        }

      registered_name_w_len *= 2;
      registered_name_w = g_realloc (registered_name_w, registered_name_w_len);
      gcfn_result = registered_name_w_len;
    } while (gcfn_result == registered_name_w_len);

  if (registered_name == NULL &&
      predef != NULL)
    {
      registered_name = g_strdup (predef);
      *is_predefined = TRUE;
    }

  return registered_name;
}

/* This turns an arbitrary string into a string that
 * *looks* like a mime/type, such as:
 * "application/x.windows.FOO_BAR" from "FOO_BAR".
 * Does nothing for strings that already look like a mime/type
 * (no spaces, one slash, with at least one char on each side of the slash).
 */
const char *
_gdk_win32_get_clipboard_format_name_as_interned_mimetype (char *w32format_name)
{
  char *mime_type;
  const char *result;
  char *space = strchr (w32format_name, ' ');
  char *slash = strchr (w32format_name, '/');

  if (space == NULL &&
      slash > w32format_name &&
      slash[1] != '\0' &&
      strchr (&slash[1], '/') == NULL)
    return g_intern_string (w32format_name);

  mime_type = g_strdup_printf ("application/x.windows.%s", w32format_name);
  result = g_intern_string (mime_type);
  g_free (mime_type);

  return result;
}

static GArray *
get_compatibility_w32formats_for_contentformat (const char *contentformat)
{
  GArray *result = NULL;
  int i;
  GdkWin32Clipdrop *clipdrop = _gdk_win32_clipdrop_get ();

  result = g_hash_table_lookup (clipdrop->compatibility_w32formats, contentformat);

  if (result != NULL)
    return result;

  for (i = 0; i < clipdrop->n_known_pixbuf_formats; i++)
    {
      if (contentformat != clipdrop->known_pixbuf_formats[i])
        continue;

      /* Any format known to gdk-pixbuf can be presented as PNG or BMP */
      result = g_hash_table_lookup (clipdrop->compatibility_w32formats,
                                    _gdk_win32_clipdrop_atom (GDK_WIN32_ATOM_INDEX_IMAGE_PNG));
      break;
    }

  return result;
}

static GArray *
_gdk_win32_get_compatibility_contentformats_for_w32format (UINT w32format)
{
  GArray *result = NULL;
  GdkWin32Clipdrop *clipdrop = _gdk_win32_clipdrop_get ();

  result = g_hash_table_lookup (clipdrop->compatibility_contentformats, GINT_TO_POINTER (w32format));

  if (result != NULL)
    return result;

  /* TODO: reverse gdk-pixbuf conversion? We have to somehow
   * match gdk-pixbuf format names to the corresponding clipboard
   * format names. The former are known only at runtime,
   * the latter are presently unknown...
   * Maybe try to get the data and then just feed it to gdk-pixbuf,
   * see if it knows what it is?
   */

  return result;
}

/* Turn W32 format into a GDK content format and add it
 * to the array of W32 format <-> GDK content format pairs
 * and/or to a GDK contentformat builder.
 * Also add compatibility GDK content formats for that W32 format.
 * The added content format string is always interned.
 * Ensures that duplicates are not added to the pairs array
 * (builder already takes care of that for itself).
 */
void
_gdk_win32_add_w32format_to_pairs (UINT                      w32format,
                                   GArray                   *pairs,
                                   GdkContentFormatsBuilder *builder)
{
  gboolean predef;
  char *w32format_name = _gdk_win32_get_clipboard_format_name (w32format, &predef);
  const char *interned_w32format_name;
  GdkWin32ContentFormatPair pair;
  GArray *comp_pairs;
  int i, j;

  if (w32format_name != NULL)
    {
      interned_w32format_name = _gdk_win32_get_clipboard_format_name_as_interned_mimetype (w32format_name);
      GDK_NOTE (DND, g_print ("Maybe add as-is format %s (%s) (0x%p)\n", w32format_name, interned_w32format_name, interned_w32format_name));
      g_free (w32format_name);

      if (pairs && interned_w32format_name != 0)
        {
          for (j = 0; j < pairs->len; j++)
            if (g_array_index (pairs, GdkWin32ContentFormatPair, j).contentformat == interned_w32format_name)
              break;
          if (j == pairs->len)
            {
              pair.w32format = w32format;
              pair.contentformat = interned_w32format_name;
              pair.transmute = FALSE;
              g_array_append_val (pairs, pair);
            }
        }
      if (builder != NULL && interned_w32format_name != 0)
        gdk_content_formats_builder_add_mime_type (builder, interned_w32format_name);
    }

  comp_pairs = _gdk_win32_get_compatibility_contentformats_for_w32format (w32format);

 if (pairs != NULL && comp_pairs != NULL)
   for (i = 0; i < comp_pairs->len; i++)
     {
       pair = g_array_index (comp_pairs, GdkWin32ContentFormatPair, i);

       for (j = 0; j < pairs->len; j++)
         if (g_array_index (pairs, GdkWin32ContentFormatPair, j).contentformat == pair.contentformat &&
             g_array_index (pairs, GdkWin32ContentFormatPair, j).w32format == pair.w32format)
           break;

       if (j == pairs->len)
         g_array_append_val (pairs, pair);
     }

 if (builder != NULL && comp_pairs != NULL)
   for (i = 0; i < comp_pairs->len; i++)
     {
       pair = g_array_index (comp_pairs, GdkWin32ContentFormatPair, i);

       gdk_content_formats_builder_add_mime_type (builder, pair.contentformat);
     }
}

static void
transmute_cf_unicodetext_to_utf8_string (const guchar    *data,
                                         gsize            length,
                                         guchar         **set_data,
                                         gsize           *set_data_length,
                                         GDestroyNotify  *set_data_destroy)
{
  wchar_t *ptr, *p, *q, *endp;
  char *result;
  glong wclen, u8_len;

  /* Replace CR and CR+LF with LF */
  ptr = (wchar_t *) data;
  p = ptr;
  q = ptr;
  endp = ptr + length / 2;
  wclen = 0;

  while (p < endp)
    {
      if (*p != L'\r')
        {
          *q++ = *p;
          wclen++;
        }
      else if (p + 1 >= endp || *(p + 1) != L'\n')
        {
          *q++ = L'\n';
          wclen++;
        }

      p++;
    }

  result = g_utf16_to_utf8 (ptr, wclen, NULL, &u8_len, NULL);

  if (result)
    {
      *set_data = (guchar *) result;

      if (set_data_length)
        *set_data_length = u8_len + 1;
      if (set_data_destroy)
        *set_data_destroy = (GDestroyNotify) g_free;
    }
}

static void
transmute_utf8_string_to_cf_unicodetext (const guchar    *data,
                                         gsize            length,
                                         guchar         **set_data,
                                         gsize           *set_data_length,
                                         GDestroyNotify  *set_data_destroy)
{
  glong wclen;
  GError *err = NULL;
  glong size;
  int i;
  wchar_t *wcptr, *p;

  wcptr = g_utf8_to_utf16 ((char *) data, length, NULL, &wclen, &err);
  if (err != NULL)
    {
      g_warning ("Failed to convert utf8: %s", err->message);
      g_clear_error (&err);
      return;
    }

  wclen++; /* Terminating 0 */
  size = wclen * 2;
  for (i = 0; i < wclen; i++)
    if (wcptr[i] == L'\n' && (i == 0 || wcptr[i - 1] != L'\r'))
      size += 2;

  *set_data = g_malloc0 (size);
  if (set_data_length)
    *set_data_length = size;
  if (set_data_destroy)
    *set_data_destroy = (GDestroyNotify) g_free;

  p = (wchar_t *) *set_data;

  for (i = 0; i < wclen; i++)
    {
      if (wcptr[i] == L'\n' && (i == 0 || wcptr[i - 1] != L'\r'))
	*p++ = L'\r';
      *p++ = wcptr[i];
    }

  g_free (wcptr);
}

static int
wchar_to_str (const wchar_t  *wstr,
              char         **retstr,
              UINT            cp)
{
  char *str;
  int   len;
  int   lenc;

  len = WideCharToMultiByte (cp, 0, wstr, -1, NULL, 0, NULL, NULL);

  if (len <= 0)
    return -1;

  str = g_malloc (sizeof (char) * len);

  lenc = WideCharToMultiByte (cp, 0, wstr, -1, str, len, NULL, NULL);

  if (lenc != len)
    {
      g_free (str);

      return -3;
    }

  *retstr = str;

  return 0;
}

static void
transmute_utf8_string_to_cf_text (const guchar    *data,
                                  gsize            length,
                                  guchar         **set_data,
                                  gsize           *set_data_length,
                                  GDestroyNotify  *set_data_destroy)
{
  glong rlen;
  GError *err = NULL;
  gsize size;
  int i;
  char *strptr, *p;
  wchar_t *wcptr;

  wcptr = g_utf8_to_utf16 ((char *) data, length, NULL, NULL, &err);
  if (err != NULL)
    {
      g_warning ("Failed to convert utf8: %s", err->message);
      g_clear_error (&err);
      return;
    }

  if (wchar_to_str (wcptr, &strptr, CP_ACP) != 0)
    {
      g_warning ("Failed to convert utf-16 %S to ACP", wcptr);
      g_free (wcptr);
      return;
    }

  rlen = strlen (strptr);
  g_free (wcptr);

  rlen++; /* Terminating 0 */
  size = rlen * sizeof (char);
  for (i = 0; i < rlen; i++)
    if (strptr[i] == '\n' && (i == 0 || strptr[i - 1] != '\r'))
      size += sizeof (char);

  *set_data = g_malloc0 (size);
  if (set_data_length)
    *set_data_length = size;
  if (set_data_destroy)
    *set_data_destroy = (GDestroyNotify) g_free;

  p = (char *) *set_data;

  for (i = 0; i < rlen; i++)
    {
      if (strptr[i] == '\n' && (i == 0 || strptr[i - 1] != '\r'))
	*p++ = '\r';
      *p++ = strptr[i];
    }

  g_free (strptr);
}

static int
str_to_wchar (const char  *str,
              wchar_t    **wretstr,
              UINT         cp)
{
  wchar_t *wstr;
  int      len;
  int      lenc;

  len = MultiByteToWideChar (cp, 0, str, -1, NULL, 0);

  if (len <= 0)
    return -1;

  wstr = g_malloc (sizeof (wchar_t) * len);

  lenc = MultiByteToWideChar (cp, 0, str, -1, wstr, len);

  if (lenc != len)
    {
      g_free (wstr);

      return -3;
    }

  *wretstr = wstr;

  return 0;
}

static void
transmute_cf_text_to_utf8_string (const guchar    *data,
                                  gsize            length,
                                  guchar         **set_data,
                                  gsize           *set_data_length,
                                  GDestroyNotify  *set_data_destroy)
{
  char *ptr, *p, *q, *endp;
  char *result;
  glong wclen, u8_len;
  wchar_t *wstr;

  /* Strip out \r */
  ptr = (char *) data;
  p = ptr;
  q = ptr;
  endp = ptr + length / 2;
  wclen = 0;

  while (p < endp)
    {
      if (*p != '\r')
        {
          *q++ = *p;
          wclen++;
        }
      else if (p + 1 > endp || *(p + 1) != '\n')
        {
          *q++ = '\n';
          wclen++;
        }

      p++;
    }

  if (str_to_wchar (ptr, &wstr, CP_ACP) < 0)
    return;

  result = g_utf16_to_utf8 (wstr, -1, NULL, &u8_len, NULL);

  if (result)
    {
      *set_data = (guchar *) result;

      if (set_data_length)
        *set_data_length = u8_len + 1;
      if (set_data_destroy)
        *set_data_destroy = (GDestroyNotify) g_free;
    }

  g_free (wstr);
}

static void
transmute_cf_dib_to_image_bmp (const guchar    *data,
                               gsize            length,
                               guchar         **set_data,
                               gsize           *set_data_length,
                               GDestroyNotify  *set_data_destroy)
{
  /* Need to add a BMP file header so gdk-pixbuf can load
   * it.
   *
   * If the data is from Mozilla Firefox or IE7, and
   * starts with an "old fashioned" BITMAPINFOHEADER,
   * i.e. with biSize==40, and biCompression == BI_RGB and
   * biBitCount==32, we assume that the "extra" byte in
   * each pixel in fact is alpha.
   *
   * The gdk-pixbuf bmp loader doesn't trust 32-bit BI_RGB
   * bitmaps to in fact have alpha, so we have to convince
   * it by changing the bitmap header to a version 5
   * BI_BITFIELDS one with explicit alpha mask indicated.
   *
   * The RGB bytes that are in bitmaps on the clipboard
   * originating from Firefox or IE7 seem to be
   * premultiplied with alpha. The gdk-pixbuf bmp loader
   * of course doesn't expect that, so we have to undo the
   * premultiplication before feeding the bitmap to the
   * bmp loader.
   *
   * Note that for some reason the bmp loader used to want
   * the alpha bytes in its input to actually be
   * 255-alpha, but here we assume that this has been
   * fixed before this is committed.
   */
  BITMAPINFOHEADER *bi = (BITMAPINFOHEADER *) data;
  BITMAPFILEHEADER *bf;
  gpointer result;
  gsize data_length = length;
  gsize new_length;
  gboolean make_dibv5 = FALSE;
  BITMAPV5HEADER *bV5;
  guchar *p;
  guint i;

  if (bi->biSize == sizeof (BITMAPINFOHEADER) &&
      bi->biPlanes == 1 &&
      bi->biBitCount == 32 &&
      bi->biCompression == BI_RGB &&
#if 0
      /* Maybe check explicitly for Mozilla or IE7?
       *
       * If the clipboard format
       * application/x-moz-nativeimage is present, that is
       * a reliable indicator that the data is offered by
       * Mozilla one would think. For IE7,
       * UniformResourceLocatorW is presumably not that
       * uniqie, so probably need to do some
       * GetClipboardOwner(), GetWindowThreadProcessId(),
       * OpenProcess(), GetModuleFileNameEx() dance to
       * check?
       */
      (IsClipboardFormatAvailable
       (RegisterClipboardFormat (L"application/x-moz-nativeimage")) ||
       IsClipboardFormatAvailable
       (RegisterClipboardFormat (L"UniformResourceLocatorW"))) &&
#endif
      TRUE)
    {
      /* We turn the BITMAPINFOHEADER into a
       * BITMAPV5HEADER before feeding it to gdk-pixbuf.
       */
      new_length = (data_length +
		    sizeof (BITMAPFILEHEADER) +
		    (sizeof (BITMAPV5HEADER) - sizeof (BITMAPINFOHEADER)));
      make_dibv5 = TRUE;
    }
  else
    {
      new_length = data_length + sizeof (BITMAPFILEHEADER);
    }

  result = g_try_malloc (new_length);

  if (result == NULL)
    return;

  bf = (BITMAPFILEHEADER *) result;
  bf->bfType = 0x4d42; /* "BM" */
  bf->bfSize = new_length;
  bf->bfReserved1 = 0;
  bf->bfReserved2 = 0;

  *set_data = result;

  if (set_data_length)
    *set_data_length = new_length;
  if (set_data_destroy)
    *set_data_destroy = g_free;

  if (!make_dibv5)
    {
      bf->bfOffBits = (sizeof (BITMAPFILEHEADER) +
		       bi->biSize +
		       bi->biClrUsed * sizeof (RGBQUAD));

      if (bi->biCompression == BI_BITFIELDS && bi->biBitCount >= 16)
        {
          /* Screenshots taken with PrintScreen or
           * Alt + PrintScreen are found on the clipboard in
           * this format. In this case the BITMAPINFOHEADER is
           * followed by three DWORD specifying the masks of the
           * red green and blue components, so adjust the offset
           * accordingly. */
          bf->bfOffBits += (3 * sizeof (DWORD));
        }

      memcpy ((char *) result + sizeof (BITMAPFILEHEADER),
	      bi,
	      data_length);

      return;
    }

  bV5 = (BITMAPV5HEADER *) ((char *) result + sizeof (BITMAPFILEHEADER));

  bV5->bV5Size = sizeof (BITMAPV5HEADER);
  bV5->bV5Width = bi->biWidth;
  bV5->bV5Height = bi->biHeight;
  bV5->bV5Planes = 1;
  bV5->bV5BitCount = 32;
  bV5->bV5Compression = BI_BITFIELDS;
  bV5->bV5SizeImage = 4 * bV5->bV5Width * ABS (bV5->bV5Height);
  bV5->bV5XPelsPerMeter = bi->biXPelsPerMeter;
  bV5->bV5YPelsPerMeter = bi->biYPelsPerMeter;
  bV5->bV5ClrUsed = 0;
  bV5->bV5ClrImportant = 0;
  /* Now the added mask fields */
  bV5->bV5RedMask   = 0x00ff0000;
  bV5->bV5GreenMask = 0x0000ff00;
  bV5->bV5BlueMask  = 0x000000ff;
  bV5->bV5AlphaMask = 0xff000000;
  ((char *) &bV5->bV5CSType)[3] = 's';
  ((char *) &bV5->bV5CSType)[2] = 'R';
  ((char *) &bV5->bV5CSType)[1] = 'G';
  ((char *) &bV5->bV5CSType)[0] = 'B';
  /* Ignore colorspace and profile fields */
  bV5->bV5Intent = LCS_GM_GRAPHICS;
  bV5->bV5Reserved = 0;

  bf->bfOffBits = (sizeof (BITMAPFILEHEADER) +
		   bV5->bV5Size);

  p = ((guchar *) result) + sizeof (BITMAPFILEHEADER) + sizeof (BITMAPV5HEADER);
  memcpy (p, ((char *) bi) + bi->biSize,
          data_length - sizeof (BITMAPINFOHEADER));

  for (i = 0; i < bV5->bV5SizeImage / 4; i++)
    {
      if (p[3] != 0)
        {
          double inverse_alpha = 255. / p[3];

          p[0] = p[0] * inverse_alpha + 0.5;
          p[1] = p[1] * inverse_alpha + 0.5;
          p[2] = p[2] * inverse_alpha + 0.5;
        }

      p += 4;
    }
}

static void
transmute_cf_shell_id_list_to_text_uri_list (const guchar    *data,
                                             gsize            length,
                                             guchar         **set_data,
                                             gsize           *set_data_length,
                                             GDestroyNotify  *set_data_destroy)
{
  guint i;
  CIDA *cida = (CIDA *) data;
  guint number_of_ids = cida->cidl;
  GString *result = g_string_new (NULL);
  PCIDLIST_ABSOLUTE folder_id = HIDA_GetPIDLFolder (cida);
  wchar_t path_w[MAX_PATH + 1];

  for (i = 0; i < number_of_ids; i++)
    {
      PCUIDLIST_RELATIVE file_id = HIDA_GetPIDLItem (cida, i);
      PIDLIST_ABSOLUTE file_id_full = ILCombine (folder_id, file_id);

      if (SHGetPathFromIDListW (file_id_full, path_w))
        {
          char *filename = g_utf16_to_utf8 (path_w, -1, NULL, NULL, NULL);

          if (filename)
            {
              char *uri = g_filename_to_uri (filename, NULL, NULL);

              if (uri)
                {
                  g_string_append (result, uri);
                  g_string_append (result, "\r\n");
                  g_free (uri);
                }

              g_free (filename);
            }
        }

      ILFree (file_id_full);
    }

  if (set_data_length)
    *set_data_length = result->len;
  *set_data = (guchar *) g_string_free (result, FALSE);
  if (set_data_destroy)
    *set_data_destroy = g_free;
}

void
transmute_image_bmp_to_cf_dib (const guchar    *data,
                               gsize            length,
                               guchar         **set_data,
                               gsize           *set_data_length,
                               GDestroyNotify  *set_data_destroy)
{
  gsize size;
  guchar *ptr;

  g_return_if_fail (length >= sizeof (BITMAPFILEHEADER));

  /* No conversion is needed, just strip the BITMAPFILEHEADER */
  size = length - sizeof (BITMAPFILEHEADER);
  ptr = g_malloc (size);

  memcpy (ptr, data + sizeof (BITMAPFILEHEADER), size);

  *set_data = ptr;

  if (set_data_length)
    *set_data_length = size;
  if (set_data_destroy)
    *set_data_destroy = g_free;
}

gboolean
_gdk_win32_transmute_windows_data (UINT          from_w32format,
                                   const char   *to_contentformat,
                                   HANDLE        hdata,
                                   guchar      **set_data,
                                   gsize        *set_data_length)
{
  const guchar *data;
  SIZE_T hdata_length;
  gsize length;
  gboolean res = FALSE;

  /* FIXME: error reporting */

  if ((data = GlobalLock (hdata)) == NULL)
    {
      return FALSE;
    }

  hdata_length = GlobalSize (hdata);
  if (hdata_length > G_MAXSIZE)
    goto out;

  length = (gsize) hdata_length;

  if ((to_contentformat == _gdk_win32_clipdrop_atom (GDK_WIN32_ATOM_INDEX_IMAGE_PNG) &&
       from_w32format == _gdk_win32_clipdrop_cf (GDK_WIN32_CF_INDEX_PNG)) ||
      (to_contentformat == _gdk_win32_clipdrop_atom (GDK_WIN32_ATOM_INDEX_IMAGE_JPEG) &&
       from_w32format == _gdk_win32_clipdrop_cf (GDK_WIN32_CF_INDEX_JFIF)) ||
      (to_contentformat == _gdk_win32_clipdrop_atom (GDK_WIN32_ATOM_INDEX_GIF) &&
       from_w32format == _gdk_win32_clipdrop_cf (GDK_WIN32_CF_INDEX_GIF)))
    {
      /* No transmutation needed */
      *set_data = g_memdup2 (data, length);
      *set_data_length = length;
    }
  else if (to_contentformat == _gdk_win32_clipdrop_atom (GDK_WIN32_ATOM_INDEX_TEXT_PLAIN_UTF8) &&
           from_w32format == CF_UNICODETEXT)
    {
      transmute_cf_unicodetext_to_utf8_string (data, length, set_data, set_data_length, NULL);
      res = TRUE;
    }
  else if (to_contentformat == _gdk_win32_clipdrop_atom (GDK_WIN32_ATOM_INDEX_TEXT_PLAIN_UTF8) &&
           from_w32format == CF_TEXT)
    {
      transmute_cf_text_to_utf8_string (data, length, set_data, set_data_length, NULL);
      res = TRUE;
    }
  else if (to_contentformat == _gdk_win32_clipdrop_atom (GDK_WIN32_ATOM_INDEX_IMAGE_BMP) &&
           (from_w32format == CF_DIB || from_w32format == CF_DIBV5))
    {
      transmute_cf_dib_to_image_bmp (data, length, set_data, set_data_length, NULL);
      res = TRUE;
    }
  else if (to_contentformat == _gdk_win32_clipdrop_atom (GDK_WIN32_ATOM_INDEX_TEXT_URI_LIST) &&
           from_w32format == _gdk_win32_clipdrop_cf (GDK_WIN32_CF_INDEX_CFSTR_SHELLIDLIST))
    {
      transmute_cf_shell_id_list_to_text_uri_list (data, length, set_data, set_data_length, NULL);
      res = TRUE;
    }
  else
    {
      g_warning ("Don't know how to transmute W32 format 0x%x to content format 0x%p (%s)",
                 from_w32format, to_contentformat, to_contentformat);
      goto out;
    }

out:
  GlobalUnlock (hdata);

  return res;
}

gboolean
_gdk_win32_transmute_contentformat (const char    *from_contentformat,
                                    UINT           to_w32format,
                                    const guchar  *data,
                                    gsize          length,
                                    guchar       **set_data,
                                    gsize         *set_data_length)
{
  if ((from_contentformat == _gdk_win32_clipdrop_atom (GDK_WIN32_ATOM_INDEX_IMAGE_PNG) &&
       to_w32format == _gdk_win32_clipdrop_cf (GDK_WIN32_CF_INDEX_PNG)) ||
      (from_contentformat == _gdk_win32_clipdrop_atom (GDK_WIN32_ATOM_INDEX_IMAGE_JPEG) &&
       to_w32format == _gdk_win32_clipdrop_cf (GDK_WIN32_CF_INDEX_JFIF)) ||
      (from_contentformat == _gdk_win32_clipdrop_atom (GDK_WIN32_ATOM_INDEX_GIF) &&
       to_w32format == _gdk_win32_clipdrop_cf (GDK_WIN32_CF_INDEX_GIF)))
    {
      /* No conversion needed */
      *set_data = g_memdup2 (data, length);
      *set_data_length = length;
    }
  else if (from_contentformat == _gdk_win32_clipdrop_atom (GDK_WIN32_ATOM_INDEX_TEXT_PLAIN_UTF8) &&
           to_w32format == CF_UNICODETEXT)
    {
      transmute_utf8_string_to_cf_unicodetext (data, length, set_data, set_data_length, NULL);
    }
  else if (from_contentformat == _gdk_win32_clipdrop_atom (GDK_WIN32_ATOM_INDEX_TEXT_PLAIN_UTF8) &&
           to_w32format == CF_TEXT)
    {
      transmute_utf8_string_to_cf_text (data, length, set_data, set_data_length, NULL);
    }
  else if (from_contentformat == _gdk_win32_clipdrop_atom (GDK_WIN32_ATOM_INDEX_IMAGE_BMP) &&
           to_w32format == CF_DIB)
    {
      transmute_image_bmp_to_cf_dib (data, length, set_data, set_data_length, NULL);
    }
  else if (from_contentformat == _gdk_win32_clipdrop_atom (GDK_WIN32_ATOM_INDEX_IMAGE_BMP) &&
           to_w32format == CF_DIBV5)
    {
      transmute_image_bmp_to_cf_dib (data, length, set_data, set_data_length, NULL);
    }
/*
  else if (from_contentformat == _gdk_win32_clipdrop_atom (GDK_WIN32_ATOM_INDEX_TEXT_URI_LIST) &&
           to_w32format == _gdk_win32_clipdrop_cf (GDK_WIN32_CF_INDEX_CFSTR_SHELLIDLIST))
    {
      transmute_text_uri_list_to_shell_id_list (data, length, set_data, set_data_length, NULL);
    }
*/
  else
    {
      g_warning ("Don't know how to transmute from target 0x%p to format 0x%x", from_contentformat, to_w32format);

      return FALSE;
    }

  return TRUE;
}

int
_gdk_win32_add_contentformat_to_pairs (const char *contentformat,
                                       GArray     *array)
{
  int added_count = 0;
  wchar_t *contentformat_w;
  GdkWin32ContentFormatPair fmt;
  int i;
  GArray *comp_pairs;
  int starting_point;
  const wchar_t *prefix = L"application/x.windows.";
  size_t prefix_len = wcslen (prefix);
  size_t offset = 0;

  for (i = 0; i < array->len; i++)
    if (g_array_index (array, GdkWin32ContentFormatPair, i).contentformat == contentformat)
      break;

  /* Don't put duplicates into the array */
  if (i < array->len)
    return added_count;

  /* Only check the newly-added pairs for duplicates,
   * all the ones that exist right now have different targets.
   */
  starting_point = array->len;

  contentformat_w = g_utf8_to_utf16 (contentformat, -1, NULL, NULL, NULL);

  if (contentformat_w == NULL)
    return added_count;

  if (wcsnicmp (contentformat_w, prefix, prefix_len) == 0)
    offset = prefix_len;
  else
    offset = 0;

  fmt.w32format = RegisterClipboardFormatW (&contentformat_w[offset]);
  GDK_NOTE (DND, g_print ("Registered clipboard format %S as 0x%x\n", &contentformat_w[offset], fmt.w32format));
  g_free (contentformat_w);
  fmt.contentformat = contentformat;
  fmt.transmute = FALSE;

  /* Add the "as-is" pair */
  g_array_append_val (array, fmt);
  added_count += 1;

  comp_pairs = get_compatibility_w32formats_for_contentformat (contentformat);
  for (i = 0; comp_pairs != NULL && i < comp_pairs->len; i++)
    {
      int j;

      fmt = g_array_index (comp_pairs, GdkWin32ContentFormatPair, i);

      /* Don't put duplicates into the array */
      for (j = starting_point; j < array->len; j++)
        if (g_array_index (array, GdkWin32ContentFormatPair, j).w32format == fmt.w32format)
          break;

      if (j < array->len)
        continue;

      /* Add a compatibility pair */
      g_array_append_val (array, fmt);
      added_count += 1;
    }

  return added_count;
}

void
_gdk_win32_advertise_clipboard_contentformats (GTask             *task,
                                               GdkContentFormats *contentformats)
{
  GdkWin32Clipdrop *clipdrop = _gdk_win32_clipdrop_get ();
  GdkWin32ClipboardThreadAdvertise *adv = g_new0 (GdkWin32ClipboardThreadAdvertise, 1);
  const char * const *mime_types;
  gsize mime_types_len;
  gsize i;

  g_assert (clipdrop->clipboard_window != NULL);

  adv->parent.item_type = GDK_WIN32_CLIPBOARD_THREAD_QUEUE_ITEM_ADVERTISE;
  adv->parent.start_time = g_get_monotonic_time ();
  adv->parent.end_time = adv->parent.start_time + CLIPBOARD_OPERATION_TIMEOUT;
  adv->parent.opaque_task = task;

  if (contentformats == NULL)
    {
      adv->unset = TRUE;
    }
  else
    {
      adv->unset = FALSE;

      adv->pairs = g_array_new (FALSE, FALSE, sizeof (GdkWin32ContentFormatPair));
      mime_types = gdk_content_formats_get_mime_types (contentformats, &mime_types_len);

      for (i = 0; i < mime_types_len; i++)
        _gdk_win32_add_contentformat_to_pairs (mime_types[i], adv->pairs);
    }

  g_async_queue_push (clipdrop->clipboard_open_thread_queue, adv);
  API_CALL (PostMessage, (clipdrop->clipboard_window, thread_wakeup_message, 0, 0));

  return;
}

void
_gdk_win32_retrieve_clipboard_contentformats (GTask             *task,
                                              GdkContentFormats *contentformats)
{
  GdkWin32Clipdrop *clipdrop = _gdk_win32_clipdrop_get ();
  GdkWin32ClipboardThreadRetrieve *retr = g_new0 (GdkWin32ClipboardThreadRetrieve, 1);
  const char * const *mime_types;
  gsize mime_types_len;
  gsize i;

  g_assert (clipdrop->clipboard_window != NULL);

  retr->parent.item_type = GDK_WIN32_CLIPBOARD_THREAD_QUEUE_ITEM_RETRIEVE;
  retr->parent.start_time = g_get_monotonic_time ();
  retr->parent.end_time = retr->parent.start_time + CLIPBOARD_OPERATION_TIMEOUT;
  retr->parent.opaque_task = task;
  retr->pairs = g_array_new (FALSE, FALSE, sizeof (GdkWin32ContentFormatPair));
  retr->sequence_number = GetClipboardSequenceNumber ();

  mime_types = gdk_content_formats_get_mime_types (contentformats, &mime_types_len);

  for (i = 0; i < mime_types_len; i++)
    _gdk_win32_add_contentformat_to_pairs (mime_types[i], retr->pairs);

  g_async_queue_push (clipdrop->clipboard_open_thread_queue, retr);
  API_CALL (PostMessage, (clipdrop->clipboard_window, thread_wakeup_message, 0, 0));

  return;
}

typedef struct _GdkWin32ClipboardHDataPrepAndStream GdkWin32ClipboardHDataPrepAndStream;

struct _GdkWin32ClipboardHDataPrepAndStream
{
  GdkWin32ClipboardStorePrep *prep;
  GdkWin32HDataOutputStream  *stream;
};

static void
clipboard_store_hdata_ready (GObject      *clipboard,
                             GAsyncResult *result,
                             gpointer      user_data)
{
  GError *error = NULL;
  int i;
  gboolean no_other_streams;
  GdkWin32ClipboardHDataPrepAndStream *prep_and_stream = (GdkWin32ClipboardHDataPrepAndStream *) user_data;
  GdkWin32ClipboardStorePrep *prep = prep_and_stream->prep;
  GdkWin32HDataOutputStream  *stream = prep_and_stream->stream;
  GdkWin32ClipboardThreadStore *store;
  GdkWin32Clipdrop *clipdrop;

  g_clear_pointer (&prep_and_stream, g_free);

  if (!gdk_clipboard_write_finish (GDK_CLIPBOARD (clipboard), result, &error))
    {
      HANDLE handle;
      gboolean is_hdata;

      GDK_NOTE(CLIPBOARD, g_printerr ("Failed to write stream: %s\n", error->message));
      g_error_free (error);
      for (i = 0; i < prep->elements->len; i++)
        free_prep_element (&g_array_index (prep->elements, GdkWin32ClipboardStorePrepElement, i));
      g_free (prep);
      g_output_stream_close (G_OUTPUT_STREAM (stream), NULL, NULL);
      handle = gdk_win32_hdata_output_stream_get_handle (stream, &is_hdata);

      if (is_hdata)
        API_CALL (GlobalFree, (handle));
      else
        API_CALL (CloseHandle, (handle));

      g_object_unref (stream);

      return;
    }

  for (i = 0, no_other_streams = TRUE; i < prep->elements->len; i++)
    {
      GdkWin32ClipboardStorePrepElement *el = &g_array_index (prep->elements, GdkWin32ClipboardStorePrepElement, i);

      if (el->stream == G_OUTPUT_STREAM (stream))
        {
          g_output_stream_close (el->stream, NULL, NULL);
          el->handle = gdk_win32_hdata_output_stream_get_handle (GDK_WIN32_HDATA_OUTPUT_STREAM (el->stream), NULL);
          g_object_unref (el->stream);
          el->stream = NULL;
        }
      else if (el->stream != NULL)
        no_other_streams = FALSE;
    }

  if (!no_other_streams)
    return;

  clipdrop = _gdk_win32_clipdrop_get ();

  store = g_new0 (GdkWin32ClipboardThreadStore, 1);

  store->parent.start_time = g_get_monotonic_time ();
  store->parent.end_time = store->parent.start_time + CLIPBOARD_OPERATION_TIMEOUT;
  store->parent.item_type = GDK_WIN32_CLIPBOARD_THREAD_QUEUE_ITEM_STORE;
  store->parent.opaque_task = prep->store_task;
  store->elements = prep->elements;

  g_async_queue_push (clipdrop->clipboard_open_thread_queue, store);
  API_CALL (PostMessage, (clipdrop->clipboard_window, thread_wakeup_message, 0, 0));

  g_free (prep);
}

gboolean
_gdk_win32_store_clipboard_contentformats (GdkClipboard      *cb,
                                           GTask             *task,
                                           GdkContentFormats *contentformats)
{
  GArray *pairs; /* of GdkWin32ContentFormatPair */
  const char * const *mime_types;
  gsize n_mime_types;
  gsize i;
  GdkWin32Clipdrop *clipdrop = _gdk_win32_clipdrop_get ();
  GdkWin32ClipboardStorePrep *prep;

  g_assert (clipdrop->clipboard_window != NULL);

  mime_types = gdk_content_formats_get_mime_types (contentformats, &n_mime_types);

  pairs = g_array_sized_new (FALSE,
                             FALSE,
                             sizeof (GdkWin32ContentFormatPair),
                             n_mime_types);

  for (i = 0; i < n_mime_types; i++)
    _gdk_win32_add_contentformat_to_pairs (mime_types[i], pairs);

  if (pairs->len <= 0)
    {
      g_array_free (pairs, TRUE);

      return FALSE;
    }

  prep = g_new0 (GdkWin32ClipboardStorePrep, 1);
  prep->elements = g_array_sized_new (FALSE, TRUE, sizeof (GdkWin32ClipboardStorePrepElement), pairs->len);
  prep->store_task = task;

  for (i = 0; i < pairs->len; i++)
    {
      GdkWin32ClipboardStorePrepElement el;
      GdkWin32ContentFormatPair *pair = &g_array_index (pairs, GdkWin32ContentFormatPair, i);

      el.stream = gdk_win32_hdata_output_stream_new (pair, NULL);

      if (!el.stream)
        continue;

      el.w32format = pair->w32format;
      el.contentformat = pair->contentformat;
      el.handle = NULL;
      g_array_append_val (prep->elements, el);
    }

  for (i = 0; i < prep->elements->len; i++)
    {
      GdkWin32ClipboardStorePrepElement *el = &g_array_index (prep->elements, GdkWin32ClipboardStorePrepElement, i);
      GdkWin32ClipboardHDataPrepAndStream *prep_and_stream = g_new0 (GdkWin32ClipboardHDataPrepAndStream, 1);
      prep_and_stream->prep = prep;
      prep_and_stream->stream = GDK_WIN32_HDATA_OUTPUT_STREAM (el->stream);

      gdk_clipboard_write_async (GDK_CLIPBOARD (cb),
                                 el->contentformat,
                                 el->stream,
                                 G_PRIORITY_DEFAULT,
                                 NULL,
                                 clipboard_store_hdata_ready,
                                 prep_and_stream);
    }

  g_array_free (pairs, TRUE);

  return TRUE;
}
