/* GStreamer
 * Copyright (C) 2010-2011 David Hoyt <dhoyt@hoytsoft.org>
 * Copyright (C) 2010 Andoni Morales <ylatuya@gmail.com>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library 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
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this library; if not, write to the
 * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, USA.
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include "d3dvideosink.h"

#define IPC_SET_WINDOW          1
#define IDT_DEVICELOST          1

/* Provide access to data that will be shared among all instantiations of this element */
#define GST_D3DVIDEOSINK_SHARED_D3D_LOCK         g_static_mutex_lock (&shared_d3d_lock)
#define GST_D3DVIDEOSINK_SHARED_D3D_UNLOCK       g_static_mutex_unlock (&shared_d3d_lock)
#define GST_D3DVIDEOSINK_SHARED_D3D_HOOK_LOCK    g_static_mutex_lock (&shared_d3d_hook_lock)
#define GST_D3DVIDEOSINK_SHARED_D3D_HOOK_UNLOCK  g_static_mutex_unlock (&shared_d3d_hook_lock)
typedef struct _GstD3DVideoSinkShared GstD3DVideoSinkShared;
struct _GstD3DVideoSinkShared
{
  LPDIRECT3D9 d3d;
  D3DCAPS9 d3dcaps;

  GList *element_list;
  gint32 element_count;

  gboolean device_lost;
  UINT_PTR device_lost_timer;
  GstD3DVideoSink *device_lost_sink;

  HWND hidden_window_handle;
  HANDLE hidden_window_created_signal;
  GThread *hidden_window_thread;

  GHashTable *hook_tbl;
};
typedef struct _GstD3DVideoSinkHookData GstD3DVideoSinkHookData;
struct _GstD3DVideoSinkHookData
{
  HHOOK hook;
  HWND window_handle;
  DWORD thread_id;
  DWORD process_id;
};
/* Holds our shared information */
static GstD3DVideoSinkShared shared;
/* Define a shared lock to synchronize the creation/destruction of the d3d device */
static GStaticMutex shared_d3d_lock = G_STATIC_MUTEX_INIT;
static GStaticMutex shared_d3d_hook_lock = G_STATIC_MUTEX_INIT;
/* Hold a reference to our dll's HINSTANCE */
static HINSTANCE g_hinstDll = NULL;

typedef struct _IPCData IPCData;
struct _IPCData
{
  HWND hwnd;
  LONG_PTR wnd_proc;
};
/* Holds data that may be used to communicate across processes */
/*static IPCData ipc_data;*/
/*static COPYDATASTRUCT ipc_cds;*/

GST_DEBUG_CATEGORY (d3dvideosink_debug);
#define GST_CAT_DEFAULT d3dvideosink_debug

static GstStaticPadTemplate sink_template = GST_STATIC_PAD_TEMPLATE ("sink",
    GST_PAD_SINK,
    GST_PAD_ALWAYS,
    GST_STATIC_CAPS (GST_VIDEO_CAPS_YUV ("{ YUY2, UYVY, YV12, I420, NV12 }")
        ";" GST_VIDEO_CAPS_BGRx ";" GST_VIDEO_CAPS_BGRA)
    );

static void gst_d3dvideosink_init_interfaces (GType type);

GST_BOILERPLATE_FULL (GstD3DVideoSink, gst_d3dvideosink, GstVideoSink,
    GST_TYPE_VIDEO_SINK, gst_d3dvideosink_init_interfaces);

enum
{
  PROP_0, PROP_KEEP_ASPECT_RATIO, PROP_PIXEL_ASPECT_RATIO,
  PROP_ENABLE_NAVIGATION_EVENTS, PROP_LAST
};

/* GObject methods */
static void gst_d3dvideosink_finalize (GObject * gobject);
static void gst_d3dvideosink_set_property (GObject * object, guint prop_id,
    const GValue * value, GParamSpec * pspec);
static void gst_d3dvideosink_get_property (GObject * object, guint prop_id,
    GValue * value, GParamSpec * pspec);

/* GstElement methods */
static GstStateChangeReturn gst_d3dvideosink_change_state (GstElement * element,
    GstStateChange transition);

/* GstBaseSink methods */
static gboolean gst_d3dvideosink_start (GstBaseSink * bsink);
static gboolean gst_d3dvideosink_stop (GstBaseSink * bsink);
static gboolean gst_d3dvideosink_set_caps (GstBaseSink * bsink, GstCaps * caps);
static GstCaps *gst_d3dvideosink_get_caps (GstBaseSink * bsink);
static GstFlowReturn gst_d3dvideosink_show_frame (GstVideoSink * sink,
    GstBuffer * buffer);

/* GstXOverlay methods */
static void gst_d3dvideosink_set_window_handle (GstXOverlay * overlay,
    guintptr window_id);
static void gst_d3dvideosink_expose (GstXOverlay * overlay);

/* GstNavigation methods */
static void gst_d3dvideosink_navigation_send_event (GstNavigation * navigation,
    GstStructure * structure);

/* WndProc methods */
LRESULT APIENTRY WndProc (HWND hWnd, UINT message, WPARAM wParam,
    LPARAM lParam);
LRESULT APIENTRY SharedHiddenWndProc (HWND hWnd, UINT message, WPARAM wParam,
    LPARAM lParam);
static void gst_d3dvideosink_wnd_proc (GstD3DVideoSink * sink, HWND hWnd,
    UINT message, WPARAM wParam, LPARAM lParam);

/* HookProc methods */
LRESULT APIENTRY WndProcHook (HWND hWnd, UINT message, WPARAM wParam,
    LPARAM lParam);
LRESULT CALLBACK gst_d3dvideosink_hook_proc (int nCode, WPARAM wParam,
    LPARAM lParam);

/* Paint/update methods */
static void gst_d3dvideosink_update (GstBaseSink * bsink);
static gboolean gst_d3dvideosink_refresh (GstD3DVideoSink * sink);
static gboolean gst_d3dvideosink_update_all (GstD3DVideoSink * sink);
static gboolean gst_d3dvideosink_refresh_all (GstD3DVideoSink * sink);
static void gst_d3dvideosink_stretch (GstD3DVideoSink * sink,
    LPDIRECT3DSURFACE9 backBuffer);

/* Misc methods */
BOOL WINAPI D3dDllMain (HINSTANCE hinstDll, DWORD fdwReason, PVOID fImpLoad);
static void gst_d3dvideosink_remove_window_for_renderer (GstD3DVideoSink *
    sink);
static gboolean gst_d3dvideosink_initialize_direct3d (GstD3DVideoSink * sink);
static gboolean gst_d3dvideosink_initialize_d3d_device (GstD3DVideoSink * sink);

static gboolean gst_d3dvideosink_notify_device_init (GstD3DVideoSink * sink);
static gboolean gst_d3dvideosink_notify_device_lost (GstD3DVideoSink * sink);
static gboolean gst_d3dvideosink_notify_device_reset (GstD3DVideoSink * sink);
static gboolean gst_d3dvideosink_notify_device_reinit (GstD3DVideoSink * sink);
static gboolean gst_d3dvideosink_notify_device_resize (GstD3DVideoSink * sink);
static gboolean gst_d3dvideosink_device_lost (GstD3DVideoSink * sink);
static gboolean gst_d3dvideosink_resize_device (GstD3DVideoSink * sink);
static gboolean gst_d3dvideosink_release_d3d_device (GstD3DVideoSink * sink);
static gboolean gst_d3dvideosink_release_direct3d (GstD3DVideoSink * sink);
static gboolean gst_d3dvideosink_window_size (GstD3DVideoSink * sink,
    gint * width, gint * height);
static gboolean gst_d3dvideosink_direct3d_supported (GstD3DVideoSink * sink);
static gboolean gst_d3dvideosink_shared_hidden_window_thread (GstD3DVideoSink *
    sink);
static void gst_d3dvideosink_flush_gpu (GstD3DVideoSink * sink);
static void gst_d3dvideosink_hook_window_for_renderer (GstD3DVideoSink * sink);
static void gst_d3dvideosink_unhook_window_for_renderer (GstD3DVideoSink *
    sink);
static void gst_d3dvideosink_unhook_all_windows (void);
static void gst_d3dvideosink_log_debug (const gchar * file,
    const gchar * function, gint line, const gchar * format, va_list args);
static void gst_d3dvideosink_log_warning (const gchar * file,
    const gchar * function, gint line, const gchar * format, va_list args);
static void gst_d3dvideosink_log_error (const gchar * file,
    const gchar * function, gint line, const gchar * format, va_list args);
static void gst_d3dvideosink_set_window_for_renderer (GstD3DVideoSink * sink);
static DirectXInitParams directx_init_params = {
  gst_d3dvideosink_log_debug, gst_d3dvideosink_log_warning,
  gst_d3dvideosink_log_error
};

/* TODO: event, preroll, buffer_alloc? 
 * buffer_alloc won't generally be all that useful because the renderers require a 
 * different stride to GStreamer's implicit values. 
 */

BOOL WINAPI
D3dDllMain (HINSTANCE hinstDll, DWORD fdwReason, PVOID fImpLoad)
{
  switch (fdwReason) {
    case DLL_PROCESS_ATTACH:
      g_hinstDll = hinstDll;
      break;
    case DLL_THREAD_ATTACH:
      break;
    case DLL_THREAD_DETACH:
      break;
    case DLL_PROCESS_DETACH:
      gst_d3dvideosink_unhook_all_windows ();
      break;
  }
  return TRUE;
}

static gboolean
gst_d3dvideosink_interface_supported (GstImplementsInterface * iface,
    GType type)
{
  return (type == GST_TYPE_X_OVERLAY || type == GST_TYPE_NAVIGATION);
}

static void
gst_d3dvideosink_interface_init (GstImplementsInterfaceClass * klass)
{
  klass->supported = gst_d3dvideosink_interface_supported;
}

static void
gst_d3dvideosink_xoverlay_interface_init (GstXOverlayClass * iface)
{
  iface->set_window_handle = gst_d3dvideosink_set_window_handle;
  iface->expose = gst_d3dvideosink_expose;
}

static void
gst_d3dvideosink_navigation_interface_init (GstNavigationInterface * iface)
{
  iface->send_event = gst_d3dvideosink_navigation_send_event;
}

static void
gst_d3dvideosink_init_interfaces (GType type)
{
  static const GInterfaceInfo iface_info = {
    (GInterfaceInitFunc) gst_d3dvideosink_interface_init,
    NULL,
    NULL
  };

  static const GInterfaceInfo xoverlay_info = {
    (GInterfaceInitFunc) gst_d3dvideosink_xoverlay_interface_init,
    NULL,
    NULL
  };

  static const GInterfaceInfo navigation_info = {
    (GInterfaceInitFunc) gst_d3dvideosink_navigation_interface_init,
    NULL,
    NULL,
  };

  g_type_add_interface_static (type, GST_TYPE_IMPLEMENTS_INTERFACE,
      &iface_info);
  g_type_add_interface_static (type, GST_TYPE_X_OVERLAY, &xoverlay_info);
  g_type_add_interface_static (type, GST_TYPE_NAVIGATION, &navigation_info);

  GST_DEBUG_CATEGORY_INIT (d3dvideosink_debug, "d3dvideosink", 0,
      "Direct3D video sink");
}

static void
gst_d3dvideosink_base_init (gpointer klass)
{
  GstElementClass *element_class = GST_ELEMENT_CLASS (klass);

  gst_element_class_add_static_pad_template (element_class, &sink_template);

  gst_element_class_set_details_simple (element_class, "Direct3D video sink",
      "Sink/Video",
      "Display data using a Direct3D video renderer",
      "David Hoyt <dhoyt@hoytsoft.org>");
}

