package common.world;

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.function.Predicate;

import common.biome.BaseBiome;
import common.block.Block;
import common.block.ITileEntityProvider;
import common.collect.Maps;
import common.entity.Entity;
import common.init.BlockRegistry;
import common.init.Blocks;
import common.log.Log;
import common.material.Material;
import common.rng.Random;
import common.tileentity.TileEntity;
import common.util.BlockPos;
import common.util.BoundingBox;
import common.util.ChunkPos;
import common.util.ClassInheritanceMultiMap;
import common.util.ExtMath;
import common.util.Facing;
import common.util.NibbleArray;
import common.worldgen.BiomeGenerator;
import common.worldgen.DebugStates;

public class Chunk {
	public final int xPos;
	public final int zPos;
	private final World world;
	private final BlockArray[] blocks = new BlockArray[32];
	private final byte[] biomes = new byte[256];
	private final int[] precHeight = new int[256];
	private final boolean[] updateSky = new boolean[256];
	private final int[] height = new int[256];
	private final Map<BlockPos, TileEntity> tiles = Maps.<BlockPos, TileEntity>newHashMap();
	private final ClassInheritanceMultiMap<Entity>[] entities = new ClassInheritanceMultiMap[32];
	private final ConcurrentLinkedQueue<BlockPos> tileQueue = new ConcurrentLinkedQueue<BlockPos>();

	private boolean loaded;
	private boolean gapUpdate;
	private boolean populated;
	private boolean lightInit;
	private boolean updated;
	private boolean modified;
	private boolean hasEntity;
	private int minHeight;
	private int lightChecks = 8192;
	private long lastSave;
	private long inhabited;

	public Chunk(World world, int x, int z) {
		this.world = world;
		this.xPos = x;
		this.zPos = z;
		for(int y = 0; y < this.entities.length; ++y) {
			this.entities[y] = new ClassInheritanceMultiMap(Entity.class);
		}
		Arrays.fill(this.precHeight, -999);
		Arrays.fill(this.biomes, (byte)-1);
	}

	public Chunk(World world, short[] data, int height, State base, State ceil, Random rand, BaseBiome[] biomes, int x, int z) {
		this(world, x, z);
		boolean sky = !world.dimension.hasNoLight();
		for(int bx = 0; bx < 16; ++bx) {
			for(int bz = 0; bz < 16; ++bz) {
				for(int by = 0; by < height; ++by) {
					State state = BlockRegistry.STATEMAP.getByValue(data[bx << 4 | bz | by << 8]);
					if(state != null && state.getBlock().getMaterial() != Material.air) {
						int y = by >> 4;
						if(this.blocks[y] == null)
							this.blocks[y] = new BlockArray(y << 4, sky);
						this.blocks[y].set(bx, by & 15, bz, state);
					}
				}
			}
		}
		if(base != null) {
			if(this.blocks[0] == null)
				this.blocks[0] = new BlockArray(0, sky);
			for(int bx = 0; bx < 16; ++bx) {
				for(int bz = 0; bz < 16; ++bz) {
					for(int by = 0; by < 5; ++by) {
						if(by <= rand.zrange(5))
							this.blocks[0].set(bx, by, bz, base);
					}
				}
			}
		}
		if(ceil != null) {
			int y = (height - 1) >> 4;
			if(this.blocks[y] == null)
				this.blocks[y] = new BlockArray(y << 4, sky);
			y = (height - 5) >> 4;
			if(this.blocks[y] == null)
				this.blocks[y] = new BlockArray(y << 4, sky);
			for(int bx = 0; bx < 16; ++bx) {
				for(int bz = 0; bz < 16; ++bz) {
					for(int by = height - 1; by >= height - 5; --by) {
						if(by >= (height - 1) - rand.zrange(5))
							this.blocks[by >> 4].set(bx, by & 15, bz, ceil);
					}
				}
			}
		}
		for(int n = 0; n < this.biomes.length; ++n) {
			this.biomes[n] = (byte)biomes[n].id;
		}
		if(ceil == null)
			this.genSkyLight();
		else
			this.resetRelight();
	}

	public int getHeight(BlockPos pos) {
		return this.getHeight(pos.getX() & 15, pos.getZ() & 15);
	}

	public int getHeight(int x, int z) {
		return this.height[z << 4 | x];
	}

