/* GdkQuartzView.m
 *
 * Copyright (C) 2005-2007 Imendio AB
 * Copyright (C) 2011 Hiroyuki Yamamoto
 *
 * 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 <AvailabilityMacros.h>
#include "config.h"
#import "GdkQuartzView.h"
#include "gdkquartzwindow.h"
#include "gdkprivate-quartz.h"
#include "gdkquartz.h"
#include "gdkinternal-quartz.h"
#include <cairo/cairo-quartz.h>
#import <AppKit/AppKit.h>
#import <IOSurface/IOSurface.h>

@implementation GdkQuartzView



-(id)initWithFrame: (NSRect)frameRect
{
  if ((self = [super initWithFrame: frameRect]))
    {
      pb_props = @{
        (id)kCVPixelBufferIOSurfaceCoreAnimationCompatibilityKey: @1,
        (id)kCVPixelBufferBytesPerRowAlignmentKey: @64,
      };
      [pb_props retain];
      cfpb_props  = (__bridge CFDictionaryRef)pb_props;

      markedRange = NSMakeRange (NSNotFound, 0);
      selectedRange = NSMakeRange (0, 0);
    }

  [self setValue: @(YES) forKey: @"postsFrameChangedNotifications"];

  return self;
}

-(BOOL)acceptsFirstResponder
{
  GDK_NOTE (EVENTS, g_message ("acceptsFirstResponder"));
  return YES;
}

-(BOOL)becomeFirstResponder
{
  GDK_NOTE (EVENTS, g_message ("becomeFirstResponder"));
  return YES;
}

-(BOOL)resignFirstResponder
{
  GDK_NOTE (EVENTS, g_message ("resignFirstResponder"));
  return YES;
}

-(void) keyDown: (NSEvent *) theEvent
{
  /* NOTE: When user press Cmd+A, interpretKeyEvents: will call noop:
     method. When user press and hold A to show the accented char window,
     it consumed repeating key down events for key 'A' do NOT call
     any other method. We use this behavior to determine if this key
     down event is filtered by interpretKeyEvents.
  */

  g_object_set_data (G_OBJECT (gdk_window), GIC_FILTER_KEY,
                     GUINT_TO_POINTER (GIC_FILTER_FILTERED));

  GDK_NOTE (EVENTS, g_message ("keyDown"));
  [self interpretKeyEvents: [NSArray arrayWithObject: theEvent]];
}

-(void)flagsChanged: (NSEvent *) theEvent
{
}

-(NSUInteger)characterIndexForPoint: (NSPoint)aPoint
{
  GDK_NOTE (EVENTS, g_message ("characterIndexForPoint"));
  return 0;
}

-(NSRect)firstRectForCharacterRange: (NSRange)aRange actualRange: (NSRangePointer)actualRange
{
  GDK_NOTE (EVENTS, g_message ("firstRectForCharacterRange"));
  gint ns_x, ns_y;
  GdkRectangle *rect;

  rect = g_object_get_data (G_OBJECT (gdk_window), GIC_CURSOR_RECT);
  if (rect)
    {
      _gdk_quartz_window_gdk_xy_to_xy (rect->x, rect->y + rect->height,
				       &ns_x, &ns_y);

      return NSMakeRect (ns_x, ns_y, rect->width, rect->height);
    }
  else
    {
      return NSMakeRect (0, 0, 0, 0);
    }
}

-(NSArray *)validAttributesForMarkedText
{
  GDK_NOTE (EVENTS, g_message ("validAttributesForMarkedText"));
  return [NSArray arrayWithObjects: NSUnderlineStyleAttributeName, nil];
}

-(NSAttributedString *)attributedSubstringForProposedRange: (NSRange)aRange actualRange: (NSRangePointer)actualRange
{
  GDK_NOTE (EVENTS, g_message ("attributedSubstringForProposedRange"));
  return nil;
}

-(BOOL)hasMarkedText
{
  GDK_NOTE (EVENTS, g_message ("hasMarkedText"));
  return markedRange.location != NSNotFound && markedRange.length != 0;
}

-(NSRange)markedRange
{
  GDK_NOTE (EVENTS, g_message ("markedRange"));
  return markedRange;
}

-(NSRange)selectedRange
{
  GDK_NOTE (EVENTS, g_message ("selectedRange"));
  return selectedRange;
}

-(void)unmarkText
{
  GDK_NOTE (EVENTS, g_message ("unmarkText"));
  selectedRange = NSMakeRange (0, 0);
  markedRange = NSMakeRange (NSNotFound, 0);

  g_object_set_data_full (G_OBJECT (gdk_window), TIC_MARKED_TEXT, NULL, g_free);
}