static void
gst_d3dvideosink_class_init (GstD3DVideoSinkClass * klass)
{
  GObjectClass *gobject_class;
  GstElementClass *gstelement_class;
  GstBaseSinkClass *gstbasesink_class;
  GstVideoSinkClass *gstvideosink_class;

  gobject_class = (GObjectClass *) klass;
  gstelement_class = (GstElementClass *) klass;
  gstbasesink_class = (GstBaseSinkClass *) klass;
  gstvideosink_class = (GstVideoSinkClass *) klass;

  gobject_class->finalize = GST_DEBUG_FUNCPTR (gst_d3dvideosink_finalize);
  gobject_class->set_property =
      GST_DEBUG_FUNCPTR (gst_d3dvideosink_set_property);
  gobject_class->get_property =
      GST_DEBUG_FUNCPTR (gst_d3dvideosink_get_property);

  gstelement_class->change_state =
      GST_DEBUG_FUNCPTR (gst_d3dvideosink_change_state);

  gstbasesink_class->get_caps = GST_DEBUG_FUNCPTR (gst_d3dvideosink_get_caps);
  gstbasesink_class->set_caps = GST_DEBUG_FUNCPTR (gst_d3dvideosink_set_caps);
  gstbasesink_class->start = GST_DEBUG_FUNCPTR (gst_d3dvideosink_start);
  gstbasesink_class->stop = GST_DEBUG_FUNCPTR (gst_d3dvideosink_stop);
  /*gstbasesink_class->unlock = GST_DEBUG_FUNCPTR (gst_d3dvideosink_unlock); */
  /*gstbasesink_class->unlock_stop = GST_DEBUG_FUNCPTR (gst_d3dvideosink_unlock_stop); */

  gstvideosink_class->show_frame =
      GST_DEBUG_FUNCPTR (gst_d3dvideosink_show_frame);

  /* Add properties */
  g_object_class_install_property (G_OBJECT_CLASS (klass),
      PROP_KEEP_ASPECT_RATIO, g_param_spec_boolean ("force-aspect-ratio",
          "Force aspect ratio",
          "When enabled, scaling will respect original aspect ratio", FALSE,
          (GParamFlags) G_PARAM_READWRITE));
  g_object_class_install_property (G_OBJECT_CLASS (klass),
      PROP_PIXEL_ASPECT_RATIO, g_param_spec_string ("pixel-aspect-ratio",
          "Pixel Aspect Ratio",
          "The pixel aspect ratio of the device", "1/1",
          (GParamFlags) (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)));
  g_object_class_install_property (G_OBJECT_CLASS (klass),
      PROP_ENABLE_NAVIGATION_EVENTS,
      g_param_spec_boolean ("enable-navigation-events",
          "Enable navigation events",
          "When enabled, navigation events are sent upstream", TRUE,
          (GParamFlags) G_PARAM_READWRITE));

  /* Initialize DirectX abstraction */
  GST_DEBUG ("Initializing DirectX abstraction layer");
  directx_initialize (&directx_init_params);

  /* Initialize DirectX API */
  if (!directx_initialize_best_available_api ())
    GST_DEBUG ("Unable to initialize DirectX");

  /* Determine DirectX version */
  klass->directx_api = directx_get_best_available_api ();
  klass->directx_version =
      (klass->directx_api !=
      NULL ? klass->directx_api->version : DIRECTX_VERSION_UNKNOWN);
  klass->is_directx_supported = directx_is_supported ();
}

static void
gst_d3dvideosink_clear (GstD3DVideoSink * sink)
{
  sink->enable_navigation_events = TRUE;
  sink->keep_aspect_ratio = FALSE;

  sink->window_closed = FALSE;
  sink->window_handle = NULL;
  sink->is_new_window = FALSE;
  sink->is_hooked = FALSE;
}

static void
gst_d3dvideosink_init (GstD3DVideoSink * sink, GstD3DVideoSinkClass * klass)
{
  gst_d3dvideosink_clear (sink);

  sink->d3d_device_lock = g_mutex_new ();

  sink->par = g_new0 (GValue, 1);
  g_value_init (sink->par, GST_TYPE_FRACTION);
  gst_value_set_fraction (sink->par, 1, 1);

  /* TODO: Copied from GstVideoSink; should we use that as base class? */
  /* 20ms is more than enough, 80-130ms is noticable */
  gst_base_sink_set_max_lateness (GST_BASE_SINK (sink), 20 * GST_MSECOND);
  gst_base_sink_set_qos_enabled (GST_BASE_SINK (sink), TRUE);
}

static void
gst_d3dvideosink_finalize (GObject * gobject)
{
  GstD3DVideoSink *sink = GST_D3DVIDEOSINK (gobject);

  if (sink->par) {
    g_free (sink->par);
    sink->par = NULL;
  }

  g_mutex_free (sink->d3d_device_lock);
  sink->d3d_device_lock = NULL;

  G_OBJECT_CLASS (parent_class)->finalize (gobject);
}

static void
gst_d3dvideosink_set_property (GObject * object, guint prop_id,
    const GValue * value, GParamSpec * pspec)
{
  GstD3DVideoSink *sink = GST_D3DVIDEOSINK (object);

  switch (prop_id) {
    case PROP_ENABLE_NAVIGATION_EVENTS:
      sink->enable_navigation_events = g_value_get_boolean (value);
      break;
    case PROP_KEEP_ASPECT_RATIO:
      sink->keep_aspect_ratio = g_value_get_boolean (value);
      break;
    case PROP_PIXEL_ASPECT_RATIO:
      g_free (sink->par);
      sink->par = g_new0 (GValue, 1);
      g_value_init (sink->par, GST_TYPE_FRACTION);
      if (!g_value_transform (value, sink->par)) {
        g_warning ("Could not transform string to aspect ratio");
        gst_value_set_fraction (sink->par, 1, 1);
      }
      GST_DEBUG_OBJECT (sink, "set PAR to %d/%d",
          gst_value_get_fraction_numerator (sink->par),
          gst_value_get_fraction_denominator (sink->par));
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
  }
}

static void
gst_d3dvideosink_get_property (GObject * object, guint prop_id,
    GValue * value, GParamSpec * pspec)
{
  GstD3DVideoSink *sink = GST_D3DVIDEOSINK (object);

  switch (prop_id) {
    case PROP_ENABLE_NAVIGATION_EVENTS:
      g_value_set_boolean (value, sink->enable_navigation_events);
      break;
    case PROP_KEEP_ASPECT_RATIO:
      g_value_set_boolean (value, sink->keep_aspect_ratio);
      break;
    case PROP_PIXEL_ASPECT_RATIO:
      g_value_transform (sink->par, value);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
  }
}

static GstCaps *
gst_d3dvideosink_get_caps (GstBaseSink * basesink)
{
  GstD3DVideoSink *sink = GST_D3DVIDEOSINK (basesink);
  GstCaps *caps = gst_caps_new_empty ();
  const GstCaps *tempCaps =
      gst_pad_get_pad_template_caps (GST_VIDEO_SINK_PAD (sink));

  /* restrict caps based on the hw capabilities */
  if (shared.d3d) {
    D3DDISPLAYMODE d3ddm;
    if (FAILED (IDirect3D9_GetAdapterDisplayMode (shared.d3d,
                D3DADAPTER_DEFAULT, &d3ddm))) {
      GST_WARNING ("Unable to request adapter display mode");
      gst_caps_unref (caps);
      caps = gst_caps_copy (tempCaps);
    } else {
      gint i;
      GstCaps *c = gst_caps_normalize (tempCaps);

      for (i = 0; i < gst_caps_get_size (c); i++) {
        D3DFORMAT d3dfourcc = 0;
        GstStructure *stru = gst_caps_get_structure (c, i);
        if (!gst_structure_has_name (stru, "video/x-raw-rgb")) {
          gst_structure_get_fourcc (stru, "format", (guint32 *) & d3dfourcc);
          switch ((guint32) d3dfourcc) {
            case GST_MAKE_FOURCC ('Y', 'V', '1', '2'):
            case GST_MAKE_FOURCC ('I', '4', '2', '0'):
              d3dfourcc = (D3DFORMAT) MAKEFOURCC ('Y', 'V', '1', '2');
              break;
            default:
              break;
          }
          if (d3dfourcc && SUCCEEDED (IDirect3D9_CheckDeviceFormat (shared.d3d,
                      D3DADAPTER_DEFAULT,
                      D3DDEVTYPE_HAL,
                      d3ddm.Format, 0, D3DRTYPE_SURFACE, d3dfourcc))) {
            /* hw supports this format */
            gst_caps_append (caps, gst_caps_copy_nth (c, i));
          }
        } else {
          /* rgb formats */
          gst_caps_append (caps, gst_caps_copy_nth (c, i));
        }
      }
      gst_caps_unref (c);
    }
  } else {
    gst_caps_unref (caps);
    caps = gst_caps_copy (tempCaps);
  }
  return caps;
}

static void
gst_d3dvideosink_close_window (GstD3DVideoSink * sink)
{
  if (!sink || !sink->window_handle)
    return;

  if (!sink->is_new_window) {
    gst_d3dvideosink_remove_window_for_renderer (sink);
    return;
  }

  SendMessage (sink->window_handle, WM_CLOSE, (WPARAM) NULL, (WPARAM) NULL);
  g_thread_join (sink->window_thread);
  sink->is_new_window = FALSE;
}

/* Call this function with the D3D_DEVICE lock */
static gboolean
gst_d3dvideosink_resize (GstD3DVideoSink *sink)
{
  gint width, height;
  gboolean resized = FALSE;

  gst_d3dvideosink_window_size(sink, &width, &height);
  if (sink->d3dpp.BackBufferWidth == 0 && sink->d3dpp.BackBufferHeight == 0) {
    return FALSE;
  }
  if (sink->d3dpp.BackBufferWidth != width ||
      sink->d3dpp.BackBufferHeight != height) {
    GST_DEBUG_OBJECT (sink, "Resizing device to %d %d",
        (gint) sink->d3dpp.BackBufferWidth,
        (gint) sink->d3dpp.BackBufferHeight);
    sink->d3dpp.BackBufferWidth = width;
    sink->d3dpp.BackBufferHeight = height;
    gst_d3dvideosink_notify_device_resize (sink);
    resized = TRUE;
  }
  return resized;
}

static gboolean
gst_d3dvideosink_create_shared_hidden_window (GstD3DVideoSink * sink)
{
  GST_DEBUG ("Creating Direct3D hidden window");

  shared.hidden_window_created_signal = CreateSemaphore (NULL, 0, 1, NULL);
  if (shared.hidden_window_created_signal == NULL)
    goto failed;

  shared.hidden_window_thread = g_thread_create ((GThreadFunc)
      gst_d3dvideosink_shared_hidden_window_thread, sink, TRUE, NULL);

  /* wait maximum 60 seconds for window to be created */
  if (WaitForSingleObject (shared.hidden_window_created_signal,
          60000) != WAIT_OBJECT_0)
    goto failed;

  CloseHandle (shared.hidden_window_created_signal);

  GST_DEBUG ("Successfully created Direct3D hidden window, handle: %p",
      shared.hidden_window_handle);

  return (shared.hidden_window_handle != NULL);

failed:
  CloseHandle (shared.hidden_window_created_signal);
  GST_ELEMENT_ERROR (sink, RESOURCE, WRITE,
      ("Error creating Direct3D hidden window"), (NULL));
  return FALSE;
}

static gboolean
gst_d3dvideosink_shared_hidden_window_created (GstD3DVideoSink * sink)
{
  /* Should only be called from the shared window thread. */
  ReleaseSemaphore (shared.hidden_window_created_signal, 1, NULL);
  return TRUE;
}

