package processing.opengl;

import java.awt.BorderLayout;
import java.awt.Canvas;
import java.awt.Color;
import java.awt.FontMetrics;
import java.io.IOException;
import java.net.URL;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

import javax.media.opengl.GL;
import javax.media.opengl.GL2;
import javax.media.opengl.GL2ES1;
import javax.media.opengl.GL2ES2;
import javax.media.opengl.GL2ES3;
import javax.media.opengl.GL2GL3;
import javax.media.opengl.GLAutoDrawable;
import javax.media.opengl.GLCapabilities;
import javax.media.opengl.GLCapabilitiesImmutable;
import javax.media.opengl.GLContext;
import javax.media.opengl.GLDrawable;
import javax.media.opengl.GLEventListener;
import javax.media.opengl.GLException;
import javax.media.opengl.GLFBODrawable;
import javax.media.opengl.GLProfile;
import javax.media.opengl.awt.GLCanvas;
import javax.media.opengl.fixedfunc.GLMatrixFunc;
import javax.media.opengl.glu.GLU;

import processing.core.PApplet;
import processing.core.PConstants;
import processing.core.PGraphics;
import processing.event.KeyEvent;
import processing.event.MouseEvent;

import com.jogamp.newt.awt.NewtCanvasAWT;
import com.jogamp.newt.event.InputEvent;
import com.jogamp.newt.opengl.GLWindow;
import com.jogamp.opengl.FBObject;

import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import java.awt.geom.PathIterator;

import javax.media.opengl.glu.GLUtessellator;
import javax.media.opengl.glu.GLUtessellatorCallbackAdapter;

public class PJOGL extends PGL {
  // OpenGL profile to use (2, 3 or 4)
  public static int PROFILE = 2;

  // The two windowing toolkits available to use in JOGL:
  public static final int AWT  = 0; // http://jogamp.org/wiki/index.php/Using_JOGL_in_AWT_SWT_and_Swing
  public static final int NEWT = 1; // http://jogamp.org/jogl/doc/NEWT-Overview.html

  // ........................................................

  // Public members to access the underlying GL objects and context

  /** Basic GL functionality, common to all profiles */
  public static GL gl;

  /** GLU interface **/
  public static GLU glu;

  /** The rendering context (holds rendering state info) */
  public static GLContext context;

  /** The canvas where OpenGL rendering takes place */
  public static Canvas canvas;

  /** Selected GL profile */
  public static GLProfile profile;

  // ........................................................

  // Additional parameters

  /** Time that the Processing's animation thread will wait for JOGL's rendering
   * thread to be done with a single frame.
   */
  protected static int DRAW_TIMEOUT_MILLIS = 500;

  // ........................................................

  // OS-specific configuration

  protected static int WINDOW_TOOLKIT;
  protected static int EVENTS_TOOLKIT;
  protected static boolean USE_JOGL_FBOLAYER;
  static {
    if (PApplet.platform == PConstants.WINDOWS) {
      // Using AWT on Windows because NEWT displays a black background while
      // initializing, and the cursor functions don't work. GLWindow has some
      // functions for basic cursor handling (hide/show):
      // GLWindow.setPointerVisible(false);
      // but apparently nothing to set the cursor icon:
      // https://jogamp.org/bugzilla/show_bug.cgi?id=409
      WINDOW_TOOLKIT = AWT;
      EVENTS_TOOLKIT = AWT;
      USE_FBOLAYER_BY_DEFAULT = false;
      USE_JOGL_FBOLAYER = false;
    } else if (PApplet.platform == PConstants.MACOSX) {
      // Note: The JOGL FBO layer (in 2.0.2) seems incompatible with NEWT.
      WINDOW_TOOLKIT = AWT;
      EVENTS_TOOLKIT = AWT;
      USE_FBOLAYER_BY_DEFAULT = true;
      USE_JOGL_FBOLAYER = true;
    } else if (PApplet.platform == PConstants.LINUX) {
      WINDOW_TOOLKIT = AWT;
      EVENTS_TOOLKIT = AWT;
      USE_FBOLAYER_BY_DEFAULT = false;
      USE_JOGL_FBOLAYER = false;
    } else if (PApplet.platform == PConstants.OTHER) {
      WINDOW_TOOLKIT = NEWT; // NEWT works on the Raspberry pi?
      EVENTS_TOOLKIT = NEWT;
      USE_FBOLAYER_BY_DEFAULT = false;
      USE_JOGL_FBOLAYER = false;
    }
  }

  // ........................................................

  // Protected JOGL-specific objects needed to access the GL profiles

  /** The capabilities of the OpenGL rendering surface */
  protected static GLCapabilitiesImmutable capabilities;

  /** The rendering surface */
  protected static GLDrawable drawable;

  /** GLES2 functionality (shaders, etc) */
  protected static GL2ES2 gl2;

  /** GL3 interface */
  protected static GL2GL3 gl3;

  /** GL2 desktop functionality (blit framebuffer, map buffer range,
   * multisampled renerbuffers) */
  protected static GL2 gl2x;

  /** The AWT-OpenGL canvas */
  protected static GLCanvas canvasAWT;

  /** The NEWT-OpenGL canvas */
  protected static NewtCanvasAWT canvasNEWT;

  /** The NEWT window */
  protected static GLWindow window;

  /** The listener that fires the frame rendering in Processing */
  protected static PGLListener listener;

  /** This countdown latch is used to maintain the synchronization between
   * Processing's drawing thread and JOGL's rendering thread */
  protected CountDownLatch drawLatch;

  /** Flag used to do request final display() call to make sure that the
   * buffers are properly swapped.
   */
  protected boolean prevCanDraw = false;

  // ........................................................

  // JOGL's FBO-layer

  /** Back (== draw, current frame) buffer */
  protected static FBObject backFBO;
  /** Sink buffer, used in the multisampled case */
  protected static FBObject sinkFBO;
  /** Front (== read, previous frame) buffer */
  protected static FBObject frontFBO;
  protected static FBObject.TextureAttachment backTexAttach;
  protected static FBObject.TextureAttachment frontTexAttach;

  protected boolean changedFrontTex = false;
  protected boolean changedBackTex = false;

  // ........................................................

  // Utility arrays to copy projection/modelview matrices to GL

  protected float[] projMatrix;
  protected float[] mvMatrix;

  // ........................................................

  // Static initialization for some parameters that need to be different for
  // JOGL

  static {
    MIN_DIRECT_BUFFER_SIZE = 2;
    INDEX_TYPE             = GL.GL_UNSIGNED_SHORT;
  }


  ///////////////////////////////////////////////////////////////

  // Initialization, finalization


  public PJOGL(PGraphicsOpenGL pg) {
    super(pg);
    if (glu == null) glu = new GLU();
  }


  @Override
  protected void setFps(float fps) {
    if (!setFps || targetFps != fps) {
      if (60 < fps) {
        // Disables v-sync
        gl.setSwapInterval(0);
      } else if (30 < fps) {
        gl.setSwapInterval(1);
      } else {
        gl.setSwapInterval(2);
      }
      targetFps = currentFps = fps;
      setFps = true;
    }
  }


  @Override
  protected void initSurface(int antialias) {
    if (profile == null) {
      if (PROFILE == 2) {
        try {
          profile = GLProfile.getGL2ES1();
        } catch (GLException ex) {
          profile = GLProfile.getMaxFixedFunc(true);
        }
      } else if (PROFILE == 3) {
        try {
          profile = GLProfile.getGL2GL3();
        } catch (GLException ex) {
          profile = GLProfile.getMaxProgrammable(true);
        }
        if (!profile.isGL3()) {
          PGraphics.showWarning("Requested profile GL3 but is not available, got: " + profile);
        }
      } else if (PROFILE == 4) {
        try {
          profile = GLProfile.getGL4ES3();
        } catch (GLException ex) {
          profile = GLProfile.getMaxProgrammable(true);
        }
        if (!profile.isGL4()) {
          PGraphics.showWarning("Requested profile GL4 but is not available, got: " + profile);
        }
      } else throw new RuntimeException(UNSUPPORTED_GLPROF_ERROR);

      if (2 < PROFILE) {
        texVertShaderSource = convertVertexSource(texVertShaderSource, 120, 150);
        tex2DFragShaderSource = convertFragmentSource(tex2DFragShaderSource, 120, 150);
        texRectFragShaderSource = convertFragmentSource(texRectFragShaderSource, 120, 150);
      }
    } else {
      // Restarting...
      if (canvasAWT != null) {
        canvasAWT.removeGLEventListener(listener);
        pg.parent.removeListeners(canvasAWT);
        pg.parent.remove(canvasAWT);
      } else if (canvasNEWT != null) {
        window.removeGLEventListener(listener);
        pg.parent.remove(canvasNEWT);
      }
      sinkFBO = backFBO = frontFBO = null;
    }

    // Setting up the desired capabilities;
    GLCapabilities caps = new GLCapabilities(profile);
    caps.setAlphaBits(REQUESTED_ALPHA_BITS);
    caps.setDepthBits(REQUESTED_DEPTH_BITS);
    caps.setStencilBits(REQUESTED_STENCIL_BITS);

    caps.setBackgroundOpaque(true);
    caps.setOnscreen(true);
    if (USE_FBOLAYER_BY_DEFAULT) {
      if (USE_JOGL_FBOLAYER) {
        caps.setPBuffer(false);
        caps.setFBO(true);
        if (1 < antialias) {
          caps.setSampleBuffers(true);
          caps.setNumSamples(antialias);
        } else {
          caps.setSampleBuffers(false);
        }
        fboLayerRequested = false;
      } else {
        caps.setPBuffer(false);
        caps.setFBO(false);
        caps.setSampleBuffers(false);
        fboLayerRequested = 1 < antialias;
      }
    } else {
      if (1 < antialias) {
        caps.setSampleBuffers(true);
        caps.setNumSamples(antialias);
      } else {
        caps.setSampleBuffers(false);
      }
      fboLayerRequested = false;
    }
    caps.setDepthBits(REQUESTED_DEPTH_BITS);
    caps.setStencilBits(REQUESTED_STENCIL_BITS);
    caps.setAlphaBits(REQUESTED_ALPHA_BITS);
    reqNumSamples = qualityToSamples(antialias);

    if (WINDOW_TOOLKIT == AWT) {
      canvasAWT = new GLCanvas(caps);
      canvasAWT.setBounds(0, 0, pg.width, pg.height);
      canvasAWT.setBackground(new Color(pg.backgroundColor, true));
      canvasAWT.setFocusable(true);

      pg.parent.setLayout(new BorderLayout());
      pg.parent.add(canvasAWT, BorderLayout.CENTER);
      canvasAWT.requestFocusInWindow();

      canvas = canvasAWT;
      canvasNEWT = null;
    } else if (WINDOW_TOOLKIT == NEWT) {
      window = GLWindow.create(caps);
      canvasNEWT = new NewtCanvasAWT(window);
      canvasNEWT.setBounds(0, 0, pg.width, pg.height);
      canvasNEWT.setBackground(new Color(pg.backgroundColor, true));
      canvasNEWT.setFocusable(true);

      pg.parent.setLayout(new BorderLayout());
      pg.parent.add(canvasNEWT, BorderLayout.CENTER);
      canvasNEWT.requestFocusInWindow();

      canvas = canvasNEWT;
      canvasAWT = null;
    }

    registerListeners();

    fboLayerCreated = false;
    fboLayerInUse = false;
    firstFrame = true;
    setFps = false;
  }


