/*
 * Copyright 2003-2007 Sun Microsystems, Inc.  All Rights Reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Sun designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Sun in the LICENSE file that accompanied this code.
 *
 * This code 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 General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
 * CA 95054 USA or visit www.sun.com if you need additional information or
 * have any questions.
 */

#ifndef HEADLESS

#include <stdlib.h>

#include "sun_java2d_opengl_OGLSurfaceData.h"

#include "jlong.h"
#include "jni_util.h"
#include "OGLSurfaceData.h"

/**
 * The following methods are implemented in the windowing system (i.e. GLX
 * and WGL) source files.
 */
extern jlong OGLSD_GetNativeConfigInfo(OGLSDOps *oglsdo);
extern jboolean OGLSD_InitOGLWindow(JNIEnv *env, OGLSDOps *oglsdo);
extern void OGLSD_DestroyOGLSurface(JNIEnv *env, OGLSDOps *oglsdo);

/**
 * This table contains the "pixel formats" for all system memory surfaces
 * that OpenGL is capable of handling, indexed by the "PF_" constants defined
 * in OGLSurfaceData.java.  These pixel formats contain information that is
 * passed to OpenGL when copying from a system memory ("Sw") surface to
 * an OpenGL "Surface" (via glDrawPixels()) or "Texture" (via glTexImage2D()).
 */
OGLPixelFormat PixelFormats[] = {
    { GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV,
      4, 1, 0,                                     }, /* 0 - IntArgb      */
    { GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV,
      4, 1, 1,                                     }, /* 1 - IntArgbPre   */
    { GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV,
      4, 0, 1,                                     }, /* 2 - IntRgb       */
    { GL_RGBA, GL_UNSIGNED_INT_8_8_8_8,
      4, 0, 1,                                     }, /* 3 - IntRgbx      */
    { GL_RGBA, GL_UNSIGNED_INT_8_8_8_8_REV,
      4, 0, 1,                                     }, /* 4 - IntBgr       */
    { GL_BGRA, GL_UNSIGNED_INT_8_8_8_8,
      4, 0, 1,                                     }, /* 5 - IntBgrx      */
    { GL_RGB,  GL_UNSIGNED_SHORT_5_6_5,
      2, 0, 1,                                     }, /* 6 - Ushort565Rgb */
    { GL_BGRA, GL_UNSIGNED_SHORT_1_5_5_5_REV,
      2, 0, 1,                                     }, /* 7 - Ushort555Rgb */
    { GL_RGBA, GL_UNSIGNED_SHORT_5_5_5_1,
      2, 0, 1,                                     }, /* 8 - Ushort555Rgbx*/
    { GL_LUMINANCE, GL_UNSIGNED_BYTE,
      1, 0, 1,                                     }, /* 9 - ByteGray     */
    { GL_LUMINANCE, GL_UNSIGNED_SHORT,
      2, 0, 1,                                     }, /*10 - UshortGray   */
};

/**
 * Given a starting value and a maximum limit, returns the first power-of-two
 * greater than the starting value.  If the resulting value is greater than
 * the maximum limit, zero is returned.
 */
jint
OGLSD_NextPowerOfTwo(jint val, jint max)
{
    jint i;

    if (val > max) {
        return 0;
    }

    for (i = 1; i < val; i *= 2);

    return i;
}

/**
 * Returns true if both given dimensions are a power of two.
 */
static jboolean
OGLSD_IsPowerOfTwo(jint width, jint height)
{
    return (((width & (width-1)) | (height & (height-1))) == 0);
}

