/*
 * 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 <AppKit/AppKit.h>
#include <float.h>
#include <gdk/gdk.h>

#import "GdkMacosCairoView.h"

#include "gdkdeviceprivate.h"
#include "gdkdisplay.h"
#include "gdkframeclockidleprivate.h"
#include "gdkinternals.h"
#include "gdksurfaceprivate.h"

#include "gdkmacosdevice.h"
#include "gdkmacosdevice-private.h"
#include "gdkmacosdisplay-private.h"
#include "gdkmacosdrag-private.h"
#include "gdkmacosdragsurface-private.h"
#include "gdkmacosglcontext-private.h"
#include "gdkmacosmonitor-private.h"
#include "gdkmacospopupsurface-private.h"
#include "gdkmacossurface-private.h"
#include "gdkmacostoplevelsurface-private.h"
#include "gdkmacosutils-private.h"

G_DEFINE_ABSTRACT_TYPE (GdkMacosSurface, gdk_macos_surface, GDK_TYPE_SURFACE)

enum {
  PROP_0,
  PROP_NATIVE,
  LAST_PROP
};

static GParamSpec *properties [LAST_PROP];

static gboolean
window_is_fullscreen (GdkMacosSurface *self)
{
  g_assert (GDK_IS_MACOS_SURFACE (self));

  return ([self->window styleMask] & NSWindowStyleMaskFullScreen) != 0;
}

void
_gdk_macos_surface_reposition_children (GdkMacosSurface *self)
{
  g_assert (GDK_IS_MACOS_SURFACE (self));

  if (GDK_SURFACE_DESTROYED (self))
    return;

  if (!gdk_surface_get_mapped (GDK_SURFACE (self)))
    return;

  for (const GList *iter = GDK_SURFACE (self)->children;
       iter != NULL;
       iter = iter->next)
    {
      GdkMacosSurface *child = iter->data;

      g_assert (GDK_IS_MACOS_SURFACE (child));

      if (GDK_IS_MACOS_POPUP_SURFACE (child))
        _gdk_macos_popup_surface_reposition (GDK_MACOS_POPUP_SURFACE (child));
    }

  if (GDK_IS_POPUP (self) && self->did_initial_present)
    gdk_surface_request_layout (GDK_SURFACE (self));
}

static void
gdk_macos_surface_set_input_region (GdkSurface     *surface,
                                    cairo_region_t *region)
{
}

static void
gdk_macos_surface_set_opaque_region (GdkSurface     *surface,
                                     cairo_region_t *region)
{
  GdkMacosSurface *self = (GdkMacosSurface *)surface;
  NSView *nsview;

  g_assert (GDK_IS_MACOS_SURFACE (self));

  if (region != self->opaque_region)
    {
      g_clear_pointer (&self->opaque_region, cairo_region_destroy);
      self->opaque_region = cairo_region_copy (region);
    }

  if ((nsview = _gdk_macos_surface_get_view (GDK_MACOS_SURFACE (surface))) &&
      GDK_IS_MACOS_CAIRO_VIEW (nsview))
    [(GdkMacosCairoView *)nsview setOpaqueRegion:region];
}

static void
gdk_macos_surface_hide (GdkSurface *surface)
{
  GdkMacosSurface *self = (GdkMacosSurface *)surface;
  GdkSeat *seat;
  gboolean was_mapped;

  g_assert (GDK_IS_MACOS_SURFACE (self));

  was_mapped = GDK_SURFACE_IS_MAPPED (GDK_SURFACE (self));

  seat = gdk_display_get_default_seat (surface->display);
  gdk_seat_ungrab (seat);

  [self->window hide];

  _gdk_surface_clear_update_area (surface);

  if (was_mapped)
    gdk_surface_freeze_updates (GDK_SURFACE (self));
}

static int
gdk_macos_surface_get_scale_factor (GdkSurface *surface)
{
  GdkMacosSurface *self = (GdkMacosSurface *)surface;

  g_assert (GDK_IS_MACOS_SURFACE (self));

  return [self->window backingScaleFactor];
}

void
_gdk_macos_surface_set_shadow (GdkMacosSurface *surface,
                               int              top,
                               int              right,
                               int              bottom,
                               int              left)
{
  GdkMacosSurface *self = (GdkMacosSurface *)surface;

  g_assert (GDK_IS_MACOS_SURFACE (self));

  if (self->shadow_top == top &&
      self->shadow_right == right &&
      self->shadow_bottom == bottom &&
      self->shadow_left == left)
    return;

  self->shadow_top = top;
  self->shadow_right = right;
  self->shadow_bottom = bottom;
  self->shadow_left = left;

  if (top || right || bottom || left)
    [self->window setHasShadow:NO];
}

static void
gdk_macos_surface_begin_frame (GdkMacosSurface *self)
{
  g_assert (GDK_IS_MACOS_SURFACE (self));

}

static void
gdk_macos_surface_end_frame (GdkMacosSurface *self)
{
  GdkFrameTimings *timings;
  GdkFrameClock *frame_clock;
  GdkDisplay *display;

  g_assert (GDK_IS_MACOS_SURFACE (self));

  display = gdk_surface_get_display (GDK_SURFACE (self));
  frame_clock = gdk_surface_get_frame_clock (GDK_SURFACE (self));

  if ((timings = gdk_frame_clock_get_current_timings (frame_clock)))
    self->pending_frame_counter = timings->frame_counter;

  _gdk_macos_display_add_frame_callback (GDK_MACOS_DISPLAY (display), self);

  gdk_surface_freeze_updates (GDK_SURFACE (self));
}

static void
gdk_macos_surface_before_paint (GdkMacosSurface *self,
                                GdkFrameClock   *frame_clock)
{
  GdkSurface *surface = (GdkSurface *)self;

  g_assert (GDK_IS_MACOS_SURFACE (self));
  g_assert (GDK_IS_FRAME_CLOCK (frame_clock));

  if (surface->update_freeze_count > 0)
    return;

  gdk_macos_surface_begin_frame (self);
}

static void
gdk_macos_surface_after_paint (GdkMacosSurface *self,
                               GdkFrameClock   *frame_clock)
{
  GdkSurface *surface = (GdkSurface *)self;

  g_assert (GDK_IS_MACOS_SURFACE (self));
  g_assert (GDK_IS_FRAME_CLOCK (frame_clock));

  if (surface->update_freeze_count > 0)
    return;

  gdk_macos_surface_end_frame (self);
}

static void
gdk_macos_surface_get_root_coords (GdkSurface *surface,
                                   int         x,
                                   int         y,
                                   int        *root_x,
                                   int        *root_y)
{
  GdkMacosSurface *self = (GdkMacosSurface *)surface;

  g_assert (GDK_IS_MACOS_SURFACE (self));

  if (root_x)
    *root_x = self->root_x + x;

  if (root_y)
    *root_y = self->root_y + y;
}

static gboolean
gdk_macos_surface_get_device_state (GdkSurface      *surface,
                                    GdkDevice       *device,
                                    double          *x,
                                    double          *y,
                                    GdkModifierType *mask)
{
  GdkDisplay *display;
  NSWindow *nswindow;
  NSPoint point;

  g_assert (GDK_IS_MACOS_SURFACE (surface));
  g_assert (GDK_IS_MACOS_DEVICE (device));
  g_assert (x != NULL);
  g_assert (y != NULL);
  g_assert (mask != NULL);

  if (GDK_SURFACE_DESTROYED (surface))
    return FALSE;

  display = gdk_surface_get_display (surface);
  nswindow = _gdk_macos_surface_get_native (GDK_MACOS_SURFACE (surface));
  point = [nswindow mouseLocationOutsideOfEventStream];

  *mask = _gdk_macos_display_get_current_keyboard_modifiers (GDK_MACOS_DISPLAY (display))
        | _gdk_macos_display_get_current_mouse_modifiers (GDK_MACOS_DISPLAY (display));

  *x = point.x;
  *y = surface->height - point.y;

  return *x >= 0 && *y >= 0 && *x < surface->width && *y < surface->height;
}

static void
gdk_macos_surface_get_geometry (GdkSurface *surface,
                                int        *x,
                                int        *y,
                                int        *width,
                                int        *height)
{
  g_assert (GDK_IS_MACOS_SURFACE (surface));

  if (x != NULL)
    *x = surface->x;

  if (y != NULL)
    *y = surface->y;

  if (width != NULL)
    *width = surface->width;

  if (height != NULL)
    *height = surface->height;
}

static GdkDrag *
gdk_macos_surface_drag_begin (GdkSurface         *surface,
                              GdkDevice          *device,
                              GdkContentProvider *content,
                              GdkDragAction       actions,
                              double              dx,
                              double              dy)
{
  GdkMacosSurface *self = (GdkMacosSurface *)surface;
  GdkMacosSurface *drag_surface;
  GdkMacosDrag *drag;
  GdkCursor *cursor;
  GdkSeat *seat;
  double px;
  double py;
  int sx;
  int sy;

  g_assert (GDK_IS_MACOS_SURFACE (self));
  g_assert (GDK_IS_MACOS_TOPLEVEL_SURFACE (self) ||
            GDK_IS_MACOS_POPUP_SURFACE (self));
  g_assert (GDK_IS_MACOS_DEVICE (device));
  g_assert (GDK_IS_CONTENT_PROVIDER (content));

  seat = gdk_device_get_seat (device);
  gdk_macos_device_query_state (device, surface, NULL, &px, &py, NULL);
  _gdk_macos_surface_get_root_coords (GDK_MACOS_SURFACE (surface), &sx, &sy);
  drag_surface = _gdk_macos_surface_new (GDK_MACOS_DISPLAY (surface->display),
                                         GDK_SURFACE_TEMP,
                                         surface,
                                         -99, -99, 1, 1);
  drag = g_object_new (GDK_TYPE_MACOS_DRAG,
                       "drag-surface", drag_surface,
                       "surface", surface,
                       "device", device,
                       "content", content,
                       "actions", actions,
                       NULL);
  g_clear_object (&drag_surface);

  cursor = gdk_drag_get_cursor (GDK_DRAG (drag),
                                gdk_drag_get_selected_action (GDK_DRAG (drag)));
  gdk_drag_set_cursor (GDK_DRAG (drag), cursor);

  if (!_gdk_macos_drag_begin (drag))
    {
      g_object_unref (drag);
      return NULL;
    }

  /* Hold a reference until drop_done is called */
  g_object_ref (drag);

  return GDK_DRAG (g_steal_pointer (&drag));
}

