package org.mcmonkey.sentinel;

import net.citizensnpcs.api.CitizensAPI;
import net.citizensnpcs.api.npc.NPC;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.Material;
import org.bukkit.Statistic;
import org.bukkit.entity.Entity;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
import org.bukkit.entity.Projectile;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.block.BlockIgniteEvent;
import org.bukkit.event.entity.*;
import org.bukkit.event.inventory.InventoryCloseEvent;
import org.bukkit.event.player.AsyncPlayerChatEvent;
import org.bukkit.event.player.PlayerMoveEvent;
import org.bukkit.event.player.PlayerStatisticIncrementEvent;
import org.bukkit.event.player.PlayerTeleportEvent;
import org.bukkit.inventory.ItemStack;
import org.bukkit.projectiles.ProjectileSource;
import org.mcmonkey.sentinel.utilities.VelocityTracker;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.UUID;

public class SentinelEventHandler implements Listener {


    private ArrayList<SentinelTrait> cleanCurrentList() {
        return SentinelPlugin.instance.cleanCurrentList();
    }

    @EventHandler
    public void onEntityExplodes(EntityExplodeEvent event) {
        if (event.isCancelled()) {
            return;
        }
        if (!SentinelPlugin.instance.preventExplosionBlockDamage) {
            return;
        }
        if (event.getEntity() instanceof Projectile) {
             ProjectileSource source = ((Projectile) event.getEntity()).getShooter();
             if (source instanceof Entity) {
                 SentinelTrait sourceSentinel = SentinelUtilities.tryGetSentinel((Entity) source);
                 if (sourceSentinel != null) {
                     event.blockList().clear();
                 }
             }
        }
    }

    @EventHandler
    public void onBlockIgnites(BlockIgniteEvent event) {
        if (event.isCancelled()) {
            return;
        }
        if (!SentinelPlugin.instance.preventExplosionBlockDamage) {
            return;
        }
        if (event.getIgnitingEntity() instanceof Projectile) {
            ProjectileSource source = ((Projectile) event.getIgnitingEntity()).getShooter();
            if (source instanceof Entity) {
                SentinelTrait sourceSentinel = SentinelUtilities.tryGetSentinel((Entity) source);
                if (sourceSentinel != null) {
                    event.setCancelled(true);
                }
            }
        }
    }

    @EventHandler
    public void onEntityCombusts(EntityCombustEvent event) {
        if (event instanceof EntityCombustByEntityEvent || event instanceof EntityCombustByBlockEvent) {
            return;
        }
        if (!SentinelPlugin.instance.blockSunburn) {
            return;
        }
        if (SentinelUtilities.tryGetSentinel(event.getEntity()) == null) {
            return;
        }
        event.setCancelled(true);
    }

    /**
     * Called when a projectile hits a block, to auto-remove Sentinel-fired arrows quickly.
     */
    @EventHandler
    public void onProjectileHitsBlock(ProjectileHitEvent event) {
        if (SentinelPlugin.instance.arrowCleanupTime <= 0) {
            return;
        }
        final Projectile projectile = event.getEntity();
        ProjectileSource source = projectile.getShooter();
        if (!(source instanceof Entity)) {
            return;
        }
        SentinelTrait sentinel = SentinelUtilities.tryGetSentinel((Entity) source);
        if (sentinel == null) {
            return;
        }
        Bukkit.getScheduler().scheduleSyncDelayedTask(SentinelPlugin.instance, new Runnable() {
            @Override
            public void run() {
                if (projectile.isValid()) {
                    projectile.remove();
                }
            }
        }, SentinelPlugin.instance.arrowCleanupTime);
    }

    /**
     * Called when players chat, to process event message targets.
     */
    @EventHandler
    public void onAsyncPlayerChat(final AsyncPlayerChatEvent event) {
        if (event.isCancelled()) {
            return;
        }
        Bukkit.getScheduler().scheduleSyncDelayedTask(SentinelPlugin.instance, new Runnable() {
            @Override
            public void run() {
                if (!event.getPlayer().isOnline()) {
                    return;
                }
                for (SentinelTrait sentinel : cleanCurrentList()) {
                    if (sentinel.allTargets.isEventTarget(sentinel, event)) {
                        sentinel.targetingHelper.addTarget(event.getPlayer().getUniqueId());
                    }
                    if (sentinel.allAvoids.isEventTarget(sentinel, event)) {
                        sentinel.targetingHelper.addAvoid(event.getPlayer().getUniqueId());
                    }
                }
            }
        });
    }

