/* GDK HData Output Stream - a stream backed by a global memory buffer
 * 
 * Copyright (C) 2018 Руслан Ижбулатов
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General
 * Public License along with this library; if not, see <http://www.gnu.org/licenses/>.
 *
 * Author: Руслан Ижбулатов <lrn1986@gmail.com>
 */

#include "config.h"

#include <windows.h>

#include "gdkprivate-win32.h"
#include "gdkhdataoutputstream-win32.h"

#include "gdkclipboard-win32.h"
#include "gdkdisplay-win32.h"
#include <glib/gi18n-lib.h>
#include "gdkwin32display.h"
#include "gdkwin32surface.h"


typedef struct _GdkWin32HDataOutputStreamPrivate  GdkWin32HDataOutputStreamPrivate;

struct _GdkWin32HDataOutputStreamPrivate
{
  HANDLE                     handle;
  GdkWin32Clipdrop          *clipdrop;
  guchar                    *data;
  gsize                      data_allocated_size;
  gsize                      data_length;
  GdkWin32ContentFormatPair  pair;
  guint                      handle_is_buffer : 1;
  guint                      closed : 1;
};

G_DEFINE_TYPE_WITH_PRIVATE (GdkWin32HDataOutputStream, gdk_win32_hdata_output_stream, G_TYPE_OUTPUT_STREAM);

static gssize
write_stream (GdkWin32HDataOutputStream         *stream,
              GdkWin32HDataOutputStreamPrivate  *priv,
              const void                        *buffer,
              gsize                              count,
              GError                           **error)
{
  gsize spillover = (priv->data_length + count) - priv->data_allocated_size;
  gsize to_copy = count;

  if (priv->closed)
    {
      g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED,
                           _("writing a closed stream"));
      return -1;
    }

  if (spillover > 0 && !priv->handle_is_buffer)
    {
      guchar *new_data;
      HANDLE new_handle = GlobalReAlloc (priv->handle, priv->data_allocated_size + spillover, 0);

      if (new_handle != NULL)
        {
          new_data = g_try_realloc (priv->data, priv->data_allocated_size + spillover);

          if (new_data != NULL)
            {
              priv->handle = new_handle;
              priv->data = new_data;
              priv->data_allocated_size += spillover;
            }
          else
            {
              g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED,
                                   _("g_try_realloc () failed"));
              return -1;
            }
        }
      else
        {
          DWORD error_code = GetLastError ();
          g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
                       "%s%lu", _("GlobalReAlloc() failed: "), error_code);
          return -1;
        }
    }

  if (priv->handle_is_buffer)
    {
      to_copy = MIN (count, priv->data_allocated_size - priv->data_length);

      if (to_copy == 0)
        {
          g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED,
                               _("Ran out of buffer space (buffer size is fixed)"));
          return -1;
        }

      memcpy (&((guchar *) priv->handle)[priv->data_length], buffer, to_copy);
    }
  else
    memcpy (&priv->data[priv->data_length], buffer, to_copy);

  priv->data_length += to_copy;

  return to_copy;
}

static gssize
gdk_win32_hdata_output_stream_write (GOutputStream  *output_stream,
                                     const void     *buffer,
                                     gsize           count,
                                     GCancellable   *cancellable,
                                     GError        **error)
{
  GdkWin32HDataOutputStream *stream = GDK_WIN32_HDATA_OUTPUT_STREAM (output_stream);
  GdkWin32HDataOutputStreamPrivate *priv = gdk_win32_hdata_output_stream_get_instance_private (stream);
  gssize result = write_stream (stream, priv, buffer, count, error);

  if (result != -1)
    GDK_NOTE(SELECTION, g_printerr ("CLIPBOARD: wrote %zd bytes, %" G_GSIZE_FORMAT " total now\n",
                                    result, priv->data_length));

  return result;
}

static void
gdk_win32_hdata_output_stream_write_async (GOutputStream        *output_stream,
                                           const void           *buffer,
                                           gsize                 count,
                                           int                   io_priority,
                                           GCancellable         *cancellable,
                                           GAsyncReadyCallback   callback,
                                           gpointer              user_data)
{
  GdkWin32HDataOutputStream *stream = GDK_WIN32_HDATA_OUTPUT_STREAM (output_stream);
  GdkWin32HDataOutputStreamPrivate *priv = gdk_win32_hdata_output_stream_get_instance_private (stream);
  GTask *task;
  gssize result;
  GError *error = NULL;

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

  result = write_stream (stream, priv, buffer, count, &error);

  if (result != -1)
    {
      GDK_NOTE (SELECTION, g_printerr ("CLIPBOARD async wrote %zd bytes, %" G_GSIZE_FORMAT " total now\n",
                                       result, priv->data_length));
      g_task_return_int (task, result);
    }
  else
    g_task_return_error (task, error);

  g_object_unref (task);

  return;
}

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

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

