package dev.redstudio.alfheim.lighting;

import dev.redstudio.alfheim.utils.DeduplicatedLongQueue;
import dev.redstudio.redcore.math.ClampUtil;
import net.minecraft.block.state.IBlockState;
import net.minecraft.util.BlockPos;
import net.minecraft.util.EnumFacing;
import net.minecraft.util.Vec3i;
import net.minecraft.world.EnumSkyBlock;
import net.minecraft.world.World;
import net.minecraft.world.chunk.Chunk;
import net.minecraft.world.chunk.storage.ExtendedBlockStorage;

/**
 * Modified by lax1dude not to abuse interfaces
 *
 * @author Luna Lage (Desoroxxx)
 * @author kappa-maintainer
 * @author embeddedt
 * @author Angeline (@jellysquid)
 * @since 1.0
 */
public final class LightingEngine {

	private static final class NeighborInfo {

		public final BlockPos mutableBlockPos = new BlockPos();

		public Chunk chunk;

		public byte light;

		public long key;
	}

	private static final byte MAX_LIGHT_LEVEL = 15;

	// Layout parameters
	// Length of bit segments
	private static final int L_X = 26, L_Y = 8, L_Z = 26, L_L = 4;

	// Bit segment shifts/positions
	private static final int S_Z = 0, S_X = S_Z + L_Z, S_Y = S_X + L_X, S_L = S_Y + L_Y;
	// Bit segment masks
	private static final long M_X = (1L << L_X) - 1, M_Y = (1L << L_Y) - 1, M_Z = (1L << L_Z) - 1,
			M_L = (1L << L_L) - 1, M_POS = (M_Y << S_Y) | (M_X << S_X) | (M_Z << S_Z);

	// Bit to check whether y had overflow
	private static final long Y_CHECK = 1L << (S_Y + L_Y);
	private static final long[] neighborShifts = new long[6];

	static {
		for (byte i = 0; i < 6; ++i) {
			final Vec3i offset = EnumFacing._VALUES[i].getDirectionVec();
			neighborShifts[i] = ((long) offset.getY() << S_Y) | ((long) offset.getX() << S_X)
					| ((long) offset.getZ() << S_Z);
		}
	}

	// Mask to extract chunk identifier
	private static final long M_CHUNK = ((M_X >> 4) << (4 + S_X)) | ((M_Z >> 4) << (4 + S_Z));

	private static BlockPos decodeWorldCoord(final BlockPos mutableBlockPos, final long longPos) {
		return mutableBlockPos.func_181079_c((int) (longPos >> S_X & M_X) - (1 << L_X - 1),
				(int) (longPos >> S_Y & M_Y), (int) (longPos >> S_Z & M_Z) - (1 << L_Z - 1));
	}

	private static long encodeWorldCoord(final BlockPos pos) {
		return ((long) pos.getY() << S_Y) | ((long) pos.getX() + (1 << L_X - 1) << S_X)
				| ((long) pos.getZ() + (1 << L_Z - 1) << S_Z);
	}

	private static byte getCachedLightFor(final Chunk chunk, final ExtendedBlockStorage storage,
			final BlockPos blockPos, final EnumSkyBlock type) {
		final int x = blockPos.getX() & 15;
		final int y = blockPos.getY();
		final int z = blockPos.getZ() & 15;

		if (storage == null)
			return type == EnumSkyBlock.SKY && chunk.canSeeSky(blockPos) ? (byte) type.defaultLightValue : 0;
		else if (type == EnumSkyBlock.SKY)
			return chunk.getWorld().provider.getHasNoSky() ? 0 : (byte) storage.getExtSkylightValue(x, y & 15, z);
		else
			return type == EnumSkyBlock.BLOCK ? (byte) storage.getExtBlocklightValue(x, y & 15, z)
					: (byte) type.defaultLightValue;
	}

	private final World world;

	// Layout of longs: [padding(4)] [y(8)] [x(26)] [z(26)]
	private final DeduplicatedLongQueue[] lightUpdateQueues = new DeduplicatedLongQueue[EnumSkyBlock.values().length];

	// Layout of longs: see above
	private final DeduplicatedLongQueue[] darkeningQueues = new DeduplicatedLongQueue[MAX_LIGHT_LEVEL + 1];

	private final DeduplicatedLongQueue[] brighteningQueues = new DeduplicatedLongQueue[MAX_LIGHT_LEVEL + 1];
	// Layout of longs: [newLight(4)] [pos(60)]
	private final DeduplicatedLongQueue initialBrightenings;
	// Layout of longs: [padding(4)] [pos(60)]
	private final DeduplicatedLongQueue initialDarkenings;
	private boolean updating = false;

