/*
 * Copyright (c) 2011, Oracle and/or its affiliates. All rights reserved.
 */

#import "GlassView3D.h"

#import "common.h"
#import "com_sun_glass_events_DndEvent.h"
#import "com_sun_glass_events_KeyEvent.h"
#import "com_sun_glass_events_MouseEvent.h"
#import "com_sun_glass_ui_View_Capability.h"
#import "com_sun_glass_ui_mac_MacGestureSupport.h"

#import "GlassMacros.h"
#import "GlassApplication.h"
#import "GlassLayer3D.h"

//#define VERBOSE
#ifndef VERBOSE
    #define LOG(MSG, ...)
#else
    #define LOG(MSG, ...) GLASS_LOG(MSG, ## __VA_ARGS__);
#endif

//#define MOUSEVERBOSE
#ifndef MOUSEVERBOSE
    #define MOUSELOG(MSG, ...)
#else
    #define MOUSELOG(MSG, ...) GLASS_LOG(MSG, ## __VA_ARGS__);
#endif

//#define DNDVERBOSE
#ifndef DNDVERBOSE
    #define DNDLOG(MSG, ...)
#else
    #define DNDLOG(MSG, ...) GLASS_LOG(MSG, ## __VA_ARGS__);
#endif

#define SHARE_GL_CONTEXT
//#define DEBUG_COLORS

// http://developer.apple.com/library/mac/#technotes/tn2085/_index.html
//#define ENABLE_MULTITHREADED_GL

@implementation GlassView3D

static inline NSString* getNSString(JNIEnv* env, jstring jstring)
{
    NSString *string = @"";
    if (jstring != NULL)
    {
        const jchar* jstrChars = (*env)->GetStringChars(env, jstring, NULL);
        string = [[[NSString alloc] initWithCharacters: jstrChars length: (*env)->GetStringLength(env, jstring)] autorelease];
        (*env)->ReleaseStringChars(env, jstring, jstrChars);
    }
    return string;
}

- (CGLPixelFormatObj)_createPixelFormatWithDepth:(CGLPixelFormatAttribute)depth
{
    CGLPixelFormatObj pix = NULL;
    {
        const CGLPixelFormatAttribute attributes[] =
        {
            kCGLPFAAccelerated,
            kCGLPFAColorSize, 32,
            kCGLPFAAlphaSize, 8,
            kCGLPFADepthSize, depth,
            (CGLPixelFormatAttribute)0
        };
        GLint npix = 0;
        CGLError err = CGLChoosePixelFormat(attributes, &pix, &npix);
        if (err != kCGLNoError)
        {
            NSLog(@"CGLChoosePixelFormat error: %d", err);
        }
    }
    return pix;
}

- (CGLContextObj)_createContextWithShared:(CGLContextObj)share withFormat:(CGLPixelFormatObj)format
{
    CGLContextObj ctx = NULL;
    {
        CGLError err = CGLCreateContext(format, share, &ctx);
        if (err != kCGLNoError)
        {
            NSLog(@"CGLCreateContext error: %d", err);
        }
    }
    return ctx;
}

