/*
 * Copyright © 2020 Red Hat, Inc.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library. If not, see <http://www.gnu.org/licenses/>.
 *
 * SPDX-License-Identifier: LGPL-2.1-or-later
 */

#include "config.h"

#include <glib/gi18n.h>

#include "gdkmacosclipboard-private.h"
#include "gdkmacosutils-private.h"

struct _GdkMacosClipboard
{
  GdkClipboard  parent_instance;
  NSPasteboard *pasteboard;
  NSInteger     last_change_count;
};

typedef struct
{
  GMemoryOutputStream *stream;
  NSPasteboardItem    *item;
  NSPasteboardType     type;
  GMainContext        *main_context;
  guint                done : 1;
} WriteRequest;

enum {
  TYPE_STRING,
  TYPE_PBOARD,
  TYPE_URL,
  TYPE_FILE_URL,
  TYPE_COLOR,
  TYPE_TIFF,
  TYPE_PNG,
  TYPE_LAST
};

#define PTYPE(k) (get_pasteboard_type(TYPE_##k))

static NSPasteboardType pasteboard_types[TYPE_LAST];

G_DEFINE_TYPE (GdkMacosClipboard, _gdk_macos_clipboard, GDK_TYPE_CLIPBOARD)

static NSPasteboardType
get_pasteboard_type (int type)
{
  static gsize initialized = FALSE;

  g_assert (type >= 0);
  g_assert (type < TYPE_LAST);

  if (g_once_init_enter (&initialized))
    {
      pasteboard_types[TYPE_PNG] = NSPasteboardTypePNG;
      pasteboard_types[TYPE_STRING] = NSPasteboardTypeString;
      pasteboard_types[TYPE_TIFF] = NSPasteboardTypeTIFF;
      pasteboard_types[TYPE_COLOR] = NSPasteboardTypeColor;

      G_GNUC_BEGIN_IGNORE_DEPRECATIONS
      pasteboard_types[TYPE_PBOARD] = NSStringPboardType;
      G_GNUC_END_IGNORE_DEPRECATIONS

#ifdef AVAILABLE_MAC_OS_X_VERSION_10_13_AND_LATER
      pasteboard_types[TYPE_URL] = NSPasteboardTypeURL;
      pasteboard_types[TYPE_FILE_URL] = NSPasteboardTypeFileURL;
#else
      pasteboard_types[TYPE_URL] = [[NSString alloc] initWithUTF8String:"public.url"];
      pasteboard_types[TYPE_FILE_URL] = [[NSString alloc] initWithUTF8String:"public.file-url"];
#endif

      g_once_init_leave (&initialized, TRUE);
    }

  return pasteboard_types[type];
}

static void
write_request_free (WriteRequest *wr)
{
  g_clear_pointer (&wr->main_context, g_main_context_unref);
  g_clear_object (&wr->stream);
  [wr->item release];
  g_slice_free (WriteRequest, wr);
}

const char *
_gdk_macos_clipboard_from_ns_type (NSPasteboardType type)
{
  G_GNUC_BEGIN_IGNORE_DEPRECATIONS;

  if ([type isEqualToString:PTYPE(STRING)] ||
      [type isEqualToString:PTYPE(PBOARD)])
    return g_intern_string ("text/plain;charset=utf-8");
  else if ([type isEqualToString:PTYPE(URL)] ||
           [type isEqualToString:PTYPE(FILE_URL)])
    return g_intern_string ("text/uri-list");
  else if ([type isEqualToString:PTYPE(COLOR)])
    return g_intern_string ("application/x-color");
  else if ([type isEqualToString:PTYPE(TIFF)])
    return g_intern_string ("image/tiff");
  else if ([type isEqualToString:PTYPE(PNG)])
    return g_intern_string ("image/png");

  G_GNUC_END_IGNORE_DEPRECATIONS;

  return NULL;
}

