package tc.oc.pgm.inventory;

import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import com.google.common.collect.Lists;
import org.apache.commons.lang.StringUtils;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.Material;
import org.bukkit.attribute.Attribute;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityDamageEvent;
import org.bukkit.event.entity.EntityRegainHealthEvent;
import org.bukkit.event.entity.FoodLevelChangeEvent;
import org.bukkit.event.inventory.ClickType;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.inventory.InventoryClickedEvent;
import org.bukkit.event.inventory.InventoryCloseEvent;
import org.bukkit.event.inventory.InventoryDragEvent;
import org.bukkit.event.inventory.InventoryType;
import org.bukkit.event.player.PlayerDropItemEvent;
import org.bukkit.event.player.PlayerPickupItemEvent;
import org.bukkit.inventory.DoubleChestInventory;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.InventoryHolder;
import org.bukkit.inventory.ItemFlag;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.PlayerInventory;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.potion.PotionEffect;
import tc.oc.api.bukkit.users.Users;
import tc.oc.commons.bukkit.util.BukkitUtils;
import tc.oc.commons.core.commands.CommandBinder;
import tc.oc.pgm.PGMTranslations;
import tc.oc.pgm.blitz.BlitzMatchModule;
import tc.oc.pgm.doublejump.DoubleJumpMatchModule;
import tc.oc.pgm.events.ListenerScope;
import tc.oc.pgm.events.ObserverInteractEvent;
import tc.oc.pgm.events.PlayerBlockTransformEvent;
import tc.oc.pgm.events.PlayerPartyChangeEvent;
import tc.oc.pgm.kits.WalkSpeedKit;
import tc.oc.pgm.match.MatchModule;
import tc.oc.pgm.match.MatchPlayer;
import tc.oc.pgm.match.MatchScope;
import tc.oc.pgm.match.Repeatable;
import tc.oc.pgm.match.inject.MatchModuleFixtureManifest;
import tc.oc.pgm.spawns.events.ParticipantSpawnEvent;
import tc.oc.time.Time;

@ListenerScope(MatchScope.LOADED)
public class ViewInventoryMatchModule extends MatchModule implements Listener {

    public static class Manifest extends MatchModuleFixtureManifest<ViewInventoryMatchModule> {
        @Override protected void configure() {
            super.configure();

            new CommandBinder(binder())
                .register(InventoryCommands.class);
        }
    }

    public static final Duration TICK = Duration.ofMillis(50);

    protected final Map<Player, View> views = new HashMap<>();
    protected final Map<Player, Instant> updateQueue = new HashMap<>();

    public static int getInventoryPreviewSlot(int inventorySlot) {
        if(inventorySlot < 9) {
            return inventorySlot + 36; // put hotbar on bottom
        }
        if(inventorySlot < 36) {
            return inventorySlot; // rest of inventory
        }
        // TODO: investigate why this method doesn't work with CraftBukkit's armor slots
        return inventorySlot; // default
    }

    @Repeatable(scope = MatchScope.LOADED, interval = @Time(ticks = 4))
    public void queuedChecks() {
        for(Iterator<Map.Entry<Player, Instant>> iterator = updateQueue.entrySet().iterator(); iterator.hasNext();) {
            final Map.Entry<Player, Instant> entry = iterator.next();
            if(entry.getValue().isAfter(Instant.now())) continue;

            checkMonitoredInventories(entry.getKey());
            iterator.remove();
        }
    }

    @EventHandler
    public void closeMonitoredInventory(final InventoryCloseEvent event) {
        views.remove(event.getActor());
    }

    @EventHandler
    public void playerQuit(final PlayerPartyChangeEvent event) {
        views.remove(event.getPlayer().getBukkit());
    }

    @EventHandler(ignoreCancelled = true)
    public void showInventories(final ObserverInteractEvent event) {
        if(event.getClickType() != ClickType.RIGHT) return;
        if(event.getPlayer().isDead()) return;

        if(event.getClickedParticipant() != null) {
            event.setCancelled(true);
            if(canPreviewInventory(event.getPlayer(), event.getClickedParticipant())) {
                this.previewPlayerInventory(event.getPlayer().getBukkit(), event.getClickedParticipant().getInventory());
            }
        } else if(event.getClickedEntity() instanceof InventoryHolder && !(event.getClickedEntity() instanceof Player)) {
            event.setCancelled(true);
            this.previewInventory(event.getPlayer().getBukkit(), ((InventoryHolder) event.getClickedEntity()).getInventory());
        } else if(event.getClickedBlockState() instanceof InventoryHolder) {
            event.setCancelled(true);
            this.previewInventory(event.getPlayer().getBukkit(), ((InventoryHolder) event.getClickedBlockState()).getInventory());
        }
    }

