content: package thebetweenlands.common.world.gen.feature.tree;
  
  import java.util.ArrayList;
  import java.util.List;
  import java.util.Random;
  import java.util.function.Consumer;
  import java.util.function.Predicate;
  
  import javax.annotation.Nullable;
  
  import org.apache.commons.lang3.tuple.Pair;
  
  import net.minecraft.block.BlockLog;
  import net.minecraft.block.state.IBlockState;
  import net.minecraft.util.EnumFacing;
  import net.minecraft.util.math.BlockPos;
  import net.minecraft.util.math.MathHelper;
  import net.minecraft.util.math.Vec3d;
  import net.minecraft.world.World;
  import thebetweenlands.common.block.terrain.BlockLeavesBetweenlands;
  import thebetweenlands.common.block.terrain.BlockLogBetweenlands;
  import thebetweenlands.common.registries.BlockRegistry;
  import thebetweenlands.common.world.gen.feature.WorldGenHelper;
  import thebetweenlands.util.CatmullRomSpline;
  import thebetweenlands.util.ISpline;
  
  public class WorldGenHearthgroveTree extends WorldGenHelper {
  	@Override
  	public boolean generate(World world, Random rand, BlockPos pos) {
  		final int x = pos.getX();
  		final int y = pos.getY();
  		final int z = pos.getZ();
  
  		int height = rand.nextInt(4) + 9;
  		int canopySize = (int)(((rand.nextDouble() * height) / 12.0D) * 4 + 5);
  
  		if(!this.rotatedCubeCantReplace(world, x, y + 2, z, 0, 0, 0, 1, height, 1, 0) && 
  				!this.rotatedCubeCantReplace(world, x, y + 2, z, -canopySize+2, 0, -canopySize+2, canopySize*2-4, height, canopySize*2-4, 0)) {
  
  			IBlockState log = BlockRegistry.LOG_HEARTHGROVE.getDefaultState().withProperty(BlockLogBetweenlands.LOG_AXIS, BlockLog.EnumAxis.NONE);
  			IBlockState logY = BlockRegistry.LOG_HEARTHGROVE.getDefaultState().withProperty(BlockLogBetweenlands.LOG_AXIS, BlockLog.EnumAxis.Y);
  			IBlockState leaves = BlockRegistry.LEAVES_HEARTHGROVE_TREE.getDefaultState().withProperty(BlockLeavesBetweenlands.CHECK_DECAY, false);
  			IBlockState hangers = BlockRegistry.HANGER.getDefaultState();
  
  			int rootHeight = rand.nextInt(3) + 1;
  
  			int canopyStart = canopySize / 2 - 1;
  
  			for(int i = rootHeight; i <= height - canopyStart - 1; i++) {
  				this.setBlockAndNotifyAdequately(world, pos.up(i), logY);
  			}
  
  			List<BlockPos> blobs = new ArrayList<>();
  			for(int i = 0; i < 4; i++) {
  				blobs.add(new BlockPos(x + rand.nextInt(canopySize*2) - canopySize, y + height - canopyStart / 2, z + rand.nextInt(canopySize*2) - canopySize));
  			}
  
  			double circumference = Math.PI * (canopySize - 1);
  
  			int branches = (int)(circumference / 4.0D) + 1;
  
  			double angleStep = Math.PI * 2 / (branches-1);
  
  			List<Pair<BlockPos, Float>> leavesSupportPoints = new ArrayList<>();
  
  			List<BlockPos> subbranchEndpoints = new ArrayList<>();
  
  			if(canopySize > 3) {
  				for(int i = 0; i < branches; i++) {
  					BlockPos branchStart = pos.up(height - canopyStart - 2);
  					double randRot = rand.nextDouble() * angleStep / 3.0D;
  					BlockPos branchEnd = pos.add(Math.cos(Math.PI * 2 / (branches-1) * i + randRot) * (canopySize - 4), height - canopyStart, Math.sin(Math.PI * 2 / (branches-1) * i + randRot) * (canopySize - 4));
  
  					List<Vec3d> branchPoints = new ArrayList<>();
  
  					branchPoints.add(new Vec3d(branchStart.up()));
  					branchPoints.add(new Vec3d(branchStart));
  
  
  					branchPoints.add(new Vec3d(branchEnd));
  					branchPoints.add(new Vec3d(branchEnd.up()));
  
  					ISpline spline = new CatmullRomSpline(branchPoints.toArray(new Vec3d[0]));
  
  					BlockPos setBranchPos = null;
  
  
  					boolean support = false;
  					int steps = 30;
  					for(int j = 0; j < steps; j++) {
  						BlockPos branchPos = new BlockPos(spline.interpolate(1.0F / (steps-1) * j).add(0.5, 0.5, 0.5));
  						if(!branchPos.equals(setBranchPos) && world.isAirBlock(branchPos)) {
  							this.setBlockAndNotifyAdequately(world, branchPos, log);
  
  							if(!support && branchPos.distanceSq(x, branchPos.getY(), z) >= 2.5D) {
  								EnumFacing supportDir = EnumFacing.getFacingFromVector(branchPos.getX() - x, 0, branchPos.getZ() - z);
  
  								int supportYO = 0;
  								int supportXO = 0;
  								int supportZO = 0;
  
  								int supportHeight = height - (branchPos.getY() - y) + (canopySize <= 6 ? -1 : 0);
  
  								for(int k = 0; k < supportHeight; k++) {
  									this.setBlockAndNotifyAdequately(world, branchPos.add(supportXO, 1 + supportYO, supportZO), logY);
  
  									if((k+1) % 3 == 0) {
  										supportXO += supportDir.getXOffset();
  										supportZO += supportDir.getZOffset();
  										k++;
  									}
  									supportYO++;
  								}
  
  								leavesSupportPoints.add(Pair.of(branchPos.add(supportXO, 1 + supportYO, supportZO), 6.0F));
  
  								support = true;
  							}
  						}
  						setBranchPos = branchPos;
  					}
  					if(setBranchPos != null) {
  						leavesSupportPoints.add(Pair.of(setBranchPos, 10.0F));
  						if(canopySize >= 5) {
  							subbranchEndpoints.add(setBranchPos);
  						}
  					}
  				}
  			}
  
  			float centerSquashX = 0.8F + rand.nextFloat() * 0.4F;
  			float centerSquashZ = 0.8F + rand.nextFloat() * 0.4F;
  			for(int i = 0; i < canopyStart + 2; i++) {
  				final int k = i;
  				this.generateCanopyLayer(world, pos.up(height - canopyStart + i), canopySize - i, leaves, p -> {
  					boolean nearSupport = false;
  					Random supportRand = new Random();
  					for(Pair<BlockPos, Float> support : leavesSupportPoints) {
  						BlockPos supportPos = support.getKey();
  						supportRand.setSeed(MathHelper.getCoordinateRandom(supportPos.getX(), supportPos.getY(), supportPos.getZ()));
  						float squashX = 0.6F + supportRand.nextFloat() * 1.2F;
  						float squashY = 0.6F + supportRand.nextFloat() * 1.2F;
  						float squashZ = 0.6F + supportRand.nextFloat() * 1.2F;
  						if((supportPos.getX()-p.getX())*(supportPos.getX()-p.getX())*squashX +
  								(supportPos.getY()-p.getY())*(supportPos.getY()-p.getY())*squashY + 
  								(supportPos.getZ()-p.getZ())*(supportPos.getZ()-p.getZ())*squashZ <= support.getValue() + rand.nextFloat() * 0.8F) {
  							nearSupport = true;
  							break;
  						}
  					}
  					if(!nearSupport) return false;
  					return world.isAirBlock(p) && (k > 1 || (canopySize - k) < 4 || Math.sqrt((p.getX()-x)*(p.getX()-x)*centerSquashX+(p.getZ()-z)*(p.getZ()-z)*centerSquashZ) >= canopySize - k - 3 - rand.nextFloat() * 0.8F);
  				}, p -> {
  					if(k == 0 && rand.nextInt(6) == 0) {
  						int hangerLength = rand.nextInt(5) + 1;
  						for(int yo = 0; yo < hangerLength; yo++) {
  							BlockPos hangerPos = p.down(1 + yo);
  							if(world.isAirBlock(hangerPos)) {
  								this.setBlockAndNotifyAdequately(world, hangerPos, hangers);
  							} else {
  								break;
  							}
  						}
  					}
  				});
  			}
  
  			for(BlockPos endpoint : subbranchEndpoints) {
  				EnumFacing branchDir = EnumFacing.getFacingFromVector(endpoint.getX() - x, 0, endpoint.getZ() - z);
  				BlockPos branch1 = endpoint.offset(branchDir).offset(branchDir.rotateY());
  				BlockPos branch2 = endpoint.offset(branchDir).offset(branchDir.rotateYCCW());
  				if(world.isAirBlock(branch1)) this.setBlockAndNotifyAdequately(world, branch1, log);
  				if(world.isAirBlock(branch2)) this.setBlockAndNotifyAdequately(world, branch2, log);
  			}
  
  			boolean[] generatedRoots = new boolean[5*5];
  
  			int roots = 0;
  
  			for(int i = 0; i < 50 && roots < 3; i++) {
  				int rootX = rand.nextInt(3) - 1;
  				int rootZ = rand.nextInt(3) - 1;
  
  				if(rootX != 0 || rootZ != 0) {
  					int rootIndex = rootX + 1 + 1 + (rootZ + 1 + 1) * 5;
  					int rootIndexN = rootIndex - 5;
  					int rootIndexS = rootIndex + 5;
  					int rootIndexE = rootIndex + 1;
  					int rootIndexW = rootIndex - 1;
  
  					if(!generatedRoots[rootIndex] && !generatedRoots[rootIndexN] && !generatedRoots[rootIndexS] &&
  							!generatedRoots[rootIndexE] && !generatedRoots[rootIndexW]) {
  						int rootXO = 0;
  						int rootZO = 0;
  
  						for(int j = rootHeight; j >= -4; j--) {
  							if(j < rootHeight && rand.nextInt(3) == 0) {
  								EnumFacing offset = EnumFacing.HORIZONTALS[rand.nextInt(EnumFacing.HORIZONTALS.length)];
  								rootXO += rootX * Math.abs(offset.getXOffset());
  								rootZO += rootZ * Math.abs(offset.getZOffset());
  							}
  							BlockPos rootPos = pos.add(rootX + rootXO, j, rootZ + rootZO);
  							if(world.getBlockState(rootPos).getBlock().isReplaceable(world, rootPos)) {
  								this.setBlockAndNotifyAdequately(world, rootPos, log);
  							} else {
  								break;
  							}
  						}
  
  						generatedRoots[rootIndex] = true;
  						roots++;
  					}
  				}
  			}
  
  			return true;
  		}
  
  		return false;
  	}
  
  	protected void generateCanopyLayer(World world, BlockPos center, final int size, IBlockState leaves, final @Nullable Predicate<BlockPos> pred, @Nullable Consumer<BlockPos> postprocessor) {
  		int x = center.getX();
  		int y = center.getY();
  		int z = center.getZ();
  
  		switch(size) {
  		default:
  			int radius = size - 1;
  			int span = MathHelper.floor(size / 2.0F);
  			span += (span-1) % 2;
  
  			boolean[] setStates = new boolean[(size*2)*(size*2)];
  
  			Predicate<BlockPos> fullPred = pos -> {
  				if(pred.test(pos)) {
  					int xo = pos.getX() - x;
  					int zo = pos.getZ() - z;
  					int index = xo + radius + 1 + (zo + radius + 1) * (size*2);
  					return !setStates[index];
  				}
  				return false;
  			};
  
  			Consumer<BlockPos> consumer = pos -> {
  				int xo = pos.getX() - x;
  				int zo = pos.getZ() - z;
  				int index = xo + radius + 1 + (zo + radius + 1) * (size*2);
  				if(!setStates[index]) {
  					setStates[index] = true;
  				}
  				if(postprocessor != null) {
  					postprocessor.accept(pos);
  				}
  			};
  
  			for(int i = 0; i <= span; i++) {
  				this.rotatedCubeVolume(world, fullPred, x, y, z, -(radius-i), 0, -(span+2*i-1)/2, leaves, (radius-i)*2+1, 1, span+2*i, 0, consumer);
  				this.rotatedCubeVolume(world, fullPred, x, y, z, -(span+2*i-1)/2, 0, -(radius-i), leaves, span+2*i, 1, (radius-i)*2+1, 0, consumer);
  			}
  			break;
  		case 4:
  			this.rotatedCubeVolume(world, pred, x, y, z, -2, 0, -2, leaves, 5, 1, 5, 0, postprocessor);
  			this.rotatedCubeVolume(world, pred, x, y, z, -3, 0, 0, leaves, 1, 1, 1, 0, postprocessor);
  			this.rotatedCubeVolume(world, pred, x, y, z, 3, 0, 0, leaves, 1, 1, 1, 0, postprocessor);
  			this.rotatedCubeVolume(world, pred, x, y, z, 0, 0, -3, leaves, 1, 1, 1, 0, postprocessor);
  			this.rotatedCubeVolume(world, pred, x, y, z, 0, 0, 3, leaves, 1, 1, 1, 0, postprocessor);
  			break;
  		case 3:
  			this.rotatedCubeVolume(world, pred, x, y, z, -1, 0, -1, leaves, 3, 1, 3, 0, postprocessor);
  			this.rotatedCubeVolume(world, pred, x, y, z, -2, 0, 0, leaves, 1, 1, 1, 0, postprocessor);
  			this.rotatedCubeVolume(world, pred, x, y, z, 2, 0, 0, leaves, 1, 1, 1, 0, postprocessor);
  			this.rotatedCubeVolume(world, pred, x, y, z, 0, 0, -2, leaves, 1, 1, 1, 0, postprocessor);
  			this.rotatedCubeVolume(world, pred, x, y, z, 0, 0, 2, leaves, 1, 1, 1, 0, postprocessor);
  			break;
  		case 2:
  			this.rotatedCubeVolume(world, pred, x, y, z, 0, 0, 0, leaves, 2, 1, 1, 0, postprocessor);
  			this.rotatedCubeVolume(world, pred, x, y, z, -1, 0, 0, leaves, 1, 1, 1, 0, postprocessor);
  			this.rotatedCubeVolume(world, pred, x, y, z, 0, 0, 1, leaves, 1, 1, 1, 0, postprocessor);
  			this.rotatedCubeVolume(world, pred, x, y, z, 0, 0, -1, leaves, 1, 1, 1, 0, postprocessor);
  			break;
  		case 1:
  			this.rotatedCubeVolume(world, pred, x, y, z, 0, 0, 0, leaves, 1, 1, 1, 0, postprocessor);
  			break;
  		}
  	}
  }
  