NSPasteboardType
_gdk_macos_clipboard_to_ns_type (const char       *mime_type,
                                 NSPasteboardType *alternate)
{
  if (alternate)
    *alternate = NULL;

  if (g_strcmp0 (mime_type, "text/plain;charset=utf-8") == 0)
    {
      return PTYPE(STRING);
    }
  else if (g_strcmp0 (mime_type, "text/uri-list") == 0)
    {
      if (alternate)
        *alternate = PTYPE(URL);
      return PTYPE(FILE_URL);
    }
  else if (g_strcmp0 (mime_type, "application/x-color") == 0)
    {
      return PTYPE(COLOR);
    }
  else if (g_strcmp0 (mime_type, "image/tiff") == 0)
    {
      return PTYPE(TIFF);
    }
  else if (g_strcmp0 (mime_type, "image/png") == 0)
    {
      return PTYPE(PNG);
    }

  return nil;
}

static void
populate_content_formats (GdkContentFormatsBuilder *builder,
                          NSPasteboardType          type)
{
  const char *mime_type;

  g_return_if_fail (builder != NULL);
  g_return_if_fail (type != NULL);

  mime_type = _gdk_macos_clipboard_from_ns_type (type);

  if (mime_type != NULL)
    gdk_content_formats_builder_add_mime_type (builder, mime_type);
}

static GdkContentFormats *
load_offer_formats (GdkMacosClipboard *self)
{
  GDK_BEGIN_MACOS_ALLOC_POOL;

  GdkContentFormatsBuilder *builder;
  GdkContentFormats *formats;

  g_assert (GDK_IS_MACOS_CLIPBOARD (self));

  builder = gdk_content_formats_builder_new ();
  for (NSPasteboardType type in [self->pasteboard types])
    populate_content_formats (builder, type);
  formats = gdk_content_formats_builder_free_to_formats (builder);

  GDK_END_MACOS_ALLOC_POOL;

  return g_steal_pointer (&formats);
}

static void
_gdk_macos_clipboard_load_contents (GdkMacosClipboard *self)
{
  GdkContentFormats *formats;
  NSInteger change_count;

  g_assert (GDK_IS_MACOS_CLIPBOARD (self));

  change_count = [self->pasteboard changeCount];

  formats = load_offer_formats (self);
  gdk_clipboard_claim_remote (GDK_CLIPBOARD (self), formats);
  gdk_content_formats_unref (formats);

  self->last_change_count = change_count;
}

static GInputStream *
create_stream_from_nsdata (NSData *data)
{
  const guint8 *bytes = [data bytes];
  gsize len = [data length];

  return g_memory_input_stream_new_from_data (g_memdup (bytes, len), len, g_free);
}

