package com.logisticscraft.occlusionculling;

import java.util.Arrays;
import java.util.BitSet;

import com.logisticscraft.occlusionculling.cache.ArrayOcclusionCache;
import com.logisticscraft.occlusionculling.cache.OcclusionCache;
import com.logisticscraft.occlusionculling.util.MathUtilities;
import com.logisticscraft.occlusionculling.util.Vec3d;

public class OcclusionCullingInstance {

	private enum Relative {
		INSIDE, POSITIVE, NEGATIVE;

		public static Relative from(int min, int max, int pos) {
			if (max > pos && min > pos) {
				return POSITIVE;
			} else if (min < pos && max < pos) {
				return NEGATIVE;
			}
			return INSIDE;
		}
	}

	private static final int ON_MIN_X = 0x01;
	private static final int ON_MAX_X = 0x02;
	private static final int ON_MIN_Y = 0x04;
	private static final int ON_MAX_Y = 0x08;
	private static final int ON_MIN_Z = 0x10;

	private static final int ON_MAX_Z = 0x20;
	private final int reach;
	private final double aabbExpansion;
	private final DataProvider provider;

	private final OcclusionCache cache;
	// Reused allocated data structures
	private final BitSet skipList = new BitSet(); // Grows bigger in case some mod introduces giant hitboxes
	private final Vec3d[] targetPoints = new Vec3d[15];
	private final Vec3d targetPos = new Vec3d(0, 0, 0);
	private final int[] cameraPos = new int[3];
	private final boolean[] dotselectors = new boolean[14];
	private boolean allowRayChecks = false;
	private final int[] lastHitBlock = new int[3];

	private boolean allowWallClipping = false;

	public OcclusionCullingInstance(int maxDistance, DataProvider provider) {
		this(maxDistance, provider, new ArrayOcclusionCache(maxDistance), 0.5);
	}

	public OcclusionCullingInstance(int maxDistance, DataProvider provider, OcclusionCache cache,
			double aabbExpansion) {
		this.reach = maxDistance;
		this.provider = provider;
		this.cache = cache;
		this.aabbExpansion = aabbExpansion;
		for (int i = 0; i < targetPoints.length; i++) {
			targetPoints[i] = new Vec3d(0, 0, 0);
		}
	}

	private void cacheResult(int x, int y, int z, boolean result) {
		int cx = x - cameraPos[0] + reach;
		int cy = y - cameraPos[1] + reach;
		int cz = z - cameraPos[2] + reach;
		if (result) {
			cache.setVisible(cx, cy, cz);
		} else {
			cache.setHidden(cx, cy, cz);
		}
	}

	private void cacheResult(Vec3d vector, boolean result) {
		int cx = MathUtilities.floor(vector.x) - cameraPos[0] + reach;
		int cy = MathUtilities.floor(vector.y) - cameraPos[1] + reach;
		int cz = MathUtilities.floor(vector.z) - cameraPos[2] + reach;
		if (result) {
			cache.setVisible(cx, cy, cz);
		} else {
			cache.setHidden(cx, cy, cz);
		}
	}

	// -1 = invalid location, 0 = not checked yet, 1 = visible, 2 = occluding
	private int getCacheValue(int x, int y, int z) {
		x -= cameraPos[0];
		y -= cameraPos[1];
		z -= cameraPos[2];
		if (Math.abs(x) > reach - 2 || Math.abs(y) > reach - 2 || Math.abs(z) > reach - 2) {
			return -1;
		}

		// check if target is already known
		return cache.getState(x + reach, y + reach, z + reach);
	}