	public int getTopSegment() {
		for(int y = this.blocks.length - 1; y >= 0; --y) {
			if(this.blocks[y] != null) {
				return this.blocks[y].getY();
			}
		}

		return 0;
	}

	public BlockArray[] getStorage() {
		return this.blocks;
	}

	protected void genHeights() {
		int h = this.getTopSegment();
		this.minHeight = Integer.MAX_VALUE;

		for(int x = 0; x < 16; ++x) {
			for(int z = 0; z < 16; ++z) {
				this.precHeight[x + (z << 4)] = -999;

				for(int y = h + 16; y > 0; --y) {
					Block block = this.getBlock0(x, y - 1, z);

					if(block.getLightOpacity() != 0) {
						this.height[z << 4 | x] = y;

						if(y < this.minHeight) {
							this.minHeight = y;
						}

						break;
					}
				}
			}
		}

		this.modified = true;
	}

	public void genSkyLight() {
		int h = this.getTopSegment();
		this.minHeight = Integer.MAX_VALUE;

		for(int x = 0; x < 16; ++x) {
			for(int z = 0; z < 16; ++z) {
				this.precHeight[x + (z << 4)] = -999;

				for(int y = h + 16; y > 0; --y) {
					if(this.getOpacity(x, y - 1, z) != 0) {
						this.height[z << 4 | x] = y;

						if(y < this.minHeight) {
							this.minHeight = y;
						}

						break;
					}
				}

				if(!this.world.dimension.hasNoLight()) {
					int l = 15;
					int y = h + 16 - 1;

					while(true) {
						int b = this.getOpacity(x, y, z);

						if(b == 0 && l != 15) {
							b = 1;
						}

						l -= b;

						if(l > 0) {
							BlockArray stor = this.blocks[y >> 4];

							if(stor != null) {
								stor.setSky(x, y & 15, z, l);
								this.world.notifyLightSet(new BlockPos((this.xPos << 4) + x, y, (this.zPos << 4) + z));
							}
						}

						--y;

						if(y <= 0 || l <= 0) {
							break;
						}
					}
				}
			}
		}

		this.modified = true;
	}

	private void propagateOcclusion(int x, int z) {
		this.updateSky[x + z * 16] = true;
		this.gapUpdate = true;
	}

	private void recheckGaps(boolean single) {
//		this.world.profiler.start("recheckGaps");

		if(this.world.isAreaLoaded(new BlockPos(this.xPos * 16 + 8, 0, this.zPos * 16 + 8), 16)) {
			for(int x = 0; x < 16; ++x) {
				for(int z = 0; z < 16; ++z) {
					if(this.updateSky[x + z * 16]) {
						this.updateSky[x + z * 16] = false;
						int h = this.getHeight(x, z);
						int cx = this.xPos * 16 + x;
						int cz = this.zPos * 16 + z;
						int max = Integer.MAX_VALUE;

						for(Facing face : Facing.Plane.HORIZONTAL) {
							max = Math.min(max, this.world.getChunksLowestHorizon(cx + face.getFrontOffsetX(), 
									cz + face.getFrontOffsetZ()));
						}

						this.checkNeighbor(cx, cz, max);

						for(Facing face : Facing.Plane.HORIZONTAL) {
							this.checkNeighbor(cx + face.getFrontOffsetX(), cz + face.getFrontOffsetZ(), h);
						}

						if(single) {
//							this.world.profiler.end();
							return;
						}
					}
				}
			}

			this.gapUpdate = false;
		}

//		this.world.profiler.end();
	}

	private void checkNeighbor(int x, int z, int max) {
		int h = this.world.getHeight(new BlockPos(x, 0, z)).getY();

		if(h > max) {
			this.updateNeighbor(x, z, max, h + 1);
		}
		else if(h < max) {
			this.updateNeighbor(x, z, h, max + 1);
		}
	}

	private void updateNeighbor(int x, int z, int bottom, int top) {
		if(top > bottom && this.world.isAreaLoaded(new BlockPos(x, 0, z), 16)) {
			for(int y = bottom; y < top; ++y) {
				this.world.checkLightFor(LightType.SKY, new BlockPos(x, y, z));
			}

			this.modified = true;
		}
	}