static GdkGLContext *
gdk_macos_surface_create_gl_context (GdkSurface    *surface,
                                     gboolean       attached,
                                     GdkGLContext  *share,
                                     GError       **error)
{
  GdkMacosSurface *self = (GdkMacosSurface *)surface;

  g_assert (GDK_IS_MACOS_SURFACE (self));
  g_assert (!share || GDK_IS_GL_CONTEXT (share));

  return _gdk_macos_gl_context_new (self, attached, share, error);
}

static void
gdk_macos_surface_destroy (GdkSurface *surface,
                           gboolean    foreign_destroy)
{
  GDK_BEGIN_MACOS_ALLOC_POOL;

  GdkMacosSurface *self = (GdkMacosSurface *)surface;
  GdkMacosWindow *window = g_steal_pointer (&self->window);
  GdkFrameClock *frame_clock;

  if ((frame_clock = gdk_surface_get_frame_clock (GDK_SURFACE (self))))
    {
      g_signal_handlers_disconnect_by_func (frame_clock,
                                            G_CALLBACK (gdk_macos_surface_before_paint),
                                            self);
      g_signal_handlers_disconnect_by_func (frame_clock,
                                            G_CALLBACK (gdk_macos_surface_before_paint),
                                            self);
    }

  g_clear_pointer (&self->title, g_free);
  g_clear_pointer (&self->opaque_region, cairo_region_destroy);

  if (window != NULL)
    [window close];

  _gdk_macos_display_surface_removed (GDK_MACOS_DISPLAY (surface->display), self);

  g_clear_pointer (&self->monitors, g_ptr_array_unref);

  g_assert (self->sorted.prev == NULL);
  g_assert (self->sorted.next == NULL);
  g_assert (self->frame.prev == NULL);
  g_assert (self->frame.next == NULL);
  g_assert (self->main.prev == NULL);
  g_assert (self->main.next == NULL);

  GDK_END_MACOS_ALLOC_POOL;
}

