package lpv.simulation.propagator;

import javax.vecmath.Point3i;
import javax.vecmath.Vector2f;
import javax.vecmath.Vector3f;
import javax.vecmath.Vector4f;

import lpv.LPV;
import lpv.SH;

public class CrytekSiggraphPropagator implements Propagator {
	// Original Author: Anton Kaplanyan
	// -
	// http://www.crytek.com/cryengine/cryengine3/presentations/light-propagation-volumes-in-cryengine-3
	// http://www.crytek.com/sites/default/files/Light_Propagation_Volumes.pdf

	private float factor;

	public CrytekSiggraphPropagator(float factor) {
		this.factor = factor;
	}

	@Override
	public void propagate(LPV in, LPV out, LPV acc, LPV geometry, Point3i pos, int iteration) {
		Vector4f pixelCoeffs = new Vector4f(0.0f, 0.0f, 0.0f, 0.0f);

		// 6-point axial gathering stencil "cross"
		IVPropagateDir(pixelCoeffs, in, geometry, pos, new Point3i(1, 0, 0));
		IVPropagateDir(pixelCoeffs, in, geometry, pos, new Point3i(-1, 0, 0));
		IVPropagateDir(pixelCoeffs, in, geometry, pos, new Point3i(0, 1, 0));
		IVPropagateDir(pixelCoeffs, in, geometry, pos, new Point3i(0, -1, 0));
		IVPropagateDir(pixelCoeffs, in, geometry, pos, new Point3i(0, 0, 1));
		IVPropagateDir(pixelCoeffs, in, geometry, pos, new Point3i(0, 0, -1));
		
		pixelCoeffs.scale(factor);
		if(iteration == 0) {
			pixelCoeffs.add(in.get(pos.x, pos.y, pos.z));
			//acc.get(pos.x, pos.y, pos.z).add(in.get(pos.x, pos.y, pos.z));
		}
		out.get(pos.x, pos.y, pos.z).set(pixelCoeffs);
		acc.get(pos.x, pos.y, pos.z).add(pixelCoeffs);
	}

	void IVPropagateDir(Vector4f pixelCoeffs, LPV in, LPV geometry, Point3i pos,
			Point3i nOffset) {
		// get adjacent cell's SH coeffs
		Point3i p = new Point3i(pos.x + nOffset.x, pos.y + nOffset.y,
				pos.z + nOffset.z);
		
		Vector4f sampleCoeffs = in.get(p.x, p.y, p.z);
		
		if(sampleCoeffs.x != 0.0f || sampleCoeffs.y != 0.0f || sampleCoeffs.z != 0.0f || sampleCoeffs.w != 0.0f) {
		
		
			// generate function for incoming direction from adjacent cell
			Vector4f shIncomingDirFunction = Cone90Degree(new Vector3f(nOffset.x, nOffset.y, nOffset.z));
					//SH.clampedCosineLobe(-nOffset.x, -nOffset.y, -nOffset.z);
					//Cone90Degree(new Vector3f(nOffset.x, nOffset.y, nOffset.z));
			// integrate incoming radiance with this function
			
			Vector4f geometryCoeffs = geometry.get(p.x, p.y, p.z);
			float occlusion = 1.0f - Math.max(0.0f, Math.min(1.0f, geometryCoeffs.dot(shIncomingDirFunction)));
			
			float incidentLuminance = occlusion * Math.max(0.0f,
					sampleCoeffs.dot(shIncomingDirFunction));
			
			Vector4f shOutgoing = shIncomingDirFunction;
					//SH.clampedCosineLobe(getPrimaryDirection(sampleCoeffs));
					//Cone90Degree(getPrimaryDirection(sampleCoeffs));
			shOutgoing.scale(incidentLuminance);
			
			pixelCoeffs.add(shOutgoing);
		}
	}
	
	private Vector4f SHProjectCone(Vector3f kDir) { 
		Vector2f kZHCoeffs = new Vector2f(0.886226925452758f, 1.023326707946488f); 	
		return SHRotate(kDir, kZHCoeffs);
	}

	private Vector4f Cone90Degree(Vector3f vcDir) {
		return SHProjectCone(vcDir, (float) Math.toRadians(90.0f));
	}
	
	private Vector3f getPrimaryDirection(Vector4f sh) {
		Vector3f dir = new Vector3f(-sh.w, -sh.y, sh.z);
		if(dir.x == 0.0f && dir.y == 0.0f && dir.z == 0.0f) {
			return dir;
		} else {
			dir.normalize();
			return dir;
		}
	}

	private Vector4f SHProjectCone(Vector3f vcDir, float angle) {
		Vector2f vZHCoeffs = new Vector2f(
				0.5f * (1.0f - (float) Math.cos(angle)), // 1/2 (1 -
															// Cos[\[Alpha]])
				0.75f * (float) Math.sin(angle) * (float) Math.sin(angle)); // 3/4
																			// Sin[\[Alpha]]^2
		return SHRotate(vcDir, vZHCoeffs);
	}

	private Vector4f SHRotate(Vector3f vcDir, Vector2f vZHCoeffs) {
		// compute sine and cosine of thetta angle
		// beware of singularity when both x and y are 0 (no need to rotate at
		// all)

		Vector2f theta12_cs;
		if(vcDir.x == 0.0f && vcDir.y == 0.0f) {
			theta12_cs = new Vector2f(0.0f, 0.0f);
		} else {
			theta12_cs = new Vector2f(vcDir.x, vcDir.y);
			theta12_cs.normalize();
		}

		// compute sine and cosine of phi angle
		Vector2f phi12_cs = new Vector2f();
		phi12_cs.x = (float) Math.sqrt(1.0f - vcDir.z * vcDir.z);
		phi12_cs.y = vcDir.z;

		Vector4f vResult = new Vector4f();
		// The first band is rotation-independent
		vResult.x = vZHCoeffs.x;
		// rotating the second band of SH
		vResult.y = vZHCoeffs.y * phi12_cs.x * theta12_cs.y;
		vResult.z = -vZHCoeffs.y * phi12_cs.y;
		vResult.w = vZHCoeffs.y * phi12_cs.x * theta12_cs.x;
		return vResult;
	}

}