package com.infinityraider.agricraft.content.core;

import com.google.common.base.Preconditions;
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.IAgriCropStickItem;
import com.infinityraider.agricraft.api.v1.content.items.IAgriRakeItem;
import com.infinityraider.agricraft.api.v1.crop.CropCapability;
import com.infinityraider.agricraft.api.v1.crop.IAgriCrop;
import com.infinityraider.agricraft.api.v1.crop.IAgriGrowthStage;
import com.infinityraider.agricraft.api.v1.event.AgriCropEvent;
import com.infinityraider.agricraft.api.v1.fertilizer.IAgriFertilizer;
import com.infinityraider.agricraft.api.v1.genetics.IAgriGenome;
import com.infinityraider.agricraft.api.v1.plant.IAgriGrowable;
import com.infinityraider.agricraft.api.v1.plant.IAgriPlant;
import com.infinityraider.agricraft.api.v1.plant.IAgriWeed;
import com.infinityraider.agricraft.api.v1.requirement.IAgriGrowthResponse;
import com.infinityraider.agricraft.api.v1.requirement.IAgriSoil;
import com.infinityraider.agricraft.api.v1.stat.IAgriStatProvider;
import com.infinityraider.agricraft.api.v1.stat.IAgriStatsMap;
import com.infinityraider.agricraft.content.AgriTileRegistry;
import com.infinityraider.agricraft.impl.v1.CoreHandler;
import com.infinityraider.agricraft.impl.v1.PluginHandler;
import com.infinityraider.agricraft.impl.v1.crop.NoGrowth;
import com.infinityraider.agricraft.impl.v1.plant.NoPlant;
import com.infinityraider.agricraft.impl.v1.plant.NoWeed;
import com.infinityraider.agricraft.impl.v1.requirement.RequirementCache;
import com.infinityraider.agricraft.impl.v1.stats.NoStats;
import com.infinityraider.agricraft.reference.AgriNBT;
import com.infinityraider.agricraft.util.CropHelper;
import com.infinityraider.infinitylib.block.tile.TileEntityBase;
import com.infinityraider.infinitylib.utility.debug.IDebuggable;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.Component;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.item.ItemEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.material.FluidState;
import net.minecraftforge.client.model.data.ModelDataMap;
import net.minecraftforge.client.model.data.ModelProperty;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.common.capabilities.Capability;
import net.minecraftforge.common.util.LazyOptional;
import net.minecraftforge.event.world.BlockEvent;
import net.minecraftforge.eventbus.api.Event;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Random;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Stream;

public class TileEntityCrop  extends TileEntityBase implements IAgriCrop, IDebuggable {
    // Model properties
    public static final ModelProperty<IAgriPlant> PROPERTY_PLANT = new ModelProperty<>();
    public static final ModelProperty<IAgriGrowthStage> PROPERTY_PLANT_GROWTH = new ModelProperty<>();
    public static final ModelProperty<IAgriWeed> PROPERTY_WEED = new ModelProperty<>();
    public static final ModelProperty<IAgriGrowthStage> PROPERTY_WEED_GROWTH = new ModelProperty<>();

    // Default instances
    private static final IAgriPlant NO_PLANT = NoPlant.getInstance();
    private static final IAgriGrowthStage NO_GROWTH = NoGrowth.getInstance();
    private static final IAgriWeed NO_WEED = NoWeed.getInstance();
    private static final IAgriStatsMap NO_STATS = NoStats.getInstance();