static void
gdk_macos_surface_constructed (GObject *object)
{
  GdkMacosSurface *self = (GdkMacosSurface *)object;
  GdkFrameClock *frame_clock;

  g_assert (GDK_IS_MACOS_SURFACE (self));

  G_OBJECT_CLASS (gdk_macos_surface_parent_class)->constructed (object);

  if (self->window != NULL)
    {
      NSRect bounds = [[self->window contentView] bounds];

      GDK_SURFACE (self)->width = bounds.size.width;
      GDK_SURFACE (self)->height = bounds.size.height;
      _gdk_macos_surface_update_position (self);
    }

  if ((frame_clock = gdk_surface_get_frame_clock (GDK_SURFACE (self))))
    {
      g_signal_connect_object (frame_clock,
                               "before-paint",
                               G_CALLBACK (gdk_macos_surface_before_paint),
                               self,
                               G_CONNECT_SWAPPED);
      g_signal_connect_object (frame_clock,
                               "after-paint",
                               G_CALLBACK (gdk_macos_surface_after_paint),
                               self,
                               G_CONNECT_SWAPPED);
    }
}

static void
gdk_macos_surface_get_property (GObject    *object,
                                guint       prop_id,
                                GValue     *value,
                                GParamSpec *pspec)
{
  GdkMacosSurface *self = GDK_MACOS_SURFACE (object);

  switch (prop_id)
    {
    case PROP_NATIVE:
      g_value_set_pointer (value, self->window);
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
    }
}

