/*
 * MACDRV Cocoa OpenGL code
 *
 * Copyright 2012, 2013 Ken Thomases for CodeWeavers Inc.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
 */

#define GL_SILENCE_DEPRECATION
#include <OpenGL/gl.h>
#import "cocoa_opengl.h"

#include "macdrv_cocoa.h"
#include "cocoa_app.h"
#include "cocoa_event.h"


@interface WineOpenGLContext ()
@property (retain, nonatomic) NSView* latentView;

    + (NSView*) dummyView;
    - (void) wine_updateBackingSize:(const CGSize*)size;

@end


@implementation WineOpenGLContext
@synthesize latentView, needsUpdate, needsReattach, shouldClearToBlack;

    - (void) dealloc
    {
        [[self view] release];
        [latentView release];
        [super dealloc];
    }

    + (NSView*) dummyView
    {
        static NSWindow* dummyWindow;
        static dispatch_once_t once;

        dispatch_once(&once, ^{
            OnMainThread(^{
                dummyWindow = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 100, 100)
                                                          styleMask:NSWindowStyleMaskBorderless
                                                            backing:NSBackingStoreBuffered
                                                              defer:NO];
            });
        });

        return dummyWindow.contentView;
    }

    // Normally, we take care that disconnecting a context from a view doesn't
    // destroy that view's GL surface (see -clearDrawableLeavingSurfaceOnScreen).
    // However, if we're using a surface backing size and that size changes, we
    // need to destroy and recreate the surface or we get weird behavior.
    - (void) resetSurfaceIfBackingSizeChanged
    {
        if (!retina_enabled)
            return;

        int view_backing[2];
        if (macdrv_get_view_backing_size((macdrv_view)self.view, view_backing) &&
            (view_backing[0] != backing_size[0] || view_backing[1] != backing_size[1]))
        {
            view_backing[0] = backing_size[0];
            view_backing[1] = backing_size[1];
            macdrv_set_view_backing_size((macdrv_view)self.view, view_backing);

            NSView* save = self.view;
            OnMainThread(^{
                [super clearDrawable];
                [super setView:save];
            });
            shouldClearToBlack = TRUE;
        }
    }

    - (void) wine_updateBackingSize:(const CGSize*)size
    {
        GLint enabled;

        if (!retina_enabled)
            return;

        if (size)
        {
            if (CGLIsEnabled(self.CGLContextObj, kCGLCESurfaceBackingSize, &enabled) != kCGLNoError)
                enabled = 0;

            if (!enabled || backing_size[0] != size->width || backing_size[1] != size->height)
            {
                backing_size[0] = size->width;
                backing_size[1] = size->height;
                CGLSetParameter(self.CGLContextObj, kCGLCPSurfaceBackingSize, backing_size);
            }

            if (!enabled)
                CGLEnable(self.CGLContextObj, kCGLCESurfaceBackingSize);

            [self resetSurfaceIfBackingSizeChanged];
        }
        else
        {
            backing_size[0] = 0;
            backing_size[1] = 0;

            if (CGLIsEnabled(self.CGLContextObj, kCGLCESurfaceBackingSize, &enabled) == kCGLNoError && enabled)
               CGLDisable(self.CGLContextObj, kCGLCESurfaceBackingSize);
        }
    }

    - (void) setView:(NSView*)newView
    {
        NSView* oldView = [self view];
        if ([NSThread isMainThread])
            [super setView:newView];
        else OnMainThread(^{
            [super setView:newView];
        });
        [newView retain];
        [oldView release];
    }

    - (void) clearDrawable
    {
        NSView* oldView = [self view];
        if ([NSThread isMainThread])
            [super clearDrawable];
        else OnMainThread(^{
            [super clearDrawable];
        });
        [oldView release];

        [self wine_updateBackingSize:NULL];
    }

    /* On at least some versions of Mac OS X, -[NSOpenGLContext clearDrawable] has the
       undesirable side effect of ordering the view's GL surface off-screen.  This isn't
       done when just changing the context's view to a different view (which I would
       think would be analogous, since the old view and surface end up without a
       context attached).  So, we finesse things by first setting the context's view to
       a different view (the content view of an off-screen window) and then letting the
       original implementation proceed. */
    - (void) clearDrawableLeavingSurfaceOnScreen
    {
        [self setView:[[self class] dummyView]];
        [self clearDrawable];
    }

    - (void) clearToBlackIfNeeded
    {
        if (shouldClearToBlack)
        {
            NSOpenGLContext* origContext = [NSOpenGLContext currentContext];
            const char *gl_version;
            unsigned int major;
            GLint draw_framebuffer_binding, draw_buffer;
            GLboolean scissor_test, color_mask[4];
            GLfloat clear_color[4];

            [self makeCurrentContext];

            gl_version = (const char *)glGetString(GL_VERSION);
            major = gl_version[0] - '0';
            /* FIXME: Should check for GL_ARB_framebuffer_object and GL_EXT_framebuffer_object
             * for older GL versions. */
            if (major >= 3)
            {
                glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &draw_framebuffer_binding);
                glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
            }
            glGetIntegerv(GL_DRAW_BUFFER, &draw_buffer);
            scissor_test = glIsEnabled(GL_SCISSOR_TEST);
            glGetBooleanv(GL_COLOR_WRITEMASK, color_mask);
            glGetFloatv(GL_COLOR_CLEAR_VALUE, clear_color);
            glDrawBuffer(GL_FRONT_AND_BACK);
            glDisable(GL_SCISSOR_TEST);
            glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
            glClearColor(0, 0, 0, gl_surface_mode == GL_SURFACE_IN_FRONT_TRANSPARENT ? 0 : 1);

            glClear(GL_COLOR_BUFFER_BIT);

            glClearColor(clear_color[0], clear_color[1], clear_color[2], clear_color[3]);
            glColorMask(color_mask[0], color_mask[1], color_mask[2], color_mask[3]);
            if (scissor_test)
                glEnable(GL_SCISSOR_TEST);
            glDrawBuffer(draw_buffer);
            if (major >= 3)
                glBindFramebuffer(GL_DRAW_FRAMEBUFFER, draw_framebuffer_binding);
            glFlush();

            if (origContext)
                [origContext makeCurrentContext];
            else
                [NSOpenGLContext clearCurrentContext];

            shouldClearToBlack = FALSE;
        }
    }

    - (void) removeFromViews:(BOOL)removeViews
    {
        if ([self view])
        {
            macdrv_remove_view_opengl_context((macdrv_view)[self view], (macdrv_opengl_context)self);
            if (removeViews)
                [self clearDrawableLeavingSurfaceOnScreen];
        }
        if ([self latentView])
        {
            macdrv_remove_view_opengl_context((macdrv_view)[self latentView], (macdrv_opengl_context)self);
            if (removeViews)
                [self setLatentView:nil];
        }
        needsUpdate = FALSE;
        needsReattach = FALSE;
    }