-(void)setMarkedText: (id)aString selectedRange: (NSRange)newSelection replacementRange: (NSRange)replacementRange
{
  GDK_NOTE (EVENTS, g_message ("setMarkedText"));
  const char *str;

  if (replacementRange.location == NSNotFound)
    {
      markedRange = NSMakeRange (newSelection.location, [aString length]);
      selectedRange = NSMakeRange (newSelection.location, newSelection.length);
    }
  else {
      markedRange = NSMakeRange (replacementRange.location, [aString length]);
      selectedRange = NSMakeRange (replacementRange.location + newSelection.location, newSelection.length);
    }

  if ([aString isKindOfClass: [NSAttributedString class]])
    {
      str = [[aString string] UTF8String];
    }
  else {
      str = [aString UTF8String];
    }

  g_object_set_data_full (G_OBJECT (gdk_window), TIC_MARKED_TEXT, g_strdup (str), g_free);
  g_object_set_data (G_OBJECT (gdk_window), TIC_SELECTED_POS,
		     GUINT_TO_POINTER (selectedRange.location));
  g_object_set_data (G_OBJECT (gdk_window), TIC_SELECTED_LEN,
		     GUINT_TO_POINTER (selectedRange.length));

  GDK_NOTE (EVENTS, g_message ("setMarkedText: set %s (%p, nsview %p): %s",
			       TIC_MARKED_TEXT, gdk_window, self,
			       str ? str : "(empty)"));

  /* handle text input changes by mouse events */
  if (!GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (gdk_window),
                                            TIC_IN_KEY_DOWN)))
    {
      _gdk_quartz_synthesize_null_key_event(gdk_window);
    }
}

-(void)doCommandBySelector: (SEL)aSelector
{
     GDK_NOTE (EVENTS, g_message ("doCommandBySelector %s", [NSStringFromSelector (aSelector) UTF8String]));
  g_object_set_data (G_OBJECT (gdk_window), GIC_FILTER_KEY,
                     GUINT_TO_POINTER (GIC_FILTER_PASSTHRU));
}

-(void)insertText: (id)aString replacementRange: (NSRange)replacementRange
{
  GDK_NOTE (EVENTS, g_message ("insertText"));
  const char *str;
  NSString *string;

  if ([self hasMarkedText])
    [self unmarkText];

  if ([aString isKindOfClass: [NSAttributedString class]])
      string = [aString string];
  else
      string = aString;

  NSCharacterSet *ctrlChars = [NSCharacterSet controlCharacterSet];
  NSCharacterSet *wsnlChars = [NSCharacterSet whitespaceAndNewlineCharacterSet];
  if ([string rangeOfCharacterFromSet:ctrlChars].length &&
      [string rangeOfCharacterFromSet:wsnlChars].length == 0)
    {
      /* discard invalid text input with Chinese input methods */
      str = "";
      [self unmarkText];
#if MAC_OS_X_VERSION_MIN_REQUIRED < 1060
      NSInputManager *currentInputManager = [NSInputManager currentInputManager];
      [currentInputManager markedTextAbandoned:self];
#else
      [[NSTextInputContext currentInputContext] discardMarkedText];
#endif
    }
  else
   {
      str = [string UTF8String];
      selectedRange = NSMakeRange ([string length], 0);
   }

  if (replacementRange.length > 0)
    {
      g_object_set_data (G_OBJECT (gdk_window), TIC_INSERT_TEXT_REPLACE_LEN,
                         GINT_TO_POINTER (replacementRange.length));
    }

  g_object_set_data_full (G_OBJECT (gdk_window), TIC_INSERT_TEXT, g_strdup (str), g_free);
  GDK_NOTE (EVENTS, g_message ("insertText: set %s (%p, nsview %p): %s",
			     TIC_INSERT_TEXT, gdk_window, self,
			     str ? str : "(empty)"));

  g_object_set_data (G_OBJECT (gdk_window), GIC_FILTER_KEY,
		     GUINT_TO_POINTER (GIC_FILTER_FILTERED));

  /* handle text input changes by mouse events */
  if (!GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (gdk_window),
                                            TIC_IN_KEY_DOWN)))
    {
      _gdk_quartz_synthesize_null_key_event(gdk_window);
    }
}
/* --------------------------------------------------------------- */

-(void)dealloc
{
  if (trackingRect)
    {
      [self removeTrackingRect: trackingRect];
#if MAC_OS_X_VERSION_MIN_REQUIRED >= 10500
      [(NSTrackingArea*)trackingRect release];
#endif
      trackingRect = 0;
    }

  if (pixels)
    {
      CVPixelBufferRelease (pixels);
    }

  [pb_props release];
  [super dealloc];
}

-(void)setGdkWindow: (GdkWindow *)window
{
  gdk_window = window;
}

-(GdkWindow *)gdkWindow
{
  return gdk_window;
}

-(NSTrackingRectTag)trackingRect
{
  return trackingRect;
}