static void
gdk_macos_surface_set_property (GObject      *object,
                                guint         prop_id,
                                const GValue *value,
                                GParamSpec   *pspec)
{
  GdkMacosSurface *self = GDK_MACOS_SURFACE (object);

  switch (prop_id)
    {
    case PROP_NATIVE:
      self->window = g_value_get_pointer (value);
      [self->window setGdkSurface:self];
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
    }
}

static void
gdk_macos_surface_class_init (GdkMacosSurfaceClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);
  GdkSurfaceClass *surface_class = GDK_SURFACE_CLASS (klass);

  object_class->constructed = gdk_macos_surface_constructed;
  object_class->get_property = gdk_macos_surface_get_property;
  object_class->set_property = gdk_macos_surface_set_property;

  surface_class->create_gl_context = gdk_macos_surface_create_gl_context;
  surface_class->destroy = gdk_macos_surface_destroy;
  surface_class->drag_begin = gdk_macos_surface_drag_begin;
  surface_class->get_device_state = gdk_macos_surface_get_device_state;
  surface_class->get_geometry = gdk_macos_surface_get_geometry;
  surface_class->get_root_coords = gdk_macos_surface_get_root_coords;
  surface_class->get_scale_factor = gdk_macos_surface_get_scale_factor;
  surface_class->hide = gdk_macos_surface_hide;
  surface_class->set_input_region = gdk_macos_surface_set_input_region;
  surface_class->set_opaque_region = gdk_macos_surface_set_opaque_region;

  properties [PROP_NATIVE] =
    g_param_spec_pointer ("native",
                          "Native",
                          "The native NSWindow",
                          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);

  g_object_class_install_properties (object_class, LAST_PROP, properties);
}

static void
gdk_macos_surface_init (GdkMacosSurface *self)
{
  self->frame.data = self;
  self->main.data = self;
  self->sorted.data = self;
  self->monitors = g_ptr_array_new_with_free_func (g_object_unref);
}