- (id)initWithFrame:(NSRect)frame withJview:(jobject)jView withJproperties:(jobject)jproperties
{
    self = [super initWithFrame:frame pixelFormat:[NSOpenGLView defaultPixelFormat]];
    if (self != nil)
    {
        GET_MAIN_JENV;
        jmethodID initMethod = (*env)->GetMethodID(env, jIntegerClass, "<init>", "(I)V");
        jobject k3dDepthKey = (*env)->NewObject(env, jIntegerClass, initMethod, com_sun_glass_ui_View_Capability_k3dDepthKeyValue);
        jobject k3dDepthKeyValue = (*env)->CallObjectMethod(env, jproperties, jMapGetMethod, k3dDepthKey);
        
        int depthBits = 0;
        if (k3dDepthKeyValue != NULL)
        {
            depthBits = (*env)->CallIntMethod(env, k3dDepthKeyValue, jIntegerValueMethod);
        }
        
        NSOpenGLContext *share = [GlassApplication getShareContext];
        
        // TODO: client should be providing the format and context here RT-17546
        CGLPixelFormatObj clientFormat = [self _createPixelFormatWithDepth:depthBits];
        CGLContextObj clientContext = [self _createContextWithShared:[share CGLContextObj] withFormat:clientFormat];
        {
            // the offscreen is the buffered context (ex. Fbo, PBuffer) that the client can draw into at any time
            self->_offscreen = [[GlassOffscreen alloc] init];
            
            // https://developer.apple.com/library/mac/documentation/Cocoa/Reference/ApplicationKit/Classes/nsview_Class/Reference/NSView.html#//apple_ref/occ/instm/NSView/setWantsLayer:
            // the order of the following 2 calls is important: here we indicate we want a layer-hosting view
            {
                [self setLayer:[[GlassLayer3D alloc] initWithContext:clientContext withFormat:clientFormat withOffscreen:self->_offscreen]];
                [self setWantsLayer:YES];
            }
            
            self->_context = [self _createContextWithShared:[share CGLContextObj] withFormat:clientFormat];
        }
        CGLSetCurrentContext(clientContext); // remove after RT-17546
        
        self->_delegate = [[GlassViewDelegate alloc] initWithView:self withJview:jView];
        self->_drawCounter = 0;
        self->_lockCounter = 0;
        self->_ready = NO;
        self->_synced = NO;
        {
            jobject jSyncKey = (*env)->NewObject(env, jIntegerClass, initMethod, com_sun_glass_ui_View_Capability_kSyncKeyValue);
            jobject jSyncKeyValue = (*env)->CallObjectMethod(env, jproperties, jMapGetMethod, jSyncKey);
            if (jSyncKeyValue != NULL)
            {
                self->_synced = (*env)->CallBooleanMethod(env, jSyncKeyValue, jBooleanValueMethod);
            }
        }
        self->_trackingArea = [[NSTrackingArea alloc] initWithRect:frame
                                                          options:(NSTrackingMouseMoved | NSTrackingActiveAlways | NSTrackingInVisibleRect)
                                                            owner:self userInfo:nil];
        [self addTrackingArea: self->_trackingArea];
    }
    return self;
}

- (void)dealloc
{
    [self->_delegate release];
    self->_delegate = nil;

    [self removeTrackingArea: self->_trackingArea];
    [self->_trackingArea release];
    self->_trackingArea = nil;
    
    [self->_offscreen release];
    self->_offscreen = nil;
    
    CGLReleaseContext(self->_context);
    self->_context = NULL;
    
    [super dealloc];
}

- (BOOL)becomeFirstResponder
{
    return YES;
}

- (BOOL)acceptsFirstResponder
{
    return YES;
}

- (BOOL)canBecomeKeyView
{
    return YES;
}

- (BOOL)postsBoundsChangedNotifications
{
    return NO;
}

- (BOOL)postsFrameChangedNotifications
{
    return NO;
}

- (BOOL)acceptsFirstMouse:(NSEvent *)theEvent
{
    return YES;
}

- (BOOL)isFlipped
{
    return YES;
}

- (BOOL)isOpaque
{
    return NO;
}

- (BOOL)mouseDownCanMoveWindow
{
    return NO;
}

// also called when closing window, when [self window] == nil
- (void)viewDidMoveToWindow
{
    if ([self window] != nil)
    {
        [self->_offscreen setBackgroundColor:[[[self window] backgroundColor] colorUsingColorSpaceName:NSDeviceRGBColorSpace]];
    }
    
    [self->_delegate viewDidMoveToWindow];
}

- (void)setFrameOrigin:(NSPoint)newOrigin
{
    [super setFrameOrigin:newOrigin];
    [self->_delegate setFrameOrigin:newOrigin];
}

- (void)setFrameSize:(NSSize)newSize
{
    [super setFrameSize:newSize];
    [self->_delegate setFrameSize:newSize];
}

- (void)setFrame:(NSRect)frameRect
{
    [super setFrame:frameRect];
    [self->_delegate setFrame:frameRect];
}

- (void)updateTrackingAreas
{
    [super updateTrackingAreas];
    [self->_delegate updateTrackingAreas];
}

- (NSMenu *)menuForEvent:(NSEvent *)theEvent
{
    [self->_delegate sendJavaMenuEvent:theEvent];
    return [super menuForEvent: theEvent];
}

- (void)mouseEntered:(NSEvent *)theEvent
{
    MOUSELOG("mouseEntered");
    [self->_delegate sendJavaMouseEvent:theEvent];
}

- (void)mouseMoved:(NSEvent *)theEvent
{
    MOUSELOG("mouseMoved");
    [self->_delegate sendJavaMouseEvent:theEvent];
}

- (void)mouseExited:(NSEvent *)theEvent
{
    MOUSELOG("mouseExited");
    [self->_delegate sendJavaMouseEvent:theEvent];
}

