content: package thebetweenlands.common.world.gen.feature;
  
  import java.util.ArrayList;
  import java.util.Collections;
  import java.util.List;
  import java.util.Random;
  import java.util.UUID;
  
  import javax.annotation.Nullable;
  
  import net.minecraft.block.state.IBlockState;
  import net.minecraft.init.Blocks;
  import net.minecraft.util.math.AxisAlignedBB;
  import net.minecraft.util.math.BlockPos;
  import net.minecraft.util.math.MathHelper;
  import net.minecraft.world.World;
  import net.minecraft.world.chunk.ChunkPrimer;
  import net.minecraft.world.gen.MapGenBase;
  import net.minecraft.world.gen.NoiseGeneratorPerlin;
  import thebetweenlands.api.storage.LocalRegion;
  import thebetweenlands.api.storage.StorageUUID;
  import thebetweenlands.common.block.terrain.BlockCragrock;
  import thebetweenlands.common.block.terrain.BlockCragrock.EnumCragrockType;
  import thebetweenlands.common.block.terrain.BlockHanger;
  import thebetweenlands.common.block.terrain.BlockSwampWater;
  import thebetweenlands.common.registries.BiomeRegistry;
  import thebetweenlands.common.registries.BlockRegistry;
  import thebetweenlands.common.world.WorldProviderBetweenlands;
  import thebetweenlands.common.world.gen.IBlockStateAccessOnly;
  import thebetweenlands.common.world.gen.biome.feature.CoarseIslandsFeature;
  import thebetweenlands.common.world.storage.BetweenlandsWorldStorage;
  import thebetweenlands.common.world.storage.location.EnumLocationType;
  import thebetweenlands.common.world.storage.location.LocationStorage;
  
  public class MapGenFloatingIslands extends MapGenBase {
  	protected CoarseIslandsFeature coarseIslandsFeature = new CoarseIslandsFeature();
  
  	protected List<BlockPos> islands = new ArrayList<>();
  
  	protected List<WorldGenGiantRoot> giantRootGens = new ArrayList<>();
  
  	private NoiseGeneratorPerlin islandNoiseGen;
  	private double[] islandNoise = new double[256];
  
  	private NoiseGeneratorPerlin cragNoiseGen;
  	private double[] cragNoise = new double[256];
  
  	public MapGenFloatingIslands(long worldSeed) {
  		this.range = 5;
  		Random rng = new Random(worldSeed);
  		this.islandNoiseGen = new NoiseGeneratorPerlin(rng, 3);
  		this.cragNoiseGen = new NoiseGeneratorPerlin(rng, 5);
  	}
  
  	@Override
  	public void generate(World worldIn, int x, int z, ChunkPrimer primer) {
  		this.islands.clear();
  		this.giantRootGens.clear();
  
  		super.generate(worldIn, x, z, primer);
  
  		for(BlockPos island : this.islands) {
  			//early skip islands outside range
  			if(island.getDistance(x * 16, island.getY(), z * 16) <= 52) {
  				Random rand = new Random();
  				rand.setSeed(island.getX() ^ island.getY() ^ island.getZ() ^ worldIn.getSeed());
  				this.generateIsland(island, rand, x, z, primer);
  			}
  		}
  
  		for(WorldGenGiantRoot root : this.giantRootGens) {
  			root.setGenBounds(new AxisAlignedBB(x * 16, 0, z * 16, x * 16 + 15, 256, z * 16 + 15));
  			Random rootRand = new Random();
  			rootRand.setSeed(root.start.getX() ^ root.start.getY() ^ root.start.getZ() ^ worldIn.getSeed());
  			root.generate(IBlockStateAccessOnly.from(x, z, primer), x, z, true, rootRand, root.start);
  		}
  	}
  
  	@Override
  	protected void recursiveGenerate(World worldIn, int chunkX, int chunkZ, int originalX, int originalZ, ChunkPrimer chunkPrimerIn) {
  		if(this.rand.nextInt(80) == 0) {
  			int inChunkX = this.rand.nextInt(16);
  			int inChunkZ = this.rand.nextInt(16);
  
  			if(worldIn.getBiomeProvider().getBiome(new BlockPos(chunkX * 16 + inChunkX, 64, chunkZ * 16 + inChunkZ)) == BiomeRegistry.RAISED_ISLES) {
  				List<BlockPos> surfaceRootCandidates = new ArrayList<>();
  				List<BlockPos> islandRootCandidates = new ArrayList<>();
  
  				BlockPos islandCandidate = new BlockPos(chunkX * 16 + inChunkX, WorldProviderBetweenlands.LAYER_HEIGHT + 25 + this.rand.nextInt(40), chunkZ * 16 + inChunkZ);
  
  				int numRoots = this.rand.nextInt(4) + 3;
  				for(int i = 0; i < numRoots; i++) {
  					int range = (i + 1) * 5 + 10;
  					surfaceRootCandidates.add(islandCandidate.add(this.rand.nextInt(range) - range / 2, -6, this.rand.nextInt(range) - range / 2));
  				}
  
  				islandRootCandidates.add(islandCandidate.add(this.rand.nextInt(32) - 16, 1, this.rand.nextInt(32) - 16));
  
  				int numSmallRoots = this.rand.nextInt(3);
  				for(int i = 0; i < numSmallRoots; i++) {
  					BlockPos start = islandCandidate.add(this.rand.nextInt(32) - 16, 1, this.rand.nextInt(32) - 16);
  					BlockPos end = islandCandidate.add(this.rand.nextInt(32) - 16, 1, this.rand.nextInt(32) - 16);
  					this.giantRootGens.add(new WorldGenGiantRoot(start, end, 1, 1.5D, 3, 2, 7, false, false, true, true, null));
  				}
  
  				Collections.shuffle(surfaceRootCandidates, this.rand);
  				Collections.shuffle(islandRootCandidates, this.rand);
  
  				this.islands.add(islandCandidate);
  
  				for(BlockPos start : surfaceRootCandidates) {
  					BlockPos end = this.findSurfaceEndpoint(start.getX() >> 4, start.getZ() >> 4, start, 2);
  
  					if(end != null) {
  						this.giantRootGens.add(new WorldGenGiantRoot(start, end, 2, 4, 10, 7, -12, true, false, true, true, null));
  					}
  				}
  
  				for(BlockPos start : islandRootCandidates) {
  					BlockPos end = this.findIslandEndpoint(start.getX() >> 4, start.getZ() >> 4, start, 3);
  
  					if(end != null) {
  						this.giantRootGens.add(new WorldGenGiantRoot(start, end, 1, 2, 6, 4, 25, false, false, true, true, null));
  					}
  				}
  			}
  		}
  	}
  
  	protected void generateIsland(BlockPos center, Random rand, int chunkX, int chunkZ, ChunkPrimer primer) {
  		long locationSeed = rand.nextLong(); //Need to always generate the seed otherwise world gen desyncs in other chunks
  		
  		if(center.getX() >> 4 == chunkX && center.getZ() >> 4 == chunkZ) {
  			BetweenlandsWorldStorage worldStorage = BetweenlandsWorldStorage.forWorld(this.world);
  			LocationStorage locationStorage = new LocationStorage(worldStorage, new StorageUUID(UUID.randomUUID()), LocalRegion.getFromBlockPos(center), "floating_island", EnumLocationType.FLOATING_ISLAND);
  			locationStorage.addBounds(new AxisAlignedBB(center).grow(30, 18, 30).offset(0, 5, 0));
  			locationStorage.setVisible(false);
  			locationStorage.setSeed(locationSeed);
  			locationStorage.setDirty(true);
  			worldStorage.getLocalStorageHandler().addLocalStorage(locationStorage);
  		}
  		
  		float[] islandMask = new float[256];
  
  		int i = rand.nextInt(4) + 6;
  
  		float size = rand.nextFloat();
  
  		float minSize = 20 + size * 8;
  		float maxSize = 40 + size * 18;
  
  		int relX = center.getX() - chunkX * 16;
  		int relZ = center.getZ() - chunkZ * 16;
  		
  		for(int round = 0; round < i; ++round) {
  			float wx = rand.nextFloat() * (maxSize - minSize) + minSize;
  			float wz = rand.nextFloat() * (maxSize - minSize) + minSize;
  			float mx = 32 + (rand.nextFloat() - 0.5F) * wx * 0.5F;
  			float mz = 32 + (rand.nextFloat() - 0.5F) * wz * 0.5F;
  
  			for(int bx = 0; bx < 16; ++bx) {
  				for(int bz = 0; bz < 16; ++bz) {
  					int ix = bx - relX + 32;
  					int iz = bz - relZ + 32;
  					
  					if(ix >= 0 && ix < 64 && iz >= 0 && iz < 64) {
  						float dx = (ix - mx) / wx;
  						float dz = (iz - mz) / wz;
  						float d = dx * dx + dz * dz;
  	
  						islandMask[bx * 16 + bz] = Math.max(islandMask[bx * 16 + bz], 1 - d);
  					}
  				}
  			}
  		}
  
  		this.islandNoise = this.islandNoiseGen.getRegion(this.islandNoise, (double) (chunkX * 16), (double) (chunkZ * 16), 16, 16, 0.4D, 0.4D, 1.0D);
  		this.cragNoise = this.cragNoiseGen.getRegion(this.cragNoise, (double) (chunkX * 16), (double) (chunkZ * 16), 16, 16, 0.55D, 0.55D, 1.0D);
  
  		float minVal = 0.8f;
  
  		boolean hasPond = rand.nextInt(3) == 0;
  
  		for(int bx = 0; bx < 16; ++bx) {
  			for(int bz = 0; bz < 16; ++bz) {
  				if(islandMask[bx * 16 + bz] > minVal) {
  					double islandNoiseVal = this.islandNoise[bz * 16 + bx] / 0.9f + 2.1f;
  
  					double cragNoise = this.cragNoise[bz * 16 + bx] / 2.1f + 2.0f;
  
  					boolean isCrag = cragNoise <= 0;
  
  					double islandMaskVal = islandMask[bx * 16 + bz] - minVal;
  
  					int depth = MathHelper.ceil(islandMaskVal * islandMaskVal * 200 + islandNoiseVal * islandNoiseVal * islandMaskVal);
  					int height = MathHelper.floor(islandMaskVal * 12.0f - islandNoiseVal * islandMaskVal * 0.1f + (cragNoise < 0.2f ? 1 : 0));
  
  					int lowering = 0;
  					if(hasPond && islandMaskVal * 12.0f > 1.5f) {
  						lowering = MathHelper.ceil((islandMaskVal * 12.0f - 1.5f) * 5.0f);
  					}
  
  					int water = -1;
  
  					int surface = center.getY();
  					for(; surface > depth + 1; surface--) {
  						IBlockState state = primer.getBlockState(bx, surface, bz);
  
  						boolean liquid = state.getMaterial().isLiquid();
  
  						if(liquid && water < 0) {
  							water = surface;
  						}
  
  						if(state.getBlock() != Blocks.AIR && !liquid) {
  							break;
  						}
  					}
  
  					for(int y = 0; y > -depth; y--) {
  						if(surface + y > 0 && !primer.getBlockState(bx, surface, bz).getMaterial().isLiquid()) {
  							if(surface + y <= WorldProviderBetweenlands.LAYER_HEIGHT) {
  								primer.setBlockState(bx, surface + y, bz, BlockRegistry.SWAMP_WATER.getDefaultState());
  							} else {
  								primer.setBlockState(bx, surface + y, bz, Blocks.AIR.getDefaultState());
  							}
  						}
  					}
  
  					boolean isPondColumn = height - lowering < 0 && lowering > 0;
  
  					for(int y = height - lowering; y > -depth - lowering; y--) {
  						int by = center.getY() + y;
  
  						if(by > 0) {
  							if(isCrag) {
  								if(y == height - lowering) {
  									primer.setBlockState(bx, by, bz, BlockRegistry.CRAGROCK.getDefaultState().withProperty(BlockCragrock.VARIANT, EnumCragrockType.MOSSY_1));
  								} else if(y == height - lowering - 1) {
  									primer.setBlockState(bx, by, bz, BlockRegistry.CRAGROCK.getDefaultState().withProperty(BlockCragrock.VARIANT, EnumCragrockType.MOSSY_2));
  								} else if(y == height - lowering - 2) {
  									primer.setBlockState(bx, by, bz, BlockRegistry.CRAGROCK.getDefaultState());
  								} else {
  									primer.setBlockState(bx, by, bz, BlockRegistry.BETWEENSTONE.getDefaultState());
  								}
  							} else {
  								if(isPondColumn) {
  									primer.setBlockState(bx, by, bz, BlockRegistry.MUD.getDefaultState());
  								} else {
  									if(y == height - lowering) {
  										primer.setBlockState(bx, by, bz, BlockRegistry.SWAMP_GRASS.getDefaultState());
  									} else if(y >= height - lowering - 2) {
  										primer.setBlockState(bx, by, bz, BlockRegistry.SWAMP_DIRT.getDefaultState());
  									} else {
  										primer.setBlockState(bx, by, bz, BlockRegistry.BETWEENSTONE.getDefaultState());
  									}
  								}
  							}
  						}
  					}
  
  					if(isPondColumn) {
  						for(int y = 0; y > height - lowering; y--) {
  							int by = center.getY() + y;
  
  							if(by > 0) {
  								primer.setBlockState(bx, by, bz, BlockRegistry.SWAMP_WATER.getDefaultState());
  							}
  						}
  					}
  
  					if(rand.nextInt(20) == 0 && height + depth <= 3 && !isCrag) {
  						int rootLength = 1 + rand.nextInt(12);
  
  						for(int y = -depth - lowering; y > -depth - rootLength - lowering; y--) {
  							int by = center.getY() + y;
  
  							if(by > 0) {
  								primer.setBlockState(bx, by, bz, BlockRegistry.ROOT.getDefaultState());
  							}
  						}
  					} else if(rand.nextInt(13) == 0) {
  						int hangerLength = 1 + rand.nextInt(8);
  
  						for(int y = -depth - lowering; y > -depth - hangerLength - lowering; y--) {
  							int by = center.getY() + y;
  
  							if(by > 0) {
  								primer.setBlockState(bx, by, bz, BlockRegistry.HANGER.getDefaultState().withProperty(BlockHanger.CAN_GROW, false));
  							}
  						}
  					} else if(rand.nextInt(400) == 0 && water > 0 && height + depth >= 4 && center.getY() - depth - lowering + 3 > 0) {
  						primer.setBlockState(bx, center.getY() - depth - lowering + 3, bz, BlockRegistry.SWAMP_WATER.getDefaultState());
  						for(int by = center.getY() - depth - lowering + 2; by > water; by--) {
  							if(by > 0) {
  								//vertical flowing fluid has meta 1
  								primer.setBlockState(bx, by, bz, BlockRegistry.SWAMP_WATER.getDefaultState().withProperty(BlockSwampWater.LEVEL, 1));
  							}
  						}
  					}
  				}
  			}
  		}
  	}
  
  	@Nullable
  	protected BlockPos findIslandEndpoint(int chunkX, int chunkZ, BlockPos start, int chunkRange) {
  		List<BlockPos> endCandidates = new ArrayList<>();
  
  		int subDivs = 2;
  		int step = 16 / subDivs;
  
  		for (int newChunkX = chunkX - chunkRange; newChunkX <= chunkX + chunkRange; ++newChunkX) {
  			for (int newChunkZ = chunkZ - chunkRange; newChunkZ <= chunkZ + chunkRange; ++newChunkZ) {
  
  				int inChunkX = 0;
  				int inChunkZ = 0;
  				for(int xs = 0; xs < subDivs; xs++) {
  					for(int zs = 0; zs < subDivs; zs++) {
  						if(this.rand.nextInt(60) == 0) {
  							int wx = newChunkX * 16 + inChunkX;
  							int wz = newChunkZ * 16 + inChunkZ;
  
  							for(BlockPos island : this.islands) {
  								if(!island.equals(start)) {
  									int dx = island.getX() - wx;
  									int dz = island.getZ() - wz;
  
  									if((dx*dx + dz*dz) >> 4 <= 2) {
  										endCandidates.add(new BlockPos(newChunkX * 16 + inChunkX, island.getY(), newChunkZ * 16 + inChunkZ));
  									}
  								}
  							}
  						}
  
  						inChunkZ += step;
  					}
  					inChunkX += step;
  				}
  
  			}
  		}
  
  		if(!endCandidates.isEmpty()) {
  			Collections.shuffle(endCandidates, this.rand);
  
  			for(BlockPos endCandidate : endCandidates) {
  				if(endCandidate.distanceSq(start) >= 20*20) {
  					return endCandidate; //TODO Check if end candidate is already used?
  				}
  			}
  		}
  
  		return null;
  	}
  
  	@Nullable
  	protected BlockPos findSurfaceEndpoint(int chunkX, int chunkZ, BlockPos start, int chunkRange) {
  		List<BlockPos> endCandidates = new ArrayList<>();
  
  		int subDivs = 4;
  		int step = 16 / subDivs;
  
  		for (int newChunkX = chunkX - chunkRange; newChunkX <= chunkX + chunkRange; ++newChunkX) {
  			for (int newChunkZ = chunkZ - chunkRange; newChunkZ <= chunkZ + chunkRange; ++newChunkZ) {
  
  				int inChunkX = 0;
  				int inChunkZ = 0;
  				for(int xs = 0; xs < subDivs; xs++) {
  					for(int zs = 0; zs < subDivs; zs++) {
  						if(this.rand.nextInt(60) == 0) {
  							endCandidates.add(new BlockPos(newChunkX * 16 + inChunkX, WorldProviderBetweenlands.LAYER_HEIGHT - 6, newChunkZ * 16 + inChunkZ));
  						}
  
  						inChunkZ += step;
  					}
  					inChunkX += step;
  				}
  
  			}
  		}
  
  		if(!endCandidates.isEmpty()) {
  			Collections.shuffle(endCandidates, this.rand);
  
  			for(BlockPos endCandidate : endCandidates) {
  				if(endCandidate.distanceSq(start) >= 40*40) {
  					return endCandidate; //TODO Check if end candidate is already used?
  				}
  			}
  		}
  
  		return null;
  	}
  }
  