static void
_gdk_macos_clipboard_read_async (GdkClipboard        *clipboard,
                                 GdkContentFormats   *formats,
                                 int                  io_priority,
                                 GCancellable        *cancellable,
                                 GAsyncReadyCallback  callback,
                                 gpointer             user_data)
{
  GDK_BEGIN_MACOS_ALLOC_POOL;

  GdkMacosClipboard *self = (GdkMacosClipboard *)clipboard;
  GdkContentFormats *offer_formats = NULL;
  const char *mime_type;
  GInputStream *stream = NULL;
  GTask *task = NULL;

  g_assert (GDK_IS_MACOS_CLIPBOARD (self));
  g_assert (formats != NULL);

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

  offer_formats = load_offer_formats (GDK_MACOS_CLIPBOARD (clipboard));
  mime_type = gdk_content_formats_match_mime_type (formats, offer_formats);

  if (mime_type == NULL)
    {
      g_task_return_new_error (task,
                               G_IO_ERROR,
                               G_IO_ERROR_NOT_SUPPORTED,
                               "%s",
                               _("No compatible transfer format found"));
      goto cleanup;
    }

  if (strcmp (mime_type, "text/plain;charset=utf-8") == 0)
    {
      NSString *nsstr = [self->pasteboard stringForType:NSPasteboardTypeString];

      if (nsstr != NULL)
        {
          const char *str = [nsstr UTF8String];
          stream = g_memory_input_stream_new_from_data (g_strdup (str),
                                                        strlen (str) + 1,
                                                        g_free);
        }
    }
  else if (strcmp (mime_type, "text/uri-list") == 0)
    {
      G_GNUC_BEGIN_IGNORE_DEPRECATIONS;

      if ([[self->pasteboard types] containsObject:PTYPE(FILE_URL)])
        {
          GString *str = g_string_new (NULL);
          NSArray *files = [self->pasteboard propertyListForType:NSFilenamesPboardType];
          gsize n_files = [files count];
          char *data;
          guint len;

          for (gsize i = 0; i < n_files; ++i)
            {
              NSString* uriString = [files objectAtIndex:i];
              uriString = [@"file://" stringByAppendingString:uriString];
              uriString = [uriString stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];

              g_string_append_printf (str,
                                      "%s\r\n",
                                      [uriString cStringUsingEncoding:NSUTF8StringEncoding]);
            }

          len = str->len;
          data = g_string_free (str, FALSE);
          stream = g_memory_input_stream_new_from_data (data, len, g_free);
        }

      G_GNUC_END_IGNORE_DEPRECATIONS;
    }
  else if (strcmp (mime_type, "application/x-color") == 0)
    {
      NSColorSpace *colorspace;
      NSColor *nscolor;
      guint16 color[4];

      colorspace = [NSColorSpace genericRGBColorSpace];
      nscolor = [[NSColor colorFromPasteboard:self->pasteboard]
                    colorUsingColorSpace:colorspace];

      color[0] = 0xffff * [nscolor redComponent];
      color[1] = 0xffff * [nscolor greenComponent];
      color[2] = 0xffff * [nscolor blueComponent];
      color[3] = 0xffff * [nscolor alphaComponent];

      stream = g_memory_input_stream_new_from_data (g_memdup (&color, sizeof color),
                                                    sizeof color,
                                                    g_free);
    }
  else if (strcmp (mime_type, "image/tiff") == 0)
    {
      NSData *data = [self->pasteboard dataForType:PTYPE(TIFF)];
      stream = create_stream_from_nsdata (data);
    }
  else if (strcmp (mime_type, "image/png") == 0)
    {
      NSData *data = [self->pasteboard dataForType:PTYPE(PNG)];
      stream = create_stream_from_nsdata (data);
    }

  if (stream != NULL)
    {
      g_task_set_task_data (task, g_strdup (mime_type), g_free);
      g_task_return_pointer (task, g_steal_pointer (&stream), g_object_unref);
    }
  else
    {
      g_task_return_new_error (task,
                               G_IO_ERROR,
                               G_IO_ERROR_FAILED,
                               _("Failed to decode contents with mime-type of '%s'"),
                               mime_type);
    }

cleanup:
  g_clear_object (&task);
  g_clear_pointer (&offer_formats, gdk_content_formats_unref);

  GDK_END_MACOS_ALLOC_POOL;
}

static GInputStream *
_gdk_macos_clipboard_read_finish (GdkClipboard  *clipboard,
                                  GAsyncResult  *result,
                                  const char   **out_mime_type,
                                  GError       **error)
{
  GTask *task = (GTask *)result;

  g_assert (GDK_IS_MACOS_CLIPBOARD (clipboard));
  g_assert (G_IS_TASK (task));

  if (out_mime_type != NULL)
    *out_mime_type = g_strdup (g_task_get_task_data (task));

  return g_task_propagate_pointer (task, error);
}

