package tc.oc.pgm.modules;

import org.bukkit.World;
import org.bukkit.entity.Arrow;
import org.bukkit.entity.Entity;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Projectile;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityDamageByEntityEvent;
import org.bukkit.event.entity.EntityShootBowEvent;
import org.bukkit.metadata.FixedMetadataValue;
import org.bukkit.plugin.Plugin;
import org.bukkit.potion.PotionEffect;
import org.bukkit.util.Vector;
import tc.oc.pgm.match.Match;
import tc.oc.pgm.match.MatchModule;
import tc.oc.pgm.match.MatchScope;
import tc.oc.pgm.events.ListenerScope;
import tc.oc.pgm.projectile.EntityLaunchEvent;

import java.util.Random;
import java.util.Set;

@ListenerScope(MatchScope.RUNNING)
public class ModifyBowProjectileMatchModule extends MatchModule implements Listener {
    protected final Class<? extends Entity> projectile;
    protected final float velocityMod;
    protected final Set<PotionEffect> potionEffects;
    protected final Random random = new Random();

    public ModifyBowProjectileMatchModule(Match match, Class<? extends Entity> projectile, float velocityMod, Set<PotionEffect> effects) {
        super(match);
        this.projectile = projectile;
        this.velocityMod = velocityMod;
        potionEffects = effects;
    }

    @EventHandler(ignoreCancelled = true)
    public void changeBowProjectile(EntityShootBowEvent event) {
        Plugin plugin = this.getMatch().getPlugin();
        Entity newProjectile;

        if(this.projectile == Arrow.class && event.getProjectile() instanceof Arrow) {
            // Don't change the projectile if it's an Arrow and the custom entity type is also Arrow
            newProjectile = event.getProjectile();
        } else {
            // Replace the projectile
            World world = event.getEntity().getWorld();
            Projectile oldEntity = (Projectile) event.getProjectile();

            if(Projectile.class.isAssignableFrom(projectile)) {
                // Ender Pearls need to be soawned this way, otherwise they will hit the shooter sometimes
                newProjectile = event.getEntity().launchProjectile(projectile.asSubclass(Projectile.class), oldEntity.getVelocity());
            } else {
                newProjectile = world.spawn(oldEntity.getLocation(), projectile);
                newProjectile.setVelocity(oldEntity.getVelocity());
            }

            event.setProjectile(newProjectile);

            // Copy some things from the old projectile
            newProjectile.setFallDistance(oldEntity.getFallDistance());
            newProjectile.setFireTicks(oldEntity.getFireTicks());

            if(newProjectile instanceof Projectile) {
                ((Projectile) newProjectile).setShooter(oldEntity.getShooter());
                ((Projectile) newProjectile).setBounce(oldEntity.doesBounce());
            }

            // Save some special properties of Arrows
            if(oldEntity instanceof Arrow) {
                Arrow arrow = (Arrow) oldEntity;
                newProjectile.setMetadata("critical", new FixedMetadataValue(plugin, arrow.isCritical()));
                newProjectile.setMetadata("knockback", new FixedMetadataValue(plugin, arrow.getKnockbackStrength()));
                newProjectile.setMetadata("damage", new FixedMetadataValue(plugin, arrow.getDamage()));
            }
        }

        // Tag the projectile as custom
        newProjectile.setMetadata("customProjectile", new FixedMetadataValue(plugin, true));

        getMatch().callEvent(new EntityLaunchEvent(newProjectile, event.getEntity()));
    }

    @EventHandler(ignoreCancelled = true)
    public void fixEntityDamage(EntityDamageByEntityEvent event) {
        Entity projectile = event.getDamager();
        if(projectile.hasMetadata("customProjectile")) {

            // If the custom projectile replaced an arrow, recreate some effects specific to arrows
            if(projectile.hasMetadata("damage")) {
                boolean critical = projectile.getMetadata("critical").get(0).asBoolean();
                int knockback = projectile.getMetadata("knockback").get(0).asInt();
                double damage = projectile.getMetadata("damage").get(0).asDouble();
                double speed = projectile.getVelocity().length();

                // Reproduce the damage calculation from nms.EntityArrow with the addition of our modifier
                int finalDamage = (int) Math.ceil(speed * damage * this.velocityMod);
                if(critical) {
                    finalDamage += random.nextInt(finalDamage / 2 + 2);
                }
                event.setDamage(finalDamage);

                // Flame arrows - target burns for 5 seconds always
                if(projectile.getFireTicks() > 0) {
                    event.getEntity().setFireTicks(100);
                }

                // Reproduce the knockback calculation for punch bows
                if(knockback > 0) {
                    Vector projectileVelocity = projectile.getVelocity();
                    double horizontalSpeed = Math.sqrt(projectileVelocity.getX() * projectileVelocity.getX() +
                                                       projectileVelocity.getZ() * projectileVelocity.getZ());
                    Vector velocity = event.getEntity().getVelocity();
                    velocity.setX(velocity.getX() + projectileVelocity.getX() * knockback * 0.6 / horizontalSpeed);
                    velocity.setY(velocity.getY() + 0.1);
                    velocity.setZ(velocity.getZ() + projectileVelocity.getZ() * knockback * 0.6 / horizontalSpeed);
                    event.getEntity().setVelocity(velocity);
                }
            }

            // Apply any potion effects attached to the projectile
            if(event.getEntity() instanceof LivingEntity) {
                for(PotionEffect potionEffect : this.potionEffects) {
                    ((LivingEntity) event.getEntity()).addPotionEffect(potionEffect);
                }
            }
        }
    }
}