    @EventHandler(priority = EventPriority.LOW, ignoreCancelled = true)
    public void cancelClicks(final InventoryClickEvent event) {
        final View view = views.get(event.getActor());
        if(view != null && event.getInventory().equals(view.preview)) {
            event.setCancelled(true);
        }
    }

    @EventHandler(priority = EventPriority.MONITOR)
    public void updateMonitoredClick(final InventoryClickedEvent event) {
        if(event.getWhoClicked() instanceof Player) {
            Player player = (Player) event.getWhoClicked();

            boolean playerInventory = event.getInventory().getType() == InventoryType.CRAFTING; // cb bug fix
            Inventory inventory;

            if(playerInventory) {
                inventory = player.getInventory();
            } else {
                inventory = event.getInventory();
            }

            invLoop: for(Map.Entry<Player, View> entry : new HashSet<>(this.views.entrySet())) { // avoid ConcurrentModificationException
                final Player viewer = entry.getKey();
                View view = entry.getValue();

                // because a player can only be viewing one inventory at a time,
                // this is how we determine if we have a match
                if(inventory.getViewers().isEmpty() ||
                   view.watched.getViewers().isEmpty() ||
                   inventory.getViewers().size() > view.watched.getViewers().size()) continue invLoop;

                for(int i = 0; i < inventory.getViewers().size(); i++) {
                    if(!inventory.getViewers().get(i).equals(view.watched.getViewers().get(i))) {
                        continue invLoop;
                    }
                }

                // a watched user is in a chest
                if(view.isPlayerInventory() && !playerInventory) {
                    inventory = view.getPlayerInventory().getHolder().getInventory();
                    playerInventory = true;
                }

                if(playerInventory) {
                    this.previewPlayerInventory(viewer, (PlayerInventory) inventory);
                } else {
                    this.previewInventory(viewer, inventory);
                }
            }
        }
    }

    @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
    public void updateMonitoredInventory(final InventoryClickEvent event) {
        this.scheduleCheck((Player) event.getWhoClicked());
    }

    @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
    public void updateMonitoredInventory(final InventoryDragEvent event) {
        this.scheduleCheck((Player) event.getWhoClicked());
    }

    @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
    public void updateMonitoredTransform(final PlayerBlockTransformEvent event) {
        MatchPlayer player = event.getPlayer();
        if(player != null) this.scheduleCheck(player.getBukkit());
    }

    @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
    public void updateMonitoredPickup(final PlayerPickupItemEvent event) {
        this.scheduleCheck(event.getPlayer());
    }

    @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
    public void updateMonitoredDrop(final PlayerDropItemEvent event) {
        this.scheduleCheck(event.getPlayer());
    }

    @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
    public void updateMonitoredDamage(final EntityDamageEvent event) {
        if(event.getEntity() instanceof Player) {
            this.scheduleCheck((Player) event.getEntity());
        }
    }

    @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
    public void updateMonitoredHealth(final EntityRegainHealthEvent event) {
        if(event.getEntity() instanceof Player) {
            Player player = (Player) event.getEntity();
            if(player.getHealth() == player.getMaxHealth()) return;
            this.scheduleCheck((Player) event.getEntity());
        }
    }

    @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
    public void updateMonitoredHunger(final FoodLevelChangeEvent event) {
        this.scheduleCheck((Player) event.getEntity());
    }

    @EventHandler(priority = EventPriority.MONITOR)
    public void updateMonitoredSpawn(final ParticipantSpawnEvent event) {
        // must have this hack so we update player's inventories when they respawn and recieve a kit
        ViewInventoryMatchModule.this.scheduleCheck(event.getPlayer().getBukkit());
    }

    public boolean canPreviewInventory(Player viewer, Player holder) {
        MatchPlayer matchViewer = getMatch().getPlayer(viewer);
        MatchPlayer matchHolder = getMatch().getPlayer(holder);
        return matchViewer != null && matchHolder != null && canPreviewInventory(matchViewer, matchHolder);
    }

    public boolean canPreviewInventory(MatchPlayer viewer, MatchPlayer holder) {
        return viewer.isObserving() && holder.isSpawned();
    }