    /**
     * Called when combat occurs in the world (and has not yet been processed by other plugins),
     * to handle things like cancelling invalid damage to/from a Sentinel NPC,
     * changing damage values given to or received from an NPC,
     * and if relevant handling config options that require overriding damage events.
     */
    @EventHandler(priority = EventPriority.LOW)
    public void whenAttacksAreHappening(EntityDamageByEntityEvent event) {
        if (event.isCancelled()) {
            return;
        }
        UUID victimUuid = event.getEntity().getUniqueId();
        for (SentinelTrait sentinel : cleanCurrentList()) {
            sentinel.whenSomethingMightDie(victimUuid);
        }
        SentinelTrait victim = SentinelUtilities.tryGetSentinel(event.getEntity());
        SentinelTrait attacker = SentinelUtilities.tryGetSentinel(event.getDamager());
        if (victim != null) {
            if (attacker != null && victim.getNPC().getId() == attacker.getNPC().getId()) {
                event.setCancelled(true);
                return;
            }
            victim.whenAttacksAreHappeningToMe(event);
        }
        if (attacker != null) {
            attacker.whenAttacksAreHappeningFromMe(event);
        }
        if (event.getDamager() instanceof Projectile) {
            ProjectileSource source = ((Projectile) event.getDamager()).getShooter();
            if (source instanceof Entity) {
                SentinelTrait shooter = SentinelUtilities.tryGetSentinel((Entity) source);
                if (shooter != null) {
                    shooter.whenAttacksAreHappeningFromMyArrow(event);
                }
            }
        }
    }

    /**
     * Called when damage has occurred in the world (before being processed by all other plugins), to handle things like
     * preventing overly rapid fire damage.
     */
    @EventHandler(priority = EventPriority.LOWEST)
    public void whenEntitiesAreHurt(EntityDamageEvent event) {
        if (event.isCancelled()) {
            return;
        }
        SentinelTrait victim = SentinelUtilities.tryGetSentinel(event.getEntity());
        if (victim != null) {
            victim.whenImHurt(event);
        }
    }

    /**
     * Called when combat has occurred in the world (and has been processed by all other plugins), to handle things like cancelling invalid damage to/from a Sentinel NPC,
     * adding targets (if combat occurs near an NPC), and if relevant handling config options that require overriding damage events.
     */
    @EventHandler(priority = EventPriority.HIGHEST)
    public void whenAttacksHappened(EntityDamageByEntityEvent event) {
        if (event.isCancelled()) {
            return;
        }
        Entity damager = event.getDamager();
        if (event.getDamager() instanceof Projectile) {
            ProjectileSource source = ((Projectile) event.getDamager()).getShooter();
            if (source instanceof Entity) {
                damager = (Entity) source;
            }
        }
        SentinelTrait victim = SentinelUtilities.tryGetSentinel(event.getEntity());
        SentinelTrait attacker = SentinelUtilities.tryGetSentinel(damager);
        if (attacker != null) {
            if (victim != null && victim.getNPC().getId() == attacker.getNPC().getId()) {
                event.setCancelled(true);
                return;
            }
            attacker.whenAttacksHappened(event);
        }
        if (victim != null) {
            victim.whenAttacksHappened(event);
        }
        for (SentinelTrait sentinel : cleanCurrentList()) {
            UUID guarding = sentinel.getGuarding();
            if (guarding != null && event.getEntity().getUniqueId().equals(guarding)) {
                sentinel.whenAttacksHappened(event);
            }
        }
        if (damager instanceof LivingEntity) {
            LivingEntity damagerLiving = (LivingEntity) damager;
            for (SentinelTrait sentinel : cleanCurrentList()) {
                if (sentinel.allTargets.isEventTarget(event)
                        && sentinel.targetingHelper.canSee(damagerLiving) && !sentinel.targetingHelper.isIgnored(damagerLiving)) {
                    sentinel.targetingHelper.addTarget(damager.getUniqueId());
                }
                if (sentinel.allAvoids.isEventTarget(event)
                        && sentinel.targetingHelper.canSee(damagerLiving) && !sentinel.targetingHelper.isIgnored(damagerLiving)) {
                    sentinel.targetingHelper.addAvoid(damager.getUniqueId());
                }
            }
        }
        if (event.getEntity() instanceof LivingEntity) {
            LivingEntity entity = (LivingEntity) event.getEntity();
            for (SentinelTrait sentinel : cleanCurrentList()) {
                if (sentinel.allTargets.isReverseEventTarget(sentinel, event)
                        && sentinel.targetingHelper.canSee(entity) && !sentinel.targetingHelper.isIgnored(entity)) {
                    sentinel.targetingHelper.addTarget(entity.getUniqueId());
                }
                if (sentinel.allAvoids.isReverseEventTarget(sentinel, event)
                        && sentinel.targetingHelper.canSee(entity) && !sentinel.targetingHelper.isIgnored(entity)) {
                    sentinel.targetingHelper.addAvoid(entity.getUniqueId());
                }
            }
        }
    }