    // NBT readers and writers
    public static final BiConsumer<Optional<IAgriGenome>, CompoundTag> GENOME_WRITER = (optional, tag) -> optional.ifPresent(genome -> {
        CompoundTag geneTag = new CompoundTag();
        genome.writeToNBT(geneTag);
        tag.put(AgriNBT.GENOME, geneTag);
    });
    public static final Function<CompoundTag, Optional<IAgriGenome>> GENOME_READER = (tag) -> {
        if (tag.contains(AgriNBT.GENOME)) {
            IAgriGenome genome = AgriApi.getAgriGenomeBuilder(NO_PLANT).build();
            genome.readFromNBT(tag.getCompound(AgriNBT.GENOME));
            return Optional.of(genome);
        } else {
            return Optional.empty();
        }
    };
    public static final BiConsumer<IAgriGrowthStage, CompoundTag> GROWTH_WRITER = (growth, tag) -> tag.putString(AgriNBT.GROWTH, growth.getId());
    public static final Function<CompoundTag, IAgriGrowthStage> GROWTH_READER = (tag) -> AgriApi.getGrowthStageRegistry().get(tag.getString(AgriNBT.GROWTH)).orElse(NO_GROWTH);
    public static final BiConsumer<IAgriWeed, CompoundTag> WEED_WRITER = (weed, tag) -> tag.putString(AgriNBT.WEED, weed.getId());
    public static final Function<CompoundTag, IAgriWeed> WEED_READER = (tag) -> AgriApi.getWeedRegistry().get(tag.getString(AgriNBT.WEED)).orElse(NO_WEED);
    public static final BiConsumer<IAgriGrowthStage, CompoundTag> WEED_GROWTH_WRITER = (growth, tag) -> tag.putString(AgriNBT.WEED_GROWTH, growth.getId());
    public static final Function<CompoundTag, IAgriGrowthStage> WEED_GROWTH_READER = (tag) -> AgriApi.getGrowthStageRegistry().get(tag.getString(AgriNBT.WEED_GROWTH)).orElse(NO_GROWTH);

    // Model data
    private final ModelDataMap data;
    // Auto synced fields
    private final AutoSyncedField<Optional<IAgriGenome>> genome;
    private final AutoSyncedField<IAgriGrowthStage> growth;
    private final AutoSyncedField<IAgriWeed> weed;
    private final AutoSyncedField<IAgriGrowthStage> weedGrowth;
    // Growth Requirements
    private RequirementCache requirement;

    // Capabilities
    private final LazyOptional<IAgriCrop> cropCapability;

    // Cache for neighbouring crops
    private final Map<Direction, Optional<IAgriCrop>> neighbours;
    private boolean needsCaching;

    public TileEntityCrop(BlockPos pos, BlockState state) {
        // Super constructor with appropriate TileEntity Type
        super(AgriTileRegistry.getInstance().crop.get(), pos, state);

        // Initialize model data map
        this.data = new ModelDataMap.Builder()
                .withInitial(PROPERTY_PLANT, NO_PLANT)
                .withInitial(PROPERTY_PLANT_GROWTH, NO_GROWTH)
                .withInitial(PROPERTY_WEED, NO_WEED)
                .withInitial(PROPERTY_WEED_GROWTH, NO_GROWTH)
                .build();

        // Initialize automatically synced fields
        this.genome = this.createAutoSyncedField(
                Optional.empty(),
                GENOME_WRITER,
                GENOME_READER,
                CoreHandler::isInitialized,
                Optional.empty());

        this.growth = this.getAutoSyncedFieldBuilder(NO_GROWTH,
                GROWTH_WRITER,
                GROWTH_READER,
                CoreHandler::isInitialized,
                NO_GROWTH).withCallBack((stage) -> {
            this.getModelData().setData(PROPERTY_PLANT_GROWTH, stage);
            this.requirement = RequirementCache.create(this);
        }).withRenderUpdate().build();

        this.weed = this.getAutoSyncedFieldBuilder(NO_WEED,
                WEED_WRITER,
                WEED_READER,
                CoreHandler::isInitialized,
                NO_WEED).withCallBack(weed -> this.getModelData().setData(PROPERTY_WEED, weed)).withRenderUpdate().build();

        this.weedGrowth = this.getAutoSyncedFieldBuilder(NO_GROWTH,
                WEED_GROWTH_WRITER,
                WEED_GROWTH_READER,
                CoreHandler::isInitialized,
                NO_GROWTH).withCallBack(stage -> this.getModelData().setData(PROPERTY_WEED_GROWTH, stage)).withRenderUpdate().build();

        // Initialize growth requirement cache
        this.requirement = RequirementCache.create(this);
        this.cropCapability = LazyOptional.of(() -> this);

        // Initialize neighbour cache
        this.neighbours = Maps.newEnumMap(Direction.class);
        Direction.Plane.HORIZONTAL.stream().forEach(dir -> neighbours.put(dir, Optional.empty()));
        this.needsCaching = true;
    }

    public BlockCrop getBlock() {
        return AgriCraft.instance.getModBlockRegistry().getCropBlock();
    }

    @Override
    public void dropItem(ItemStack item) {
        if(this.getLevel() == null || this.getLevel().isClientSide()) {
            return;
        }
        this.getLevel().addFreshEntity(new ItemEntity(
                this.getLevel(),
                this.getPosition().getX(),
                this.getPosition().getY(),
                this.getPosition().getZ(),
                item));
    }

