package net.novucs.ftop.listener;

import com.google.common.collect.ImmutableMap;
import net.novucs.ftop.FactionsTopPlugin;
import net.novucs.ftop.PluginService;
import net.novucs.ftop.RecalculateReason;
import net.novucs.ftop.WorthType;
import net.novucs.ftop.entity.BlockPos;
import net.novucs.ftop.entity.ChestWorth;
import net.novucs.ftop.hook.event.*;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.block.Chest;
import org.bukkit.block.CreatureSpawner;
import org.bukkit.block.DoubleChest;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.BlockPlaceEvent;
import org.bukkit.event.entity.EntityExplodeEvent;
import org.bukkit.event.inventory.InventoryCloseEvent;
import org.bukkit.event.inventory.InventoryOpenEvent;
import org.bukkit.event.world.ChunkUnloadEvent;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.scheduler.BukkitRunnable;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

public class WorthListener extends BukkitRunnable implements Listener, PluginService {

    private final FactionsTopPlugin plugin;
    private final Map<BlockPos, ChestWorth> chests = new HashMap<>();
    private final Set<String> recentDisbands = new HashSet<>();

    public WorthListener(FactionsTopPlugin plugin) {
        this.plugin = plugin;
    }

    @Override
    public void initialize() {
        plugin.getServer().getPluginManager().registerEvents(this, plugin);
        runTaskTimer(plugin, 1, 1);
    }

    @Override
    public void terminate() {
        HandlerList.unregisterAll(this);
        cancel();
    }

    @Override
    public void run() {
        recentDisbands.clear();
    }

    @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
    public void updateWorth(BlockPlaceEvent event) {
        updateWorth(event.getBlock(), RecalculateReason.PLACE, false);
    }

    @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
    public void updateWorth(BlockBreakEvent event) {
        updateWorth(event.getBlock(), RecalculateReason.BREAK, true);
    }

    @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
    public void updateWorth(EntityExplodeEvent event) {
        event.blockList().forEach(block -> updateWorth(block, RecalculateReason.EXPLODE, true));
    }

    private void updateWorth(Block block, RecalculateReason reason, boolean negate) {
        // Do nothing if this area should not be calculated.
        String factionId = plugin.getFactionsHook().getFactionAt(block);
        if (plugin.getSettings().getIgnoredFactionIds().contains(factionId)) {
            return;
        }

        // Get the worth type and price of this event.
        int multiplier = negate ? -1 : 1;
        double price = multiplier * plugin.getSettings().getBlockPrice(block.getType());
        WorthType worthType = WorthType.BLOCK;
        Map<Material, Integer> materials = new HashMap<>();
        Map<EntityType, Integer> spawners = new HashMap<>();

        plugin.getWorthManager().add(block.getChunk(), reason, worthType, price,
                ImmutableMap.of(block.getType(), multiplier), spawners);

        switch (block.getType()) {
            case MOB_SPAWNER:
                worthType = WorthType.SPAWNER;
                CreatureSpawner spawner = (CreatureSpawner) block.getState();
                EntityType spawnedType = spawner.getSpawnedType();
                multiplier *= plugin.getSpawnerStackerHook().getStackSize(spawner);
                price = multiplier * plugin.getSettings().getSpawnerPrice(spawnedType);
                spawners.put(spawnedType, multiplier);
                break;
            case CHEST:
            case TRAPPED_CHEST:
                if (plugin.getSettings().isDisableChestEvents()) {
                    return;
                }

                worthType = WorthType.CHEST;
                Chest chest = (Chest) block.getState();
                ChestWorth chestWorth = negate ? getWorthNegative(chest.getBlockInventory()) : getWorth(chest.getBlockInventory());
                price = chestWorth.getTotalWorth();
                materials.putAll(chestWorth.getMaterials());
                spawners.putAll(chestWorth.getSpawners());
                break;
            default:
                return;
        }

        // Add block price to the count.
        plugin.getWorthManager().add(block.getChunk(), reason, worthType, price, materials, spawners);
    }

