//
// Copyright 2015 The ANGLE Project Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//

// OSXWindow.mm: Implementation of OSWindow for OSX

#include "util/osx/OSXWindow.h"

#include <set>
// Include Carbon to use the keycode names in Carbon's Event.h
#include <Carbon/Carbon.h>

#include "anglebase/no_destructor.h"
#include "common/debug.h"

// On OSX 10.12 a number of AppKit interfaces have been renamed for consistency, and the previous
// symbols tagged as deprecated. However we can't simply use the new symbols as it would break
// compilation on our automated testing that doesn't use OSX 10.12 yet. So we just ignore the
// warnings.
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"

// Some events such as "ShouldTerminate" are sent to the whole application so we keep a list of
// all the windows in order to forward the event to each of them. However this and calling pushEvent
// in ApplicationDelegate is inherently unsafe in a multithreaded environment.
static std::set<OSXWindow *> &AllWindows()
{
    static angle::base::NoDestructor<std::set<OSXWindow *>> allWindows;
    return *allWindows;
}

@interface Application : NSApplication
@end

@implementation Application
- (void)sendEvent:(NSEvent *)nsEvent
{
    if ([nsEvent type] == NSApplicationDefined)
    {
        for (auto window : AllWindows())
        {
            if ([window->getNSWindow() windowNumber] == [nsEvent windowNumber])
            {
                Event event;
                event.Type = Event::EVENT_TEST;
                window->pushEvent(event);
            }
        }
    }
    [super sendEvent:nsEvent];
}

// Override internal method to try to diagnose unexpected crashes in Core Animation.
// anglebug.com/6570
// See also:
//   https://github.com/microsoft/appcenter-sdk-apple/issues/1944
//   https://stackoverflow.com/questions/220159/how-do-you-print-out-a-stack-trace-to-the-console-log-in-cocoa
- (void)_crashOnException:(NSException *)exception
{
    NSLog(@"*** OSXWindow aborting on exception:  <%@> %@", [exception name], [exception reason]);
    NSLog(@"%@", [exception callStackSymbols]);
    abort();
}
@end

// The Delegate receiving application-wide events.
@interface ApplicationDelegate : NSObject
@end

@implementation ApplicationDelegate
- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender
{
    Event event;
    event.Type = Event::EVENT_CLOSED;
    for (auto window : AllWindows())
    {
        window->pushEvent(event);
    }
    return NSTerminateCancel;
}
@end
static ApplicationDelegate *gApplicationDelegate = nil;

static bool InitializeAppKit()
{
    if (NSApp != nil)
    {
        return true;
    }

    // Initialize the global variable "NSApp"
    [Application sharedApplication];

    // Make us appear in the dock
    [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];

    // Register our global event handler
    gApplicationDelegate = [[ApplicationDelegate alloc] init];
    if (gApplicationDelegate == nil)
    {
        return false;
    }
    [NSApp setDelegate:static_cast<id>(gApplicationDelegate)];

    // Set our status to "started" so we are not bouncing in the doc and can activate
    [NSApp finishLaunching];
    return true;
}

// NS's and CG's coordinate systems start at the bottom left, while OSWindow's coordinate
// system starts at the top left. This function converts the Y coordinate accordingly.
static float YCoordToFromCG(float y)
{
    float screenHeight = CGDisplayBounds(CGMainDisplayID()).size.height;
    return screenHeight - y;
}

// Delegate for window-wide events, note that the protocol doesn't contain anything input related.
@implementation WindowDelegate
- (id)initWithWindow:(OSXWindow *)window
{
    self = [super init];
    if (self != nil)
    {
        mWindow = window;
    }
    return self;
}

- (void)onOSXWindowDeleted
{
    mWindow = nil;
}

- (BOOL)windowShouldClose:(id)sender
{
    Event event;
    event.Type = Event::EVENT_CLOSED;
    mWindow->pushEvent(event);
    return NO;
}