    protected void scheduleCheck(Player updater) {
        updateQueue.computeIfAbsent(updater, player -> Instant.now().plus(TICK));
    }

    protected void checkMonitoredInventories(Player updater) {
        views.forEach((viewer, view) -> {
            if(view.isPlayerInventory() && updater.equals(view.getPlayerInventory().getHolder())) {
                previewPlayerInventory(viewer, view.getPlayerInventory());
            }
        });
    }

    protected void previewPlayerInventory(Player viewer, PlayerInventory inventory) {
        if(viewer == null) { return; }

        Player holder = (Player) inventory.getHolder();
        // Ensure that the title of the inventory is <= 32 characters long to appease Minecraft's restrictions on inventory titles
        String title = StringUtils.substring(holder.getDisplayName(viewer), 0, 32);

        Inventory preview = Bukkit.getServer().createInventory(viewer, 45, title);

        // handle inventory mapping
        for(int i = 0; i <= 35; i++) {
            preview.setItem(getInventoryPreviewSlot(i), inventory.getItem(i));
        }

        MatchPlayer matchHolder = this.match.getPlayer(holder);
        if (matchHolder != null && matchHolder.isParticipating()) {
            BlitzMatchModule module = matchHolder.getMatch().getMatchModule(BlitzMatchModule.class);
            if (module != null) {
                int livesLeft = module.lifeManager.getLives(Users.playerId(holder));
                ItemStack lives = new ItemStack(Material.EGG, livesLeft);
                ItemMeta lifeMeta = lives.getItemMeta();
                lifeMeta.addItemFlags(ItemFlag.values());
                String key = livesLeft == 1 ? "match.blitz.livesRemaining.singularLives" : "match.blitz.livesRemaining.pluralLives";
                lifeMeta.setDisplayName(ChatColor.GREEN + PGMTranslations.get().t(key, viewer, ChatColor.AQUA + String.valueOf(livesLeft) + ChatColor.GREEN));
                lives.setItemMeta(lifeMeta);
                preview.setItem(4, lives);
            }

            List<String> specialLore = new ArrayList<>();

            if(holder.getAllowFlight()) {
                specialLore.add(ChatColor.LIGHT_PURPLE + PGMTranslations.get().t("specialAbility.flying", viewer));
            }

            DoubleJumpMatchModule djmm = matchHolder.getMatch().getMatchModule(DoubleJumpMatchModule.class);
            if(djmm != null && djmm.hasKit(matchHolder)) {
                specialLore.add(ChatColor.LIGHT_PURPLE + PGMTranslations.get().t("specialAbility.doubleJump", viewer));
            }

            double knockbackResistance = holder.getAttribute(Attribute.GENERIC_KNOCKBACK_RESISTANCE).getValue();
            if(knockbackResistance > 0) {
                specialLore.add(ChatColor.LIGHT_PURPLE + PGMTranslations.get().t("specialAbility.knockbackResistance", viewer, (int) Math.ceil(knockbackResistance * 100)));
            }

            double knockbackReduction = holder.getKnockbackReduction();
            if(knockbackReduction > 0) {
                specialLore.add(ChatColor.LIGHT_PURPLE + PGMTranslations.get().t("specialAbility.knockbackReduction", viewer, (int) Math.ceil(knockbackReduction * 100)));
            }

            double walkSpeed = holder.getWalkSpeed();
            if(walkSpeed != WalkSpeedKit.BUKKIT_DEFAULT) {
                specialLore.add(ChatColor.LIGHT_PURPLE + PGMTranslations.get().t("specialAbility.walkSpeed", viewer, String.format("%.1f", walkSpeed / WalkSpeedKit.BUKKIT_DEFAULT)));
            }


            if(!specialLore.isEmpty()) {
                ItemStack special = new ItemStack(Material.NETHER_STAR);
                ItemMeta specialMeta = special.getItemMeta();
                specialMeta.addItemFlags(ItemFlag.values());
                specialMeta.setDisplayName(ChatColor.AQUA.toString() + ChatColor.ITALIC + PGMTranslations.get().t("player.inventoryPreview.specialAbilities", viewer));
                specialMeta.setLore(specialLore);
                special.setItemMeta(specialMeta);
                preview.setItem(5, special);
            }
        }

        // potions
        boolean hasPotions = holder.getActivePotionEffects().size() > 0;
        ItemStack potions = new ItemStack(hasPotions? Material.POTION : Material.GLASS_BOTTLE);
        ItemMeta potionMeta = potions.getItemMeta();
        potionMeta.addItemFlags(ItemFlag.values());
        potionMeta.setDisplayName(ChatColor.AQUA.toString() + ChatColor.ITALIC + PGMTranslations.get().t("player.inventoryPreview.potionEffects", viewer));
        List<String> lore = Lists.newArrayList();
        if(hasPotions) {
            for(PotionEffect effect : holder.getActivePotionEffects()) {
                lore.add(ChatColor.YELLOW + BukkitUtils.potionEffectTypeName(effect.getType()) + " " + (effect.getAmplifier() + 1));
            }
        } else {
            lore.add(ChatColor.YELLOW + PGMTranslations.get().t("player.inventoryPreview.noPotionEffects", viewer));
        }
        potionMeta.setLore(lore);
        potions.setItemMeta(potionMeta);
        preview.setItem(6, potions);

        // hunger and health
        ItemStack hunger = new ItemStack(Material.COOKED_BEEF, holder.getFoodLevel());
        ItemMeta hungerMeta = hunger.getItemMeta();
        hungerMeta.addItemFlags(ItemFlag.values());
        hungerMeta.setDisplayName(ChatColor.AQUA.toString() + ChatColor.ITALIC + PGMTranslations.get().t("player.inventoryPreview.hungerLevel", viewer));
        hungerMeta.addItemFlags(ItemFlag.HIDE_POTION_EFFECTS);
        hunger.setItemMeta(hungerMeta);
        preview.setItem(7, hunger);

        ItemStack health = new ItemStack(Material.REDSTONE, (int) holder.getHealth());
        ItemMeta healthMeta = health.getItemMeta();
        healthMeta.addItemFlags(ItemFlag.values());
        healthMeta.setDisplayName(ChatColor.AQUA.toString() + ChatColor.ITALIC + PGMTranslations.get().t("player.inventoryPreview.healthLevel", viewer));
        healthMeta.addItemFlags(ItemFlag.HIDE_POTION_EFFECTS);
        health.setItemMeta(healthMeta);
        preview.setItem(8, health);

        // set armor manually because craftbukkit is a derp
        preview.setItem(0, inventory.getHelmet());
        preview.setItem(1, inventory.getChestplate());
        preview.setItem(2, inventory.getLeggings());
        preview.setItem(3, inventory.getBoots());

        this.showInventoryPreview(viewer, inventory, preview);
    }