static gboolean
gdk_win32_hdata_output_stream_close (GOutputStream  *output_stream,
                                     GCancellable   *cancellable,
                                     GError        **error)
{
  GdkWin32HDataOutputStream *stream = GDK_WIN32_HDATA_OUTPUT_STREAM (output_stream);
  GdkWin32HDataOutputStreamPrivate *priv = gdk_win32_hdata_output_stream_get_instance_private (stream);
  guchar *hdata;

  if (priv->closed)
    return TRUE;

  if (priv->pair.transmute)
    {
      guchar *transmuted_data = NULL;
      gsize   transmuted_data_length;

      if (priv->handle_is_buffer)
        {
          g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED,
                               _("Can’t transmute a single handle"));
          return FALSE;
        }

      if (!gdk_win32_clipdrop_transmute_contentformat (priv->clipdrop,
                                                       priv->pair.contentformat,
                                                       priv->pair.w32format,
                                                       priv->data,
                                                       priv->data_length,
                                                      &transmuted_data,
                                                      &transmuted_data_length))
        {
          g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
                       _("Failed to transmute %zu bytes of data from %s to %u"),
                       priv->data_length,
                       priv->pair.contentformat,
                       priv->pair.w32format);
          return FALSE;
        }
      else
        {
          HANDLE new_handle;

          new_handle = GlobalReAlloc (priv->handle, transmuted_data_length, 0);

          if (new_handle == NULL)
            {
              DWORD error_code = GetLastError ();
              g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
                           "%s%lu", _("GlobalReAlloc() failed: "), error_code);
              return FALSE;
            }

          priv->handle = new_handle;
          priv->data_length = transmuted_data_length;
          g_clear_pointer (&priv->data, g_free);
          priv->data = transmuted_data;
        }
    }

  if (!priv->handle_is_buffer)
    {
      hdata = GlobalLock (priv->handle);

      if (hdata == NULL)
        {
          DWORD error_code = GetLastError ();
          g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
                       "%s%lu", _("GlobalLock() failed: "), error_code);
          return FALSE;
        }

      memcpy (hdata, priv->data, priv->data_length);
      GlobalUnlock (priv->handle);
      g_clear_pointer (&priv->data, g_free);
    }

  priv->closed = 1;

  return TRUE;
}

static void
gdk_win32_hdata_output_stream_close_async (GOutputStream       *stream,
                                           int                  io_priority,
                                           GCancellable        *cancellable,
                                           GAsyncReadyCallback  callback,
                                           gpointer             user_data)
{
  GTask *task;
  GError *error = NULL;

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

  if (gdk_win32_hdata_output_stream_close (stream, NULL, &error))
    g_task_return_boolean (task, TRUE);
  else
    g_task_return_error (task, error);

  g_object_unref (task);
}

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

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

static void
gdk_win32_hdata_output_stream_finalize (GObject *object)
{
  GdkWin32HDataOutputStream *stream = GDK_WIN32_HDATA_OUTPUT_STREAM (object);
  GdkWin32HDataOutputStreamPrivate *priv = gdk_win32_hdata_output_stream_get_instance_private (stream);

  g_clear_pointer (&priv->data, g_free);

  /* We deliberately don't close the memory object,
   * as it will be used elsewhere (it's a shame that
   * MS global memory handles are not refcounted and
   * not duplicateable).
   * Except when the stream isn't closed, which means
   * that the caller never bothered to get the handle.
   */
  if (!priv->closed && priv->handle)
    {
      if (_gdk_win32_format_uses_hdata (priv->pair.w32format))
        GlobalFree (priv->handle);
      else
        CloseHandle (priv->handle);
    }

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

static void
gdk_win32_hdata_output_stream_class_init (GdkWin32HDataOutputStreamClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);
  GOutputStreamClass *output_stream_class = G_OUTPUT_STREAM_CLASS (klass);

  object_class->finalize = gdk_win32_hdata_output_stream_finalize;

  output_stream_class->write_fn = gdk_win32_hdata_output_stream_write;
  output_stream_class->close_fn = gdk_win32_hdata_output_stream_close;

  output_stream_class->write_async = gdk_win32_hdata_output_stream_write_async;
  output_stream_class->write_finish = gdk_win32_hdata_output_stream_write_finish;
  output_stream_class->close_async = gdk_win32_hdata_output_stream_close_async;
  output_stream_class->close_finish = gdk_win32_hdata_output_stream_close_finish;
}

static void
gdk_win32_hdata_output_stream_init (GdkWin32HDataOutputStream *stream)
{
}

GOutputStream *
gdk_win32_hdata_output_stream_new (GdkWin32Clipdrop           *clipdrop,
                                   GdkWin32ContentFormatPair  *pair,
                                   GError                    **error)
{
  GdkWin32HDataOutputStream *stream;
  GdkWin32HDataOutputStreamPrivate *priv;
  HANDLE handle;
  gboolean hmem;

  hmem = _gdk_win32_format_uses_hdata (pair->w32format);

  if (hmem)
    {
      handle = GlobalAlloc (GMEM_MOVEABLE | GMEM_ZEROINIT, 0);

      if (handle == NULL)
        {
          DWORD error_code = GetLastError ();
          g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
                       "%s%lu", _("GlobalAlloc() failed: "), error_code);

          return NULL;
        }
    }
  else
    handle = NULL; /* silence compiler */

  stream = g_object_new (GDK_TYPE_WIN32_HDATA_OUTPUT_STREAM, NULL);
  priv = gdk_win32_hdata_output_stream_get_instance_private (stream);
  priv->clipdrop = clipdrop;
  priv->pair = *pair;

  if (hmem)
    {
      priv->handle = handle;
    }
  else
    {
      priv->data_allocated_size = sizeof (priv->handle);
      priv->handle_is_buffer = 1;
    }

  return G_OUTPUT_STREAM (stream);
}

HANDLE
gdk_win32_hdata_output_stream_get_handle (GdkWin32HDataOutputStream *stream,
                                          gboolean                  *is_hdata)
{
  GdkWin32HDataOutputStreamPrivate *priv;
  priv = gdk_win32_hdata_output_stream_get_instance_private (stream);

  if (!priv->closed)
    return NULL;

  if (is_hdata)
    *is_hdata = _gdk_win32_format_uses_hdata (priv->pair.w32format);

  return priv->handle;
}
