/* GDK - The GIMP Drawing Kit
 *
 * Copyright (C) 2017,2020 Benjamin Otte <otte@gnome.org>
 *
 * 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/>.
 */

#include "config.h"

#include "gdkcontentproviderprivate.h"

#include <gobject/gvaluecollector.h>

#include "gdkcontentformats.h"
#include "gdkcontentserializer.h"
#include "gdkintl.h"
#include "gdkcontentproviderimpl.h"

#define GDK_TYPE_CONTENT_PROVIDER_VALUE            (gdk_content_provider_value_get_type ())
#define GDK_CONTENT_PROVIDER_VALUE(obj)            (G_TYPE_CHECK_INSTANCE_CAST ((obj), GDK_TYPE_CONTENT_PROVIDER_VALUE, GdkContentProviderValue))
#define GDK_IS_CONTENT_PROVIDER_VALUE(obj)         (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GDK_TYPE_CONTENT_PROVIDER_VALUE))
#define GDK_CONTENT_PROVIDER_VALUE_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST ((klass), GDK_TYPE_CONTENT_PROVIDER_VALUE, GdkContentProviderValueClass))
#define GDK_IS_CONTENT_PROVIDER_VALUE_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GDK_TYPE_CONTENT_PROVIDER_VALUE))
#define GDK_CONTENT_PROVIDER_VALUE_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS ((obj), GDK_TYPE_CONTENT_PROVIDER_VALUE, GdkContentProviderValueClass))

typedef struct _GdkContentProviderValue GdkContentProviderValue;
typedef struct _GdkContentProviderValueClass GdkContentProviderValueClass;

struct _GdkContentProviderValue
{
  GdkContentProvider parent;

  GValue value;
};

struct _GdkContentProviderValueClass
{
  GdkContentProviderClass parent_class;
};

GType gdk_content_provider_value_get_type (void) G_GNUC_CONST;

G_DEFINE_TYPE (GdkContentProviderValue, gdk_content_provider_value, GDK_TYPE_CONTENT_PROVIDER)