	public boolean isAABBVisible(Vec3d aabbMin, Vec3d aabbMax, Vec3d viewerPosition) {
		try {
			int maxX = MathUtilities.floor(aabbMax.x + aabbExpansion);
			int maxY = MathUtilities.floor(aabbMax.y + aabbExpansion);
			int maxZ = MathUtilities.floor(aabbMax.z + aabbExpansion);
			int minX = MathUtilities.floor(aabbMin.x - aabbExpansion);
			int minY = MathUtilities.floor(aabbMin.y - aabbExpansion);
			int minZ = MathUtilities.floor(aabbMin.z - aabbExpansion);

			cameraPos[0] = MathUtilities.floor(viewerPosition.x);
			cameraPos[1] = MathUtilities.floor(viewerPosition.y);
			cameraPos[2] = MathUtilities.floor(viewerPosition.z);

			Relative relX = Relative.from(minX, maxX, cameraPos[0]);
			Relative relY = Relative.from(minY, maxY, cameraPos[1]);
			Relative relZ = Relative.from(minZ, maxZ, cameraPos[2]);

			if (relX == Relative.INSIDE && relY == Relative.INSIDE && relZ == Relative.INSIDE) {
				return true; // We are inside of the AABB, don't cull
			}

			skipList.clear();

			// Just check the cache first
			int id = 0;
			for (int x = minX; x <= maxX; x++) {
				for (int y = minY; y <= maxY; y++) {
					for (int z = minZ; z <= maxZ; z++) {
						int cachedValue = getCacheValue(x, y, z);

						if (cachedValue == 1) {
							// non-occluding
							return true;
						}

						if (cachedValue != 0) {
							// was checked and it wasn't visible
							skipList.set(id);
						}
						id++;
					}
				}
			}

			// only after the first hit wall the cache becomes valid.
			allowRayChecks = false;

			// since the cache wasn't helpfull
			id = 0;
			for (int x = minX; x <= maxX; x++) {
				byte visibleOnFaceX = 0;
				byte faceEdgeDataX = 0;
				faceEdgeDataX |= (x == minX) ? ON_MIN_X : 0;
				faceEdgeDataX |= (x == maxX) ? ON_MAX_X : 0;
				visibleOnFaceX |= (x == minX && relX == Relative.POSITIVE) ? ON_MIN_X : 0;
				visibleOnFaceX |= (x == maxX && relX == Relative.NEGATIVE) ? ON_MAX_X : 0;
				for (int y = minY; y <= maxY; y++) {
					byte faceEdgeDataY = faceEdgeDataX;
					byte visibleOnFaceY = visibleOnFaceX;
					faceEdgeDataY |= (y == minY) ? ON_MIN_Y : 0;
					faceEdgeDataY |= (y == maxY) ? ON_MAX_Y : 0;
					visibleOnFaceY |= (y == minY && relY == Relative.POSITIVE) ? ON_MIN_Y : 0;
					visibleOnFaceY |= (y == maxY && relY == Relative.NEGATIVE) ? ON_MAX_Y : 0;
					for (int z = minZ; z <= maxZ; z++) {
						byte faceEdgeData = faceEdgeDataY;
						byte visibleOnFace = visibleOnFaceY;
						faceEdgeData |= (z == minZ) ? ON_MIN_Z : 0;
						faceEdgeData |= (z == maxZ) ? ON_MAX_Z : 0;
						visibleOnFace |= (z == minZ && relZ == Relative.POSITIVE) ? ON_MIN_Z : 0;
						visibleOnFace |= (z == maxZ && relZ == Relative.NEGATIVE) ? ON_MAX_Z : 0;
						if (skipList.get(id)) { // was checked and it wasn't visible
							id++;
							continue;
						}

						if (visibleOnFace != 0) {
							targetPos.set(x, y, z);
							if (isVoxelVisible(viewerPosition, targetPos, faceEdgeData, visibleOnFace)) {
								return true;
							}
						}
						id++;
					}
				}
			}

			return false;
		} catch (Throwable t) {
			// Failsafe
			t.printStackTrace();
		}
		return true;
	}