@end


/***********************************************************************
 *              macdrv_create_opengl_context
 *
 * Returns a Cocoa OpenGL context created from a CoreGL context.  The
 * caller is responsible for calling macdrv_dispose_opengl_context()
 * when done with the context object.
 */
macdrv_opengl_context macdrv_create_opengl_context(void* cglctx)
{
    NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
    WineOpenGLContext *context;

    context = [[WineOpenGLContext alloc] initWithCGLContextObj:cglctx];

    [pool release];
    return (macdrv_opengl_context)context;
}

/***********************************************************************
 *              macdrv_dispose_opengl_context
 *
 * Destroys a Cocoa OpenGL context previously created by
 * macdrv_create_opengl_context();
 */
void macdrv_dispose_opengl_context(macdrv_opengl_context c)
{
    NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
    WineOpenGLContext *context = (WineOpenGLContext*)c;

    [context removeFromViews:YES];
    [context release];

    [pool release];
}

/***********************************************************************
 *              macdrv_make_context_current
 */
void macdrv_make_context_current(macdrv_opengl_context c, macdrv_view v, CGRect r)
{
    NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
    WineOpenGLContext *context = (WineOpenGLContext*)c;
    NSView* view = (NSView*)v;

    if (context && view)
    {
        if (view == [context view] || view == [context latentView])
        {
            [context wine_updateBackingSize:&r.size];
            macdrv_update_opengl_context(c);
        }
        else
        {
            [context removeFromViews:NO];
            macdrv_add_view_opengl_context(v, c);

            if (context.needsUpdate)
            {
                context.needsUpdate = FALSE;
                context.needsReattach = FALSE;
                if (context.view)
                    [context setView:[[context class] dummyView]];
                [context wine_updateBackingSize:&r.size];
                [context setView:view];
                [context setLatentView:nil];
                [context resetSurfaceIfBackingSizeChanged];
            }
            else
            {
                if ([context view])
                    [context clearDrawableLeavingSurfaceOnScreen];
                [context wine_updateBackingSize:&r.size];
                [context setLatentView:view];
            }
        }

        [context makeCurrentContext];

        if ([context view])
            [context clearToBlackIfNeeded];
    }
    else
    {
        WineOpenGLContext* currentContext = (WineOpenGLContext*)[WineOpenGLContext currentContext];

        if ([currentContext isKindOfClass:[WineOpenGLContext class]])
        {
            [WineOpenGLContext clearCurrentContext];
            if (currentContext != context)
                [currentContext removeFromViews:YES];
        }

        if (context)
            [context removeFromViews:YES];
    }

    [pool release];
}

/***********************************************************************
 *              macdrv_update_opengl_context
 */
void macdrv_update_opengl_context(macdrv_opengl_context c)
{
    NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
    WineOpenGLContext *context = (WineOpenGLContext*)c;

    if (context.needsUpdate)
    {
        BOOL reattach = context.needsReattach;
        context.needsUpdate = FALSE;
        context.needsReattach = FALSE;
        if (context.latentView)
        {
            [context setView:context.latentView];
            context.latentView = nil;

            [context resetSurfaceIfBackingSizeChanged];
            [context clearToBlackIfNeeded];
        }
        else
        {
            if (reattach)
            {
                NSView* view = [[context.view retain] autorelease];
                [context clearDrawableLeavingSurfaceOnScreen];
                context.view = view;
            }
            else OnMainThread(^{
                [context update];
            });
            [context resetSurfaceIfBackingSizeChanged];
        }
    }

    [pool release];
}

/***********************************************************************
 *              macdrv_flush_opengl_context
 *
 * Performs an implicit glFlush() and then swaps the back buffer to the
 * front (if the context is double-buffered).
 */
void macdrv_flush_opengl_context(macdrv_opengl_context c)
{
    NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
    WineOpenGLContext *context = (WineOpenGLContext*)c;

    macdrv_update_opengl_context(c);
    [context flushBuffer];

    [pool release];
}
