# ----------------------------------------------------------------------------
# pyglet
# Copyright (c) 2006-2008 Alex Holkner
# Copyright (c) 2008-2021 pyglet contributors
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
#  * Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
#  * Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in
#    the documentation and/or other materials provided with the
#    distribution.
#  * Neither the name of pyglet nor the names of its
#    contributors may be used to endorse or promote products
#    derived from this software without specific prior written
#    permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
# ----------------------------------------------------------------------------

from ctypes import *

import pyglet
from pyglet.window import BaseWindow
from pyglet.window import MouseCursor, DefaultMouseCursor
from pyglet.event import EventDispatcher

from pyglet.canvas.cocoa import CocoaCanvas

from pyglet.libs.darwin import cocoapy, CGPoint

from .systemcursor import SystemCursor
from .pyglet_delegate import PygletDelegate
from .pyglet_window import PygletWindow, PygletToolWindow
from .pyglet_view import PygletView

NSApplication = cocoapy.ObjCClass('NSApplication')
NSCursor = cocoapy.ObjCClass('NSCursor')
NSAutoreleasePool = cocoapy.ObjCClass('NSAutoreleasePool')
NSColor = cocoapy.ObjCClass('NSColor')
NSEvent = cocoapy.ObjCClass('NSEvent')
NSImage = cocoapy.ObjCClass('NSImage')

quartz = cocoapy.quartz
cf = cocoapy.cf


class CocoaMouseCursor(MouseCursor):
    gl_drawable = False

    def __init__(self, cursorName):
        # cursorName is a string identifying one of the named default NSCursors
        # e.g. 'pointingHandCursor', and can be sent as message to NSCursor class.
        self.cursorName = cursorName

    def set(self):
        cursor = getattr(NSCursor, self.cursorName)()
        cursor.set()