  @Override
  protected void reinitSurface() {
    sinkFBO = backFBO = frontFBO = null;
    fboLayerCreated = false;
    fboLayerInUse = false;
    firstFrame = true;
  }


  @Override
  protected void registerListeners() {
    if (WINDOW_TOOLKIT == AWT) {
      pg.parent.removeListeners(pg.parent);
      pg.parent.addListeners(canvasAWT);

      listener = new PGLListener();
      canvasAWT.addGLEventListener(listener);
    } else if (WINDOW_TOOLKIT == NEWT) {
      if (EVENTS_TOOLKIT == NEWT) {
        NEWTMouseListener mouseListener = new NEWTMouseListener();
        window.addMouseListener(mouseListener);
        NEWTKeyListener keyListener = new NEWTKeyListener();
        window.addKeyListener(keyListener);
        NEWTWindowListener winListener = new NEWTWindowListener();
        window.addWindowListener(winListener);
      } else if (EVENTS_TOOLKIT == AWT) {
        pg.parent.removeListeners(canvasNEWT);
        pg.parent.addListeners(canvasNEWT);
      }

      listener = new PGLListener();
      window.addGLEventListener(listener);
    }

    if (canvas != null) {
      canvas.setFocusTraversalKeysEnabled(false);
    }
  }


  @Override
  protected void deleteSurface() {
    super.deleteSurface();

    if (canvasAWT != null) {
      canvasAWT.removeGLEventListener(listener);
      pg.parent.removeListeners(canvasAWT);
    } else if (canvasNEWT != null) {
      window.removeGLEventListener(listener);
    }
    GLProfile.shutdown();
  }


  @Override
  protected int getReadFramebuffer() {
    if (fboLayerInUse) {
      return glColorFbo.get(0);
    } else if (capabilities.isFBO()) {
      return context.getDefaultReadFramebuffer();
    } else {
      return 0;
    }
  }


  @Override
  protected int getDrawFramebuffer() {
    if (fboLayerInUse) {
      if (1 < numSamples) {
        return glMultiFbo.get(0);
      } else {
        return glColorFbo.get(0);
      }
    } else if (capabilities.isFBO()) {
      return context.getDefaultDrawFramebuffer();
    } else {
      return 0;
    }
  }


  @Override
  protected int getDefaultDrawBuffer() {
    if (fboLayerInUse) {
      return COLOR_ATTACHMENT0;
    } else if (capabilities.isFBO()) {
      return GL.GL_COLOR_ATTACHMENT0;
    } else if (capabilities.getDoubleBuffered()) {
      return GL.GL_BACK;
    } else {
      return GL.GL_FRONT;
    }
  }


  @Override
  protected int getDefaultReadBuffer() {
    if (fboLayerInUse) {
      return COLOR_ATTACHMENT0;
    } else if (capabilities.isFBO()) {
      return GL.GL_COLOR_ATTACHMENT0;
    } else if (capabilities.getDoubleBuffered()) {
      return GL.GL_BACK;
    } else {
      return GL.GL_FRONT;
    }
  }


  @Override
  protected boolean isFBOBacked() {
    return super.isFBOBacked() || capabilities.isFBO();
  }


  @Override
  protected int getDepthBits() {
    return capabilities.getDepthBits();
  }


  @Override
  protected int getStencilBits() {
    return capabilities.getStencilBits();
  }


  @Override
  protected Texture wrapBackTexture(Texture texture) {
    if (texture == null || changedBackTex) {
      if (USE_JOGL_FBOLAYER) {
        texture = new Texture();
        texture.init(pg.width, pg.height,
                     backTexAttach.getName(), TEXTURE_2D, RGBA,
                     backTexAttach.getWidth(), backTexAttach.getHeight(),
                     backTexAttach.minFilter, backTexAttach.magFilter,
                     backTexAttach.wrapS, backTexAttach.wrapT);
        texture.invertedY(true);
        texture.colorBuffer(true);
        pg.setCache(pg, texture);
      } else {
        texture = super.wrapBackTexture(null);
      }
    } else {
      if (USE_JOGL_FBOLAYER) {
        texture.glName = backTexAttach.getName();
      } else {
        texture = super.wrapBackTexture(texture);
      }
    }
    return texture;
  }


  @Override
  protected Texture wrapFrontTexture(Texture texture) {
    if (texture == null || changedFrontTex) {
      if (USE_JOGL_FBOLAYER) {
        texture = new Texture();
        texture.init(pg.width, pg.height,
                     backTexAttach.getName(), TEXTURE_2D, RGBA,
                     frontTexAttach.getWidth(), frontTexAttach.getHeight(),
                     frontTexAttach.minFilter, frontTexAttach.magFilter,
                     frontTexAttach.wrapS, frontTexAttach.wrapT);
        texture.invertedY(true);
        texture.colorBuffer(true);
      } else {
        texture = super.wrapFrontTexture(null);
      }
    } else {
      if (USE_JOGL_FBOLAYER) {
        texture.glName = frontTexAttach.getName();
      } else {
        texture = super.wrapFrontTexture(texture);
      }
    }
    return texture;
  }


  @Override
  protected void bindFrontTexture() {
    if (USE_JOGL_FBOLAYER) {
      usingFrontTex = true;
      if (!texturingIsEnabled(TEXTURE_2D)) {
        enableTexturing(TEXTURE_2D);
      }
      bindTexture(TEXTURE_2D, frontTexAttach.getName());
    } else super.bindFrontTexture();
  }


  @Override
  protected void unbindFrontTexture() {
    if (USE_JOGL_FBOLAYER) {
      if (textureIsBound(TEXTURE_2D, frontTexAttach.getName())) {
        // We don't want to unbind another texture
        // that might be bound instead of this one.
        if (!texturingIsEnabled(TEXTURE_2D)) {
          enableTexturing(TEXTURE_2D);
          bindTexture(TEXTURE_2D, 0);
          disableTexturing(TEXTURE_2D);
        } else {
          bindTexture(TEXTURE_2D, 0);
        }
      }
    } else super.unbindFrontTexture();
  }


  @Override
  protected void syncBackTexture() {
    if (USE_JOGL_FBOLAYER) {
      if (usingFrontTex) needSepFrontTex = true;
      if (1 < numSamples) {
        backFBO.syncSamplingSink(gl);
        backFBO.bind(gl);
      }
    } else super.syncBackTexture();
  }


  @Override
  protected void beginDraw(boolean clear0) {
    if (!setFps) setFps(targetFps);
    if (USE_JOGL_FBOLAYER) return;
    super.beginDraw(clear0);
  }


  @Override
  protected void endDraw(boolean clear0) {
    if (isFBOBacked()) {
      if (USE_JOGL_FBOLAYER) {
        if (!clear0 && isFBOBacked() && !isMultisampled()) {
          // Draw the back texture into the front texture, which will be used as
          // back texture in the next frame. Otherwise flickering will occur if
          // the sketch uses "incremental drawing" (background() not called).
          frontFBO.bind(gl);
          gl.glDisable(GL.GL_BLEND);
          drawTexture(TEXTURE_2D, backTexAttach.getName(),
                      backTexAttach.getWidth(), backTexAttach.getHeight(),
                      pg.width, pg.height,
                      0, 0, pg.width, pg.height, 0, 0, pg.width, pg.height);
          backFBO.bind(gl);
        }
      } else {
        super.endDraw(clear0);
      }
    }
  }


  @Override
  protected boolean canDraw() {
    return pg.initialized && pg.parent.isDisplayable();
  }


  @Override
  protected void requestFocus() { }