	private void relightBlock(int x, int y, int z) {
		int h = this.height[z << 4 | x] & 511;
		int cy = h;

		if(y > h) {
			cy = y;
		}

		while(cy > 0 && this.getOpacity(x, cy - 1, z) == 0) {
			--cy;
		}

		if(cy != h) {
			this.world.markBlocksDirtyVertical(x + this.xPos * 16, z + this.zPos * 16, cy, h);
			this.height[z << 4 | x] = cy;
			int cx = this.xPos * 16 + x;
			int cz = this.zPos * 16 + z;

			if(!this.world.dimension.hasNoLight()) {
				if(cy < h) {
					for(int n = cy; n < h; ++n) {
						BlockArray stor = this.blocks[n >> 4];

						if(stor != null) {
							stor.setSky(x, n & 15, z, 15);
							this.world.notifyLightSet(new BlockPos((this.xPos << 4) + x, n, (this.zPos << 4) + z));
						}
					}
				}
				else {
					for(int n = h; n < cy; ++n) {
						BlockArray stor = this.blocks[n >> 4];

						if(stor != null) {
							stor.setSky(x, n & 15, z, 0);
							this.world.notifyLightSet(new BlockPos((this.xPos << 4) + x, n, (this.zPos << 4) + z));
						}
					}
				}

				int l = 15;

				while(cy > 0 && l > 0) {
					--cy;
					int b = this.getOpacity(x, cy, z);

					if(b == 0) {
						b = 1;
					}

					l -= b;

					if(l < 0) {
						l = 0;
					}

					BlockArray stor = this.blocks[cy >> 4];

					if(stor != null) {
						stor.setSky(x, cy & 15, z, l);
					}
				}
			}

			int sh = this.height[z << 4 | x];
			int sy = h;
			int ey = sh;

			if(sh < h) {
				sy = sh;
				ey = h;
			}

			if(sh < this.minHeight) {
				this.minHeight = sh;
			}

			if(!this.world.dimension.hasNoLight()) {
				for(Facing face : Facing.Plane.HORIZONTAL) {
					this.updateNeighbor(cx + face.getFrontOffsetX(), cz + face.getFrontOffsetZ(), sy, ey);
				}

				this.updateNeighbor(cx, cz, sy, ey);
			}

			this.modified = true;
		}
	}

	public int getOpacity(BlockPos pos) {
		return this.getBlock(pos).getLightOpacity();
	}

	private int getOpacity(int x, int y, int z) {
		return this.getBlock0(x, y, z).getLightOpacity();
	}

	private Block getBlock0(int x, int y, int z) {
		Block block = Blocks.air;

		if(y >= 0 && y >> 4 < this.blocks.length) {
			BlockArray stor = this.blocks[y >> 4];

			if(stor != null) {
				block = stor.getBlock(x, y & 15, z);
			}
		}

		return block;
	}

	public Block getBlock(int x, int y, int z) {
		return this.getBlock0(x & 15, y, z & 15);
	}

	public Block getBlock(BlockPos pos) {
		return this.getBlock0(pos.getX() & 15, pos.getY(), pos.getZ() & 15);
	}

	public State getState(BlockPos pos) {
		if(this.world.debug) {
			State state = null;

//			if(pos.getY() == 60) {
//				state = Blocks.glass.getDefaultState();
//			}

			if(pos.getY() == 1) {
				state = DebugStates.getState(pos.getX(), pos.getZ());
			}

			return state == null ? Blocks.air.getState() : state;
		}
		else {
			if(pos.getY() >= 0 && pos.getY() >> 4 < this.blocks.length) {
				BlockArray stor = this.blocks[pos.getY() >> 4];

				if(stor != null) {
					int x = pos.getX() & 15;
					int y = pos.getY() & 15;
					int z = pos.getZ() & 15;
					return stor.get(x, y, z);
				}
			}

			return Blocks.air.getState();
		}
	}

	private int getMeta(int x, int y, int z) {
		if(y >> 4 >= this.blocks.length) {
			return 0;
		}
		else {
			BlockArray stor = this.blocks[y >> 4];
			return stor != null ? stor.getMeta(x, y & 15, z) : 0;
		}
	}

	public int getMeta(BlockPos pos) {
		return this.getMeta(pos.getX() & 15, pos.getY(), pos.getZ() & 15);
	}