/**
 * Initializes an OpenGL texture object.
 *
 * If the isOpaque parameter is JNI_FALSE, then the texture will have a
 * full alpha channel; otherwise, the texture will be opaque (this can
 * help save VRAM when translucency is not needed).
 *
 * If the GL_ARB_texture_non_power_of_two extension is present (texNonPow2
 * is JNI_TRUE), the actual texture is allowed to have non-power-of-two
 * dimensions, and therefore width==textureWidth and height==textureHeight.
 *
 * Failing that, if the GL_ARB_texture_rectangle extension is present
 * (texRect is JNI_TRUE), the actual texture is allowed to have
 * non-power-of-two dimensions, except that instead of using the usual
 * GL_TEXTURE_2D target, we need to use the GL_TEXTURE_RECTANGLE_ARB target.
 * Note that the GL_REPEAT wrapping mode is not allowed with this target,
 * so if that mode is needed (e.g. as is the case in the TexturePaint code)
 * one should pass JNI_FALSE to avoid using this extension.  Also note that
 * when the texture target is GL_TEXTURE_RECTANGLE_ARB, texture coordinates
 * must be specified in the range [0,width] and [0,height] rather than
 * [0,1] as is the case with the usual GL_TEXTURE_2D target (so take care)!
 *
 * Otherwise, the actual texture must have power-of-two dimensions, and
 * therefore the textureWidth and textureHeight will be the next
 * power-of-two greater than (or equal to) the requested width and height.
 */
static jboolean
OGLSD_InitTextureObject(OGLSDOps *oglsdo,
                        jboolean isOpaque,
                        jboolean texNonPow2, jboolean texRect,
                        jint width, jint height)
{
    GLenum texTarget, texProxyTarget;
    GLint format = isOpaque ? GL_RGB : GL_RGBA;
    GLuint texID;
    GLsizei texWidth, texHeight, realWidth, realHeight;
    GLint texMax;

    J2dTraceLn4(J2D_TRACE_INFO,
                "OGLSD_InitTextureObject: w=%d h=%d opq=%d nonpow2=%d",
                width, height, isOpaque, texNonPow2);

    if (oglsdo == NULL) {
        J2dRlsTraceLn(J2D_TRACE_ERROR,
                      "OGLSD_InitTextureObject: ops are null");
        return JNI_FALSE;
    }

    if (texNonPow2) {
        // use non-pow2 dimensions with GL_TEXTURE_2D target
        j2d_glGetIntegerv(GL_MAX_TEXTURE_SIZE, &texMax);
        texWidth = (width <= texMax) ? width : 0;
        texHeight = (height <= texMax) ? height : 0;
        texTarget = GL_TEXTURE_2D;
        texProxyTarget = GL_PROXY_TEXTURE_2D;
    } else if (texRect) {
        // use non-pow2 dimensions with GL_TEXTURE_RECTANGLE_ARB target
        j2d_glGetIntegerv(GL_MAX_RECTANGLE_TEXTURE_SIZE_ARB, &texMax);
        texWidth = (width <= texMax) ? width : 0;
        texHeight = (height <= texMax) ? height : 0;
        texTarget = GL_TEXTURE_RECTANGLE_ARB;
        texProxyTarget = GL_PROXY_TEXTURE_RECTANGLE_ARB;
    } else {
        // find the appropriate power-of-two dimensions
        j2d_glGetIntegerv(GL_MAX_TEXTURE_SIZE, &texMax);
        texWidth = OGLSD_NextPowerOfTwo(width, texMax);
        texHeight = OGLSD_NextPowerOfTwo(height, texMax);
        texTarget = GL_TEXTURE_2D;
        texProxyTarget = GL_PROXY_TEXTURE_2D;
    }

    J2dTraceLn3(J2D_TRACE_VERBOSE,
                "  desired texture dimensions: w=%d h=%d max=%d",
                texWidth, texHeight, texMax);

    // if either dimension is 0, we cannot allocate a texture with the
    // requested dimensions
    if ((texWidth == 0) || (texHeight == 0)) {
        J2dRlsTraceLn(J2D_TRACE_ERROR,
            "OGLSD_InitTextureObject: texture dimensions too large");
        return JNI_FALSE;
    }

    // now use a proxy to determine whether we can create a texture with
    // the calculated power-of-two dimensions and the given internal format
    j2d_glTexImage2D(texProxyTarget, 0, format,
                     texWidth, texHeight, 0,
                     format, GL_UNSIGNED_BYTE, NULL);
    j2d_glGetTexLevelParameteriv(texProxyTarget, 0,
                                 GL_TEXTURE_WIDTH, &realWidth);
    j2d_glGetTexLevelParameteriv(texProxyTarget, 0,
                                 GL_TEXTURE_HEIGHT, &realHeight);

    // if the requested dimensions and proxy dimensions don't match,
    // we shouldn't attempt to create the texture
    if ((realWidth != texWidth) || (realHeight != texHeight)) {
        J2dRlsTraceLn2(J2D_TRACE_ERROR,
            "OGLSD_InitTextureObject: actual (w=%d h=%d) != requested",
                       realWidth, realHeight);
        return JNI_FALSE;
    }

    // initialize the texture with some dummy data (this allows us to create
    // a texture object once with 2^n dimensions, and then use
    // glTexSubImage2D() to provide further updates)
    j2d_glGenTextures(1, &texID);
    j2d_glBindTexture(texTarget, texID);
    j2d_glTexImage2D(texTarget, 0, format,
                     texWidth, texHeight, 0,
                     format, GL_UNSIGNED_BYTE, NULL);

    oglsdo->isOpaque = isOpaque;
    oglsdo->xOffset = 0;
    oglsdo->yOffset = 0;
    oglsdo->width = width;
    oglsdo->height = height;
    oglsdo->textureID = texID;
    oglsdo->textureWidth = texWidth;
    oglsdo->textureHeight = texHeight;
    oglsdo->textureTarget = texTarget;
    OGLSD_INIT_TEXTURE_FILTER(oglsdo, GL_NEAREST);
    OGLSD_RESET_TEXTURE_WRAP(texTarget);

    J2dTraceLn3(J2D_TRACE_VERBOSE, "  created texture: w=%d h=%d id=%d",
                width, height, texID);

    return JNI_TRUE;
}

