import static com.jogamp.common.nio.Buffers.SIZEOF_FLOAT;
import static com.jogamp.common.nio.Buffers.SIZEOF_SHORT;
import com.jogamp.opengl.util.GLBuffers;
import java.awt.Canvas;
import java.awt.Dimension;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.FloatBuffer;
import java.nio.ShortBuffer;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.media.opengl.DebugGL3;
import javax.media.opengl.GL;
import javax.media.opengl.GL3;
import javax.media.opengl.GLAutoDrawable;
import javax.media.opengl.GLCapabilities;
import javax.media.opengl.GLEventListener;
import javax.media.opengl.GLException;
import javax.media.opengl.GLProfile;
import javax.media.opengl.awt.GLCanvas;
import javax.swing.JFrame;
import javax.swing.SwingUtilities;
import jogamp.opengl.GLContextImpl;
import jogamp.opengl.gl4.GL4bcImpl;
import jogamp.opengl.gl4.GL4bcProcAddressTable;

public class VAOTest implements GLEventListener {
    
    /** Different modes of displaying the geometry */
    private enum Mode {
        /** Traditional one without using VAO */
        NORMAL {
            @Override
            void display(VAOTest t, GL3 gl) {
                t.displayNormal(gl);
            }
        },
        
        /** Using VAOs throws [incorrectly as of JOGL 2.0rc11] a GLException */
        VAO_EXCEPTION {
            @Override
            void display(VAOTest t, GL3 gl) {
                t.displayVAOException(gl);
            }
        },
        
        /** 
         * Using VAO specifying the {@code ELEMENT_ARRAY_BUFFER } binding again
         * as a workaround to avoid the JOGL exception
         */
        VAO_WORKAROUND {
            @Override
            void display(VAOTest t, GL3 gl) {
                t.displayVAOWorkaround(gl);
            }
        },
        
        /**
         * Call the native method directly through reflection, avoiding the
         * JOGL runtime checks. This way VAOs behave as expected.
         */
        REFLECTION_DIRECT {
            @Override
            void display(VAOTest t, GL3 gl) {
                t.displayReflection(gl);
            }
        };
        
        abstract void display(VAOTest t, GL3 gl);
    }
    
    private final static float[] vertexData = new float[]{
         0.0f,  0.75f, 0.0f,  1,0,0,
        -0.5f, -0.75f, 0.0f,  0,1,0,
         0.9f, -0.75f, 0.0f,  0,0,1
    };
    
    private int ibo = -1;
    private int vbo = -1;
    private int vertID = -1;
    private int fragID = -1;
    private int progID = -1;
    
    private int vaoException  = -1;
    private int vaoWorkaround = -1;
    private int vaoReflection = -1;
    
    private Mode mode = Mode.REFLECTION_DIRECT;
    
    private Field clazz_DebugGL3_downstreamGL3;
    private Method clazz_GL4bcImpl_dispatch_glDrawElements1;
    private long clazz_GL4bcImpl_addressof_glDrawElements;
    
    private static int createShader(final GL3 gl, int type,
            final String[] srcLines){
        int shaderID = gl.glCreateShader(type);
        assert shaderID > 0;
        int[] lengths  = new int[srcLines.length];
        for (int i = 0; i < srcLines.length; i++) {
            lengths[i] = srcLines[i].length();
        }
        gl.glShaderSource(shaderID, srcLines.length, srcLines, lengths, 0);
        gl.glCompileShader(shaderID);
        return shaderID;
    }
    