static gboolean
gst_d3dvideosink_shared_hidden_window_thread (GstD3DVideoSink * sink)
{
  WNDCLASS WndClass;
  HWND hWnd;
  MSG msg;

  memset (&WndClass, 0, sizeof (WNDCLASS));
  WndClass.hInstance = GetModuleHandle (NULL);
  WndClass.lpszClassName = TEXT ("GST-Shared-Hidden-D3DSink");
  WndClass.lpfnWndProc = SharedHiddenWndProc;
  if (!RegisterClass (&WndClass)) {
    GST_ERROR ("Unable to register Direct3D hidden window class");
    return FALSE;
  }

  hWnd = CreateWindowEx (0, WndClass.lpszClassName,
      TEXT ("GStreamer Direct3D hidden window"),
      WS_POPUP, 0, 0, 1, 1, HWND_MESSAGE, NULL, WndClass.hInstance, sink);

  if (hWnd == NULL) {
    GST_ERROR_OBJECT (sink, "Failed to create Direct3D hidden window");
    goto error;
  }

  GST_DEBUG ("Direct3D hidden window handle: %p", hWnd);

  shared.hidden_window_handle = hWnd;
  shared.device_lost_timer = 0;
  shared.device_lost = FALSE;

  gst_d3dvideosink_shared_hidden_window_created (sink);

  GST_DEBUG ("Entering Direct3D hidden window message loop");

  /* start message loop processing */
  while (TRUE) {
    while (GetMessage (&msg, NULL, 0, 0)) {
      TranslateMessage (&msg);
      DispatchMessage (&msg);
    }

    if (msg.message == WM_QUIT || msg.message == WM_CLOSE)
      break;
  }

  GST_DEBUG ("Leaving Direct3D hidden window message loop");

/*success:*/
  /* Kill the device lost timer if it's running */
  if (shared.device_lost_timer != 0)
    KillTimer (hWnd, shared.device_lost_timer);
  UnregisterClass (WndClass.lpszClassName, WndClass.hInstance);

  shared.device_lost_timer = 0;
  return TRUE;

error:
  /* Kill the device lost timer if it's running */
  if (shared.device_lost_timer != 0)
    KillTimer (hWnd, shared.device_lost_timer);
  if (hWnd)
    DestroyWindow (hWnd);
  UnregisterClass (WndClass.lpszClassName, WndClass.hInstance);

  shared.hidden_window_handle = NULL;
  shared.device_lost_timer = 0;

  ReleaseSemaphore (shared.hidden_window_created_signal, 1, NULL);
  return FALSE;
}

LRESULT APIENTRY
SharedHiddenWndProc (HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
  GstD3DVideoSink *sink;

  /* lParam holds pointer to the sink. */

  sink = (GstD3DVideoSink *) lParam;
  switch (message) {
    case WM_DIRECTX_D3D_INIT_DEVICE:
    {
      shared.device_lost_sink = NULL;
      GST_DEBUG ("Initializing Direct3D");
      if (!gst_d3dvideosink_initialize_d3d_device (sink))
        gst_d3dvideosink_notify_device_lost(sink);
      else
        GST_DEBUG ("Direct3D initialization complete");
      break;

    }
    case WM_DIRECTX_D3D_INIT_DEVICELOST:
    {
      if (shared.device_lost)
        break;

      shared.device_lost = TRUE;
      GST_D3DVIDEOSINK_D3D_DEVICE_LOCK (sink);


      /* Handle device lost by creating a timer and posting WM_D3D_DEVICELOST twice a second */
      /* Create a timer to periodically check the d3d device and attempt to recreate it */
      shared.device_lost_timer = SetTimer (hWnd, IDT_DEVICELOST, 500, NULL);
      shared.device_lost_sink = sink;

      /* Try it once immediately */
      SendMessage (hWnd, WM_DIRECTX_D3D_DEVICELOST, 0, (LPARAM) sink);
      break;
    }
    case WM_TIMER:
    {
      /* Did we receive a message to check if the device is available again? */
      if (wParam == IDT_DEVICELOST) {
        /* This will synchronously call SharedHiddenWndProc() because this thread is the one that created the window. */
        SendMessage (hWnd, WM_DIRECTX_D3D_DEVICELOST, 0,
            (LPARAM) shared.device_lost_sink);
        return 0;
      }
      break;
    }
    case WM_DIRECTX_D3D_DEVICELOST:
    {
      return gst_d3dvideosink_device_lost (sink);
    }
    case WM_DIRECTX_D3D_END_DEVICELOST:
    {
      if (!shared.device_lost)
        break;

      /* gst_d3dvideosink_notify_device_reset() sends this message. */
      if (shared.device_lost_timer != 0)
        KillTimer (hWnd, shared.device_lost_timer);

      shared.device_lost_timer = 0;
      shared.device_lost = FALSE;

      GST_D3DVIDEOSINK_D3D_DEVICE_UNLOCK (sink);

      /* Refresh the video with the last buffer */
      gst_d3dvideosink_update_all (sink);

      /* Then redraw just in case we don't have a last buffer */
      gst_d3dvideosink_refresh_all (sink);

      shared.device_lost_sink = NULL;
      break;
    }
    case WM_DESTROY:
    {
      PostQuitMessage (0);
      return 0;
    }
    case WM_DIRECTX_D3D_RESIZE:
    {
      return gst_d3dvideosink_resize_device (sink);
    }
  }

  return DefWindowProc (hWnd, message, wParam, lParam);
}

static void
gst_d3dvideosink_close_shared_hidden_window (GstD3DVideoSink * sink)
{
  if (!shared.hidden_window_handle)
    return;

  SendMessage (shared.hidden_window_handle, WM_CLOSE, (WPARAM) NULL,
      (WPARAM) NULL);
  if (shared.hidden_window_thread) {
    g_thread_join (shared.hidden_window_thread);
    shared.hidden_window_thread = NULL;
  }
  shared.hidden_window_handle = NULL;

  GST_DEBUG ("Successfully closed Direct3D hidden window");
}

/* WNDPROC for application-supplied windows */
LRESULT APIENTRY
WndProcHook (HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
  /* Handle certain actions specially on the window passed to us.
   * Then forward back to the original window.
   */
  GstD3DVideoSink *sink =
      (GstD3DVideoSink *) GetProp (hWnd, TEXT ("GstD3DVideoSink"));

  if (!sink)
    return FALSE;

  switch (message) {
    case WM_ERASEBKGND:
      return TRUE;
    case WM_COPYDATA:
    {
      gst_d3dvideosink_wnd_proc (sink, hWnd, message, wParam, lParam);
      return TRUE;
    }
    case WM_PAINT:
    {
      LRESULT ret;
      ret = CallWindowProc (sink->prevWndProc, hWnd, message, wParam, lParam);
      /* Call this afterwards to ensure that our paint happens last */
      gst_d3dvideosink_wnd_proc (sink, hWnd, message, wParam, lParam);
      return ret;
    }
    default:
    {
      /* Check it */
      gst_d3dvideosink_wnd_proc (sink, hWnd, message, wParam, lParam);
      return CallWindowProc (sink->prevWndProc, hWnd, message, wParam, lParam);
    }
  }
}

/* WndProc for our default window, if the application didn't supply one */
LRESULT APIENTRY
WndProc (HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
  GstD3DVideoSink *sink;

  if (message == WM_CREATE) {
    /* lParam holds a pointer to a CREATESTRUCT instance which in turn holds the parameter used when creating the window. */
    GstD3DVideoSink *sink =
        (GstD3DVideoSink *) ((LPCREATESTRUCT) lParam)->lpCreateParams;

    /* In our case, this is a pointer to the sink. So we immediately attach it for use in subsequent calls. */
    SetWindowLongPtr (hWnd, GWLP_USERDATA, (LONG_PTR) sink);

    /* signal application we created a window */
    gst_x_overlay_got_window_handle (GST_X_OVERLAY (sink), (guintptr) hWnd);
  }


  sink = (GstD3DVideoSink *) GetWindowLongPtr (hWnd, GWLP_USERDATA);
  gst_d3dvideosink_wnd_proc (sink, hWnd, message, wParam, lParam);

  switch (message) {
    case WM_ERASEBKGND:
    case WM_COPYDATA:
      return TRUE;

    case WM_DESTROY:
    {
      PostQuitMessage (0);
      return 0;
    }
  }

  return DefWindowProc (hWnd, message, wParam, lParam);
}

static void
gst_d3dvideosink_wnd_proc (GstD3DVideoSink * sink, HWND hWnd, UINT message,
    WPARAM wParam, LPARAM lParam)
{
  if (!sink)
    return;
  switch (message) {
    case WM_COPYDATA:
    {
      PCOPYDATASTRUCT p_ipc_cds;
      p_ipc_cds = (PCOPYDATASTRUCT) lParam;
      switch (p_ipc_cds->dwData) {
        case IPC_SET_WINDOW:
        {
          IPCData *p_ipc_data;
          p_ipc_data = (IPCData *) p_ipc_cds->dwData;

          GST_DEBUG ("Received IPC call to subclass the window handler");

          sink->window_handle = p_ipc_data->hwnd;
          sink->prevWndProc =
              (WNDPROC) SetWindowLongPtr (sink->window_handle, GWLP_WNDPROC,
              (LONG_PTR) p_ipc_data->wnd_proc);
          break;
        }
      }
      break;
    }
    case WM_PAINT:
    {
      gst_d3dvideosink_refresh (sink);
      break;
    }
    case WM_CLOSE:
    case WM_DESTROY:
    {
      sink->window_closed = TRUE;
      //GST_ELEMENT_ERROR (sink, RESOURCE, NOT_FOUND, ("Output window was closed"), (NULL));
      break;
    }
    case WM_CHAR:
    case WM_KEYDOWN:
    case WM_KEYUP:
    {
      if (!sink->enable_navigation_events)
        break;
    }
      {
        gunichar2 wcrep[128];
        if (GetKeyNameTextW (lParam, (LPWSTR) wcrep, 128)) {
          gchar *utfrep = g_utf16_to_utf8 (wcrep, 128, NULL, NULL, NULL);
          if (utfrep) {
            if (message == WM_CHAR || message == WM_KEYDOWN)
              gst_navigation_send_key_event (GST_NAVIGATION (sink), "key-press",
                  utfrep);
            if (message == WM_CHAR || message == WM_KEYUP)
              gst_navigation_send_key_event (GST_NAVIGATION (sink),
                  "key-release", utfrep);
            g_free (utfrep);
          }
        }
        break;
      }
    case WM_LBUTTONDOWN:
    case WM_LBUTTONUP:
    case WM_RBUTTONDOWN:
    case WM_RBUTTONUP:
    case WM_MBUTTONDOWN:
    case WM_MBUTTONUP:
    case WM_MOUSEMOVE:
    {
      if (!sink->enable_navigation_events)
        break;
    }
      {
        gint x, y, button;
        const gchar *action;

        switch (message) {
          case WM_MOUSEMOVE:
            button = 0;
            action = "mouse-move";
            break;
          case WM_LBUTTONDOWN:
            button = 1;
            action = "mouse-button-press";
            break;
          case WM_LBUTTONUP:
            button = 1;
            action = "mouse-button-release";
            break;
          case WM_RBUTTONDOWN:
            button = 2;
            action = "mouse-button-press";
            break;
          case WM_RBUTTONUP:
            button = 2;
            action = "mouse-button-release";
            break;
          case WM_MBUTTONDOWN:
            button = 3;
            action = "mouse-button-press";
            break;
          case WM_MBUTTONUP:
            button = 3;
            action = "mouse-button-release";
            break;
          default:
            button = 4;
            action = NULL;
            break;
        }

        x = LOWORD (lParam);
        y = HIWORD (lParam);

        if (button == 0) {
          GST_DEBUG_OBJECT (sink, "Mouse moved to %dx%d", x, y);
        } else
          GST_DEBUG_OBJECT (sink, "Mouse button %d pressed at %dx%d", button, x,
              y);

        if (button < 4)
          gst_navigation_send_mouse_event (GST_NAVIGATION (sink), action,
              button, x, y);

        break;
      }
  }
}