- (void)windowDidResize:(NSNotification *)notification
{
    NSSize windowSize = [[mWindow->getNSWindow() contentView] frame].size;
    Event event;
    event.Type        = Event::EVENT_RESIZED;
    event.Size.Width  = (int)windowSize.width;
    event.Size.Height = (int)windowSize.height;
    mWindow->pushEvent(event);
}

- (void)windowDidMove:(NSNotification *)notification
{
    NSRect screenspace = [mWindow->getNSWindow() frame];
    Event event;
    event.Type   = Event::EVENT_MOVED;
    event.Move.X = (int)screenspace.origin.x;
    event.Move.Y = (int)YCoordToFromCG(screenspace.origin.y + screenspace.size.height);
    mWindow->pushEvent(event);
}

- (void)windowDidBecomeKey:(NSNotification *)notification
{
    Event event;
    event.Type = Event::EVENT_GAINED_FOCUS;
    mWindow->pushEvent(event);
    [self retain];
}

- (void)windowDidResignKey:(NSNotification *)notification
{
    if (mWindow != nil)
    {
        Event event;
        event.Type = Event::EVENT_LOST_FOCUS;
        mWindow->pushEvent(event);
    }
    [self release];
}
@end

static Key NSCodeToKey(int keyCode)
{
    // Missing KEY_PAUSE
    switch (keyCode)
    {
        case kVK_Shift:
            return KEY_LSHIFT;
        case kVK_RightShift:
            return KEY_RSHIFT;
        case kVK_Option:
            return KEY_LALT;
        case kVK_RightOption:
            return KEY_RALT;
        case kVK_Control:
            return KEY_LCONTROL;
        case kVK_RightControl:
            return KEY_RCONTROL;
        case kVK_Command:
            return KEY_LSYSTEM;
        // Right System doesn't have a name, but shows up as 0x36.
        case 0x36:
            return KEY_RSYSTEM;
        case kVK_Function:
            return KEY_MENU;

        case kVK_ANSI_Semicolon:
            return KEY_SEMICOLON;
        case kVK_ANSI_Slash:
            return KEY_SLASH;
        case kVK_ANSI_Equal:
            return KEY_EQUAL;
        case kVK_ANSI_Minus:
            return KEY_DASH;
        case kVK_ANSI_LeftBracket:
            return KEY_LBRACKET;
        case kVK_ANSI_RightBracket:
            return KEY_RBRACKET;
        case kVK_ANSI_Comma:
            return KEY_COMMA;
        case kVK_ANSI_Period:
            return KEY_PERIOD;
        case kVK_ANSI_Backslash:
            return KEY_BACKSLASH;
        case kVK_ANSI_Grave:
            return KEY_TILDE;
        case kVK_Escape:
            return KEY_ESCAPE;
        case kVK_Space:
            return KEY_SPACE;
        case kVK_Return:
            return KEY_RETURN;
        case kVK_Delete:
            return KEY_BACK;
        case kVK_Tab:
            return KEY_TAB;
        case kVK_PageUp:
            return KEY_PAGEUP;
        case kVK_PageDown:
            return KEY_PAGEDOWN;
        case kVK_End:
            return KEY_END;
        case kVK_Home:
            return KEY_HOME;
        case kVK_Help:
            return KEY_INSERT;
        case kVK_ForwardDelete:
            return KEY_DELETE;
        case kVK_ANSI_KeypadPlus:
            return KEY_ADD;
        case kVK_ANSI_KeypadMinus:
            return KEY_SUBTRACT;
        case kVK_ANSI_KeypadMultiply:
            return KEY_MULTIPLY;
        case kVK_ANSI_KeypadDivide:
            return KEY_DIVIDE;

        case kVK_F1:
            return KEY_F1;
        case kVK_F2:
            return KEY_F2;
        case kVK_F3:
            return KEY_F3;
        case kVK_F4:
            return KEY_F4;
        case kVK_F5:
            return KEY_F5;
        case kVK_F6:
            return KEY_F6;
        case kVK_F7:
            return KEY_F7;
        case kVK_F8:
            return KEY_F8;
        case kVK_F9:
            return KEY_F9;
        case kVK_F10:
            return KEY_F10;
        case kVK_F11:
            return KEY_F11;
        case kVK_F12:
            return KEY_F12;
        case kVK_F13:
            return KEY_F13;
        case kVK_F14:
            return KEY_F14;
        case kVK_F15:
            return KEY_F15;

        case kVK_LeftArrow:
            return KEY_LEFT;
        case kVK_RightArrow:
            return KEY_RIGHT;
        case kVK_DownArrow:
            return KEY_DOWN;
        case kVK_UpArrow:
            return KEY_UP;

        case kVK_ANSI_Keypad0:
            return KEY_NUMPAD0;
        case kVK_ANSI_Keypad1:
            return KEY_NUMPAD1;
        case kVK_ANSI_Keypad2:
            return KEY_NUMPAD2;
        case kVK_ANSI_Keypad3:
            return KEY_NUMPAD3;
        case kVK_ANSI_Keypad4:
            return KEY_NUMPAD4;
        case kVK_ANSI_Keypad5:
            return KEY_NUMPAD5;
        case kVK_ANSI_Keypad6:
            return KEY_NUMPAD6;
        case kVK_ANSI_Keypad7:
            return KEY_NUMPAD7;
        case kVK_ANSI_Keypad8:
            return KEY_NUMPAD8;
        case kVK_ANSI_Keypad9:
            return KEY_NUMPAD9;

        case kVK_ANSI_A:
            return KEY_A;
        case kVK_ANSI_B:
            return KEY_B;
        case kVK_ANSI_C:
            return KEY_C;
        case kVK_ANSI_D:
            return KEY_D;
        case kVK_ANSI_E:
            return KEY_E;
        case kVK_ANSI_F:
            return KEY_F;
        case kVK_ANSI_G:
            return KEY_G;
        case kVK_ANSI_H:
            return KEY_H;
        case kVK_ANSI_I:
            return KEY_I;
        case kVK_ANSI_J:
            return KEY_J;
        case kVK_ANSI_K:
            return KEY_K;
        case kVK_ANSI_L:
            return KEY_L;
        case kVK_ANSI_M:
            return KEY_M;
        case kVK_ANSI_N:
            return KEY_N;
        case kVK_ANSI_O:
            return KEY_O;
        case kVK_ANSI_P:
            return KEY_P;
        case kVK_ANSI_Q:
            return KEY_Q;
        case kVK_ANSI_R:
            return KEY_R;
        case kVK_ANSI_S:
            return KEY_S;
        case kVK_ANSI_T:
            return KEY_T;
        case kVK_ANSI_U:
            return KEY_U;
        case kVK_ANSI_V:
            return KEY_V;
        case kVK_ANSI_W:
            return KEY_W;
        case kVK_ANSI_X:
            return KEY_X;
        case kVK_ANSI_Y:
            return KEY_Y;
        case kVK_ANSI_Z:
            return KEY_Z;

        case kVK_ANSI_1:
            return KEY_NUM1;
        case kVK_ANSI_2:
            return KEY_NUM2;
        case kVK_ANSI_3:
            return KEY_NUM3;
        case kVK_ANSI_4:
            return KEY_NUM4;
        case kVK_ANSI_5:
            return KEY_NUM5;
        case kVK_ANSI_6:
            return KEY_NUM6;
        case kVK_ANSI_7:
            return KEY_NUM7;
        case kVK_ANSI_8:
            return KEY_NUM8;
        case kVK_ANSI_9:
            return KEY_NUM9;
        case kVK_ANSI_0:
            return KEY_NUM0;
    }

    return Key(0);
}