    private void initBuffers(GL3 gl) {
        // IDs for 2 buffers
        int[] buffArray = new int[2];
        gl.glGenBuffers(buffArray.length, buffArray, 0);
        vbo = buffArray[0];
        assert vbo > 0;
        
        // Bind buffer and upload data
        gl.glBindBuffer(GL3.GL_ARRAY_BUFFER, vbo);
        FloatBuffer buffer = GLBuffers.newDirectFloatBuffer(vertexData);
        assert buffer.remaining() == vertexData.length;
        gl.glBufferData(GL3.GL_ARRAY_BUFFER, vertexData.length * SIZEOF_FLOAT,
                buffer, GL3.GL_STATIC_DRAW);
        gl.glBindBuffer(GL3.GL_ARRAY_BUFFER, 0);
        
        // Buffer with the 3 indices required for one triangle
        ibo = buffArray[1];
        assert ibo > 0;
        final short[] indices = new short[]{0, 1, 2};
        ShortBuffer shortBuffer = GLBuffers.newDirectShortBuffer(indices);
        assert shortBuffer.remaining() == indices.length;
        gl.glBindBuffer(GL3.GL_ELEMENT_ARRAY_BUFFER, ibo);
        gl.glBufferData(GL3.GL_ELEMENT_ARRAY_BUFFER,indices.length*SIZEOF_SHORT,
                shortBuffer, GL3.GL_STATIC_DRAW);
        gl.glBindBuffer(GL3.GL_ELEMENT_ARRAY_BUFFER, 0);
    }
    private void initShaders(GL3 gl) {
        final String[] vertSrc = new String[]{
            "#version 150 core\n",
            "in vec4 vPosition;\n",
            "in vec4 vColor;\n",
            "out vec4 pColor;\n",
            "void main() {\n",
            "    pColor       = vColor;\n",
            "    gl_Position = vPosition;\n",
            "}\n"
        };
        vertID = createShader(gl, GL3.GL_VERTEX_SHADER, vertSrc);

        final String[] fragSrc = new String[]{
            "#version 150 core\n",
            "in vec4 pColor;\n",
            "void main() {\n",
            "    gl_FragColor = pColor;\n",
            "}\n"
        };
        fragID = createShader(gl, GL3.GL_FRAGMENT_SHADER, fragSrc);

        // We're done with the compiler
        gl.glReleaseShaderCompiler();

        progID = gl.glCreateProgram();
        assert progID > 0;
        gl.glAttachShader(progID, vertID);
        gl.glAttachShader(progID, fragID);
        gl.glLinkProgram(progID);
        gl.glValidateProgram(progID);
    }
    
    private int initVAO(GL3 gl) {
        int[] buff = new int[1];
        gl.glGenVertexArrays(1, buff, 0);
        int vao = buff[0];
        assert vao > 0;
        
        
        gl.glUseProgram(progID);
        final int posLoc = gl.glGetAttribLocation(progID, "vPosition");
        final int colorLoc = gl.glGetAttribLocation(progID, "vColor");
        gl.glUseProgram(0);
        
        gl.glBindVertexArray(vao);
        gl.glBindBuffer(GL3.GL_ARRAY_BUFFER, vbo);
        gl.glBindBuffer(GL3.GL_ELEMENT_ARRAY_BUFFER, ibo);
        
        gl.glEnableVertexAttribArray(posLoc);
        gl.glEnableVertexAttribArray(colorLoc);
        
        final int stride = 6 * SIZEOF_FLOAT;
        final int cOff   = 3 * SIZEOF_FLOAT;
        gl.glVertexAttribPointer(posLoc,  3, GL3.GL_FLOAT, false, stride, 0L);
        gl.glVertexAttribPointer(colorLoc,3, GL3.GL_FLOAT, false, stride, cOff);

        gl.glBindVertexArray(0);
        return vao;
    }

    @Override
    public void init(GLAutoDrawable drawable) {
        initReflection(drawable.getGL().getGL3());
        drawable.setGL(new DebugGL3(drawable.getGL().getGL3()));
        
        final GL3 gl = drawable.getGL().getGL3();
        gl.glEnable(GL3.GL_DEPTH_TEST);
        gl.glDisable(GL3.GL_CULL_FACE);
        initBuffers(gl);
        initShaders(gl);
        
        vaoException  = initVAO(gl);
        vaoWorkaround = initVAO(gl);
        vaoReflection = initVAO(gl);
    }

