// Copyright 2011-2022 David Robillard <d@drobilla.net>
// Copyright 2014 Robin Gareus <robin@gareus.org>
// SPDX-License-Identifier: ISC

#include "suil_internal.h"
#include "warnings.h"

#include "lv2/options/options.h"
#include "lv2/urid/urid.h"

SUIL_DISABLE_GTK_WARNINGS
#include <gdk/gdkquartz.h>
#include <gtk/gtk.h>
SUIL_RESTORE_WARNINGS

#include <string.h>

#ifndef MAC_OS_X_VERSION_10_12
#  define MAC_OS_X_VERSION_10_12 101200
#endif

#if MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_12
#  define NSEventTypeFlagsChanged NSFlagsChanged
#  define NSEventTypeLeftMouseDown NSLeftMouseDown
#  define NSEventTypeLeftMouseDragged NSLeftMouseDragged
#  define NSEventTypeLeftMouseUp NSLeftMouseUp
#  define NSEventTypeMouseEntered NSMouseEntered
#  define NSEventTypeMouseExited NSMouseExited
#  define NSEventTypeMouseMoved NSMouseMoved
#  define NSEventTypeRightMouseDown NSRightMouseDown
#  define NSEventTypeRightMouseUp NSRightMouseUp
#  define NSEventTypeScrollWheel NSScrollWheel
#endif