GdkMacosSurface *
_gdk_macos_surface_new (GdkMacosDisplay   *display,
                        GdkSurfaceType     surface_type,
                        GdkSurface        *parent,
                        int                x,
                        int                y,
                        int                width,
                        int                height)
{
  GdkFrameClock *frame_clock;
  GdkMacosSurface *ret;

  g_return_val_if_fail (GDK_IS_MACOS_DISPLAY (display), NULL);

  if (parent != NULL)
    frame_clock = g_object_ref (gdk_surface_get_frame_clock (parent));
  else
    frame_clock = _gdk_frame_clock_idle_new ();

  switch (surface_type)
    {
    case GDK_SURFACE_TOPLEVEL:
      ret = _gdk_macos_toplevel_surface_new (display, parent, frame_clock, x, y, width, height);
      break;

    case GDK_SURFACE_POPUP:
      ret = _gdk_macos_popup_surface_new (display, parent, frame_clock, x, y, width, height);
      break;

    case GDK_SURFACE_TEMP:
      ret = _gdk_macos_drag_surface_new (display, frame_clock, x, y, width, height);
      break;

    default:
      g_warn_if_reached ();
      ret = NULL;
    }

  if (ret != NULL)
    {
      gdk_surface_freeze_updates (GDK_SURFACE (ret));
      _gdk_macos_surface_monitor_changed (ret);
    }

  g_object_unref (frame_clock);

  return g_steal_pointer (&ret);
}

void
_gdk_macos_surface_get_shadow (GdkMacosSurface *self,
                               int             *top,
                               int             *right,
                               int             *bottom,
                               int             *left)
{

  g_return_if_fail (GDK_IS_MACOS_SURFACE (self));

  if (top)
    *top = self->shadow_top;

  if (left)
    *left = self->shadow_left;

  if (bottom)
    *bottom = self->shadow_bottom;

  if (right)
    *right = self->shadow_right;
}

const char *
_gdk_macos_surface_get_title (GdkMacosSurface *self)
{

  return self->title;
}

void
_gdk_macos_surface_set_title (GdkMacosSurface *self,
                              const char      *title)
{
  g_return_if_fail (GDK_IS_MACOS_SURFACE (self));

  if (title == NULL)
    title = "";

  if (g_strcmp0 (self->title, title) != 0)
    {
      g_free (self->title);
      self->title = g_strdup (title);

      GDK_BEGIN_MACOS_ALLOC_POOL;
      [self->window setTitle:[NSString stringWithUTF8String:title]];
      GDK_END_MACOS_ALLOC_POOL;
    }
}

CGDirectDisplayID
_gdk_macos_surface_get_screen_id (GdkMacosSurface *self)
{
  g_return_val_if_fail (GDK_IS_MACOS_SURFACE (self), (CGDirectDisplayID)-1);

  if (self->window != NULL)
    {
      NSScreen *screen = [self->window screen];
      return [[[screen deviceDescription] objectForKey:@"NSScreenNumber"] unsignedIntValue];
    }

  return (CGDirectDisplayID)-1;
}

NSWindow *
_gdk_macos_surface_get_native (GdkMacosSurface *self)
{
  g_return_val_if_fail (GDK_IS_MACOS_SURFACE (self), NULL);

  return (NSWindow *)self->window;
}

void
_gdk_macos_surface_set_geometry_hints (GdkMacosSurface   *self,
                                       const GdkGeometry *geometry,
                                       GdkSurfaceHints    geom_mask)
{
  NSSize max_size;
  NSSize min_size;

  g_return_if_fail (GDK_IS_MACOS_SURFACE (self));
  g_return_if_fail (geometry != NULL);
  g_return_if_fail (self->window != NULL);

  if (geom_mask & GDK_HINT_MAX_SIZE)
    max_size = NSMakeSize (geometry->max_width, geometry->max_height);
  else
    max_size = NSMakeSize (FLT_MAX, FLT_MAX);
  [self->window setContentMaxSize:max_size];

  if (geom_mask & GDK_HINT_MIN_SIZE)
    min_size = NSMakeSize (geometry->min_width, geometry->min_height);
  else
    min_size = NSMakeSize (1, 1);
  [self->window setContentMinSize:min_size];
}