    @Override
    public boolean isValid() {
        return this.getLevel() != null && !this.isRemoved();
    }

    @Override
    @Nonnull
    public BlockPos getPosition() {
        return this.getBlockPos();
    }

    @Nonnull
    @Override
    public FluidState getFluidState() {
        return this.getBlockState().getFluidState();
    }

    @Override
    @Nonnull
    public IAgriGrowthStage getGrowthStage() {
        return this.growth.get();
    }

    @Override
    public boolean setGrowthStage(@Nonnull IAgriGrowthStage stage) {
        if(this.growth.get().equals(stage)) {
            return false;
        }
        if(!this.getPlant().getGrowthStages().contains(stage)) {
            return false;
        }
        if(!this.checkGrowthSpace(this.getPlant(), stage)) {
            return false;
        }
        this.growth.set(stage);
        this.handlePlantUpdate();
        return true;
    }

    @Override
    public boolean hasCropSticks() {
        return this.getBlock().hasCropSticks(this.getBlockState());
    }

    @Override
    public boolean isCrossCrop() {
        return this.getBlock().hasCrossSticks(this.getBlockState());
    }

    @Override
    public Optional<IAgriCropStickItem.Variant> getCropStickVariant() {
        return this.getBlock().getCropStickVariant(this.getBlockState());
    }

    @Override
    public boolean applyCropStick(IAgriCropStickItem.Variant variant) {
        Level world = this.getLevel();
        if(world == null) {
            return false;
        }
        return this.getBlock().applyCropSticks(world, this.getBlockPos(), this.getBlockState(), variant) == InteractionResult.SUCCESS;
    }

    @Override
    public Optional<IAgriCropStickItem.Variant> removeCropStick() {
        Level world = this.getLevel();
        if(world == null) {
            return Optional.empty();
        }
        return Optional.of(this.getBlock().removeCropSticks(world, this.getBlockPos(), this.getBlockState()))
                .flatMap(result -> result.getResult() == InteractionResult.SUCCESS ? Optional.of(result.getObject()) : Optional.empty());
    }

    protected boolean checkGrowthSpace(IAgriGrowable plant, IAgriGrowthStage stage) {
        return CropHelper.checkGrowthSpace(this.getLevel(),this.getBlockPos(), plant, stage);
    }

    @Override
    public IAgriGrowthResponse getFertilityResponse() {
        if(this.getLevel() == null) {
            return IAgriGrowthResponse.INFERTILE;
        }
        if(!this.checkGrowthSpace(this.getPlant(), this.getGrowthStage())) {
            return IAgriGrowthResponse.INFERTILE;
        }
        return this.requirement.check();
    }

    @Override
    public boolean isMature() {
        return this.getGrowthStage().isMature();
    }

    @Override
    public boolean isFullyGrown() {
        return this.getGrowthStage().isFinal();
    }

    @Nonnull
    @Override
    public Optional<IAgriSoil> getSoil() {
        return Optional.ofNullable(this.getLevel())
                .flatMap(world -> AgriApi.getSoil(world, this.getPosition().below()));
    }

    @Override
    public void breakCrop(@Nullable LivingEntity entity) {
        if(this.getLevel() == null) {
            return;
        }
        if(!MinecraftForge.EVENT_BUS.post(new AgriCropEvent.Break.Pre(this, entity))) {
            IAgriPlant plant = this.getPlant();
            IAgriWeed weed = this.getWeeds();
            Block.dropResources(this.getBlockState(), this.getLevel(), this.getPosition(), this);
            this.getLevel().setBlock(this.getPosition(), Blocks.AIR.defaultBlockState(), 3);
            plant.onBroken(this, entity);
            weed.onBroken(this, entity);
            MinecraftForge.EVENT_BUS.post(new AgriCropEvent.Break.Post(this, entity));
        }
    }

    // Use neighbour cache instead to prevent having to read TileEntities from the world
    @Nonnull
    @Override
    public Stream<IAgriCrop> streamNeighbours() {
        if(this.needsCaching) {
            this.readNeighbours();
        }
        return this.neighbours.values().stream()
                .filter(Optional::isPresent)
                .map(Optional::get)
                .filter(IAgriCrop::hasCropSticks);
    }