static void
_gdk_macos_clipboard_send_to_pasteboard (GdkMacosClipboard  *self,
                                         GdkContentProvider *content)
{
  GDK_BEGIN_MACOS_ALLOC_POOL;

  GdkMacosClipboardDataProvider *dataProvider;
  GdkContentFormats *serializable;
  NSPasteboardItem *item;
  const char * const *mime_types;
  gsize n_mime_types;

  g_return_if_fail (GDK_IS_MACOS_CLIPBOARD (self));
  g_return_if_fail (GDK_IS_CONTENT_PROVIDER (content));

  serializable = gdk_content_provider_ref_storable_formats (content);
  serializable = gdk_content_formats_union_serialize_mime_types (serializable);
  mime_types = gdk_content_formats_get_mime_types (serializable, &n_mime_types);

  dataProvider = [[GdkMacosClipboardDataProvider alloc] initClipboard:GDK_CLIPBOARD (self)
                                                            mimetypes:mime_types];
  item = [[NSPasteboardItem alloc] init];
  [item setDataProvider:dataProvider forTypes:[dataProvider types]];

  [self->pasteboard clearContents];
  if ([self->pasteboard writeObjects:[NSArray arrayWithObject:item]] == NO)
    g_warning ("Failed to write object to pasteboard");

  self->last_change_count = [self->pasteboard changeCount];

  g_clear_pointer (&serializable, gdk_content_formats_unref);

  GDK_END_MACOS_ALLOC_POOL;
}

static gboolean
_gdk_macos_clipboard_claim (GdkClipboard       *clipboard,
                            GdkContentFormats  *formats,
                            gboolean            local,
                            GdkContentProvider *provider)
{
  GdkMacosClipboard *self = (GdkMacosClipboard *)clipboard;
  gboolean ret;

  g_assert (GDK_IS_CLIPBOARD (clipboard));
  g_assert (formats != NULL);
  g_assert (!provider || GDK_IS_CONTENT_PROVIDER (provider));

  ret = GDK_CLIPBOARD_CLASS (_gdk_macos_clipboard_parent_class)->claim (clipboard, formats, local, provider);

  if (local)
    _gdk_macos_clipboard_send_to_pasteboard (self, provider);

  return ret;
}

static void
_gdk_macos_clipboard_constructed (GObject *object)
{
  GdkMacosClipboard *self = (GdkMacosClipboard *)object;

  if (self->pasteboard == nil)
    self->pasteboard = [[NSPasteboard generalPasteboard] retain];

  G_OBJECT_CLASS (_gdk_macos_clipboard_parent_class)->constructed (object);
}

static void
_gdk_macos_clipboard_finalize (GObject *object)
{
  GdkMacosClipboard *self = (GdkMacosClipboard *)object;

  if (self->pasteboard != nil)
    {
      [self->pasteboard release];
      self->pasteboard = nil;
    }

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

static void
_gdk_macos_clipboard_class_init (GdkMacosClipboardClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);
  GdkClipboardClass *clipboard_class = GDK_CLIPBOARD_CLASS (klass);

  object_class->constructed = _gdk_macos_clipboard_constructed;
  object_class->finalize = _gdk_macos_clipboard_finalize;

  clipboard_class->claim = _gdk_macos_clipboard_claim;
  clipboard_class->read_async = _gdk_macos_clipboard_read_async;
  clipboard_class->read_finish = _gdk_macos_clipboard_read_finish;
}

static void
_gdk_macos_clipboard_init (GdkMacosClipboard *self)
{
}

GdkClipboard *
_gdk_macos_clipboard_new (GdkMacosDisplay *display)
{
  GdkMacosClipboard *self;

  g_return_val_if_fail (GDK_IS_MACOS_DISPLAY (display), NULL);

  self = g_object_new (GDK_TYPE_MACOS_CLIPBOARD,
                       "display", display,
                       NULL);

  _gdk_macos_clipboard_load_contents (self);

  return GDK_CLIPBOARD (self);
}

void
_gdk_macos_clipboard_check_externally_modified (GdkMacosClipboard *self)
{
  g_return_if_fail (GDK_IS_MACOS_CLIPBOARD (self));

  if ([self->pasteboard changeCount] != self->last_change_count)
    _gdk_macos_clipboard_load_contents (self);
}

@implementation GdkMacosClipboardDataProvider