static void AddNSKeyStateToEvent(Event *event, NSEventModifierFlags state)
{
    event->Key.Shift   = state & NSShiftKeyMask;
    event->Key.Control = state & NSControlKeyMask;
    event->Key.Alt     = state & NSAlternateKeyMask;
    event->Key.System  = state & NSCommandKeyMask;
}

static MouseButton TranslateMouseButton(NSInteger button)
{
    switch (button)
    {
        case 2:
            return MOUSEBUTTON_MIDDLE;
        case 3:
            return MOUSEBUTTON_BUTTON4;
        case 4:
            return MOUSEBUTTON_BUTTON5;
        default:
            return MOUSEBUTTON_UNKNOWN;
    }
}

// Delegate for NSView events, mostly the input events
@implementation ContentView
- (id)initWithWindow:(OSXWindow *)window
{
    self = [super init];
    if (self != nil)
    {
        mWindow          = window;
        mTrackingArea    = nil;
        mCurrentModifier = 0;
        [self updateTrackingAreas];
    }
    return self;
}

- (void)dealloc
{
    [mTrackingArea release];
    [super dealloc];
}

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

    NSRect bounds               = [self bounds];
    NSTrackingAreaOptions flags = NSTrackingMouseEnteredAndExited | NSTrackingActiveInKeyWindow |
                                  NSTrackingCursorUpdate | NSTrackingInVisibleRect |
                                  NSTrackingAssumeInside;
    mTrackingArea = [[NSTrackingArea alloc] initWithRect:bounds
                                                 options:flags
                                                   owner:self
                                                userInfo:nil];

    [self addTrackingArea:mTrackingArea];
    [super updateTrackingAreas];
}

