package shadersmod.client;

import java.nio.FloatBuffer;

import net.minecraft.client.renderer.GLAllocation;
import net.minecraft.client.renderer.GlStateManager;
import net.minecraft.client.renderer.culling.ClippingHelper;
import net.minecraft.client.renderer.culling.ClippingHelperImpl;
import net.minecraft.util.math.MathHelper;

public class ClippingHelperShadow extends ClippingHelper {

    private static ClippingHelperShadow instance = new ClippingHelperShadow();
    float[] frustumTest = new float[6];
    float[][] shadowClipPlanes = new float[10][4];
    int shadowClipPlaneCount;
    float[] matInvMP = new float[16];
    float[] vecIntersection = new float[4];
    //float[] vecShadowPlane = new float[4];
	
    /**
     * Returns true if the box is inside all clipping planes, otherwise returns false.
     */
    public boolean isBoxInFrustum(double x1, double y1, double z1, double x2, double y2, double z2)
    {
        for (int index = 0; index < shadowClipPlaneCount; ++index)
        {
            float[] plane = this.shadowClipPlanes[index];
            if (this.dot4(plane, x1, y1, z1) <= 0.0D &&
                this.dot4(plane, x2, y1, z1) <= 0.0D && 
                this.dot4(plane, x1, y2, z1) <= 0.0D && 
                this.dot4(plane, x2, y2, z1) <= 0.0D && 
                this.dot4(plane, x1, y1, z2) <= 0.0D && 
                this.dot4(plane, x2, y1, z2) <= 0.0D && 
                this.dot4(plane, x1, y2, z2) <= 0.0D && 
                this.dot4(plane, x2, y2, z2) <= 0.0D)
            {
                // All corner points are outside
                return false;
            }
        }

        return true;
    }

    private double dot4(float[] plane, double x, double y, double z)
    {
        return (double)plane[0] * x + (double)plane[1] * y + (double)plane[2] * z + (double)plane[3];
    }

    private double dot3(float[] vecA, float[] vecB)
    {
        return (double)vecA[0] * vecB[0] + (double)vecA[1] * vecB[1] + (double)vecA[2] * vecB[2];
    }

    public static ClippingHelper getInstance()
    {
        instance.init();
        return instance;
    }

    private void normalizePlane(float[] plane)
    {
        float length = MathHelper.sqrt(plane[0] * plane[0] + plane[1] * plane[1] + plane[2] * plane[2]);
        plane[0] /= length;
        plane[1] /= length;
        plane[2] /= length;
        plane[3] /= length;
    }

    private void normalize3(float[] plane)
    {
        float length = MathHelper.sqrt(plane[0] * plane[0] + plane[1] * plane[1] + plane[2] * plane[2]);
    	if (length==0f) length=1f;
        plane[0] /= length;
        plane[1] /= length;
        plane[2] /= length;
    }

    private void assignPlane(float[] plane, float a, float b, float c, float d)
    {
    	float length = (float)Math.sqrt(a*a + b*b + c*c);
    	plane[0] = a/length;
    	plane[1] = b/length;
    	plane[2] = c/length;
    	plane[3] = d/length;
    }

    private void copyPlane(float[] dst, float[] src)
    {
    	dst[0] = src[0];
    	dst[1] = src[1];
    	dst[2] = src[2];
    	dst[3] = src[3];
    }

    private void cross3(float[] out, float[] a, float[] b)
    {
    	out[0] = a[1]*b[2] - a[2]*b[1];
    	out[1] = a[2]*b[0] - a[0]*b[2];
    	out[2] = a[0]*b[1] - a[1]*b[0];
    }
    
    private void addShadowClipPlane(float[] plane)
    {
    	copyPlane(shadowClipPlanes[shadowClipPlaneCount++], plane);
    }
    