static gpointer
gst_d3dvideosink_window_thread (GstD3DVideoSink * sink)
{
  WNDCLASS WndClass;
  int width, height;
  int offx, offy;
  DWORD exstyle, style;
  HWND video_window;
  RECT rect;
  int screenwidth;
  int screenheight;
  MSG msg;

  memset (&WndClass, 0, sizeof (WNDCLASS));
  WndClass.style = CS_OWNDC | CS_HREDRAW | CS_VREDRAW;
  WndClass.hInstance = GetModuleHandle (NULL);
  WndClass.lpszClassName = TEXT ("GST-D3DSink");
  WndClass.hbrBackground = (HBRUSH) GetStockObject (BLACK_BRUSH);
  WndClass.hCursor = LoadCursor (NULL, IDC_ARROW);
  WndClass.hIcon = LoadIcon (NULL, IDI_APPLICATION);
  WndClass.cbClsExtra = 0;
  WndClass.cbWndExtra = 0;
  WndClass.lpfnWndProc = WndProc;
  RegisterClass (&WndClass);

  /* By default, create a normal top-level window, the size of the video. */

  /* GST_VIDEO_SINK_WIDTH() is the aspect-ratio-corrected size of the video. */
  /* GetSystemMetrics() returns the width of the dialog's border (doubled b/c of left and right borders). */
  width = GST_VIDEO_SINK_WIDTH (sink) + GetSystemMetrics (SM_CXSIZEFRAME) * 2;
  height = GST_VIDEO_SINK_HEIGHT (sink) + GetSystemMetrics (SM_CYCAPTION) +
      (GetSystemMetrics (SM_CYSIZEFRAME) * 2);

  SystemParametersInfo (SPI_GETWORKAREA, 0, &rect, 0);
  screenwidth = rect.right - rect.left;
  screenheight = rect.bottom - rect.top;
  offx = rect.left;
  offy = rect.top;

  /* Make it fit into the screen without changing the aspect ratio. */
  if (width > screenwidth) {
    double ratio = (double) screenwidth / (double) width;
    width = screenwidth;
    height = (int) (height * ratio);
  }

  if (height > screenheight) {
    double ratio = (double) screenheight / (double) height;
    height = screenheight;
    width = (int) (width * ratio);
  }

  style = WS_OVERLAPPEDWINDOW;  /* Normal top-level window */
  exstyle = 0;

  video_window = CreateWindowEx (exstyle, TEXT ("GST-D3DSink"),
      TEXT ("GStreamer Direct3D sink default window"),
      style, offx, offy, width, height, NULL, NULL, WndClass.hInstance, sink);

  if (video_window == NULL) {
    GST_ERROR_OBJECT (sink, "Failed to create window");
    return NULL;
  }

  sink->is_new_window = TRUE;
  sink->window_handle = video_window;

  /* Now show the window, as appropriate */
  ShowWindow (video_window, SW_SHOWNORMAL);

  /* Trigger the initial paint of the window */
  UpdateWindow (video_window);

  ReleaseSemaphore (sink->window_created_signal, 1, NULL);

  /* start message loop processing our default window messages */
  while (TRUE) {
    while (GetMessage (&msg, NULL, 0, 0)) {
      TranslateMessage (&msg);
      DispatchMessage (&msg);
    }

    if (msg.message == WM_QUIT || msg.message == WM_CLOSE)
      break;
  }

  UnregisterClass (WndClass.lpszClassName, WndClass.hInstance);
  sink->window_handle = NULL;
  return NULL;

}

static gboolean
gst_d3dvideosink_create_default_window (GstD3DVideoSink * sink)
{
  if (shared.device_lost)
    return FALSE;

  sink->window_created_signal = CreateSemaphore (NULL, 0, 1, NULL);
  if (sink->window_created_signal == NULL)
    goto failed;

  sink->window_thread =
      g_thread_create ((GThreadFunc) gst_d3dvideosink_window_thread, sink, TRUE,
      NULL);

  /* wait maximum 10 seconds for window to be created */
  if (WaitForSingleObject (sink->window_created_signal, 10000) != WAIT_OBJECT_0)
    goto failed;

  CloseHandle (sink->window_created_signal);
  return (sink->window_handle != NULL);

failed:
  CloseHandle (sink->window_created_signal);
  GST_ELEMENT_ERROR (sink, RESOURCE, WRITE,
      ("Error creating our default window"), (NULL));
  return FALSE;
}

static void
gst_d3dvideosink_set_window_handle (GstXOverlay * overlay, guintptr window_id)
{
  GstD3DVideoSink *sink = GST_D3DVIDEOSINK (overlay);
  HWND hWnd = (HWND) window_id;

  if (hWnd == sink->window_handle) {
    GST_DEBUG ("Window already set");
    return;
  }

  GST_D3DVIDEOSINK_D3D_DEVICE_LOCK (sink);
  /* If we're already playing/paused, then we need to lock the swap chain, and recreate it with the new window. */
  if (sink->d3ddev != NULL) {
    /* Close our existing window if there is one */
    gst_d3dvideosink_close_window (sink);
    /* Save our window id */
    sink->window_handle = hWnd;
    gst_d3dvideosink_set_window_for_renderer (sink);
    sink->window_closed = FALSE;

    gst_d3dvideosink_notify_device_reinit (sink);
  } else {
    sink->window_handle = hWnd;
  }

  GST_D3DVIDEOSINK_D3D_DEVICE_UNLOCK (sink);

  GST_DEBUG_OBJECT (sink, "Direct3D window id successfully changed to %p",
      hWnd);

  gst_d3dvideosink_update (GST_BASE_SINK_CAST (sink));
  return;
}

/* Hook for out-of-process rendering */
LRESULT CALLBACK
gst_d3dvideosink_hook_proc (int nCode, WPARAM wParam, LPARAM lParam)
{
  return CallNextHookEx (NULL, nCode, wParam, lParam);
}

static void
gst_d3dvideosink_set_window_for_renderer (GstD3DVideoSink * sink)
{
  WNDPROC currWndProc;

  /* Application has requested a specific window ID */
  sink->is_new_window = FALSE;
  currWndProc = (WNDPROC) GetWindowLongPtr (sink->window_handle, GWLP_WNDPROC);
  if (sink->prevWndProc != currWndProc && currWndProc != WndProcHook)
    sink->prevWndProc =
        (WNDPROC) SetWindowLongPtr (sink->window_handle, GWLP_WNDPROC,
        (LONG_PTR) WndProcHook);

  /* Allows us to pick up the video sink inside the msg handler */
  SetProp (sink->window_handle, TEXT ("GstD3DVideoSink"), sink);

  if (!(sink->prevWndProc)) {
    /* If we were unable to set the window procedure, it's possible we're attempting to render into the  */
    /* window from a separate process. In that case, we need to use a windows hook to see the messages   */
    /* going to the window we're drawing on. We must take special care that our hook is properly removed */
    /* when we're done. */
    GST_DEBUG ("Unable to set window procedure. Error: %s",
        g_win32_error_message (GetLastError ()));
    GST_D3DVIDEOSINK_SHARED_D3D_HOOK_LOCK;
    gst_d3dvideosink_hook_window_for_renderer (sink);
    GST_D3DVIDEOSINK_SHARED_D3D_HOOK_UNLOCK;
  } else {
    GST_DEBUG ("Set wndproc to %p from %p", WndProcHook, sink->prevWndProc);
    GST_DEBUG ("Set renderer window to %p", sink->window_handle);
  }

  sink->is_new_window = FALSE;
}

static HHOOK
gst_d3dvideosink_find_hook (DWORD pid, DWORD tid)
{
  HWND key;
  GHashTableIter iter;
  GstD3DVideoSinkHookData *value;

  if (!shared.hook_tbl)
    return NULL;

  g_hash_table_iter_init (&iter, shared.hook_tbl);
  while (g_hash_table_iter_next (&iter, (gpointer) & key, (gpointer) & value)) {
    if (value && value->process_id == pid && value->thread_id == tid)
      return value->hook;
  }
  return NULL;
}

static GstD3DVideoSinkHookData *
gst_d3dvideosink_hook_data (HWND window_id)
{
  if (!shared.hook_tbl)
    return NULL;
  return (GstD3DVideoSinkHookData *) g_hash_table_lookup (shared.hook_tbl,
      window_id);
}

static GstD3DVideoSinkHookData *
gst_d3dvideosink_register_hook_data (HWND window_id)
{
  GstD3DVideoSinkHookData *data;
  if (!shared.hook_tbl)
    shared.hook_tbl = g_hash_table_new (NULL, NULL);
  data =
      (GstD3DVideoSinkHookData *) g_hash_table_lookup (shared.hook_tbl,
      window_id);
  if (!data) {
    data =
        (GstD3DVideoSinkHookData *) g_malloc (sizeof (GstD3DVideoSinkHookData));
    memset (data, 0, sizeof (GstD3DVideoSinkHookData));
    g_hash_table_insert (shared.hook_tbl, window_id, data);
  }
  return data;
}

static gboolean
gst_d3dvideosink_unregister_hook_data (HWND window_id)
{
  GstD3DVideoSinkHookData *data;
  if (!shared.hook_tbl)
    return FALSE;
  data =
      (GstD3DVideoSinkHookData *) g_hash_table_lookup (shared.hook_tbl,
      window_id);
  if (!data)
    return TRUE;
  if (g_hash_table_remove (shared.hook_tbl, window_id))
    g_free (data);
  return TRUE;
}

static void
gst_d3dvideosink_hook_window_for_renderer (GstD3DVideoSink * sink)
{
  /* Ensure that our window hook isn't already installed. */
  if (!sink->is_new_window && !sink->is_hooked && sink->window_handle) {
    DWORD pid;
    DWORD tid;

    GST_DEBUG ("Attempting to apply a windows hook in process %lu.",
        GetCurrentProcessId ());

    /* Get thread id of the window in question. */
    tid = GetWindowThreadProcessId (sink->window_handle, &pid);

    if (tid) {
      HHOOK hook;
      GstD3DVideoSinkHookData *data;

      /* Only apply a hook if there's not one already there. It's possible this is the case if there are multiple */
      /* embedded windows that we're hooking inside of the same dialog/thread. */

      hook = gst_d3dvideosink_find_hook (pid, tid);
      data = gst_d3dvideosink_register_hook_data (sink->window_handle);
      if (data && !hook) {
        GST_DEBUG
            ("No other hooks exist for pid %lu and tid %lu. Attempting to add one.",
            pid, tid);
        hook =
            SetWindowsHookEx (WH_CALLWNDPROCRET, gst_d3dvideosink_hook_proc,
            g_hinstDll, tid);
      }

      sink->is_hooked = (hook ? TRUE : FALSE);

      if (sink->is_hooked) {
        data->hook = hook;
        data->process_id = pid;
        data->thread_id = tid;
        data->window_handle = sink->window_handle;

        PostThreadMessage (tid, WM_NULL, 0, 0);

        GST_DEBUG ("Window successfully hooked. GetLastError() returned: %s",
            g_win32_error_message (GetLastError ()));
      } else {
        /* Ensure that we clean up any allocated memory. */
        if (data)
          gst_d3dvideosink_unregister_hook_data (sink->window_handle);
        GST_DEBUG
            ("Unable to hook the window. The system provided error was: %s",
            g_win32_error_message (GetLastError ()));
      }
    }
  }
}