  @Override
  protected void requestDraw() {
    boolean canDraw = pg.parent.canDraw();
    if (pg.initialized && (canDraw || prevCanDraw)) {
      try {
        drawLatch = new CountDownLatch(1);
        if (WINDOW_TOOLKIT == AWT) {
          canvasAWT.display();
        } else if (WINDOW_TOOLKIT == NEWT) {
          window.display();
        }
        try {
          drawLatch.await(DRAW_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }

        if (canDraw) prevCanDraw = true;
        else prevCanDraw = false;
      } catch (GLException e) {
        // Unwrap GLException so that only the causing exception is shown.
        Throwable tr = e.getCause();
        if (tr instanceof RuntimeException) {
          throw (RuntimeException)tr;
        } else {
          throw new RuntimeException(tr);
        }
      }
    }
  }


  @Override
  protected void swapBuffers() {
    if (WINDOW_TOOLKIT == AWT) {
      canvasAWT.swapBuffers();
    } else if (WINDOW_TOOLKIT == NEWT) {
      window.swapBuffers();
    }
  }


  @Override
  protected void beginGL() {
    if (gl2x != null) {
      if (projMatrix == null) {
        projMatrix = new float[16];
      }
      gl2x.glMatrixMode(GLMatrixFunc.GL_PROJECTION);
      projMatrix[ 0] = pg.projection.m00;
      projMatrix[ 1] = pg.projection.m10;
      projMatrix[ 2] = pg.projection.m20;
      projMatrix[ 3] = pg.projection.m30;
      projMatrix[ 4] = pg.projection.m01;
      projMatrix[ 5] = pg.projection.m11;
      projMatrix[ 6] = pg.projection.m21;
      projMatrix[ 7] = pg.projection.m31;
      projMatrix[ 8] = pg.projection.m02;
      projMatrix[ 9] = pg.projection.m12;
      projMatrix[10] = pg.projection.m22;
      projMatrix[11] = pg.projection.m32;
      projMatrix[12] = pg.projection.m03;
      projMatrix[13] = pg.projection.m13;
      projMatrix[14] = pg.projection.m23;
      projMatrix[15] = pg.projection.m33;
      gl2x.glLoadMatrixf(projMatrix, 0);

      if (mvMatrix == null) {
        mvMatrix = new float[16];
      }
      gl2x.glMatrixMode(GLMatrixFunc.GL_MODELVIEW);
      mvMatrix[ 0] = pg.modelview.m00;
      mvMatrix[ 1] = pg.modelview.m10;
      mvMatrix[ 2] = pg.modelview.m20;
      mvMatrix[ 3] = pg.modelview.m30;
      mvMatrix[ 4] = pg.modelview.m01;
      mvMatrix[ 5] = pg.modelview.m11;
      mvMatrix[ 6] = pg.modelview.m21;
      mvMatrix[ 7] = pg.modelview.m31;
      mvMatrix[ 8] = pg.modelview.m02;
      mvMatrix[ 9] = pg.modelview.m12;
      mvMatrix[10] = pg.modelview.m22;
      mvMatrix[11] = pg.modelview.m32;
      mvMatrix[12] = pg.modelview.m03;
      mvMatrix[13] = pg.modelview.m13;
      mvMatrix[14] = pg.modelview.m23;
      mvMatrix[15] = pg.modelview.m33;
      gl2x.glLoadMatrixf(mvMatrix, 0);
    }
  }


  @Override
  protected boolean hasFBOs() {
    if (context.hasBasicFBOSupport()) return true;
    else return super.hasFBOs();
  }


  @Override
  protected boolean hasShaders() {
    if (context.hasGLSL()) return true;
    else return super.hasShaders();
  }


  ///////////////////////////////////////////////////////////

  // JOGL event listeners


  protected class PGLListener implements GLEventListener {
    public PGLListener() {}

    @Override
    public void display(GLAutoDrawable glDrawable) {
      getGL(glDrawable);

      if (USE_JOGL_FBOLAYER && capabilities.isFBO()) {
        // The onscreen drawing surface is backed by an FBO layer.
        GLFBODrawable fboDrawable = null;

        if (WINDOW_TOOLKIT == AWT) {
          GLCanvas glCanvas = (GLCanvas)glDrawable;
          fboDrawable = (GLFBODrawable)glCanvas.getDelegatedDrawable();
        } else {
          GLWindow glWindow = (GLWindow)glDrawable;
          fboDrawable = (GLFBODrawable)glWindow.getDelegatedDrawable();
        }

        if (fboDrawable != null) {
          backFBO = fboDrawable.getFBObject(GL.GL_BACK);
          if (1 < numSamples) {
            if (needSepFrontTex) {
              // When using multisampled FBO, the back buffer is the MSAA
              // surface so it cannot be read from. The sink buffer contains
              // the readable 2D texture.
              // In this case, we create an auxiliary "front" buffer that it is
              // swapped with the sink buffer at the beginning of each frame.
              // In this way, we always have a readable copy of the previous
              // frame in the front texture, while the back is synchronized
              // with the contents of the MSAA back buffer when requested.
              if (frontFBO == null) {
                // init
                frontFBO = new FBObject();
                frontFBO.reset(gl, pg.width, pg.height);
                frontFBO.attachTexture2D(gl, 0, true);
                sinkFBO = backFBO.getSamplingSinkFBO();
                changedFrontTex = changedBackTex = true;
              } else {
                // swap
                FBObject temp = sinkFBO;
                sinkFBO = frontFBO;
                frontFBO = temp;
                backFBO.setSamplingSink(sinkFBO);
                changedFrontTex = changedBackTex = false;
              }
              backTexAttach  = (FBObject.TextureAttachment) sinkFBO.
                               getColorbuffer(0);
              frontTexAttach = (FBObject.TextureAttachment)frontFBO.
                               getColorbuffer(0);
            } else {
              // Default setting (to save resources): the front and back
              // textures are the same.
              sinkFBO = backFBO.getSamplingSinkFBO();
              backTexAttach = (FBObject.TextureAttachment) sinkFBO.
                              getColorbuffer(0);
              frontTexAttach = backTexAttach;
            }

          } else {
            // w/out multisampling, rendering is done on the back buffer.
            frontFBO = fboDrawable.getFBObject(GL.GL_FRONT);

            backTexAttach  = fboDrawable.getTextureBuffer(GL.GL_BACK);
            frontTexAttach = fboDrawable.getTextureBuffer(GL.GL_FRONT);
          }
        }
      }

      pg.parent.handleDraw();
      drawLatch.countDown();
    }

    @Override
    public void dispose(GLAutoDrawable adrawable) {
    }

    @Override
    public void init(GLAutoDrawable glDrawable) {
      getGL(glDrawable);

      capabilities = glDrawable.getChosenGLCapabilities();
      if (!hasFBOs()) {
        throw new RuntimeException(MISSING_FBO_ERROR);
      }
      if (!hasShaders()) {
        throw new RuntimeException(MISSING_GLSL_ERROR);
      }
      if (USE_JOGL_FBOLAYER && capabilities.isFBO()) {
        int maxs = maxSamples();
        numSamples = PApplet.min(capabilities.getNumSamples(), maxs);
      }
    }

    @Override
    public void reshape(GLAutoDrawable glDrawable, int x, int y, int w, int h) {
      //getGL(glDrawable);
    }

    private void getGL(GLAutoDrawable glDrawable) {
      drawable = glDrawable;
      context = glDrawable.getContext();
      glContext = context.hashCode();
      glThread = Thread.currentThread();

      gl = context.getGL();
      gl2 = gl.getGL2ES2();
      try {
        gl2x = gl.getGL2();
      } catch (javax.media.opengl.GLException e) {
        gl2x = null;
      }
      try {
        gl3 = gl.getGL2GL3();
      } catch (javax.media.opengl.GLException e) {
        gl3 = null;
      }
    }
  }

  protected void nativeMouseEvent(com.jogamp.newt.event.MouseEvent nativeEvent,
                                  int peAction) {
    int modifiers = nativeEvent.getModifiers();
    int peModifiers = modifiers &
                      (InputEvent.SHIFT_MASK |
                       InputEvent.CTRL_MASK |
                       InputEvent.META_MASK |
                       InputEvent.ALT_MASK);

    int peButton = 0;
    if ((modifiers & InputEvent.BUTTON1_MASK) != 0) {
      peButton = PConstants.LEFT;
    } else if ((modifiers & InputEvent.BUTTON2_MASK) != 0) {
      peButton = PConstants.CENTER;
    } else if ((modifiers & InputEvent.BUTTON3_MASK) != 0) {
      peButton = PConstants.RIGHT;
    }

    if (PApplet.platform == PConstants.MACOSX) {
      //if (nativeEvent.isPopupTrigger()) {
      if ((modifiers & InputEvent.CTRL_MASK) != 0) {
        peButton = PConstants.RIGHT;
      }
    }

    int peCount = 0;
    if (peAction == MouseEvent.WHEEL) {
      peCount = nativeEvent.isShiftDown() ? (int)nativeEvent.getRotation()[0] :
                                            (int)nativeEvent.getRotation()[1];
    } else {
      peCount = nativeEvent.getClickCount();
    }

    MouseEvent me = new MouseEvent(nativeEvent, nativeEvent.getWhen(),
                                   peAction, peModifiers,
                                   nativeEvent.getX(), nativeEvent.getY(),
                                   peButton,
                                   peCount);

    pg.parent.postEvent(me);
  }

  protected void nativeKeyEvent(com.jogamp.newt.event.KeyEvent nativeEvent,
                                int peAction) {
    int peModifiers = nativeEvent.getModifiers() &
                      (InputEvent.SHIFT_MASK |
                       InputEvent.CTRL_MASK |
                       InputEvent.META_MASK |
                       InputEvent.ALT_MASK);

    char keyChar;
    if ((int)nativeEvent.getKeyChar() == 0) {
      keyChar = PConstants.CODED;
    } else {
      keyChar = nativeEvent.getKeyChar();
    }

    KeyEvent ke = new KeyEvent(nativeEvent, nativeEvent.getWhen(),
                               peAction, peModifiers,
                               keyChar,
                               nativeEvent.getKeyCode());

    pg.parent.postEvent(ke);
  }

  protected class NEWTWindowListener implements com.jogamp.newt.event.WindowListener {
    @Override
    public void windowGainedFocus(com.jogamp.newt.event.WindowEvent arg0) {
      pg.parent.focusGained(null);
    }

    @Override
    public void windowLostFocus(com.jogamp.newt.event.WindowEvent arg0) {
      pg.parent.focusLost(null);
    }

    @Override
    public void windowDestroyNotify(com.jogamp.newt.event.WindowEvent arg0) {
    }

    @Override
    public void windowDestroyed(com.jogamp.newt.event.WindowEvent arg0) {
    }

    @Override
    public void windowMoved(com.jogamp.newt.event.WindowEvent arg0) {
    }

    @Override
    public void windowRepaint(com.jogamp.newt.event.WindowUpdateEvent arg0) {
    }

    @Override
    public void windowResized(com.jogamp.newt.event.WindowEvent arg0) { }
  }

  // NEWT mouse listener
  protected class NEWTMouseListener extends com.jogamp.newt.event.MouseAdapter {
    @Override
    public void mousePressed(com.jogamp.newt.event.MouseEvent e) {
      nativeMouseEvent(e, MouseEvent.PRESS);
    }
    @Override
    public void mouseReleased(com.jogamp.newt.event.MouseEvent e) {
      nativeMouseEvent(e, MouseEvent.RELEASE);
    }
    @Override
    public void mouseClicked(com.jogamp.newt.event.MouseEvent e) {
      nativeMouseEvent(e, MouseEvent.CLICK);
    }
    @Override
    public void mouseDragged(com.jogamp.newt.event.MouseEvent e) {
      nativeMouseEvent(e, MouseEvent.DRAG);
    }
    @Override
    public void mouseMoved(com.jogamp.newt.event.MouseEvent e) {
      nativeMouseEvent(e, MouseEvent.MOVE);
    }
    @Override
    public void mouseWheelMoved(com.jogamp.newt.event.MouseEvent e) {
      nativeMouseEvent(e, MouseEvent.WHEEL);
    }
    @Override
    public void mouseEntered(com.jogamp.newt.event.MouseEvent e) {
      nativeMouseEvent(e, MouseEvent.ENTER);
    }
    @Override
    public void mouseExited(com.jogamp.newt.event.MouseEvent e) {
      nativeMouseEvent(e, MouseEvent.EXIT);
    }
  }

  // NEWT key listener
  protected class NEWTKeyListener extends com.jogamp.newt.event.KeyAdapter {
    @Override
    public void keyPressed(com.jogamp.newt.event.KeyEvent e) {
      nativeKeyEvent(e, KeyEvent.PRESS);
    }
    @Override
    public void keyReleased(com.jogamp.newt.event.KeyEvent e) {
      nativeKeyEvent(e, KeyEvent.RELEASE);
    }
    public void keyTyped(com.jogamp.newt.event.KeyEvent e)  {
      nativeKeyEvent(e, KeyEvent.TYPE);
    }
  }


  ///////////////////////////////////////////////////////////

  // Utility functions


  @Override
  protected void enableTexturing(int target) {
    if (PROFILE == 2) enable(target);
    if (target == TEXTURE_2D) {
      texturingTargets[0] = true;
    } else if (target == TEXTURE_RECTANGLE) {
      texturingTargets[1] = true;
    }
  }


  @Override
  protected void disableTexturing(int target) {
    if (PROFILE == 2) disable(target);
    if (target == TEXTURE_2D) {
      texturingTargets[0] = false;
    } else if (target == TEXTURE_RECTANGLE) {
      texturingTargets[1] = false;
    }
  }



  @Override
  protected int getFontAscent(Object font) {
    FontMetrics metrics = pg.parent.getFontMetrics((Font)font);
    return metrics.getAscent();
  }


  @Override
  protected int getFontDescent(Object font) {
    FontMetrics metrics = pg.parent.getFontMetrics((Font)font);
    return metrics.getDescent();
  }


  @Override
  protected int getTextWidth(Object font, char buffer[], int start, int stop) {
    // maybe should use one of the newer/fancier functions for this?
    int length = stop - start;
    FontMetrics metrics = pg.parent.getFontMetrics((Font)font);
    return metrics.charsWidth(buffer, start, length);
  }


  @Override
  protected Object getDerivedFont(Object font, float size) {
    return ((Font)font).deriveFont(size);
  }


  @Override
  protected String[] loadVertexShader(String filename, int version) {
    if (2 < PROFILE && version < 150) {
      String[] fragSrc0 = pg.parent.loadStrings(filename);
      return convertFragmentSource(fragSrc0, version, 150);
    } else {
      return pg.parent.loadStrings(filename);
    }
  }


  @Override
  protected String[] loadFragmentShader(String filename, int version) {
    if (2 < PROFILE && version < 150) {
      String[] vertSrc0 = pg.parent.loadStrings(filename);
      return convertVertexSource(vertSrc0, version, 150);
    } else {
      return pg.parent.loadStrings(filename);
    }
  }


  @Override
  protected String[] loadFragmentShader(URL url, int version) {
    try {
      if (2 < PROFILE && version < 150) {
        String[] fragSrc0 = PApplet.loadStrings(url.openStream());
        return convertFragmentSource(fragSrc0, version, 150);
      } else {
        return PApplet.loadStrings(url.openStream());
      }
    } catch (IOException e) {
      PGraphics.showException("Cannot load fragment shader " + url.getFile());
    }
    return null;
  }


  @Override
  protected String[] loadVertexShader(URL url, int version) {
    try {
      if (2 < PROFILE && version < 150) {
        String[] vertSrc0 = PApplet.loadStrings(url.openStream());
        return convertVertexSource(vertSrc0, version, 150);
      } else {
        return PApplet.loadStrings(url.openStream());
      }
    } catch (IOException e) {
      PGraphics.showException("Cannot load vertex shader " + url.getFile());
    }
    return null;
  }


  @Override
  protected String[] convertFragmentSource(String[] fragSrc0,
                                           int version0, int version1) {
    String[] fragSrc = new String[fragSrc0.length + 2];
    fragSrc[0] = "#version 150";
    fragSrc[1] = "out vec4 fragColor;";
    for (int i = 0; i < fragSrc0.length; i++) {
      String line = fragSrc0[i];
      line = line.replace("varying", "in");
      line = line.replace("attribute", "in");
      line = line.replace("gl_FragColor", "fragColor");
      line = line.replace("texture", "texMap");
      line = line.replace("texMap2D(", "texture(");
      line = line.replace("texMap2DRect(", "texture(");
      fragSrc[i + 2] = line;
    }
    return fragSrc;
  }


  @Override
  protected String[] convertVertexSource(String[] vertSrc0,
                                         int version0, int version1) {
    String[] vertSrc = new String[vertSrc0.length + 1];
    vertSrc[0] = "#version 150";
    for (int i = 0; i < vertSrc0.length; i++) {
      String line = vertSrc0[i];
      line = line.replace("attribute", "in");
      line = line.replace("varying", "out");
      vertSrc[i + 1] = line;
    }
    return vertSrc;
  }


  ///////////////////////////////////////////////////////////

  // Tessellator


  @Override
  protected Tessellator createTessellator(TessellatorCallback callback) {
    return new Tessellator(callback);
  }


  protected class Tessellator implements PGL.Tessellator {
    protected GLUtessellator tess;
    protected TessellatorCallback callback;
    protected GLUCallback gluCallback;

    public Tessellator(TessellatorCallback callback) {
      this.callback = callback;
      tess = GLU.gluNewTess();
      gluCallback = new GLUCallback();

      GLU.gluTessCallback(tess, GLU.GLU_TESS_BEGIN, gluCallback);
      GLU.gluTessCallback(tess, GLU.GLU_TESS_END, gluCallback);
      GLU.gluTessCallback(tess, GLU.GLU_TESS_VERTEX, gluCallback);
      GLU.gluTessCallback(tess, GLU.GLU_TESS_COMBINE, gluCallback);
      GLU.gluTessCallback(tess, GLU.GLU_TESS_ERROR, gluCallback);
    }

    @Override
    public void beginPolygon() {
      GLU.gluTessBeginPolygon(tess, null);
    }

    @Override
    public void endPolygon() {
      GLU.gluTessEndPolygon(tess);
    }

    @Override
    public void setWindingRule(int rule) {
      GLU.gluTessProperty(tess, GLU.GLU_TESS_WINDING_RULE, rule);
    }

    @Override
    public void beginContour() {
      GLU.gluTessBeginContour(tess);
    }

    @Override
    public void endContour() {
      GLU.gluTessEndContour(tess);
    }

    @Override
    public void addVertex(double[] v) {
      GLU.gluTessVertex(tess, v, 0, v);
    }

    protected class GLUCallback extends GLUtessellatorCallbackAdapter {
      @Override
      public void begin(int type) {
        callback.begin(type);
      }

      @Override
      public void end() {
        callback.end();
      }

      @Override
      public void vertex(Object data) {
        callback.vertex(data);
      }

      @Override
      public void combine(double[] coords, Object[] data,
                          float[] weight, Object[] outData) {
        callback.combine(coords, data, weight, outData);
      }

      @Override
      public void error(int errnum) {
        callback.error(errnum);
      }
    }
  }


  @Override
  protected String tessError(int err) {
    return glu.gluErrorString(err);
  }


  ///////////////////////////////////////////////////////////

  // Font outline


  static {
    SHAPE_TEXT_SUPPORTED = true;
    SEG_MOVETO  = PathIterator.SEG_MOVETO;
    SEG_LINETO  = PathIterator.SEG_LINETO;
    SEG_QUADTO  = PathIterator.SEG_QUADTO;
    SEG_CUBICTO = PathIterator.SEG_CUBICTO;
    SEG_CLOSE   = PathIterator.SEG_CLOSE;
  }


  @Override
  protected FontOutline createFontOutline(char ch, Object font) {
    return new FontOutline(ch, font);
  }


  protected class FontOutline implements PGL.FontOutline {
    PathIterator iter;

    public FontOutline(char ch, Object font) {
      char textArray[] = new char[] { ch };
      Graphics2D graphics = (Graphics2D) pg.parent.getGraphics();
      FontRenderContext frc = graphics.getFontRenderContext();
      GlyphVector gv = ((Font)font).createGlyphVector(frc, textArray);
      Shape shp = gv.getOutline();
      iter = shp.getPathIterator(null);
    }

    public boolean isDone() {
      return iter.isDone();
    }

    public int currentSegment(float coords[]) {
      return iter.currentSegment(coords);
    }

    public void next() {
      iter.next();
    }
  }


  ///////////////////////////////////////////////////////////

  // Constants

  static {
    FALSE = GL.GL_FALSE;
    TRUE  = GL.GL_TRUE;

    INT            = GL2ES2.GL_INT;
    BYTE           = GL.GL_BYTE;
    SHORT          = GL.GL_SHORT;
    FLOAT          = GL.GL_FLOAT;
    BOOL           = GL2ES2.GL_BOOL;
    UNSIGNED_INT   = GL.GL_UNSIGNED_INT;
    UNSIGNED_BYTE  = GL.GL_UNSIGNED_BYTE;
    UNSIGNED_SHORT = GL.GL_UNSIGNED_SHORT;

    RGB             = GL.GL_RGB;
    RGBA            = GL.GL_RGBA;
    ALPHA           = GL.GL_ALPHA;
    LUMINANCE       = GL.GL_LUMINANCE;
    LUMINANCE_ALPHA = GL.GL_LUMINANCE_ALPHA;

    UNSIGNED_SHORT_5_6_5   = GL.GL_UNSIGNED_SHORT_5_6_5;
    UNSIGNED_SHORT_4_4_4_4 = GL.GL_UNSIGNED_SHORT_4_4_4_4;
    UNSIGNED_SHORT_5_5_5_1 = GL.GL_UNSIGNED_SHORT_5_5_5_1;

    RGBA4   = GL.GL_RGBA4;
    RGB5_A1 = GL.GL_RGB5_A1;
    RGB565  = GL.GL_RGB565;
    RGB8    = GL.GL_RGB8;
    RGBA8   = GL.GL_RGBA8;
    ALPHA8  = GL.GL_ALPHA8;

    READ_ONLY  = GL2GL3.GL_READ_ONLY;
    WRITE_ONLY = GL.GL_WRITE_ONLY;
    READ_WRITE = GL2GL3.GL_READ_WRITE;

    TESS_WINDING_NONZERO = GLU.GLU_TESS_WINDING_NONZERO;
    TESS_WINDING_ODD     = GLU.GLU_TESS_WINDING_ODD;

    GENERATE_MIPMAP_HINT = GL.GL_GENERATE_MIPMAP_HINT;
    FASTEST              = GL.GL_FASTEST;
    NICEST               = GL.GL_NICEST;
    DONT_CARE            = GL.GL_DONT_CARE;

    VENDOR                   = GL.GL_VENDOR;
    RENDERER                 = GL.GL_RENDERER;
    VERSION                  = GL.GL_VERSION;
    EXTENSIONS               = GL.GL_EXTENSIONS;
    SHADING_LANGUAGE_VERSION = GL2ES2.GL_SHADING_LANGUAGE_VERSION;

    MAX_SAMPLES = GL2ES3.GL_MAX_SAMPLES;
    SAMPLES     = GL.GL_SAMPLES;

    ALIASED_LINE_WIDTH_RANGE = GL.GL_ALIASED_LINE_WIDTH_RANGE;
    ALIASED_POINT_SIZE_RANGE = GL.GL_ALIASED_POINT_SIZE_RANGE;

    DEPTH_BITS   = GL.GL_DEPTH_BITS;
    STENCIL_BITS = GL.GL_STENCIL_BITS;

    CCW = GL.GL_CCW;
    CW  = GL.GL_CW;

    VIEWPORT = GL.GL_VIEWPORT;

    ARRAY_BUFFER         = GL.GL_ARRAY_BUFFER;
    ELEMENT_ARRAY_BUFFER = GL.GL_ELEMENT_ARRAY_BUFFER;

    MAX_VERTEX_ATTRIBS  = GL2ES2.GL_MAX_VERTEX_ATTRIBS;

    STATIC_DRAW  = GL.GL_STATIC_DRAW;
    DYNAMIC_DRAW = GL.GL_DYNAMIC_DRAW;
    STREAM_DRAW  = GL2ES2.GL_STREAM_DRAW;

    BUFFER_SIZE  = GL.GL_BUFFER_SIZE;
    BUFFER_USAGE = GL.GL_BUFFER_USAGE;

    POINTS         = GL.GL_POINTS;
    LINE_STRIP     = GL.GL_LINE_STRIP;
    LINE_LOOP      = GL.GL_LINE_LOOP;
    LINES          = GL.GL_LINES;
    TRIANGLE_FAN   = GL.GL_TRIANGLE_FAN;
    TRIANGLE_STRIP = GL.GL_TRIANGLE_STRIP;
    TRIANGLES      = GL.GL_TRIANGLES;

    CULL_FACE      = GL.GL_CULL_FACE;
    FRONT          = GL.GL_FRONT;
    BACK           = GL.GL_BACK;
    FRONT_AND_BACK = GL.GL_FRONT_AND_BACK;

    POLYGON_OFFSET_FILL = GL.GL_POLYGON_OFFSET_FILL;

    UNPACK_ALIGNMENT = GL.GL_UNPACK_ALIGNMENT;
    PACK_ALIGNMENT   = GL.GL_PACK_ALIGNMENT;

    TEXTURE_2D        = GL.GL_TEXTURE_2D;
    TEXTURE_RECTANGLE = GL2GL3.GL_TEXTURE_RECTANGLE;

    TEXTURE_BINDING_2D        = GL.GL_TEXTURE_BINDING_2D;
    TEXTURE_BINDING_RECTANGLE = GL2GL3.GL_TEXTURE_BINDING_RECTANGLE;

    MAX_TEXTURE_SIZE           = GL.GL_MAX_TEXTURE_SIZE;
    TEXTURE_MAX_ANISOTROPY     = GL.GL_TEXTURE_MAX_ANISOTROPY_EXT;
    MAX_TEXTURE_MAX_ANISOTROPY = GL.GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT;

    MAX_VERTEX_TEXTURE_IMAGE_UNITS   = GL2ES2.GL_MAX_VERTEX_TEXTURE_IMAGE_UNITS;
    MAX_TEXTURE_IMAGE_UNITS          = GL2ES2.GL_MAX_TEXTURE_IMAGE_UNITS;
    MAX_COMBINED_TEXTURE_IMAGE_UNITS = GL2ES2.GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS;

    NUM_COMPRESSED_TEXTURE_FORMATS = GL.GL_NUM_COMPRESSED_TEXTURE_FORMATS;
    COMPRESSED_TEXTURE_FORMATS     = GL.GL_COMPRESSED_TEXTURE_FORMATS;

    NEAREST               = GL.GL_NEAREST;
    LINEAR                = GL.GL_LINEAR;
    LINEAR_MIPMAP_NEAREST = GL.GL_LINEAR_MIPMAP_NEAREST;
    LINEAR_MIPMAP_LINEAR  = GL.GL_LINEAR_MIPMAP_LINEAR;

    CLAMP_TO_EDGE = GL.GL_CLAMP_TO_EDGE;
    REPEAT        = GL.GL_REPEAT;

    TEXTURE0           = GL.GL_TEXTURE0;
    TEXTURE1           = GL.GL_TEXTURE1;
    TEXTURE2           = GL.GL_TEXTURE2;
    TEXTURE3           = GL.GL_TEXTURE3;
    TEXTURE_MIN_FILTER = GL.GL_TEXTURE_MIN_FILTER;
    TEXTURE_MAG_FILTER = GL.GL_TEXTURE_MAG_FILTER;
    TEXTURE_WRAP_S     = GL.GL_TEXTURE_WRAP_S;
    TEXTURE_WRAP_T     = GL.GL_TEXTURE_WRAP_T;
    TEXTURE_WRAP_R     = GL2ES2.GL_TEXTURE_WRAP_R;

    TEXTURE_CUBE_MAP = GL.GL_TEXTURE_CUBE_MAP;
    TEXTURE_CUBE_MAP_POSITIVE_X = GL.GL_TEXTURE_CUBE_MAP_POSITIVE_X;
    TEXTURE_CUBE_MAP_POSITIVE_Y = GL.GL_TEXTURE_CUBE_MAP_POSITIVE_Y;
    TEXTURE_CUBE_MAP_POSITIVE_Z = GL.GL_TEXTURE_CUBE_MAP_POSITIVE_Z;
    TEXTURE_CUBE_MAP_NEGATIVE_X = GL.GL_TEXTURE_CUBE_MAP_NEGATIVE_X;
    TEXTURE_CUBE_MAP_NEGATIVE_Y = GL.GL_TEXTURE_CUBE_MAP_NEGATIVE_Y;
    TEXTURE_CUBE_MAP_NEGATIVE_Z = GL.GL_TEXTURE_CUBE_MAP_NEGATIVE_Z;

    VERTEX_SHADER        = GL2ES2.GL_VERTEX_SHADER;
    FRAGMENT_SHADER      = GL2ES2.GL_FRAGMENT_SHADER;
    INFO_LOG_LENGTH      = GL2ES2.GL_INFO_LOG_LENGTH;
    SHADER_SOURCE_LENGTH = GL2ES2.GL_SHADER_SOURCE_LENGTH;
    COMPILE_STATUS       = GL2ES2.GL_COMPILE_STATUS;
    LINK_STATUS          = GL2ES2.GL_LINK_STATUS;
    VALIDATE_STATUS      = GL2ES2.GL_VALIDATE_STATUS;
    SHADER_TYPE          = GL2ES2.GL_SHADER_TYPE;
    DELETE_STATUS        = GL2ES2.GL_DELETE_STATUS;

    FLOAT_VEC2   = GL2ES2.GL_FLOAT_VEC2;
    FLOAT_VEC3   = GL2ES2.GL_FLOAT_VEC3;
    FLOAT_VEC4   = GL2ES2.GL_FLOAT_VEC4;
    FLOAT_MAT2   = GL2ES2.GL_FLOAT_MAT2;
    FLOAT_MAT3   = GL2ES2.GL_FLOAT_MAT3;
    FLOAT_MAT4   = GL2ES2.GL_FLOAT_MAT4;
    INT_VEC2     = GL2ES2.GL_INT_VEC2;
    INT_VEC3     = GL2ES2.GL_INT_VEC3;
    INT_VEC4     = GL2ES2.GL_INT_VEC4;
    BOOL_VEC2    = GL2ES2.GL_BOOL_VEC2;
    BOOL_VEC3    = GL2ES2.GL_BOOL_VEC3;
    BOOL_VEC4    = GL2ES2.GL_BOOL_VEC4;
    SAMPLER_2D   = GL2ES2.GL_SAMPLER_2D;
    SAMPLER_CUBE = GL2ES2.GL_SAMPLER_CUBE;

    LOW_FLOAT    = GL2ES2.GL_LOW_FLOAT;
    MEDIUM_FLOAT = GL2ES2.GL_MEDIUM_FLOAT;
    HIGH_FLOAT   = GL2ES2.GL_HIGH_FLOAT;
    LOW_INT      = GL2ES2.GL_LOW_INT;
    MEDIUM_INT   = GL2ES2.GL_MEDIUM_INT;
    HIGH_INT     = GL2ES2.GL_HIGH_INT;

    CURRENT_VERTEX_ATTRIB = GL2ES2.GL_CURRENT_VERTEX_ATTRIB;

    VERTEX_ATTRIB_ARRAY_BUFFER_BINDING = GL2ES2.GL_VERTEX_ATTRIB_ARRAY_BUFFER_BINDING;
    VERTEX_ATTRIB_ARRAY_ENABLED        = GL2ES2.GL_VERTEX_ATTRIB_ARRAY_ENABLED;
    VERTEX_ATTRIB_ARRAY_SIZE           = GL2ES2.GL_VERTEX_ATTRIB_ARRAY_SIZE;
    VERTEX_ATTRIB_ARRAY_STRIDE         = GL2ES2.GL_VERTEX_ATTRIB_ARRAY_STRIDE;
    VERTEX_ATTRIB_ARRAY_TYPE           = GL2ES2.GL_VERTEX_ATTRIB_ARRAY_TYPE;
    VERTEX_ATTRIB_ARRAY_NORMALIZED     = GL2ES2.GL_VERTEX_ATTRIB_ARRAY_NORMALIZED;
    VERTEX_ATTRIB_ARRAY_POINTER        = GL2ES2.GL_VERTEX_ATTRIB_ARRAY_POINTER;

    BLEND               = GL.GL_BLEND;
    ONE                 = GL.GL_ONE;
    ZERO                = GL.GL_ZERO;
    SRC_ALPHA           = GL.GL_SRC_ALPHA;
    DST_ALPHA           = GL.GL_DST_ALPHA;
    ONE_MINUS_SRC_ALPHA = GL.GL_ONE_MINUS_SRC_ALPHA;
    ONE_MINUS_DST_COLOR = GL.GL_ONE_MINUS_DST_COLOR;
    ONE_MINUS_SRC_COLOR = GL.GL_ONE_MINUS_SRC_COLOR;
    DST_COLOR           = GL.GL_DST_COLOR;
    SRC_COLOR           = GL.GL_SRC_COLOR;

    SAMPLE_ALPHA_TO_COVERAGE = GL.GL_SAMPLE_ALPHA_TO_COVERAGE;
    SAMPLE_COVERAGE          = GL.GL_SAMPLE_COVERAGE;

    KEEP      = GL.GL_KEEP;
    REPLACE   = GL.GL_REPLACE;
    INCR      = GL.GL_INCR;
    DECR      = GL.GL_DECR;
    INVERT    = GL.GL_INVERT;
    INCR_WRAP = GL.GL_INCR_WRAP;
    DECR_WRAP = GL.GL_DECR_WRAP;
    NEVER     = GL.GL_NEVER;
    ALWAYS    = GL.GL_ALWAYS;

    EQUAL    = GL.GL_EQUAL;
    LESS     = GL.GL_LESS;
    LEQUAL   = GL.GL_LEQUAL;
    GREATER  = GL.GL_GREATER;
    GEQUAL   = GL.GL_GEQUAL;
    NOTEQUAL = GL.GL_NOTEQUAL;

    FUNC_ADD              = GL.GL_FUNC_ADD;
    FUNC_MIN              = GL2ES3.GL_MIN;
    FUNC_MAX              = GL2ES3.GL_MAX;
    FUNC_REVERSE_SUBTRACT = GL.GL_FUNC_REVERSE_SUBTRACT;
    FUNC_SUBTRACT         = GL.GL_FUNC_SUBTRACT;

    DITHER = GL.GL_DITHER;

    CONSTANT_COLOR           = GL2ES2.GL_CONSTANT_COLOR;
    CONSTANT_ALPHA           = GL2ES2.GL_CONSTANT_ALPHA;
    ONE_MINUS_CONSTANT_COLOR = GL2ES2.GL_ONE_MINUS_CONSTANT_COLOR;
    ONE_MINUS_CONSTANT_ALPHA = GL2ES2.GL_ONE_MINUS_CONSTANT_ALPHA;
    SRC_ALPHA_SATURATE       = GL.GL_SRC_ALPHA_SATURATE;

    SCISSOR_TEST    = GL.GL_SCISSOR_TEST;
    STENCIL_TEST    = GL.GL_STENCIL_TEST;
    DEPTH_TEST      = GL.GL_DEPTH_TEST;
    DEPTH_WRITEMASK = GL.GL_DEPTH_WRITEMASK;
    ALPHA_TEST      = GL2ES1.GL_ALPHA_TEST;

    COLOR_BUFFER_BIT   = GL.GL_COLOR_BUFFER_BIT;
    DEPTH_BUFFER_BIT   = GL.GL_DEPTH_BUFFER_BIT;
    STENCIL_BUFFER_BIT = GL.GL_STENCIL_BUFFER_BIT;

    FRAMEBUFFER        = GL.GL_FRAMEBUFFER;
    COLOR_ATTACHMENT0  = GL.GL_COLOR_ATTACHMENT0;
    COLOR_ATTACHMENT1  = GL2ES2.GL_COLOR_ATTACHMENT1;
    COLOR_ATTACHMENT2  = GL2ES2.GL_COLOR_ATTACHMENT2;
    COLOR_ATTACHMENT3  = GL2ES2.GL_COLOR_ATTACHMENT3;
    RENDERBUFFER       = GL.GL_RENDERBUFFER;
    DEPTH_ATTACHMENT   = GL.GL_DEPTH_ATTACHMENT;
    STENCIL_ATTACHMENT = GL.GL_STENCIL_ATTACHMENT;
    READ_FRAMEBUFFER   = GL2ES3.GL_READ_FRAMEBUFFER;
    DRAW_FRAMEBUFFER   = GL2ES3.GL_DRAW_FRAMEBUFFER;

    RGBA8            = GL.GL_RGBA8;
    DEPTH24_STENCIL8 = GL.GL_DEPTH24_STENCIL8;

    DEPTH_COMPONENT   = GL2ES2.GL_DEPTH_COMPONENT;
    DEPTH_COMPONENT16 = GL.GL_DEPTH_COMPONENT16;
    DEPTH_COMPONENT24 = GL.GL_DEPTH_COMPONENT24;
    DEPTH_COMPONENT32 = GL.GL_DEPTH_COMPONENT32;

    STENCIL_INDEX  = GL2ES2.GL_STENCIL_INDEX;
    STENCIL_INDEX1 = GL.GL_STENCIL_INDEX1;
    STENCIL_INDEX4 = GL.GL_STENCIL_INDEX4;
    STENCIL_INDEX8 = GL.GL_STENCIL_INDEX8;

    DEPTH_STENCIL = GL.GL_DEPTH_STENCIL;

    FRAMEBUFFER_COMPLETE                      = GL.GL_FRAMEBUFFER_COMPLETE;
    FRAMEBUFFER_INCOMPLETE_ATTACHMENT         = GL.GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT;
    FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT = GL.GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT;
    FRAMEBUFFER_INCOMPLETE_DIMENSIONS         = GL.GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS;
    FRAMEBUFFER_INCOMPLETE_FORMATS            = GL.GL_FRAMEBUFFER_INCOMPLETE_FORMATS;
    FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER        = GL2GL3.GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER;
    FRAMEBUFFER_INCOMPLETE_READ_BUFFER        = GL2GL3.GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER;
    FRAMEBUFFER_UNSUPPORTED                   = GL.GL_FRAMEBUFFER_UNSUPPORTED;

    FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE           = GL.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE;
    FRAMEBUFFER_ATTACHMENT_OBJECT_NAME           = GL.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME;
    FRAMEBUFFER_ATTACHMENT_TEXTURE_LEVEL         = GL.GL_FRAMEBUFFER_ATTACHMENT_TEXTURE_LEVEL;
    FRAMEBUFFER_ATTACHMENT_TEXTURE_CUBE_MAP_FACE = GL.GL_FRAMEBUFFER_ATTACHMENT_TEXTURE_CUBE_MAP_FACE;

    RENDERBUFFER_WIDTH           = GL.GL_RENDERBUFFER_WIDTH;
    RENDERBUFFER_HEIGHT          = GL.GL_RENDERBUFFER_HEIGHT;
    RENDERBUFFER_RED_SIZE        = GL.GL_RENDERBUFFER_RED_SIZE;
    RENDERBUFFER_GREEN_SIZE      = GL.GL_RENDERBUFFER_GREEN_SIZE;
    RENDERBUFFER_BLUE_SIZE       = GL.GL_RENDERBUFFER_BLUE_SIZE;
    RENDERBUFFER_ALPHA_SIZE      = GL.GL_RENDERBUFFER_ALPHA_SIZE;
    RENDERBUFFER_DEPTH_SIZE      = GL.GL_RENDERBUFFER_DEPTH_SIZE;
    RENDERBUFFER_STENCIL_SIZE    = GL.GL_RENDERBUFFER_STENCIL_SIZE;
    RENDERBUFFER_INTERNAL_FORMAT = GL.GL_RENDERBUFFER_INTERNAL_FORMAT;

    MULTISAMPLE    = GL.GL_MULTISAMPLE;
    POINT_SMOOTH   = GL2ES1.GL_POINT_SMOOTH;
    LINE_SMOOTH    = GL.GL_LINE_SMOOTH;
    POLYGON_SMOOTH = GL2GL3.GL_POLYGON_SMOOTH;
  }

  ///////////////////////////////////////////////////////////

  // Special Functions

  @Override
  public void flush() {
    gl.glFlush();
  }

  @Override
  public void finish() {
    gl.glFinish();
  }

  @Override
  public void hint(int target, int hint) {
    gl.glHint(target, hint);
  }

  ///////////////////////////////////////////////////////////

  // State and State Requests

  @Override
  public void enable(int value) {
    if (-1 < value) {
      gl.glEnable(value);
    }
  }

  @Override
  public void disable(int value) {
    if (-1 < value) {
      gl.glDisable(value);
    }
  }

  @Override
  public void getBooleanv(int value, IntBuffer data) {
    if (-1 < value) {
      if (byteBuffer.capacity() < data.capacity()) {
        byteBuffer = allocateDirectByteBuffer(data.capacity());
      }
      gl.glGetBooleanv(value, byteBuffer);
      for (int i = 0; i < data.capacity(); i++) {
        data.put(i, byteBuffer.get(i));
      }
    } else {
      fillIntBuffer(data, 0, data.capacity() - 1, 0);
    }
  }

  @Override
  public void getIntegerv(int value, IntBuffer data) {
    if (-1 < value) {
      gl.glGetIntegerv(value, data);
    } else {
      fillIntBuffer(data, 0, data.capacity() - 1, 0);
    }
  }

  @Override
  public void getFloatv(int value, FloatBuffer data) {
    if (-1 < value) {
      gl.glGetFloatv(value, data);
    } else {
      fillFloatBuffer(data, 0, data.capacity() - 1, 0);
    }
  }

  @Override
  public boolean isEnabled(int value) {
    return gl.glIsEnabled(value);
  }

  @Override
  public String getString(int name) {
    return gl.glGetString(name);
  }

  ///////////////////////////////////////////////////////////

  // Error Handling

  @Override
  public int getError() {
    return gl.glGetError();
  }

  @Override
  public String errorString(int err) {
    return glu.gluErrorString(err);
  }

  //////////////////////////////////////////////////////////////////////////////

  // Buffer Objects

  @Override
  public void genBuffers(int n, IntBuffer buffers) {
    gl.glGenBuffers(n, buffers);
  }

  @Override
  public void deleteBuffers(int n, IntBuffer buffers) {
    gl.glDeleteBuffers(n, buffers);
  }

  @Override
  public void bindBuffer(int target, int buffer) {
    gl.glBindBuffer(target, buffer);
  }

  @Override
  public void bufferData(int target, int size, Buffer data, int usage) {
    gl.glBufferData(target, size, data, usage);
  }

  @Override
  public void bufferSubData(int target, int offset, int size, Buffer data) {
    gl.glBufferSubData(target, offset, size, data);
  }

  @Override
  public void isBuffer(int buffer) {
    gl.glIsBuffer(buffer);
  }

  @Override
  public void getBufferParameteriv(int target, int value, IntBuffer data) {
    gl.glGetBufferParameteriv(target, value, data);
  }

  @Override
  public ByteBuffer mapBuffer(int target, int access) {
    return gl2.glMapBuffer(target, access);
  }

  @Override
  public ByteBuffer mapBufferRange(int target, int offset, int length, int access) {
    if (gl2x != null) {
      return gl2x.glMapBufferRange(target, offset, length, access);
    } else if (gl3 != null) {
      return gl3.glMapBufferRange(target, offset, length, access);
    } else {
      throw new RuntimeException(String.format(MISSING_GLFUNC_ERROR, "glMapBufferRange()"));
    }
  }

  @Override
  public void unmapBuffer(int target) {
    gl2.glUnmapBuffer(target);
  }

  //////////////////////////////////////////////////////////////////////////////

  // Viewport and Clipping

  @Override
  public void depthRangef(float n, float f) {
    gl.glDepthRangef(n, f);
  }

  @Override
  public void viewport(int x, int y, int w, int h) {
    gl.glViewport(x, y, w, h);
  }

  //////////////////////////////////////////////////////////////////////////////

  // Reading Pixels

  @Override
  protected void readPixelsImpl(int x, int y, int width, int height, int format, int type, Buffer buffer) {
    gl.glReadPixels(x, y, width, height, format, type, buffer);
  }

  //////////////////////////////////////////////////////////////////////////////

  // Vertices

  @Override
  public void vertexAttrib1f(int index, float value) {
    gl2.glVertexAttrib1f(index, value);
  }

  @Override
  public void vertexAttrib2f(int index, float value0, float value1) {
    gl2.glVertexAttrib2f(index, value0, value1);
  }

  @Override
  public void vertexAttrib3f(int index, float value0, float value1, float value2) {
    gl2.glVertexAttrib3f(index, value0, value1, value2);
  }

  @Override
  public void vertexAttrib4f(int index, float value0, float value1, float value2, float value3) {
    gl2.glVertexAttrib4f(index, value0, value1, value2, value3);
  }

  @Override
  public void vertexAttrib1fv(int index, FloatBuffer values) {
    gl2.glVertexAttrib1fv(index, values);
  }

  @Override
  public void vertexAttrib2fv(int index, FloatBuffer values) {
    gl2.glVertexAttrib2fv(index, values);
  }

  @Override
  public void vertexAttrib3fv(int index, FloatBuffer values) {
    gl2.glVertexAttrib3fv(index, values);
  }

  @Override
  public void vertexAttri4fv(int index, FloatBuffer values) {
    gl2.glVertexAttrib4fv(index, values);
  }

  @Override
  public void vertexAttribPointer(int index, int size, int type, boolean normalized, int stride, int offset) {
    gl2.glVertexAttribPointer(index, size, type, normalized, stride, offset);
  }

  @Override
  public void vertexAttribPointer(int index, int size, int type, boolean normalized, int stride, Buffer data) {
    if (gl2x != null) {
      gl2x.glVertexAttribPointer(index, size, type, normalized, stride, data);
    } else {
      throw new RuntimeException(String.format(MISSING_GLFUNC_ERROR, "glVertexAttribPointer()"));
    }
  }

  @Override
  public void enableVertexAttribArray(int index) {
    gl2.glEnableVertexAttribArray(index);
  }

  @Override
  public void disableVertexAttribArray(int index) {
    gl2.glDisableVertexAttribArray(index);
  }

  @Override
  public void drawArrays(int mode, int first, int count) {
    gl.glDrawArrays(mode, first, count);
  }

  @Override
  public void drawElements(int mode, int count, int type, int offset) {
    gl.glDrawElements(mode, count, type, offset);
  }

  @Override
  public void drawElements(int mode, int count, int type, Buffer indices) {
    if (gl2x != null) {
      gl2x.glDrawElements(mode, count, type, indices);
    } else {
      throw new RuntimeException(String.format(MISSING_GLFUNC_ERROR, "glDrawElements()"));
    }
  }

  //////////////////////////////////////////////////////////////////////////////

  // Rasterization

  @Override
  public void lineWidth(float width) {
    gl.glLineWidth(width);
  }

  @Override
  public void frontFace(int dir) {
    gl.glFrontFace(dir);
  }

  @Override
  public void cullFace(int mode) {
    gl.glCullFace(mode);
  }

  @Override
  public void polygonOffset(float factor, float units) {
    gl.glPolygonOffset(factor, units);
  }

  //////////////////////////////////////////////////////////////////////////////

  // Pixel Rectangles

  @Override
  public void pixelStorei(int pname, int param) {
    gl.glPixelStorei(pname, param);
  }

  ///////////////////////////////////////////////////////////

  // Texturing

  @Override
  public void texImage2D(int target, int level, int internalFormat, int width, int height, int border, int format, int type, Buffer data) {
    gl.glTexImage2D(target, level, internalFormat, width, height, border, format, type, data);
  }

  @Override
  public void copyTexImage2D(int target, int level, int internalFormat, int x, int y, int width, int height, int border) {
    gl.glCopyTexImage2D(target, level, internalFormat, x, y, width, height, border);
  }

  @Override
  public void texSubImage2D(int target, int level, int xOffset, int yOffset, int width, int height, int format, int type, Buffer data) {
    gl.glTexSubImage2D(target, level, xOffset, yOffset, width, height, format, type, data);
  }

  @Override
  public void copyTexSubImage2D(int target, int level, int xOffset, int yOffset, int x, int y, int width, int height) {
    gl.glCopyTexSubImage2D(target, level, x, y, xOffset, xOffset, width, height);
  }

  @Override
  public void compressedTexImage2D(int target, int level, int internalFormat, int width, int height, int border, int imageSize, Buffer data) {
    gl.glCompressedTexImage2D(target, level, internalFormat, width, height, border, imageSize, data);
  }

  @Override
  public void compressedTexSubImage2D(int target, int level, int xOffset, int yOffset, int width, int height, int format, int imageSize, Buffer data) {
    gl.glCompressedTexSubImage2D(target, level, xOffset, yOffset, width, height, format, imageSize, data);
  }

  @Override
  public void texParameteri(int target, int pname, int param) {
    gl.glTexParameteri(target, pname, param);
  }

  @Override
  public void texParameterf(int target, int pname, float param) {
    gl.glTexParameterf(target, pname, param);
  }

  @Override
  public void texParameteriv(int target, int pname, IntBuffer params) {
    gl.glTexParameteriv(target, pname, params);
  }

  @Override
  public void texParameterfv(int target, int pname, FloatBuffer params) {
    gl.glTexParameterfv(target, pname, params);
  }

  @Override
  public void generateMipmap(int target) {
    gl.glGenerateMipmap(target);
  }

  @Override
  public void genTextures(int n, IntBuffer textures) {
    gl.glGenTextures(n, textures);
  }

  @Override
  public void deleteTextures(int n, IntBuffer textures) {
    gl.glDeleteTextures(n, textures);
  }

  @Override
  public void getTexParameteriv(int target, int pname, IntBuffer params) {
    gl.glGetTexParameteriv(target, pname, params);
  }

  @Override
  public void getTexParameterfv(int target, int pname, FloatBuffer params) {
    gl.glGetTexParameterfv(target, pname, params);
  }

  @Override
  public boolean isTexture(int texture) {
    return gl.glIsTexture(texture);
  }

  @Override
  protected void activeTextureImpl(int texture) {
    gl.glActiveTexture(texture);
  }

  @Override
  protected void bindTextureImpl(int target, int texture) {
    gl.glBindTexture(target, texture);
  }

  ///////////////////////////////////////////////////////////

  // Shaders and Programs

  @Override
  public int createShader(int type) {
    return gl2.glCreateShader(type);
  }

  @Override
  public void shaderSource(int shader, String source) {
    gl2.glShaderSource(shader, 1, new String[] { source }, (int[]) null, 0);
  }

  @Override
  public void compileShader(int shader) {
    gl2.glCompileShader(shader);
  }

  @Override
  public void releaseShaderCompiler() {
    gl2.glReleaseShaderCompiler();
  }

  @Override
  public void deleteShader(int shader) {
    gl2.glDeleteShader(shader);
  }

  @Override
  public void shaderBinary(int count, IntBuffer shaders, int binaryFormat, Buffer binary, int length) {
    gl2.glShaderBinary(count, shaders, binaryFormat, binary, length);
  }

  @Override
  public int createProgram() {
    return gl2.glCreateProgram();
  }

  @Override
  public void attachShader(int program, int shader) {
    gl2.glAttachShader(program, shader);
  }

  @Override
  public void detachShader(int program, int shader) {
    gl2.glDetachShader(program, shader);
  }

  @Override
  public void linkProgram(int program) {
    gl2.glLinkProgram(program);
  }

  @Override
  public void useProgram(int program) {
    gl2.glUseProgram(program);
  }

  @Override
  public void deleteProgram(int program) {
    gl2.glDeleteProgram(program);
  }

  @Override
  public String getActiveAttrib(int program, int index, IntBuffer size, IntBuffer type) {
    int[] tmp = {0, 0, 0};
    byte[] namebuf = new byte[1024];
    gl2.glGetActiveAttrib(program, index, 1024, tmp, 0, tmp, 1, tmp, 2, namebuf, 0);
    size.put(tmp[1]);
    type.put(tmp[2]);
    String name = new String(namebuf, 0, tmp[0]);
    return name;
  }

  @Override
  public int getAttribLocation(int program, String name) {
    return gl2.glGetAttribLocation(program, name);
  }

  @Override
  public void bindAttribLocation(int program, int index, String name) {
    gl2.glBindAttribLocation(program, index, name);
  }

  @Override
  public int getUniformLocation(int program, String name) {
    return gl2.glGetUniformLocation(program, name);
  }

  @Override
  public String getActiveUniform(int program, int index, IntBuffer size, IntBuffer type) {
    int[] tmp= {0, 0, 0};
    byte[] namebuf = new byte[1024];
    gl2.glGetActiveUniform(program, index, 1024, tmp, 0, tmp, 1, tmp, 2, namebuf, 0);
    size.put(tmp[1]);
    type.put(tmp[2]);
    String name = new String(namebuf, 0, tmp[0]);
    return name;
  }

  @Override
  public void uniform1i(int location, int value) {
    gl2.glUniform1i(location, value);
  }

  @Override
  public void uniform2i(int location, int value0, int value1) {
    gl2.glUniform2i(location, value0, value1);
  }

  @Override
  public void uniform3i(int location, int value0, int value1, int value2) {
    gl2.glUniform3i(location, value0, value1, value2);
  }

  @Override
  public void uniform4i(int location, int value0, int value1, int value2, int value3) {
    gl2.glUniform4i(location, value0, value1, value2, value3);
  }

  @Override
  public void uniform1f(int location, float value) {
    gl2.glUniform1f(location, value);
  }

  @Override
  public void uniform2f(int location, float value0, float value1) {
    gl2.glUniform2f(location, value0, value1);
  }

  @Override
  public void uniform3f(int location, float value0, float value1, float value2) {
    gl2.glUniform3f(location, value0, value1, value2);
  }

  @Override
  public void uniform4f(int location, float value0, float value1, float value2, float value3) {
    gl2.glUniform4f(location, value0, value1, value2, value3);
  }

  @Override
  public void uniform1iv(int location, int count, IntBuffer v) {
    gl2.glUniform1iv(location, count, v);
  }

  @Override
  public void uniform2iv(int location, int count, IntBuffer v) {
    gl2.glUniform2iv(location, count, v);
  }

  @Override
  public void uniform3iv(int location, int count, IntBuffer v) {
    gl2.glUniform3iv(location, count, v);
  }

  @Override
  public void uniform4iv(int location, int count, IntBuffer v) {
    gl2.glUniform4iv(location, count, v);
  }

  @Override
  public void uniform1fv(int location, int count, FloatBuffer v) {
    gl2.glUniform1fv(location, count, v);
  }

  @Override
  public void uniform2fv(int location, int count, FloatBuffer v) {
    gl2.glUniform2fv(location, count, v);
  }

  @Override
  public void uniform3fv(int location, int count, FloatBuffer v) {
    gl2.glUniform3fv(location, count, v);
  }

  @Override
  public void uniform4fv(int location, int count, FloatBuffer v) {
    gl2.glUniform4fv(location, count, v);
  }

  @Override
  public void uniformMatrix2fv(int location, int count, boolean transpose, FloatBuffer mat) {
    gl2.glUniformMatrix2fv(location, count, transpose, mat);
  }

  @Override
  public void uniformMatrix3fv(int location, int count, boolean transpose, FloatBuffer mat) {
    gl2.glUniformMatrix3fv(location, count, transpose, mat);
  }

  @Override
  public void uniformMatrix4fv(int location, int count, boolean transpose, FloatBuffer mat) {
    gl2.glUniformMatrix4fv(location, count, transpose, mat);
  }

  @Override
  public void validateProgram(int program) {
    gl2.glValidateProgram(program);
  }

  @Override
  public boolean isShader(int shader) {
    return gl2.glIsShader(shader);
  }

  @Override
  public void getShaderiv(int shader, int pname, IntBuffer params) {
    gl2.glGetShaderiv(shader, pname, params);
  }

  @Override
  public void getAttachedShaders(int program, int maxCount, IntBuffer count, IntBuffer shaders) {
    gl2.glGetAttachedShaders(program, maxCount, count, shaders);
  }

  @Override
  public String getShaderInfoLog(int shader) {
    int[] val = { 0 };
    gl2.glGetShaderiv(shader, GL2ES2.GL_INFO_LOG_LENGTH, val, 0);
    int length = val[0];

    byte[] log = new byte[length];
    gl2.glGetShaderInfoLog(shader, length, val, 0, log, 0);
    return new String(log);
  }

  @Override
  public String getShaderSource(int shader) {
    int[] len = {0};
    byte[] buf = new byte[1024];
    gl2.glGetShaderSource(shader, 1024, len, 0, buf, 0);
    return new String(buf, 0, len[0]);
  }

  @Override
  public void getShaderPrecisionFormat(int shaderType, int precisionType, IntBuffer range, IntBuffer precision) {
    gl2.glGetShaderPrecisionFormat(shaderType, precisionType, range, precision);
  }

  @Override
  public void getVertexAttribfv(int index, int pname, FloatBuffer params) {
    gl2.glGetVertexAttribfv(index, pname, params);
  }

  @Override
  public void getVertexAttribiv(int index, int pname, IntBuffer params) {
    gl2.glGetVertexAttribiv(index, pname, params);
  }

  @Override
  public void getVertexAttribPointerv(int index, int pname, ByteBuffer data) {
    throw new RuntimeException(String.format(MISSING_GLFUNC_ERROR, "glGetVertexAttribPointerv()"));
  }

  @Override
  public void getUniformfv(int program, int location, FloatBuffer params) {
    gl2.glGetUniformfv(program, location, params);
  }

  @Override
  public void getUniformiv(int program, int location, IntBuffer params) {
    gl2.glGetUniformiv(program, location, params);
  }

  @Override
  public boolean isProgram(int program) {
    return gl2.glIsProgram(program);
  }

  @Override
  public void getProgramiv(int program, int pname, IntBuffer params) {
    gl2.glGetProgramiv(program, pname, params);
  }

  @Override
  public String getProgramInfoLog(int program) {
    int[] val = { 0 };
    gl2.glGetShaderiv(program, GL2ES2.GL_INFO_LOG_LENGTH, val, 0);
    int length = val[0];

    if (0 < length) {
      byte[] log = new byte[length];
      gl2.glGetProgramInfoLog(program, length, val, 0, log, 0);
      return new String(log);
    } else {
      return "Unknow error";
    }
  }

  ///////////////////////////////////////////////////////////

  // Per-Fragment Operations

  @Override
  public void scissor(int x, int y, int w, int h) {
    gl.glScissor(x, y, w, h);
  }

  @Override
  public void sampleCoverage(float value, boolean invert) {
    gl2.glSampleCoverage(value, invert);
  }

  @Override
  public void stencilFunc(int func, int ref, int mask) {
    gl2.glStencilFunc(func, ref, mask);
  }

  @Override
  public void stencilFuncSeparate(int face, int func, int ref, int mask) {
    gl2.glStencilFuncSeparate(face, func, ref, mask);
  }

  @Override
  public void stencilOp(int sfail, int dpfail, int dppass) {
    gl2.glStencilOp(sfail, dpfail, dppass);
  }

  @Override
  public void stencilOpSeparate(int face, int sfail, int dpfail, int dppass) {
    gl2.glStencilOpSeparate(face, sfail, dpfail, dppass);
  }

  @Override
  public void depthFunc(int func) {
    gl.glDepthFunc(func);
  }

  @Override
  public void blendEquation(int mode) {
    gl.glBlendEquation(mode);
  }

  @Override
  public void blendEquationSeparate(int modeRGB, int modeAlpha) {
    gl.glBlendEquationSeparate(modeRGB, modeAlpha);
  }

  @Override
  public void blendFunc(int src, int dst) {
    gl.glBlendFunc(src, dst);
  }

  @Override
  public void blendFuncSeparate(int srcRGB, int dstRGB, int srcAlpha, int dstAlpha) {
    gl.glBlendFuncSeparate(srcRGB, dstRGB, srcAlpha, dstAlpha);
  }

  @Override
  public void blendColor(float red, float green, float blue, float alpha) {
    gl2.glBlendColor(red, green, blue, alpha);
  }

  @Override
  public void alphaFunc(int func, float ref) {
    if (gl2x != null) {
      gl2x.glAlphaFunc(func, ref);
    } else {
      throw new RuntimeException(String.format(MISSING_GLFUNC_ERROR, "glAlphaFunc()"));
    }
  }

  ///////////////////////////////////////////////////////////

  // Whole Framebuffer Operations

  @Override
  public void colorMask(boolean r, boolean g, boolean b, boolean a) {
    gl.glColorMask(r, g, b, a);
  }

  @Override
  public void depthMask(boolean mask) {
    gl.glDepthMask(mask);
  }

  @Override
  public void stencilMask(int mask) {
    gl.glStencilMask(mask);
  }

  @Override
  public void stencilMaskSeparate(int face, int mask) {
    gl2.glStencilMaskSeparate(face, mask);
  }

  @Override
  public void clear(int buf) {
    gl.glClear(buf);
  }

  @Override
  public void clearColor(float r, float g, float b, float a) {
    gl.glClearColor(r, g, b, a);
  }

  @Override
  public void clearDepth(float d) {
    gl.glClearDepthf(d);
  }

  @Override
  public void clearStencil(int s) {
    gl.glClearStencil(s);
  }

  ///////////////////////////////////////////////////////////

  // Framebuffers Objects

  @Override
  public void bindFramebuffer(int target, int framebuffer) {
    gl.glBindFramebuffer(target, framebuffer);
  }

  @Override
  public void deleteFramebuffers(int n, IntBuffer framebuffers) {
    gl.glDeleteFramebuffers(n, framebuffers);
  }

  @Override
  public void genFramebuffers(int n, IntBuffer framebuffers) {
    gl.glGenFramebuffers(n, framebuffers);
  }

  @Override
  public void bindRenderbuffer(int target, int renderbuffer) {
    gl.glBindRenderbuffer(target, renderbuffer);
  }

  @Override
  public void deleteRenderbuffers(int n, IntBuffer renderbuffers) {
    gl.glDeleteRenderbuffers(n, renderbuffers);
  }

  @Override
  public void genRenderbuffers(int n, IntBuffer renderbuffers) {
    gl.glGenRenderbuffers(n, renderbuffers);
  }

  @Override
  public void renderbufferStorage(int target, int internalFormat, int width, int height) {
    gl.glRenderbufferStorage(target, internalFormat, width, height);
  }

  @Override
  public void framebufferRenderbuffer(int target, int attachment, int rendbuferfTarget, int renderbuffer) {
    gl.glFramebufferRenderbuffer(target, attachment, rendbuferfTarget, renderbuffer);
  }

  @Override
  public void framebufferTexture2D(int target, int attachment, int texTarget, int texture, int level) {
    gl.glFramebufferTexture2D(target, attachment, texTarget, texture, level);
  }

  @Override
  public int checkFramebufferStatus(int target) {
    return gl.glCheckFramebufferStatus(target);
  }

  @Override
  public boolean isFramebuffer(int framebuffer) {
    return gl2.glIsFramebuffer(framebuffer);
  }

  @Override
  public void getFramebufferAttachmentParameteriv(int target, int attachment, int pname, IntBuffer params) {
    gl2.glGetFramebufferAttachmentParameteriv(target, attachment, pname, params);
  }

  @Override
  public boolean isRenderbuffer(int renderbuffer) {
    return gl2.glIsRenderbuffer(renderbuffer);
  }

  @Override
  public void getRenderbufferParameteriv(int target, int pname, IntBuffer params) {
    gl2.glGetRenderbufferParameteriv(target, pname, params);
  }

  @Override
  public void blitFramebuffer(int srcX0, int srcY0, int srcX1, int srcY1, int dstX0, int dstY0, int dstX1, int dstY1, int mask, int filter) {
    if (gl2x != null) {
      gl2x.glBlitFramebuffer(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter);
    } else if (gl3 != null) {
      gl3.glBlitFramebuffer(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter);
    } else {
      throw new RuntimeException(String.format(MISSING_GLFUNC_ERROR, "glBlitFramebuffer()"));
    }
  }

  @Override
  public void renderbufferStorageMultisample(int target, int samples, int format, int width, int height) {
    if (gl2x != null) {
      gl2x.glRenderbufferStorageMultisample(target, samples, format, width, height);
    } else if (gl3 != null) {
      gl3.glRenderbufferStorageMultisample(target, samples, format, width, height);
    } else {
      throw new RuntimeException(String.format(MISSING_GLFUNC_ERROR, "glRenderbufferStorageMultisample()"));
    }
  }

  @Override
  public void readBuffer(int buf) {
    if (gl2x != null) {
      gl2x.glReadBuffer(buf);
    } else if (gl3 != null) {
      gl3.glReadBuffer(buf);
    } else {
      throw new RuntimeException(String.format(MISSING_GLFUNC_ERROR, "glReadBuffer()"));
    }
  }

  @Override
  public void drawBuffer(int buf) {
    if (gl2x != null) {
      gl2x.glDrawBuffer(buf);
    } else if (gl3 != null) {
      gl3.glDrawBuffer(buf);
    } else {
      throw new RuntimeException(String.format(MISSING_GLFUNC_ERROR, "glDrawBuffer()"));
    }
  }
}
