content: package com.infinityraider.agricraft.impl.v1.genetics;
  
  import com.infinityraider.agricraft.AgriCraft;
  import com.infinityraider.agricraft.api.v1.AgriApi;
  import com.infinityraider.agricraft.api.v1.crop.IAgriCrop;
  import com.infinityraider.agricraft.api.v1.genetics.*;
  import com.infinityraider.agricraft.api.v1.genetics.IAgriCrossBreedEngine;
  import com.infinityraider.agricraft.api.v1.plant.IAgriPlant;
  import com.infinityraider.agricraft.api.v1.stat.IAgriStat;
  import com.infinityraider.agricraft.impl.v1.stats.AgriStatRegistry;
  import net.minecraft.util.Tuple;
  
  import javax.annotation.Nonnull;
  import java.util.*;
  import java.util.stream.Collectors;
  import java.util.stream.Stream;
  
  public class AgriCrossBreedEngine implements IAgriCrossBreedEngine {
      private IParentSelector selector;
      private ICloneLogic cloner;
      private ICombineLogic combiner;
  
      public AgriCrossBreedEngine() {
          this.selector = this::selectAndSortCandidates;
          this.cloner = (crop, parent, random) -> AgriApi.getAgriGenomeBuilder(parent.getTrait(GeneSpecies.getInstance()))
                  .populate(gene -> this.cloneGene(crop, gene, parent, random)).build();
          this.combiner = (crop, parents, random) -> AgriApi.getAgriGenomeBuilder(parents.getA().getTrait(GeneSpecies.getInstance()))
                  .populate(gene -> this.mutateGene(crop, gene, parents, random)).build();
      }
  
      protected IParentSelector getSelector() {
          return this.selector;
      }
  
      protected ICloneLogic getCloner() {
          return this.cloner;
      }
  
      protected ICombineLogic getCombiner() {
          return this.combiner;
      }
  
      @Override
      public boolean handleCrossBreedTick(IAgriCrop crop, Stream<IAgriCrop> neighbours, Random random) {
          // select candidate parents from the neighbours
          List<IAgriCrop> candidates = this.getSelector().selectAndOrder(neighbours, random);
          // No candidates: do nothing
          if(candidates.size() <= 0) {
              return false;
          }
          // Only one candidate: clone
          if(candidates.size() == 1) {
              return this.doClone(crop, candidates.get(0), random);
          }
          // More than one candidate passed, pick the two parents with the highest fertility stat:
          return this.doCombine(crop, candidates.get(0), candidates.get(1), random);
      }
  
      @Nonnull
      protected List<IAgriCrop> selectAndSortCandidates(Stream<IAgriCrop> neighbours, Random random) {
          return neighbours
                  // Valid crops only
                  .filter(IAgriCrop::isValid)
                  // Mature crops only
                  .filter(IAgriCrop::isMature)
                  // Fertile crops only
                  .filter(crop -> (!AgriCraft.instance.getConfig().onlyFertileCropsCanSpread()) || crop.isFertile())
                  // Sort based on fertility stat
                  .sorted(Comparator.comparingInt(this::sorter))
                  // Roll for fertility stat
                  .filter(neighbour -> this.rollFertility(neighbour, random))
                  // Collect successful passes
                  .collect(Collectors.toList());
      }
  
      protected boolean doClone(IAgriCrop target, IAgriCrop parent, Random random) {
          IAgriPlant plant = parent.getPlant();
          // Try spawning a clone if cloning is allowed
          if (plant.allowsCloning(parent.getGrowthStage())) {
              // roll for spread chance
              if(random.nextDouble() < parent.getPlant().getSpreadChance(parent.getGrowthStage())) {
                  return parent.getGenome().map(genome -> this.spawnChild(target, this.getCloner().clone(target, genome, random))).orElse(false);
              }
          }
          // spreading failed
          return false;
      }
  
      protected boolean doCombine(IAgriCrop target, IAgriCrop a, IAgriCrop b, Random random) {
          return a.getGenome().flatMap(genA ->
                  b.getGenome().map(genB -> {
                      // Determine the child's genome
                      IAgriGenome genome = this.getCombiner().combine(target, new Tuple<>(genA, genB), random);
                      // Spawn the child
                      return this.spawnChild(target, genome);
                  })
          ).orElse(false);
      }
  
      protected int sorter(IAgriCrop crop) {
          IAgriStat fertility = AgriStatRegistry.getInstance().fertilityStat();
          return fertility.getMax() - crop.getStats().getFertility();
      }
  
      protected boolean rollFertility(IAgriCrop crop, Random random) {
          IAgriStat fertility = AgriStatRegistry.getInstance().fertilityStat();
          return random.nextInt(fertility.getMax()) < crop.getStats().getFertility();
      }
  
      protected boolean spawnChild(IAgriCrop target, IAgriGenome genome) {
          return target.spawnGenome(genome);
      }
  
      protected <T> IAgriGenePair<T> mutateGene(IAgriCrop crop, IAgriGene<T> gene, Tuple<IAgriGenome, IAgriGenome> parents, Random rand) {
          return gene.mutator().pickOrMutate(
                  crop,
                  gene,
                  this.pickRandomAllele(parents.getA().getGenePair(gene), rand),
                  this.pickRandomAllele(parents.getB().getGenePair(gene), rand),
                  parents,
                  rand
          );
      }
  
      protected <T> IAgriGenePair<T> cloneGene(IAgriCrop crop, IAgriGene<T> gene, IAgriGenome parent, Random rand) {
          if(AgriCraft.instance.getConfig().allowCloneMutations()) {
              return gene.mutator().pickOrMutate(
                      crop,
                      gene,
                      parent.getGenePair(gene).getDominant(),
                      parent.getGenePair(gene).getRecessive(),
                      new Tuple<>(parent, parent),
                      rand
              );
          } else {
              return parent.getGenePair(gene).clone();
          }
      }
  
      protected  <T> IAllele<T> pickRandomAllele(IAgriGenePair<T> pair, Random random) {
          return random.nextBoolean() ? pair.getDominant() : pair.getRecessive();
      }
  
      @Override
      public IAgriCrossBreedEngine setSelectionLogic(@Nonnull IParentSelector selector) {
          this.selector = Objects.requireNonNull(selector);
          return this;
      }
  
      @Override
      public IAgriCrossBreedEngine setCloneLogic(@Nonnull ICloneLogic cloneLogic) {
          this.cloner = Objects.requireNonNull(cloneLogic);
          return this;
      }
  
      @Override
      public IAgriCrossBreedEngine setCombineLogic(@Nonnull ICombineLogic combineLogic) {
          this.combiner = Objects.requireNonNull(combineLogic);
          return this;
      }
  }
  