/**
 * Initializes an OpenGL texture, using the given width and height as
 * a guide.  See OGLSD_InitTextureObject() for more information.
 */
JNIEXPORT jboolean JNICALL
Java_sun_java2d_opengl_OGLSurfaceData_initTexture
    (JNIEnv *env, jobject oglsd,
     jlong pData, jboolean isOpaque,
     jboolean texNonPow2, jboolean texRect,
     jint width, jint height)
{
    OGLSDOps *oglsdo = (OGLSDOps *)jlong_to_ptr(pData);

    J2dTraceLn2(J2D_TRACE_INFO, "OGLSurfaceData_initTexture: w=%d h=%d",
                width, height);

    if (oglsdo == NULL) {
        J2dRlsTraceLn(J2D_TRACE_ERROR,
            "OGLSurfaceData_initTexture: ops are null");
        return JNI_FALSE;
    }

    /*
     * We only use the GL_ARB_texture_rectangle extension if it is available
     * and the requested bounds are not pow2 (it is probably faster to use
     * GL_TEXTURE_2D for pow2 textures, and besides, our TexturePaint
     * code relies on GL_REPEAT, which is not allowed for
     * GL_TEXTURE_RECTANGLE_ARB targets).
     */
    texRect = texRect && !OGLSD_IsPowerOfTwo(width, height);

    if (!OGLSD_InitTextureObject(oglsdo, isOpaque, texNonPow2, texRect,
                                 width, height))
    {
        J2dRlsTraceLn(J2D_TRACE_ERROR,
            "OGLSurfaceData_initTexture: could not init texture object");
        return JNI_FALSE;
    }

    oglsdo->drawableType = OGLSD_TEXTURE;
    // other fields (e.g. width, height) are set in OGLSD_InitTextureObject()

    return JNI_TRUE;
}

/**
 * Initializes a framebuffer object based on the given textureID and its
 * width/height.  This method will iterate through all possible depth formats
 * to find one that is supported by the drivers/hardware.  (Since our use of
 * the depth buffer is fairly simplistic, we hope to find a depth format that
 * uses as little VRAM as possible.)  If an appropriate depth buffer is found
 * and all attachments are successful (i.e. the framebuffer object is
 * "complete"), then this method will return JNI_TRUE and will initialize
 * the values of fbobjectID and depthID using the IDs created by this method.
 * Otherwise, this method returns JNI_FALSE.  Note that the caller is only
 * responsible for deleting the allocated fbobject and depth renderbuffer
 * resources if this method returned JNI_TRUE.
 */