    @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
    public void checkWorth(InventoryOpenEvent event) {
        // Do nothing if a player did not open the inventory or if chest events
        // are disabled.
        if (!(event.getPlayer() instanceof Player) || plugin.getSettings().isDisableChestEvents()) {
            return;
        }

        Inventory inventory = event.getInventory();

        // Set all default worth values for this chest.
        if (inventory.getHolder() instanceof DoubleChest) {
            DoubleChest chest = (DoubleChest) inventory.getHolder();
            checkWorth((Chest) chest.getLeftSide());
            checkWorth((Chest) chest.getRightSide());
        }

        if (inventory.getHolder() instanceof Chest) {
            checkWorth((Chest) inventory.getHolder());
        }
    }

    private void checkWorth(Chest chest) {
        chests.put(BlockPos.of(chest.getBlock()), getWorth(chest.getBlockInventory()));
    }

    private ChestWorth getWorth(Inventory inventory) {
        double worth = 0;
        Map<Material, Integer> materials = new HashMap<>();
        Map<EntityType, Integer> spawners = new HashMap<>();

        for (ItemStack item : inventory.getContents()) {
            if (item == null) continue;

            if (item.getType() == Material.MOB_SPAWNER) {
                int stackSize = plugin.getSpawnerStackerHook().getStackSize(item);
                EntityType spawnerType = plugin.getSpawnerStackerHook().getSpawnedType(item);
                worth += plugin.getSettings().getSpawnerPrice(spawnerType) * item.getAmount() * stackSize;

                int count = spawners.getOrDefault(spawnerType, 0);
                spawners.put(spawnerType, count + (item.getAmount() * stackSize));
                continue;
            }

            worth += plugin.getSettings().getBlockPrice(item.getType()) * item.getAmount();
            int count = materials.getOrDefault(item.getType(), 0);
            materials.put(item.getType(), count + item.getAmount());
        }

        return new ChestWorth(worth, materials, spawners);
    }

    private ChestWorth getWorthNegative(Inventory inventory) {
        double worth = 0;
        Map<Material, Integer> materials = new HashMap<>();
        Map<EntityType, Integer> spawners = new HashMap<>();

        for (ItemStack item : inventory.getContents()) {
            if (item == null) continue;

            if (item.getType() == Material.MOB_SPAWNER) {
                int stackSize = plugin.getSpawnerStackerHook().getStackSize(item);
                EntityType spawnerType = plugin.getSpawnerStackerHook().getSpawnedType(item);
                worth -= plugin.getSettings().getSpawnerPrice(spawnerType) * item.getAmount() * stackSize;

                int count = spawners.getOrDefault(spawnerType, 0);
                spawners.put(spawnerType, count - (item.getAmount() + stackSize));
                continue;
            }

            worth -= plugin.getSettings().getBlockPrice(item.getType()) * item.getAmount();
            int count = materials.getOrDefault(item.getType(), 0);
            materials.put(item.getType(), count - item.getAmount());
        }

        return new ChestWorth(worth, materials, spawners);
    }

    @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
    public void updateWorth(InventoryCloseEvent event) {
        // Do nothing if a player did not close the inventory or if chest
        // events are disabled.
        if (!(event.getPlayer() instanceof Player) || plugin.getSettings().isDisableChestEvents()) {
            return;
        }

        // Get cached values from when chest was opened and add the difference
        // to the worth manager.
        if (event.getInventory().getHolder() instanceof DoubleChest) {
            DoubleChest chest = (DoubleChest) event.getInventory().getHolder();
            updateWorth((Chest) chest.getLeftSide());
            updateWorth((Chest) chest.getRightSide());
        }

        if (event.getInventory().getHolder() instanceof Chest) {
            updateWorth((Chest) event.getInventory().getHolder());
        }
    }

    private void updateWorth(Chest chest) {
        if (chest == null) return;
        BlockPos pos = BlockPos.of(chest.getBlock());
        ChestWorth worth = chests.remove(pos);
        if (worth == null) return;

        worth = getDifference(worth, getWorth(chest.getBlockInventory()));

        plugin.getWorthManager().add(chest.getChunk(), RecalculateReason.CHEST, WorthType.CHEST,
                worth.getTotalWorth(), worth.getMaterials(), worth.getSpawners());
    }