static void
gdk_content_provider_value_finalize (GObject *object)
{
  GdkContentProviderValue *content = GDK_CONTENT_PROVIDER_VALUE (object);

  g_value_unset (&content->value);

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

static GdkContentFormats *
gdk_content_provider_value_ref_formats (GdkContentProvider *provider)
{
  GdkContentProviderValue *content = GDK_CONTENT_PROVIDER_VALUE (provider);

  return gdk_content_formats_new_for_gtype (G_VALUE_TYPE (&content->value));
}

static gboolean
gdk_content_provider_value_get_value (GdkContentProvider  *provider,
                                      GValue              *value,
                                      GError             **error)
{
  GdkContentProviderValue *content = GDK_CONTENT_PROVIDER_VALUE (provider);

  if (G_VALUE_HOLDS (value, G_VALUE_TYPE (&content->value)))
    {
      g_value_copy (&content->value, value);
      return TRUE;
    }

  return GDK_CONTENT_PROVIDER_CLASS (gdk_content_provider_value_parent_class)->get_value (provider, value, error);
}

static void
gdk_content_provider_value_class_init (GdkContentProviderValueClass *class)
{
  GObjectClass *object_class = G_OBJECT_CLASS (class);
  GdkContentProviderClass *provider_class = GDK_CONTENT_PROVIDER_CLASS (class);

  object_class->finalize = gdk_content_provider_value_finalize;

  provider_class->ref_formats = gdk_content_provider_value_ref_formats;
  provider_class->get_value = gdk_content_provider_value_get_value;
}

static void
gdk_content_provider_value_init (GdkContentProviderValue *content)
{
}

/**
 * gdk_content_provider_new_for_value:
 * @value: a #GValue
 *
 * Create a content provider that provides the given @value.
 *
 * Returns: a new #GdkContentProvider
 **/
GdkContentProvider *
gdk_content_provider_new_for_value (const GValue *value)
{
  GdkContentProviderValue *content;

  g_return_val_if_fail (G_IS_VALUE (value), NULL);

  content = g_object_new (GDK_TYPE_CONTENT_PROVIDER_VALUE, NULL);
  g_value_init (&content->value, G_VALUE_TYPE (value));
  g_value_copy (value, &content->value);
  
  return GDK_CONTENT_PROVIDER (content);
}

/**
 * gdk_content_provider_new_typed:
 * @type: Type of value to follow
 * @...: value
 *
 * Create a content provider that provides the value of the given
 * @type.
 *
 * The value is provided using G_VALUE_COLLECT(), so the same rules
 * apply as when calling g_object_new() or g_object_set().
 *
 * Returns: a new #GdkContentProvider
 **/
GdkContentProvider *
gdk_content_provider_new_typed (GType type,
                                ...)
{
  GdkContentProviderValue *content;
  va_list args;
  char *error;

  content = g_object_new (GDK_TYPE_CONTENT_PROVIDER_VALUE, NULL);

  va_start (args, type);
  G_VALUE_COLLECT_INIT (&content->value, type, args, 0, &error);
  if (error)
    {
      g_warning ("%s: %s", G_STRLOC, error);
      g_free (error);
      /* we purposely leak the value here, it might not be
       * in a sane state if an error condition occurred
       */
    }
  va_end (args);

  return GDK_CONTENT_PROVIDER (content);
}

#define GDK_TYPE_CONTENT_PROVIDER_UNION            (gdk_content_provider_union_get_type ())
#define GDK_CONTENT_PROVIDER_UNION(obj)            (G_TYPE_CHECK_INSTANCE_CAST ((obj), GDK_TYPE_CONTENT_PROVIDER_UNION, GdkContentProviderUnion))
#define GDK_IS_CONTENT_PROVIDER_UNION(obj)         (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GDK_TYPE_CONTENT_PROVIDER_UNION))
#define GDK_CONTENT_PROVIDER_UNION_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST ((klass), GDK_TYPE_CONTENT_PROVIDER_UNION, GdkContentProviderUnionClass))
#define GDK_IS_CONTENT_PROVIDER_UNION_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GDK_TYPE_CONTENT_PROVIDER_UNION))
#define GDK_CONTENT_PROVIDER_UNION_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS ((obj), GDK_TYPE_CONTENT_PROVIDER_UNION, GdkContentProviderUnionClass))

typedef struct _GdkContentProviderUnion GdkContentProviderUnion;
typedef struct _GdkContentProviderUnionClass GdkContentProviderUnionClass;

struct _GdkContentProviderUnion
{
  GdkContentProvider parent;

  GdkContentProvider **providers;
  gsize n_providers;
};

struct _GdkContentProviderUnionClass
{
  GdkContentProviderClass parent_class;
};

GType gdk_content_provider_union_get_type (void) G_GNUC_CONST;

G_DEFINE_TYPE (GdkContentProviderUnion, gdk_content_provider_union, GDK_TYPE_CONTENT_PROVIDER)

static void
gdk_content_provider_union_attach_clipboard (GdkContentProvider *provider,
                                             GdkClipboard       *clipboard)
{
  GdkContentProviderUnion *self = GDK_CONTENT_PROVIDER_UNION (provider);
  gsize i;

  for (i = 0; i < self->n_providers; i++)
    gdk_content_provider_attach_clipboard (self->providers[i], clipboard);
}

static void
gdk_content_provider_union_detach_clipboard (GdkContentProvider *provider,
                                             GdkClipboard       *clipboard)
{
  GdkContentProviderUnion *self = GDK_CONTENT_PROVIDER_UNION (provider);
  gsize i;

  for (i = 0; i < self->n_providers; i++)
    gdk_content_provider_detach_clipboard (self->providers[i], clipboard);
}

static GdkContentFormats *
gdk_content_provider_union_ref_formats (GdkContentProvider *provider)
{
  GdkContentProviderUnion *self = GDK_CONTENT_PROVIDER_UNION (provider);
  GdkContentFormatsBuilder *builder;
  gsize i;

  builder = gdk_content_formats_builder_new ();

  for (i = 0; i < self->n_providers; i++)
    {
      GdkContentFormats *formats = gdk_content_provider_ref_formats (self->providers[i]);
      gdk_content_formats_builder_add_formats (builder, formats);
      gdk_content_formats_unref (formats);
    }

  return gdk_content_formats_builder_free_to_formats (builder);
}

static GdkContentFormats *
gdk_content_provider_union_ref_storable_formats (GdkContentProvider *provider)
{
  GdkContentProviderUnion *self = GDK_CONTENT_PROVIDER_UNION (provider);
  GdkContentFormatsBuilder *builder;
  gsize i;

  builder = gdk_content_formats_builder_new ();

  for (i = 0; i < self->n_providers; i++)
    {
      GdkContentFormats *formats = gdk_content_provider_ref_storable_formats (self->providers[i]);
      gdk_content_formats_builder_add_formats (builder, formats);
      gdk_content_formats_unref (formats);
    }

  return gdk_content_formats_builder_free_to_formats (builder);
}

static void
gdk_content_provider_union_write_mime_type_done (GObject      *source_object,
                                                 GAsyncResult *res,
                                                 gpointer      data)
{
  GTask *task = data;
  GError *error = NULL;

  if (!gdk_content_provider_write_mime_type_finish (GDK_CONTENT_PROVIDER (source_object), res, &error))
    {
      g_task_return_error (task, error);
    }
  else
    {
      g_task_return_boolean (task, TRUE);
    }

  g_object_unref (task);
}

static void
gdk_content_provider_union_write_mime_type_async (GdkContentProvider     *provider,
                                                  const char             *mime_type,
                                                  GOutputStream          *stream,
                                                  int                     io_priority,
                                                  GCancellable           *cancellable,
                                                  GAsyncReadyCallback     callback,
                                                  gpointer                user_data)
{
  GdkContentProviderUnion *self = GDK_CONTENT_PROVIDER_UNION (provider);
  GTask *task;
  gsize i;

  task = g_task_new (self, cancellable, callback, user_data);
  g_task_set_priority (task, io_priority);
  g_task_set_source_tag (task, gdk_content_provider_union_write_mime_type_async);

  for (i = 0; i < self->n_providers; i++)
    {
      GdkContentFormats *formats = gdk_content_provider_ref_formats (self->providers[i]);

      if (gdk_content_formats_contain_mime_type (formats, mime_type))
        {
          gdk_content_provider_write_mime_type_async (self->providers[i],
                                                      mime_type,
                                                      stream,
                                                      io_priority,
                                                      cancellable,
                                                      gdk_content_provider_union_write_mime_type_done,
                                                      task);
          gdk_content_formats_unref (formats);
          return;
        }
      gdk_content_formats_unref (formats);
    }

  g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED,
                           _("Cannot provide contents as “%s”"), mime_type);
  g_object_unref (task);
}

