package demos.renderToTexture;

import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;

import javax.media.opengl.DebugGL;
import javax.media.opengl.GL;
import javax.media.opengl.GLAutoDrawable;
import javax.media.opengl.GLCanvas;
import javax.media.opengl.GLCapabilities;
import javax.media.opengl.GLDrawableFactory;
import javax.media.opengl.GLEventListener;
import javax.media.opengl.GLException;
import javax.media.opengl.GLPbuffer;
import javax.swing.JFrame;

import com.sun.opengl.utils.GLUT;

public class RenderToTextureBasic implements GLEventListener
{
    private static GLUT glut = new GLUT();

    public static void main(String[] args)
    {
        RenderToTextureBasic renderer = new RenderToTextureBasic();

        GLCanvas canvas = new GLCanvas();

        canvas.addGLEventListener(renderer);

        JFrame frame = new JFrame("Render To Texture Demo");

        frame.addWindowListener(new WindowAdapter()
        {
            public void windowClosing(WindowEvent evt)
            {
                System.exit(0);
            }
        });

        frame.getContentPane().add(canvas);
        canvas.setSize(300, 300);

        frame.pack();
        frame.setVisible(true);
    }

    private static float[] lightPosition = new float[]
    {
        1, 0, 10, 1
    };

    private OffscreenRenderer offscreenRenderer;

    private boolean isOffscreenRendererCreated = false;

    public void init(GLAutoDrawable drawable)
    {
        drawable.setGL(new DebugGL(drawable.getGL()));

        GL gl = drawable.getGL();

        gl.glEnable(GL.GL_DEPTH_TEST);
        gl.glEnable(GL.GL_NORMALIZE);
        gl.glEnable(GL.GL_LIGHTING);

        gl.glClearColor(0.5f, 0.5f, 0.5f, 0);

        // Place the camera
        gl.glPushAttrib(GL.GL_TRANSFORM_BIT);
        {
            gl.glMatrixMode(GL.GL_PROJECTION);
            gl.glPushMatrix();
            gl.glLoadIdentity();
            gl.glFrustum(-2.0 / 10, 2.0 / 10, -2.0 / 10, 2.0 / 10, 1, 11);

            gl.glMatrixMode(GL.GL_MODELVIEW);
            gl.glPushMatrix();
            gl.glLoadIdentity();
            gl.glTranslated(0, 0, -10);
        }

        // Configure the light
        gl.glEnable(GL.GL_LIGHT0);
        gl.glLightfv(GL.GL_LIGHT0, GL.GL_POSITION, lightPosition, 0);

        if (!isOffscreenRendererCreated)
        {
            offscreenRenderer = new OffscreenRenderer(256);
            offscreenRenderer.initFromParent(drawable);
            isOffscreenRendererCreated = true;
        }
    }

    public void display(GLAutoDrawable drawable)
    {
        GL gl = drawable.getGL();

        gl.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT);

        offscreenRenderer.renderOffscreen();
        