	public State setState(BlockPos pos, State state) {
		int x = pos.getX() & 15;
		int y = pos.getY();
		int z = pos.getZ() & 15;
		int o = z << 4 | x;

		if(y >= this.precHeight[o] - 1) {
			this.precHeight[o] = -999;
		}

		int h = this.height[o];
		State old = this.getState(pos);

		if(old == state) {
			return null;
		}
		else {
			Block block = state.getBlock();
			Block oldb = old.getBlock();
			BlockArray stor = this.blocks[y >> 4];
			boolean up = false;

			if(stor == null) {
				if(block == Blocks.air) {
					return null;
				}

				stor = this.blocks[y >> 4] = new BlockArray(y >> 4 << 4, !this.world.dimension.hasNoLight());
				up = y >= h;
			}

			stor.set(x, y & 15, z, state);

			if(oldb != block) {
				if(!this.world.client) {
					oldb.onBlockRemoved((AWorldServer)this.world, pos, old);
				}
				else if(oldb instanceof ITileEntityProvider) {
					this.world.removeTileEntity(pos);
				}
			}

			if(stor.getBlock(x, y & 15, z) != block) {
				return null;
			}
			else {
				if(up) {
					this.genSkyLight();
				}
				else {
					int b = block.getLightOpacity();
					int ob = oldb.getLightOpacity();

					if(b > 0) {
						if(y >= h) {
							this.relightBlock(x, y + 1, z);
						}
					}
					else if(y == h - 1) {
						this.relightBlock(x, y, z);
					}

					if(b != ob && (b < ob || this.getLight(LightType.SKY, pos) > 0 || this.getLight(LightType.BLOCK, pos) > 0)) {
						this.propagateOcclusion(x, z);
					}
				}

				if(oldb instanceof ITileEntityProvider) {
					TileEntity tile = this.getTileEntity(pos, TileEntity.EnumCreateEntityType.CHECK);

					if(tile != null) {
						tile.updateContainingBlockInfo();
					}
				}

				if(!this.world.client && oldb != block) {
					block.onBlockAdded((AWorldServer)this.world, pos, state);
				}

				if(block instanceof ITileEntityProvider) {
					TileEntity tile = this.getTileEntity(pos, TileEntity.EnumCreateEntityType.CHECK);

					if(tile == null) {
						tile = ((ITileEntityProvider)block).createNewTileEntity(this.world, block.getMetaFromState(state));
						this.world.setTileEntity(pos, tile);
					}

					if(tile != null) {
						tile.updateContainingBlockInfo();
					}
				}

				this.modified = true;
				return old;
			}
		}
	}

	public int getLight(LightType type, BlockPos pos) {
		int x = pos.getX() & 15;
		int y = pos.getY();
		int z = pos.getZ() & 15;
		BlockArray stor = this.blocks[y >> 4];
		return stor == null ? (this.canSeeSky(pos) ? type.defValue : 0)
				: (type == LightType.SKY ? (this.world.dimension.hasNoLight() ? 0 : stor.getSky(x, y & 15, z))
				: (type == LightType.BLOCK ? stor.getLight(x, y & 15, z) : type.defValue));
	}

	public void setLight(LightType type, BlockPos pos, int value) {
		int x = pos.getX() & 15;
		int y = pos.getY();
		int z = pos.getZ() & 15;
		BlockArray stor = this.blocks[y >> 4];

		if(stor == null) {
			stor = this.blocks[y >> 4] = new BlockArray(y >> 4 << 4, !this.world.dimension.hasNoLight());
			this.genSkyLight();
		}

		this.modified = true;

		if(type == LightType.SKY) {
			if(!this.world.dimension.hasNoLight()) {
				stor.setSky(x, y & 15, z, value);
			}
		}
		else if(type == LightType.BLOCK) {
			stor.setLight(x, y & 15, z, value);
		}
	}

	public int getLightSub(BlockPos pos, int amount) {
		int x = pos.getX() & 15;
		int y = pos.getY();
		int z = pos.getZ() & 15;
		BlockArray stor = this.blocks[y >> 4];

		if(stor == null) {
			return !this.world.dimension.hasNoLight() && amount < LightType.SKY.defValue 
					? LightType.SKY.defValue - amount : 0;
		}
		else {
			int l = this.world.dimension.hasNoLight() ? 0 : stor.getSky(x, y & 15, z);
			l = l - amount;
			int b = stor.getLight(x, y & 15, z);

			if(b > l) {
				l = b;
			}

			return l;
		}
	}