    // Initialize neighbours cache
    protected void readNeighbours() {
        if(this.getLevel() != null) {
            Direction.Plane.HORIZONTAL.stream().forEach(dir -> neighbours.put(dir, AgriApi.getCrop(this.getLevel(), this.getBlockPos().relative(dir))));
            this.needsCaching = false;
        }
    }

    // Update neighbour cache
    protected void onNeighbourChange(Direction direction, BlockPos pos, BlockState newState) {
        if(newState.getBlock() instanceof BlockCrop) {
            if(this.getLevel() != null) {
                this.neighbours.put(direction, AgriApi.getCrop(this.getLevel(), pos));
            }
        } else {
            this.neighbours.put(direction, Optional.empty());
        }
    }

    @Override
    public void applyGrowthTick() {
        if (this.getLevel() == null || this.getLevel().isClientSide()) {
            return;
        }
        if(!MinecraftForge.EVENT_BUS.post(new AgriCropEvent.Grow.General.Pre(this))) {
            // Decide if the weeds receives the growth tick or not
            if (this.rollForWeedAction()) {
                // Weeds have the word
                this.executeWeedGrowthTick();
            } else if (this.isCrossCrop()) {
                // mutation tick
                this.executeCrossGrowthTick();
            } else {
                IAgriGrowthResponse fertility = this.getFertilityResponse();
                if(fertility.killInstantly()) {
                    // kill plant
                    fertility.onPlantKilled(this);
                    this.removeGenome();
                } else if(fertility.isLethal()) {
                    // reverse growth stage
                    this.revertGrowthStage();
                } else if(fertility.isFertile()) {
                    // plant growth tick
                    this.executePlantGrowthTick();
                }
            }
            MinecraftForge.EVENT_BUS.post(new AgriCropEvent.Grow.General.Post(this));
        }
    }

    // method to allow for overriding
    protected boolean rollForWeedAction() {
        return CropHelper.rollForWeedAction(this);
    }

    protected void executeWeedGrowthTick() {
        if(!MinecraftForge.EVENT_BUS.post(new AgriCropEvent.Grow.Weeds.Pre(this))) {
            if (!this.hasWeeds()) {
                //The aren't weeds yet, try to spawn new weeds
                this.spawnWeeds();
            } else {
                // There are weeds already, apply the growth tick
                if (this.getWeedGrowthStage().isFinal()) {
                    //Weeds are mature, try killing the plant
                    this.tryWeedKillPlant();
                    //Weeds are mature, try spreading
                    this.spreadWeeds();
                } else {
                    // Weeds are not mature yet, increment their growth
                    double f = this.getSoil().map(IAgriSoil::getGrowthModifier).orElse(1.0D);
                    if(this.getRandom().nextDouble() < f*this.getWeeds().getGrowthChance(this.getWeedGrowthStage())) {
                        this.setWeed(this.getWeeds(), this.getWeedGrowthStage().getNextStage(this, this.getRandom()));
                    }
                }
            }
            MinecraftForge.EVENT_BUS.post(new AgriCropEvent.Grow.Weeds.Post(this));
        }
    }

    // Method to allow for overrides
    protected void spawnWeeds() {
        CropHelper.spawnWeeds(this);
    }

    // Method to allow for overrides
    protected void spreadWeeds() {
        CropHelper.spreadWeeds(this);
    }

    protected void tryWeedKillPlant() {
        if(AgriCraft.instance.getConfig().allowLethalWeeds() && this.getWeeds().isLethal()) {
            if(this.hasPlant() && this.rollForWeedAction()) {
                this.revertGrowthStage();
            }
        }
    }

    protected void revertGrowthStage() {
        IAgriGrowthStage current = this.getGrowthStage();
        IAgriGrowthStage previous = current.getPreviousStage(this, this.getRandom());
        if(current.equals(previous)) {
            this.removeGenome();
        } else {
            this.setGrowthStage(previous);
        }
    }

    protected void executeCrossGrowthTick() {
        // Do not do mutation growth ticks if the plant has weeds
        if(!this.hasWeeds() && !MinecraftForge.EVENT_BUS.post(new AgriCropEvent.Grow.Cross.Pre(this))) {
            if(AgriApi.getAgriMutationHandler().getActiveCrossBreedEngine().handleCrossBreedTick(this, this.streamNeighbours(), this.getRandom())) {
                MinecraftForge.EVENT_BUS.post(new AgriCropEvent.Grow.Cross.Post(this));
            }
        }
    }