// Helps with performance
- (BOOL)isOpaque
{
    return YES;
}

- (BOOL)canBecomeKeyView
{
    return YES;
}

- (BOOL)acceptsFirstResponder
{
    return YES;
}

// Handle mouse events from the NSResponder protocol
- (float)translateMouseY:(float)y
{
    return [self frame].size.height - y;
}

- (void)addButtonEvent:(NSEvent *)nsEvent
                  type:(Event::EventType)eventType
                button:(MouseButton)button
{
    Event event;
    event.Type               = eventType;
    event.MouseButton.Button = button;
    event.MouseButton.X      = (int)[nsEvent locationInWindow].x;
    event.MouseButton.Y      = (int)[self translateMouseY:[nsEvent locationInWindow].y];
    mWindow->pushEvent(event);
}

- (void)mouseDown:(NSEvent *)event
{
    [self addButtonEvent:event type:Event::EVENT_MOUSE_BUTTON_PRESSED button:MOUSEBUTTON_LEFT];
}

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

- (void)mouseUp:(NSEvent *)event
{
    [self addButtonEvent:event type:Event::EVENT_MOUSE_BUTTON_RELEASED button:MOUSEBUTTON_LEFT];
}

- (void)mouseMoved:(NSEvent *)nsEvent
{
    Event event;
    event.Type        = Event::EVENT_MOUSE_MOVED;
    event.MouseMove.X = (int)[nsEvent locationInWindow].x;
    event.MouseMove.Y = (int)[self translateMouseY:[nsEvent locationInWindow].y];
    mWindow->pushEvent(event);
}

- (void)mouseEntered:(NSEvent *)nsEvent
{
    Event event;
    event.Type = Event::EVENT_MOUSE_ENTERED;
    mWindow->pushEvent(event);
}

- (void)mouseExited:(NSEvent *)nsEvent
{
    Event event;
    event.Type = Event::EVENT_MOUSE_LEFT;
    mWindow->pushEvent(event);
}

- (void)rightMouseDown:(NSEvent *)event
{
    [self addButtonEvent:event type:Event::EVENT_MOUSE_BUTTON_PRESSED button:MOUSEBUTTON_RIGHT];
}

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

- (void)rightMouseUp:(NSEvent *)event
{
    [self addButtonEvent:event type:Event::EVENT_MOUSE_BUTTON_RELEASED button:MOUSEBUTTON_RIGHT];
}

- (void)otherMouseDown:(NSEvent *)event
{
    [self addButtonEvent:event
                    type:Event::EVENT_MOUSE_BUTTON_PRESSED
                  button:TranslateMouseButton([event buttonNumber])];
}

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