static void
gst_d3dvideosink_unhook_window_for_renderer (GstD3DVideoSink * sink)
{
  if (!sink->is_new_window && sink->is_hooked && sink->window_handle) {
    GstD3DVideoSinkHookData *data;

    GST_DEBUG ("Unhooking a window in process %lu.", GetCurrentProcessId ());

    data = gst_d3dvideosink_hook_data (sink->window_handle);
    if (data) {
      DWORD pid;
      DWORD tid;
      HHOOK hook;

      /* Save off a temp ref to the data */
      hook = data->hook;
      tid = data->thread_id;
      pid = data->process_id;

      /* Free the memory */
      if (gst_d3dvideosink_unregister_hook_data (sink->window_handle)) {
        /* Check if there's anyone else who still has the hook. If so, then we do nothing. */
        /* If not, then go ahead and unhook. */
        if (gst_d3dvideosink_find_hook (pid, tid)) {
          UnhookWindowsHookEx (hook);
          GST_DEBUG ("Unhooked the window for process %lu and thread %lu.", pid,
              tid);
        }
      }
    }

    sink->is_hooked = FALSE;

    GST_DEBUG ("Window successfully unhooked in process %lu.",
        GetCurrentProcessId ());
  }
}

static void
gst_d3dvideosink_unhook_all_windows (void)
{
  /* Unhook all windows that may be currently hooked. This is mainly a precaution in case     */
  /* a wayward process doesn't properly set state back to NULL (which would remove the hook). */

  GST_D3DVIDEOSINK_SHARED_D3D_LOCK;
  GST_D3DVIDEOSINK_SHARED_D3D_HOOK_LOCK;
  {
    GList *item;
    GstD3DVideoSink *s;

    GST_DEBUG ("Attempting to unhook all windows for process %lu",
        GetCurrentProcessId ());

    for (item = g_list_first (shared.element_list); item; item = item->next) {
      s = (GstD3DVideoSink *) item->data;
      gst_d3dvideosink_unhook_window_for_renderer (s);
    }
  }
  GST_D3DVIDEOSINK_SHARED_D3D_HOOK_UNLOCK;
  GST_D3DVIDEOSINK_SHARED_D3D_UNLOCK;
}

static void
gst_d3dvideosink_remove_window_for_renderer (GstD3DVideoSink * sink)
{
  {
    GST_DEBUG ("Removing custom rendering window procedure");
    if (!sink->is_new_window && sink->window_handle) {
      WNDPROC currWndProc;

      /* Retrieve current msg handler */
      currWndProc =
          (WNDPROC) GetWindowLongPtr (sink->window_handle, GWLP_WNDPROC);

      /* Return control of application window */
      if (sink->prevWndProc != NULL && currWndProc == WndProcHook) {
        SetWindowLongPtr (sink->window_handle, GWLP_WNDPROC,
            (LONG_PTR) sink->prevWndProc);

        sink->prevWndProc = NULL;
        sink->window_handle = NULL;
        sink->is_new_window = FALSE;
      }
    }

    GST_D3DVIDEOSINK_SHARED_D3D_HOOK_LOCK;
    gst_d3dvideosink_unhook_window_for_renderer (sink);
    GST_D3DVIDEOSINK_SHARED_D3D_HOOK_UNLOCK;
    /* Remove the property associating our sink with the window */
    RemoveProp (sink->window_handle, TEXT ("GstD3DVideoSink"));
  }
}

static void
gst_d3dvideosink_prepare_window (GstD3DVideoSink * sink)
{
  /* Give the app a last chance to supply a window id */
  if (!sink->window_handle) {
    gst_x_overlay_prepare_xwindow_id (GST_X_OVERLAY (sink));
  }

  /* If the app supplied one, use it. Otherwise, go ahead
   * and create (and use) our own window, if we didn't create
   * one before */
  if (sink->window_handle && sink->is_new_window) {
    GST_D3DVIDEOSINK_D3D_DEVICE_LOCK(sink);
    gst_d3dvideosink_release_d3d_device(sink);
    GST_D3DVIDEOSINK_D3D_DEVICE_UNLOCK(sink);
  } else if (sink->window_handle) {
      gst_d3dvideosink_set_window_for_renderer (sink);
  } else {
    gst_d3dvideosink_create_default_window (sink);
  }
  GST_D3DVIDEOSINK_D3D_DEVICE_LOCK(sink);
  gst_d3dvideosink_notify_device_init (sink);
  GST_D3DVIDEOSINK_D3D_DEVICE_UNLOCK(sink);
}

static GstStateChangeReturn
gst_d3dvideosink_change_state (GstElement * element, GstStateChange transition)
{
  GstD3DVideoSink *sink = GST_D3DVIDEOSINK (element);
  GstStateChangeReturn ret = GST_STATE_CHANGE_SUCCESS;

  switch (transition) {
    case GST_STATE_CHANGE_NULL_TO_READY:
      if (!gst_d3dvideosink_initialize_direct3d (sink))
        return GST_STATE_CHANGE_FAILURE;
      break;
    case GST_STATE_CHANGE_READY_TO_PAUSED:
      break;
    case GST_STATE_CHANGE_PAUSED_TO_PLAYING:
      break;
    case GST_STATE_CHANGE_PLAYING_TO_PAUSED:
      break;
    case GST_STATE_CHANGE_PAUSED_TO_READY:
      break;
    case GST_STATE_CHANGE_READY_TO_NULL:
      break;
  }

  ret = GST_ELEMENT_CLASS (parent_class)->change_state (element, transition);
  if (ret == GST_STATE_CHANGE_FAILURE)
    return ret;

  switch (transition) {
    case GST_STATE_CHANGE_PLAYING_TO_PAUSED:
      break;
    case GST_STATE_CHANGE_PAUSED_TO_READY:
      if (!sink->is_new_window) {
        gst_d3dvideosink_remove_window_for_renderer (sink);
      }
      break;
    case GST_STATE_CHANGE_READY_TO_NULL:
      gst_d3dvideosink_release_direct3d (sink);
      gst_d3dvideosink_clear (sink);
      break;
    case GST_STATE_CHANGE_NULL_TO_READY:
      break;
    case GST_STATE_CHANGE_READY_TO_PAUSED:
      break;
    case GST_STATE_CHANGE_PAUSED_TO_PLAYING:
      break;
  }

  return ret;
}

static gboolean
gst_d3dvideosink_start (GstBaseSink * bsink)
{
  GstD3DVideoSink *sink = GST_D3DVIDEOSINK (bsink);

  /* Determine if Direct 3D is supported */
  return gst_d3dvideosink_direct3d_supported (sink);
}

static gboolean
gst_d3dvideosink_set_caps (GstBaseSink * bsink, GstCaps * caps)
{
  GstD3DVideoSink *sink;
  GstCaps *sink_caps;
  gint video_width, video_height;
  gint video_par_n, video_par_d;        /* video's PAR */
  gint display_par_n, display_par_d;    /* display's PAR */
  gint fps_n, fps_d;
  guint num, den;

  sink = GST_D3DVIDEOSINK (bsink);
  sink_caps = gst_static_pad_template_get_caps (&sink_template);

  GST_DEBUG_OBJECT (sink,
      "In setcaps. Possible caps %" GST_PTR_FORMAT ", setting caps %"
      GST_PTR_FORMAT, sink_caps, caps);

  if (!gst_caps_can_intersect (sink_caps, caps))
    goto incompatible_caps;

  if (!gst_video_format_parse_caps (caps, &sink->format, &video_width,
          &video_height))
    goto invalid_format;

  if (!gst_video_parse_caps_framerate (caps, &fps_n, &fps_d) ||
      !video_width || !video_height)
    goto incomplete_caps;

  /* get aspect ratio from caps if it's present, and
   * convert video width and height to a display width and height
   * using wd / hd = wv / hv * PARv / PARd */

  /* get video's PAR */
  if (!gst_video_parse_caps_pixel_aspect_ratio (caps, &video_par_n,
          &video_par_d)) {
    video_par_n = 1;
    video_par_d = 1;
  }
  /* get display's PAR */
  if (sink->par) {
    display_par_n = gst_value_get_fraction_numerator (sink->par);
    display_par_d = gst_value_get_fraction_denominator (sink->par);
  } else {
    display_par_n = 1;
    display_par_d = 1;
  }

  if (!gst_video_calculate_display_ratio (&num, &den, video_width,
          video_height, video_par_n, video_par_d, display_par_n, display_par_d))
    goto no_disp_ratio;

  GST_DEBUG_OBJECT (sink,
      "video width/height: %dx%d, calculated display ratio: %d/%d",
      video_width, video_height, num, den);

  /* now find a width x height that respects this display ratio.
   * prefer those that have one of w/h the same as the incoming video
   * using wd / hd = num / den */

  /* start with same height, because of interlaced video */
  /* check hd / den is an integer scale factor, and scale wd with the PAR */
  if (video_height % den == 0) {
    GST_DEBUG_OBJECT (sink, "keeping video height");
    GST_VIDEO_SINK_WIDTH (sink) = (guint)
        gst_util_uint64_scale_int (video_height, num, den);
    GST_VIDEO_SINK_HEIGHT (sink) = video_height;
  } else if (video_width % num == 0) {
    GST_DEBUG_OBJECT (sink, "keeping video width");
    GST_VIDEO_SINK_WIDTH (sink) = video_width;
    GST_VIDEO_SINK_HEIGHT (sink) = (guint)
        gst_util_uint64_scale_int (video_width, den, num);
  } else {
    GST_DEBUG_OBJECT (sink, "approximating while keeping video height");
    GST_VIDEO_SINK_WIDTH (sink) = (guint)
        gst_util_uint64_scale_int (video_height, num, den);
    GST_VIDEO_SINK_HEIGHT (sink) = video_height;
  }
  GST_DEBUG_OBJECT (sink, "scaling to %dx%d",
      GST_VIDEO_SINK_WIDTH (sink), GST_VIDEO_SINK_HEIGHT (sink));

  if (GST_VIDEO_SINK_WIDTH (sink) <= 0 || GST_VIDEO_SINK_HEIGHT (sink) <= 0)
    goto no_display_size;

  sink->width = video_width;
  sink->height = video_height;

  /* Create a window (or start using an application-supplied one, then connect the graph */
  gst_d3dvideosink_prepare_window (sink);

  return TRUE;
  /* ERRORS */
incompatible_caps:
  {
    GST_ERROR_OBJECT (sink, "caps incompatible");
    return FALSE;
  }
incomplete_caps:
  {
    GST_DEBUG_OBJECT (sink, "Failed to retrieve either width, "
        "height or framerate from intersected caps");
    return FALSE;
  }
invalid_format:
  {
    gchar *caps_txt = gst_caps_to_string (caps);
    GST_DEBUG_OBJECT (sink,
        "Could not locate image format from caps %s", caps_txt);
    g_free (caps_txt);
    return FALSE;
  }
no_disp_ratio:
  {
    GST_ELEMENT_ERROR (sink, CORE, NEGOTIATION, (NULL),
        ("Error calculating the output display ratio of the video."));
    return FALSE;
  }
no_display_size:
  {
    GST_ELEMENT_ERROR (sink, CORE, NEGOTIATION, (NULL),
        ("Error calculating the output display ratio of the video."));
    return FALSE;
  }
}

static gboolean
gst_d3dvideosink_stop (GstBaseSink * bsink)
{
  GstD3DVideoSink *sink = GST_D3DVIDEOSINK (bsink);
  gst_d3dvideosink_close_window (sink);
  return TRUE;
}