-(BOOL)isFlipped
{
  return NO;
}

-(BOOL)isOpaque
{
  if (GDK_WINDOW_DESTROYED (gdk_window))
    return YES;

  /* A view is opaque if its GdkWindow doesn't have the RGBA visual */
  return gdk_window_get_visual (gdk_window) !=
    gdk_screen_get_rgba_visual (_gdk_screen);
}

- (void) viewWillDraw
{
  /* MacOS 11 (Big Sur) has added a new, dynamic "accent" as default.
   * This uses a 10-bit colorspace so every GIMP drawing operation
   * has the additional cost of an 8-bit (ARGB) to 10-bit conversion.
   * Let's disable this mode to regain the lost performance.
   */
  if(gdk_quartz_osx_version() >= GDK_OSX_BIGSUR)
  {
#if MAC_OS_X_VERSION_MIN_REQUIRED >= 101100
    CALayer* layer = self.layer;
    layer.contentsFormat = kCAContentsFormatRGBA8Uint;
#endif
  }

  [super viewWillDraw];
}

#if MAC_OS_X_VERSION_MIN_REQUIRED >= 10900
-(BOOL)wantsUpdateLayer
{
     return YES;
}
#endif

-(BOOL)wantsLayer
{
     return YES;
}

static void
cairo_rect_from_nsrect (cairo_rectangle_int_t *rect, NSRect *nsrect)
{
  rect->x = (int)nsrect->origin.x;
  rect->y = (int)nsrect->origin.y;
  rect->width = (int)nsrect->size.width;
  rect->height = (int)nsrect->size.height;
}

static cairo_status_t
copy_rectangle_argb32 (cairo_surface_t *dest, cairo_surface_t *source,
                       cairo_region_t *region)
{
  cairo_surface_t *source_img, *dest_img;
  cairo_status_t status;
  cairo_format_t format;
  int height, width, stride;
  cairo_rectangle_int_t extents;

  cairo_region_get_extents (region, &extents);
  source_img = cairo_surface_map_to_image (source, &extents);
  status = cairo_surface_status (source_img);

  if (status)
    {
      g_warning ("Failed to map source image surface, %d %d %d %d on %d %d: %s\n",
                 extents.x, extents.y, extents.width, extents.height,
                 cairo_image_surface_get_width (source),
                 cairo_image_surface_get_height (source),
                 cairo_status_to_string (status));
       return status;
    }

  format = cairo_image_surface_get_format (source_img);
  dest_img = cairo_surface_map_to_image (dest, &extents);
  status = cairo_surface_status (dest_img);

  if (status)
    {
      g_warning ("Failed to map destination image surface, %d %d %d %d on %d %d: %s\n",
                 extents.x, extents.y, extents.width, extents.height,
                 cairo_image_surface_get_width (dest),
                 cairo_image_surface_get_height (dest),
                 cairo_status_to_string (status));
       goto CLEANUP;
    }

  width = cairo_image_surface_get_width (source_img);
  stride = cairo_format_stride_for_width (format, width);
  height = cairo_image_surface_get_height (source_img);
  memcpy (cairo_image_surface_get_data (dest_img),
          cairo_image_surface_get_data (source_img),
          stride * height);
  cairo_surface_unmap_image (dest, dest_img);

 CLEANUP:
  cairo_surface_unmap_image (source, source_img);
  return status;
}

-(void)updateLayer
{
  GdkWindowImplQuartz *impl = GDK_WINDOW_IMPL_QUARTZ (gdk_window->impl);
  cairo_rectangle_int_t impl_rect = {0, 0, 0, 0};
  CGRect layer_bounds = [self.layer bounds];
  CGRect backing_bounds = [self convertRectToBacking: layer_bounds];
  cairo_rectangle_int_t bounds_rect;
  cairo_region_t *bounds_region;
  cairo_surface_t *cvpb_surface;

  if (GDK_WINDOW_DESTROYED (gdk_window))
    return;

  ++impl->in_paint_rect_count;

  if (impl->needs_display_region)
    {
      cairo_region_t *region = impl->needs_display_region;
      _gdk_window_process_updates_recurse (gdk_window, region);
      cairo_region_destroy (region);
      impl->needs_display_region = NULL;
    }
  else
    {
      cairo_rectangle_int_t bounds;
      cairo_region_t *region;

      cairo_rect_from_nsrect (&bounds, &layer_bounds);
      region = cairo_region_create_rectangle(&bounds);
      _gdk_window_process_updates_recurse (gdk_window, region);
      cairo_region_destroy (region);
    }

  if (!impl || !impl->cairo_surface)
    return;

  CVPixelBufferLockBaseAddress (pixels, 0);
  cvpb_surface =
    cairo_image_surface_create_for_data (CVPixelBufferGetBaseAddress (pixels),
                                         CAIRO_FORMAT_ARGB32,
                                         (int)CVPixelBufferGetWidth (pixels),
                                         (int)CVPixelBufferGetHeight (pixels),
                                         (int)CVPixelBufferGetBytesPerRow (pixels));


  cairo_rect_from_nsrect (&bounds_rect, &backing_bounds);
  bounds_region = cairo_region_create_rectangle (&bounds_rect);

  impl_rect.width = cairo_image_surface_get_width (impl->cairo_surface);
  impl_rect.height = cairo_image_surface_get_height (impl->cairo_surface);

  cairo_region_intersect_rectangle (bounds_region, &impl_rect);
  copy_rectangle_argb32 (cvpb_surface, impl->cairo_surface, bounds_region);

  cairo_surface_destroy (cvpb_surface);
  cairo_region_destroy (bounds_region);
  _gdk_quartz_unref_cairo_surface (gdk_window); // reffed in gdk_window_impl_quartz_begin_paint
  CVPixelBufferUnlockBaseAddress (pixels, 0);

  --impl->in_paint_rect_count;

  self.layer.contents = NULL;
  self.layer.contents = (id)CVPixelBufferGetIOSurface (pixels);
}