extern "C" {

#define SUIL_TYPE_COCOA_WRAPPER (suil_cocoa_wrapper_get_type())
#define SUIL_COCOA_WRAPPER(obj) \
  (G_TYPE_CHECK_INSTANCE_CAST((obj), SUIL_TYPE_COCOA_WRAPPER, SuilCocoaWrapper))

typedef struct _SuilCocoaWrapper      SuilCocoaWrapper;
typedef struct _SuilCocoaWrapperClass SuilCocoaWrapperClass;

struct _SuilCocoaWrapper {
  GtkWidget     widget;
  SuilWrapper*  wrapper;
  SuilInstance* instance;

  GdkWindow* flt_win;
  bool       custom_size;
  bool       mapped;
  int        req_width;
  int        req_height;
  int        alo_width;
  int        alo_height;

  const LV2UI_Idle_Interface* idle_iface;
  guint                       idle_id;
  guint                       idle_ms;
};

struct _SuilCocoaWrapperClass {
  GtkWidgetClass parent_class;
};

GType
suil_cocoa_wrapper_get_type(void); // Accessor for SUIL_TYPE_COCOA_WRAPPER

G_DEFINE_TYPE(SuilCocoaWrapper, suil_cocoa_wrapper, GTK_TYPE_WIDGET)

static void
suil_cocoa_wrapper_finalize(GObject* gobject)
{
  SuilCocoaWrapper* const self = SUIL_COCOA_WRAPPER(gobject);

  self->wrapper->impl = NULL;

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

static void
suil_cocoa_realize(GtkWidget* widget)
{
  SuilCocoaWrapper* const self = SUIL_COCOA_WRAPPER(widget);
  g_return_if_fail(self != NULL);

  GTK_WIDGET_SET_FLAGS(widget, GTK_REALIZED);

  GdkWindowAttr attrs;
  attrs.x           = widget->allocation.x;
  attrs.y           = widget->allocation.y;
  attrs.width       = widget->allocation.width;
  attrs.height      = widget->allocation.height;
  attrs.wclass      = GDK_INPUT_OUTPUT;
  attrs.window_type = GDK_WINDOW_CHILD;
  attrs.visual      = gtk_widget_get_visual(widget);
  attrs.colormap    = gtk_widget_get_colormap(widget);
  attrs.event_mask  = gtk_widget_get_events(widget) | GDK_EXPOSURE_MASK |
                     GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK |
                     GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK;

  widget->window =
    gdk_window_new(widget->parent->window,
                   &attrs,
                   GDK_WA_X | GDK_WA_Y | GDK_WA_VISUAL | GDK_WA_COLORMAP);

  widget->style = gtk_style_attach(widget->style, widget->window);

  gdk_window_set_user_data(widget->window, widget);
  gtk_style_set_background(widget->style, widget->window, GTK_STATE_ACTIVE);
  gtk_widget_queue_resize(widget);
}

static void
suil_cocoa_size_request(GtkWidget* widget, GtkRequisition* requisition)
{
  SuilCocoaWrapper* const self = SUIL_COCOA_WRAPPER(widget);
  if (self->custom_size) {
    requisition->width  = self->req_width;
    requisition->height = self->req_height;
  } else {
    NSView* view  = (NSView*)self->instance->ui_widget;
    NSRect  frame = [view frame];

    requisition->width =
      static_cast<int>(CGRectGetWidth(NSRectToCGRect(frame)));
    requisition->height =
      static_cast<int>(CGRectGetHeight(NSRectToCGRect(frame)));
  }
}

static void
suil_cocoa_size_allocate(GtkWidget* widget, GtkAllocation* allocation)
{
  SuilCocoaWrapper* const self = SUIL_COCOA_WRAPPER(widget);
  self->alo_width              = allocation->width;
  self->alo_height             = allocation->height;

  if (!self->mapped) {
    return;
  }

  gint xx, yy;
  gtk_widget_translate_coordinates(
    gtk_widget_get_parent(widget), widget, 0, 0, &xx, &yy);

  NSView* view = (NSView*)self->instance->ui_widget;
  [view setFrame:NSMakeRect(xx, yy, self->alo_width, self->alo_height)];
}

static void
suil_cocoa_map(GtkWidget* widget)
{
  SuilCocoaWrapper* const self = SUIL_COCOA_WRAPPER(widget);
  self->mapped                 = true;

  if (self->alo_width == 0 || self->alo_height == 0) {
    return;
  }

  gint xx, yy;
  gtk_widget_translate_coordinates(
    gtk_widget_get_parent(widget), widget, 0, 0, &xx, &yy);

  NSView* view = (NSView*)self->instance->ui_widget;
  [view setHidden:NO];
  [view setFrame:NSMakeRect(xx, yy, self->alo_width, self->alo_height)];
}

static void
suil_cocoa_unmap(GtkWidget* widget)
{
  SuilCocoaWrapper* const self = SUIL_COCOA_WRAPPER(widget);
  NSView*                 view = (NSView*)self->instance->ui_widget;

  self->mapped = false;
  [view setHidden:YES];
}

static gboolean
suil_cocoa_key_press(GtkWidget* widget, GdkEventKey* event)
{
  SuilCocoaWrapper* const self = SUIL_COCOA_WRAPPER(widget);
  if (!self->instance || !self->wrapper || !self->wrapper->impl) {
    return FALSE;
  }

  NSEvent* nsevent = gdk_quartz_event_get_nsevent((GdkEvent*)event);
  NSView*  view    = (NSView*)self->instance->ui_widget;
  [view keyDown:nsevent];
  return TRUE;
}

static gboolean
suil_cocoa_key_release(GtkWidget* widget, GdkEventKey* event)
{
  SuilCocoaWrapper* const self = SUIL_COCOA_WRAPPER(widget);
  if (!self->instance || !self->wrapper || !self->wrapper->impl) {
    return FALSE;
  }

  NSEvent* nsevent = gdk_quartz_event_get_nsevent((GdkEvent*)event);
  NSView*  view    = (NSView*)self->instance->ui_widget;
  [view keyUp:nsevent];
  return TRUE;
}

static gboolean
suil_cocoa_expose(GtkWidget* widget, GdkEventExpose* event)
{
  SuilCocoaWrapper* const self = SUIL_COCOA_WRAPPER(widget);
  NSView*                 view = (NSView*)self->instance->ui_widget;
  [view drawRect:NSMakeRect(event->area.x,
                            event->area.y,
                            event->area.width,
                            event->area.height)];
  return TRUE;
}

static void
suil_cocoa_wrapper_class_init(SuilCocoaWrapperClass* klass)
{
  GObjectClass* const   gobject_class = G_OBJECT_CLASS(klass);
  GtkWidgetClass* const widget_class  = (GtkWidgetClass*)(klass);

  gobject_class->finalize = suil_cocoa_wrapper_finalize;

  widget_class->realize           = suil_cocoa_realize;
  widget_class->expose_event      = suil_cocoa_expose;
  widget_class->size_request      = suil_cocoa_size_request;
  widget_class->size_allocate     = suil_cocoa_size_allocate;
  widget_class->map               = suil_cocoa_map;
  widget_class->unmap             = suil_cocoa_unmap;
  widget_class->key_press_event   = suil_cocoa_key_press;
  widget_class->key_release_event = suil_cocoa_key_release;
}

static void
suil_cocoa_wrapper_init(SuilCocoaWrapper* self)
{
  self->wrapper     = NULL;
  self->instance    = NULL;
  self->flt_win     = NULL;
  self->custom_size = false;
  self->mapped      = false;
  self->req_width   = 0;
  self->req_height  = 0;
  self->alo_width   = 0;
  self->alo_height  = 0;
  self->idle_iface  = NULL;
  self->idle_ms     = 1000 / 30; // 30 Hz default
}

static int
wrapper_resize(LV2UI_Feature_Handle handle, int width, int height)
{
  SuilCocoaWrapper* const wrap = SUIL_COCOA_WRAPPER(handle);

  wrap->req_width   = width;
  wrap->req_height  = height;
  wrap->custom_size = true;

  gtk_widget_queue_resize(GTK_WIDGET(handle));

  return 0;
}

static gboolean
suil_cocoa_wrapper_idle(void* data)
{
  SuilCocoaWrapper* const wrap = SUIL_COCOA_WRAPPER(data);
  wrap->idle_iface->idle(wrap->instance->handle);
  return TRUE; // Continue calling
}

static GdkFilterReturn
event_filter(GdkXEvent* xevent, GdkEvent* event, gpointer data)
{
  (void)event;

  SuilCocoaWrapper* wrap = (SuilCocoaWrapper*)data;
  if (!wrap->instance || !wrap->wrapper || !wrap->wrapper->impl) {
    return GDK_FILTER_CONTINUE;
  }

  NSEvent* nsevent = (NSEvent*)xevent;
  NSView*  view    = (NSView*)wrap->instance->ui_widget;
  if (view && nsevent) {
    switch ([nsevent type]) {
    case NSEventTypeFlagsChanged:
      [view flagsChanged:nsevent];
      return GDK_FILTER_REMOVE;
    case NSEventTypeMouseEntered:
      [view mouseEntered:nsevent];
      return GDK_FILTER_REMOVE;
    case NSEventTypeMouseExited:
      [view mouseExited:nsevent];
      return GDK_FILTER_REMOVE;

    /* Explicitly pass though mouse events.  Needed for mouse-drags leaving
       the window, and mouse-up after that. */
    case NSEventTypeMouseMoved:
      [view mouseMoved:nsevent];
      break;
    case NSEventTypeLeftMouseDragged:
      [view mouseDragged:nsevent];
      break;
    case NSEventTypeLeftMouseDown:
      [view mouseDown:nsevent];
      break;
    case NSEventTypeLeftMouseUp:
      [view mouseUp:nsevent];
      break;
    case NSEventTypeRightMouseDown:
      [view rightMouseDown:nsevent];
      break;
    case NSEventTypeRightMouseUp:
      [view rightMouseUp:nsevent];
      break;
    case NSEventTypeScrollWheel:
      [view scrollWheel:nsevent];
      break;
    default:
      break;
    }
  }
  return GDK_FILTER_CONTINUE;
}

static int
wrapper_wrap(SuilWrapper* wrapper, SuilInstance* instance)
{
  SuilCocoaWrapper* const wrap = SUIL_COCOA_WRAPPER(wrapper->impl);

  instance->host_widget = GTK_WIDGET(wrap);
  wrap->wrapper         = wrapper;
  wrap->instance        = instance;

  const LV2UI_Idle_Interface* idle_iface = NULL;
  if (instance->descriptor->extension_data) {
    idle_iface =
      (const LV2UI_Idle_Interface*)instance->descriptor->extension_data(
        LV2_UI__idleInterface);
  }

  if (idle_iface) {
    wrap->idle_iface = idle_iface;
    wrap->idle_id = g_timeout_add(wrap->idle_ms, suil_cocoa_wrapper_idle, wrap);
  }

  return 0;
}

static void
wrapper_free(SuilWrapper* wrapper)
{
  if (wrapper->impl) {
    SuilCocoaWrapper* const wrap = SUIL_COCOA_WRAPPER(wrapper->impl);
    if (wrap->idle_id) {
      g_source_remove(wrap->idle_id);
      wrap->idle_id = 0;
    }

    gdk_window_remove_filter(wrap->flt_win, event_filter, wrapper->impl);
    gtk_object_destroy(GTK_OBJECT(wrap));
  }
}

SUIL_LIB_EXPORT
SuilWrapper*
suil_wrapper_new(SuilHost*      host,
                 const char*    host_type_uri,
                 const char*    ui_type_uri,
                 LV2_Feature*** features,
                 unsigned       n_features)
{
  (void)host;
  (void)host_type_uri;

  GtkWidget* parent = NULL;
  for (unsigned i = 0; i < n_features; ++i) {
    if (!strcmp((*features)[i]->URI, LV2_UI__parent)) {
      parent = (GtkWidget*)(*features)[i]->data;
    }
  }

  if (!GTK_CONTAINER(parent)) {
    SUIL_ERRORF("No GtkContainer parent given for %s UI\n", ui_type_uri);
    return NULL;
  }

  SuilWrapper* wrapper = (SuilWrapper*)calloc(1, sizeof(SuilWrapper));
  wrapper->wrap        = wrapper_wrap;
  wrapper->free        = wrapper_free;

  SuilCocoaWrapper* const wrap =
    SUIL_COCOA_WRAPPER(g_object_new(SUIL_TYPE_COCOA_WRAPPER, NULL));

  wrapper->impl             = wrap;
  wrapper->resize.handle    = wrap;
  wrapper->resize.ui_resize = wrapper_resize;

  gtk_container_add(GTK_CONTAINER(parent), GTK_WIDGET(wrap));
  gtk_widget_set_can_focus(GTK_WIDGET(wrap), TRUE);
  gtk_widget_set_sensitive(GTK_WIDGET(wrap), TRUE);
  gtk_widget_realize(GTK_WIDGET(wrap));

  GdkWindow* window = gtk_widget_get_window(GTK_WIDGET(wrap));
  wrap->flt_win     = gtk_widget_get_window(parent);
  gdk_window_add_filter(wrap->flt_win, event_filter, wrap);

  NSView* parent_view = gdk_quartz_window_get_nsview(window);
  suil_add_feature(features, &n_features, LV2_UI__parent, parent_view);
  suil_add_feature(features, &n_features, LV2_UI__resize, &wrapper->resize);
  suil_add_feature(features, &n_features, LV2_UI__idleInterface, NULL);

  // Scan for URID map and options
  LV2_URID_Map*       map     = NULL;
  LV2_Options_Option* options = NULL;
  for (LV2_Feature** f = *features; *f && (!map || !options); ++f) {
    if (!strcmp((*f)->URI, LV2_OPTIONS__options)) {
      options = (LV2_Options_Option*)(*f)->data;
    } else if (!strcmp((*f)->URI, LV2_URID__map)) {
      map = (LV2_URID_Map*)(*f)->data;
    }
  }

  if (map && options) {
    // Set UI update rate if given
    LV2_URID ui_updateRate = map->map(map->handle, LV2_UI__updateRate);
    for (LV2_Options_Option* o = options; o->key; ++o) {
      if (o->key == ui_updateRate) {
        wrap->idle_ms = static_cast<guint>(1000.0f / *(const float*)o->value);
        break;
      }
    }
  }

  return wrapper;
}

} // extern "C"