class CocoaWindow(BaseWindow):

    # NSWindow instance.
    _nswindow = None

    # Delegate object.
    _delegate = None

    # Window properties
    _mouse_platform_visible = True
    _mouse_ignore_motion = False

    # Flag set during close() method.
    _was_closed = False

    # NSWindow style masks.
    _style_masks = {
        BaseWindow.WINDOW_STYLE_DEFAULT:    cocoapy.NSTitledWindowMask |
                                            cocoapy.NSClosableWindowMask |
                                            cocoapy.NSMiniaturizableWindowMask,
        BaseWindow.WINDOW_STYLE_DIALOG:     cocoapy.NSTitledWindowMask |
                                            cocoapy.NSClosableWindowMask,
        BaseWindow.WINDOW_STYLE_TOOL:       cocoapy.NSTitledWindowMask |
                                            cocoapy.NSClosableWindowMask |
                                            cocoapy.NSUtilityWindowMask,
        BaseWindow.WINDOW_STYLE_BORDERLESS: cocoapy.NSBorderlessWindowMask,
    }

    def _recreate(self, changes):
        if 'context' in changes:
            self.context.set_current()

        if 'fullscreen' in changes:
            if not self._fullscreen:  # leaving fullscreen
                self.screen.release_display()

        self._create()

    def _create(self):
        # Create a temporary autorelease pool for this method.
        pool = NSAutoreleasePool.alloc().init()

        if self._nswindow:
            # The window is about the be recreated so destroy everything
            # associated with the old window, then destroy the window itself.
            nsview = self.canvas.nsview
            self.canvas = None
            self._nswindow.orderOut_(None)
            self._nswindow.close()
            self.context.detach()
            self._nswindow.release()
            self._nswindow = None
            nsview.release()
            self._delegate.release()
            self._delegate = None

        # Determine window parameters.
        content_rect = cocoapy.NSMakeRect(0, 0, self._width, self._height)
        WindowClass = PygletWindow
        if self._fullscreen:
            style_mask = cocoapy.NSBorderlessWindowMask
        else:
            if self._style not in self._style_masks:
                self._style = self.WINDOW_STYLE_DEFAULT
            style_mask = self._style_masks[self._style]
            if self._resizable:
                style_mask |= cocoapy.NSResizableWindowMask
            if self._style == BaseWindow.WINDOW_STYLE_TOOL:
                WindowClass = PygletToolWindow

        # First create an instance of our NSWindow subclass.

        # FIX ME:
        # Need to use this initializer to have any hope of multi-monitor support.
        # But currently causes problems on Mac OS X Lion.  So for now, we initialize the
        # window without including screen information.
        #
        # self._nswindow = WindowClass.alloc().initWithContentRect_styleMask_backing_defer_screen_(
        #     content_rect,           # contentRect
        #     style_mask,             # styleMask
        #     NSBackingStoreBuffered, # backing
        #     False,                  # defer
        #     self.screen.get_nsscreen())  # screen

        self._nswindow = WindowClass.alloc().initWithContentRect_styleMask_backing_defer_(
            content_rect,                    # contentRect
            style_mask,                      # styleMask
            cocoapy.NSBackingStoreBuffered,  # backing
            False)                           # defer

        if self._fullscreen:
            # BUG: I suspect that this doesn't do the right thing when using
            # multiple monitors (which would be to go fullscreen on the monitor
            # where the window is located).  However I've no way to test.
            blackColor = NSColor.blackColor()
            self._nswindow.setBackgroundColor_(blackColor)
            self._nswindow.setOpaque_(True)
            self.screen.capture_display()
            self._nswindow.setLevel_(quartz.CGShieldingWindowLevel())
            self.context.set_full_screen()
            self._center_window()
            self._mouse_in_window = True
        else:
            self._set_nice_window_location()
            self._mouse_in_window = self._mouse_in_content_rect()

        # Then create a view and set it as our NSWindow's content view.
        self._nsview = PygletView.alloc().initWithFrame_cocoaWindow_(content_rect, self)
        self._nswindow.setContentView_(self._nsview)
        self._nswindow.makeFirstResponder_(self._nsview)

        # Create a canvas with the view as its drawable and attach context to it.
        self.canvas = CocoaCanvas(self.display, self.screen, self._nsview)
        self.context.attach(self.canvas)

        # Configure the window.
        self._nswindow.setAcceptsMouseMovedEvents_(True)
        self._nswindow.setReleasedWhenClosed_(False)
        self._nswindow.useOptimizedDrawing_(True)
        self._nswindow.setPreservesContentDuringLiveResize_(False)

        # Set the delegate.
        self._delegate = PygletDelegate.alloc().initWithWindow_(self)

        # Configure CocoaWindow.
        self.set_caption(self._caption)
        if self._minimum_size is not None:
            self.set_minimum_size(*self._minimum_size)
        if self._maximum_size is not None:
            self.set_maximum_size(*self._maximum_size)

        # TODO: Add support for file drops.
        if self._file_drops:
            raise NotImplementedError("File drops are not implemented on MacOS")

        self.context.update_geometry()
        self.switch_to()
        self.set_vsync(self._vsync)
        self.set_visible(self._visible)

        pool.drain()

    def _set_nice_window_location(self):
        # Construct a list of all visible windows that aren't us.
        visible_windows = [win for win in pyglet.app.windows if
                           win is not self and
                           win._nswindow and
                           win._nswindow.isVisible()]
        # If there aren't any visible windows, then center this window.
        if not visible_windows:
            self._center_window()
        # Otherwise, cascade from last window in list.
        else:
            point = visible_windows[-1]._nswindow.cascadeTopLeftFromPoint_(cocoapy.NSZeroPoint)
            self._nswindow.cascadeTopLeftFromPoint_(point)

    def _center_window(self):
        # [NSWindow center] does not move the window to a true center position
        # and also always moves the window to the main display.
        x = self.screen.x + int((self.screen.width - self._width) // 2)
        y = self.screen.y + int((self.screen.height - self._height) // 2)
        self._nswindow.setFrameOrigin_(cocoapy.NSPoint(x, y))

    def close(self):
        # If we've already gone through this once, don't do it again.
        if self._was_closed:
            return

        # Create a temporary autorelease pool for this method.
        pool = NSAutoreleasePool.new()

        # Restore cursor visibility
        self.set_mouse_platform_visible(True)
        self.set_exclusive_mouse(False)
        self.set_exclusive_keyboard(False)

        # Remove the delegate object
        if self._delegate:
            self._nswindow.setDelegate_(None)
            self._delegate.release()
            self._delegate = None

        # Remove window from display and remove its view.
        if self._nswindow:
            self._nswindow.orderOut_(None)
            self._nswindow.setContentView_(None)
            self._nswindow.close()

        # Restore screen mode. This also releases the display
        # if it was captured for fullscreen mode.
        self.screen.restore_mode()

        # Remove view from canvas and then remove canvas.
        if self.canvas:
            self.canvas.nsview.release()
            self.canvas.nsview = None
            self.canvas = None

        # Do this last, so that we don't see white flash
        # when exiting application from fullscreen mode.
        super(CocoaWindow, self).close()

        self._was_closed = True
        pool.drain()

    def switch_to(self):
        if self.context:
            self.context.set_current()

    def flip(self):
        self.draw_mouse_cursor()
        if self.context:
            self.context.flip()

    def dispatch_events(self):
        self._allow_dispatch_event = True
        # Process all pyglet events.
        self.dispatch_pending_events()
        event = True

        # Dequeue and process all of the pending Cocoa events.
        pool = NSAutoreleasePool.new()
        NSApp = NSApplication.sharedApplication()
        while event and self._nswindow and self._context:
            event = NSApp.nextEventMatchingMask_untilDate_inMode_dequeue_(
                cocoapy.NSAnyEventMask, None, cocoapy.NSEventTrackingRunLoopMode, True)

            if event:
                event_type = event.type()
                # Pass on all events.
                NSApp.sendEvent_(event)
                # And resend key events to special handlers.
                if event_type == cocoapy.NSKeyDown and not event.isARepeat():
                    NSApp.sendAction_to_from_(cocoapy.get_selector('pygletKeyDown:'), None, event)
                elif event_type == cocoapy.NSKeyUp:
                    NSApp.sendAction_to_from_(cocoapy.get_selector('pygletKeyUp:'), None, event)
                elif event_type == cocoapy.NSFlagsChanged:
                    NSApp.sendAction_to_from_(cocoapy.get_selector('pygletFlagsChanged:'), None, event)
                NSApp.updateWindows()

        pool.drain()

        self._allow_dispatch_event = False

    def dispatch_pending_events(self):
        while self._event_queue:
            event = self._event_queue.pop(0)
            EventDispatcher.dispatch_event(self, *event)

    def set_caption(self, caption):
        self._caption = caption
        if self._nswindow is not None:
            self._nswindow.setTitle_(cocoapy.get_NSString(caption))

    def set_icon(self, *images):
        # Only use the biggest image from the list.
        max_image = images[0]
        for img in images:
            if img.width > max_image.width and img.height > max_image.height:
                max_image = img

        # Grab image data from pyglet image.
        image = max_image.get_image_data()
        format = 'ARGB'
        bytesPerRow = len(format) * image.width
        data = image.get_data(format, -bytesPerRow)

        # Use image data to create a data provider.
        # Using CGDataProviderCreateWithData crashes PyObjC 2.2b3, so we create
        # a CFDataRef object first and use it to create the data provider.
        cfdata = c_void_p(cf.CFDataCreate(None, data, len(data)))
        provider = c_void_p(quartz.CGDataProviderCreateWithCFData(cfdata))

        colorSpace = c_void_p(quartz.CGColorSpaceCreateDeviceRGB())

        # Then create a CGImage from the provider.
        cgimage = c_void_p(quartz.CGImageCreate(
            image.width, image.height, 8, 32, bytesPerRow,
            colorSpace,
            cocoapy.kCGImageAlphaFirst,
            provider,
            None,
            True,
            cocoapy.kCGRenderingIntentDefault))

        if not cgimage:
            return

        cf.CFRelease(cfdata)
        quartz.CGDataProviderRelease(provider)
        quartz.CGColorSpaceRelease(colorSpace)

        # Turn the CGImage into an NSImage.
        size = cocoapy.NSMakeSize(image.width, image.height)
        nsimage = NSImage.alloc().initWithCGImage_size_(cgimage, size)
        if not nsimage:
            return

        # And finally set the app icon.
        NSApp = NSApplication.sharedApplication()
        NSApp.setApplicationIconImage_(nsimage)
        nsimage.release()

    def get_location(self):
        window_frame = self._nswindow.frame()
        rect = self._nswindow.contentRectForFrameRect_(window_frame)
        screen_frame = self._nswindow.screen().frame()
        screen_width = int(screen_frame.size.width)
        screen_height = int(screen_frame.size.height)
        return int(rect.origin.x), int(screen_height - rect.origin.y - rect.size.height)

    def set_location(self, x, y):
        window_frame = self._nswindow.frame()
        rect = self._nswindow.contentRectForFrameRect_(window_frame)
        screen_frame = self._nswindow.screen().frame()
        screen_width = int(screen_frame.size.width)
        screen_height = int(screen_frame.size.height)
        origin = cocoapy.NSPoint(x, screen_height - y - rect.size.height)
        self._nswindow.setFrameOrigin_(origin)

    def get_framebuffer_size(self):
        view = self.context._nscontext.view()
        bounds = view.convertRectToBacking_(view.bounds()).size
        return int(bounds.width), int(bounds.height)

    def set_size(self, width: int, height: int) -> None:
        super().set_size(width, height)
        # Move frame origin down so that top-left corner of window doesn't move.
        window_frame = self._nswindow.frame()
        rect = self._nswindow.contentRectForFrameRect_(window_frame)
        rect.origin.y += rect.size.height - self._height
        rect.size.width = self._width
        rect.size.height = self._height
        new_frame = self._nswindow.frameRectForContentRect_(rect)
        # The window background flashes when the frame size changes unless it's
        # animated, but we can set the window's animationResizeTime to zero.
        is_visible = self._nswindow.isVisible()
        self._nswindow.setFrame_display_animate_(new_frame, True, is_visible)

    def set_minimum_size(self, width: int, height: int) -> None:
        super().set_minimum_size(width, height)

        if self._nswindow is not None:
            ns_minimum_size = cocoapy.NSSize(*self._minimum_size)
            self._nswindow.setContentMinSize_(ns_minimum_size)

    def set_maximum_size(self, width: int, height: int) -> None:
        super().set_maximum_size(width, height)

        if self._nswindow is not None:
            ns_maximum_size = cocoapy.NSSize(*self._maximum_size)
            self._nswindow.setContentMaxSize_(ns_maximum_size)

    def activate(self):
        if self._nswindow is not None:
            NSApp = NSApplication.sharedApplication()
            NSApp.activateIgnoringOtherApps_(True)
            self._nswindow.makeKeyAndOrderFront_(None)

    def set_visible(self, visible: bool = True) -> None:
        super().set_visible(visible)

        if self._nswindow is not None:
            if visible:
                # Not really sure why on_resize needs to be here,
                # but it's what pyglet wants.
                self.dispatch_event('on_resize', self._width, self._height)
                self.dispatch_event('on_show')
                self.dispatch_event('on_expose')
                self._nswindow.makeKeyAndOrderFront_(None)
            else:
                self._nswindow.orderOut_(None)

    def minimize(self):
        self._mouse_in_window = False
        if self._nswindow is not None:
            self._nswindow.miniaturize_(None)

    def maximize(self):
        if self._nswindow is not None:
            self._nswindow.zoom_(None)

    def set_vsync(self, vsync: bool) -> None:
        if pyglet.options['vsync'] is not None:
            vsync = pyglet.options['vsync']

        super().set_vsync(vsync)
        self.context.set_vsync(vsync)

    def _mouse_in_content_rect(self):
        # Returns true if mouse is inside the window's content rectangle.
        # Better to use this method to check manually rather than relying
        # on instance variables that may not be set correctly.
        point = NSEvent.mouseLocation()
        window_frame = self._nswindow.frame()
        rect = self._nswindow.contentRectForFrameRect_(window_frame)
        return cocoapy.foundation.NSMouseInRect(point, rect, False)

    def set_mouse_platform_visible(self, platform_visible=None):
        # When the platform_visible argument is supplied with a boolean, then this
        # method simply sets whether or not the platform mouse cursor is visible.
        if platform_visible is not None:
            if platform_visible:
                SystemCursor.unhide()
            else:
                SystemCursor.hide()
        # But if it has been called without an argument, it turns into
        # a completely different function.  Now we are trying to figure out
        # whether or not the mouse *should* be visible, and if so, what it should
        # look like.
        else:
            # If we are in mouse exclusive mode, then hide the mouse cursor.
            if self._mouse_exclusive:
                SystemCursor.hide()
            # If we aren't inside the window, then always show the mouse
            # and make sure that it is the default cursor.
            elif not self._mouse_in_content_rect():
                NSCursor.arrowCursor().set()
                SystemCursor.unhide()
            # If we are in the window, then what we do depends on both
            # the current pyglet-set visibility setting for the mouse and
            # the type of the mouse cursor.  If the cursor has been hidden
            # in the window with set_mouse_visible() then don't show it.
            elif not self._mouse_visible:
                SystemCursor.hide()
            # If the mouse is set as a system-defined cursor, then we
            # need to set the cursor and show the mouse.
            # *** FIX ME ***
            elif isinstance(self._mouse_cursor, CocoaMouseCursor):
                self._mouse_cursor.set()
                SystemCursor.unhide()
            # If the mouse cursor is OpenGL drawable, then it we need to hide
            # the system mouse cursor, so that the cursor can draw itself.
            elif self._mouse_cursor.gl_drawable:
                SystemCursor.hide()
            # Otherwise, show the default cursor.
            else:
                NSCursor.arrowCursor().set()
                SystemCursor.unhide()

    def get_system_mouse_cursor(self, name):
        # It would make a lot more sense for most of this code to be
        # inside the CocoaMouseCursor class, but all of the CURSOR_xxx
        # constants are defined as properties of BaseWindow.
        if name == self.CURSOR_DEFAULT:
            return DefaultMouseCursor()
        cursors = {
            self.CURSOR_CROSSHAIR:       'crosshairCursor',
            self.CURSOR_HAND:            'pointingHandCursor',
            self.CURSOR_HELP:            'arrowCursor',
            self.CURSOR_NO:              'operationNotAllowedCursor',  # Mac OS 10.6
            self.CURSOR_SIZE:            'arrowCursor',
            self.CURSOR_SIZE_UP:         'resizeUpCursor',
            self.CURSOR_SIZE_UP_RIGHT:   'arrowCursor',
            self.CURSOR_SIZE_RIGHT:      'resizeRightCursor',
            self.CURSOR_SIZE_DOWN_RIGHT: 'arrowCursor',
            self.CURSOR_SIZE_DOWN:       'resizeDownCursor',
            self.CURSOR_SIZE_DOWN_LEFT:  'arrowCursor',
            self.CURSOR_SIZE_LEFT:       'resizeLeftCursor',
            self.CURSOR_SIZE_UP_LEFT:    'arrowCursor',
            self.CURSOR_SIZE_UP_DOWN:    'resizeUpDownCursor',
            self.CURSOR_SIZE_LEFT_RIGHT: 'resizeLeftRightCursor',
            self.CURSOR_TEXT:            'IBeamCursor',
            self.CURSOR_WAIT:            'arrowCursor',  # No wristwatch cursor in Cocoa
            self.CURSOR_WAIT_ARROW:      'arrowCursor',  # No wristwatch cursor in Cocoa
            }
        if name not in cursors:
            raise RuntimeError('Unknown cursor name "%s"' % name)
        return CocoaMouseCursor(cursors[name])

    def set_mouse_position(self, x, y, absolute=False):
        if absolute:
            # If absolute, then x, y is given in global display coordinates
            # which sets (0,0) at top left corner of main display.  It is possible
            # to warp the mouse position to a point inside of another display.
            quartz.CGWarpMouseCursorPosition(CGPoint(x,y))
        else:
            # Window-relative coordinates: (x, y) are given in window coords
            # with (0,0) at bottom-left corner of window and y up.  We find
            # which display the window is in and then convert x, y into local
            # display coords where (0,0) is now top-left of display and y down.
            screenInfo = self._nswindow.screen().deviceDescription()
            displayID = screenInfo.objectForKey_(cocoapy.get_NSString('NSScreenNumber'))
            displayID = displayID.intValue()
            displayBounds = quartz.CGDisplayBounds(displayID)
            frame = self._nswindow.frame()
            windowOrigin = frame.origin
            x += windowOrigin.x
            y = displayBounds.size.height - windowOrigin.y - y
            quartz.CGDisplayMoveCursorToPoint(displayID, cocoapy.NSPoint(x, y))

    def set_exclusive_mouse(self, exclusive=True):
        super().set_exclusive_mouse(exclusive)
        if exclusive:
            # Skip the next motion event, which would return a large delta.
            self._mouse_ignore_motion = True
            # Move mouse to center of window.
            frame = self._nswindow.frame()
            width, height = frame.size.width, frame.size.height
            self.set_mouse_position(width/2, height/2)
            quartz.CGAssociateMouseAndMouseCursorPosition(False)
        else:
            quartz.CGAssociateMouseAndMouseCursorPosition(True)

        # Update visibility of mouse cursor.
        self.set_mouse_platform_visible()

    def set_exclusive_keyboard(self, exclusive=True):
        # http://developer.apple.com/mac/library/technotes/tn2002/tn2062.html
        # http://developer.apple.com/library/mac/#technotes/KioskMode/

        # BUG: System keys like F9 or command-tab are disabled, however
        # pyglet also does not receive key press events for them.

        # This flag is queried by window delegate to determine whether
        # the quit menu item is active.
        super().set_exclusive_keyboard(exclusive)

        if exclusive:
            # "Be nice! Don't disable force-quit!"
            #          -- Patrick Swayze, Road House (1989)
            options = cocoapy.NSApplicationPresentationHideDock | \
                      cocoapy.NSApplicationPresentationHideMenuBar | \
                      cocoapy.NSApplicationPresentationDisableProcessSwitching | \
                      cocoapy.NSApplicationPresentationDisableHideApplication
        else:
            options = cocoapy.NSApplicationPresentationDefault

        NSApp = NSApplication.sharedApplication()
        NSApp.setPresentationOptions_(options)
