/*
  Copyright 2012-2020 David Robillard <d@drobilla.net>
  Copyright 2017 Hanspeter Portner <dev@open-music-kontrollers.ch>

  Permission to use, copy, modify, and/or distribute this software for any
  purpose with or without fee is hereby granted, provided that the above
  copyright notice and this permission notice appear in all copies.

  THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
  WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
  MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
  ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
  WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
  ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
  OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/

#define GL_SILENCE_DEPRECATION 1

#include "mac.h"

#include "implementation.h"

#include "pugl/pugl.h"

#import <Cocoa/Cocoa.h>

#include <mach/mach_time.h>

#include <stdlib.h>

#ifndef __MAC_10_10
typedef NSUInteger NSEventModifierFlags;
#endif

#ifndef __MAC_10_12
typedef NSUInteger NSWindowStyleMask;
#endif

static NSRect
rectToScreen(NSScreen* screen, NSRect rect)
{
  const double screenHeight = [screen frame].size.height;

  rect.origin.y = screenHeight - rect.origin.y - rect.size.height;
  return rect;
}

static NSScreen*
viewScreen(PuglView* view)
{
  return view->impl->window ? [view->impl->window screen]
                            : [NSScreen mainScreen];
}

static NSRect
nsRectToPoints(PuglView* view, const NSRect rect)
{
  const double scaleFactor = [viewScreen(view) backingScaleFactor];

  return NSMakeRect(rect.origin.x / scaleFactor,
                    rect.origin.y / scaleFactor,
                    rect.size.width / scaleFactor,
                    rect.size.height / scaleFactor);
}

static NSRect
nsRectFromPoints(PuglView* view, const NSRect rect)
{
  const double scaleFactor = [viewScreen(view) backingScaleFactor];

  return NSMakeRect(rect.origin.x * scaleFactor,
                    rect.origin.y * scaleFactor,
                    rect.size.width * scaleFactor,
                    rect.size.height * scaleFactor);
}

static NSPoint
nsPointFromPoints(PuglView* view, const NSPoint point)
{
  const double scaleFactor = [viewScreen(view) backingScaleFactor];

  return NSMakePoint(point.x * scaleFactor, point.y * scaleFactor);
}

static NSRect
rectToNsRect(const PuglRect rect)
{
  return NSMakeRect(rect.x, rect.y, rect.width, rect.height);
}

static NSSize
sizePoints(PuglView* view, const double width, const double height)
{
  const double scaleFactor = [viewScreen(view) backingScaleFactor];

  return NSMakeSize(width / scaleFactor, height / scaleFactor);
}

static void
updateViewRect(PuglView* view)
{
  NSWindow* const window = view->impl->window;
  if (window) {
    const NSRect screenFramePt = [[NSScreen mainScreen] frame];
    const NSRect screenFramePx = nsRectFromPoints(view, screenFramePt);
    const NSRect framePt       = [window frame];
    const NSRect contentPt     = [window contentRectForFrameRect:framePt];
    const NSRect contentPx     = nsRectFromPoints(view, contentPt);
    const double screenHeight  = screenFramePx.size.height;

    view->frame.x = contentPx.origin.x;
    view->frame.y = screenHeight - contentPx.origin.y - contentPx.size.height;
    view->frame.width  = contentPx.size.width;
    view->frame.height = contentPx.size.height;
  }
}

@implementation PuglWindow {
@public
  PuglView* puglview;
}

- (id)initWithContentRect:(NSRect)contentRect
                styleMask:(NSWindowStyleMask)aStyle
                  backing:(NSBackingStoreType)bufferingType
                    defer:(BOOL)flag
{
  (void)flag;

  NSWindow* result = [super initWithContentRect:contentRect
                                      styleMask:aStyle
                                        backing:bufferingType
                                          defer:NO];

  [result setAcceptsMouseMovedEvents:YES];
  return (PuglWindow*)result;
}

- (void)setPuglview:(PuglView*)view
{
  puglview = view;

  [self setContentSize:sizePoints(view, view->frame.width, view->frame.height)];
}

- (BOOL)canBecomeKeyWindow
{
  return YES;
}

- (BOOL)canBecomeMainWindow
{
  return YES;
}

- (void)setIsVisible:(BOOL)flag
{
  if (flag && !puglview->visible) {
    const PuglEventConfigure ev = {
      PUGL_CONFIGURE,
      0,
      puglview->frame.x,
      puglview->frame.y,
      puglview->frame.width,
      puglview->frame.height,
    };

    puglDispatchEvent(puglview, (const PuglEvent*)&ev);
    puglDispatchSimpleEvent(puglview, PUGL_MAP);
  } else if (!flag && puglview->visible) {
    puglDispatchSimpleEvent(puglview, PUGL_UNMAP);
  }

  puglview->visible = flag;

  [super setIsVisible:flag];
}

@end

@implementation PuglWrapperView {
@public
  PuglView*                  puglview;
  NSTrackingArea*            trackingArea;
  NSMutableAttributedString* markedText;
  NSMutableDictionary*       userTimers;
  bool                       reshaped;
}

- (void)dispatchExpose:(NSRect)rect
{
  const double scaleFactor = [[NSScreen mainScreen] backingScaleFactor];

  if (reshaped) {
    updateViewRect(puglview);

    const PuglEventConfigure ev = {
      PUGL_CONFIGURE,
      0,
      puglview->frame.x,
      puglview->frame.y,
      puglview->frame.width,
      puglview->frame.height,
    };

    puglDispatchEvent(puglview, (const PuglEvent*)&ev);
    reshaped = false;
  }

  if (![[puglview->impl->drawView window] isVisible]) {
    return;
  }

  const PuglEventExpose ev = {
    PUGL_EXPOSE,
    0,
    rect.origin.x * scaleFactor,
    rect.origin.y * scaleFactor,
    rect.size.width * scaleFactor,
    rect.size.height * scaleFactor,
  };

  puglDispatchEvent(puglview, (const PuglEvent*)&ev);
}

- (NSSize)intrinsicContentSize
{
  if (puglview->defaultWidth || puglview->defaultHeight) {
    return sizePoints(
      puglview, puglview->defaultWidth, puglview->defaultHeight);
  }

  return NSMakeSize(NSViewNoInstrinsicMetric, NSViewNoInstrinsicMetric);
}

- (BOOL)isFlipped
{
  return YES;
}

- (BOOL)acceptsFirstResponder
{
  return YES;
}

- (void)setReshaped
{
  reshaped = true;
}

static uint32_t
getModifiers(const NSEvent* const ev)
{
  const NSEventModifierFlags modifierFlags = [ev modifierFlags];

  return (((modifierFlags & NSShiftKeyMask) ? PUGL_MOD_SHIFT : 0) |
          ((modifierFlags & NSControlKeyMask) ? PUGL_MOD_CTRL : 0) |
          ((modifierFlags & NSAlternateKeyMask) ? PUGL_MOD_ALT : 0) |
          ((modifierFlags & NSCommandKeyMask) ? PUGL_MOD_SUPER : 0));
}

static PuglKey
keySymToSpecial(const NSEvent* const ev)
{
  NSString* chars = [ev charactersIgnoringModifiers];
  if ([chars length] == 1) {
    switch ([chars characterAtIndex:0]) {
    case NSF1FunctionKey:
      return PUGL_KEY_F1;
    case NSF2FunctionKey:
      return PUGL_KEY_F2;
    case NSF3FunctionKey:
      return PUGL_KEY_F3;
    case NSF4FunctionKey:
      return PUGL_KEY_F4;
    case NSF5FunctionKey:
      return PUGL_KEY_F5;
    case NSF6FunctionKey:
      return PUGL_KEY_F6;
    case NSF7FunctionKey:
      return PUGL_KEY_F7;
    case NSF8FunctionKey:
      return PUGL_KEY_F8;
    case NSF9FunctionKey:
      return PUGL_KEY_F9;
    case NSF10FunctionKey:
      return PUGL_KEY_F10;
    case NSF11FunctionKey:
      return PUGL_KEY_F11;
    case NSF12FunctionKey:
      return PUGL_KEY_F12;
    case NSDeleteCharacter:
      return PUGL_KEY_BACKSPACE;
    case NSDeleteFunctionKey:
      return PUGL_KEY_DELETE;
    case NSLeftArrowFunctionKey:
      return PUGL_KEY_LEFT;
    case NSUpArrowFunctionKey:
      return PUGL_KEY_UP;
    case NSRightArrowFunctionKey:
      return PUGL_KEY_RIGHT;
    case NSDownArrowFunctionKey:
      return PUGL_KEY_DOWN;
    case NSPageUpFunctionKey:
      return PUGL_KEY_PAGE_UP;
    case NSPageDownFunctionKey:
      return PUGL_KEY_PAGE_DOWN;
    case NSHomeFunctionKey:
      return PUGL_KEY_HOME;
    case NSEndFunctionKey:
      return PUGL_KEY_END;
    case NSInsertFunctionKey:
      return PUGL_KEY_INSERT;
    case NSMenuFunctionKey:
      return PUGL_KEY_MENU;
    case NSScrollLockFunctionKey:
      return PUGL_KEY_SCROLL_LOCK;
    case NSClearLineFunctionKey:
      return PUGL_KEY_NUM_LOCK;
    case NSPrintScreenFunctionKey:
      return PUGL_KEY_PRINT_SCREEN;
    case NSPauseFunctionKey:
      return PUGL_KEY_PAUSE;
    }
    // SHIFT, CTRL, ALT, and SUPER are handled in [flagsChanged]
  }
  return (PuglKey)0;
}

- (void)updateTrackingAreas
{
  if (trackingArea != nil) {
    [self removeTrackingArea:trackingArea];
    [trackingArea release];
  }

  const int opts = (NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved |
                    NSTrackingActiveAlways);
  trackingArea   = [[NSTrackingArea alloc] initWithRect:[self bounds]
                                              options:opts
                                                owner:self
                                             userInfo:nil];
  [self addTrackingArea:trackingArea];
  [super updateTrackingAreas];
}

- (NSPoint)eventLocation:(NSEvent*)event
{
  return nsPointFromPoints(
    puglview, [self convertPoint:[event locationInWindow] fromView:nil]);
}

static void
handleCrossing(PuglWrapperView* view, NSEvent* event, const PuglEventType type)
{
  const NSPoint           wloc = [view eventLocation:event];
  const NSPoint           rloc = [NSEvent mouseLocation];
  const PuglEventCrossing ev   = {
    type,
    0,
    [event timestamp],
    wloc.x,
    wloc.y,
    rloc.x,
    [[NSScreen mainScreen] frame].size.height - rloc.y,
    getModifiers(event),
    PUGL_CROSSING_NORMAL,
  };

  puglDispatchEvent(view->puglview, (const PuglEvent*)&ev);
}

- (void)mouseEntered:(NSEvent*)event
{
  handleCrossing(self, event, PUGL_POINTER_IN);
  [puglview->impl->cursor set];
  puglview->impl->mouseTracked = true;
}

- (void)mouseExited:(NSEvent*)event
{
  [[NSCursor arrowCursor] set];
  handleCrossing(self, event, PUGL_POINTER_OUT);
  puglview->impl->mouseTracked = false;
}

- (void)cursorUpdate:(NSEvent*)event
{
  (void)event;
  [puglview->impl->cursor set];
}

- (void)mouseMoved:(NSEvent*)event
{
  const NSPoint         wloc = [self eventLocation:event];
  const NSPoint         rloc = [NSEvent mouseLocation];
  const PuglEventMotion ev   = {
    PUGL_MOTION,
    0,
    [event timestamp],
    wloc.x,
    wloc.y,
    rloc.x,
    [[NSScreen mainScreen] frame].size.height - rloc.y,
    getModifiers(event),
  };

  puglDispatchEvent(puglview, (const PuglEvent*)&ev);
}

- (void)mouseDragged:(NSEvent*)event
{
  [self mouseMoved:event];
}

- (void)rightMouseDragged:(NSEvent*)event
{
  [self mouseMoved:event];
}

- (void)otherMouseDragged:(NSEvent*)event
{
  [self mouseMoved:event];
}

- (void)mouseDown:(NSEvent*)event
{
  const NSPoint         wloc = [self eventLocation:event];
  const NSPoint         rloc = [NSEvent mouseLocation];
  const PuglEventButton ev   = {
    PUGL_BUTTON_PRESS,
    0,
    [event timestamp],
    wloc.x,
    wloc.y,
    rloc.x,
    [[NSScreen mainScreen] frame].size.height - rloc.y,
    getModifiers(event),
    (uint32_t)[event buttonNumber] + 1,
  };

  puglDispatchEvent(puglview, (const PuglEvent*)&ev);
}

- (void)mouseUp:(NSEvent*)event
{
  const NSPoint         wloc = [self eventLocation:event];
  const NSPoint         rloc = [NSEvent mouseLocation];
  const PuglEventButton ev   = {
    PUGL_BUTTON_RELEASE,
    0,
    [event timestamp],
    wloc.x,
    wloc.y,
    rloc.x,
    [[NSScreen mainScreen] frame].size.height - rloc.y,
    getModifiers(event),
    (uint32_t)[event buttonNumber] + 1,
  };

  puglDispatchEvent(puglview, (const PuglEvent*)&ev);
}

- (void)rightMouseDown:(NSEvent*)event
{
  [self mouseDown:event];
}

- (void)rightMouseUp:(NSEvent*)event
{
  [self mouseUp:event];
}

- (void)otherMouseDown:(NSEvent*)event
{
  [self mouseDown:event];
}

- (void)otherMouseUp:(NSEvent*)event
{
  [self mouseUp:event];
}

- (void)scrollWheel:(NSEvent*)event
{
  const NSPoint             wloc = [self eventLocation:event];
  const NSPoint             rloc = [NSEvent mouseLocation];
  const double              dx   = [event scrollingDeltaX];
  const double              dy   = [event scrollingDeltaY];
  const PuglScrollDirection dir =
    ((dx == 0.0 && dy > 0.0)
       ? PUGL_SCROLL_UP
       : ((dx == 0.0 && dy < 0.0)
            ? PUGL_SCROLL_DOWN
            : ((dy == 0.0 && dx > 0.0)
                 ? PUGL_SCROLL_RIGHT
                 : ((dy == 0.0 && dx < 0.0) ? PUGL_SCROLL_LEFT
                                            : PUGL_SCROLL_SMOOTH))));

  const PuglEventScroll ev = {
    PUGL_SCROLL,
    0,
    [event timestamp],
    wloc.x,
    wloc.y,
    rloc.x,
    [[NSScreen mainScreen] frame].size.height - rloc.y,
    getModifiers(event),
    [event hasPreciseScrollingDeltas] ? PUGL_SCROLL_SMOOTH : dir,
    dx,
    dy,
  };

  puglDispatchEvent(puglview, (const PuglEvent*)&ev);
}

- (void)keyDown:(NSEvent*)event
{
  if (puglview->hints[PUGL_IGNORE_KEY_REPEAT] && [event isARepeat]) {
    return;
  }

  const NSPoint   wloc  = [self eventLocation:event];
  const NSPoint   rloc  = [NSEvent mouseLocation];
  const PuglKey   spec  = keySymToSpecial(event);
  const NSString* chars = [event charactersIgnoringModifiers];
  const char*     str   = [[chars lowercaseString] UTF8String];
  const uint32_t  code  = (spec ? spec : puglDecodeUTF8((const uint8_t*)str));

  const PuglEventKey ev = {
    PUGL_KEY_PRESS,
    0,
    [event timestamp],
    wloc.x,
    wloc.y,
    rloc.x,
    [[NSScreen mainScreen] frame].size.height - rloc.y,
    getModifiers(event),
    [event keyCode],
    (code != 0xFFFD) ? code : 0,
  };

  puglDispatchEvent(puglview, (const PuglEvent*)&ev);

  if (!spec) {
    [self interpretKeyEvents:@[event]];
  }
}

- (void)keyUp:(NSEvent*)event
{
  const NSPoint   wloc  = [self eventLocation:event];
  const NSPoint   rloc  = [NSEvent mouseLocation];
  const PuglKey   spec  = keySymToSpecial(event);
  const NSString* chars = [event charactersIgnoringModifiers];
  const char*     str   = [[chars lowercaseString] UTF8String];
  const uint32_t  code  = (spec ? spec : puglDecodeUTF8((const uint8_t*)str));

  const PuglEventKey ev = {
    PUGL_KEY_RELEASE,
    0,
    [event timestamp],
    wloc.x,
    wloc.y,
    rloc.x,
    [[NSScreen mainScreen] frame].size.height - rloc.y,
    getModifiers(event),
    [event keyCode],
    (code != 0xFFFD) ? code : 0,
  };

  puglDispatchEvent(puglview, (const PuglEvent*)&ev);
}

- (BOOL)hasMarkedText
{
  return [markedText length] > 0;
}

- (NSRange)markedRange
{
  return (([markedText length] > 0) ? NSMakeRange(0, [markedText length] - 1)
                                    : NSMakeRange(NSNotFound, 0));
}

- (NSRange)selectedRange
{
  return NSMakeRange(NSNotFound, 0);
}

- (void)setMarkedText:(id)string
        selectedRange:(NSRange)selected
     replacementRange:(NSRange)replacement
{
  (void)selected;
  (void)replacement;
  [markedText release];
  markedText =
    ([(NSObject*)string isKindOfClass:[NSAttributedString class]]
       ? [[NSMutableAttributedString alloc] initWithAttributedString:string]
       : [[NSMutableAttributedString alloc] initWithString:string]);
}

- (void)unmarkText
{
  [[markedText mutableString] setString:@""];
}

- (NSArray*)validAttributesForMarkedText
{
  return @[];
}

- (NSAttributedString*)attributedSubstringForProposedRange:(NSRange)range
                                               actualRange:
                                                 (NSRangePointer)actual
{
  (void)range;
  (void)actual;
  return nil;
}

- (NSUInteger)characterIndexForPoint:(NSPoint)point
{
  (void)point;
  return 0;
}

- (NSRect)firstRectForCharacterRange:(NSRange)range
                         actualRange:(NSRangePointer)actual
{
  (void)range;
  (void)actual;

  const NSRect frame = [self bounds];
  return NSMakeRect(frame.origin.x, frame.origin.y, 0.0, 0.0);
}

- (void)doCommandBySelector:(SEL)selector
{
  (void)selector;
}

- (void)insertText:(id)string replacementRange:(NSRange)replacement
{
  (void)replacement;

  NSEvent* const  event = [NSApp currentEvent];
  NSString* const characters =
    ([(NSObject*)string isKindOfClass:[NSAttributedString class]]
       ? [(NSAttributedString*)string string]
       : (NSString*)string);

  const NSPoint wloc = [self eventLocation:event];
  const NSPoint rloc = [NSEvent mouseLocation];
  for (size_t i = 0; i < [characters length]; ++i) {
    const uint32_t code    = [characters characterAtIndex:i];
    char           utf8[8] = {0};
    NSUInteger     len     = 0;

    [characters getBytes:utf8
               maxLength:sizeof(utf8)
              usedLength:&len
                encoding:NSUTF8StringEncoding
                 options:0
                   range:NSMakeRange(i, i + 1)
          remainingRange:nil];

    PuglEventText ev = {
      PUGL_TEXT,
      0,
      [event timestamp],
      wloc.x,
      wloc.y,
      rloc.x,
      [[NSScreen mainScreen] frame].size.height - rloc.y,
      getModifiers(event),
      [event keyCode],
      code,
      { 0, 0, 0, 0, 0, 0, 0, 0 },
    };

    memcpy(ev.string, utf8, len);
    puglDispatchEvent(puglview, (const PuglEvent*)&ev);
  }
}

- (void)flagsChanged:(NSEvent*)event
{
  const uint32_t mods    = getModifiers(event);
  PuglEventType  type    = PUGL_NOTHING;
  PuglKey        special = (PuglKey)0;

  if ((mods & PUGL_MOD_SHIFT) != (puglview->impl->mods & PUGL_MOD_SHIFT)) {
    type    = mods & PUGL_MOD_SHIFT ? PUGL_KEY_PRESS : PUGL_KEY_RELEASE;
    special = PUGL_KEY_SHIFT;
  } else if ((mods & PUGL_MOD_CTRL) != (puglview->impl->mods & PUGL_MOD_CTRL)) {
    type    = mods & PUGL_MOD_CTRL ? PUGL_KEY_PRESS : PUGL_KEY_RELEASE;
    special = PUGL_KEY_CTRL;
  } else if ((mods & PUGL_MOD_ALT) != (puglview->impl->mods & PUGL_MOD_ALT)) {
    type    = mods & PUGL_MOD_ALT ? PUGL_KEY_PRESS : PUGL_KEY_RELEASE;
    special = PUGL_KEY_ALT;
  } else if ((mods & PUGL_MOD_SUPER) !=
             (puglview->impl->mods & PUGL_MOD_SUPER)) {
    type    = mods & PUGL_MOD_SUPER ? PUGL_KEY_PRESS : PUGL_KEY_RELEASE;
    special = PUGL_KEY_SUPER;
  }

  if (special != 0) {
    const NSPoint wloc = [self eventLocation:event];
    const NSPoint rloc = [NSEvent mouseLocation];
    PuglEventKey  ev   = {type,
                       0,
                       [event timestamp],
                       wloc.x,
                       wloc.y,
                       rloc.x,
                       [[NSScreen mainScreen] frame].size.height - rloc.y,
                       mods,
                       [event keyCode],
                       special};
    puglDispatchEvent(puglview, (const PuglEvent*)&ev);
  }

  puglview->impl->mods = mods;
}

- (BOOL)preservesContentInLiveResize
{
  return NO;
}

- (void)viewWillStartLiveResize
{
  puglDispatchSimpleEvent(puglview, PUGL_LOOP_ENTER);
}

- (void)viewWillDraw
{
  puglDispatchSimpleEvent(puglview, PUGL_UPDATE);
  [super viewWillDraw];
}

- (void)resizeTick
{
  puglPostRedisplay(puglview);
}

- (void)timerTick:(NSTimer*)userTimer
{
  const NSNumber*      userInfo = userTimer.userInfo;
  const PuglEventTimer ev       = {PUGL_TIMER, 0, userInfo.unsignedLongValue};

  puglDispatchEvent(puglview, (const PuglEvent*)&ev);
}

- (void)viewDidEndLiveResize
{
  puglDispatchSimpleEvent(puglview, PUGL_LOOP_LEAVE);
}

@end

@interface PuglWindowDelegate : NSObject<NSWindowDelegate>

- (instancetype)initWithPuglWindow:(PuglWindow*)window;

@end

@implementation PuglWindowDelegate {
  PuglWindow* window;
}

- (instancetype)initWithPuglWindow:(PuglWindow*)puglWindow
{
  if ((self = [super init])) {
    window = puglWindow;
  }

  return self;
}

- (BOOL)windowShouldClose:(id)sender
{
  (void)sender;

  puglDispatchSimpleEvent(window->puglview, PUGL_CLOSE);
  return YES;
}

- (void)windowDidMove:(NSNotification*)notification
{
  (void)notification;

  updateViewRect(window->puglview);
}

- (void)windowDidBecomeKey:(NSNotification*)notification
{
  (void)notification;

  PuglEvent ev  = {{PUGL_FOCUS_IN, 0}};
  ev.focus.mode = PUGL_CROSSING_NORMAL;
  puglDispatchEvent(window->puglview, &ev);
}

- (void)windowDidResignKey:(NSNotification*)notification
{
  (void)notification;

  PuglEvent ev  = {{PUGL_FOCUS_OUT, 0}};
  ev.focus.mode = PUGL_CROSSING_NORMAL;
  puglDispatchEvent(window->puglview, &ev);
}

@end

PuglWorldInternals*
puglInitWorldInternals(PuglWorldType type, PuglWorldFlags PUGL_UNUSED(flags))
{
  PuglWorldInternals* impl =
    (PuglWorldInternals*)calloc(1, sizeof(PuglWorldInternals));

  impl->app = [NSApplication sharedApplication];

  if (type == PUGL_PROGRAM) {
    impl->autoreleasePool = [NSAutoreleasePool new];
  }

  return impl;
}

void
puglFreeWorldInternals(PuglWorld* world)
{
  if (world->impl->autoreleasePool) {
    [world->impl->autoreleasePool drain];
  }

  free(world->impl);
}

void*
puglGetNativeWorld(PuglWorld* PUGL_UNUSED(world))
{
  return NULL;
}

PuglInternals*
puglInitViewInternals(void)
{
  PuglInternals* impl = (PuglInternals*)calloc(1, sizeof(PuglInternals));

  impl->cursor = [NSCursor arrowCursor];

  return impl;
}

static NSLayoutConstraint*
puglConstraint(id item, NSLayoutAttribute attribute, float constant)
{
  return
    [NSLayoutConstraint constraintWithItem:item
                                 attribute:attribute
                                 relatedBy:NSLayoutRelationGreaterThanOrEqual
                                    toItem:nil
                                 attribute:NSLayoutAttributeNotAnAttribute
                                multiplier:1.0
                                  constant:(CGFloat)constant];
}

PuglStatus
puglRealize(PuglView* view)
{
  PuglInternals* impl = view->impl;
  if (impl->wrapperView) {
    return PUGL_FAILURE;
  }

  const NSScreen* const screen      = [NSScreen mainScreen];
  const double          scaleFactor = [screen backingScaleFactor];

  // Getting depth from the display mode seems tedious, just set usual values
  if (view->hints[PUGL_RED_BITS] == PUGL_DONT_CARE) {
    view->hints[PUGL_RED_BITS] = 8;
  }
  if (view->hints[PUGL_BLUE_BITS] == PUGL_DONT_CARE) {
    view->hints[PUGL_BLUE_BITS] = 8;
  }
  if (view->hints[PUGL_GREEN_BITS] == PUGL_DONT_CARE) {
    view->hints[PUGL_GREEN_BITS] = 8;
  }
  if (view->hints[PUGL_ALPHA_BITS] == PUGL_DONT_CARE) {
    view->hints[PUGL_ALPHA_BITS] = 8;
  }

  CGDirectDisplayID displayId = CGMainDisplayID();
  CGDisplayModeRef  mode      = CGDisplayCopyDisplayMode(displayId);

  // Try to get refresh rate from mode (usually fails)
  view->hints[PUGL_REFRESH_RATE] = (int)CGDisplayModeGetRefreshRate(mode);

  CGDisplayModeRelease(mode);
  if (view->hints[PUGL_REFRESH_RATE] == 0) {
    // Get refresh rate from a display link
    // TODO: Keep and actually use the display link for something?
    CVDisplayLinkRef link;
    CVDisplayLinkCreateWithCGDisplay(displayId, &link);

    const CVTime p = CVDisplayLinkGetNominalOutputVideoRefreshPeriod(link);
    const double r = p.timeScale / (double)p.timeValue;
    view->hints[PUGL_REFRESH_RATE] = (int)lrint(r);

    CVDisplayLinkRelease(link);
  }

  if (view->frame.width == 0.0 && view->frame.height == 0.0) {
    if (view->defaultWidth == 0.0 && view->defaultHeight == 0.0) {
      return PUGL_BAD_CONFIGURATION;
    }

    const double screenWidthPx  = [screen frame].size.width * scaleFactor;
    const double screenHeightPx = [screen frame].size.height * scaleFactor;

    view->frame.width  = view->defaultWidth;
    view->frame.height = view->defaultHeight;
    view->frame.x      = screenWidthPx / 2.0 - view->frame.width / 2.0;
    view->frame.y      = screenHeightPx / 2.0 - view->frame.height / 2.0;
  }

  const NSRect framePx = rectToNsRect(view->frame);
  const NSRect framePt = NSMakeRect(framePx.origin.x / scaleFactor,
                                    framePx.origin.y / scaleFactor,
                                    framePx.size.width / scaleFactor,
                                    framePx.size.height / scaleFactor);

  // Create wrapper view to handle input
  impl->wrapperView             = [PuglWrapperView alloc];
  impl->wrapperView->puglview   = view;
  impl->wrapperView->userTimers = [[NSMutableDictionary alloc] init];
  impl->wrapperView->markedText = [[NSMutableAttributedString alloc] init];
  [impl->wrapperView setAutoresizesSubviews:YES];
  [impl->wrapperView initWithFrame:framePt];
  [impl->wrapperView addConstraint:puglConstraint(impl->wrapperView,
                                                  NSLayoutAttributeWidth,
                                                  view->minWidth)];
  [impl->wrapperView addConstraint:puglConstraint(impl->wrapperView,
                                                  NSLayoutAttributeHeight,
                                                  view->minHeight)];

  // Create draw view to be rendered to
  PuglStatus st = PUGL_SUCCESS;
  if ((st = view->backend->configure(view)) ||
      (st = view->backend->create(view))) {
    return st;
  }

  // Add draw view to wrapper view
  [impl->wrapperView addSubview:impl->drawView];
  [impl->wrapperView setHidden:NO];
  [impl->drawView setHidden:NO];

  if (view->parent) {
    NSView* pview = (NSView*)view->parent;
    [pview addSubview:impl->wrapperView];
    [impl->drawView setHidden:NO];
    [[impl->drawView window] makeFirstResponder:impl->wrapperView];
  } else {
    unsigned style =
      (NSClosableWindowMask | NSTitledWindowMask | NSMiniaturizableWindowMask);
    if (view->hints[PUGL_RESIZABLE]) {
      style |= NSResizableWindowMask;
    }

    PuglWindow* window = [[[PuglWindow alloc]
      initWithContentRect:rectToScreen([NSScreen mainScreen], framePt)
                styleMask:style
                  backing:NSBackingStoreBuffered
                    defer:NO] retain];
    [window setPuglview:view];

    if (view->title) {
      NSString* titleString =
        [[NSString alloc] initWithBytes:view->title
                                 length:strlen(view->title)
                               encoding:NSUTF8StringEncoding];

      [window setTitle:titleString];
    }

    if (view->minWidth || view->minHeight) {
      [window
        setContentMinSize:sizePoints(view, view->minWidth, view->minHeight)];
    }
    impl->window = window;

    ((NSWindow*)window).delegate =
      [[PuglWindowDelegate alloc] initWithPuglWindow:window];

    if (view->minAspectX && view->minAspectY) {
      [window setContentAspectRatio:sizePoints(view,
                                               view->minAspectX,
                                               view->minAspectY)];
    }

    puglSetFrame(view, view->frame);

    [window setContentView:impl->wrapperView];
    [view->world->impl->app activateIgnoringOtherApps:YES];
    [window makeFirstResponder:impl->wrapperView];
    [window makeKeyAndOrderFront:window];
    [impl->window setIsVisible:NO];
  }

  [impl->wrapperView updateTrackingAreas];

  puglDispatchSimpleEvent(view, PUGL_CREATE);

  return PUGL_SUCCESS;
}

PuglStatus
puglShow(PuglView* view)
{
  if (!view->impl->wrapperView) {
    const PuglStatus st = puglRealize(view);
    if (st) {
      return st;
    }
  }

  if (![view->impl->window isVisible]) {
    [view->impl->window setIsVisible:YES];
    [view->impl->drawView setNeedsDisplay:YES];
    updateViewRect(view);
  }

  return PUGL_SUCCESS;
}

PuglStatus
puglHide(PuglView* view)
{
  [view->impl->window setIsVisible:NO];
  return PUGL_SUCCESS;
}

void
puglFreeViewInternals(PuglView* view)
{
  if (view) {
    if (view->backend) {
      view->backend->destroy(view);
    }

    if (view->impl) {
      [view->impl->wrapperView removeFromSuperview];
      view->impl->wrapperView->puglview = NULL;
      if (view->impl->window) {
        [view->impl->window close];
      }
      [view->impl->wrapperView release];
      if (view->impl->window) {
        [view->impl->window release];
      }
      free(view->impl);
    }
  }
}

PuglStatus
puglGrabFocus(PuglView* view)
{
  NSWindow* window = [view->impl->wrapperView window];

  [window makeKeyWindow];
  [window makeFirstResponder:view->impl->wrapperView];
  return PUGL_SUCCESS;
}

bool
puglHasFocus(const PuglView* view)
{
  PuglInternals* const impl = view->impl;

  return ([[impl->wrapperView window] isKeyWindow] &&
          [[impl->wrapperView window] firstResponder] == impl->wrapperView);
}

PuglStatus
puglRequestAttention(PuglView* view)
{
  if (![view->impl->window isKeyWindow]) {
    [view->world->impl->app requestUserAttention:NSInformationalRequest];
  }

  return PUGL_SUCCESS;
}

PuglStatus
puglStartTimer(PuglView* view, uintptr_t id, double timeout)
{
  puglStopTimer(view, id);

  NSNumber* idNumber = [NSNumber numberWithUnsignedLong:id];

  NSTimer* timer = [NSTimer timerWithTimeInterval:timeout
                                           target:view->impl->wrapperView
                                         selector:@selector(timerTick:)
                                         userInfo:idNumber
                                          repeats:YES];

  [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

  view->impl->wrapperView->userTimers[idNumber] = timer;

  return PUGL_SUCCESS;
}

PuglStatus
puglStopTimer(PuglView* view, uintptr_t id)
{
  NSNumber* idNumber = [NSNumber numberWithUnsignedLong:id];
  NSTimer*  timer    = view->impl->wrapperView->userTimers[idNumber];

  if (timer) {
    [view->impl->wrapperView->userTimers removeObjectForKey:timer];
    [timer invalidate];
    return PUGL_SUCCESS;
  }

  return PUGL_UNKNOWN_ERROR;
}

PuglStatus
puglSendEvent(PuglView* view, const PuglEvent* event)
{
  if (event->type == PUGL_CLIENT) {
    PuglWrapperView* wrapper = view->impl->wrapperView;
    const NSWindow*  window  = [wrapper window];
    const NSRect     rect    = [wrapper frame];
    const NSPoint    center  = {NSMidX(rect), NSMidY(rect)};

    NSEvent* nsevent =
      [NSEvent otherEventWithType:NSApplicationDefined
                         location:center
                    modifierFlags:0
                        timestamp:[[NSProcessInfo processInfo] systemUptime]
                     windowNumber:window.windowNumber
                          context:nil
                          subtype:PUGL_CLIENT
                            data1:(NSInteger)event->client.data1
                            data2:(NSInteger)event->client.data2];

    [view->world->impl->app postEvent:nsevent atStart:false];
    return PUGL_SUCCESS;
  }

  return PUGL_UNSUPPORTED_TYPE;
}

#ifndef PUGL_DISABLE_DEPRECATED
PuglStatus
puglWaitForEvent(PuglView* view)
{
  return puglPollEvents(view->world, -1.0);
}
#endif

static void
dispatchClientEvent(PuglWorld* world, NSEvent* ev)
{
  NSWindow* win = [ev window];
  NSPoint   loc = [ev locationInWindow];
  for (size_t i = 0; i < world->numViews; ++i) {
    PuglView*        view    = world->views[i];
    PuglWrapperView* wrapper = view->impl->wrapperView;
    if ([wrapper window] == win && NSPointInRect(loc, [wrapper frame])) {
      const PuglEventClient event = {
        PUGL_CLIENT, 0, (uintptr_t)[ev data1], (uintptr_t)[ev data2]};

      puglDispatchEvent(view, (const PuglEvent*)&event);
    }
  }
}

PuglStatus
puglUpdate(PuglWorld* world, const double timeout)
{
  NSDate* date =
    ((timeout < 0) ? [NSDate distantFuture]
                   : [NSDate dateWithTimeIntervalSinceNow:timeout]);

  for (NSEvent* ev = NULL;
       (ev = [world->impl->app nextEventMatchingMask:NSAnyEventMask
                                           untilDate:date
                                              inMode:NSDefaultRunLoopMode
                                             dequeue:YES]);) {
    if ([ev type] == NSApplicationDefined && [ev subtype] == PUGL_CLIENT) {
      dispatchClientEvent(world, ev);
    }

    [world->impl->app sendEvent:ev];

    if (timeout < 0) {
      // Now that we've waited and got an event, set the date to now to
      // avoid looping forever
      date = [NSDate date];
    }
  }

  for (size_t i = 0; i < world->numViews; ++i) {
    PuglView* const view = world->views[i];

    if ([[view->impl->drawView window] isVisible]) {
      puglDispatchSimpleEvent(view, PUGL_UPDATE);
    }

    [view->impl->drawView displayIfNeeded];
  }

  return PUGL_SUCCESS;
}

#ifndef PUGL_DISABLE_DEPRECATED
PuglStatus
puglProcessEvents(PuglView* view)
{
  return puglDispatchEvents(view->world);
}
#endif

double
puglGetTime(const PuglWorld* world)
{
  return (mach_absolute_time() / 1e9) - world->startTime;
}

PuglStatus
puglPostRedisplay(PuglView* view)
{
  [view->impl->drawView setNeedsDisplay:YES];
  return PUGL_SUCCESS;
}

PuglStatus
puglPostRedisplayRect(PuglView* view, const PuglRect rect)
{
  const NSRect rectPx = rectToNsRect(rect);

  [view->impl->drawView setNeedsDisplayInRect:nsRectToPoints(view, rectPx)];

  return PUGL_SUCCESS;
}

PuglNativeView
puglGetNativeWindow(PuglView* view)
{
  return (PuglNativeView)view->impl->wrapperView;
}

PuglStatus
puglSetWindowTitle(PuglView* view, const char* title)
{
  puglSetString(&view->title, title);

  if (view->impl->window) {
    NSString* titleString =
      [[NSString alloc] initWithBytes:title
                               length:strlen(title)
                             encoding:NSUTF8StringEncoding];

    [view->impl->window setTitle:titleString];
  }

  return PUGL_SUCCESS;
}

PuglStatus
puglSetFrame(PuglView* view, const PuglRect frame)
{
  PuglInternals* const impl = view->impl;

  // Update view frame to exactly the requested frame in Pugl coordinates
  view->frame = frame;

  const NSRect framePx = rectToNsRect(frame);
  const NSRect framePt = nsRectToPoints(view, framePx);
  if (impl->window) {
    // Resize window to fit new content rect
    const NSRect screenPt = rectToScreen(viewScreen(view), framePt);
    const NSRect winFrame = [impl->window frameRectForContentRect:screenPt];

    [impl->window setFrame:winFrame display:NO];
  }

  // Resize views
  const NSRect sizePx = NSMakeRect(0, 0, frame.width, frame.height);
  const NSRect sizePt = [impl->drawView convertRectFromBacking:sizePx];

  [impl->wrapperView setFrame:(impl->window ? sizePt : framePt)];
  [impl->drawView setFrame:sizePt];

  return PUGL_SUCCESS;
}

PuglStatus
puglSetDefaultSize(PuglView* const view, const int width, const int height)
{
  view->defaultWidth  = width;
  view->defaultHeight = height;
  return PUGL_SUCCESS;
}

PuglStatus
puglSetMinSize(PuglView* const view, const int width, const int height)
{
  view->minWidth  = width;
  view->minHeight = height;

  if (view->impl->window && (view->minWidth || view->minHeight)) {
    [view->impl->window
      setContentMinSize:sizePoints(view, view->minWidth, view->minHeight)];
  }

  return PUGL_SUCCESS;
}

PuglStatus
puglSetMaxSize(PuglView* const view, const int width, const int height)
{
  view->maxWidth  = width;
  view->maxHeight = height;

  if (view->impl->window && (view->maxWidth || view->maxHeight)) {
    [view->impl->window
      setContentMaxSize:sizePoints(view, view->maxWidth, view->maxHeight)];
  }

  return PUGL_SUCCESS;
}

PuglStatus
puglSetAspectRatio(PuglView* const view,
                   const int       minX,
                   const int       minY,
                   const int       maxX,
                   const int       maxY)
{
  view->minAspectX = minX;
  view->minAspectY = minY;
  view->maxAspectX = maxX;
  view->maxAspectY = maxY;

  if (view->impl->window && view->minAspectX && view->minAspectY) {
    [view->impl->window setContentAspectRatio:sizePoints(view,
                                                         view->minAspectX,
                                                         view->minAspectY)];
  }

  return PUGL_SUCCESS;
}

PuglStatus
puglSetTransientFor(PuglView* view, PuglNativeView parent)
{
  view->transientParent = parent;

  if (view->impl->window) {
    NSWindow* parentWindow = [(NSView*)parent window];
    if (parentWindow) {
      [parentWindow addChildWindow:view->impl->window ordered:NSWindowAbove];
      return PUGL_SUCCESS;
    }
  }

  return PUGL_FAILURE;
}

const void*
puglGetClipboard(PuglView* const    view,
                 const char** const type,
                 size_t* const      len)
{
  NSPasteboard* const pasteboard = [NSPasteboard generalPasteboard];

  if ([[pasteboard types] containsObject:NSStringPboardType]) {
    const NSString* str  = [pasteboard stringForType:NSStringPboardType];
    const char*     utf8 = [str UTF8String];

    puglSetBlob(&view->clipboard, utf8, strlen(utf8) + 1);
  }

  return puglGetInternalClipboard(view, type, len);
}

static NSCursor*
puglGetNsCursor(const PuglCursor cursor)
{
  switch (cursor) {
  case PUGL_CURSOR_ARROW:
    return [NSCursor arrowCursor];
  case PUGL_CURSOR_CARET:
    return [NSCursor IBeamCursor];
  case PUGL_CURSOR_CROSSHAIR:
    return [NSCursor crosshairCursor];
  case PUGL_CURSOR_HAND:
    return [NSCursor pointingHandCursor];
  case PUGL_CURSOR_NO:
    return [NSCursor operationNotAllowedCursor];
  case PUGL_CURSOR_LEFT_RIGHT:
    return [NSCursor resizeLeftRightCursor];
  case PUGL_CURSOR_UP_DOWN:
    return [NSCursor resizeUpDownCursor];
  }

  return NULL;
}

PuglStatus
puglSetCursor(PuglView* view, PuglCursor cursor)
{
  PuglInternals* const impl = view->impl;
  NSCursor* const      cur  = puglGetNsCursor(cursor);
  if (!cur) {
    return PUGL_FAILURE;
  }

  impl->cursor = cur;

  if (impl->mouseTracked) {
    [cur set];
  }

  return PUGL_SUCCESS;
}

PuglStatus
puglSetClipboard(PuglView* const   view,
                 const char* const type,
                 const void* const data,
                 const size_t      len)
{
  NSPasteboard* const pasteboard = [NSPasteboard generalPasteboard];
  const char* const   str        = (const char*)data;

  PuglStatus st = puglSetInternalClipboard(view, type, data, len);
  if (st) {
    return st;
  }

  NSString* nsString = [NSString stringWithUTF8String:str];
  if (nsString) {
    [pasteboard declareTypes:[NSArray arrayWithObjects:NSStringPboardType, nil]
                       owner:nil];

    [pasteboard setString:nsString forType:NSStringPboardType];

    return PUGL_SUCCESS;
  }

  return PUGL_UNKNOWN_ERROR;
}