	// Iteration state data
	// Cache position to avoid allocation of new object each time
	private final BlockPos currentPos = new BlockPos();

	private Chunk currentChunk;
	private long currentChunkIdentifier;

	private long currentData;

	// Cached data about neighboring blocks (of tempPos)
	private boolean isNeighborDataValid = false;

	private final NeighborInfo[] neighborInfos = new NeighborInfo[6];

	private DeduplicatedLongQueue currentQueue;

	public LightingEngine(final World world) {
		this.world = world;

		initialBrightenings = new DeduplicatedLongQueue(16384);
		initialDarkenings = new DeduplicatedLongQueue(16384);

		for (int i = 0; i < EnumSkyBlock.values().length; ++i)
			lightUpdateQueues[i] = new DeduplicatedLongQueue(16384);

		for (int i = 0; i < darkeningQueues.length; ++i)
			darkeningQueues[i] = new DeduplicatedLongQueue(16384);

		for (int i = 0; i < brighteningQueues.length; ++i)
			brighteningQueues[i] = new DeduplicatedLongQueue(16384);

		for (int i = 0; i < neighborInfos.length; ++i)
			neighborInfos[i] = new NeighborInfo();
	}

	private byte calculateNewLightFromCursor(final byte luminosity, final byte opacity, final EnumSkyBlock lightType) {
		if (luminosity >= MAX_LIGHT_LEVEL - opacity)
			return luminosity;

		byte newLight = luminosity;

		fetchNeighborDataFromCursor(lightType);

		for (final NeighborInfo neighborInfo : neighborInfos) {
			if (neighborInfo.chunk == null)
				continue;

			newLight = (byte) Math.max(neighborInfo.light - opacity, newLight);
		}

		return newLight;
	}

	private byte calculateNewLightFromCursor(final EnumSkyBlock lightType) {
		final IBlockState blockState = currentChunk.getBlockState(currentPos);

		final byte luminosity = getCursorLuminosity(blockState, lightType);
		final byte opacity;

		if (luminosity >= MAX_LIGHT_LEVEL - 1)
			opacity = 1;
		else
			opacity = getPosOpacity(currentPos, blockState);

		return calculateNewLightFromCursor(luminosity, opacity, lightType);
	}

	/**
	 * Enqueues the blockPos for brightening and sets its light value to newLight
	 */
	private void enqueueBrightening(final BlockPos blockPos, final long longPos, final byte newLight, final Chunk chunk,
			final EnumSkyBlock lightType) {
		brighteningQueues[newLight].enqueue(longPos);

		chunk.setLightFor(lightType, blockPos, newLight);
	}

	private void enqueueBrighteningFromCursor(final byte newLight, final EnumSkyBlock lightType) {
		enqueueBrightening(currentPos, currentData, newLight, currentChunk, lightType);
	}

	/**
	 * Enqueues the blockPos for darkening and sets its light value to 0
	 */
	private void enqueueDarkening(final BlockPos blockPos, final long longPos, final byte oldLight, final Chunk chunk,
			final EnumSkyBlock lightType) {
		darkeningQueues[oldLight].enqueue(longPos);

		chunk.setLightFor(lightType, blockPos, 0);
	}

	/**
	 * Gets data for neighbors of {@link #currentPos} and saves the results into
	 * neighbor state data members. If a neighbor can't be accessed/doesn't exist,
	 * the corresponding entry in neighborChunks is null - others are not reset
	 */
	private void fetchNeighborDataFromCursor(final EnumSkyBlock lightType) {
		// Only update if curPos was changed
		if (isNeighborDataValid)
			return;

		isNeighborDataValid = true;

		for (int i = 0; i < neighborInfos.length; ++i) {
			final NeighborInfo neighborInfo = neighborInfos[i];
			final long neighborLongPos = neighborInfo.key = currentData + neighborShifts[i];

			if ((neighborLongPos & Y_CHECK) != 0) {
				neighborInfo.chunk = null;
				continue;
			}

			final BlockPos neighborPos = decodeWorldCoord(neighborInfo.mutableBlockPos, neighborLongPos);

			final Chunk neighborChunk;

			if ((neighborLongPos & M_CHUNK) == currentChunkIdentifier)
				neighborChunk = neighborInfo.chunk = currentChunk;
			else
				neighborChunk = neighborInfo.chunk = getChunk(neighborPos);

			if (neighborChunk != null) {
				final ExtendedBlockStorage neighborSection = neighborChunk
						.getBlockStorageArray()[neighborPos.getY() >> 4];

				neighborInfo.light = getCachedLightFor(neighborChunk, neighborSection, neighborPos, lightType);
			}
		}
	}