    protected void executePlantGrowthTick() {
        if (!this.getGrowthStage().isFinal()) {
            BlockState state = this.getBlockState();
            if (this.calculateGrowthRate() > this.getRandom().nextDouble() && !MinecraftForge.EVENT_BUS.post(new AgriCropEvent.Grow.Plant.Pre(this))) {
                // also do the MinecraftForge BlockEvent for crop growth
                BlockEvent.CropGrowEvent.Pre blockEvent = new BlockEvent.CropGrowEvent.Pre(this.getLevel(), this.getBlockPos(), state);
                MinecraftForge.EVENT_BUS.post(blockEvent);
                if(blockEvent.getResult() == Event.Result.ALLOW || blockEvent.getResult() == Event.Result.DEFAULT) {
                    this.setGrowthStage(this.getGrowthStage().getNextStage(this, this.getRandom()));
                    this.getPlant().onGrowth(this);
                    MinecraftForge.EVENT_BUS.post(new AgriCropEvent.Grow.Plant.Post(this));
                    MinecraftForge.EVENT_BUS.post(new BlockEvent.CropGrowEvent.Post(this.getLevel(), this.getBlockPos(), state, state));
                }
            }
        }
    }

    protected double calculateGrowthRate() {
        int growth = this.getStats().getGrowth();
        double soilFactor = this.getSoil().map(IAgriSoil::getGrowthModifier).orElse(1.0D);
        return soilFactor * (this.getPlant().getGrowthChanceBase(this.getGrowthStage())
                + growth * this.getPlant().getGrowthChanceBonus(this.getGrowthStage()) * AgriCraft.instance.getConfig().growthMultiplier());
    }

    @Override
    @Nonnull
    public Optional<IAgriGenome> getGenome() {
        return this.genome.get();
    }

    @Nonnull
    @Override
    public IAgriStatsMap getStats() {
        return this.getGenome().map(IAgriStatProvider::getStats).orElse(NO_STATS);
    }

    @Override
    public boolean acceptsFertilizer(@Nonnull IAgriFertilizer fertilizer) {
        Objects.requireNonNull(fertilizer);
        if(this.isCrossCrop()) {
            return AgriCraft.instance.getConfig().allowFertilizerMutations() && fertilizer.canTriggerMutation();
        } else if(this.hasPlant()) {
            return !this.isFullyGrown()
                    && fertilizer.canFertilize(this)
                    && this.getPlant().isFertilizable(this.getGrowthStage(), fertilizer);
        } else {
            return fertilizer.canTriggerWeeds();
        }
    }

    @Override
    public void onApplyFertilizer(@Nonnull IAgriFertilizer fertilizer, @Nonnull Random rand) {

    }

    @Override
    public boolean canBeHarvested(@Nullable LivingEntity entity) {
        return this.getPlant().allowsHarvest(this.getGrowthStage(), entity);
    }

    @Nonnull
    @Override
    public InteractionResult harvest(@Nonnull Consumer<ItemStack> consumer, LivingEntity entity) {
        if (this.getLevel() != null && this.canBeHarvested(entity)) {
            if(!MinecraftForge.EVENT_BUS.post(new AgriCropEvent.Harvest.Pre(this, entity))) {
                CropHelper.executePlantHarvestRolls(this, consumer);
                this.setGrowthStage(this.getPlant().getGrowthStageAfterHarvest());
                this.getPlant().onHarvest(this, entity);
                MinecraftForge.EVENT_BUS.post(new AgriCropEvent.Harvest.Post(this, entity));
                return InteractionResult.SUCCESS;
            }
        }
        return InteractionResult.PASS;
    }

    @Override
    public boolean canBeRaked(@Nonnull IAgriRakeItem item, @Nonnull ItemStack stack, @Nullable LivingEntity entity) {
        return this.getWeeds().isWeed();
    }

    @Override
    public boolean rake(@Nonnull Consumer<ItemStack> consumer, @Nullable LivingEntity entity) {
        if(this.getLevel() == null || this.getLevel().isClientSide()) {
            return false;
        }
        IAgriWeed weed = this.getWeeds();
        if (weed.isWeed()) {
            IAgriGrowthStage stage = this.getWeedGrowthStage();
            this.setWeed(NO_WEED, NO_GROWTH);
            weed.onRake(stage, consumer, this.getRandom(), entity);
            return true;
        }
        return false;
    }