jboolean
OGLSD_InitFBObject(GLuint *fbobjectID, GLuint *depthID,
                   GLuint textureID, GLenum textureTarget,
                   jint textureWidth, jint textureHeight)
{
    GLenum depthFormats[] = {
        GL_DEPTH_COMPONENT16, GL_DEPTH_COMPONENT24, GL_DEPTH_COMPONENT32
    };
    GLuint fboTmpID, depthTmpID;
    jboolean foundDepth = JNI_FALSE;
    int i;

    J2dTraceLn3(J2D_TRACE_INFO, "OGLSD_InitFBObject: w=%d h=%d texid=%d",
                textureWidth, textureHeight, textureID);

    // initialize framebuffer object
    j2d_glGenFramebuffersEXT(1, &fboTmpID);
    j2d_glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, fboTmpID);

    // attach color texture to framebuffer object
    j2d_glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT,
                                  GL_COLOR_ATTACHMENT0_EXT,
                                  textureTarget, textureID, 0);

    // attempt to create a depth renderbuffer of a particular format; we
    // will start with the smallest size and then work our way up
    for (i = 0; i < 3; i++) {
        GLenum error, status;
        GLenum depthFormat = depthFormats[i];
        int depthSize = 16 + (i * 8);

        // initialize depth renderbuffer
        j2d_glGenRenderbuffersEXT(1, &depthTmpID);
        j2d_glBindRenderbufferEXT(GL_RENDERBUFFER_EXT, depthTmpID);
        j2d_glRenderbufferStorageEXT(GL_RENDERBUFFER_EXT, depthFormat,
                                     textureWidth, textureHeight);

        // creation of depth buffer could potentially fail, so check for error
        error = j2d_glGetError();
        if (error != GL_NO_ERROR) {
            J2dTraceLn2(J2D_TRACE_VERBOSE,
                "OGLSD_InitFBObject: could not create depth buffer: depth=%d error=%x",
                           depthSize, error);
            j2d_glDeleteRenderbuffersEXT(1, &depthTmpID);
            continue;
        }

        // attach depth renderbuffer to framebuffer object
        j2d_glFramebufferRenderbufferEXT(GL_FRAMEBUFFER_EXT,
                                         GL_DEPTH_ATTACHMENT_EXT,
                                         GL_RENDERBUFFER_EXT, depthTmpID);

        // now check for framebuffer "completeness"
        status = j2d_glCheckFramebufferStatusEXT(GL_FRAMEBUFFER_EXT);

        if (status == GL_FRAMEBUFFER_COMPLETE_EXT) {
            // we found a valid format, so break out of the loop
            J2dTraceLn1(J2D_TRACE_VERBOSE,
                        "  framebuffer is complete: depth=%d", depthSize);
            foundDepth = JNI_TRUE;
            break;
        } else {
            // this depth format didn't work, so delete and try another format
            J2dTraceLn2(J2D_TRACE_VERBOSE,
                        "  framebuffer is incomplete: depth=%d status=%x",
                        depthSize, status);
            j2d_glDeleteRenderbuffersEXT(1, &depthTmpID);
        }
    }

    // unbind the texture and framebuffer objects (they will be bound again
    // later as needed)
    j2d_glBindTexture(textureTarget, 0);
    j2d_glBindRenderbufferEXT(GL_RENDERBUFFER_EXT, 0);
    j2d_glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0);

    if (!foundDepth) {
        J2dRlsTraceLn(J2D_TRACE_ERROR,
            "OGLSD_InitFBObject: could not find valid depth format");
        j2d_glDeleteFramebuffersEXT(1, &fboTmpID);
        return JNI_FALSE;
    }

    *fbobjectID = fboTmpID;
    *depthID = depthTmpID;

    return JNI_TRUE;
}