-(void)setNeedsInvalidateShadow: (BOOL)invalidate
{
  needsInvalidateShadow = invalidate;
}

/* For information on setting up tracking rects properly, see here:
 * http://developer.apple.com/documentation/Cocoa/Conceptual/EventOverview/EventOverview.pdf
 */
-(void)updateTrackingRect
{
  GdkWindowImplQuartz *impl = GDK_WINDOW_IMPL_QUARTZ (gdk_window->impl);
  NSRect rect;
#if MAC_OS_X_VERSION_MIN_REQUIRED >= 10500
  NSTrackingArea *trackingArea;
#endif
  
  if (!impl || !impl->toplevel)
    return;

  if (trackingRect)
    {
      [self removeTrackingRect: trackingRect];
#if MAC_OS_X_VERSION_MIN_REQUIRED >= 10500
      [(NSTrackingArea*)trackingRect release];
#endif
      trackingRect = 0;
    }

  if (!impl->toplevel)
    return;

  /* Note, if we want to set assumeInside we can use:
   * NSPointInRect ([[self window] convertScreenToBase:[NSEvent mouseLocation]], rect)
   */

  rect = [self bounds];
#if MAC_OS_X_VERSION_MIN_REQUIRED >= 10500
  trackingArea = [[NSTrackingArea alloc] initWithRect: rect
                  options: NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | NSTrackingCursorUpdate | NSTrackingActiveInActiveApp | NSTrackingInVisibleRect | NSTrackingEnabledDuringMouseDrag
                  owner: self
                  userInfo: nil];
  [self addTrackingArea: trackingArea];
  trackingRect = (NSInteger)[trackingArea retain];
#else
  trackingRect = [self addTrackingRect: rect
		  owner: self
		  userData: nil
		  assumeInside: NO];
#endif
}

-(void)viewDidMoveToWindow
{
  if (![self window]) /* We are destroyed already */
    return;

  [self updateTrackingRect];
}

-(void)viewWillMoveToWindow: (NSWindow *)newWindow
{
  if (newWindow == nil && trackingRect)
    {
      [self removeTrackingRect: trackingRect];
#if MAC_OS_X_VERSION_MIN_REQUIRED >= 10500
      [(NSTrackingArea*)trackingRect release];
#endif
      trackingRect = 0;
    }
}

-(void)createBackingStoreWithWidth: (CGFloat) width andHeight: (CGFloat) height
{
  IOSurfaceRef surface;

  g_return_if_fail (width && height);

  CVPixelBufferRelease (pixels);
  CVPixelBufferCreate (NULL, width, height,
                       kCVPixelFormatType_32BGRA,
                       cfpb_props, &pixels);

  surface = CVPixelBufferGetIOSurface (pixels);
  IOSurfaceSetValue(surface, CFSTR("IOSurfaceColorSpace"),
                    kCGColorSpaceSRGB);
}

#if MAC_OS_X_VERSION_MIN_REQUIRED >= 10700
-(BOOL)layer:(CALayer*) layer shouldInheritContentsScale: (CGFloat)scale fromWindow: (NSWindow *) window
{
  if (layer == self.layer && window == self.window)
    {
      _gdk_quartz_unref_cairo_surface (gdk_window);
      [self setNeedsDisplay: YES];
    }
  return YES;
}
#endif

-(void)setFrame: (NSRect)frame
{
  if (GDK_WINDOW_DESTROYED (gdk_window))
    return;

  _gdk_quartz_unref_cairo_surface (gdk_window);
  [super setFrame: frame];

  if ([self window])
    [self updateTrackingRect];
}

@end