	public void addEntity(Entity entity) {
		this.hasEntity = true;
		int x = ExtMath.floord(entity.posX / 16.0D);
		int z = ExtMath.floord(entity.posZ / 16.0D);

		if(x != this.xPos || z != this.zPos) {
			Log.JNI.warn("Falsche Position! (" + x + ", " + z + ") sollte (" + this.xPos + ", " + this.zPos + ") sein, " + entity);
			entity.setDead();
		}

		int y = ExtMath.floord(entity.posY / 16.0D);

		if(y < 0) {
			y = 0;
		}

		if(y >= this.entities.length) {
			y = this.entities.length - 1;
		}

		entity.addedToChunk = true;
		entity.chunkCoordX = this.xPos;
		entity.chunkCoordY = y;
		entity.chunkCoordZ = this.zPos;
		this.entities[y].add(entity);
	}

	public void removeEntity(Entity entity) {
		int y = entity.chunkCoordY;

		if(y < 0) {
			y = 0;
		}

		if(y >= this.entities.length) {
			y = this.entities.length - 1;
		}

		this.entities[y].remove(entity);
	}

	public boolean canSeeSky(BlockPos pos) {
		int x = pos.getX() & 15;
		int y = pos.getY();
		int z = pos.getZ() & 15;
		return y >= this.height[z << 4 | x];
	}

	private TileEntity createNewTileEntity(BlockPos pos) {
		Block block = this.getBlock(pos);
		return !block.hasTileEntity() ? null : ((ITileEntityProvider)block).createNewTileEntity(this.world, this.getMeta(pos));
	}

	public TileEntity getTileEntity(BlockPos pos, TileEntity.EnumCreateEntityType type) {
		TileEntity tile = this.tiles.get(pos);

		if(tile == null) {
			if(type == TileEntity.EnumCreateEntityType.IMMEDIATE) {
				tile = this.createNewTileEntity(pos);
				this.world.setTileEntity(pos, tile);
			}
			else if(type == TileEntity.EnumCreateEntityType.QUEUED) {
				this.tileQueue.add(pos);
			}
		}
		else if(tile.isInvalid()) {
			this.tiles.remove(pos);
			return null;
		}

		return tile;
	}

	public void addTileEntity(TileEntity tile) {
		this.addTileEntity(tile.getPos(), tile);

		if(this.loaded) {
			this.world.addTileEntity(tile);
		}
	}

	public void addTileEntity(BlockPos pos, TileEntity tile) {
		tile.setWorldObj(this.world);
		tile.setPos(pos);

		if(this.getBlock(pos) instanceof ITileEntityProvider) {
			if(this.tiles.containsKey(pos)) {
				this.tiles.get(pos).invalidate();
			}

			if(tile.validate()) {
				this.setModified();
			}
			this.tiles.put(pos, tile);
		}
	}

	public void removeTileEntity(BlockPos pos) {
		if(this.loaded) {
			TileEntity tile = this.tiles.remove(pos);

			if(tile != null) {
				tile.invalidate();
			}
		}
	}

	public void onChunkLoad() {
		this.loaded = true;
		this.world.addTileEntities(this.tiles.values());

		for(int n = 0; n < this.entities.length; ++n) {
			for(Entity entity : this.entities[n]) {
				entity.onChunkLoad();
			}

			this.world.loadEntities(this.entities[n]);
		}
	}

	public void onChunkUnload() {
		this.loaded = false;

		for(TileEntity tile : this.tiles.values()) {
			this.world.markTileEntityForRemoval(tile);
		}

		for(int n = 0; n < this.entities.length; ++n) {
			this.world.unloadEntities(this.entities[n]);
		}
	}

	public void setModified() {
		this.modified = true;
	}

	public void getEntities(Entity exclude, BoundingBox bb, List<Entity> list, Predicate<? super Entity> pred) {
		int sy = ExtMath.floord((bb.minY - 2.0D) / 16.0D);
		int ey = ExtMath.floord((bb.maxY + 2.0D) / 16.0D);
		sy = ExtMath.clampi(sy, 0, this.entities.length - 1);
		ey = ExtMath.clampi(ey, 0, this.entities.length - 1);

		for(int y = sy; y <= ey; ++y) {
			if(!this.entities[y].isEmpty()) {
				for(Entity entity : this.entities[y]) {
					if(entity.getEntityBoundingBox().intersectsWith(bb) && entity != exclude) {
						if(pred == null || pred.test(entity)) {
							list.add(entity);
						}

						Entity[] parts = entity.getParts();

						if(parts != null) {
							for(int l = 0; l < parts.length; ++l) {
								entity = parts[l];

								if(entity != exclude && entity.getEntityBoundingBox().intersectsWith(bb) 
										&& (pred == null || pred.test(entity))) {
									list.add(entity);
								}
							}
						}
					}
				}
			}
		}
	}