/**
 * Initializes a framebuffer object, using the given width and height as
 * a guide.  See OGLSD_InitTextureObject() and OGLSD_InitFBObject()
 * for more information.
 */
JNIEXPORT jboolean JNICALL
Java_sun_java2d_opengl_OGLSurfaceData_initFBObject
    (JNIEnv *env, jobject oglsd,
     jlong pData, jboolean isOpaque,
     jboolean texNonPow2, jboolean texRect,
     jint width, jint height)
{
    OGLSDOps *oglsdo = (OGLSDOps *)jlong_to_ptr(pData);
    GLuint fbobjectID, depthID;

    J2dTraceLn2(J2D_TRACE_INFO,
                "OGLSurfaceData_initFBObject: w=%d h=%d",
                width, height);

    if (oglsdo == NULL) {
        J2dRlsTraceLn(J2D_TRACE_ERROR,
            "OGLSurfaceData_initFBObject: ops are null");
        return JNI_FALSE;
    }

    // initialize color texture object
    if (!OGLSD_InitTextureObject(oglsdo, isOpaque, texNonPow2, texRect,
                                 width, height))
    {
        J2dRlsTraceLn(J2D_TRACE_ERROR,
            "OGLSurfaceData_initFBObject: could not init texture object");
        return JNI_FALSE;
    }

    // initialize framebuffer object using color texture created above
    if (!OGLSD_InitFBObject(&fbobjectID, &depthID,
                            oglsdo->textureID, oglsdo->textureTarget,
                            oglsdo->textureWidth, oglsdo->textureHeight))
    {
        J2dRlsTraceLn(J2D_TRACE_ERROR,
            "OGLSurfaceData_initFBObject: could not init fbobject");
        j2d_glDeleteTextures(1, &oglsdo->textureID);
        return JNI_FALSE;
    }

    oglsdo->drawableType = OGLSD_FBOBJECT;
    // other fields (e.g. width, height) are set in OGLSD_InitTextureObject()
    oglsdo->fbobjectID = fbobjectID;
    oglsdo->depthID = depthID;

    // framebuffer objects differ from other OpenGL surfaces in that the
    // value passed to glRead/DrawBuffer() must be GL_COLOR_ATTACHMENTn_EXT,
    // rather than GL_FRONT (or GL_BACK)
    oglsdo->activeBuffer = GL_COLOR_ATTACHMENT0_EXT;

    return JNI_TRUE;
}

/**
 * Initializes a surface in the backbuffer of a given double-buffered
 * onscreen window for use in a BufferStrategy.Flip situation.  The bounds of
 * the backbuffer surface should always be kept in sync with the bounds of
 * the underlying native window.
 */
JNIEXPORT jboolean JNICALL
Java_sun_java2d_opengl_OGLSurfaceData_initFlipBackbuffer
    (JNIEnv *env, jobject oglsd,
     jlong pData)
{
    OGLSDOps *oglsdo = (OGLSDOps *)jlong_to_ptr(pData);

    J2dTraceLn(J2D_TRACE_INFO, "OGLSurfaceData_initFlipBackbuffer");

    if (oglsdo == NULL) {
        J2dRlsTraceLn(J2D_TRACE_ERROR,
            "OGLSurfaceData_initFlipBackbuffer: ops are null");
        return JNI_FALSE;
    }

    if (oglsdo->drawableType == OGLSD_UNDEFINED) {
        if (!OGLSD_InitOGLWindow(env, oglsdo)) {
            J2dRlsTraceLn(J2D_TRACE_ERROR,
                "OGLSurfaceData_initFlipBackbuffer: could not init window");
            return JNI_FALSE;
        }
    }

    if (oglsdo->drawableType != OGLSD_WINDOW) {
        J2dRlsTraceLn(J2D_TRACE_ERROR,
            "OGLSurfaceData_initFlipBackbuffer: drawable is not a window");
        return JNI_FALSE;
    }

    oglsdo->drawableType = OGLSD_FLIP_BACKBUFFER;
    // x/yOffset have already been set in OGLSD_InitOGLWindow()...
    // REMIND: for some reason, flipping won't work properly on IFB unless we
    //         explicitly use BACK_LEFT rather than BACK...
    oglsdo->activeBuffer = GL_BACK_LEFT;

    return JNI_TRUE;
}