	/**
	 * returns the grid cells that intersect with this Vec3d<br>
	 * <a href=
	 * "http://playtechs.blogspot.de/2007/03/raytracing-on-grid.html">http://playtechs.blogspot.de/2007/03/raytracing-on-grid.html</a>
	 * <p>
	 * Caching assumes that all Vec3d's are inside the same block
	 */
	private boolean isVisible(Vec3d start, Vec3d[] targets, int size) {
		// start cell coordinate
		int x = cameraPos[0];
		int y = cameraPos[1];
		int z = cameraPos[2];

		for (int v = 0; v < size; v++) {
			// ray-casting target
			Vec3d target = targets[v];

			double relativeX = start.x - target.getX();
			double relativeY = start.y - target.getY();
			double relativeZ = start.z - target.getZ();

			if (allowRayChecks
					&& rayIntersection(lastHitBlock, start, new Vec3d(relativeX, relativeY, relativeZ).normalize())) {
				continue;
			}

			// horizontal and vertical cell amount spanned
			double dimensionX = Math.abs(relativeX);
			double dimensionY = Math.abs(relativeY);
			double dimensionZ = Math.abs(relativeZ);

			// distance between horizontal intersection points with cell border as a
			// fraction of the total Vec3d length
			double dimFracX = 1f / dimensionX;
			// distance between vertical intersection points with cell border as a fraction
			// of the total Vec3d length
			double dimFracY = 1f / dimensionY;
			double dimFracZ = 1f / dimensionZ;

			// total amount of intersected cells
			int intersectCount = 1;

			// 1, 0 or -1
			// determines the direction of the next cell (horizontally / vertically)
			int x_inc, y_inc, z_inc;

			// the distance to the next horizontal / vertical intersection point with a cell
			// border as a fraction of the total Vec3d length
			double t_next_y, t_next_x, t_next_z;

			if (dimensionX == 0f) {
				x_inc = 0;
				t_next_x = dimFracX; // don't increment horizontally because the Vec3d is perfectly vertical
			} else if (target.x > start.x) {
				x_inc = 1; // target point is horizontally greater than starting point so increment every
				// step by 1
				intersectCount += MathUtilities.floor(target.x) - x; // increment total amount of intersecting cells
				t_next_x = (float) ((x + 1 - start.x) * dimFracX); // calculate the next horizontal
				// intersection
				// point based on the position inside
				// the first cell
			} else {
				x_inc = -1; // target point is horizontally smaller than starting point so reduce every step
				// by 1
				intersectCount += x - MathUtilities.floor(target.x); // increment total amount of intersecting cells
				t_next_x = (float) ((start.x - x) * dimFracX); // calculate the next horizontal
				// intersection point
				// based on the position inside
				// the first cell
			}

			if (dimensionY == 0f) {
				y_inc = 0;
				t_next_y = dimFracY; // don't increment vertically because the Vec3d is perfectly horizontal
			} else if (target.y > start.y) {
				y_inc = 1; // target point is vertically greater than starting point so increment every
				// step by 1
				intersectCount += MathUtilities.floor(target.y) - y; // increment total amount of intersecting cells
				t_next_y = (float) ((y + 1 - start.y) * dimFracY); // calculate the next vertical
				// intersection
				// point based on the position inside
				// the first cell
			} else {
				y_inc = -1; // target point is vertically smaller than starting point so reduce every step
				// by 1
				intersectCount += y - MathUtilities.floor(target.y); // increment total amount of intersecting cells
				t_next_y = (float) ((start.y - y) * dimFracY); // calculate the next vertical intersection
				// point
				// based on the position inside
				// the first cell
			}

			if (dimensionZ == 0f) {
				z_inc = 0;
				t_next_z = dimFracZ; // don't increment vertically because the Vec3d is perfectly horizontal
			} else if (target.z > start.z) {
				z_inc = 1; // target point is vertically greater than starting point so increment every
				// step by 1
				intersectCount += MathUtilities.floor(target.z) - z; // increment total amount of intersecting cells
				t_next_z = (float) ((z + 1 - start.z) * dimFracZ); // calculate the next vertical
				// intersection
				// point based on the position inside
				// the first cell
			} else {
				z_inc = -1; // target point is vertically smaller than starting point so reduce every step
				// by 1
				intersectCount += z - MathUtilities.floor(target.z); // increment total amount of intersecting cells
				t_next_z = (float) ((start.z - z) * dimFracZ); // calculate the next vertical intersection
				// point
				// based on the position inside
				// the first cell
			}

			boolean finished = stepRay(start, x, y, z, dimFracX, dimFracY, dimFracZ, intersectCount, x_inc, y_inc,
					z_inc, t_next_y, t_next_x, t_next_z);
			provider.cleanup();
			if (finished) {
				cacheResult(targets[0], true);
				return true;
			} else {
				allowRayChecks = true;
			}
		}
		cacheResult(targets[0], false);
		return false;
	}