	public <T extends Entity> void getEntities(Class<? extends T> clazz, BoundingBox bb, List<T> list, Predicate<? super T> pred) {
		int sy = ExtMath.floord((bb.minY - 2.0D) / 16.0D);
		int ey = ExtMath.floord((bb.maxY + 2.0D) / 16.0D);
		sy = ExtMath.clampi(sy, 0, this.entities.length - 1);
		ey = ExtMath.clampi(ey, 0, this.entities.length - 1);

		for(int y = sy; y <= ey; ++y) {
			for(T entity : this.entities[y].getByClass(clazz)) {
				if(entity.getEntityBoundingBox().intersectsWith(bb) && (pred == null || pred.test(entity))) {
					list.add(entity);
				}
			}
		}
	}

	public boolean isDirty(long time) {
//        if (all)
//        {
		if(this.hasEntity && time /* this.world.getTime() */ != this.lastSave || this.modified) {
			return true;
		}
//        }
//        else if (this.hasEntity && this.world.getTime() >= this.lastSave + 600L)
//        {
//            return true;
//        }

		return this.modified;
	}

	public boolean isEmpty() {
		return false;
	}

	public BlockPos getPrecipitation(BlockPos pos) {
		int x = pos.getX() & 15;
		int z = pos.getZ() & 15;
		int o = x | z << 4;
		BlockPos loc = new BlockPos(pos.getX(), this.precHeight[o], pos.getZ());

		if(loc.getY() == -999) {
			int y = this.getTopSegment() + 15;
			loc = new BlockPos(pos.getX(), y, pos.getZ());
			int h = -1;

			while(loc.getY() > 0 && h == -1) {
				Block block = this.getBlock(loc);
				Material mat = block.getMaterial();

				if((!mat.blocksMovement() && !mat.isLiquid())
						|| (mat == Material.leaves && ((mat = this.getBlock(loc.up()).getMaterial()) == Material.snow) 
						|| mat == Material.leaves)) {
					loc = loc.down();
				}
				else {
					h = loc.getY() + 1;
				}
			}

			this.precHeight[o] = h;
		}

		return new BlockPos(pos.getX(), this.precHeight[o], pos.getZ());
	}

	public void update(boolean noGaps) {
		if(this.gapUpdate && !this.world.dimension.hasNoLight() && !noGaps) {
			this.recheckGaps(this.world.client);
		}

		this.updated = true;

		if(!this.lightInit && this.populated) {
			this.checkLight();
		}

		while(!this.tileQueue.isEmpty()) {
			BlockPos pos = (BlockPos)this.tileQueue.poll();

			if(this.getTileEntity(pos, TileEntity.EnumCreateEntityType.CHECK) == null && this.getBlock(pos).hasTileEntity()) {
				TileEntity tile = this.createNewTileEntity(pos);
				this.world.setTileEntity(pos, tile);
				this.world.markBlockRangeForRenderUpdate(pos, pos);
			}
		}
	}

	public boolean isPopulated() {
		return this.updated && this.populated && this.lightInit;
	}

	public ChunkPos getPos() {
		return new ChunkPos(this.xPos, this.zPos);
	}

	public boolean isEmpty(int bottom, int top) {
		if(bottom < 0) {
			bottom = 0;
		}

		if(top >= 512) {
			top = 511;
		}

		for(int y = bottom; y <= top; y += 16) {
			BlockArray stor = this.blocks[y >> 4];

			if(stor != null && !stor.isEmpty()) {
				return false;
			}
		}

		return true;
	}

	public void setStorage(BlockArray[] arrays) {
		if(this.blocks.length != arrays.length) {
			Log.JNI.warn("Konnte Sektionen des Chunks nicht setzen, Länge des Arrays ist " + arrays.length + " statt " 
			+ this.blocks.length);
		}
		else {
			for(int n = 0; n < this.blocks.length; ++n) {
				this.blocks[n] = arrays[n];
			}
		}
	}

