package lpv.simulation.propagator;

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

import lpv.LPV;
import lpv.SH;

public class NvidiaPropagator implements Propagator {
	// /home/dt07jo1/Documents/DiffuseGlobalIllumination/Direct3D/Source/DiffuseGlobalIllumination/LPV_Propagate.hlsl
	// http://developer.nvidia.com/nvidia-graphics-sdk-11-direct3d

	private static class PropagateConsts {
		Vector4f neighborOffset;
		float solidAngle;
		float x;
		float y;
		float z;
	}

	private float factor;
	private PropagateConsts[] g_propagateValues;
	private boolean copyInjected;

	private static final Point3i[] offsets = new Point3i[] {
			new Point3i(0, 0, 1), new Point3i(1, 0, 0), new Point3i(0, 0, -1),
			new Point3i(-1, 0, 0), new Point3i(0, 1, 0), new Point3i(0, -1, 0) };

	private static final Vector4f[] faceCoeffs = new Vector4f[] {
			SH.clampedCosineLobe(offsets[0]), SH.clampedCosineLobe(offsets[1]),
			SH.clampedCosineLobe(offsets[2]), SH.clampedCosineLobe(offsets[3]),
			SH.clampedCosineLobe(offsets[4]), SH.clampedCosineLobe(offsets[5]) };

	public NvidiaPropagator(float factor, boolean copyInjected) {
		this.factor = factor;
		this.copyInjected = copyInjected;

		g_propagateValues = new PropagateConsts[36];
		for (int i = 0; i < g_propagateValues.length; i++) {
			g_propagateValues[i] = new PropagateConsts();
		}

		for (int neighbor = 0; neighbor < 6; neighbor++) {
			Vector3f neighborCellCenter = new Vector3f(offsets[neighbor].x,
					offsets[neighbor].y, offsets[neighbor].z);

			// for each of the six faces of a cell
			for (int face = 0; face < 6; face++) {
				Vector3f facePosition = new Vector3f(offsets[face].x,
						offsets[face].y, offsets[face].z);
				facePosition.scale(0.5f);

				// the vector from the neighbor's cell center
				Vector3f vecFromNCC = new Vector3f(facePosition);
				vecFromNCC.sub(neighborCellCenter);

				float length = vecFromNCC.length();
				vecFromNCC.scale(1.0f / length);

				g_propagateValues[neighbor * 6 + face].neighborOffset = new Vector4f(
						neighborCellCenter.x, neighborCellCenter.y,
						neighborCellCenter.z, 1.0f);
				g_propagateValues[neighbor * 6 + face].x = vecFromNCC.x;
				g_propagateValues[neighbor * 6 + face].y = vecFromNCC.y;
				g_propagateValues[neighbor * 6 + face].z = vecFromNCC.z;

				// the solid angle subtended by the face onto the neighbor cell
				// center is one of two values below, depending on whether the
				// cell center is directly below
				// the face or off to one side.
				// note, there is a derivation for these numbers (based on the
				// solid angle subtended at the apex of a foursided right
				// regular pyramid)
				// we also normalize the solid angle by dividing by 4*PI (the
				// solid angle of a sphere measured from a point in its
				// interior)
				if (length <= 0.5f) {
					g_propagateValues[neighbor * 6 + face].solidAngle = 0.0f;
				} else {
					g_propagateValues[neighbor * 6 + face].solidAngle = length >= 1.5f ? 22.95668f / (4 * 180.0f)
							: 24.26083f / (4 * 180.0f);
				}
			}
		}
	}