	/**
	 * @param viewerPosition
	 * @param position
	 * @param faceData       contains rather this Block is on the outside for a
	 *                       given face
	 * @param visibleOnFace  contains rather a face should be concidered
	 * @return
	 */
	private boolean isVoxelVisible(Vec3d viewerPosition, Vec3d position, byte faceData, byte visibleOnFace) {
		int targetSize = 0;
		Arrays.fill(dotselectors, false);
		if ((visibleOnFace & ON_MIN_X) == ON_MIN_X) {
			dotselectors[0] = true;
			if ((faceData & ~ON_MIN_X) != 0) {
				dotselectors[1] = true;
				dotselectors[4] = true;
				dotselectors[5] = true;
			}
			dotselectors[8] = true;
		}
		if ((visibleOnFace & ON_MIN_Y) == ON_MIN_Y) {
			dotselectors[0] = true;
			if ((faceData & ~ON_MIN_Y) != 0) {
				dotselectors[3] = true;
				dotselectors[4] = true;
				dotselectors[7] = true;
			}
			dotselectors[9] = true;
		}
		if ((visibleOnFace & ON_MIN_Z) == ON_MIN_Z) {
			dotselectors[0] = true;
			if ((faceData & ~ON_MIN_Z) != 0) {
				dotselectors[1] = true;
				dotselectors[4] = true;
				dotselectors[5] = true;
			}
			dotselectors[10] = true;
		}
		if ((visibleOnFace & ON_MAX_X) == ON_MAX_X) {
			dotselectors[4] = true;
			if ((faceData & ~ON_MAX_X) != 0) {
				dotselectors[5] = true;
				dotselectors[6] = true;
				dotselectors[7] = true;
			}
			dotselectors[11] = true;
		}
		if ((visibleOnFace & ON_MAX_Y) == ON_MAX_Y) {
			dotselectors[1] = true;
			if ((faceData & ~ON_MAX_Y) != 0) {
				dotselectors[2] = true;
				dotselectors[5] = true;
				dotselectors[6] = true;
			}
			dotselectors[12] = true;
		}
		if ((visibleOnFace & ON_MAX_Z) == ON_MAX_Z) {
			dotselectors[2] = true;
			if ((faceData & ~ON_MAX_Z) != 0) {
				dotselectors[3] = true;
				dotselectors[6] = true;
				dotselectors[7] = true;
			}
			dotselectors[13] = true;
		}

		if (dotselectors[0])
			targetPoints[targetSize++].setAdd(position, 0.05, 0.05, 0.05);
		if (dotselectors[1])
			targetPoints[targetSize++].setAdd(position, 0.05, 0.95, 0.05);
		if (dotselectors[2])
			targetPoints[targetSize++].setAdd(position, 0.05, 0.95, 0.95);
		if (dotselectors[3])
			targetPoints[targetSize++].setAdd(position, 0.05, 0.05, 0.95);
		if (dotselectors[4])
			targetPoints[targetSize++].setAdd(position, 0.95, 0.05, 0.05);
		if (dotselectors[5])
			targetPoints[targetSize++].setAdd(position, 0.95, 0.95, 0.05);
		if (dotselectors[6])
			targetPoints[targetSize++].setAdd(position, 0.95, 0.95, 0.95);
		if (dotselectors[7])
			targetPoints[targetSize++].setAdd(position, 0.95, 0.05, 0.95);
		// middle points
		if (dotselectors[8])
			targetPoints[targetSize++].setAdd(position, 0.05, 0.5, 0.5);
		if (dotselectors[9])
			targetPoints[targetSize++].setAdd(position, 0.5, 0.05, 0.5);
		if (dotselectors[10])
			targetPoints[targetSize++].setAdd(position, 0.5, 0.5, 0.05);
		if (dotselectors[11])
			targetPoints[targetSize++].setAdd(position, 0.95, 0.5, 0.5);
		if (dotselectors[12])
			targetPoints[targetSize++].setAdd(position, 0.5, 0.95, 0.5);
		if (dotselectors[13])
			targetPoints[targetSize++].setAdd(position, 0.5, 0.5, 0.95);

		return isVisible(viewerPosition, targetPoints, targetSize);
	}