    @Override
    public void dispose(GLAutoDrawable drawable) {
        final GL3 gl = drawable.getGL().getGL3();
        gl.glDeleteBuffers(2, new int[]{vbo, ibo}, 0);
        gl.glDetachShader(progID, fragID);
        gl.glDetachShader(progID, vertID);
        gl.glDeleteProgram(progID);
        gl.glDeleteShader(fragID);
        gl.glDeleteShader(vertID);
    }
    
    private void displayNormal(final GL3 gl) {
       final int posLoc    = gl.glGetAttribLocation(progID, "vPosition");
        final int colorLoc = gl.glGetAttribLocation(progID, "vColor");
        gl.glEnableVertexAttribArray(posLoc);
        gl.glEnableVertexAttribArray(colorLoc);

        gl.glBindBuffer(GL3.GL_ARRAY_BUFFER, vbo);
        final int stride = 6 * SIZEOF_FLOAT;
        final int cOff   = 3 * SIZEOF_FLOAT;
        gl.glVertexAttribPointer(posLoc,  3, GL3.GL_FLOAT, false, stride, 0L);
        gl.glVertexAttribPointer(colorLoc,3, GL3.GL_FLOAT, false, stride, cOff);
        gl.glBindBuffer(GL3.GL_ELEMENT_ARRAY_BUFFER, ibo);
        gl.glDrawElements(GL3.GL_TRIANGLES, 3, GL3.GL_UNSIGNED_SHORT, 0L);

        gl.glDisableVertexAttribArray(posLoc);
        gl.glDisableVertexAttribArray(colorLoc);
        gl.glBindBuffer(GL3.GL_ARRAY_BUFFER, 0);
        gl.glBindBuffer(GL3.GL_ELEMENT_ARRAY_BUFFER, 0);
    }
    
    private void displayVAOException(final GL3 gl) {
        try {
            gl.glBindVertexArray(vaoException);
            gl.glDrawElements(GL3.GL_TRIANGLES, 3, GL3.GL_UNSIGNED_SHORT, 0L);
            gl.glBindVertexArray(0);
        } catch (GLException ex) {
            Logger.getLogger(VAOTest.class.getName()).log(Level.SEVERE,null,ex);
        }
    }
    
    private void displayVAOWorkaround(final GL3 gl) {
        gl.glBindVertexArray(vaoWorkaround);
        gl.glBindBuffer(GL.GL_ELEMENT_ARRAY_BUFFER, ibo);
        gl.glDrawElements(GL3.GL_TRIANGLES, 3, GL3.GL_UNSIGNED_SHORT, 0L);
        gl.glBindBuffer(GL.GL_ELEMENT_ARRAY_BUFFER, 0);
        gl.glBindVertexArray(0);
    }
    
    private void displayReflection(final GL3 gl) {
        if (clazz_DebugGL3_downstreamGL3 == null ||
            clazz_GL4bcImpl_dispatch_glDrawElements1 == null ||
            clazz_GL4bcImpl_addressof_glDrawElements == 0L) {
            throw new IllegalStateException("Cannot use the reflection method");
        }
        
        gl.glBindVertexArray(vaoReflection);
        
        int[] buffer = new int[1];
        gl.glGetIntegerv(GL3.GL_ELEMENT_ARRAY_BUFFER_BINDING, buffer, 0);
        if (buffer[0] == 0) {
            throw new GLException("ELEMENT_ARRAY_BUFFER binding missing!");
        }
        
        try {
            GL4bcImpl gl4bcImpl=(GL4bcImpl)clazz_DebugGL3_downstreamGL3.get(gl);
            clazz_GL4bcImpl_dispatch_glDrawElements1.invoke(gl4bcImpl,
                    GL3.GL_TRIANGLES, 3, GL3.GL_UNSIGNED_SHORT, 0L,
                    clazz_GL4bcImpl_addressof_glDrawElements);
            gl.glBindVertexArray(0);
        } catch (IllegalAccessException | IllegalArgumentException |
                InvocationTargetException | ClassCastException ex) {
            Logger.getLogger(VAOTest.class.getName()).log(Level.SEVERE,null,ex);
        } finally {
            gl.glBindVertexArray(0);
        }
    }