static void
gst_d3dvideosink_flush_gpu (GstD3DVideoSink * sink)
{
  LPDIRECT3DQUERY9 pEventQuery = NULL;

  IDirect3DDevice9_CreateQuery (sink->d3ddev, D3DQUERYTYPE_EVENT, &pEventQuery);
  if (pEventQuery) {
    IDirect3DQuery9_Issue (pEventQuery, D3DISSUE_END);
    /* Empty the command buffer and wait until the GPU is idle. */
    while (S_FALSE == IDirect3DQuery9_GetData (pEventQuery, NULL, 0,
            D3DGETDATA_FLUSH));
    IDirect3DQuery9_Release (pEventQuery);
  }
}

static G_GNUC_UNUSED void
gst_d3dvideosink_wait_for_vsync (GstD3DVideoSink * sink)
{
  if (sink->d3dpp.PresentationInterval == D3DPRESENT_INTERVAL_IMMEDIATE) {
    D3DRASTER_STATUS raster_stat;
    D3DDISPLAYMODE d3ddm;
    UINT lastScanline = 0;
    UINT vblankStart = 0;
    HANDLE thdl = GetCurrentThread ();
    int prio = GetThreadPriority (thdl);
    ZeroMemory (&d3ddm, sizeof (d3ddm));

    IDirect3DDevice9_GetDisplayMode (sink->d3ddev, 0, &d3ddm);
    vblankStart = d3ddm.Height - 10;
    SetThreadPriority (thdl, THREAD_PRIORITY_TIME_CRITICAL);
    do {
      if (FAILED (IDirect3DDevice9_GetRasterStatus (sink->d3ddev, 0,
                  &raster_stat))) {
        GST_ERROR_OBJECT (sink, "GetRasterStatus failed");
      }
      break;
      if (!raster_stat.InVBlank) {
        if (raster_stat.ScanLine < lastScanline) {
          GST_INFO_OBJECT (sink, "missed last vsync curr : %d",
              raster_stat.ScanLine);
          break;
        }
        lastScanline = raster_stat.ScanLine;
        SwitchToThread ();
      }
    } while (raster_stat.ScanLine < vblankStart);
    SetThreadPriority (thdl, prio);
  }
}

/* Call this function with the D3D_DEV lock */
static GstFlowReturn
gst_d3dvideosink_copy_frame (GstD3DVideoSink * sink, GstBuffer * buffer)
{
  LPDIRECT3DSURFACE9 drawSurface = NULL;

  if (!sink->d3ddev) {
    if (!shared.device_lost) {
      GST_ERROR_OBJECT (sink, "No Direct3D device has been created, stopping");
      goto error;
    } else {
      GST_WARNING_OBJECT (sink,
          "Direct3D device is lost. Maintaining flow until it has been reset.");
      goto success;
    }
  }

  if (sink->window_closed) {
    GST_ERROR_OBJECT (sink, "Window has been closed, stopping");
    goto error;
  }


  drawSurface = sink->d3d_offscreen_surface;

  if (SUCCEEDED (IDirect3DDevice9_BeginScene (sink->d3ddev))) {
    if (GST_BUFFER_DATA (buffer)) {
      D3DLOCKED_RECT lr;
      guint8 *dest, *source;
      int srcstride, dststride, i;

      IDirect3DSurface9_LockRect (drawSurface, &lr, NULL, 0);
      dest = (guint8 *) lr.pBits;
      source = GST_BUFFER_DATA (buffer);

      if (dest) {
        if (gst_video_format_is_yuv (sink->format)) {
          guint32 fourcc = gst_video_format_to_fourcc (sink->format);

          switch (fourcc) {
            case GST_MAKE_FOURCC ('Y', 'U', 'Y', '2'):
            case GST_MAKE_FOURCC ('Y', 'U', 'Y', 'V'):
            case GST_MAKE_FOURCC ('U', 'Y', 'V', 'Y'):
              dststride = lr.Pitch;
              srcstride = GST_BUFFER_SIZE (buffer) / sink->height;
              for (i = 0; i < sink->height; ++i)
                memcpy (dest + dststride * i, source + srcstride * i,
                    srcstride);
              break;
            case GST_MAKE_FOURCC ('I', '4', '2', '0'):
            case GST_MAKE_FOURCC ('Y', 'V', '1', '2'):
            {
              int srcystride, srcvstride, srcustride;
              int dstystride, dstvstride, dstustride;
              int rows;
              guint8 *srcv, *srcu, *dstv, *dstu;

              rows = sink->height;

              /* Source y, u and v strides */
              srcystride = GST_ROUND_UP_4 (sink->width);
              srcustride = GST_ROUND_UP_8 (sink->width) / 2;
              srcvstride = GST_ROUND_UP_8 (srcystride) / 2;

              /* Destination y, u and v strides */
              dstystride = lr.Pitch;
              dstustride = dstystride / 2;
              dstvstride = dstustride;

              srcu = source + srcystride * GST_ROUND_UP_2 (rows);
              srcv = srcu + srcustride * GST_ROUND_UP_2 (rows) / 2;

              if (fourcc == GST_MAKE_FOURCC ('I', '4', '2', '0')) {
                /* swap u and v planes */
                dstv = dest + dstystride * rows;
                dstu = dstv + dstustride * rows / 2;
              } else {
                dstu = dest + dstystride * rows;
                dstv = dstu + dstustride * rows / 2;
              }

              for (i = 0; i < rows; ++i) {
                /* Copy the y plane */
                memcpy (dest + dstystride * i, source + srcystride * i,
                    srcystride);
              }

              for (i = 0; i < rows / 2; ++i) {
                /* Copy the u plane */
                memcpy (dstu + dstustride * i, srcu + srcustride * i,
                    srcustride);
                /* Copy the v plane */
                memcpy (dstv + dstvstride * i, srcv + srcvstride * i,
                    srcvstride);
              }
              break;
            }
            case GST_MAKE_FOURCC ('N', 'V', '1', '2'):
            {
              guint8 *dst = dest;
              int component;
              dststride = lr.Pitch;
              for (component = 0; component < 2; component++) {
                const int compHeight =
                    gst_video_format_get_component_height (sink->format,
                    component, sink->height);
                guint8 *src =
                    source +
                    gst_video_format_get_component_offset (sink->format,
                    component, sink->width, sink->height);
                srcstride =
                    gst_video_format_get_row_stride (sink->format, component,
                    sink->width);
                for (i = 0; i < compHeight; i++) {
                  memcpy (dst + dststride * i, src + srcstride * i, srcstride);
                }
                dst += dststride * compHeight;
              }
              break;
            }
            default:
              g_assert_not_reached ();
          }
        } else if (gst_video_format_is_rgb (sink->format)) {
          dststride = lr.Pitch;
          srcstride = GST_BUFFER_SIZE (buffer) / sink->height;
          for (i = 0; i < sink->height; ++i)
            memcpy (dest + dststride * i, source + srcstride * i, srcstride);
        }
      }
      IDirect3DSurface9_UnlockRect (drawSurface);
    }
    IDirect3DDevice9_EndScene (sink->d3ddev);
  }
success:
  return GST_FLOW_OK;
error:
  return GST_FLOW_ERROR;
}

static GstFlowReturn
gst_d3dvideosink_show_frame (GstVideoSink * vsink, GstBuffer * buffer)
{
  GstFlowReturn ret;
  GstD3DVideoSink *sink = GST_D3DVIDEOSINK (vsink);

  if (!GST_D3DVIDEOSINK_D3D_DEVICE_TRYLOCK (sink))
    return GST_FLOW_OK;
  ret = gst_d3dvideosink_copy_frame (sink, buffer);
  GST_D3DVIDEOSINK_D3D_DEVICE_UNLOCK (sink);
  if (ret == GST_FLOW_OK) {
    gst_d3dvideosink_refresh (sink);
  }
  return ret;
}

/* Simply redraws the last item on our offscreen surface to the window */
static gboolean
gst_d3dvideosink_refresh (GstD3DVideoSink * sink)
{
  HRESULT hr;
  LPDIRECT3DSURFACE9 backBuffer;

  if (!GST_D3DVIDEOSINK_D3D_DEVICE_TRYLOCK (sink))
    return TRUE;

  if (!sink->d3ddev) {
    if (!shared.device_lost)
      GST_DEBUG ("No Direct3D device has been created");
    goto error;
  }

  if (!sink->d3d_offscreen_surface) {
    GST_DEBUG ("No Direct3D offscreen surface has been created");
    goto error;
  }

  if (sink->window_closed) {
    GST_DEBUG ("Window has been closed");
    goto error;
  }

  /* Check if the window size changed and resize our device.
   * Our backbuffer must have the size of the output window so scalling
   * happens in StretchRect using the bilinear filters and not in the window
   * itself.
   * Ideally we should track the window size with WM_SIZE messages, but those
   * are dropped when an external window is set */
  if (gst_d3dvideosink_resize (sink)) {
    /* If the resize succeed, update the newly created offscreen surface with
     * the last buffer */
    GstBuffer *buf;

    buf = gst_base_sink_get_last_buffer (GST_BASE_SINK (sink));
    if (buf) {
      gst_d3dvideosink_copy_frame (sink, buf);
      gst_buffer_unref (buf);
    }
  }

  /* Set the render target to our swap chain */
  if (FAILED (IDirect3DDevice9_GetBackBuffer (sink->d3ddev, 0, 0,
              D3DBACKBUFFER_TYPE_MONO, &backBuffer))) {
    GST_ERROR_OBJECT (sink, "failed to get back buffer");
    goto error;
  }
  IDirect3DDevice9_SetRenderTarget (sink->d3ddev, 0, backBuffer);
  IDirect3DSurface9_Release (backBuffer);

  /* Clear the target */
  IDirect3DDevice9_Clear (sink->d3ddev, 0, NULL, D3DCLEAR_TARGET,
      D3DCOLOR_XRGB (0, 0, 0), 1.0f, 0);

  if (SUCCEEDED (IDirect3DDevice9_BeginScene (sink->d3ddev))) {
    gst_d3dvideosink_stretch (sink, backBuffer);
    IDirect3DDevice9_EndScene (sink->d3ddev);
  }
  IDirect3DSurface9_Release (backBuffer);
  gst_d3dvideosink_flush_gpu (sink);
  /* Swap back and front buffers on video card and present to the user */
  if (FAILED (hr =
          IDirect3DDevice9_Present (sink->d3ddev, NULL, NULL, NULL, NULL))) {
    switch (hr) {
      case D3DERR_DEVICELOST:
      case D3DERR_DEVICENOTRESET:
        gst_d3dvideosink_notify_device_lost (sink);
        break;
      default:
        goto error;
    }
  }

  GST_D3DVIDEOSINK_D3D_DEVICE_UNLOCK (sink);
  return TRUE;

error:
  GST_D3DVIDEOSINK_D3D_DEVICE_UNLOCK (sink);
  return FALSE;
}

static gboolean
gst_d3dvideosink_update_all (GstD3DVideoSink * sink)
{
  GList *item;
  GstD3DVideoSink *s;

  GST_D3DVIDEOSINK_SHARED_D3D_LOCK;
  for (item = g_list_first (shared.element_list); item; item = item->next) {
    s = (GstD3DVideoSink *) item->data;
    gst_d3dvideosink_update (GST_BASE_SINK (s));
  }
  GST_D3DVIDEOSINK_SHARED_D3D_UNLOCK;
  return TRUE;
}

