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

#import "GlassFrameBufferObject.h"

#import <OpenGL/glu.h>
//#import <OpenGL/CGLMacro.h> // macros for faster GL APIs access (which require that we name local GL context as "cgl_ctx")

#define TARGET GL_TEXTURE_RECTANGLE_EXT
#define FORMAT GL_BGRA
#define TYPE GL_UNSIGNED_INT_8_8_8_8_REV

@implementation GlassFrameBufferObject

- (CGLContextObj)_assertContext
{
    CGLContextObj cgl_ctx = CGLGetCurrentContext();
    assert(cgl_ctx != NULL);
    return cgl_ctx;
}

- (BOOL)_supportsFbo
{
    return gluCheckExtension((const GLubyte *)"GL_EXT_framebuffer_object", glGetString(GL_EXTENSIONS));
}

- (BOOL)_checkFbo
{
    BOOL ok = NO;
    {
        switch (glCheckFramebufferStatusEXT(GL_FRAMEBUFFER))
        {
            case GL_FRAMEBUFFER_COMPLETE:
                ok = YES;
                break;
            case GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT:
                NSLog(@"GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT");
                break;
            case GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT:
                NSLog(@"GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT");
                break;
            case GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS_EXT:
                NSLog(@"GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS");
                break;
            case GL_FRAMEBUFFER_INCOMPLETE_FORMATS_EXT:
                NSLog(@"GL_FRAMEBUFFER_INCOMPLETE_FORMATS");
                break;
            case GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER:
                NSLog(@"GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER");                          
                break;
            case GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER:
                NSLog(@"GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER");
                break;
            case GL_FRAMEBUFFER_UNSUPPORTED:
                NSLog(@"GL_FRAMEBUFFER_UNSUPPORTED");
                break;
            default:
                NSLog(@"Unknown FBO Error");
                break;
        }
    }
    return ok;
}

- (void)_destroyFbo
{
    if (self->_texture != 0)
    {
        glDeleteTextures(1, &self->_texture);
        self->_texture = 0;
    }
    if (self->_fbo != 0)
    {
        glDeleteFramebuffersEXT(1, &self->_fbo);
        self->_fbo = 0;
    }
}

- (void)_createFboIfNeededForWidth:(GLsizei)width andHeight:(GLsizei)height
{
    if ((self->_width != width) || (self->_height != height))
    {
        // TODO optimization: is it possible to just resize an FBO's texture without destroying it first?
        [self _destroyFbo];
    }
    
    if (self->_fbo == 0)
    {
        glEnable(TARGET);
        glActiveTextureARB(GL_TEXTURE0);
        glGenTextures(1, &self->_texture);
        glBindTexture(TARGET, self->_texture);
        glTexParameteri(TARGET, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
        glTexParameteri(TARGET, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
        glTexParameteri(TARGET, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
        glTexParameteri(TARGET, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
        {
            GLenum target = TARGET;
            GLint level = 0;
            GLint internalformat = GL_RGBA;
            GLint border = 0;
            GLenum format = FORMAT;
            GLenum type = TYPE;
            const GLvoid *pixels = NULL;
            glTexImage2D(target, level, internalformat, width, height, border, format, type, pixels);
        }
        
        glGenFramebuffersEXT(1, &self->_fbo);
        glBindFramebufferEXT(GL_FRAMEBUFFER, self->_fbo);
        {
            GLenum target = GL_FRAMEBUFFER;
            GLenum attachment = GL_COLOR_ATTACHMENT0;
            GLenum textarget = TARGET;
            GLuint texture = self->_texture;
            GLint level = 0;
            glFramebufferTexture2DEXT(target, attachment, textarget, texture, level);
        }
        
        if ([self _checkFbo] == NO)
        {
            [self _destroyFbo];
        }
        
        glDisable(TARGET);
        
        glViewport(0, 0, width, height);
    }
//    DOES NOT WORK
//    else if ((self->_width != width) || (self->_height != height))
//    {
//        glEnable(TARGET);
//        glBindTexture(TARGET, self->_texture);
//        
//        GLenum target = TARGET;
//        GLint level = 0;
//        GLint xoffset = 0;
//        GLint yoffset = 0;
//        GLenum format = FORMAT;
//        GLenum type = TYPE;
//        const GLvoid *pixels = NULL;
//        glTexSubImage2D(target, level, xoffset, yoffset, width, height, format, type, pixels);
//
//        glDisable(TARGET);
//    }
    
    self->_width = width;
    self->_height = height;
}

- (id)init
{
    self = [super init];
    if (self != nil)
    {
        self->_width = 0;
        self->_height = 0;
        self->_texture = 0;
        self->_fbo = 0;
        
        [self _assertContext];
        if ([self _supportsFbo] == NO)
        {
            [super dealloc];
            self = nil;
        }
    }
    return self;
}

- (void)dealloc
{
    [self _assertContext];
    {
        [self _destroyFbo];
    }
    
    [super dealloc];
}

- (NSSize)size
{
    return NSMakeSize(self->_width, self->_height);
}

- (void)bindForWidth:(NSUInteger)width andHeight:(NSUInteger)height
{
    [self _assertContext];
    {
        if ((width > 0) && (height > 0))
        {
            [self _createFboIfNeededForWidth:(GLsizei)width andHeight:(GLsizei)height];
            if (self->_fbo != 0)
            {
                self->_fboToRestore = 0; // default to screen
                glGetIntegerv(GL_FRAMEBUFFER_BINDING, (GLint*)&self->_fboToRestore);
                
                GLuint framebufferToBind = self->_fbo; // our own FBO
                glBindFramebufferEXT(GL_FRAMEBUFFER, framebufferToBind);
            }
        }
    }
}

- (void)unbind
{
    [self _assertContext];
    {
        GLint framebufferCurrent = -1;
        glGetIntegerv(GL_FRAMEBUFFER_BINDING, &framebufferCurrent);
#if 0
        assert(framebufferCurrent == self->_fbo);
#else
        if (framebufferCurrent != self->_fbo)
        {
            fprintf(stderr, "   ERROR: unexpected fbo is bound! Expected %d, but found %d\n", self->_fbo, framebufferCurrent);
        }
#endif
        
        GLuint framebufferToRevertTo = self->_fboToRestore;
        glBindFramebufferEXT(GL_FRAMEBUFFER, framebufferToRevertTo);
    }
}

- (void)blitForWidth:(NSUInteger)width andHeight:(NSUInteger)height
{
    if (self->_texture != 0)
    {
        glMatrixMode(GL_PROJECTION);
        glLoadIdentity();
        glOrtho(0.0f, width, height, 0.0f, -1.0f, 1.0f);
        
        glMatrixMode(GL_MODELVIEW);
        glLoadIdentity();
        
        glEnable(TARGET);
        {
            glActiveTextureARB(GL_TEXTURE0);
            glBindTexture(TARGET, self->_texture);
            glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
            {
                glBegin(GL_QUADS);
                {
                    glTexCoord2f(        0.0f, self->_height); glVertex2f(        0.0f,          0.0f);
                    glTexCoord2f(self->_width, self->_height); glVertex2f(self->_width,          0.0f);
                    glTexCoord2f(self->_width,          0.0f); glVertex2f(self->_width, self->_height);
                    glTexCoord2f(        0.0f,          0.0f); glVertex2f(        0.0f, self->_height);
                }
                glEnd();
            }
        }
        glDisable(TARGET);
    }
}

@end