- (void)mouseDown:(NSEvent *)theEvent
{
    MOUSELOG("mouseDown");
    [self->_delegate sendJavaMouseEvent:theEvent];
}

- (void)mouseDragged:(NSEvent *)theEvent
{
    MOUSELOG("mouseDragged");
    [self->_delegate sendJavaMouseEvent:theEvent];
}

- (void)mouseUp:(NSEvent *)theEvent
{
    MOUSELOG("mouseUp");
    [self->_delegate sendJavaMouseEvent:theEvent];
}

- (void)rightMouseDown:(NSEvent *)theEvent
{
    MOUSELOG("rightMouseDown");
    [self->_delegate sendJavaMouseEvent:theEvent];
    // By default, calling rightMouseDown: generates menuForEvent: but none of the other glass mouse handlers call the super
    // To be consistent with the rest of glass, call the menu event handler directly rather than letting the operating system do it
    [self->_delegate sendJavaMenuEvent:theEvent];
//    return [super rightMouseDown: theEvent];
}

- (void)rightMouseDragged:(NSEvent *)theEvent
{
    MOUSELOG("rightMouseDragged");
    [self->_delegate sendJavaMouseEvent:theEvent];
}

- (void)rightMouseUp:(NSEvent *)theEvent
{
    MOUSELOG("rightMouseUp");
    [self->_delegate sendJavaMouseEvent:theEvent];
}

- (void)otherMouseDown:(NSEvent *)theEvent
{
    MOUSELOG("otherMouseDown");
    [self->_delegate sendJavaMouseEvent:theEvent];
}

- (void)otherMouseDragged:(NSEvent *)theEvent
{
    MOUSELOG("otherMouseDragged");
    [self->_delegate sendJavaMouseEvent:theEvent];
}

- (void)otherMouseUp:(NSEvent *)theEvent
{
    MOUSELOG("otherMouseUp");
    [self->_delegate sendJavaMouseEvent:theEvent];
}

- (void)rotateWithEvent:(NSEvent *)theEvent
{
    [self->_delegate sendJavaGestureEvent:theEvent type:com_sun_glass_ui_mac_MacGestureSupport_GESTURE_ROTATE];
}

- (void)swipeWithEvent:(NSEvent *)theEvent
{
    [self->_delegate sendJavaGestureEvent:theEvent type:com_sun_glass_ui_mac_MacGestureSupport_GESTURE_SWIPE];
}

- (void)magnifyWithEvent:(NSEvent *)theEvent
{
    [self->_delegate sendJavaGestureEvent:theEvent type:com_sun_glass_ui_mac_MacGestureSupport_GESTURE_MAGNIFY];
}

- (void)endGestureWithEvent:(NSEvent *)theEvent
{
    [self->_delegate sendJavaGestureEndEvent:theEvent];
}

- (void)touchesBeganWithEvent:(NSEvent *)theEvent
{
    [self->_delegate sendJavaTouchEvent:theEvent];
}

- (void)touchesCancelledWithEvent:(NSEvent *)theEvent
{
    [self->_delegate sendJavaTouchEvent:theEvent];
}

- (void)touchesEndedWithEvent:(NSEvent *)theEvent
{
    [self->_delegate sendJavaTouchEvent:theEvent];
}

- (void)touchesMovedWithEvent:(NSEvent *)theEvent
{
    [self->_delegate sendJavaTouchEvent:theEvent];
}

- (void)scrollWheel:(NSEvent *)theEvent
{
    MOUSELOG("scrollWheel");
    [self->_delegate sendJavaMouseEvent:theEvent];
}

- (BOOL)performKeyEquivalent:(NSEvent *)theEvent
{
    [self->_delegate sendJavaKeyEvent:theEvent isDown:YES];
    return NO; // return NO to allow system-default processing of Cmd+Q, etc.
}

- (void)keyDown:(NSEvent *)theEvent
{
    [self->_delegate sendJavaKeyEvent:theEvent isDown:YES];
}

- (void)keyUp:(NSEvent *)theEvent
{
    [self->_delegate sendJavaKeyEvent:theEvent isDown:NO];
}

- (void)flagsChanged:(NSEvent *)theEvent
{
    [self->_delegate sendJavaModifierKeyEvent:theEvent];
}

- (BOOL)wantsPeriodicDraggingUpdates
{
    // we only want want updated drag operations when the mouse position changes
    return NO;
}

- (BOOL)prepareForDragOperation:(id <NSDraggingInfo>)sender
{
    DNDLOG("prepareForDragOperation");
    return YES;
}

