/*
 * Copyright © 2021 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 <IOSurface/IOSurface.h>
#include <Foundation/Foundation.h>
#include <OpenGL/CGLIOSurface.h>
#include <QuartzCore/QuartzCore.h>

#include "gdkmacosbuffer-private.h"

struct _GdkMacosBuffer
{
  GObject          parent_instance;
  cairo_region_t  *damage;
  IOSurfaceRef     surface;
  int              lock_count;
  guint            bytes_per_element;
  guint            bits_per_pixel;
  guint            width;
  guint            height;
  guint            stride;
  double           device_scale;
  guint            flipped : 1;
};

G_DEFINE_TYPE (GdkMacosBuffer, gdk_macos_buffer, G_TYPE_OBJECT)

static void
gdk_macos_buffer_dispose (GObject *object)
{
  GdkMacosBuffer *self = (GdkMacosBuffer *)object;

  if (self->lock_count != 0)
    g_critical ("Attempt to dispose %s while lock is held",
                G_OBJECT_TYPE_NAME (self));

  /* We could potentially force the unload of our surface here with
   * IOSurfaceSetPurgeable (self->surface, kIOSurfacePurgeableEmpty, NULL)
   * but that would cause it to empty when the layers may still be attached
   * to it. Better to just let it get GC'd by the system after they have
   * moved on to a new buffer.
   */
  g_clear_pointer (&self->surface, CFRelease);
  g_clear_pointer (&self->damage, cairo_region_destroy);

  G_OBJECT_CLASS (gdk_macos_buffer_parent_class)->dispose (object);
}

static void
gdk_macos_buffer_class_init (GdkMacosBufferClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  object_class->dispose = gdk_macos_buffer_dispose;
}

static void
gdk_macos_buffer_init (GdkMacosBuffer *self)
{
}

static void
add_int (CFMutableDictionaryRef dict,
         const CFStringRef      key,
         int                    value)
{
  CFNumberRef number = CFNumberCreate (NULL, kCFNumberIntType, &value);
  CFDictionaryAddValue (dict, key, number);
  CFRelease (number);
}

static IOSurfaceRef
create_surface (int   width,
                int   height,
                int   bytes_per_element,
                guint *stride)
{
  CFMutableDictionaryRef props;
  IOSurfaceRef ret;
  size_t bytes_per_row;
  size_t total_bytes;

  props = CFDictionaryCreateMutable (kCFAllocatorDefault,
                                     16,
                                     &kCFTypeDictionaryKeyCallBacks,
                                     &kCFTypeDictionaryValueCallBacks);
  if (props == NULL)
    return NULL;

  bytes_per_row = IOSurfaceAlignProperty (kIOSurfaceBytesPerRow, width * bytes_per_element);
  total_bytes = IOSurfaceAlignProperty (kIOSurfaceAllocSize, height * bytes_per_row);

  add_int (props, kIOSurfaceAllocSize, total_bytes);
  add_int (props, kIOSurfaceBytesPerElement, bytes_per_element);
  add_int (props, kIOSurfaceBytesPerRow, bytes_per_row);
  add_int (props, kIOSurfaceHeight, height);
  add_int (props, kIOSurfacePixelFormat, (int)'BGRA');
  add_int (props, kIOSurfaceWidth, width);

  ret = IOSurfaceCreate (props);

  CFRelease (props);

  *stride = bytes_per_row;

  return ret;
}

GdkMacosBuffer *
_gdk_macos_buffer_new (int    width,
                       int    height,
                       double device_scale,
                       int    bytes_per_element,
                       int    bits_per_pixel)
{
  GdkMacosBuffer *self;

  g_return_val_if_fail (width > 0, NULL);
  g_return_val_if_fail (height > 0, NULL);

  self = g_object_new (GDK_TYPE_MACOS_BUFFER, NULL);
  self->bytes_per_element = bytes_per_element;
  self->bits_per_pixel = bits_per_pixel;
  self->surface = create_surface (width, height, bytes_per_element, &self->stride);
  self->width = width;
  self->height = height;
  self->device_scale = device_scale;
  self->lock_count = 0;

  if (self->surface == NULL)
    g_clear_object (&self);

  return self;
}

IOSurfaceRef
_gdk_macos_buffer_get_native (GdkMacosBuffer *self)
{
  g_return_val_if_fail (GDK_IS_MACOS_BUFFER (self), NULL);

  return self->surface;
}