void
_gdk_macos_surface_resize (GdkMacosSurface *self,
                           int              width,
                           int              height)
{
  g_return_if_fail (GDK_IS_MACOS_SURFACE (self));

  _gdk_macos_surface_move_resize (self, -1, -1, width, height);
}

void
_gdk_macos_surface_update_fullscreen_state (GdkMacosSurface *self)
{
  GdkToplevelState state;
  gboolean is_fullscreen;
  gboolean was_fullscreen;

  g_return_if_fail (GDK_IS_MACOS_SURFACE (self));

  state = GDK_SURFACE (self)->state;
  is_fullscreen = window_is_fullscreen (self);
  was_fullscreen = (state & GDK_TOPLEVEL_STATE_FULLSCREEN) != 0;

  if (is_fullscreen != was_fullscreen)
    {
      if (is_fullscreen)
        gdk_synthesize_surface_state (GDK_SURFACE (self), 0, GDK_TOPLEVEL_STATE_FULLSCREEN);
      else
        gdk_synthesize_surface_state (GDK_SURFACE (self), GDK_TOPLEVEL_STATE_FULLSCREEN, 0);
    }
}

void
_gdk_macos_surface_update_position (GdkMacosSurface *self)
{
  GdkSurface *surface = GDK_SURFACE (self);
  GdkDisplay *display = gdk_surface_get_display (surface);
  NSRect frame_rect = [self->window frame];
  NSRect content_rect = [self->window contentRectForFrameRect:frame_rect];

  _gdk_macos_display_from_display_coords (GDK_MACOS_DISPLAY (display),
                                          content_rect.origin.x,
                                          content_rect.origin.y + content_rect.size.height,
                                          &self->root_x, &self->root_y);

  if (surface->parent != NULL)
    {
      surface->x = self->root_x - GDK_MACOS_SURFACE (surface->parent)->root_x;
      surface->y = self->root_y - GDK_MACOS_SURFACE (surface->parent)->root_y;
    }
  else
    {
      surface->x = self->root_x;
      surface->y = self->root_y;
    }
}

void
_gdk_macos_surface_thaw (GdkMacosSurface *self,
                         gint64           presentation_time,
                         gint64           refresh_interval)
{
  GdkFrameTimings *timings;
  GdkFrameClock *frame_clock;

  g_return_if_fail (GDK_IS_MACOS_SURFACE (self));

  gdk_surface_thaw_updates (GDK_SURFACE (self));

  frame_clock = gdk_surface_get_frame_clock (GDK_SURFACE (self));

  if (self->pending_frame_counter)
    {
      timings = gdk_frame_clock_get_timings (frame_clock, self->pending_frame_counter);

      if (timings != NULL)
        {
          timings->presentation_time = presentation_time - refresh_interval;
          timings->complete = TRUE;
        }

      self->pending_frame_counter = 0;
    }

  timings = gdk_frame_clock_get_current_timings (frame_clock);

  if (timings != NULL)
    {
      timings->refresh_interval = refresh_interval;
      timings->predicted_presentation_time = presentation_time;
    }
}

void
_gdk_macos_surface_show (GdkMacosSurface *self)
{
  gboolean was_mapped;

  g_return_if_fail (GDK_IS_MACOS_SURFACE (self));

  if (GDK_SURFACE_DESTROYED (self))
    return;

  was_mapped = GDK_SURFACE_IS_MAPPED (GDK_SURFACE (self));

  if (!was_mapped)
    gdk_surface_set_is_mapped (GDK_SURFACE (self), TRUE);

  _gdk_macos_display_clear_sorting (GDK_MACOS_DISPLAY (GDK_SURFACE (self)->display));

  [self->window showAndMakeKey:YES];

  if (!was_mapped)
    {
      if (gdk_surface_get_mapped (GDK_SURFACE (self)))
        {
          _gdk_macos_surface_update_position (self);
          gdk_surface_invalidate_rect (GDK_SURFACE (self), NULL);
          gdk_surface_thaw_updates (GDK_SURFACE (self));
        }
    }

  [[self->window contentView] setNeedsDisplay:YES];
}