- (void)otherMouseUp:(NSEvent *)event
{
    [self addButtonEvent:event
                    type:Event::EVENT_MOUSE_BUTTON_RELEASED
                  button:TranslateMouseButton([event buttonNumber])];
}

- (void)scrollWheel:(NSEvent *)nsEvent
{
    if (static_cast<int>([nsEvent deltaY]) == 0)
    {
        return;
    }

    Event event;
    event.Type             = Event::EVENT_MOUSE_WHEEL_MOVED;
    event.MouseWheel.Delta = (int)[nsEvent deltaY];
    mWindow->pushEvent(event);
}

// Handle key events from the NSResponder protocol
- (void)keyDown:(NSEvent *)nsEvent
{
    // TODO(cwallez) also send text events
    Event event;
    event.Type     = Event::EVENT_KEY_PRESSED;
    event.Key.Code = NSCodeToKey([nsEvent keyCode]);
    AddNSKeyStateToEvent(&event, [nsEvent modifierFlags]);
    mWindow->pushEvent(event);
}

- (void)keyUp:(NSEvent *)nsEvent
{
    Event event;
    event.Type     = Event::EVENT_KEY_RELEASED;
    event.Key.Code = NSCodeToKey([nsEvent keyCode]);
    AddNSKeyStateToEvent(&event, [nsEvent modifierFlags]);
    mWindow->pushEvent(event);
}

// Modifier keys do not trigger keyUp/Down events but only flagsChanged events.
- (void)flagsChanged:(NSEvent *)nsEvent
{
    Event event;

    // Guess if the key has been pressed or released with the change of modifiers
    // It currently doesn't work when modifiers are unchanged, such as when pressing
    // both shift keys. GLFW has a solution for this but it requires tracking the
    // state of the keys. Implementing this is still TODO(cwallez)
    int modifier = [nsEvent modifierFlags] & NSDeviceIndependentModifierFlagsMask;
    if (modifier < mCurrentModifier)
    {
        event.Type = Event::EVENT_KEY_RELEASED;
    }
    else
    {
        event.Type = Event::EVENT_KEY_PRESSED;
    }
    mCurrentModifier = modifier;

    event.Key.Code = NSCodeToKey([nsEvent keyCode]);
    AddNSKeyStateToEvent(&event, [nsEvent modifierFlags]);
    mWindow->pushEvent(event);
}
@end

OSXWindow::OSXWindow() : mWindow(nil), mDelegate(nil), mView(nil) {}

OSXWindow::~OSXWindow()
{
    destroy();
}

bool OSXWindow::initializeImpl(const std::string &name, int width, int height)
{
    if (!InitializeAppKit())
    {
        return false;
    }

    unsigned int styleMask = NSTitledWindowMask | NSClosableWindowMask | NSResizableWindowMask |
                             NSMiniaturizableWindowMask;
    mWindow = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, width, height)
                                          styleMask:styleMask
                                            backing:NSBackingStoreBuffered
                                              defer:NO];

    if (mWindow == nil)
    {
        return false;
    }

    mDelegate = [[WindowDelegate alloc] initWithWindow:this];
    if (mDelegate == nil)
    {
        return false;
    }
    [mWindow setDelegate:static_cast<id>(mDelegate)];

    mView = [[ContentView alloc] initWithWindow:this];
    if (mView == nil)
    {
        return false;
    }
    [mView setWantsLayer:YES];

    // Disable scaling for this view. If scaling is enabled, the metal backend's
    // frame buffer's size will be this window's size multiplied by contentScale.
    // It will cause inconsistent testing & example apps' results.
    mView.layer.contentsScale = 1;

    [mWindow setContentView:mView];
    [mWindow setTitle:[NSString stringWithUTF8String:name.c_str()]];
    [mWindow setAcceptsMouseMovedEvents:YES];
    [mWindow center];

    [NSApp activateIgnoringOtherApps:YES];

    mX      = 0;
    mY      = 0;
    mWidth  = width;
    mHeight = height;

    AllWindows().insert(this);
    return true;
}