	private boolean rayIntersection(int[] b, Vec3d rayOrigin, Vec3d rayDir) {
		Vec3d rInv = new Vec3d(1, 1, 1).div(rayDir);

		double t1 = (b[0] - rayOrigin.x) * rInv.x;
		double t2 = (b[0] + 1 - rayOrigin.x) * rInv.x;
		double t3 = (b[1] - rayOrigin.y) * rInv.y;
		double t4 = (b[1] + 1 - rayOrigin.y) * rInv.y;
		double t5 = (b[2] - rayOrigin.z) * rInv.z;
		double t6 = (b[2] + 1 - rayOrigin.z) * rInv.z;

		double tmin = Math.max(Math.max(Math.min(t1, t2), Math.min(t3, t4)), Math.min(t5, t6));
		double tmax = Math.min(Math.min(Math.max(t1, t2), Math.max(t3, t4)), Math.max(t5, t6));

		// if tmax > 0, ray (line) is intersecting AABB, but the whole AABB is behind us
		if (tmax > 0) {
			return false;
		}

		// if tmin > tmax, ray doesn't intersect AABB
		if (tmin > tmax) {
			return false;
		}

		return true;
	}

	public void resetCache() {
		this.cache.resetCache();
	}

	private boolean stepRay(Vec3d start, int currentX, int currentY, int currentZ, double distInX, double distInY,
			double distInZ, int n, int x_inc, int y_inc, int z_inc, double t_next_y, double t_next_x, double t_next_z) {
		allowWallClipping = true; // initially allow rays to go through walls till they are on the outside
		// iterate through all intersecting cells (n times)
		for (; n > 1; n--) { // n-1 times because we don't want to check the last block
			// towards - where from

			// get cached value, 0 means uncached (default)
			int cVal = getCacheValue(currentX, currentY, currentZ);

			if (cVal == 2 && !allowWallClipping) {
				// block cached as occluding, stop ray
				lastHitBlock[0] = currentX;
				lastHitBlock[1] = currentY;
				lastHitBlock[2] = currentZ;
				return false;
			}

			if (cVal == 0) {
				// save current cell
				int chunkX = currentX >> 4;
				int chunkZ = currentZ >> 4;
				if (!provider.prepareChunk(chunkX, chunkZ)) { // Chunk not ready
					return false;
				}

				if (provider.isOpaqueFullCube(currentX, currentY, currentZ)) {
					if (!allowWallClipping) {
						cache.setLastHidden();
						lastHitBlock[0] = currentX;
						lastHitBlock[1] = currentY;
						lastHitBlock[2] = currentZ;
						return false;
					}
				} else {
					// outside of wall, now clipping is not allowed
					allowWallClipping = false;
					cache.setLastVisible();
				}
			}

			if (cVal == 1) {
				// outside of wall, now clipping is not allowed
				allowWallClipping = false;
			}

			if (t_next_y < t_next_x && t_next_y < t_next_z) { // next cell is upwards/downwards because the distance to
				// the next vertical
				// intersection point is smaller than to the next horizontal intersection point
				currentY += y_inc; // move up/down
				t_next_y += distInY; // update next vertical intersection point
			} else if (t_next_x < t_next_y && t_next_x < t_next_z) { // next cell is right/left
				currentX += x_inc; // move right/left
				t_next_x += distInX; // update next horizontal intersection point
			} else {
				currentZ += z_inc; // move right/left
				t_next_z += distInZ; // update next horizontal intersection point
			}

		}
		return true;
	}

}