    private ChestWorth getDifference(ChestWorth first, ChestWorth second) {
        double worth = second.getTotalWorth() - first.getTotalWorth();
        Map<Material, Integer> materials = getDifference(first.getMaterials(), second.getMaterials());
        Map<EntityType, Integer> spawners = getDifference(first.getSpawners(), second.getSpawners());
        return new ChestWorth(worth, materials, spawners);
    }

    private <T> Map<T, Integer> getDifference(Map<T, Integer> first, Map<T, Integer> second) {
        Map<T, Integer> target = new HashMap<>();

        for (Map.Entry<T, Integer> entry : first.entrySet()) {
            int difference = second.getOrDefault(entry.getKey(), 0) - entry.getValue();
            target.put(entry.getKey(), difference);
        }

        for (Map.Entry<T, Integer> entry : second.entrySet()) {
            if (target.containsKey(entry.getKey())) continue;
            target.put(entry.getKey(), entry.getValue());
        }

        return target;
    }

    @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
    public void updateWorth(FactionClaimEvent event) {
        String newFactionId = event.getFactionId();
        event.getClaims().asMap().forEach((oldFactionId, claims) -> {
            if (!oldFactionId.equals(newFactionId)) {
                plugin.getWorthManager().update(newFactionId, claims, false);
                plugin.getWorthManager().update(oldFactionId, claims, true);
            }
        });
    }

    @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
    public void removeFaction(FactionDisbandEvent event) {
        recentDisbands.add(event.getFactionId());
        plugin.getWorthManager().remove(event.getFactionId());
    }

    @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
    public void renameFaction(FactionRenameEvent event) {
        plugin.getWorthManager().rename(event.getFactionId(), event.getNewName());
    }

    @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
    public void updateWorth(FactionEconomyEvent event) {
        plugin.getWorthManager().add(event.getFactionId(), WorthType.FACTION_BALANCE, event.getNewBalance() - event.getOldBalance());
    }

    @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
    public void updateWorth(PlayerEconomyEvent event) {
        String factionId = plugin.getFactionsHook().getFaction(event.getPlayer());
        plugin.getWorthManager().add(factionId, WorthType.PLAYER_BALANCE, event.getNewBalance() - event.getOldBalance());
    }

    @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
    public void updateWorth(FactionJoinEvent event) {
        double balance = plugin.getEconomyHook().getBalance(event.getPlayer());
        plugin.getWorthManager().add(event.getFactionId(), WorthType.PLAYER_BALANCE, balance);
    }

    @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
    public void updateWorth(FactionLeaveEvent event) {
        // Do nothing if the faction was disbanded within this tick.
        if (recentDisbands.contains(event.getFactionId())) {
            return;
        }

        double balance = plugin.getEconomyHook().getBalance(event.getPlayer());
        plugin.getWorthManager().add(event.getFactionId(), WorthType.PLAYER_BALANCE, -balance);
    }

    @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
    public void recalculate(ChunkUnloadEvent event) {
        plugin.getWorthManager().recalculate(event.getChunk(), RecalculateReason.UNLOAD);
    }

    @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
    public void updateWorth(SpawnerMultiplierChangeEvent event) {
        // Do nothing if this area should not be calculated.
        Block block = event.getBlock();
        String factionId = plugin.getFactionsHook().getFactionAt(block);
        if (plugin.getSettings().getIgnoredFactionIds().contains(factionId)) {
            return;
        }

        // Get the worth type and price of this event.
        int difference = event.getNewMultiplier() - event.getOldMultiplier();
        WorthType worthType = WorthType.SPAWNER;
        Map<Material, Integer> materials = new HashMap<>();
        Map<EntityType, Integer> spawners = new HashMap<>();

        EntityType spawnType = ((CreatureSpawner) block.getState()).getSpawnedType();
        double price = difference * plugin.getSettings().getSpawnerPrice(spawnType);
        spawners.put(spawnType, difference);

        RecalculateReason reason = difference > 0 ? RecalculateReason.PLACE : RecalculateReason.BREAK;

        // Add block price to the count.
        plugin.getWorthManager().add(block.getChunk(), reason, worthType, price, materials, spawners);
    }
}