    private float length(float x, float y, float z)
    {
    	return (float)Math.sqrt(x*x + y*y + z*z);
    }
    private float distance(float x1, float y1, float z1, float x2, float y2, float z2)
    {
    	return length(x1-x2, y1-y2, z1-z2);
    }
    
    private void makeShadowPlane(float[] shadowPlane, float[] positivePlane, float[] negativePlane, float[] vecSun)
    {
		cross3(vecIntersection,positivePlane,negativePlane);
		cross3(shadowPlane,vecIntersection,vecSun);
		normalize3(shadowPlane);
		// calculate vector component
		// shadowPlane = k1 * positivePlane + k2 * negativePlane 
		// k1 = distance(S,N*dot(S,N)) / distance(P,N*dot(P,N))
		// k2 = distance(S,P*dot(S,P)) / distance(P,P*dot(N,P))
		float dotPN = (float)dot3(positivePlane,negativePlane);
		float dotNP = dotPN;
		float dotSN = (float)dot3(shadowPlane,negativePlane);
		float disSN = distance(shadowPlane  [0],shadowPlane  [1],shadowPlane  [2], negativePlane[0]*dotSN,negativePlane[1]*dotSN,negativePlane[2]*dotSN);
		float disPN = distance(positivePlane[0],positivePlane[1],positivePlane[2], negativePlane[0]*dotPN,negativePlane[1]*dotPN,negativePlane[2]*dotPN);
		float k1 = disSN/disPN;
		float dotSP = (float)dot3(shadowPlane,positivePlane);
		float disSP = distance(shadowPlane  [0],shadowPlane  [1],shadowPlane  [2], positivePlane[0]*dotSP,positivePlane[1]*dotSP,positivePlane[2]*dotSP);
		float disNP = distance(negativePlane[0],negativePlane[1],negativePlane[2], positivePlane[0]*dotNP,positivePlane[1]*dotNP,positivePlane[2]*dotNP);
		float k2 = disSP/disNP;
		//
		//if (debug) {
		//	float s0 = positivePlane[0]*k1 + negativePlane[0]*k2;
		//	float s1 = positivePlane[1]*k1 + negativePlane[1]*k2;
		//	float s2 = positivePlane[2]*k1 + negativePlane[2]*k2;
		//	float s3 = positivePlane[3]*k1 + negativePlane[3]*k2;
		//	System.out.printf("normal %f %f %f\nplane  %f %f %f %f\n\n", shadowPlane[0],shadowPlane[1],shadowPlane[2],s0,s1,s2,s3);
		//}
		shadowPlane[3] = positivePlane[3]*k1 + negativePlane[3]*k2;
    }