-(id)initClipboard:(GdkClipboard *)gdkClipboard mimetypes:(const char * const *)mime_types;
{
  [super init];

  self->mimeTypes = g_strdupv ((char **)mime_types);
  self->clipboard = g_object_ref (gdkClipboard);

  return self;
}

-(void)dealloc
{
  g_cancellable_cancel (self->cancellable);

  g_clear_pointer (&self->mimeTypes, g_strfreev);
  g_clear_object (&self->clipboard);
  g_clear_object (&self->cancellable);

  [super dealloc];
}

-(void)pasteboardFinishedWithDataProvider:(NSPasteboard *)pasteboard
{
  g_clear_object (&self->clipboard);
}

-(NSArray<NSPasteboardType> *)types
{
  NSMutableArray *ret = [[NSMutableArray alloc] init];

  for (guint i = 0; self->mimeTypes[i]; i++)
    {
      const char *mime_type = self->mimeTypes[i];
      NSPasteboardType type;
      NSPasteboardType alternate = nil;

      if ((type = _gdk_macos_clipboard_to_ns_type (mime_type, &alternate)))
        {
          [ret addObject:type];
          if (alternate)
            [ret addObject:alternate];
        }
    }

  return g_steal_pointer (&ret);
}

static void
on_data_ready_cb (GObject      *object,
                  GAsyncResult *result,
                  gpointer      user_data)
{
  GDK_BEGIN_MACOS_ALLOC_POOL;

  GdkClipboard *clipboard = (GdkClipboard *)object;
  WriteRequest *wr = user_data;
  GError *error = NULL;
  NSData *data = nil;

  g_assert (GDK_IS_CLIPBOARD (clipboard));
  g_assert (G_IS_ASYNC_RESULT (result));
  g_assert (wr != NULL);
  g_assert (G_IS_MEMORY_OUTPUT_STREAM (wr->stream));
  g_assert ([wr->item isKindOfClass:[NSPasteboardItem class]]);

  if (gdk_clipboard_write_finish (clipboard, result, &error))
    {
      gsize size;
      gpointer bytes;

      g_output_stream_close (G_OUTPUT_STREAM (wr->stream), NULL, NULL);

      size = g_memory_output_stream_get_size (wr->stream);
      bytes = g_memory_output_stream_steal_data (wr->stream);
      data = [[NSData alloc] initWithBytesNoCopy:bytes
                                          length:size
                                     deallocator:^(void *alloc, NSUInteger length) { g_free (alloc); }];
    }
  else
    {
      g_warning ("Failed to serialize clipboard contents: %s",
                 error->message);
      g_clear_error (&error);
    }

  [wr->item setData:data forType:wr->type];

  wr->done = TRUE;

  GDK_END_MACOS_ALLOC_POOL;
}

-(void)   pasteboard:(NSPasteboard *)pasteboard
                item:(NSPasteboardItem *)item
  provideDataForType:(NSPasteboardType)type
{
  const char *mime_type = _gdk_macos_clipboard_from_ns_type (type);
  GMainContext *main_context = g_main_context_default ();
  WriteRequest *wr;

  if (self->clipboard == NULL || mime_type == NULL)
    {
      [item setData:[NSData data] forType:type];
      return;
    }

  wr = g_slice_new0 (WriteRequest);
  wr->item = [item retain];
  wr->stream = G_MEMORY_OUTPUT_STREAM (g_memory_output_stream_new_resizable ());
  wr->type = type;
  wr->main_context = g_main_context_ref (main_context);
  wr->done = FALSE;

  gdk_clipboard_write_async (self->clipboard,
                             mime_type,
                             G_OUTPUT_STREAM (wr->stream),
                             G_PRIORITY_DEFAULT,
                             self->cancellable,
                             on_data_ready_cb,
                             wr);

  /* We're forced to provide data synchronously via this API
   * so we must block on the main loop. Using another main loop
   * than the default tends to get us locked up here, so that is
   * what we'll do for now.
   */
  while (!wr->done)
    g_main_context_iteration (wr->main_context, TRUE);

  write_request_free (wr);
}

@end
