package comp599.tools.viewer;

import java.nio.FloatBuffer;

import javax.media.opengl.GL;
import javax.media.opengl.GLAutoDrawable;
import javax.media.opengl.GLException;
import javax.media.opengl.glu.GLU;
import javax.swing.JPanel;
import javax.vecmath.Matrix4f;

import com.sun.opengl.util.GLUT;

import mintools.parameters.BooleanParameter;
import mintools.parameters.DoubleParameter;
import mintools.swing.VerticalFlowPanel;
import mintools.viewer.EasyViewer;

/**
 * Creates a frame buffer object for a shadow map
 * @author kry
 */
public class ShadowMap {

    private DoubleParameter posx = new DoubleParameter( "posx (of light)", 1, -5, 5 );

    private DoubleParameter posy = new DoubleParameter( "posy (of light)", 10, -5, 15 );

    private DoubleParameter posz = new DoubleParameter( "posz (of light)", 2, -5, 5 );

    private float[] lightPosition = new float[4]; 
    
    private int lightNumber = 0;
    
    private DoubleParameter factor = new DoubleParameter( "factor", 16, 0, 16 );
    
    private DoubleParameter offset = new DoubleParameter( "offset", 16000, 1, 16000 );
    
    private BooleanParameter debugDepthMap = new BooleanParameter( "debug depth map" , false );
    
    private BooleanParameter debugLightFrustum = new BooleanParameter( "debug light frustum" , false );

    private DoubleParameter ambient = new DoubleParameter( "ambient", 0.2, 0, 1 );

    private DoubleParameter diffuse = new DoubleParameter( "diffuse", 0.5, 0, 1 );
    
    private DoubleParameter near = new DoubleParameter( "near plane for light", 2, 1, 8 );
    private DoubleParameter far = new DoubleParameter( "far plane for light" , 12, 8, 24 );
    
    int depthTextureID;
    int savedFrameBufferID;
    int shadowframeBufferID;    
    int size;
    
    private FloatBuffer depthBufferPixels; // for testing!
    
    /**
     * Creates a shadow map of the given size
     * @param size must be a power of 2
     */
    public ShadowMap( int size ) {
        this.size = size;
    }
    
    /**
     * @return controls for the shadow mapped light
     */
    public JPanel getControls() {
        VerticalFlowPanel vfp = new VerticalFlowPanel();
        vfp.add( posx.getSliderControls(false));
        vfp.add( posy.getSliderControls(false));
        vfp.add( posz.getSliderControls(false));
        vfp.add( offset.getSliderControls(true) );
        vfp.add( factor.getSliderControls(false) );
        vfp.add( debugDepthMap.getControls() );
        vfp.add( debugLightFrustum.getControls() );
        vfp.add( ambient.getSliderControls(false) );
        vfp.add( diffuse.getSliderControls(false) );
        vfp.add( near.getSliderControls(false) );
        vfp.add( far.getSliderControls(false) );
        
        return vfp.getPanel();
    }
    