static gboolean
gst_d3dvideosink_refresh_all (GstD3DVideoSink * sink)
{
  GList *item;
  GstD3DVideoSink *s;

  GST_D3DVIDEOSINK_SHARED_D3D_LOCK;
  for (item = g_list_first (shared.element_list); item; item = item->next) {
    s = (GstD3DVideoSink *) item->data;
    gst_d3dvideosink_refresh (s);
  }
  GST_D3DVIDEOSINK_SHARED_D3D_UNLOCK;
  return TRUE;
}

static void
gst_d3dvideosink_stretch (GstD3DVideoSink * sink, LPDIRECT3DSURFACE9 backBuffer)
{
  if (sink->keep_aspect_ratio) {
    gint window_width, window_height;
    RECT r;
    GstVideoRectangle src, dst, result;

    gst_d3dvideosink_window_size (sink, &window_width, &window_height);

    src.x = 0;
    src.y = 0;
    src.w = GST_VIDEO_SINK_WIDTH (sink);
    src.h = GST_VIDEO_SINK_HEIGHT (sink);

    dst.x = 0;
    dst.y = 0;
    dst.w = window_width;
    dst.h = window_height;

    gst_video_sink_center_rect (src, dst, &result, TRUE);

    r.left = result.x;
    r.top = result.y;
    r.right = result.x + result.w;
    r.bottom = result.y + result.h;

    if (FAILED (IDirect3DDevice9_StretchRect (sink->d3ddev,
                sink->d3d_offscreen_surface, NULL, backBuffer, &r,
                sink->d3dfiltertype))) {
      GST_ERROR_OBJECT (sink, "StretchRect failed");
    }
  } else {
    IDirect3DDevice9_StretchRect (sink->d3ddev, sink->d3d_offscreen_surface,
        NULL, backBuffer, NULL, sink->d3dfiltertype);
  }
}

static void
gst_d3dvideosink_expose (GstXOverlay * overlay)
{
  GstBaseSink *sink = GST_BASE_SINK (overlay);
  gst_d3dvideosink_update (sink);
}

static void
gst_d3dvideosink_update (GstBaseSink * bsink)
{
  GstBuffer *last_buffer;

  last_buffer = gst_base_sink_get_last_buffer (bsink);
  if (last_buffer) {
    gst_d3dvideosink_show_frame (GST_VIDEO_SINK (bsink), last_buffer);
    gst_buffer_unref (last_buffer);
  }
}

/* TODO: How can we implement these? Figure that out... */
/*
static gboolean
gst_d3dvideosink_unlock (GstBaseSink * bsink)
{
  GstD3DVideoSink *sink = GST_D3DVIDEOSINK (bsink);

  return TRUE;
}

static gboolean
gst_d3dvideosink_unlock_stop (GstBaseSink * bsink)
{
  GstD3DVideoSink *sink = GST_D3DVIDEOSINK (bsink);

  return TRUE;
}
*/

static gboolean
gst_d3dvideosink_initialize_direct3d (GstD3DVideoSink * sink)
{
  DirectXAPI *api;
  GstD3DVideoSinkClass *klass;

  /* Let's hope this is never a problem (they have millions of d3d elements going at the same time) */
  if (shared.element_count >= G_MAXINT32) {
    GST_ERROR
        ("There are too many d3dvideosink elements. Creating more elements would put this element into an unknown state.");
    return FALSE;
  }

  GST_D3DVIDEOSINK_SHARED_D3D_LOCK;
  /* Add to our GList containing all of our elements. */
  /* GLists are doubly-linked lists and calling prepend() prevents it from having to traverse the entire list just to add one item. */
  shared.element_list = g_list_prepend (shared.element_list, sink);

  /* Increment our count of the number of elements we have */
  shared.element_count++;
  if (shared.element_count > 1)
    goto success;

  /* We want to initialize direct3d only for the first element that's using it. */
  /* We'll destroy this once all elements using direct3d have been finalized. */
  /* See gst_d3dvideosink_release_direct3d() for details. */

  if (!sink) {
    GST_WARNING ("Missing gobject instance.");
    return FALSE;
  }

  klass = GST_D3DVIDEOSINK_GET_CLASS (sink);
  if (!klass) {
    GST_WARNING ("Unable to retrieve gobject class");
    goto error;
  }

  api = klass->directx_api;
  if (!api) {
    GST_WARNING ("Missing DirectX api");
    goto error;
  }

  shared.d3d =
      (LPDIRECT3D9) DX9_D3D_COMPONENT_CALL_FUNC (DIRECTX_D3D (api),
      Direct3DCreate9, D3D_SDK_VERSION);
  if (!shared.d3d) {
    GST_ERROR ("Unable to create Direct3D interface");
    goto error;
  }

  /* We create a window that's hidden, so we can control the 
     device's from a single thread */

  GST_DEBUG ("Creating hidden window for Direct3D");
  if (!gst_d3dvideosink_create_shared_hidden_window (sink))
    goto error;

success:
  GST_D3DVIDEOSINK_SHARED_D3D_UNLOCK;
  return TRUE;
error:
  GST_D3DVIDEOSINK_SHARED_D3D_UNLOCK;
  return FALSE;
}

static gboolean
gst_d3dvideosink_initialize_d3d_device (GstD3DVideoSink * sink)
{
  HRESULT hr;
  DWORD d3dcreate;
  D3DCAPS9 d3dcaps;
  HWND hwnd = sink->window_handle;
  D3DFORMAT d3dformat = sink->d3dformat;
  D3DFORMAT d3dfourcc;
  D3DDISPLAYMODE d3ddm;
  D3DTEXTUREFILTERTYPE d3dfiltertype;
  gint width, height;



  /* Get the current size of the window */
  gst_d3dvideosink_window_size (sink, &width, &height);

  if (!shared.d3d) {
    GST_WARNING ("Direct3D object has not been initialized");
    goto error;
  }
  if (FAILED (IDirect3D9_GetAdapterDisplayMode (shared.d3d, D3DADAPTER_DEFAULT,
              &d3ddm))) {
    GST_WARNING ("Unable to request adapter display mode");
    goto error;
  }

  if (FAILED (IDirect3D9_GetDeviceCaps (shared.d3d, D3DADAPTER_DEFAULT,
              D3DDEVTYPE_HAL, &d3dcaps))) {
    GST_WARNING ("Unable to request device caps");
    goto error;
  }

  /* Ask DirectX to please not clobber the FPU state when making DirectX API calls. */
  /* This can cause libraries such as cairo to misbehave in certain scenarios. */
  d3dcreate = 0 | D3DCREATE_FPU_PRESERVE | D3DCREATE_MULTITHREADED;

  /* Determine vertex processing capabilities. Some cards have issues using software vertex processing. */
  /* Courtesy http://www.chadvernon.com/blog/resources/directx9/improved-direct3d-initialization/ */
  if ((d3dcaps.DevCaps & D3DDEVCAPS_HWTRANSFORMANDLIGHT) ==
      D3DDEVCAPS_HWTRANSFORMANDLIGHT) {
    d3dcreate |= D3DCREATE_HARDWARE_VERTEXPROCESSING;
    /* if ((d3dcaps.DevCaps & D3DDEVCAPS_PUREDEVICE) == D3DDEVCAPS_PUREDEVICE) */
    /*  d3dcreate |= D3DCREATE_PUREDEVICE; */
  } else {
    d3dcreate |= D3DCREATE_SOFTWARE_VERTEXPROCESSING;
  }

  /* Check the filter type. */
  if ((d3dcaps.StretchRectFilterCaps & D3DPTFILTERCAPS_MINFLINEAR) ==
      D3DPTFILTERCAPS_MINFLINEAR
      || (d3dcaps.StretchRectFilterCaps & D3DPTFILTERCAPS_MAGFLINEAR) ==
      D3DPTFILTERCAPS_MAGFLINEAR) {
    d3dfiltertype = D3DTEXF_LINEAR;
  } else {
    d3dfiltertype = D3DTEXF_NONE;
  }

  if (gst_video_format_is_yuv (sink->format)) {
    switch (gst_video_format_to_fourcc (sink->format)) {
      case GST_MAKE_FOURCC ('Y', 'U', 'Y', '2'):
        d3dformat = D3DFMT_X8R8G8B8;
        d3dfourcc = (D3DFORMAT) MAKEFOURCC ('Y', 'U', 'Y', '2');
        break;
        //case GST_MAKE_FOURCC ('Y', 'U', 'V', 'Y'):
        //  d3dformat = D3DFMT_X8R8G8B8;
        //  d3dfourcc = (D3DFORMAT)MAKEFOURCC('Y', 'U', 'V', 'Y');
        //  break;
      case GST_MAKE_FOURCC ('U', 'Y', 'V', 'Y'):
        d3dformat = D3DFMT_X8R8G8B8;
        d3dfourcc = (D3DFORMAT) MAKEFOURCC ('U', 'Y', 'V', 'Y');
        break;
      case GST_MAKE_FOURCC ('Y', 'V', '1', '2'):
      case GST_MAKE_FOURCC ('I', '4', '2', '0'):
        d3dformat = D3DFMT_X8R8G8B8;
        d3dfourcc = (D3DFORMAT) MAKEFOURCC ('Y', 'V', '1', '2');
        break;
      case GST_MAKE_FOURCC ('N', 'V', '1', '2'):
        d3dformat = D3DFMT_X8R8G8B8;
        d3dfourcc = (D3DFORMAT) MAKEFOURCC ('N', 'V', '1', '2');
        break;
      default:
        g_assert_not_reached ();
        goto error;
    }
  } else if (gst_video_format_is_rgb (sink->format)) {
    d3dformat = D3DFMT_X8R8G8B8;
    d3dfourcc = D3DFMT_X8R8G8B8;
  } else {
    g_assert_not_reached ();
    goto error;
  }

  GST_DEBUG ("Determined Direct3D format: %" GST_FOURCC_FORMAT,
      GST_FOURCC_ARGS (d3dfourcc));


  GST_DEBUG ("Direct3D back buffer size: %dx%d", GST_VIDEO_SINK_WIDTH (sink),
      GST_VIDEO_SINK_HEIGHT (sink));

  sink->d3dformat = d3dformat;
  sink->d3dfourcc = d3dfourcc;


  ZeroMemory (&sink->d3dpp, sizeof (sink->d3dpp));
  sink->d3dpp.Flags = D3DPRESENTFLAG_VIDEO | D3DPRESENTFLAG_LOCKABLE_BACKBUFFER;
  sink->d3dpp.Windowed = TRUE;
  sink->d3dpp.hDeviceWindow = hwnd;
  sink->d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
  sink->d3dpp.BackBufferCount = 1;
  //sink->d3dpp.BackBufferFormat = d3dformat;
  sink->d3dpp.BackBufferWidth = GST_VIDEO_SINK_WIDTH(sink);
  sink->d3dpp.BackBufferHeight = GST_VIDEO_SINK_HEIGHT(sink);
  sink->d3dpp.MultiSampleType = D3DMULTISAMPLE_NONE;
  sink->d3dpp.PresentationInterval = D3DPRESENT_INTERVAL_ONE;

  GST_DEBUG ("Creating Direct3D device for window %p", hwnd);

  sink->d3ddev = NULL;

  if (FAILED (hr = IDirect3D9_CreateDevice (shared.d3d,
              D3DADAPTER_DEFAULT,
              D3DDEVTYPE_HAL, hwnd, d3dcreate, &sink->d3dpp, &sink->d3ddev))) {
    GST_WARNING ("Unable to create Direct3D device. Result: %ld (0x%lx)", hr,
        hr);
    goto error;
  }

  if (FAILED (IDirect3DDevice9_CreateOffscreenPlainSurface (sink->d3ddev,
              sink->width, sink->height, d3dfourcc, D3DPOOL_DEFAULT,
              &sink->d3d_offscreen_surface, NULL))) {
    goto error;
  }


  /* Determine texture filtering support. If it's supported for this format, use the filter 
     type determined when we created the dev and checked the dev caps. 
   */
  if (FAILED (IDirect3D9_CheckDeviceFormat (shared.d3d, D3DADAPTER_DEFAULT,
              D3DDEVTYPE_HAL, d3dformat, D3DUSAGE_QUERY_FILTER,
              D3DRTYPE_TEXTURE, d3dformat))) {
    d3dfiltertype = D3DTEXF_NONE;
  }

  GST_DEBUG ("Direct3D stretch rect texture filter: %d", d3dfiltertype);

  sink->d3dfiltertype = d3dfiltertype;

/*success:*/
  return TRUE;
error:
  return FALSE;
}