    @Override
    public boolean hasPlant() {
        return this.getGenome().map(IAgriGenome::getPlant).map(IAgriPlant::isPlant).orElse(false);
    }

    @Nonnull
    @Override
    public IAgriPlant getPlant() {
        return this.getGenome().map(IAgriGenome::getPlant).orElse(NO_PLANT);
    }

    @Override
    public boolean hasWeeds() {
        return this.weed.get().isWeed();
    }

    @Nonnull
    @Override
    public IAgriWeed getWeeds() {
        return this.weed.get();
    }

    @Nonnull
    @Override
    public IAgriGrowthStage getWeedGrowthStage() {
        return this.weedGrowth.get();
    }

    @Override
    public boolean setWeed(@Nonnull IAgriWeed weed, @Nonnull IAgriGrowthStage stage) {
        if(this.getWeeds().equals(weed)) {
            if(this.weedGrowth.get().equals(stage)) {
                return false;
            }
            if(this.getWeeds().getGrowthStages().contains(stage) && this.checkGrowthSpace(this.getWeeds(), stage)) {
                this.weedGrowth.set(stage);
                this.getWeeds().onGrowthTick(this);
                this.handlePlantUpdate();
                return true;
            }
            return false;
        } else if(weed.getGrowthStages().contains(stage) && this.checkGrowthSpace(this.getWeeds(), stage)) {
            if(!MinecraftForge.EVENT_BUS.post(new AgriCropEvent.Spawn.Weed.Pre(this, weed))) {
                this.setWeedImpl(weed, stage);
                MinecraftForge.EVENT_BUS.post(new AgriCropEvent.Spawn.Weed.Post(this, weed));
                return true;
            }
        }
        return false;
    }

    protected void setWeedImpl(@Nonnull IAgriWeed weed, @Nonnull IAgriGrowthStage stage) {
        this.weed.set(weed);
        this.weedGrowth.set(stage);
        this.getWeeds().onSpawned(this);
        this.handlePlantUpdate();
    }

    @Override
    public boolean removeWeed() {
        if(this.hasWeeds()) {
            this.weed.set(NO_WEED);
            this.weedGrowth.set(NO_GROWTH);
            this.handlePlantUpdate();
            return true;
        }
        return false;
    }

    @Override
    public boolean acceptsGenome(@Nonnull IAgriGenome genome) {
        return !this.hasGenome() && !this.isCrossCrop();
    }

    @Override
    public boolean spawnGenome(@Nonnull IAgriGenome genome) {
        if(!this.hasPlant() && !MinecraftForge.EVENT_BUS.post(new AgriCropEvent.Spawn.Plant.Pre(this, genome))) {
            this.setGenomeImpl(genome);
            this.getPlant().onSpawned(this);
            MinecraftForge.EVENT_BUS.post(new AgriCropEvent.Spawn.Plant.Post(this, genome));
            return true;
        }
        return false;
    }

    @Override
    public boolean plantGenome(@Nonnull IAgriGenome genome) {
        return this.plantGenome(genome, null);
    }

    @Override
    public boolean plantGenome(@Nonnull IAgriGenome genome, @Nullable LivingEntity entity) {
        if (this.acceptsGenome(genome) && !MinecraftForge.EVENT_BUS.post(new AgriCropEvent.Plant.Pre(this, genome, entity))) {
            this.setGenomeImpl(genome);
            if(this.getLevel() != null) {
                CropHelper.playPlantingSound(this.getLevel(), this.getBlockPos(), (entity instanceof Player) ? (Player) entity : null);
            }
            this.getPlant().onPlanted(this, entity);
            MinecraftForge.EVENT_BUS.post(new AgriCropEvent.Plant.Post(this, genome, entity));
            return true;
        }
        return false;
    }

    protected void setGenomeImpl(@Nonnull IAgriGenome genome) {
        this.genome.set(Optional.of(genome));
        this.growth.set(genome.getPlant().getInitialGrowthStage());
        this.handlePlantUpdate();
    }

    @Override
    public boolean removeGenome() {
        if(this.hasGenome()) {
            this.getPlant().onRemoved(this);
            this.genome.set(Optional.empty());
            this.handlePlantUpdate();
            return true;
        }
        return false;
    }