    /** 
     * Creates a frame buffer object and sets it up as a depth texture for shadow mapping
     * @param drawable
     */
    public void init( GLAutoDrawable drawable ) {
        GL gl = drawable.getGL();
        
        int[] tmp = new int[1];
        gl.glGetIntegerv(GL.GL_DEPTH_BITS, tmp, 0);
        int depthbits = tmp[0];
        
        // Save the current frame buffer binding        
        gl.glGetIntegerv( GL.GL_FRAMEBUFFER_BINDING_EXT, tmp, 0 );
        savedFrameBufferID = tmp[0];

        // Allocate the frame buffer object        
        gl.glGenFramebuffersEXT(1, tmp, 0);
        shadowframeBufferID = tmp[0];
        gl.glBindFramebufferEXT( GL.GL_FRAMEBUFFER_EXT, shadowframeBufferID );

        // Allocate the depth texture
        gl.glGenTextures(1, tmp, 0);
        depthTextureID = tmp[0];
        
        gl.glBindTexture( GL.GL_TEXTURE_2D, depthTextureID ); 
        gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, GL.GL_LINEAR);//LINEAR_MIPMAP_LINEAR );//GL_NEAREST);
        gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER, GL.GL_LINEAR);//NEAREST);
        gl.glTexParameteri( GL.GL_TEXTURE_2D,GL.GL_TEXTURE_WRAP_S, GL.GL_CLAMP_TO_EDGE );
        gl.glTexParameteri( GL.GL_TEXTURE_2D,GL.GL_TEXTURE_WRAP_T, GL.GL_CLAMP_TO_EDGE );
         
        int depthFormat;        
        if ( depthbits == 16 ) {        
            depthFormat = GL.GL_DEPTH_COMPONENT16;
        } else if ( depthbits == 24 ) {
            depthFormat = GL.GL_DEPTH_COMPONENT24;
        } else if ( depthbits == 32 ) {   
            depthFormat = GL.GL_DEPTH_COMPONENT32;
        } else {
            throw new GLException("Unexpected number of depth bits: " + depthbits);
        }
        gl.glTexImage2D(GL.GL_TEXTURE_2D, 0, depthFormat, size, size, 0, GL.GL_DEPTH_COMPONENT, GL.GL_UNSIGNED_INT, null );

        // Attach the texture to the frame buffer
        gl.glFramebufferTexture2DEXT( GL.GL_FRAMEBUFFER_EXT, GL.GL_DEPTH_ATTACHMENT_EXT, GL.GL_TEXTURE_2D, depthTextureID, 0);        
        gl.glDrawBuffer( GL.GL_NONE );
        gl.glReadBuffer( GL.GL_NONE );

        StringBuilder status = new StringBuilder();
        checkFramebufferStatus(gl, status );
        System.out.println( status );
        
        // Restore the original frame buffer binding
        gl.glBindFramebufferEXT( GL.GL_FRAMEBUFFER_EXT, savedFrameBufferID );
        
        gl.glHint( GL.GL_PERSPECTIVE_CORRECTION_HINT, GL.GL_NICEST );
        
        int depthBufferPixelsSize = size * size;
        depthBufferPixels = FloatBuffer.allocate( depthBufferPixelsSize );
        
        copyFlatToMatrix( new float[] {
                0.5f, 0.0f, 0.0f, 0.0f,
                0.0f, 0.5f, 0.0f, 0.0f,
                0.0f, 0.0f, 0.5f, 0.0f,
                0.5f, 0.5f, 0.5f, 1.0f}, biasMatrix );
        
    }        
    
    Matrix4f biasMatrix = new Matrix4f();
    Matrix4f lightProjectionMatrix = new Matrix4f();
    Matrix4f lightViewMatrix = new Matrix4f();
    Matrix4f textureMatrix = new Matrix4f();
    
    float[] cpm = new float[16];
    float[] cvm = new float[16];
    float[] lpm = new float[16];
    float[] lvm = new float[16];
    
    GLU glu = new GLU();
    GLUT glut = new GLUT();
 
    /** 
     * Displays debug information for the shadow mapping, if appropriate check-boxes are selected.
     * @param drawable
     */
    public void displayDebug( GLAutoDrawable drawable ) {
        GL gl = drawable.getGL();
        gl.glEnable(GL.GL_BLEND);
        gl.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE_MINUS_SRC_ALPHA);
        gl.glLineWidth( 2 );
        // Check to see if the light frustum makes sense        
        if ( debugLightFrustum.getValue() ) {
            debugLightFrustum( drawable );
        }        
        // Check to see if the depth texture makes sense
        if ( debugDepthMap.getValue() ) {
            debug(drawable);
        }
    }
    
    /**
     * Prepares for drawing the light view
     * @param drawable
     */
    public void beginLightPass( GLAutoDrawable drawable ) {
        
        GL gl = drawable.getGL(); 

        gl.glEnable( GL.GL_POLYGON_OFFSET_FILL );
        gl.glPolygonOffset( (float) factor.getValue(), (float) offset.getValue() );
        
        lightPosition[0] = (float) posx.getValue();
        lightPosition[1] = (float) posy.getValue();
        lightPosition[2] = (float) posz.getValue();
        lightPosition[3] = 1;
        
        float[] colour = { 1, 1, 1, 1 };
        gl.glLightfv(GL.GL_LIGHT0 + lightNumber, GL.GL_SPECULAR, colour, 0);
        gl.glLightfv(GL.GL_LIGHT0 + lightNumber, GL.GL_DIFFUSE, colour, 0);
        gl.glLightfv(GL.GL_LIGHT0 + lightNumber, GL.GL_AMBIENT, colour, 0);
        gl.glLightfv(GL.GL_LIGHT0 + lightNumber, GL.GL_POSITION, lightPosition, 0);
        gl.glEnable( GL.GL_LIGHT0 + lightNumber );
        
        gl.glPushMatrix();
        gl.glGetFloatv( GL.GL_PROJECTION_MATRIX, cpm, 0 );
        gl.glGetFloatv( GL.GL_MODELVIEW_MATRIX, cvm, 0 );        
        gl.glLoadIdentity();
        glu.gluPerspective(75.0f, 1.0f, (float) near.getValue(), (float) far.getValue() );
        gl.glGetFloatv( GL.GL_MODELVIEW_MATRIX, lpm, 0 );
        gl.glLoadIdentity();
        glu.gluLookAt( lightPosition[0], lightPosition[1], lightPosition[2], 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f);
        gl.glGetFloatv( GL.GL_MODELVIEW_MATRIX, lvm, 0 );
        gl.glPopMatrix();
    
        
        // First pass - from light's point of view
        gl.glBindFramebufferEXT( GL.GL_FRAMEBUFFER_EXT, shadowframeBufferID );
        gl.glViewport( 0, 0, size, size );
        gl.glClear( GL.GL_DEPTH_BUFFER_BIT );
        gl.glMatrixMode( GL.GL_PROJECTION ); 
        gl.glLoadMatrixf( lpm, 0 );
        gl.glMatrixMode( GL.GL_MODELVIEW );
        gl.glLoadMatrixf( lvm, 0 );
        
        copyFlatToMatrix( lpm, lightProjectionMatrix );
        copyFlatToMatrix( lvm, lightViewMatrix );
        textureMatrix.mul( biasMatrix, lightProjectionMatrix );
        textureMatrix.mul( lightViewMatrix );        
    }
    
    /**
     * Finishes the light view drawing and prepares for normal camera view
     * @param drawable
     */
    public void endLightPass( GLAutoDrawable drawable ) {
        GL gl = drawable.getGL();
        gl.glBindFramebufferEXT( GL.GL_FRAMEBUFFER_EXT, savedFrameBufferID );
        gl.glViewport( 0, 0, drawable.getWidth(), drawable.getHeight() );

        // Next pass - from the object's view
        gl.glMatrixMode( GL.GL_PROJECTION ); 
        gl.glLoadMatrixf( cpm, 0 );
        gl.glMatrixMode( GL.GL_MODELVIEW );
        gl.glLoadIdentity();
        gl.glLoadMatrixf( cvm, 0 );
        
        gl.glPolygonOffset( 0, 0 );
    }

    /**
     * sets up rendering for the ambient pass
     * @param drawable
     */
    public void beginAmbientPass( GLAutoDrawable drawable) {
        GL gl = drawable.getGL();
        gl.glEnable( GL.GL_LIGHTING );     
        float a = (float) ambient.getValue();
        final float [] black = { 0,0,0,1 };
        float [] acol = { a,a,a, 1 };
        gl.glLightfv(GL.GL_LIGHT0 , GL.GL_SPECULAR, black, 0);
        gl.glLightfv(GL.GL_LIGHT0, GL.GL_DIFFUSE, black, 0);
        gl.glLightfv(GL.GL_LIGHT0, GL.GL_AMBIENT, black, 0);
        gl.glLightModelfv( GL.GL_LIGHT_MODEL_AMBIENT, acol, 0);
    }
    
    /**
     * signals end of the ambient pass
     * @param drawable
     */
    public void endAmbientPass( GLAutoDrawable drawable ) {
        final float [] black = { 0,0,0,1 };
        GL gl = drawable.getGL();
        gl.glLightModelfv( GL.GL_LIGHT_MODEL_AMBIENT, black, 0);
    }
    
    /**
     * Draws the light frustum and light position
     * @param drawable
     */
    public void debugLightFrustum( GLAutoDrawable drawable ) {
        GL gl = drawable.getGL();
        gl.glDisable( GL.GL_LIGHTING );
        gl.glColor4f(1,1,1,0.75f);
        gl.glPushMatrix();        
        float[] tmpM = new float[16];
        Matrix4f lpvm = new Matrix4f();
        lpvm.mul( lightProjectionMatrix, lightViewMatrix );
        lpvm.invert();
        copyMatrixToFlat(lpvm, tmpM);        
        gl.glMultMatrixf( tmpM, 0 );
        glut.glutWireCube(2);
        gl.glPopMatrix();
        gl.glPushMatrix();
        gl.glTranslated( lightPosition[0],lightPosition[1],lightPosition[2] );
        glut.glutSolidSphere(0.2,16,16);
        gl.glPopMatrix();
        gl.glEnable( GL.GL_LIGHTING );
    }
    
    /**
     * Prepares for drawing with the shadow map depth test
     * @param drawable
     */
    public void beginShadowMapping(GLAutoDrawable drawable) {
        GL gl = drawable.getGL();
        
        // ADD up other contributions...
        gl.glEnable( GL.GL_BLEND );
        gl.glBlendFunc( GL.GL_ONE, GL.GL_ONE );//GL.GL_SRC_COLOR, GL.GL_DST_COLOR );
        gl.glDepthFunc( GL.GL_EQUAL );
       
        // Draw with shadow map, layer on the diffuse lighting component only       
        float c1 = (float) diffuse.getValue();
        float [] d = { c1, c1,c1, 1 };
        final float [] black = { 0,0,0,1 };
        gl.glLightfv(GL.GL_LIGHT0 , GL.GL_SPECULAR, d, 0); // 
        gl.glLightfv(GL.GL_LIGHT0, GL.GL_DIFFUSE, d, 0);
        gl.glLightfv(GL.GL_LIGHT0, GL.GL_AMBIENT, black, 0);

        gl.glEnable( GL.GL_LIGHTING );

        
        gl.glBindTexture( GL.GL_TEXTURE_2D, depthTextureID );
        gl.glEnable( GL.GL_TEXTURE_2D );
        gl.glGenerateMipmapEXT(GL.GL_TEXTURE_2D);      
        gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, GL.GL_LINEAR_MIPMAP_LINEAR );//GL_NEAREST);

        // Enable shadow comparison
        gl.glTexParameteri( GL.GL_TEXTURE_2D, GL.GL_TEXTURE_COMPARE_MODE_ARB, GL.GL_COMPARE_R_TO_TEXTURE );
        // Shadow comparison should be true (ie not in shadow) if r<=texture
        gl.glTexParameteri( GL.GL_TEXTURE_2D, GL.GL_TEXTURE_COMPARE_FUNC_ARB, GL.GL_LEQUAL);
        // Shadow comparison should generate an INTENSITY result
        gl.glTexParameteri( GL.GL_TEXTURE_2D, GL.GL_DEPTH_TEXTURE_MODE_ARB, GL.GL_INTENSITY);        
        // Set alpha test to discard false comparisons