static gboolean
gst_d3dvideosink_notify_device_init (GstD3DVideoSink * sink)
{
  if (sink->window_handle) {
    SendMessage (shared.hidden_window_handle, WM_DIRECTX_D3D_INIT_DEVICE, 0,
        (LPARAM) sink);
  }
  return TRUE;
}

static gboolean
gst_d3dvideosink_notify_device_reinit (GstD3DVideoSink * sink)
{
  if (sink->window_handle) {
    SendMessage (shared.hidden_window_handle, WM_DIRECTX_D3D_DEVICELOST, 0,
        (LPARAM) sink);
  }
  return TRUE;
}

static gboolean
gst_d3dvideosink_notify_device_lost (GstD3DVideoSink * sink)
{
  /* Send notification asynchronously */
  PostMessage (shared.hidden_window_handle, WM_DIRECTX_D3D_INIT_DEVICELOST, 0,
      (LPARAM) sink);

  GST_DEBUG ("Successfully sent notification of device lost event for sink %p",
      sink);
  return TRUE;
}

static gboolean
gst_d3dvideosink_notify_device_reset (GstD3DVideoSink * sink)
{
  {
    /* Send notification synchronously -- let's ensure the timer's been killed before returning */
    SendMessage (shared.hidden_window_handle, WM_DIRECTX_D3D_END_DEVICELOST, 0,
        (LPARAM) sink);
  }
  GST_DEBUG ("Successfully sent notification of device reset event for sink %p",
      sink);
  return TRUE;
}

static gboolean
gst_d3dvideosink_notify_device_resize (GstD3DVideoSink * sink)
{
  /* Send notification synchronously */
  SendMessage (shared.hidden_window_handle, WM_DIRECTX_D3D_RESIZE, 0,
      (LPARAM) sink);
  return TRUE;
}

static gboolean
gst_d3dvideosink_resize_device (GstD3DVideoSink *sink)
{
  /* Release all resources allocated in the current device's pool */
  GST_LOG_OBJECT (sink, "Reset device and recreate offscreen surface");
  while (IDirect3DSurface9_Release (sink->d3d_offscreen_surface) != 0);
  if (FAILED (IDirect3DDevice9_Reset (sink->d3ddev, &sink->d3dpp))) {
    return gst_d3dvideosink_notify_device_lost (sink);
  }

  if (FAILED (IDirect3DDevice9_CreateOffscreenPlainSurface (sink->d3ddev,
              sink->width, sink->height, sink->d3dfourcc, D3DPOOL_DEFAULT,
              &sink->d3d_offscreen_surface, NULL))) {
    return gst_d3dvideosink_notify_device_lost (sink);
  }
  return TRUE;
}

static gboolean
gst_d3dvideosink_device_lost (GstD3DVideoSink * sink)
{
  /* Must be called from hidden window's message loop! */

  if (shared.device_lost)
    GST_DEBUG ("Direct3D device lost");

  GST_DEBUG_OBJECT (sink, ". Resetting the device.");

  if (g_thread_self () != shared.hidden_window_thread) {
    GST_ERROR
        ("Direct3D device can only be reset by the thread that created it.");
    goto error;
  }

  if (!shared.d3d) {
    GST_ERROR ("Direct3D device has not been initialized");
    goto error;
  }

  /* This is technically a bit different from the normal. We don't call reset(), instead */
  /* we recreate everything from scratch. */

  /* Release the device */
  if (!gst_d3dvideosink_release_d3d_device (sink))
    goto error;

  /* Recreate device */
  if (!gst_d3dvideosink_initialize_d3d_device (sink))
    goto error;

  /* Let the hidden window know that it's okay to kill the timer */
  gst_d3dvideosink_notify_device_reset (sink);

  GST_DEBUG ("Direct3D device has successfully been reset.");
  return TRUE;
error:
  GST_DEBUG ("Unable to successfully reset the Direct3D device.");
  return FALSE;
}

static gboolean
gst_d3dvideosink_release_d3d_device (GstD3DVideoSink * sink)
{
  if (sink->d3d_offscreen_surface) {
    int ref_count;
    ref_count = IDirect3DSurface9_Release (sink->d3d_offscreen_surface);
    sink->d3d_offscreen_surface = NULL;
    GST_DEBUG_OBJECT (sink,
        "Direct3D offscreen surface released. Reference count: %d", ref_count);
  }
  if (sink->d3ddev) {
    int ref_count;
    ref_count = IDirect3DDevice9_Release (sink->d3ddev);
    sink->d3ddev = NULL;
    GST_DEBUG_OBJECT (sink, "Direct3D device released. Reference count: %d",
        ref_count);
  }
  return TRUE;
}

static gboolean
gst_d3dvideosink_release_direct3d (GstD3DVideoSink * sink)
{
  GST_DEBUG ("Cleaning all Direct3D objects");
  GST_D3DVIDEOSINK_SHARED_D3D_LOCK;
  /* Be absolutely sure that we've released this sink's hook (if any). */
  gst_d3dvideosink_unhook_window_for_renderer (sink);

  /* Remove item from the list */
  shared.element_list = g_list_remove (shared.element_list, sink);

  /* Decrement our count of the number of elements we have */
  shared.element_count--;
  if (shared.element_count < 0)
    shared.element_count = 0;
  if (shared.element_count > 0)
    goto success;

  GST_D3DVIDEOSINK_D3D_DEVICE_LOCK (sink);
  gst_d3dvideosink_release_d3d_device (sink);
  GST_D3DVIDEOSINK_D3D_DEVICE_UNLOCK (sink);

  if (shared.d3d) {
    int ref_count;
    ref_count = IDirect3D9_Release (shared.d3d);
    shared.d3d = NULL;
    GST_DEBUG ("Direct3D object released. Reference count: %d", ref_count);
  }

  GST_DEBUG ("Closing hidden Direct3D window");
  gst_d3dvideosink_close_shared_hidden_window (sink);

success:
  GST_D3DVIDEOSINK_SHARED_D3D_UNLOCK;
  return TRUE;
}

static gboolean
gst_d3dvideosink_window_size (GstD3DVideoSink * sink, gint * width,
    gint * height)
{
  if (!sink || !sink->window_handle) {
    if (width && height) {
      *width = 0;
      *height = 0;
    }
    return FALSE;
  }

  {
    RECT sz;
    GetClientRect (sink->window_handle, &sz);

    *width = MAX (1, ABS (sz.right - sz.left));
    *height = MAX (1, ABS (sz.bottom - sz.top));
  }
  return TRUE;
}

static void
gst_d3dvideosink_navigation_send_event (GstNavigation * navigation,
    GstStructure * structure)
{
  GstD3DVideoSink *sink = GST_D3DVIDEOSINK (navigation);
  gint window_width;
  gint window_height;
  GstEvent *e;
  GstVideoRectangle src, dst, result;
  double x, y, old_x, old_y, xscale = 1.0, yscale = 1.0;
  GstPad *pad = NULL;

  gst_d3dvideosink_window_size (sink, &window_width, &window_height);

  src.w = GST_VIDEO_SINK_WIDTH (sink);
  src.h = GST_VIDEO_SINK_HEIGHT (sink);
  dst.w = window_width;
  dst.h = window_height;

  e = gst_event_new_navigation (structure);

  if (sink->keep_aspect_ratio) {
    gst_video_sink_center_rect (src, dst, &result, TRUE);
  } else {
    result.x = 0;
    result.y = 0;
    result.w = dst.w;
    result.h = dst.h;
  }

  /* We calculate scaling using the original video frames geometry to include
     pixel aspect ratio scaling. */
  xscale = (gdouble) sink->width / result.w;
  yscale = (gdouble) sink->height / result.h;

  /* Converting pointer coordinates to the non scaled geometry */
  if (gst_structure_get_double (structure, "pointer_x", &old_x)) {
    x = old_x;
    x = MIN (x, result.x + result.w);
    x = MAX (x - result.x, 0);
    gst_structure_set (structure, "pointer_x", G_TYPE_DOUBLE,
        (gdouble) x * xscale, NULL);
    GST_DEBUG_OBJECT (sink,
        "translated navigation event x coordinate from %f to %f", old_x, x);
  }
  if (gst_structure_get_double (structure, "pointer_y", &old_y)) {
    y = old_y;
    y = MIN (y, result.y + result.h);
    y = MAX (y - result.y, 0);
    gst_structure_set (structure, "pointer_y", G_TYPE_DOUBLE,
        (gdouble) y * yscale, NULL);
    GST_DEBUG_OBJECT (sink,
        "translated navigation event x coordinate from %f to %f", old_y, y);
  }

  pad = gst_pad_get_peer (GST_VIDEO_SINK_PAD (sink));

  if (GST_IS_PAD (pad) && GST_IS_EVENT (e)) {
    gst_pad_send_event (pad, e);
    gst_object_unref (pad);
  }
}

static gboolean
gst_d3dvideosink_direct3d_supported (GstD3DVideoSink * sink)
{
  GstD3DVideoSinkClass *klass = GST_D3DVIDEOSINK_GET_CLASS (sink);

  return (klass != NULL && klass->is_directx_supported);
}

static void
gst_d3dvideosink_log_debug (const gchar * file, const gchar * function,
    gint line, const gchar * format, va_list args)
{
  if (G_UNLIKELY (GST_LEVEL_DEBUG <= __gst_debug_min))
    gst_debug_log_valist (GST_CAT_DEFAULT, GST_LEVEL_DEBUG, file, function,
        line, NULL, format, args);
}

static void
gst_d3dvideosink_log_warning (const gchar * file, const gchar * function,
    gint line, const gchar * format, va_list args)
{
  if (G_UNLIKELY (GST_LEVEL_WARNING <= __gst_debug_min))
    gst_debug_log_valist (GST_CAT_DEFAULT, GST_LEVEL_WARNING, file, function,
        line, NULL, format, args);
}

static void
gst_d3dvideosink_log_error (const gchar * file, const gchar * function,
    gint line, const gchar * format, va_list args)
{
  if (G_UNLIKELY (GST_LEVEL_ERROR <= __gst_debug_min))
    gst_debug_log_valist (GST_CAT_DEFAULT, GST_LEVEL_ERROR, file, function,
        line, NULL, format, args);
}

/* Plugin entry point */
static gboolean
plugin_init (GstPlugin * plugin)
{
  /* PRIMARY: this is the best videosink to use on windows */
  if (!gst_element_register (plugin, "d3dvideosink",
          GST_RANK_PRIMARY, GST_TYPE_D3DVIDEOSINK))
    return FALSE;

  return TRUE;
}

GST_PLUGIN_DEFINE2 (GST_VERSION_MAJOR,
    GST_VERSION_MINOR,
    d3dsinkwrapper,
    "Direct3D sink wrapper plugin",
    plugin_init, VERSION, "LGPL", GST_PACKAGE_NAME, GST_PACKAGE_ORIGIN)