    /**
     * Called when any entity dies, to process drops handling and targeting updates.
     */
    @EventHandler
    public void whenAnEnemyDies(EntityDeathEvent event) {
        UUID dead = event.getEntity().getUniqueId();
        if (event.getEntity() instanceof Player) {
            VelocityTracker.playerVelocityEstimates.remove(dead);
        }
        for (SentinelTrait sentinel : cleanCurrentList()) {
            sentinel.whenAnEnemyDies(dead);
            sentinel.whenSomethingDies(event);
        }
    }

    /**
     * Called when a Sentinel NPC dies.
     */
    @EventHandler
    public void whenWeDie(EntityDeathEvent event) {
        SentinelTrait sentinel = SentinelUtilities.tryGetSentinel(event.getEntity());
        if (sentinel != null) {
            if (event.getEntity().getLastDamageCause() instanceof EntityDamageByEntityEvent) {
                Entity damager = ((EntityDamageByEntityEvent) event.getEntity().getLastDamageCause()).getDamager();
                if (damager instanceof Projectile && ((Projectile) damager).getShooter() instanceof Entity) {
                    damager = (Entity) ((Projectile) damager).getShooter();
                }
                if (damager instanceof Player && !CitizensAPI.getNPCRegistry().isNPC(damager)) {
                    TrackedKillToBlock blocker = killStatsToBlock.get(damager.getUniqueId());
                    if (blocker == null) {
                        blocker = new TrackedKillToBlock();
                        killStatsToBlock.put(damager.getUniqueId(), blocker);
                    }
                    blocker.addOne();
                }
            }
            sentinel.whenWeDie(event);
        }
    }

    /**
     * Class to help prevent kill statistic incrementing.
     */
    public static class TrackedKillToBlock {

        /**
         * The number of kills to block.
         */
        public int killCount = 0;

        /**
         * The server tick time of the block.
         */
        public long systemTick = 0;

        /**
         * Adds one kill and handles system time.
         */
        public void addOne() {
            if (systemTick != SentinelPlugin.instance.tickTimeTotal) {
                killCount = 0;
                systemTick = SentinelPlugin.instance.tickTimeTotal;
            }
            killCount++;
        }
    }

    /**
     * Map to track automatic kill-statistic incrementing.
     */
    public static HashMap<UUID, TrackedKillToBlock> killStatsToBlock = new HashMap<>();

    /**
     * Called when a player statistic increments, to prevent the "PLAYER_KILLS" stat updating for killing an NPC.
     */
    @EventHandler
    public void onStatisticIncrement(PlayerStatisticIncrementEvent event) {
        if (event.getStatistic() == Statistic.PLAYER_KILLS) {
            UUID uuid = event.getPlayer().getUniqueId();
            TrackedKillToBlock blocker = killStatsToBlock.get(uuid);
            if (blocker != null) {
                blocker.killCount--;
                if (blocker.systemTick != SentinelPlugin.instance.tickTimeTotal) {
                    killStatsToBlock.remove(uuid);
                    return;
                }
                if (blocker.killCount <= 0) {
                    killStatsToBlock.remove(uuid);
                }
                event.setCancelled(true);
            }
        }
    }

    /**
     * Called when a player teleports, to handle NPC guard updates.
     */
    @EventHandler(priority = EventPriority.MONITOR)
    public void onPlayerTeleports(final PlayerTeleportEvent event) {
        if (event.isCancelled()) {
            return;
        }
        UUID uuid = event.getPlayer().getUniqueId();
        for (SentinelTrait sentinel : cleanCurrentList()) {
            if (sentinel.getGuarding() != null && sentinel.getGuarding().equals(uuid)) {
                sentinel.onPlayerTeleports(event);
            }
        }
    }

    /**
     * Called every time a player moves at all, for use with monitoring if players move into range of an NPC.
     */
    @EventHandler
    public void onPlayerMovesInRange(PlayerMoveEvent event) {
        if (event.isCancelled()) {
            return;
        }
        if (event.getTo().toVector().equals(event.getFrom().toVector())) {
            return;
        }
        for (SentinelTrait sentinel : cleanCurrentList()) {
            sentinel.onPlayerMovesInRange(event);
        }
    }

    /**
     * Prefix string for an inventory title.
     */
    public final static String InvPrefix = ChatColor.GREEN + "Sentinel ";

    /**
     * Called when an inventory is closed.
     */
    @EventHandler
    public void onInvClose(InventoryCloseEvent event) {
        String invTitle = SentinelUtilities.getInventoryTitle(event);
        if (invTitle.startsWith(InvPrefix)) {
            int id = Integer.parseInt(invTitle.substring(InvPrefix.length()));
            NPC npc = CitizensAPI.getNPCRegistry().getById(id);
            if (npc != null && npc.hasTrait(SentinelTrait.class)) {
                ArrayList<ItemStack> its = npc.getTrait(SentinelTrait.class).drops;
                its.clear();
                for (ItemStack it : event.getInventory().getContents()) {
                    if (it != null && it.getType() != Material.AIR) {
                        its.add(it);
                    }
                }
            }
        }
    }
}