//                gl.glAlphaFunc( GL.GL_GEQUAL, 0.99f );
//                gl.glEnable( GL.GL_ALPHA_TEST );
        
        float[] row0 = new float[4];  textureMatrix.getRow( 0, row0 );        
        float[] row1 = new float[4];  textureMatrix.getRow( 1, row1 );
        float[] row2 = new float[4];  textureMatrix.getRow( 2, row2 );
        float[] row3 = new float[4];  textureMatrix.getRow( 3, row3 );
        
        //Set up texture coordinate generation.
        gl.glTexGeni(GL.GL_S, GL.GL_TEXTURE_GEN_MODE, GL.GL_EYE_LINEAR);
        gl.glTexGeni(GL.GL_T, GL.GL_TEXTURE_GEN_MODE, GL.GL_EYE_LINEAR);
        gl.glTexGeni(GL.GL_R, GL.GL_TEXTURE_GEN_MODE, GL.GL_EYE_LINEAR);
        gl.glTexGeni(GL.GL_Q, GL.GL_TEXTURE_GEN_MODE, GL.GL_EYE_LINEAR);        
        gl.glTexGenfv(GL.GL_S, GL.GL_EYE_PLANE, row0, 0 );
        gl.glTexGenfv(GL.GL_T, GL.GL_EYE_PLANE, row1, 0 );
        gl.glTexGenfv(GL.GL_R, GL.GL_EYE_PLANE, row2, 0 );
        gl.glTexGenfv(GL.GL_Q, GL.GL_EYE_PLANE, row3, 0 );
        gl.glEnable(GL.GL_TEXTURE_GEN_S);
        gl.glEnable(GL.GL_TEXTURE_GEN_T);
        gl.glEnable(GL.GL_TEXTURE_GEN_R);
        gl.glEnable(GL.GL_TEXTURE_GEN_Q);
    }

    /**
     * Finishes drawing with the shadow map depth test
     * @param drawable
     */
    public void endShadowMapping( GLAutoDrawable drawable ) {
        GL gl = drawable.getGL();
        gl.glDisable(GL.GL_TEXTURE_GEN_S);
        gl.glDisable(GL.GL_TEXTURE_GEN_T);
        gl.glDisable(GL.GL_TEXTURE_GEN_R);
        gl.glDisable(GL.GL_TEXTURE_GEN_Q);
//        gl.glDisable(GL.GL_ALPHA_TEST);
        
        gl.glDisable( GL.GL_BLEND );
        gl.glDepthFunc( GL.GL_LESS );                    
        gl.glDisable( GL.GL_TEXTURE_2D );
    }

    
    /** 
     * Displays the shadow map in the upper left corner of the window at 1/4 size
     * @param drawable
     */
    public void debug( GLAutoDrawable drawable ) {
        GL gl = drawable.getGL();
        EasyViewer.beginOverlay(drawable);
        gl.glEnable( GL.GL_TEXTURE_2D );
        gl.glBindTexture( GL.GL_TEXTURE_2D, depthTextureID );
        gl.glRasterPos2f(10, 256 + 10);
        gl.glColor3f(1.f, 1.f, 1.f);
        gl.glGetTexImage( GL.GL_TEXTURE_2D, 0, GL.GL_DEPTH_COMPONENT, GL.GL_FLOAT, depthBufferPixels );
        //gl.glPixelZoom( 0.25f, 0.25f );
        gl.glPixelZoom(256.f / (float) size, 256.f / (float) size);
        gl.glDrawPixels( size, size, GL.GL_LUMINANCE, GL.GL_FLOAT, depthBufferPixels );
        gl.glPixelZoom( 1, 1 );
        gl.glEnable( GL.GL_DEPTH_TEST );
        EasyViewer.endOverlay(drawable);
    }
    
    private int checkFramebufferStatus(GL gl, StringBuilder statusString) {
        int framebufferStatus = gl.glCheckFramebufferStatusEXT(GL.GL_FRAMEBUFFER_EXT);
        switch (framebufferStatus) {
            case GL.GL_FRAMEBUFFER_COMPLETE_EXT:
                statusString.append("GL_FRAMEBUFFER_COMPLETE_EXT");
                break;
            case GL.GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT_EXT:
                statusString.append("GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENTS_EXT");
                break;
            case GL.GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT_EXT:
                statusString.append("GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT_EXT");
                break;
            case GL.GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS_EXT:
                statusString.append("GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS_EXT");
                break;
            case GL.GL_FRAMEBUFFER_INCOMPLETE_FORMATS_EXT:
                statusString.append("GL_FRAMEBUFFER_INCOMPLETE_FORMATS_EXT");
                break;
            case GL.GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER_EXT:
                statusString.append("GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER_EXT");
                break;
            case GL.GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER_EXT:
                statusString.append("GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER_EXT");
                break;
            case GL.GL_FRAMEBUFFER_UNSUPPORTED_EXT:
                statusString.append("GL_FRAMEBUFFER_UNSUPPORTED_EXT");
                break;
        }
        return framebufferStatus;
    }
    
    /**
     * Copies the Matrix4f to the float array
     * @param src 
     * @param dst
     */
    public static void copyMatrixToFlat( Matrix4f src, float[] dst ) {
        dst[0] = src.m00;
        dst[1] = src.m10;
        dst[2] = src.m20;
        dst[3] = src.m30;
        dst[4] = src.m01;
        dst[5] = src.m11;
        dst[6] = src.m21;
        dst[7] = src.m31;
        dst[8] = src.m02;
        dst[9] = src.m12;
        dst[10] = src.m22;
        dst[11] = src.m32;
        dst[12] = src.m03;
        dst[13] = src.m13;
        dst[14] = src.m23;
        dst[15] = src.m33;
    }
    
    /**
     * Copies the float array to the Matrix4f
     * @param src 
     * @param dst
     */
    public static void copyFlatToMatrix( float[] src, Matrix4f dst ) {
        dst.m00 = src[0];
        dst.m10 = src[1];
        dst.m20 = src[2];
        dst.m30 = src[3];
        dst.m01 = src[4];
        dst.m11 = src[5];
        dst.m21 = src[6];
        dst.m31 = src[7];
        dst.m02 = src[8];
        dst.m12 = src[9];
        dst.m22 = src[10];
        dst.m32 = src[11];
        dst.m03 = src[12];
        dst.m13 = src[13];
        dst.m23 = src[14];
        dst.m33 = src[15];
    }
}