- (BOOL)performDragOperation:(id <NSDraggingInfo>)sender
{
    DNDLOG("performDragOperation");
    [self->_delegate sendJavaDndEvent:sender type:com_sun_glass_events_DndEvent_PERFORM];
    
    return YES;
}

- (void)concludeDragOperation:(id <NSDraggingInfo>)sender
{
    DNDLOG("concludeDragOperation");
}

- (NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender
{
    DNDLOG("draggingEntered");
    return [self->_delegate sendJavaDndEvent:sender type:com_sun_glass_events_DndEvent_ENTER];
}

- (NSDragOperation)draggingUpdated:(id <NSDraggingInfo>)sender
{
    DNDLOG("draggingUpdated");
    return [self->_delegate sendJavaDndEvent:sender type:com_sun_glass_events_DndEvent_UPDATE];
}

- (void)draggingEnded:(id <NSDraggingInfo>)sender
{
    DNDLOG("draggingEnded");
    [self->_delegate sendJavaDndEvent:sender type:com_sun_glass_events_DndEvent_END];
}

- (void)draggingExited:(id <NSDraggingInfo>)sender
{
    DNDLOG("draggingExited");
    [self->_delegate sendJavaDndEvent:sender type:com_sun_glass_events_DndEvent_EXIT];
}

- (NSDragOperation)draggingSourceOperationMaskForLocal:(BOOL)isLocal
{
    // Deprecated for 10.7
    // use NSDraggingSession - (NSDragOperation)draggingSession:(NSDraggingSession *)session sourceOperationMaskForDraggingContext:(NSDraggingContext)context
    DNDLOG("draggingSourceOperationMaskForLocal");
    return [self->_delegate draggingSourceOperationMaskForLocal:isLocal];
}

#pragma mark --- Callbacks

- (uint32_t)getRemoteLayerIdForServer:(NSString*)serverName
{
    GlassLayer3D *view = (GlassLayer3D*)[self layer];
    return [view getRemoteIdForServer:serverName];
}

- (void)enterFullscreenWithAnimate:(BOOL)animate withKeepRatio:(BOOL)keepRatio withHideCursor:(BOOL)hideCursor
{
    [self->_delegate enterFullscreenWithAnimate:animate withKeepRatio:keepRatio withHideCursor:hideCursor];
}

- (void)exitFullscreenWithAnimate:(BOOL)animate
{
    [self->_delegate exitFullscreenWithAnimate:animate];
}

- (void)setNeedsDisplay:(BOOL)flag
{
    if (flag == YES)
    {
        [[self layer] setNeedsDisplay];
    }
    [super setNeedsDisplay:flag];
}

- (void)lockFocus
{
    @try
    {
        [[self window] disableFlushWindow];
        
        [super lockFocus]; // 1st view lock
        [self->_offscreen lock]; // 2nd offscreen lock
        
        assert(self->_lockCounter >= 0);
        if (self->_lockCounter == 0)
        {
            NSDisableScreenUpdates();
            
            self->_drawn = NO;
            [self->_delegate sendJavaLockedEvent];
        }
        self->_lockCounter++;
    }
    @catch (NSException *e)
    {
        NSLog(@"lockFocus caught exception: %@", e);
    }
}

- (void)unlockFocus
{
    @try
    {
        assert(self->_lockCounter > 0);
        self->_lockCounter--;
        if (self->_lockCounter == 0)
        {
            if ((self->_drawn == NO) && ([self layer] != nil))
            {
#ifdef DEBUG_COLORS
                glViewport(0, 0, [self bounds].size.width, [self bounds].size.height);
                glClearColor(1.0, 0.0, 0.0, 1.0);
                glClear(GL_COLOR_BUFFER_BIT);
                glFinish();
#endif
                
                // force system repaint if lockFocus/unlockFocus was called by OS without us repainting
                [self drawRect:[self frame]];
            }
            [self->_delegate sendJavaUnlockedEvent];
            
            NSEnableScreenUpdates();
        }
        
        [self->_offscreen unlock];
        [super unlockFocus];
        [[self window] enableFlushWindow];
    }
    @catch (NSException *e)
    {
        NSLog(@"unlockFocus caught exception: %@", e);
    }
}

- (void)begin
{
    assert(self->_lockCounter > 0);
    assert(self->_drawCounter >= 0);
    
    if (self->_drawCounter == 0)
    {
        self->_contextToRestore = CGLGetCurrentContext();
        CGLSetCurrentContext(self->_context);
        
        NSSize size = [self bounds].size;
        [self->_offscreen bindForWidth:size.width andHeight:size.height];
        {
            // client is now ready to render
            self->_ready = YES;
        }
    }
    self->_drawCounter++;
}

- (BOOL)ready
{
    return self->_ready;
}

- (void)end:(BOOL)flush
{
    assert(self->_ready == YES);
    assert(self->_lockCounter > 0);
    assert(self->_drawCounter > 0);
    assert(CGLGetCurrentContext() == self->_context);
    
    self->_drawCounter--;
    if (self->_drawCounter == 0)
    {
        // client is done rendering
        
        self->_drawn = YES;
        if (flush == YES)
        {
            [[self layer] setNeedsDisplay];
        }
        [self->_offscreen unbind];
        
        //CGLSetCurrentContext(self->_contextToRestore); // enable after RT-17546
    }
}

- (void)drawRect:(NSRect)dirtyRect
{
    [self->_delegate drawRect:dirtyRect];
}

- (void)pushPixels:(void*)pixels withWidth:(int)width withHeight:(int)height withEnv:(JNIEnv *)env
{
    assert(self->_ready == YES);
    assert(CGLGetCurrentContext() == self->_context);
    
    BOOL uploaded = NO;
    if ((self->_textureWidth != width) || (self->_textureHeight != height))
    {
        uploaded = YES;
        
        self->_textureWidth = width;
        self->_textureHeight = height;
        
        // GL_EXT_texture_rectangle is defined in OS X 10.6 GL headers, so we can depend on GL_TEXTURE_RECTANGLE_EXT being available
        glBindTexture(GL_TEXTURE_RECTANGLE_EXT, self->_texture);
        glTexParameteri(GL_TEXTURE_RECTANGLE_EXT, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
        glTexParameteri(GL_TEXTURE_RECTANGLE_EXT, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
        glTexParameteri(GL_TEXTURE_RECTANGLE_EXT, GL_TEXTURE_WRAP_S, GL_CLAMP);
        glTexParameteri(GL_TEXTURE_RECTANGLE_EXT, GL_TEXTURE_WRAP_T, GL_CLAMP);
        glTexImage2D(GL_TEXTURE_RECTANGLE_EXT, 0, GL_RGBA8, self->_textureWidth, self->_textureHeight, 0, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, pixels);
    }
    
    glEnable(GL_TEXTURE_RECTANGLE_EXT);
    glBindTexture(GL_TEXTURE_RECTANGLE_EXT, self->_texture);
    {
        if (uploaded == NO)
        {
            glTexSubImage2D(GL_TEXTURE_RECTANGLE_EXT, 0, 0, 0, self->_textureWidth, self->_textureHeight, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, pixels);
        }
        
        GLfloat w = self->_textureWidth;
        GLfloat h = self->_textureHeight;
        
        NSSize size = [self bounds].size;
        if ((size.width != w) || (size.height != h))
        {
            //NSLog(@"Glass View3D size: %dx%d, but pixels size: %dx%d", (int)size.width, (int)size.height, (int)w, (int)h);
            //glClear(GL_COLOR_BUFFER_BIT);
        }
        
        glMatrixMode(GL_PROJECTION);
        glPushMatrix();
        glLoadIdentity();
        glOrtho(0.0f, size.width, size.height, 0.0f, -1.0f, 1.0f);
        {
            glMatrixMode(GL_MODELVIEW);
            glPushMatrix();
            glLoadIdentity();
            {
                glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE); // copy
                
                glBegin(GL_QUADS);
                {
                    glTexCoord2f(0.0f, 0.0f); glVertex2f(0.0f, 0.0f);
                    glTexCoord2f(   w, 0.0f); glVertex2f(   w, 0.0f);
                    glTexCoord2f(   w,    h); glVertex2f(   w,    h);
                    glTexCoord2f(0.0f,    h); glVertex2f(0.0f,    h);
                }
                glEnd();
            }
            glMatrixMode(GL_MODELVIEW);
            glPopMatrix();
        }
        glMatrixMode(GL_PROJECTION);
        glPopMatrix();
    }
    glBindTexture(GL_TEXTURE_RECTANGLE_EXT, 0);
    glDisable(GL_TEXTURE_RECTANGLE_EXT);
    
    glFinish();
    
    [[self layer] setNeedsDisplay];
}

- (GlassViewDelegate*)delegate
{
    return self->_delegate;
}

@end