static gboolean
gdk_content_provider_union_write_mime_type_finish (GdkContentProvider  *provider,
                                                   GAsyncResult        *result,
                                                   GError             **error)
{
  g_return_val_if_fail (g_task_is_valid (result, provider), FALSE);
  g_return_val_if_fail (g_task_get_source_tag (G_TASK (result)) == gdk_content_provider_union_write_mime_type_async, FALSE);

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

static gboolean
gdk_content_provider_union_get_value (GdkContentProvider  *provider,
                                      GValue              *value,
                                      GError             **error)
{
  GdkContentProviderUnion *self = GDK_CONTENT_PROVIDER_UNION (provider);
  gsize i;

  for (i = 0; i < self->n_providers; i++)
    {
      GError *provider_error = NULL;

      if (gdk_content_provider_get_value (self->providers[i], value, &provider_error))
        return TRUE;

      if (!g_error_matches (provider_error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED))
        {
          g_propagate_error (error, provider_error);
          return FALSE;
        }

      g_clear_error (&provider_error);
    }

  return FALSE;
}

static void
gdk_content_provider_union_finalize (GObject *object)
{
  GdkContentProviderUnion *self = GDK_CONTENT_PROVIDER_UNION (object);
  gsize i;

  for (i = 0; i < self->n_providers; i++)
    {
      g_signal_handlers_disconnect_by_func (self->providers[i], gdk_content_provider_content_changed, self);
      g_object_unref (self->providers[i]);
    }

  g_free (self->providers);

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

static void
gdk_content_provider_union_class_init (GdkContentProviderUnionClass *class)
{
  GObjectClass *object_class = G_OBJECT_CLASS (class);
  GdkContentProviderClass *provider_class = GDK_CONTENT_PROVIDER_CLASS (class);

  object_class->finalize = gdk_content_provider_union_finalize;

  provider_class->attach_clipboard = gdk_content_provider_union_attach_clipboard;
  provider_class->detach_clipboard = gdk_content_provider_union_detach_clipboard;
  provider_class->ref_formats = gdk_content_provider_union_ref_formats;
  provider_class->ref_storable_formats = gdk_content_provider_union_ref_storable_formats;
  provider_class->write_mime_type_async = gdk_content_provider_union_write_mime_type_async;
  provider_class->write_mime_type_finish = gdk_content_provider_union_write_mime_type_finish;
  provider_class->get_value = gdk_content_provider_union_get_value;
}

static void
gdk_content_provider_union_init (GdkContentProviderUnion *self)
{
}

/**
 * gdk_content_provider_new_union:
 * @providers: (nullable) (array length=n_providers) (transfer full):
 *     The #GdkContentProviders to present the union of
 * @n_providers: the number of providers
 *
 * Creates a content provider that represents all the given @providers.
 *
 * Whenever data needs to be written, the union provider will try the given
 * @providers in the given order and the first one supporting a format will
 * be chosen to provide it.
 *
 * This allows an easy way to support providing data in different formats.
 * For example, an image may be provided by its file and by the image
 * contents with a call such as
 * |[<!-- language="C" -->
 * gdk_content_provider_new_union ((GdkContentProvider *[2]) {
 *                                   gdk_content_provider_new_typed (G_TYPE_FILE, file),
 *                                   gdk_content_provider_new_typed (G_TYPE_TEXTURE, texture)
 *                                 }, 2);
 * ]|
 *
 *
 * Returns: a new #GdkContentProvider
 **/
GdkContentProvider *
gdk_content_provider_new_union (GdkContentProvider **providers,
                                gsize                n_providers)
{
  GdkContentProviderUnion *result;
  gsize i;

  g_return_val_if_fail (providers != NULL || n_providers == 0, NULL);

  result = g_object_new (GDK_TYPE_CONTENT_PROVIDER_UNION, NULL);

  result->n_providers = n_providers;
  result->providers = g_memdup (providers, sizeof (GdkContentProvider *) * n_providers);

  for (i = 0; i < n_providers; i++)
    {
      g_signal_connect_swapped (result->providers[i],
                                "content-changed",
                                G_CALLBACK (gdk_content_provider_content_changed),
                                result);
    }

  return GDK_CONTENT_PROVIDER (result);
}

#define GDK_TYPE_CONTENT_PROVIDER_BYTES            (gdk_content_provider_bytes_get_type ())
#define GDK_CONTENT_PROVIDER_BYTES(obj)            (G_TYPE_CHECK_INSTANCE_CAST ((obj), GDK_TYPE_CONTENT_PROVIDER_BYTES, GdkContentProviderBytes))
#define GDK_IS_CONTENT_PROVIDER_BYTES(obj)         (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GDK_TYPE_CONTENT_PROVIDER_BYTES))
#define GDK_CONTENT_PROVIDER_BYTES_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST ((klass), GDK_TYPE_CONTENT_PROVIDER_BYTES, GdkContentProviderBytesClass))
#define GDK_IS_CONTENT_PROVIDER_BYTES_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GDK_TYPE_CONTENT_PROVIDER_BYTES))
#define GDK_CONTENT_PROVIDER_BYTES_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS ((obj), GDK_TYPE_CONTENT_PROVIDER_BYTES, GdkContentProviderBytesClass))

typedef struct _GdkContentProviderBytes GdkContentProviderBytes;
typedef struct _GdkContentProviderBytesClass GdkContentProviderBytesClass;

struct _GdkContentProviderBytes
{
  GdkContentProvider parent;

  /* interned */const char *mime_type;
  GBytes *bytes;
};

struct _GdkContentProviderBytesClass
{
  GdkContentProviderClass parent_class;
};

GType gdk_content_provider_bytes_get_type (void) G_GNUC_CONST;

G_DEFINE_TYPE (GdkContentProviderBytes, gdk_content_provider_bytes, GDK_TYPE_CONTENT_PROVIDER)

static void
gdk_content_provider_bytes_finalize (GObject *object)
{
  GdkContentProviderBytes *content = GDK_CONTENT_PROVIDER_BYTES (object);

  g_bytes_unref (content->bytes);

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

static GdkContentFormats *
gdk_content_provider_bytes_ref_formats (GdkContentProvider *provider)
{
  GdkContentProviderBytes *content = GDK_CONTENT_PROVIDER_BYTES (provider);
  GdkContentFormatsBuilder *builder;

  builder = gdk_content_formats_builder_new ();
  gdk_content_formats_builder_add_mime_type (builder, content->mime_type);
  return gdk_content_formats_builder_free_to_formats (builder);
}

static void
gdk_content_provider_bytes_write_mime_type_done (GObject      *stream,
                                                 GAsyncResult *result,
                                                 gpointer      task)
{
  GError *error = NULL;

  if (!g_output_stream_write_all_finish (G_OUTPUT_STREAM (stream),
                                         result,
                                         NULL,
                                         &error))
    {
      g_task_return_error (task, error);
    }
  else
    {
      g_task_return_boolean (task, TRUE);
    }

  g_object_unref (task);
}

static void
gdk_content_provider_bytes_write_mime_type_async (GdkContentProvider     *provider,
                                                  const char             *mime_type,
                                                  GOutputStream          *stream,
                                                  int                     io_priority,
                                                  GCancellable           *cancellable,
                                                  GAsyncReadyCallback     callback,
                                                  gpointer                user_data)
{
  GdkContentProviderBytes *content = GDK_CONTENT_PROVIDER_BYTES (provider);
  GTask *task;

  task = g_task_new (content, cancellable, callback, user_data);
  g_task_set_priority (task, io_priority);
  g_task_set_source_tag (task, gdk_content_provider_bytes_write_mime_type_async);

  if (mime_type != content->mime_type)
    {
      g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED,
                               _("Cannot provide contents as “%s”"), mime_type);
      g_object_unref (task);
      return;
    }

  g_output_stream_write_all_async (stream,
                                   g_bytes_get_data (content->bytes, NULL),
                                   g_bytes_get_size (content->bytes),
                                   io_priority,
                                   cancellable,
                                   gdk_content_provider_bytes_write_mime_type_done,
                                   task);
}

static gboolean
gdk_content_provider_bytes_write_mime_type_finish (GdkContentProvider *provider,
                                                   GAsyncResult       *result,
                                                   GError            **error)
{
  g_return_val_if_fail (g_task_is_valid (result, provider), FALSE);
  g_return_val_if_fail (g_task_get_source_tag (G_TASK (result)) == gdk_content_provider_bytes_write_mime_type_async, FALSE);

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

static void
gdk_content_provider_bytes_class_init (GdkContentProviderBytesClass *class)
{
  GObjectClass *object_class = G_OBJECT_CLASS (class);
  GdkContentProviderClass *provider_class = GDK_CONTENT_PROVIDER_CLASS (class);

  object_class->finalize = gdk_content_provider_bytes_finalize;

  provider_class->ref_formats = gdk_content_provider_bytes_ref_formats;
  provider_class->write_mime_type_async = gdk_content_provider_bytes_write_mime_type_async;
  provider_class->write_mime_type_finish = gdk_content_provider_bytes_write_mime_type_finish;
}

static void
gdk_content_provider_bytes_init (GdkContentProviderBytes *content)
{
}

/**
 * gdk_content_provider_new_for_bytes:
 * @mime_type: the mime type
 * @bytes: (transfer none): a #GBytes with the data for @mime_type
 *
 * Create a content provider that provides the given @bytes as data for
 * the given @mime_type.
 *
 * Returns: a new #GdkContentProvider
 **/
GdkContentProvider *
gdk_content_provider_new_for_bytes (const char *mime_type,
                                    GBytes     *bytes)
{
  GdkContentProviderBytes *content;

  g_return_val_if_fail (mime_type != NULL, NULL);
  g_return_val_if_fail (bytes != NULL, NULL);

  content = g_object_new (GDK_TYPE_CONTENT_PROVIDER_BYTES, NULL);
  content->mime_type = g_intern_string (mime_type);
  content->bytes = g_bytes_ref (bytes);
  
  return GDK_CONTENT_PROVIDER (content);
}