	private Chunk getChunk(final BlockPos blockPos) {
		return world.getChunkProvider().getLoadedChunk(blockPos.getX() >> 4, blockPos.getZ() >> 4);
	}

	private byte getCursorCachedLight(final EnumSkyBlock lightType) {
		return currentChunk.alfheim$getCachedLightFor(lightType, currentPos);
	}

	/**
	 * Calculates the luminosity for {@link #currentPos}, taking into account the
	 * light type
	 */
	private byte getCursorLuminosity(final IBlockState state, final EnumSkyBlock lightType) {
		if (lightType == EnumSkyBlock.SKY) {
			if (currentChunk.canSeeSky(currentPos))
				return (byte) EnumSkyBlock.SKY.defaultLightValue;
			else
				return 0;
		}

		return (byte) ClampUtil.clampMinFirst(state.getBlock().getLightValue(), 0, MAX_LIGHT_LEVEL);
	}

	private byte getPosOpacity(final BlockPos blockPos, final IBlockState blockState) {
		return (byte) ClampUtil.clampMinFirst(blockState.getBlock().getLightOpacity(), 1, MAX_LIGHT_LEVEL);
	}

	/**
	 * Polls a new item from {@link #currentQueue} and fills in state data members
	 *
	 * @return If there was an item to poll
	 */
	private boolean nextItem() {
		if (currentQueue.isEmpty()) {
			currentQueue = null;

			return false;
		}

		currentData = currentQueue.dequeue();
		isNeighborDataValid = false;

		decodeWorldCoord(currentPos, currentData);

		final long chunkIdentifier = currentData & M_CHUNK;

		if (currentChunkIdentifier != chunkIdentifier) {
			currentChunk = getChunk(currentPos);
			currentChunkIdentifier = chunkIdentifier;
		}

		return true;
	}

	/**
	 * Calls {@link LightingEngine#processLightUpdatesForType(EnumSkyBlock)} for
	 * both light types
	 */
	public void processLightUpdates() {
		processLightUpdatesForType(EnumSkyBlock.SKY);
		processLightUpdatesForType(EnumSkyBlock.BLOCK);
	}

	/**
	 * Processes light updates of the given light type
	 */
	public void processLightUpdatesForType(final EnumSkyBlock lightType) {
		final DeduplicatedLongQueue queue = lightUpdateQueues[lightType.ordinal()];

		// Quickly check if the queue is empty before we acquire a more expensive lock.
		if (queue.isEmpty())
			return;

		processLightUpdatesForTypeInner(lightType, queue);
	}