/**
 * _gdk_macos_buffer_lock:
 *
 * This function matches the IOSurfaceLock() name but what it really
 * does is page the buffer back for the CPU to access from VRAM.
 *
 * Generally we don't want to do that, but we do need to in some
 * cases such as when we are rendering with Cairo. There might
 * be an opportunity later to avoid that, but since we are using
 * GL pretty much everywhere already, we don't try.
 */
void
_gdk_macos_buffer_lock (GdkMacosBuffer *self)
{
  g_return_if_fail (GDK_IS_MACOS_BUFFER (self));
  g_return_if_fail (self->lock_count == 0);

  self->lock_count++;

  IOSurfaceLock (self->surface, 0, NULL);
}

void
_gdk_macos_buffer_unlock (GdkMacosBuffer *self)
{
  g_return_if_fail (GDK_IS_MACOS_BUFFER (self));
  g_return_if_fail (self->lock_count == 1);

  self->lock_count--;

  IOSurfaceUnlock (self->surface, 0, NULL);
}

/**
 * _gdk_macos_buffer_lock_readonly:
 *
 * Like _gdk_macos_buffer_lock() but uses the read-only flag to
 * indicate we are not interested in retrieving the updates from
 * the GPU before modifying the CPU-side cache.
 *
 * Must be used with _gdk_macos_buffer_unlock_readonly().
 */
void
_gdk_macos_buffer_read_lock (GdkMacosBuffer *self)
{
  kern_return_t ret;

  g_return_if_fail (GDK_IS_MACOS_BUFFER (self));
  g_return_if_fail (self->lock_count == 0);

  self->lock_count++;

  ret = IOSurfaceLock (self->surface, kIOSurfaceLockReadOnly, NULL);

  g_return_if_fail (ret == KERN_SUCCESS);
}

void
_gdk_macos_buffer_read_unlock (GdkMacosBuffer *self)
{
  kern_return_t ret;

  g_return_if_fail (GDK_IS_MACOS_BUFFER (self));
  g_return_if_fail (self->lock_count == 1);

  self->lock_count--;

  ret = IOSurfaceUnlock (self->surface, kIOSurfaceLockReadOnly, NULL);

  g_return_if_fail (ret == KERN_SUCCESS);
}

guint
_gdk_macos_buffer_get_width (GdkMacosBuffer *self)
{
  g_return_val_if_fail (GDK_IS_MACOS_BUFFER (self), 0);

  return self->width;
}

guint
_gdk_macos_buffer_get_height (GdkMacosBuffer *self)
{
  g_return_val_if_fail (GDK_IS_MACOS_BUFFER (self), 0);

  return self->height;
}

guint
_gdk_macos_buffer_get_stride (GdkMacosBuffer *self)
{
  g_return_val_if_fail (GDK_IS_MACOS_BUFFER (self), 0);

  return self->stride;
}

double
_gdk_macos_buffer_get_device_scale (GdkMacosBuffer *self)
{
  g_return_val_if_fail (GDK_IS_MACOS_BUFFER (self), 1.0);

  return self->device_scale;
}

const cairo_region_t *
_gdk_macos_buffer_get_damage (GdkMacosBuffer *self)
{
  g_return_val_if_fail (GDK_IS_MACOS_BUFFER (self), NULL);

  return self->damage;
}

void
_gdk_macos_buffer_set_damage (GdkMacosBuffer *self,
                              cairo_region_t *damage)
{
  g_return_if_fail (GDK_IS_MACOS_BUFFER (self));

  if (damage == self->damage)
    return;

  g_clear_pointer (&self->damage, cairo_region_destroy);

  self->damage = cairo_region_copy (damage);
  cairo_region_intersect_rectangle (self->damage,
                                    &(cairo_rectangle_int_t) {
                                      0, 0,
                                      self->width, self->height
                                    });
  
}

gpointer
_gdk_macos_buffer_get_data (GdkMacosBuffer *self)
{
  g_return_val_if_fail (GDK_IS_MACOS_BUFFER (self), NULL);

  return IOSurfaceGetBaseAddress (self->surface);
}

gboolean
_gdk_macos_buffer_get_flipped (GdkMacosBuffer *self)
{
  g_return_val_if_fail (GDK_IS_MACOS_BUFFER (self), FALSE);

  return self->flipped;
}

void
_gdk_macos_buffer_set_flipped (GdkMacosBuffer *self,
                               gboolean        flipped)
{
  g_return_if_fail (GDK_IS_MACOS_BUFFER (self));

  self->flipped = !!flipped;
}