    public void init()
    {
        float[] matPrj = this.projectionMatrix;
        float[] matMdv = this.modelviewMatrix;
        float[] matMP = this.clippingMatrix;
        System.arraycopy(Shaders.faProjection, 0, matPrj, 0, 16);
        System.arraycopy(Shaders.faModelView, 0, matMdv, 0, 16);
        // matMdv[14] += 20.0; //debug
        
        SMath.multiplyMat4xMat4(matMP, matMdv, matPrj);
        // model frustum planes
        // 0: right plane
        assignPlane(this.frustum[0], matMP[4*0+3] - matMP[4*0+0], matMP[4*1+3] - matMP[4*1+0], matMP[4*2+3] - matMP[4*2+0], matMP[4*3+3] - matMP[4*3+0]);
        // 1: left plane
        assignPlane(this.frustum[1], matMP[4*0+3] + matMP[4*0+0], matMP[4*1+3] + matMP[4*1+0], matMP[4*2+3] + matMP[4*2+0], matMP[4*3+3] + matMP[4*3+0]);
        // 2: bottom plane
        assignPlane(this.frustum[2], matMP[4*0+3] + matMP[4*0+1], matMP[4*1+3] + matMP[4*1+1], matMP[4*2+3] + matMP[4*2+1], matMP[4*3+3] + matMP[4*3+1]);
        // 3: top plane
        assignPlane(this.frustum[3], matMP[4*0+3] - matMP[4*0+1], matMP[4*1+3] - matMP[4*1+1], matMP[4*2+3] - matMP[4*2+1], matMP[4*3+3] - matMP[4*3+1]);
        // 4: far plane
        assignPlane(this.frustum[4], matMP[4*0+3] - matMP[4*0+2], matMP[4*1+3] - matMP[4*1+2], matMP[4*2+3] - matMP[4*2+2], matMP[4*3+3] - matMP[4*3+2]);
        // 5: near plane
        assignPlane(this.frustum[5], matMP[4*0+3] + matMP[4*0+2], matMP[4*1+3] + matMP[4*1+2], matMP[4*2+3] + matMP[4*2+2], matMP[4*3+3] + matMP[4*3+2]);

        //float[] matInvMP = this.matInvMP;
        //SMath.multiplyMat4xMat4(matInvMP, Shaders.faProjectionInverse, Shaders.faModelViewInverse);
        
        float[] vecSun = Shaders.shadowLightPositionVector;
        float test0 = (float)dot3(this.frustum[0],vecSun);
        float test1 = (float)dot3(this.frustum[1],vecSun);
        float test2 = (float)dot3(this.frustum[2],vecSun);
        float test3 = (float)dot3(this.frustum[3],vecSun);
        float test4 = (float)dot3(this.frustum[4],vecSun);
        float test5 = (float)dot3(this.frustum[5],vecSun);
        
        shadowClipPlaneCount = 0;
        // check each plane and edge
        // 0: right plane
        if (test0 >= 0) {
        	copyPlane(shadowClipPlanes[shadowClipPlaneCount++], frustum[0]);
        	if (test0 > 0) {
	        	if (test2 < 0) { // bottom plane
	        		makeShadowPlane(shadowClipPlanes[shadowClipPlaneCount++],frustum[0],frustum[2],vecSun);
	        	}
	        	if (test3 < 0) { // top plane
	        		makeShadowPlane(shadowClipPlanes[shadowClipPlaneCount++],frustum[0],frustum[3],vecSun);
	        	}
	        	if (test4 < 0) { // far plane
	        		makeShadowPlane(shadowClipPlanes[shadowClipPlaneCount++],frustum[0],frustum[4],vecSun);
	        	}
	        	if (test5 < 0) { // near plane
	        		makeShadowPlane(shadowClipPlanes[shadowClipPlaneCount++],frustum[0],frustum[5],vecSun);
	        	}
        	}
        }
        // 1: left plane
        if (test1 >= 0) {
        	copyPlane(shadowClipPlanes[shadowClipPlaneCount++], frustum[1]);
        	if (test1 > 0) {
	        	if (test2 < 0) { // bottom plane
	        		makeShadowPlane(shadowClipPlanes[shadowClipPlaneCount++],frustum[1],frustum[2],vecSun);
	        	}
	        	if (test3 < 0) { // top plane
	        		makeShadowPlane(shadowClipPlanes[shadowClipPlaneCount++],frustum[1],frustum[3],vecSun);
	        	}
	        	if (test4 < 0) { // far plane
	        		makeShadowPlane(shadowClipPlanes[shadowClipPlaneCount++],frustum[1],frustum[4],vecSun);
	        	}
	        	if (test5 < 0) { // near plane
	        		makeShadowPlane(shadowClipPlanes[shadowClipPlaneCount++],frustum[1],frustum[5],vecSun);
	        	}
        	}
        }
        // 2: bottom plane
        if (test2 >= 0) {
        	copyPlane(shadowClipPlanes[shadowClipPlaneCount++], frustum[2]);
        	if (test2 > 0) {
	        	if (test0 < 0) { // left plane
	        		makeShadowPlane(shadowClipPlanes[shadowClipPlaneCount++],frustum[2],frustum[0],vecSun);
	        	}
	        	if (test1 < 0) { // right plane
	        		makeShadowPlane(shadowClipPlanes[shadowClipPlaneCount++],frustum[2],frustum[1],vecSun);
	        	}
	        	if (test4 < 0) { // far plane
	        		makeShadowPlane(shadowClipPlanes[shadowClipPlaneCount++],frustum[2],frustum[4],vecSun);
	        	}
	        	if (test5 < 0) { // near plane
	        		makeShadowPlane(shadowClipPlanes[shadowClipPlaneCount++],frustum[2],frustum[5],vecSun);
	        	}
        	}
        }
        // 3: top plane
        if (test3 >= 0) {
        	copyPlane(shadowClipPlanes[shadowClipPlaneCount++], frustum[3]);
        	if (test3 > 0) {
	        	if (test0 < 0) { // left plane
	        		makeShadowPlane(shadowClipPlanes[shadowClipPlaneCount++],frustum[3],frustum[0],vecSun);
	        	}
	        	if (test1 < 0) { // right plane
	        		makeShadowPlane(shadowClipPlanes[shadowClipPlaneCount++],frustum[3],frustum[1],vecSun);
	        	}
	        	if (test4 < 0) { // far plane
	        		makeShadowPlane(shadowClipPlanes[shadowClipPlaneCount++],frustum[3],frustum[4],vecSun);
	        	}
	        	if (test5 < 0) { // near plane
	        		makeShadowPlane(shadowClipPlanes[shadowClipPlaneCount++],frustum[3],frustum[5],vecSun);
	        	}
        	}
        }
        // 4: far plane
        if (test4 >= 0) {
        	copyPlane(shadowClipPlanes[shadowClipPlaneCount++], frustum[4]);
        	if (test4 > 0) {
	        	if (test0 < 0) { // left plane
	        		makeShadowPlane(shadowClipPlanes[shadowClipPlaneCount++],frustum[4],frustum[0],vecSun);
	        	}
	        	if (test1 < 0) { // right plane
	        		makeShadowPlane(shadowClipPlanes[shadowClipPlaneCount++],frustum[4],frustum[1],vecSun);
	        	}
	        	if (test2 < 0) { // bottom plane
	        		makeShadowPlane(shadowClipPlanes[shadowClipPlaneCount++],frustum[4],frustum[2],vecSun);
	        	}
	        	if (test3 < 0) { // top plane
	        		makeShadowPlane(shadowClipPlanes[shadowClipPlaneCount++],frustum[4],frustum[3],vecSun);
	        	}
        	}
        }
        // 5: near plane
        if (test5 >= 0) {
        	copyPlane(shadowClipPlanes[shadowClipPlaneCount++], frustum[5]);
        	if (test5 > 0) {
	        	if (test0 < 0) { // left plane
	        		makeShadowPlane(shadowClipPlanes[shadowClipPlaneCount++],frustum[5],frustum[0],vecSun);
	        	}
	        	if (test1 < 0) { // right plane
	        		makeShadowPlane(shadowClipPlanes[shadowClipPlaneCount++],frustum[5],frustum[1],vecSun);
	        	}
	        	if (test2 < 0) { // bottom plane
	        		makeShadowPlane(shadowClipPlanes[shadowClipPlaneCount++],frustum[5],frustum[2],vecSun);
	        	}
	        	if (test3 < 0) { // top plane
	        		makeShadowPlane(shadowClipPlanes[shadowClipPlaneCount++],frustum[5],frustum[3],vecSun);
	        	}
        	}
        }
//        System.out.printf("test %3.0f %3.0f %3.0f %3.0f %3.0f %3.0f %3d\n",
//        		Math.signum(test0),Math.signum(test1),Math.signum(test2),
//        		Math.signum(test3),Math.signum(test4),Math.signum(test5),
//        		shadowClipPlaneCount);

    }
}
