content: package com.infinityraider.agricraft.content.core;
  
  import com.agricraft.agricore.util.TypeHelper;
  import com.google.common.collect.Lists;
  import com.google.common.collect.Maps;
  import com.infinityraider.agricraft.AgriCraft;
  import com.infinityraider.agricraft.api.v1.AgriApi;
  import com.infinityraider.agricraft.api.v1.content.items.*;
  import com.infinityraider.agricraft.api.v1.crop.IAgriCrop;
  import com.infinityraider.agricraft.api.v1.genetics.IAgriGeneCarrierItem;
  import com.infinityraider.agricraft.api.v1.genetics.IAgriGenome;
  import com.infinityraider.agricraft.api.v1.plant.IAgriPlant;
  import com.infinityraider.agricraft.api.v1.requirement.IAgriGrowthResponse;
  import com.infinityraider.agricraft.api.v1.requirement.IAgriSoil;
  import com.infinityraider.agricraft.content.tools.ItemSeedBag;
  import com.infinityraider.agricraft.reference.Names;
  import com.infinityraider.agricraft.util.CropHelper;
  import com.infinityraider.infinitylib.block.BlockBaseTile;
  import com.infinityraider.infinitylib.block.IFluidLoggable;
  import com.infinityraider.infinitylib.block.property.InfProperty;
  import com.infinityraider.infinitylib.block.property.InfPropertyConfiguration;
  import com.infinityraider.infinitylib.reference.Constants;
  import net.minecraft.MethodsReturnNonnullByDefault;
  import net.minecraft.client.renderer.RenderType;
  import net.minecraft.core.BlockPos;
  import net.minecraft.core.Direction;
  import net.minecraft.core.NonNullList;
  import net.minecraft.core.particles.ParticleTypes;
  import net.minecraft.server.level.ServerLevel;
  import net.minecraft.sounds.SoundEvents;
  import net.minecraft.sounds.SoundSource;
  import net.minecraft.util.StringRepresentable;
  import net.minecraft.world.InteractionHand;
  import net.minecraft.world.InteractionResult;
  import net.minecraft.world.InteractionResultHolder;
  import net.minecraft.world.entity.Entity;
  import net.minecraft.world.entity.LivingEntity;
  import net.minecraft.world.entity.player.Player;
  import net.minecraft.world.item.CreativeModeTab;
  import net.minecraft.world.item.ItemStack;
  import net.minecraft.world.item.Items;
  import net.minecraft.world.item.context.BlockPlaceContext;
  import net.minecraft.world.level.BlockGetter;
  import net.minecraft.world.level.Level;
  import net.minecraft.world.level.LevelReader;
  import net.minecraft.world.level.block.Block;
  import net.minecraft.world.level.block.Blocks;
  import net.minecraft.world.level.block.BonemealableBlock;
  import net.minecraft.world.level.block.SoundType;
  import net.minecraft.world.level.block.entity.BlockEntity;
  import net.minecraft.world.level.block.state.BlockState;
  import net.minecraft.world.level.material.Fluid;
  import net.minecraft.world.level.material.Material;
  import net.minecraft.world.level.storage.loot.LootContext;
  import net.minecraft.world.level.storage.loot.parameters.LootContextParams;
  import net.minecraft.world.phys.BlockHitResult;
  import net.minecraft.world.phys.HitResult;
  import net.minecraft.world.phys.shapes.BooleanOp;
  import net.minecraft.world.phys.shapes.CollisionContext;
  import net.minecraft.world.phys.shapes.Shapes;
  import net.minecraft.world.phys.shapes.VoxelShape;
  import net.minecraftforge.api.distmarker.Dist;
  import net.minecraftforge.api.distmarker.OnlyIn;
  import net.minecraftforge.common.IPlantable;
  
  import javax.annotation.Nullable;
  import javax.annotation.ParametersAreNonnullByDefault;
  import java.util.*;
  import java.util.function.BiFunction;
  import java.util.function.UnaryOperator;
  import java.util.stream.Stream;
  
  @MethodsReturnNonnullByDefault
  @ParametersAreNonnullByDefault
  public class BlockCrop extends BlockBaseTile<TileEntityCrop> implements IFluidLoggable, BonemealableBlock, IPlantable {
      /** Properties */
      protected static final InfProperty<CropStickVariant> VARIANT = InfProperty.Creators.create("stick_type", CropStickVariant.class, CropStickVariant.WOOD);
      protected static final InfProperty<CropState> STATE = InfProperty.Creators.create("crop_state", CropState.class, CropState.SINGLE_STICKS);
      protected static final InfProperty<Integer> LIGHT = InfProperty.Creators.create("light", 0, 0, 16);
  
      private static final InfPropertyConfiguration PROPERTIES = InfPropertyConfiguration.builder()
              .add(VARIANT)
              .add(STATE)
              .add(LIGHT)
              .fluidloggable()
              .build();
  
      /** Excluded classes for Item usage logic */
      private static final Class<?>[] ITEM_EXCLUDES = new Class[]{
              IAgriRakeItem.class,
              IAgriClipperItem.class,
              IAgriTrowelItem.class,
              ItemCropSticks.class,
              ItemSeedBag.class,
              ItemDebugger.class
      };
  
      /** TileEntity factory */
      private static final BiFunction<BlockPos, BlockState, TileEntityCrop> TILE_FACTORY = TileEntityCrop::new;
  
      /** Constructor */
      public BlockCrop() {
          super(Names.Blocks.CROP, Properties.of(Material.PLANT)
                  .randomTicks()
                  .noOcclusion()
                  .lightLevel(LIGHT::fetch)
                  .sound(SoundType.CROP)
          );
      }
  
      /**
       * --------------------------------
       * InfinityLib registration methods
       * --------------------------------
       */
  
      /** For TileEntity creation */
      @Override
      public BiFunction<BlockPos, BlockState, TileEntityCrop> getTileEntityFactory() {
          return TILE_FACTORY;
      }
  
      /** For BlockState definition */
      @Override
      protected InfPropertyConfiguration getPropertyConfiguration() {
          return PROPERTIES;
      }
  
      /** For render type registration */
      @Override
      @OnlyIn(Dist.CLIENT)
      public RenderType getRenderType() {
          return RenderType.cutout();
      }
  
      /**
       * ------------------------------------------
       * Block state and TileEntity utility methods
       * ------------------------------------------
       */
  
      /** TileEntity fetch utility method */
      public Optional<IAgriCrop> getCrop(BlockGetter world, BlockPos pos) {
          return AgriApi.getCrop(world, pos);
      }
  
      /** Checks if the block state has a plant or weeds */
      public boolean hasPlantOrWeeds(BlockState state) {
          return STATE.fetch(state).hasPlant();
      }
  
      /** Checks if the block state has crop sticks */
      public boolean hasCropSticks(BlockState state) {
          return STATE.fetch(state).hasSticks();
      }
  
      /** Checks if the block state has cross crop sticks */
      public boolean hasCrossSticks(BlockState state) {
          return STATE.fetch(state).isCross();
      }
  
      /** Gets the light level emitted by the block state */
      public int getLightLevel(BlockState state) {
          return LIGHT.fetch(state);
      }
  
      /** Fetches the crop stick variant if the state has crop sticks */
      public Optional<IAgriCropStickItem.Variant> getCropStickVariant(BlockState state) {
          if(this.hasCropSticks(state)) {
              return Optional.of(VARIANT.fetch(state));
          } else {
              return Optional.empty();
          }
      }
  
      /** BlockState for a plant only */
      public BlockState blockStatePlant() {
          return STATE.apply(this.defaultBlockState(), CropState.PLANT);
      }
  
      /** BlockState for crop sticks only */
      public BlockState blockStateCropSticks(CropStickVariant variant) {
          return VARIANT.apply(STATE.apply(this.defaultBlockState(), CropState.SINGLE_STICKS), variant);
      }
  
      /** Applies a plant to the block state */
      public BlockState applyPlant(BlockState state) {
          return STATE.fetch(state).addPlant(state);
      }
  
      /** Removes a plant from the block state */
      public BlockState removePlant(BlockState state) {
          return STATE.fetch(state).removePlant(state);
      }
  
      /** Sets the light level emitted by the blockstate */
      public BlockState setLightLevel(BlockState state, int level) {
          return LIGHT.apply(state, level);
      }
  
      /** Method to apply crop sticks to an existing crop */
      public InteractionResult applyCropSticks(Level world, BlockPos pos, BlockState state, @Nullable IAgriCropStickItem.Variant variant) {
          if(variant == null) {
              return InteractionResult.FAIL;
          }
          if(world.isClientSide()) {
              return InteractionResult.PASS;
          }
          BlockState newState = STATE.fetch(state).applyCropSticks(state, variant);
          if(newState == state) {
              return InteractionResult.FAIL;
          } else {
              world.setBlock(pos, newState, 3);
              if(STATE.fetch(state).hasSticks()) {
                  variant.playCropStickSound(world, pos);
              } else {
                  CropHelper.playPlantingSound(world, pos);
              }
              return InteractionResult.SUCCESS;
          }
      }
  
      /** Method to remove crop sticks from an existing crop */
      public InteractionResultHolder<IAgriCropStickItem.Variant> removeCropSticks(Level world, BlockPos pos, BlockState state) {
          if(world.isClientSide()) {
              return InteractionResultHolder.pass(VARIANT.fetch(state));
          }
          BlockState newState = STATE.fetch(state).addPlant(state);
          if(newState == state) {
              return InteractionResultHolder.fail(VARIANT.fetch(state));
          } else {
              world.setBlock(pos, newState, 3);
              if(STATE.fetch(state).isCross()) {
                  VARIANT.fetch(state).playCropStickSound(world, pos);
              }
              return InteractionResultHolder.success(VARIANT.fetch(state));
          }
      }
  
      /** Method to apply a plant to an existing crop */
      public InteractionResult applyPlant(Level world, BlockPos pos, BlockState state) {
          if(world.isClientSide()) {
              return InteractionResult.PASS;
          }
          BlockState newState = this.applyPlant(state);
          if(newState == state) {
              return InteractionResult.FAIL;
          } else {
              world.setBlock(pos, newState, 3);
              return InteractionResult.SUCCESS;
          }
      }
  
      /** Method to remove a plant from an existing crop */
      public InteractionResult removePlant(Level world, BlockPos pos, BlockState state) {
          if(world.isClientSide()) {
              return InteractionResult.PASS;
          }
          BlockState newState = this.removePlant(state);
          if(newState == state) {
              return InteractionResult.FAIL;
          } else {
              world.setBlock(pos, newState, 3);
              return InteractionResult.SUCCESS;
          }
      }
  
      /** Method to set the light level of an existing crop */
      public InteractionResult setLightLevel(Level world, BlockPos pos, BlockState state, int level) {
          if(world.isClientSide()) {
              return InteractionResult.PASS;
          }
          BlockState newState = LIGHT.apply(state, level);
          if(newState == state) {
              return InteractionResult.FAIL;
          } else {
              world.setBlock(pos, newState, 3);
              return InteractionResult.SUCCESS;
          }
      }
  
  
      /**
       * -----------------------
       * Block Behaviour methods
       * -----------------------
       */
  
      @Override
      @Deprecated
      @SuppressWarnings("deprecation")
      public void neighborChanged(BlockState state, Level world, BlockPos pos, Block block, BlockPos fromPos, boolean isMoving) {
          if(fromPos.above().equals(pos)) {
              if(!state.canSurvive(world, pos)) {
                  this.breakBlock(state, world, pos, true);
              }
          }
          super.neighborChanged(state, world, pos, block, fromPos, isMoving);
          BlockEntity tile = world.getBlockEntity(pos);
          if(tile instanceof TileEntityCrop) {
              Arrays.stream(Direction.values())
                      .filter(dir -> dir.getAxis().isHorizontal())
                      .filter(dir -> pos.relative(dir).equals(fromPos))
                      .forEach(dir -> ((TileEntityCrop) tile).onNeighbourChange(dir, fromPos, world.getBlockState(fromPos)));
          }
      }
  
      @Override
      @Deprecated
      @SuppressWarnings("deprecation")
      public void onRemove(BlockState oldState, Level world, BlockPos pos, BlockState newState, boolean isMoving) {
          // Detect fluid state change
          if(oldState.getBlock() == newState.getBlock()) {
              Fluid oldFluid = this.getFluidState(oldState).getType();
              Fluid newFluid = this.getFluidState(newState).getType();
              if(oldFluid != newFluid && this.onFluidChanged(world, pos, newState, oldFluid, newFluid)) {
                  return;
              }
          }
          // Call super
          super.onRemove(oldState, world, pos, newState, isMoving);
      }
  
      protected boolean onFluidChanged(Level world, BlockPos pos, BlockState state, Fluid oldFluid, Fluid newFluid) {
          Optional<IAgriCrop> optCrop = this.getCrop(world, pos);
          boolean noMorePlant = optCrop.map(crop -> {
              if (!crop.hasPlant()) {
                  return true;
              }
              IAgriGrowthResponse response = crop.getPlant().getGrowthRequirement(crop.getGrowthStage()).getFluidResponse(newFluid, crop.getStats().getStrength());
              if (response.killInstantly()) {
                  response.onPlantKilled(crop);
                  crop.removeGenome();
                  return true;
              }
              return false;
          }).orElse(true);
          if(VARIANT.fetch(state).canExistInFluid(newFluid)) {
              // the crop sticks remain, regardless of what happened to the plant
              return false;
          } else {
              if(noMorePlant) {
                  // no more crop sticks, no more plant, only fluid
                  world.setBlock(pos, newFluid.defaultFluidState().createLegacyBlock(), 3);
                  if(world instanceof ServerLevel) {
                      double x = pos.getX() + 0.5;
                      double y = pos.getY() + 0.5;
                      double z = pos.getZ() + 0.5;
                      for(int i = 0; i < 2; i++) {
                          world.addParticle(ParticleTypes.SMOKE,
                                  x + 0.25*world.getRandom().nextDouble(), y, z + 0.25*world.getRandom().nextDouble(),
                                  0, 1, 0);
                      }
                      world.playSound(null, x, y, z, SoundEvents.LAVA_EXTINGUISH, SoundSource.BLOCKS,
                              0.2F + world.getRandom().nextFloat() * 0.2F, 0.9F + world.getRandom().nextFloat() * 0.05F);
                  }
              } else {
                  // no more crop sticks, but still plant, and fluid
                  BlockState newState = this.defaultBlockState();
                  newState = STATE.apply(newState, CropState.PLANT);
                  newState = LIGHT.mimic(state, newState);
                  newState = InfProperty.Defaults.fluidlogged().mimic(state, newState);
                  world.setBlock(pos, newState, 3);
              }
              return true;
          }
      }
  
      @Override
      @Deprecated
      @SuppressWarnings("deprecation")
      public boolean canSurvive(BlockState state, LevelReader world, BlockPos pos) {
          return this.canSurvive(world, pos);
      }
  
      protected boolean canSurvive(LevelReader world, BlockPos pos) {
          Optional<IAgriSoil> soil = AgriApi.getSoil(world, pos.below());
          return AgriApi.getSoil(world, pos.below()).map(IAgriSoil::isSoil).orElse(false);
      }
  
      @Override
      @Nullable
      public BlockState getStateForPlacement(BlockPlaceContext context) {
          Level world = context.getLevel();
          BlockPos pos = context.getClickedPos();
          ItemStack stack = context.getItemInHand();
          BlockState state = this.defaultBlockState();
          // add a safety check
          if(!this.canSurvive(world, pos)) {
              return null;
          }
          if(stack.getItem() instanceof IAgriCropStickItem) {
              // define crop stick type
              state = this.blockStateCropSticks(CropStickVariant.fromItem(stack));
          } else if (stack.getItem() instanceof IAgriSeedItem) {
              if(AgriCraft.instance.getConfig().allowPlantingOutsideCropSticks()) {
                  // define plant type
                  state = this.blockStatePlant();
              } else {
                  // Not allowed to plant outside crop sticks
                  return null;
              }
          }
          return this.adaptStateForPlacement(state, world, pos);
      }
  
      @Nullable
      public BlockState adaptStateForPlacement(BlockState state, Level world, BlockPos pos) {
          if(state.canSurvive(world, pos)) {
              return this.fluidlog(state, world, pos);
          }
          return null;
      }
  
      @Override
      public void setPlacedBy(Level world, BlockPos pos, BlockState state, @Nullable LivingEntity entity, ItemStack stack) {
          super.setPlacedBy(world, pos, state, entity, stack);
          if(stack.getItem() instanceof IAgriGeneCarrierItem) {
              IAgriGeneCarrierItem seed = (IAgriGeneCarrierItem) stack.getItem();
              this.getCrop(world, pos).ifPresent(crop ->
                  seed.getGenome(stack).map(genome -> crop.plantGenome(genome, entity)).map(result -> {
                      if (result) {
                          // consume item
                          if (entity == null || (entity instanceof Player && !((Player) entity).isCreative()) ) {
                              stack.shrink(1);
                          }
                      }
                      return result;
                  })
              );
          }
      }
  
      public void breakBlock(BlockState state, Level world, BlockPos pos, boolean doDrops) {
          if(!world.isClientSide()) {
              if(doDrops) {
                  dropResources(state, world, pos, world.getBlockEntity(pos));
              }
              world.setBlock(pos, Blocks.AIR.defaultBlockState(), 3);
          }
      }
  
      @Override
      @Deprecated
      @SuppressWarnings("deprecation")
      public final InteractionResult use(BlockState state, Level world, BlockPos pos, Player player, InteractionHand hand, BlockHitResult hit) {
          // fetch crop
          Optional<IAgriCrop> optional = this.getCrop(world, pos);
          if (!optional.isPresent()) {
              return InteractionResult.FAIL;
          }
          IAgriCrop crop = optional.get();
          // run plant pre logic
          if(crop.hasPlant()) {
              IAgriPlant plant = crop.getPlant();
              if(plant.isPlant()) {
                  Optional<InteractionResult> result = plant.onRightClickPre(crop, player.getItemInHand(hand), player);
                  if(result.isPresent()) {
                      return result.get();
                  }
              }
          }
          // run block logic
          InteractionResult result = this.runRightClickLogic(world, pos, state, crop, player, hand, hit);
          // run plant post logic
          if(crop.hasPlant()) {
              IAgriPlant plant = crop.getPlant();
              if(plant.isPlant()) {
                  Optional<InteractionResult> override = plant.onRightClickPost(crop, player.getItemInHand(hand), player);
                  if(override.isPresent()) {
                      return override.get();
                  }
              }
          }
          return result;
      }
  
      protected InteractionResult runRightClickLogic(Level world, BlockPos pos, BlockState state, IAgriCrop crop, Player player, InteractionHand hand, BlockHitResult hit) {
          // do nothing from off hand
          if(hand == InteractionHand.OFF_HAND) {
              return InteractionResult.PASS;
          }
          ItemStack heldItem = player.getItemInHand(hand);
          // harvesting (or de-cross-crop'ing)
          if (heldItem.isEmpty()) {
              if(crop.isCrossCrop()) {
                  InteractionResultHolder<IAgriCropStickItem.Variant> result = this.removeCropSticks(world, pos, state);
                  if (result.getResult() == InteractionResult.SUCCESS) {
                      if(!player.isCreative()) {
                          this.spawnItem(crop, result.getObject().createItemStack());
                      }
                      return InteractionResult.CONSUME;
                  }
              } else {
                  return crop.harvest(stack -> this.spawnItem(crop, stack), player);
              }
          }
          // Specific item interactions
          if (TypeHelper.isAnyType(heldItem.getItem(), ITEM_EXCLUDES)) {
              return InteractionResult.PASS;
          }
          // Fertilization
          if (AgriApi.getFertilizerAdapterizer().hasAdapter(heldItem)) {
              return AgriApi.getFertilizerAdapterizer().valueOf(heldItem).map(fertilizer -> {
                  if(crop.acceptsFertilizer(fertilizer)) {
                      InteractionResult result = fertilizer.applyFertilizer(world, pos, crop, heldItem, world.getRandom(), player);
                      if(result == InteractionResult.CONSUME || result == InteractionResult.SUCCESS) {
                          crop.onApplyFertilizer(fertilizer, world.getRandom());
                      }
                      return result;
                  } else {
                      return InteractionResult.CONSUME;
                  }
              }).orElse(InteractionResult.PASS);
          }
          // Placement of crop sticks or creation of cross crop
          if(heldItem.getItem() instanceof IAgriCropStickItem) {
              InteractionResult result = this.applyCropSticks(world, pos, state, CropStickVariant.fromItem(heldItem));
              if (result == InteractionResult.SUCCESS) {
                  if (!player.isCreative()) {
                      player.getItemInHand(hand).shrink(1);
                  }
                  return result;
              }
          }
          // Planting from seed
          final BlockState prevState = state;
          if(!crop.hasPlant()) {
              if (AgriApi.getGenomeAdapterizer().hasAdapter(heldItem)) {
                  return AgriApi.getGenomeAdapterizer().valueOf(heldItem)
                          .map(seed -> {
                              if (crop.plantGenome(seed, player)) {
                                  if (!player.isCreative()) {
                                      player.getItemInHand(hand).shrink(1);
                                  }
                                  // TODO: clean up
                                  BlockState newState = STATE.apply(prevState, CropState.STICKS_PLANT);
                                  newState = LIGHT.apply(newState, crop.getPlant().getBrightness(crop));
                                  world.setBlock(pos, newState, 3);
                                  player.swing(hand);
                                  return InteractionResult.CONSUME;
                              } else {
                                  return InteractionResult.PASS;
                              }
                          })
                          .orElse(InteractionResult.PASS);
              }
          }
          // Fall Back to harvesting
          return crop.harvest(stack -> this.spawnItem(crop, stack), player);
      }
  
      @Override
      @Deprecated
      @SuppressWarnings("deprecation")
      public void randomTick(BlockState state, ServerLevel world, BlockPos pos, Random random) {
          this.getCrop(world, pos).ifPresent(IAgriCrop::applyGrowthTick);
      }
  
      @Override
      public ItemStack getCloneItemStack(BlockState state, HitResult target, BlockGetter world, BlockPos pos, Player player) {
          double x = target.getLocation().x() - pos.getX();
          double z = target.getLocation().z() - pos.getZ();
          if(x >= 3*Constants.UNIT && x <= 13*Constants.UNIT && z >= 3*Constants.UNIT && z <= 13*Constants.UNIT) {
              // prioritize the plant
              ItemStack stack = this.getSeedStack(world, pos);
              return stack.isEmpty() ? this.getCropStickStack(state) : stack;
          } else {
              // prioritize the crop sticks
              ItemStack stack = this.getCropStickStack(state);
              return stack.isEmpty() ? this.getSeedStack(world, pos) : stack;
          }
      }
  
      public ItemStack getCropStickStack(BlockState state) {
          return this.getCropStickVariant(state)
                  .map(IAgriCropStickItem.Variant::createItemStack)
                  .orElse(ItemStack.EMPTY);
      }
  
      public ItemStack getSeedStack(BlockGetter world, BlockPos pos) {
          return AgriApi.getCrop(world, pos)
                  .flatMap(IAgriCrop::getGenome)
                  .map(IAgriGenome::toSeedStack)
                  .orElse(ItemStack.EMPTY);
      }
  
      @Override
      @Deprecated
      @SuppressWarnings("deprecation")
      public List<ItemStack> getDrops(BlockState state, LootContext.Builder context) {
          List<ItemStack> drops = Lists.newArrayList();
          BlockEntity tile = context.getOptionalParameter(LootContextParams.BLOCK_ENTITY);
          if (tile == null) {
              return drops;
          }
          CropState cropState = STATE.fetch(state);
          CropStickVariant stickType = VARIANT.fetch(state);
          // add crop sticks
          boolean doStickDrops = (!AgriCraft.instance.getConfig().weedsDestroyCropSticks()) || (tile instanceof IAgriCrop) && (!((IAgriCrop) tile).hasWeeds());
          if(doStickDrops && cropState.hasSticks()) {
              drops.add(new ItemStack(stickType.getItem().asItem(), cropState.isCross() ? 2 : 1));
          }
          // add seed and drops
          if (tile instanceof IAgriCrop) {
              IAgriCrop crop = (IAgriCrop) tile;
              if (crop.hasPlant()) {
                  // add plant fruits
                  crop.getPlant().getHarvestProducts(drops::add, crop.getGrowthStage(), crop.getStats(), context.getLevel().getRandom());
                  // drop the seed
                  if(crop.getGrowthStage().canDropSeed()) {
                      crop.getGenome().map(IAgriGenome::toSeedStack).ifPresent(drops::add);
                  }
              }
          } else {
              // The TileEntity no longer exists, log an error
              AgriCraft.instance.getLogger().error("Could not find IAgriCrop instance associated with crop sticks: " + context);
          }
          return drops;
      }
  
      @Override
      @Deprecated
      @SuppressWarnings("deprecation")
      public void attack(BlockState state, Level world, BlockPos pos, Player player) {
          this.getCrop(world, pos).ifPresent(crop -> crop.breakCrop(player));
      }
  
      @Override
      @Deprecated
      @SuppressWarnings("deprecation")
      public void entityInside(BlockState state, Level world, BlockPos pos, Entity entity) {
          this.getCrop(world, pos).ifPresent(crop -> crop.getPlant().onEntityCollision(crop, entity));
      }
  
      @Override
      public SoundType getSoundType(BlockState state, LevelReader level, BlockPos pos, @Nullable Entity entity) {
          return VARIANT.fetch(state).getSound();
      }
  
      @Override
      @Deprecated
      @SuppressWarnings("deprecation")
      public boolean isSignalSource(BlockState state) {
          return STATE.fetch(state).hasPlant();
      }
  
      @Override
      @Deprecated
      @SuppressWarnings("deprecation")
      public int getSignal(BlockState state, BlockGetter world, BlockPos pos, Direction side) {
          return this.getDirectSignal(state, world, pos, side);
      }
  
      @Override
      @Deprecated
      @SuppressWarnings("deprecation")
      public boolean hasAnalogOutputSignal(BlockState state) {
          return  STATE.fetch(state).hasPlant();
      }
  
      @Override
      public int getAnalogOutputSignal(BlockState state, Level world, BlockPos pos) {
          return this.getDirectSignal(state, world, pos, null);
      }
  
      @Override
      @Deprecated
      @SuppressWarnings("deprecation")
      public int getDirectSignal(BlockState state, BlockGetter world, BlockPos pos, @Nullable Direction side) {
          return STATE.fetch(state).hasPlant()
                  ? this.getCrop(world, pos).map(crop -> crop.getPlant().getRedstonePower(crop)).orElse(0)
                  : 0;
      }
  
      public void spawnItem(IAgriCrop crop, ItemStack stack) {
          Level world = crop.world();
          if(world != null) {
              this.spawnItem(world, crop.getPosition(), stack);
          }
      }
  
      @Override
      @OnlyIn(Dist.CLIENT)
      public void animateTick(BlockState state, Level world, BlockPos pos, Random rand) {
          this.getCrop(world, pos).ifPresent(crop -> {
              if(crop.hasPlant()) {
                  crop.getPlant().spawnParticles(crop, rand);
              }
          });
      }
  
      @Override
      public void fillItemCategory(CreativeModeTab tab, NonNullList<ItemStack> items) {
          // override to prevent filling of creative tab
      }
  
  
      /**
       * ---------------
       * Hit box methods
       * --------------
       */
  
      @Override
      @Deprecated
      @SuppressWarnings("deprecation")
      public VoxelShape getOcclusionShape(BlockState state, BlockGetter world, BlockPos pos) {
          return this.getShape(state, world, pos, CollisionContext.empty());
      }
  
      @Override
      @Deprecated
      @SuppressWarnings("deprecation")
      public VoxelShape getBlockSupportShape(BlockState state, BlockGetter world, BlockPos pos) {
          return this.getCollisionShape(state, world, pos, CollisionContext.empty());
      }
  
      @Override
      @Deprecated
      @SuppressWarnings("deprecation")
      public VoxelShape getInteractionShape(BlockState state, BlockGetter world, BlockPos pos) {
          return this.getVisualShape(state, world, pos, CollisionContext.empty());
      }
  
      @Override
      @Deprecated
      @SuppressWarnings("deprecation")
      public VoxelShape getShape(BlockState state, BlockGetter world, BlockPos pos, CollisionContext context) {
          CropState cropState = STATE.fetch(state);
          if(cropState.isCross()) {
              return HitBoxes.CROSS_STICKS;
          } else if(cropState.hasPlant()) {
              int height = this.getCrop(world, pos)
                      .map(crop -> Math.max(
                              crop.getPlant().getPlantHeight(crop.getGrowthStage()),
                              crop.getWeeds().getPlantHeight(crop.getWeedGrowthStage())))
                      .map(Double::intValue)
                      .orElse(0);
              if (cropState.hasSticks()) {
                  return HitBoxes.getStickPlantShape(height);
              } else {
                  return HitBoxes.getPlantShape(height);
              }
          } else {
              return HitBoxes.SINGLE_STICKS;
          }
      }
  
      @Override
      @Deprecated
      @SuppressWarnings("deprecation")
      public VoxelShape getCollisionShape(BlockState state, BlockGetter world, BlockPos pos, CollisionContext context) {
          if(AgriCraft.instance.getConfig().cropSticksCollide()) {
              if(STATE.fetch(state).hasSticks()) {
                  return STATE.fetch(state).isCross() ? HitBoxes.CROSS_STICKS : HitBoxes.SINGLE_STICKS;
              }
          }
          return Shapes.empty();
      }
  
      @Override
      @Deprecated
      @SuppressWarnings("deprecation")
      public VoxelShape getVisualShape(BlockState state, BlockGetter world, BlockPos pos, CollisionContext context) {
          return this.getShape(state, world, pos, context);
      }
  
      /**
       * ---------------------------------
       * Vanilla BonemealableBlock methods
       * ---------------------------------
       */
  
      @Override
      public boolean isValidBonemealTarget(BlockGetter world, BlockPos pos, BlockState state, boolean isClient) {
          return this.getCrop(world, pos).map(crop -> crop.isFertile() && !crop.isFullyGrown()).orElse(true);
      }
  
      private static final ItemStack BONE_MEAL = new ItemStack(Items.BONE_MEAL);
  
      @Override
      public boolean isBonemealSuccess(Level world, Random rand, BlockPos pos, BlockState state) {
          return AgriApi.getFertilizer(BONE_MEAL).flatMap(fertilizer ->
                  this.getCrop(world, pos).map(crop ->
                          !crop.isFullyGrown()
                                  && crop.acceptsFertilizer(fertilizer)))
                  .orElse(false);
      }
  
      @Override
      public void performBonemeal(ServerLevel world, Random rand, BlockPos pos, BlockState state) {
          AgriApi.getFertilizer(BONE_MEAL).ifPresent(fertilizer ->
                  this.getCrop(world, pos).ifPresent(crop ->
                          fertilizer.applyFertilizer(world, pos, crop, BONE_MEAL, rand, null)));
      }
  
      /**
       * ------------------------
       * Forge IPlantable methods
       * ------------------------
       */
  
      @Override
      public BlockState getPlant(BlockGetter world, BlockPos pos) {
          BlockState state = world.getBlockState(pos);
          if(world instanceof Level) {
              return this.getCrop(world, pos)
                      .flatMap(crop -> crop.getPlant().asBlockState(crop.getGrowthStage()))
                      .orElse(state);
          }
          return state;
      }
  
      /**
       * -------------------------
       * Utility enums and classes
       * -------------------------
       */
  
      /** enum representing crop stick and plant state */
      protected enum CropState implements StringRepresentable {
          PLANT(true, false,
                  // add crop sticks function
                  (state, variant) -> {
                      state = STATE.apply(state, sticksPlant());
                      return VARIANT.apply(state, variant);
                  },
                  // remove crop sticks function
                  UnaryOperator.identity(),
                  // add plant function
                  UnaryOperator.identity(),
                  // remove plant function
                  state -> Blocks.AIR.defaultBlockState()
          ),
  
          SINGLE_STICKS(false, true,
                  // add crop sticks function
                  (state, variant) -> {
                      if(VARIANT.fetch(state) == variant) {
                          return STATE.apply(state, doubleSticks());
                      } else {
                          return state;
                      }
                  },
                  // remove crop sticks function
                  state -> Blocks.AIR.defaultBlockState(),
                  // add plant function
                  state -> STATE.apply(state, sticksPlant()),
                  // remove plant function
                  UnaryOperator.identity()
          ),
  
          DOUBLE_STICKS(false, true,
                  // add crop sticks function
                  (state, variant) -> state,
                  // remove crop sticks function
                  state -> STATE.apply(state, SINGLE_STICKS),
                  // add plant function
                  state -> STATE.apply(state, sticksPlant()),
                  // remove plant function
                  UnaryOperator.identity()
          ),
  
          STICKS_PLANT(true, true,
                  // add crop sticks function
                  (state, variant) -> state,
                  // remove crop sticks function
                  state -> STATE.apply(state, PLANT),
                  // add plant function
                  UnaryOperator.identity(),
                  // remove plant function
                  state -> STATE.apply(state, SINGLE_STICKS)
          );
  
          private final boolean plant;
          private final boolean sticks;
  
          private final BiFunction<BlockState, CropStickVariant, BlockState> addCropSticksFunction;
          private final UnaryOperator<BlockState> removeCropSticksFunction;
          private final UnaryOperator<BlockState> addPlantFunction;
          private final UnaryOperator<BlockState> removePlantFunction;
  
          CropState(boolean plant, boolean sticks,
                    BiFunction<BlockState, CropStickVariant, BlockState> addCropSticksFunction,
                    UnaryOperator<BlockState> removeCropSticksFunction,
                    UnaryOperator<BlockState> addPlantFunction,
                    UnaryOperator<BlockState> removePlantFunction) {
              this.plant = plant;
              this.sticks = sticks;
              this.addCropSticksFunction = addCropSticksFunction;
              this.removeCropSticksFunction = removeCropSticksFunction;
              this.addPlantFunction = addPlantFunction;
              this.removePlantFunction = removePlantFunction;
          }
  
          public boolean hasPlant() {
              return this.plant;
          }
  
          public boolean hasSticks() {
              return this.sticks;
          }
  
          public boolean isCross() {
              return this == DOUBLE_STICKS;
          }
  
          public BlockState applyCropSticks(BlockState state, IAgriCropStickItem.Variant variant) {
              if(variant instanceof CropStickVariant) {
                  return this.addCropSticksFunction.apply(state, (CropStickVariant) variant);
              } else {
                  AgriCraft.instance.getLogger().error("Attempted to apply unregistered crop stick variant to crop sticks: " + variant.getId());
                  return state;
              }
          }
  
          public BlockState removeCropSticks(BlockState state) {
              return this.removeCropSticksFunction.apply(state);
          }
  
          public BlockState addPlant(BlockState state) {
              return this.addPlantFunction.apply(state);
          }
  
          public BlockState removePlant(BlockState state) {
              return this.removePlantFunction.apply(state);
          }
  
          @Override
          public String getSerializedName() {
              return this.name().toLowerCase();
          }
  
          // helper method to prevent illegal forward references in the constructors
          private static CropState sticksPlant() {
              return STICKS_PLANT;
          }
  
          // helper method to prevent illegal forward references in the constructors
          private static CropState doubleSticks() {
              return DOUBLE_STICKS;
          }
      }
  
      /** Inner class for voxel shapes */
      protected static class HitBoxes {
          public static final VoxelShape SINGLE_STICKS = Stream.of(
                  Block.box(2, -3, 2, 3, 14, 3),
                  Block.box(13, -3, 2, 14, 14, 3),
                  Block.box(2, -3, 13, 3, 14, 14),
                  Block.box(13, -3, 13, 14, 14, 14)
          ).reduce((v1, v2) -> Shapes.join(v1, v2, BooleanOp.OR)).get();
  
          public static final VoxelShape CROSS_STICKS = Stream.of(
                  SINGLE_STICKS,
                  Block.box(0, 11, 2, 16, 12, 3),
                  Block.box(0, 11, 13, 16, 12, 14),
                  Block.box(2, 11, 0, 3, 12, 16),
                  Block.box(13, 11, 0, 14, 12, 16)
          ).reduce((v1, v2) -> Shapes.join(v1, v2, BooleanOp.OR)).get();
  
          private static final Map<Integer, VoxelShape> PLANTS = Maps.newHashMap();
  
          public static final Map<Integer, VoxelShape> STICK_PLANTS = Maps.newHashMap();
  
          public static VoxelShape getPlantShape(int height) {
              return PLANTS.computeIfAbsent(height, h -> Optional.of(Block.box(3, 0, 3, 13, h, 13)).get());
          }
  
          public static VoxelShape getStickPlantShape(int height) {
              return STICK_PLANTS.computeIfAbsent(height, h -> Stream.of(
                      SINGLE_STICKS,
                     getPlantShape(height)
              ).reduce((v1, v2) -> Shapes.join(v1, v2, BooleanOp.OR)).get());
          }
      }
  }
  