void OSXWindow::disableErrorMessageDialog() {}

void OSXWindow::destroy()
{
    AllWindows().erase(this);

    [mView release];
    mView = nil;
    [mDelegate onOSXWindowDeleted];
    [mDelegate release];
    mDelegate = nil;
    // NSWindow won't be completely released unless its content view is set to nil:
    [mWindow setContentView:nil];
    [mWindow release];
    mWindow = nil;
}

void OSXWindow::resetNativeWindow() {}

EGLNativeWindowType OSXWindow::getNativeWindow() const
{
    return [mView layer];
}

EGLNativeDisplayType OSXWindow::getNativeDisplay() const
{
    // TODO(cwallez): implement it once we have defined what EGLNativeDisplayType is
    return static_cast<EGLNativeDisplayType>(0);
}

void OSXWindow::messageLoop()
{
    @autoreleasepool
    {
        while (true)
        {
            // TODO(http://anglebug.com/6570): @try/@catch is a workaround for
            // exceptions being thrown from Cocoa-internal function
            // NS_setFlushesWithDisplayLink starting in macOS 11.
            @try
            {
                NSEvent *event = [NSApp nextEventMatchingMask:NSAnyEventMask
                                                    untilDate:[NSDate distantPast]
                                                       inMode:NSDefaultRunLoopMode
                                                      dequeue:YES];
                if (event == nil)
                {
                    break;
                }

                if ([event type] == NSAppKitDefined)
                {
                    continue;
                }
                [NSApp sendEvent:event];
            }
            @catch (NSException *localException)
            {
                NSLog(@"*** OSXWindow discarding exception: <%@> %@", [localException name],
                      [localException reason]);
            }
        }
    }
}

void OSXWindow::setMousePosition(int x, int y)
{
    y = (int)([mWindow frame].size.height) - y - 1;
    NSPoint screenspace;

#if MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_7
    screenspace = [mWindow convertBaseToScreen:NSMakePoint(x, y)];
#else
    screenspace = [mWindow convertRectToScreen:NSMakeRect(x, y, 0, 0)].origin;
#endif
    CGWarpMouseCursorPosition(CGPointMake(screenspace.x, YCoordToFromCG(screenspace.y)));
}

bool OSXWindow::setOrientation(int width, int height)
{
    UNIMPLEMENTED();
    return false;
}

bool OSXWindow::setPosition(int x, int y)
{
    // Given CG and NS's coordinate system, the "Y" position of a window is the Y coordinate
    // of the bottom of the window.
    int newBottom    = (int)([mWindow frame].size.height) + y;
    NSRect emptyRect = NSMakeRect(x, YCoordToFromCG(newBottom), 0, 0);
    [mWindow setFrameOrigin:[mWindow frameRectForContentRect:emptyRect].origin];
    return true;
}

bool OSXWindow::resize(int width, int height)
{
    [mWindow setContentSize:NSMakeSize(width, height)];
    return true;
}

void OSXWindow::setVisible(bool isVisible)
{
    if (isVisible)
    {
        [mWindow makeKeyAndOrderFront:nil];
    }
    else
    {
        [mWindow orderOut:nil];
    }
}

void OSXWindow::signalTestEvent()
{
    @autoreleasepool
    {
        NSEvent *event = [NSEvent otherEventWithType:NSApplicationDefined
                                            location:NSMakePoint(0, 0)
                                       modifierFlags:0
                                           timestamp:0.0
                                        windowNumber:[mWindow windowNumber]
                                             context:nil
                                             subtype:0
                                               data1:0
                                               data2:0];
        [NSApp postEvent:event atStart:YES];
    }
}

NSWindow *OSXWindow::getNSWindow() const
{
    return mWindow;
}

// static
OSWindow *OSWindow::New()
{
    return new OSXWindow;
}