    public void previewInventory(Player viewer, Inventory realInventory) {
        if(viewer == null) { return; }

        if(realInventory instanceof PlayerInventory) {
            previewPlayerInventory(viewer, (PlayerInventory) realInventory);
        }else {
            Inventory fakeInventory;
            if(realInventory instanceof DoubleChestInventory) {
                if(realInventory.hasCustomName()) {
                    fakeInventory = Bukkit.createInventory(viewer, realInventory.getSize(), realInventory.getName());
                } else {
                    fakeInventory = Bukkit.createInventory(viewer, realInventory.getSize());
                }
            } else {
                if(realInventory.hasCustomName()) {
                    fakeInventory = Bukkit.createInventory(viewer, realInventory.getType(), realInventory.getName());
                } else {
                    fakeInventory = Bukkit.createInventory(viewer, realInventory.getType());
                }
            }
            fakeInventory.setContents(realInventory.contents());

            this.showInventoryPreview(viewer, realInventory, fakeInventory);
        }
    }

    protected void showInventoryPreview(Player viewer, Inventory realInventory, Inventory fakeInventory) {
        if(viewer == null) return;

        View view = views.get(viewer);
        if(view != null && view.watched.equals(realInventory) && view.preview.getSize() == fakeInventory.getSize()) {
            view.preview.setContents(fakeInventory.contents());
        } else {
            view = new View(realInventory, fakeInventory);
            views.put(viewer, view);
            viewer.openInventory(fakeInventory);
        }
    }

    private static class View {
        final Inventory watched;
        final Inventory preview;

        View(Inventory watched, Inventory preview) {
            this.watched = watched;
            this.preview = preview;
        }

        boolean isPlayerInventory() {
            return this.watched instanceof PlayerInventory;
        }

        PlayerInventory getPlayerInventory() {
            return (PlayerInventory) this.watched;
        }
    }
}