    @Override
    public boolean hasGenome() {
        return this.hasPlant();
    }

    @Override
    protected void writeTileNBT(@Nonnull CompoundTag tag) {
        // No need to write anything since everything is covered by the AutoSyncedFields
    }

    @Override
    protected void readTileNBT(@Nonnull CompoundTag tag) {
        // A cache update will be required though (either on the client, or on the server after being loaded)
        this.needsCaching = true;
    }

    protected void handlePlantUpdate()  {
        if(this.getLevel() != null) {
            BlockState oldState = this.getBlockState();
            BlockState newState = oldState;
            // Update brightness
            newState = this.getBlock().setLightLevel(newState, this.getPlant().getBrightness(this));
            // Update plant
            newState = this.hasPlant() || this.hasWeeds()
                    ? this.getBlock().applyPlant(newState)
                    : this.getBlock().removePlant(newState);
            // Set block state if necessary
            if(newState != oldState) {
                this.getLevel().setBlock(this.getBlockPos(), newState, 3);
            }
            // If the new stat is still a crop block, update the requirements and model data
            if(newState.getBlock() == oldState.getBlock()) {
                // Update growth requirement
                this.requirement.flush();
                this.requirement = RequirementCache.create(this);
                // Update model data
                this.getModelData().setData(PROPERTY_PLANT, this.getPlant());
                this.getModelData().setData(PROPERTY_PLANT_GROWTH, this.getGrowthStage());
                this.getModelData().setData(PROPERTY_WEED, this.getWeeds());
                this.getModelData().setData(PROPERTY_WEED_GROWTH, this.getWeedGrowthStage());
            }
        }
    }

    @Nonnull
    @Override
    public <T> LazyOptional<T> getCapability(@Nonnull Capability<T> cap, @Nullable Direction side) {
        if(cap == CropCapability.getCapability()) {
            return this.cropCapability.cast();
        }
        return super.getCapability(cap, side);
    }

    @Override
    public void onChunkUnloaded() {
        this.requirement.flush();
        super.onChunkUnloaded();
    }

    @Override
    public void setRemoved() {
        this.requirement.flush();
        super.setRemoved();
    }

    @Override
    public void addServerDebugInfo(@Nonnull Consumer<String> consumer) {
        Preconditions.checkNotNull(consumer);
        consumer.accept("CROP:");
        if(this.hasCropSticks()) {
            consumer.accept(" - Crop Sticks Present");
        }
        if (this.isCrossCrop()) {
            consumer.accept(" - This is a cross crop");
        } else {
            final IAgriPlant plant = this.getPlant();
            final IAgriWeed weed = this.getWeeds();
            final IAgriStatsMap stats = this.getStats();
            if(plant.isPlant()) {
                consumer.accept(" - This crop has a plant");
            }
            if(weed.isWeed()) {
                consumer.accept(" - This crop has weeds");
            }
            consumer.accept(" - Plant Id: " + plant.getId());
            consumer.accept(" - Plant Stage: " + this.getGrowthStage());
            consumer.accept(" - Plant Stages: " + plant.getGrowthStages());
            consumer.accept(" - Weed Id: " + weed.getId());
            consumer.accept(" - Weed Stage: " + this.getWeedGrowthStage());
            consumer.accept(" - Weed Stages: " + weed.getGrowthStages());
            consumer.accept(" - stats: " + stats);
            consumer.accept(" - Fertile: " + this.isFertile());
            consumer.accept(" - Mature: " + this.isMature());
            consumer.accept(" - Fully Grown: " + this.isFullyGrown());
            consumer.accept(" - AgriSoil: " + this.getSoil());
        }
    }

    @Override
    public void addClientDebugInfo(@Nonnull Consumer<String> consumer) {
        this.addServerDebugInfo(consumer);
    }

    @Override
    public void addDisplayInfo(@Nonnull Consumer<Component> consumer) {
        CropHelper.addDisplayInfo(this, this.requirement, consumer);
    }

    @Nonnull
    @Override
    public final ModelDataMap getModelData() {
        return this.data;
    }

    @Nonnull
    @Override
    public <T> LazyOptional<T> getCapability(@Nonnull Capability<T> cap) {
        LazyOptional<T> fromPlugins = PluginHandler.getCropCapability(cap, this);
        if(fromPlugins.isPresent()) {
            return fromPlugins;
        } else {
            return super.getCapability(cap);
        }
    }
}