	public void setData(byte[] data, int update, boolean biomes) {
		int pos = 0;
		boolean sky = !this.world.dimension.hasNoLight();

		for(int n = 0; n < this.blocks.length; ++n) {
			if((update & 1 << n) != 0) {
				if(this.blocks[n] == null) {
					this.blocks[n] = new BlockArray(n << 4, sky);
				}

				char[] blocks = this.blocks[n].getData();

				for(int k = 0; k < blocks.length; ++k) {
					blocks[k] = (char)((data[pos + 1] & 255) << 8 | data[pos] & 255);
					pos += 2;
				}
			}
			else if(biomes && this.blocks[n] != null) {
				this.blocks[n] = null;
			}
		}

		for(int n = 0; n < this.blocks.length; ++n) {
			if((update & 1 << n) != 0 && this.blocks[n] != null) {
				NibbleArray light = this.blocks[n].getBlocklight();
				System.arraycopy(data, pos, light.getData(), 0, light.getData().length);
				pos += light.getData().length;
			}
		}

		if(sky) {
			for(int n = 0; n < this.blocks.length; ++n) {
				if((update & 1 << n) != 0 && this.blocks[n] != null) {
					NibbleArray slight = this.blocks[n].getSkylight();
					System.arraycopy(data, pos, slight.getData(), 0, slight.getData().length);
					pos += slight.getData().length;
				}
			}
		}

		if(biomes) {
			System.arraycopy(data, pos, this.biomes, 0, this.biomes.length);
//            int unk = pos + this.biomes.length;
		}

		for(int n = 0; n < this.blocks.length; ++n) {
			if(this.blocks[n] != null && (update & 1 << n) != 0) {
				this.blocks[n].update();
			}
		}

		this.lightInit = true;
		this.populated = true;
		this.genHeights();

		for(TileEntity tile : this.tiles.values()) {
			tile.updateContainingBlockInfo();
		}
	}

	public BaseBiome getBiome(BlockPos pos, BiomeGenerator gen) {
		int x = pos.getX() & 15;
		int z = pos.getZ() & 15;
		int o = this.biomes[z << 4 | x] & 255;

		if(o == 255) {
			BaseBiome biome = gen == null ? BaseBiome.DEF_BIOME : gen.getBiomeGenerator(pos, BaseBiome.DEF_BIOME);
			o = biome.id;
			this.biomes[z << 4 | x] = (byte)(o & 255);
		}

		return BaseBiome.getBiomeDef(o);
	}

	public byte[] getBiomes() {
		return this.biomes;
	}

	public void setBiomes(byte[] biomes) {
		if(this.biomes.length != biomes.length) {
			Log.JNI.warn("Konnte Biome des Chunks nicht setzen, Länge des Arrays ist " + biomes.length + " statt " + this.biomes.length);
		}
		else {
			for(int n = 0; n < this.biomes.length; ++n) {
				this.biomes[n] = biomes[n];
			}
		}
	}

	public void resetRelight() {
		this.lightChecks = 0;
	}

	/**
	 * Called once-per-chunk-per-tick, and advances the round-robin relight check
	 * index by up to 8 blocks at a time. In a worst-case scenario, can potentially
	 * take up to 51.2 seconds, calculated via (8192/8)/20, to re-check all blocks
	 * in a chunk, which may explain lagging light updates on initial world
	 * generation.
	 */
	public void enqueueRelight() {
		BlockPos pos = new BlockPos(this.xPos << 4, 0, this.zPos << 4);

		for(int n = 0; n < 8; ++n) {
			if(this.lightChecks >= 8192)
				return;

			int s = this.lightChecks % 31;
			int x = this.lightChecks / 31 % 16;
			int z = this.lightChecks / 512;
			++this.lightChecks;

			for(int y = 0; y < 16; ++y) {
				BlockPos block = pos.add(x, (s << 4) + y, z);
				boolean edge = y == 0 || y == 15 || x == 0 || x == 15 || z == 0 || z == 15;

				if(this.blocks[s] == null && edge || this.blocks[s] != null 
						&& this.blocks[s].getBlock(x, y, z).getMaterial() == Material.air) {
					for(Facing face : Facing.values()) {
						BlockPos side = block.offset(face);

						if(this.world.getState(side).getBlock().getLightValue() > 0) {
							this.world.checkLight(side);
						}
					}

					this.world.checkLight(block);
				}
			}
		}
	}