    private Mode lastMode = null;
    @Override
    public void display(GLAutoDrawable drawable) {
        final GL3 gl = drawable.getGL().getGL3();
        float color = ((float) mode.ordinal() + 1) / (Mode.values().length + 2);
        gl.glClearColor(color, color, color, 0);
        gl.glClear(GL3.GL_COLOR_BUFFER_BIT | GL3.GL_DEPTH_BUFFER_BIT);
        gl.glUseProgram(progID);
        if (lastMode != mode) {
            System.out.println("Display mode: " + mode);
            lastMode = mode;
        }
        mode.display(this, gl);
        gl.glUseProgram(0);
    }

    @Override
    public void reshape(GLAutoDrawable drawable, int x, int y, int w, int h) {
        ((Canvas) drawable.getUpstreamWidget()).repaint();
    }
    
    private void initReflection(GL3 gl) {
        if (!(gl instanceof GL4bcImpl)) {
            System.err.println("Unexpected: "+gl.getClass().getCanonicalName());
            return;
        }
        final Field field;
        final Method method;
        final long addr;
        try {
            Class<DebugGL3> clzDebugGL3 = DebugGL3.class;
            field = clzDebugGL3.getDeclaredField("downstreamGL3");
            field.setAccessible(true);
            
            Class<GL4bcImpl> clzGL4bcImpl = GL4bcImpl.class;
            Field contextField = clzGL4bcImpl.getDeclaredField("_context");
            contextField.setAccessible(true);
            final GLContextImpl context = (GLContextImpl) contextField.get(gl);
            final GL4bcProcAddressTable table =
                    (GL4bcProcAddressTable) context.getGLProcAddressTable();
            addr = table._addressof_glDrawElements;
            if (addr == 0) {
                System.err.println("Method \"glDrawElements\" not available");
                return;
            }
            method = clzGL4bcImpl.getDeclaredMethod("dispatch_glDrawElements1",
                    Integer.TYPE,Integer.TYPE,Integer.TYPE,Long.TYPE,Long.TYPE);
            method.setAccessible(true);
        } catch (NoSuchFieldException | SecurityException |
                IllegalArgumentException | IllegalAccessException |
                NoSuchMethodException ex) {
            Logger.getLogger(VAOTest.class.getName()).log(Level.SEVERE,null,ex);
            return;
        }
        clazz_DebugGL3_downstreamGL3 = field;
        clazz_GL4bcImpl_dispatch_glDrawElements1 = method;
        clazz_GL4bcImpl_addressof_glDrawElements = addr;
    }
    
    private void cycleDisplayMode() {
        assert mode != null;
        final Mode[] vals = Mode.values();
        mode = vals[(mode.ordinal() + 1) % vals.length];
    }
    
    private static void run() {
        JFrame frame = new JFrame("VertexArrayObject - TEST");
        frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);        
        GLProfile profile = GLProfile.get(GLProfile.GL3);
        GLCapabilities capabilities = new GLCapabilities(profile);
        final GLCanvas canvas = new GLCanvas(capabilities);
        final VAOTest vaoTest = new VAOTest();
        canvas.setPreferredSize(new Dimension(512, 512));
        canvas.addGLEventListener(vaoTest);
        canvas.addMouseListener(new MouseAdapter() {
            @Override
            public void mousePressed(MouseEvent e) {
                vaoTest.cycleDisplayMode();
                canvas.repaint();
            }        
        });
        frame.add(canvas);
        frame.pack();
        frame.setVisible(true);
        canvas.requestFocusInWindow();
    }
    
    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {

            @Override
            public void run() {
                VAOTest.run();
            }
        });
    }
    
}