	@Override
	public void propagate(LPV in, LPV out, LPV acc, LPV geometry, Point3i pos,
			int iteration) {

		Vector4f[] GV = new Vector4f[] { new Vector4f(), new Vector4f(),
				new Vector4f(), new Vector4f(), new Vector4f(), new Vector4f(),
				new Vector4f(), new Vector4f() };
		loadOffsetTexValue(geometry, pos, new Vector4f(0.0f, 0.0f, 0.0f, 1.0f),
				GV[0]);
		loadOffsetTexValue(geometry, pos,
				new Vector4f(0.0f, 0.0f, -1.0f, 1.0f), GV[1]);
		loadOffsetTexValue(geometry, pos,
				new Vector4f(0.0f, -1.0f, 0.0f, 1.0f), GV[2]);
		loadOffsetTexValue(geometry, pos,
				new Vector4f(-1.0f, 0.0f, 0.0f, 1.0f), GV[3]);
		loadOffsetTexValue(geometry, pos,
				new Vector4f(-1.0f, -1.0f, 0.0f, 1.0f), GV[4]);
		loadOffsetTexValue(geometry, pos,
				new Vector4f(-1.0f, 0.0f, -1.0f, 1.0f), GV[5]);
		loadOffsetTexValue(geometry, pos,
				new Vector4f(0.0f, -1.0f, -1.0f, 1.0f), GV[6]);
		loadOffsetTexValue(geometry, pos, new Vector4f(-1.0f, -1.0f, -1.0f,
				1.0f), GV[7]);

		Vector4f SHCoefficients = new Vector4f(0, 0, 0, 0);

		int index = 0;
		for (int neighbor = 0; neighbor < 6; neighbor++) {
			Vector4f inSHCoefficients = new Vector4f(0, 0, 0, 0);

			Vector4f neighborOffset = g_propagateValues[neighbor * 6].neighborOffset;

			// load the light value in the neighbor cell
			loadOffsetTexValue(in, pos, neighborOffset, inSHCoefficients);

			Vector4f GVSHCoefficients = new Vector4f(0.0f, 0.0f, 0.0f, 0.0f);
			if (neighbor == 1) {
				GVSHCoefficients.add(GV[6]);
				GVSHCoefficients.add(GV[1]);
				GVSHCoefficients.add(GV[2]);
				GVSHCoefficients.add(GV[0]);
			} else if (neighbor == 3) {
				GVSHCoefficients.add(GV[7]);
				GVSHCoefficients.add(GV[5]);
				GVSHCoefficients.add(GV[4]);
				GVSHCoefficients.add(GV[3]);
			} else if (neighbor == 4) {
				GVSHCoefficients.add(GV[0]);
				GVSHCoefficients.add(GV[3]);
				GVSHCoefficients.add(GV[1]);
				GVSHCoefficients.add(GV[5]);
			} else if (neighbor == 5) {
				GVSHCoefficients.add(GV[2]);
				GVSHCoefficients.add(GV[4]);
				GVSHCoefficients.add(GV[6]);
				GVSHCoefficients.add(GV[7]);
			} else if (neighbor == 0) {
				GVSHCoefficients.add(GV[0]);
				GVSHCoefficients.add(GV[3]);
				GVSHCoefficients.add(GV[2]);
				GVSHCoefficients.add(GV[4]);
			} else if (neighbor == 2) {
				GVSHCoefficients.add(GV[1]);
				GVSHCoefficients.add(GV[5]);
				GVSHCoefficients.add(GV[6]);
				GVSHCoefficients.add(GV[7]);
			}
			GVSHCoefficients.scale(0.25f);

			for (int face = 0; face < 6; face++) {
				// evaluate the SH approximation of the intensity coming from
				// the neighboring cell to this face
				Vector3f dir = new Vector3f();
				dir.x = g_propagateValues[index].x;
				dir.y = g_propagateValues[index].y;
				dir.z = g_propagateValues[index].z;
				dir.normalize();
				float solidAngle = g_propagateValues[index].solidAngle;

				Vector4f dirSH = SH.construct(dir.x, dir.y, dir.z);
				
				float occlusion = 1.0f - Math.max(0.0f, Math.min(1.0f, GVSHCoefficients.dot(dirSH)));

				float inFlux = 0;

				// approximate our SH coefficients in the direction dir.
				// to do this we sum the product of the stored SH coefficients
				// with the SH basis function in the direction dir
				float flux = occlusion * solidAngle
						* Math.max(
								0,
								(inSHCoefficients.x * dirSH.x
										+ inSHCoefficients.y * dirSH.y
										+ inSHCoefficients.z * dirSH.z + inSHCoefficients.w
										* dirSH.w));

				inFlux += flux;

				Vector4f coeffs = faceCoeffs[face];

				inFlux *= factor;

				SHCoefficients.add(new Vector4f(inFlux * coeffs.x, inFlux
						* coeffs.y, inFlux * coeffs.z, inFlux * coeffs.w));

				index++;
			}
		}

		// write back the updated flux
		if (copyInjected && iteration == 0) {
			SHCoefficients.add(in.get(pos.x, pos.y, pos.z));
		}
		out.get(pos.x, pos.y, pos.z).set(SHCoefficients);
		acc.get(pos.x, pos.y, pos.z).add(SHCoefficients);
	}

	private void loadOffsetTexValue(LPV in, Point3i pos,
			Vector4f neighborOffset, Vector4f inSHCoefficients) {
		int x = (int) (pos.x + neighborOffset.x);
		int y = (int) (pos.y + neighborOffset.y);
		int z = (int) (pos.z + neighborOffset.z);
		inSHCoefficients.set(in.get(x, y, z));
	}
}