	public void checkLight() {
		this.populated = true;
		this.lightInit = true;
		BlockPos pos = new BlockPos(this.xPos << 4, 0, this.zPos << 4);

		if(!this.world.dimension.hasNoLight()) {
			if(this.world.isAreaLoaded(pos.add(-1, 0, -1), pos.add(16, this.world.getSeaLevel(), 16))) {
				label92:

				for(int x = 0; x < 16; ++x) {
					for(int z = 0; z < 16; ++z) {
						if(!this.updateColumn(x, z)) {
							this.lightInit = false;
							break label92;
						}
					}
				}

				if(this.lightInit) {
					for(Facing face : Facing.Plane.HORIZONTAL) {
						int d = face.getAxisDirection() == Facing.AxisDirection.POSITIVE ? 16 : 1;
						this.world.getChunk(pos.offset(face, d)).updateColumns(face.getOpposite());
					}

					this.setSkyDirty();
				}
			}
			else {
				this.lightInit = false;
			}
		}
	}

	private void setSkyDirty() {
		for(int n = 0; n < this.updateSky.length; ++n) {
			this.updateSky[n] = true;
		}

		this.recheckGaps(false);
	}

	private void updateColumns(Facing facing) {
		if(this.populated) {
			if(facing == Facing.EAST) {
				for(int z = 0; z < 16; ++z) {
					this.updateColumn(15, z);
				}
			}
			else if(facing == Facing.WEST) {
				for(int z = 0; z < 16; ++z) {
					this.updateColumn(0, z);
				}
			}
			else if(facing == Facing.SOUTH) {
				for(int x = 0; x < 16; ++x) {
					this.updateColumn(x, 15);
				}
			}
			else if(facing == Facing.NORTH) {
				for(int x = 0; x < 16; ++x) {
					this.updateColumn(x, 0);
				}
			}
		}
	}

	private boolean updateColumn(int x, int z) {
		int top = this.getTopSegment();
		boolean opaque = false;
		boolean below = false;
		BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos((this.xPos << 4) + x, 0, (this.zPos << 4) + z);

		for(int y = top + 16 - 1; y > this.world.getSeaLevel() || y > 0 && !below; --y) {
			pos.set(pos.getX(), y, pos.getZ());
			int o = this.getOpacity(pos);

			if(o == 255 && pos.getY() < this.world.getSeaLevel()) {
				below = true;
			}

			if(!opaque && o > 0) {
				opaque = true;
			}
			else if(opaque && o == 0 && !this.world.checkLight(pos)) {
				return false;
			}
		}

		for(int y = pos.getY(); y > 0; --y) {
			pos.set(pos.getX(), y, pos.getZ());

			if(this.getBlock(pos).getLightValue() > 0) {
				this.world.checkLight(pos);
			}
		}

		return true;
	}

	public boolean isLoaded() {
		return this.loaded;
	}

	public void setLoaded(boolean loaded) {
		this.loaded = loaded;
	}

	public World getWorld() {
		return this.world;
	}

	public int[] getHeights() {
		return this.height;
	}

	public void setHeights(int[] map) {
		if(this.height.length != map.length) {
			Log.JNI.warn("Konnte Höhen des Chunks nicht setzen, Länge des Arrays ist " + map.length + " statt " + this.height.length);
		}
		else {
			for(int n = 0; n < this.height.length; ++n) {
				this.height[n] = map[n];
			}
		}
	}

	public Map<BlockPos, TileEntity> getTiles() {
		return this.tiles;
	}

	public ClassInheritanceMultiMap<Entity>[] getEntities() {
		return this.entities;
	}

	public boolean isTerrainPopulated() {
		return this.populated;
	}

	public void setTerrainPopulated(boolean populated) {
		this.populated = populated;
	}

	public boolean isLightPopulated() {
		return this.lightInit;
	}

	public void setLightPopulated(boolean populated) {
		this.lightInit = populated;
	}

	public void setModified(boolean modified) {
		this.modified = modified;
	}

	public void setHasEntities(boolean entities) {
		this.hasEntity = entities;
	}

	public void setSaved(long time) {
		this.lastSave = time;
	}

	public int getLowest() {
		return this.minHeight;
	}

	public long getInhabited() {
		return this.inhabited;
	}

	public void setInhabited(long time) {
		this.inhabited = time;
	}
}