CGContextRef
_gdk_macos_surface_acquire_context (GdkMacosSurface *self,
                                    gboolean         clear_scale,
                                    gboolean         antialias)
{
  CGContextRef cg_context;

  g_return_val_if_fail (GDK_IS_MACOS_SURFACE (self), NULL);

  if (GDK_SURFACE_DESTROYED (self))
    return NULL;

  if (!(cg_context = [[NSGraphicsContext currentContext] CGContext]))
    return NULL;

  CGContextSaveGState (cg_context);

  if (!antialias)
    CGContextSetAllowsAntialiasing (cg_context, antialias);

  if (clear_scale)
    {
      CGSize scale;

      scale = CGSizeMake (1.0, 1.0);
      scale = CGContextConvertSizeToDeviceSpace (cg_context, scale);

      CGContextScaleCTM (cg_context, 1.0 / scale.width, 1.0 / scale.height);
    }

  return cg_context;
}

void
_gdk_macos_surface_release_context (GdkMacosSurface *self,
                                    CGContextRef     cg_context)
{
  g_return_if_fail (GDK_IS_MACOS_SURFACE (self));

  CGContextRestoreGState (cg_context);
  CGContextSetAllowsAntialiasing (cg_context, TRUE);
}

void
_gdk_macos_surface_synthesize_null_key (GdkMacosSurface *self)
{
  GdkTranslatedKey translated = {0};
  GdkTranslatedKey no_lock = {0};
  GdkDisplay *display;
  GdkEvent *event;
  GdkSeat *seat;

  g_return_if_fail (GDK_IS_MACOS_SURFACE (self));

  translated.keyval = GDK_KEY_VoidSymbol;
  no_lock.keyval = GDK_KEY_VoidSymbol;

  display = gdk_surface_get_display (GDK_SURFACE (self));
  seat = gdk_display_get_default_seat (display);
  event = gdk_key_event_new (GDK_KEY_PRESS,
                             GDK_SURFACE (self),
                             gdk_seat_get_keyboard (seat),
                             GDK_CURRENT_TIME,
                             0,
                             0,
                             FALSE,
                             &translated,
                             &no_lock);
  _gdk_event_queue_append (display, event);
}

void
_gdk_macos_surface_move (GdkMacosSurface *self,
                         int              x,
                         int              y)
{
  g_return_if_fail (GDK_IS_MACOS_SURFACE (self));

  _gdk_macos_surface_move_resize (self, x, y, -1, -1);
}

void
_gdk_macos_surface_move_resize (GdkMacosSurface *self,
                                int              x,
                                int              y,
                                int              width,
                                int              height)
{
  GdkSurface *surface = (GdkSurface *)self;
  GdkDisplay *display;
  NSRect content_rect;
  NSRect frame_rect;
  gboolean size_changed;

  g_return_if_fail (GDK_IS_MACOS_SURFACE (self));

  if ((x == -1 || (x == self->root_x)) &&
      (y == -1 || (y == self->root_y)) &&
      (width == -1 || (width == surface->width)) &&
      (height == -1 || (height == surface->height)))
    return;

  display = gdk_surface_get_display (surface);

  if (width == -1)
    width = surface->width;

  if (height == -1)
    height = surface->height;

  if (x == -1)
    x = self->root_x;

  if (y == -1)
    y = self->root_y;

  size_changed = height != surface->height || width != surface->width;

  if (GDK_IS_MACOS_SURFACE (surface->parent))
    {
      surface->x = x - GDK_MACOS_SURFACE (surface->parent)->root_x;
      surface->y = y - GDK_MACOS_SURFACE (surface->parent)->root_y;
    }
  else
    {
      surface->x = x;
      surface->y = y;
    }

  _gdk_macos_display_to_display_coords (GDK_MACOS_DISPLAY (display),
                                        x, y + height, &x, &y);

  content_rect = NSMakeRect (x, y, width, height);
  frame_rect = [self->window frameRectForContentRect:content_rect];
  [self->window setFrame:frame_rect display:YES];

  if (size_changed)
    gdk_surface_invalidate_rect (surface, NULL);
}