        // Draw the teapot and a repeated picture of the offscreen colour
        // buffer.
        // We'll use multi-texture unit 0 for the picture
        offscreenRenderer.prepareForColouredRendering(drawable, GL.GL_TEXTURE0);
        drawSquare(drawable);
        offscreenRenderer.stopColouredRendering(drawable);
        drawFloatingTeapot(drawable);

    }

    private static void drawFloatingTeapot(GLAutoDrawable drawable)
    {
        GL gl = drawable.getGL();
        
        gl.glPushAttrib(GL.GL_TRANSFORM_BIT);
        {
            gl.glMatrixMode(GL.GL_MODELVIEW);
            gl.glPushMatrix();
            {
                gl.glTranslated(0.5, 0, 3);
                glut.glutSolidTeapot(0.25, true);
            }
            gl.glPopMatrix();
        }
        gl.glPopAttrib();
    }

    private static void drawSquare(GLAutoDrawable drawable)
    {
        GL gl = drawable.getGL();

        gl.glBegin(GL.GL_QUADS);
        {
            gl.glNormal3d(0, 0, 1);
            gl.glMultiTexCoord2d(GL.GL_TEXTURE0, 0, 0);
            gl.glVertex3d(-1, -1, 0);
            gl.glMultiTexCoord2d(GL.GL_TEXTURE0, 2, 0);
            gl.glVertex3d(1, -1, 0);
            gl.glMultiTexCoord2d(GL.GL_TEXTURE0, 2, 2);
            gl.glVertex3d(1, 1, 0);
            gl.glMultiTexCoord2d(GL.GL_TEXTURE0, 0, 2);
            gl.glVertex3d(-1, 1, 0);
        }
        gl.glEnd();
    }

    public void displayChanged(GLAutoDrawable drawable,
                               boolean modeChanged,
                               boolean deviceChanged)
    {
        // Nothing to do
    }

    public void reshape(GLAutoDrawable drawable,
                        int x,
                        int y,
                        int width,
                        int height)
    {
        // Nothing to do
    }

    private static class OffscreenRenderer implements GLEventListener
    {
        private GLPbuffer offscreenTarget;

        private int textureDimension;

        public OffscreenRenderer(int textureDimension)
        {
            super();

            this.textureDimension = textureDimension;
        }

        private boolean isDebug = false;

        public void initFromParent(GLAutoDrawable parent)
        {
            isDebug = parent.getGL() instanceof DebugGL;
            if (!GLDrawableFactory.getFactory().canCreateGLPbuffer())
            {
                throw new GLException("Can not create pbuffer");
            }
            if (offscreenTarget != null)
            {
                offscreenTarget.destroy();
                offscreenTarget = null;
            }
                        
            GLCapabilities caps = new GLCapabilities();
            caps.setDoubleBuffered(false);
            caps.setDepthBits(24);
            caps.setRedBits(8);
            caps.setGreenBits(8);
            caps.setBlueBits(8);
            caps.setAlphaBits(8);
            // We want to bind both the colour buffer and the depth buffer to
            // textures
            caps.setOffscreenRenderToTexture(true);
            caps.setOffscreenRenderToTextureRectangle(false);
            offscreenTarget = GLDrawableFactory.getFactory()
                .createGLPbuffer(caps,
                                 null,
                                 textureDimension,
                                 textureDimension,
                                 parent.getContext());
            offscreenTarget.addGLEventListener(this);
        }

        public void init(GLAutoDrawable drawable)
        {
            if (isDebug && !(drawable.getGL() instanceof DebugGL))
            {
                drawable.setGL(new DebugGL(drawable.getGL()));
            }

            GL gl = drawable.getGL();

            gl.glEnable(GL.GL_DEPTH_TEST);
            gl.glEnable(GL.GL_NORMALIZE);
            gl.glDisable(GL.GL_LIGHTING);

            gl.glClearColor(0, 0, 1, 0);

            // Place the offscreen camera at the light position
            gl.glPushAttrib(GL.GL_TRANSFORM_BIT);
            {
                gl.glMatrixMode(GL.GL_PROJECTION);
                gl.glPushMatrix();
                gl.glLoadIdentity();
                applyLightFrustum(gl);

                gl.glMatrixMode(GL.GL_MODELVIEW);
                gl.glPushMatrix();
                gl.glLoadIdentity();
                applyLightTransformation(gl);
            }
            gl.glPopAttrib();
        }

        private void applyLightFrustum(GL gl)
        {
            // Set up the viewing frustum so that the light only sees the
            // volume between itself and the unit X-Y square at the origin
            // (because that is the only volume in which we will be casting
            // shadows).
            gl.glFrustum(-2.0 / 10, 0, -1.0 / 10, 1.0 / 10, 1, 10);
        }

        private void applyLightTransformation(GL gl)
        {
            gl.glTranslated(-lightPosition[0],
                            -lightPosition[1],
                            -lightPosition[2]);
        }

        public void renderOffscreen()
        {
            offscreenTarget.display();
        }

        private int counter = 0;
        public void display(GLAutoDrawable drawable)
        {
            System.out.println("Pbuffer rendering");
            GL gl = drawable.getGL();

            gl.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT);

            if (counter++ % 2 == 0)
            {
                // Draw the teapot as a red silhouette
              gl.glColor3d(1, 0, 0);
            }
            else
            {
                // Draw the teapot as a green silhouette
                gl.glColor3d(0, 1, 0);
            }
            drawFloatingTeapot(drawable);
        }

        public void prepareForColouredRendering(GLAutoDrawable targetDrawable,
                                                int textureUnitID)
        {
            GL gl = targetDrawable.getGL();

            // The state modified by this preparation will be restored by a call
            // to finishShadowedRendering()
            gl.glPushAttrib(GL.GL_TEXTURE_BIT);

            gl.glActiveTexture(textureUnitID);
            // Use the pbuffer as a colour texture
            System.out.println("Pbuffer bound");
            offscreenTarget.bindTexture();

            // Set the texture up to be used for painting a surface
            int textureTarget = GL.GL_TEXTURE_2D;
            gl.glEnable(textureTarget);
            gl.glTexEnvi(GL.GL_TEXTURE_ENV,
                         GL.GL_TEXTURE_ENV_MODE,
                         GL.GL_MODULATE);
            gl.glTexParameteri(textureTarget,
                               GL.GL_TEXTURE_MIN_FILTER,
                               GL.GL_LINEAR);
            gl.glTexParameteri(textureTarget,
                               GL.GL_TEXTURE_MAG_FILTER,
                               GL.GL_LINEAR);
            gl.glTexParameteri(textureTarget,
                               GL.GL_TEXTURE_WRAP_S,
                               GL.GL_REPEAT);
            gl.glTexParameteri(textureTarget,
                               GL.GL_TEXTURE_WRAP_T,
                               GL.GL_REPEAT);
        }

        public void stopColouredRendering(GLAutoDrawable targetDrawable)
        {
            GL gl = targetDrawable.getGL();

            // The pbuffer MUST be released from the texture before it can be
            // drawn to again.
            System.out.println("Pbuffer released");
            offscreenTarget.releaseTexture();

            // Restore the active texture
            gl.glPopAttrib();
        }

        public void displayChanged(GLAutoDrawable drawable,
                                   boolean modeChanged,
                                   boolean deviceChanged)
        {
            // Nothing to do
        }

        public void reshape(GLAutoDrawable drawable,
                            int x,
                            int y,
                            int width,
                            int height)
        {
            // Nothing to do
        }

    }

}