JNIEXPORT jint JNICALL
Java_sun_java2d_opengl_OGLSurfaceData_getTextureTarget
    (JNIEnv *env, jobject oglsd,
     jlong pData)
{
    OGLSDOps *oglsdo = (OGLSDOps *)jlong_to_ptr(pData);

    J2dTraceLn(J2D_TRACE_INFO, "OGLSurfaceData_getTextureTarget");

    if (oglsdo == NULL) {
        J2dRlsTraceLn(J2D_TRACE_ERROR,
            "OGLSurfaceData_getTextureTarget: ops are null");
        return 0;
    }

    return (jint)oglsdo->textureTarget;
}

/**
 * Disposes of all native resources associated with this surface.
 */
void
OGLSD_Flush(JNIEnv *env, OGLSDOps *oglsdo)
{
    J2dTraceLn1(J2D_TRACE_INFO, "OGLSD_Flush: type=%d",
                oglsdo->drawableType);

    if (oglsdo->drawableType == OGLSD_TEXTURE) {
        if (oglsdo->textureID != 0) {
            j2d_glDeleteTextures(1, &oglsdo->textureID);
            oglsdo->textureID = 0;
        }
    } else if (oglsdo->drawableType == OGLSD_FBOBJECT) {
        if (oglsdo->textureID != 0) {
            j2d_glDeleteTextures(1, &oglsdo->textureID);
            oglsdo->textureID = 0;
        }
        if (oglsdo->depthID != 0) {
            j2d_glDeleteRenderbuffersEXT(1, &oglsdo->depthID);
            oglsdo->depthID = 0;
        }
        if (oglsdo->fbobjectID != 0) {
            j2d_glDeleteFramebuffersEXT(1, &oglsdo->fbobjectID);
            oglsdo->fbobjectID = 0;
        }
    } else {
        // dispose windowing system resources (pbuffer, pixmap, etc)
        OGLSD_DestroyOGLSurface(env, oglsdo);
    }
}

/**
 * This is the implementation of the general DisposeFunc defined in
 * SurfaceData.h and used by the Disposer mechanism.  It first flushes all
 * native OpenGL resources and then frees any memory allocated within the
 * native OGLSDOps structure.
 */
void
OGLSD_Dispose(JNIEnv *env, SurfaceDataOps *ops)
{
    OGLSDOps *oglsdo = (OGLSDOps *)ops;
    jlong pConfigInfo = OGLSD_GetNativeConfigInfo(oglsdo);

    JNU_CallStaticMethodByName(env, NULL, "sun/java2d/opengl/OGLSurfaceData",
                               "dispose", "(JJ)V",
                               ptr_to_jlong(ops), pConfigInfo);
}

/**
 * This is the implementation of the general surface LockFunc defined in
 * SurfaceData.h.
 */
jint
OGLSD_Lock(JNIEnv *env,
           SurfaceDataOps *ops,
           SurfaceDataRasInfo *pRasInfo,
           jint lockflags)
{
    JNU_ThrowInternalError(env, "OGLSD_Lock not implemented!");
    return SD_FAILURE;
}

/**
 * This is the implementation of the general GetRasInfoFunc defined in
 * SurfaceData.h.
 */
void
OGLSD_GetRasInfo(JNIEnv *env,
                 SurfaceDataOps *ops,
                 SurfaceDataRasInfo *pRasInfo)
{
    JNU_ThrowInternalError(env, "OGLSD_GetRasInfo not implemented!");
}

/**
 * This is the implementation of the general surface UnlockFunc defined in
 * SurfaceData.h.
 */
void
OGLSD_Unlock(JNIEnv *env,
             SurfaceDataOps *ops,
             SurfaceDataRasInfo *pRasInfo)
{
    JNU_ThrowInternalError(env, "OGLSD_Unlock not implemented!");
}

#endif /* !HEADLESS */
