.MinecraftForge;
import net.minecraftforge.registries.ObjectHolder;
import se.mickelus.tetra.ConfigHandler;
import se.mickelus.tetra.TetraMod;
import se.mickelus.tetra.items.modular.ItemModular;
import se.mickelus.tetra.module.ItemEffect;
import se.mickelus.tetra.module.ItemModule;
import se.mickelus.tetra.module.SchemaRegistry;
import se.mickelus.tetra.module.data.ModuleModel;
import se.mickelus.tetra.module.schema.RemoveSchema;
import se.mickelus.tetra.module.schema.RepairSchema;
import se.mickelus.tetra.util.CastOptional;

import javax.annotation.Nullable;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;

public class ModularBowItem extends ItemModular {

    public final static String staveKey = "bow/stave";
    public final static String stringKey = "bow/string";
    public final static String riserKey = "bow/riser";

    public static final String unlocalizedName = "modular_bow";

    protected ModuleModel arrowModel0 = new ModuleModel("draw_0", new ResourceLocation(TetraMod.MOD_ID, "items/module/bow/arrow_0"));
    protected ModuleModel arrowModel1 = new ModuleModel("draw_1", new ResourceLocation(TetraMod.MOD_ID, "items/module/bow/arrow_1"));
    protected ModuleModel arrowModel2 = new ModuleModel("draw_2", new ResourceLocation(TetraMod.MOD_ID, "items/module/bow/arrow_2"));

    protected ItemStack vanillaBow;

    @ObjectHolder(TetraMod.MOD_ID + ":" + unlocalizedName)
    public static ModularBowItem instance;

    public ModularBowItem() {
        super(new Properties().maxStackSize(1));
        setRegistryName(unlocalizedName);

        majorModuleKeys = new String[] { stringKey, staveKey };
        minorModuleKeys = new String[] { riserKey };

        requiredModules = new String[] { stringKey, staveKey };

        vanillaBow = new ItemStack(Items.BOW);

        updateConfig(ConfigHandler.honeBowBase.get(), ConfigHandler.honeBowIntegrityMultiplier.get());

        SchemaRegistry.instance.registerSchema(new RepairSchema(this));
        RemoveSchema.registerRemoveSchemas(this);
    }

    public void updateConfig(int honeBase, int honeIntegrityMultiplier) {
        this.honeBase = honeBase;
        this.honeIntegrityMultiplier = honeIntegrityMultiplier;
    }

    @Override
    public void clientInit() {
        super.clientInit();
        MinecraftForge.EVENT_BUS.register(new RangedProgressOverlay(Minecraft.getInstance()));
        MinecraftForge.EVENT_BUS.register(new RangedFOVTransformer());
    }

    /**
     * Called when the player stops using an Item (stops holding the right mouse button).
     */
    public void onPlayerStoppedUsing(ItemStack itemStack, World world, LivingEntity entity, int timeLeft) {
        fireArrow(itemStack, world, entity, timeLeft);
    }

    @Override
    public void onUsingTick(ItemStack itemStack, LivingEntity player, int count) {
        if (getEffectLevel(itemStack, ItemEffect.releaseLatch) > 0 && getProgress(itemStack, player) >= 1) {
            player.stopActiveHand();
        }
    }

    protected void fireArrow(ItemStack itemStack, World world, LivingEntity entity, int timeLeft) {
        if (entity instanceof PlayerEntity) {
            PlayerEntity player = (PlayerEntity)entity;
            boolean playerInfinite = player.abilities.isCreativeMode
                    || EnchantmentHelper.getEnchantmentLevel(Enchantments.INFINITY, itemStack) > 0;
            ItemStack ammoStack = player.findAmmo(vanillaBow);

            // multiply by 20 to align progress with vanilla bow (fully drawn at 1sec/20ticks)
            int drawProgress = Math.round(getProgress(itemStack, entity) * 20);
            drawProgress = net.minecraftforge.event.ForgeEventFactory.onArrowLoose(itemStack, world, player, drawProgress,
                   !ammoStack.isEmpty() || playerInfinite);

            if (drawProgress < 0) {
                return;
            }

            if (!ammoStack.isEmpty() || playerInfinite) {
                if (ammoStack.isEmpty()) {
                    ammoStack = new ItemStack(Items.ARROW);
                }

                float projectileVelocity = getArrowVelocity(drawProgress, getOverbowCap(itemStack), (float) getOverbowRate(itemStack));
                if (projectileVelocity > 0.1f) {
                    ArrowItem ammoItem = CastOptional.cast(ammoStack.getItem(), ArrowItem.class)
                           .orElse((ArrowItem) Items.ARROW);

                    boolean infiniteAmmo = player.abilities.isCreativeMode || ammoItem.isInfinite(ammoStack, itemStack, player);

                    if (!world.isRemote) {
                        AbstractArrowEntity projectile = ammoItem.createArrow(world, ammoStack, player);
                        projectile.shoot(player, player.rotationPitch, player.rotationYaw, 0.0F,
                                projectileVelocity * 3.0F, 1.0F);

                        if (projectileVelocity == 1.0F) {
                            projectile.setIsCritical(true);
                        }

                        // the damage modifier is based on fully drawn damage, vanilla bows deal 5 times the base damage when fully drawn
                        projectile.setDamage(projectile.getDamage() + getDamageModifier(itemStack) / 5 - 2);

                        int powerLevel = EnchantmentHelper.getEnchantmentLevel(Enchantments.POWER, itemStack);
                        if (powerLevel > 0) {
                            projectile.setDamage(projectile.getDamage() + powerLevel * 0.5D + 0.5D);
                        }

                        int punchLevel = EnchantmentHelper.getEnchantmentLevel(Enchantments.PUNCH, itemStack);
                        if (punchLevel > 0) {
                            projectile.setKnockbackStrength(punchLevel);
                        }

                        if (EnchantmentHelper.getEnchantmentLevel(Enchantments.FLAME, itemStack) > 0) {
                            projectile.setFire(100);
                        }

                        int piercingLevel = getEffectLevel(itemStack, ItemEffect.piercing);
                        if (piercingLevel > 0) {
                            projectile.setPierceLevel((byte) piercingLevel);
                        }

                        itemStack.damageItem(1, player, (p_220009_1_) -> {
                            p_220009_1_.sendBreakAnimation(player.getActiveHand());
                        });
                        if (infiniteAmmo || player.abilities.isCreativeMode
                                && (ammoStack.getItem() == Items.SPECTRAL_ARROW || ammoStack.getItem() == Items.TIPPED_ARROW)) {
                            projectile.pickupStatus = AbstractArrowEntity.PickupStatus.CREATIVE_ONLY;
                        }

                        world.addEntity(projectile);

                        applyUsageEffects(entity, itemStack, 1);
                    }

                    world.playSound(null, player.getPosX(), player.getPosY(), player.getPosZ(),
                            SoundEvents.ENTITY_ARROW_SHOOT, SoundCategory.PLAYERS,
                            1.0F, 1.0F / (random.nextFloat() * 0.4F + 1.2F) + projectileVelocity * 0.5F);

                    if (!infiniteAmmo &&!player.abilities.isCreativeMode) {
                        ammoStack.shrink(1);
                        if (ammoStack.isEmpty()) {
                            player.inventory.deleteStack(ammoStack);
                        }
                    }

                    player.addStat(Stats.ITEM_USED.get(this));
                }
            }
        }
    }

    public double getDamageModifier(ItemStack itemStack) {
        if (isBroken