	private void processLightUpdatesForTypeInner(final EnumSkyBlock lightType, final DeduplicatedLongQueue queue) {
		// Avoid nested calls
		if (updating)
			throw new IllegalStateException("Already processing updates!");

		updating = true;

		currentChunkIdentifier = -1; // Reset chunk cache

		currentQueue = queue;

		if (currentQueue != null)
			currentQueue.newDeduplicationSet();

		// Process the queued updates and enqueue them for further processing
		while (nextItem()) {
			if (currentChunk == null)
				continue;

			final byte oldLight = getCursorCachedLight(lightType);
			final byte newLight = calculateNewLightFromCursor(lightType);

			if (oldLight < newLight)
				initialBrightenings.enqueue(((long) newLight << S_L) | currentData); // Don't enqueue directly for
																						// brightening to avoid
																						// duplicate scheduling
			else if (oldLight > newLight)
				initialDarkenings.enqueue(currentData); // Don't enqueue directly for darkening to avoid duplicate
														// scheduling
		}

		currentQueue = initialBrightenings;

		if (currentQueue != null)
			currentQueue.newDeduplicationSet();

		while (nextItem()) {
			final byte newLight = (byte) (currentData >> S_L & M_L);

			if (newLight > getCursorCachedLight(lightType))
				enqueueBrightening(currentPos, currentData & M_POS, newLight, currentChunk, lightType); // Sets the
																										// light to
																										// newLight to
																										// only schedule
																										// once. Clear
																										// leading bits
																										// of curData
																										// for later
		}

		currentQueue = initialDarkenings;

		if (currentQueue != null)
			currentQueue.newDeduplicationSet();

		while (nextItem()) {
			final byte oldLight = getCursorCachedLight(lightType);

			if (oldLight != 0)
				enqueueDarkening(currentPos, currentData, oldLight, currentChunk, lightType); // Sets the light to zero
																								// to only schedule once
		}

		// Iterate through enqueued updates (brightening and darkening in parallel) from
		// brightest to darkest so that we only need to iterate once
		for (byte currentLight = MAX_LIGHT_LEVEL; currentLight >= 0; --currentLight) {
			currentQueue = darkeningQueues[currentLight];

			if (currentQueue != null)
				currentQueue.newDeduplicationSet();

			while (nextItem()) {
				// Don't darken if we got brighter due to some other change
				if (getCursorCachedLight(lightType) >= currentLight)
					continue;

				final IBlockState blockState = currentChunk.getBlockState(currentPos);
				final byte luminosity = getCursorLuminosity(blockState, lightType);
				final byte opacity; // If luminosity is high enough, opacity is irrelevant

				if (luminosity >= MAX_LIGHT_LEVEL - 1)
					opacity = 1;
				else
					opacity = getPosOpacity(currentPos, blockState);

				// Only darken neighbors if we indeed became darker
				if (calculateNewLightFromCursor(luminosity, opacity, lightType) < currentLight) {
					// Need to calculate new light value from neighbors IGNORING neighbors which are
					// scheduled for darkening
					byte newLight = luminosity;

					fetchNeighborDataFromCursor(lightType);

					for (final NeighborInfo neighborInfo : neighborInfos) {
						final Chunk neighborChunk = neighborInfo.chunk;

						if (neighborChunk == null)
							continue;

						final byte neighborLight = neighborInfo.light;

						if (neighborLight == 0)
							continue;

						final BlockPos neighborPos = neighborInfo.mutableBlockPos;

						if (currentLight - getPosOpacity(neighborPos, neighborChunk
								.getBlockState(neighborPos)) >= neighborLight) /*
																				 * Schedule neighbor for darkening if we
																				 * possibly light it
																				 */ {
							enqueueDarkening(neighborPos, neighborInfo.key, neighborLight, neighborChunk, lightType);
						} else /* Only use for new light calculation if not */ {
							// If we can't darken the neighbor, no one else can (because of processing
							// order) -> safe to let us be illuminated by it
							newLight = (byte) Math.max(newLight, neighborLight - opacity);
						}
					}

					// Schedule brightening since light level was set to 0
					enqueueBrighteningFromCursor(newLight, lightType);
				} else /*
						 * We didn't become darker, so we need to re-set our initial light value (was
						 * set to zero) and notify neighbors
						 */ {
					enqueueBrighteningFromCursor(currentLight, lightType); // Do not spread to neighbors immediately to
																			// avoid scheduling multiple times
				}
			}

			currentQueue = brighteningQueues[currentLight];

			if (currentQueue != null)
				currentQueue.newDeduplicationSet();

			while (nextItem()) {
				final byte oldLight = getCursorCachedLight(lightType);

				// Only process this if nothing else has happened at this position since
				// scheduling
				if (oldLight == currentLight) {
					world.notifyLightSet(currentPos);

					if (currentLight > 1)
						spreadLightFromCursor(currentLight, lightType);
				}
			}
		}

		updating = false;
	}

	/**
	 * Schedules a light update for the specified light type and position to be
	 * processed later by
	 * {@link LightingEngine#processLightUpdatesForType(EnumSkyBlock)}
	 */
	public void scheduleLightUpdate(final EnumSkyBlock lightType, final BlockPos pos) {
		scheduleLightUpdate(lightType, encodeWorldCoord(pos));
	}

	/**
	 * Schedules a light update for the specified light type and position to be
	 * processed later by {@link LightingEngine#processLightUpdates()}
	 */
	private void scheduleLightUpdate(final EnumSkyBlock lightType, final long blockPos) {
		lightUpdateQueues[lightType.ordinal()].enqueue(blockPos);
	}

	private void spreadLightFromCursor(final byte currentLight, final EnumSkyBlock lightType) {
		fetchNeighborDataFromCursor(lightType);

		for (final NeighborInfo neighborInfo : neighborInfos) {
			final Chunk neighborChunk = neighborInfo.chunk;

			if (neighborChunk == null || currentLight < neighborInfo.light)
				continue;

			final BlockPos neighborBlockPos = neighborInfo.mutableBlockPos;

			final byte newLight = (byte) (currentLight
					- getPosOpacity(neighborBlockPos, neighborChunk.getBlockState(neighborBlockPos)));

			if (newLight > neighborInfo.light)
				enqueueBrightening(neighborBlockPos, neighborInfo.key, newLight, neighborChunk, lightType);
		}
	}
}