gboolean
_gdk_macos_surface_is_tracking (GdkMacosSurface *self,
                                NSTrackingArea  *area)
{
  GdkMacosBaseView *view;

  g_return_val_if_fail (GDK_IS_MACOS_SURFACE (self), FALSE);

  if (self->window == NULL)
    return FALSE;

  view = (GdkMacosBaseView *)[self->window contentView];
  if (view == NULL)
    return FALSE;

  return [view trackingArea] == area;
}

void
_gdk_macos_surface_monitor_changed (GdkMacosSurface *self)
{
  GListModel *monitors;
  GdkRectangle rect;
  GdkRectangle intersect;
  GdkDisplay *display;
  GdkMonitor *monitor;
  guint n_monitors;

  g_return_if_fail (GDK_IS_MACOS_SURFACE (self));

  rect.x = self->root_x;
  rect.y = self->root_y;
  rect.width = GDK_SURFACE (self)->width;
  rect.height = GDK_SURFACE (self)->height;

  for (guint i = self->monitors->len; i > 0; i--)
    {
      monitor = g_ptr_array_index (self->monitors, i-1);

      if (!gdk_rectangle_intersect (&monitor->geometry, &rect, &intersect))
        {
          g_object_ref (monitor);
          g_ptr_array_remove_index (self->monitors, i-1);
          gdk_surface_leave_monitor (GDK_SURFACE (self), monitor);
          g_object_unref (monitor);
        }
    }

  display = gdk_surface_get_display (GDK_SURFACE (self));
  monitors = gdk_display_get_monitors (display);
  n_monitors = g_list_model_get_n_items (monitors);

  for (guint i = 0; i < n_monitors; i++)
    {
      monitor = g_list_model_get_item (monitors, i);

      if (!g_ptr_array_find (self->monitors, monitor, NULL))
        {
          gdk_surface_enter_monitor (GDK_SURFACE (self), monitor);
          g_ptr_array_add (self->monitors, g_object_ref (monitor));
        }

      g_object_unref (monitor);
    }
}

GdkMonitor *
_gdk_macos_surface_get_best_monitor (GdkMacosSurface *self)
{
  GdkMonitor *best = NULL;
  GdkRectangle rect;
  int best_area = 0;

  g_return_val_if_fail (GDK_IS_MACOS_SURFACE (self), NULL);

  rect.x = self->root_x;
  rect.y = self->root_y;
  rect.width = GDK_SURFACE (self)->width;
  rect.height = GDK_SURFACE (self)->height;

  for (guint i = 0; i < self->monitors->len; i++)
    {
      GdkMonitor *monitor = g_ptr_array_index (self->monitors, i);
      GdkRectangle intersect;

      if (gdk_rectangle_intersect (&monitor->geometry, &rect, &intersect))
        {
          int area = intersect.width * intersect.height;

          if (area > best_area)
            {
              best = monitor;
              best_area = area;
            }
        }
    }

  return best;
}

NSView *
_gdk_macos_surface_get_view (GdkMacosSurface *self)
{
  g_return_val_if_fail (GDK_IS_MACOS_SURFACE (self), NULL);

  if (self->window == NULL)
    return NULL;

  return [self->window contentView];
}

void
_gdk_macos_surface_set_opacity (GdkMacosSurface *self,
                                double           opacity)
{
  g_return_if_fail (GDK_IS_MACOS_SURFACE (self));

  if (self->window != NULL)
    [self->window setAlphaValue:opacity];
}

void
_gdk_macos_surface_get_root_coords (GdkMacosSurface *self,
                                    int             *x,
                                    int             *y)
{
  GdkSurface *surface;
  int out_x = 0;
  int out_y = 0;

  g_return_if_fail (GDK_IS_MACOS_SURFACE (self));

  for (surface = GDK_SURFACE (self); surface; surface = surface->parent)
    {
      out_x += surface->x;
      out_y += surface->y;
    }

  if (x)
    *x = out_x;

  if (y)
    *y = out_y;
}
