/*
 * Copyright © 2015, Leon Mangler and the SuperVanish contributors
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */

package de.myzelyam.supervanish.features;

import com.comphenix.protocol.ProtocolLibrary;
import com.comphenix.protocol.events.ListenerPriority;
import com.comphenix.protocol.events.PacketAdapter;
import com.comphenix.protocol.events.PacketEvent;
import com.comphenix.protocol.reflect.FieldAccessException;
import com.comphenix.protocol.wrappers.EnumWrappers;
import com.comphenix.protocol.wrappers.PlayerInfoData;
import com.google.common.collect.ImmutableList;
import de.myzelyam.api.vanish.PlayerShowEvent;
import de.myzelyam.supervanish.SuperVanish;
import lombok.Data;
import org.bukkit.Bukkit;
import org.bukkit.GameMode;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.block.Action;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.inventory.InventoryCloseEvent;
import org.bukkit.event.inventory.InventoryType;
import org.bukkit.event.player.*;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.InventoryView;
import org.bukkit.scheduler.BukkitRunnable;

import java.util.*;

import static com.comphenix.protocol.PacketType.Play.Server.*;
import static org.bukkit.Material.*;

public class SilentOpenChest extends Feature {

    private final Map<Player, StateInfo> playerStateInfoMap = new HashMap<>();

    private final Collection<Material> shulkerBoxes;

    public SilentOpenChest(SuperVanish plugin) {
        super(plugin);
        shulkerBoxes = new ArrayList<>();
        if (plugin.getVersionUtil().isOneDotXOrHigher(11)) {
            try {
                //noinspection unused
                InventoryType testInvType = InventoryType.SHULKER_BOX;
                shulkerBoxes.addAll(Arrays.asList(BLACK_SHULKER_BOX, BLUE_SHULKER_BOX, BROWN_SHULKER_BOX,
                        CYAN_SHULKER_BOX, GRAY_SHULKER_BOX, GREEN_SHULKER_BOX, LIGHT_BLUE_SHULKER_BOX,
                        LIME_SHULKER_BOX, MAGENTA_SHULKER_BOX, ORANGE_SHULKER_BOX, PINK_SHULKER_BOX,
                        PURPLE_SHULKER_BOX, RED_SHULKER_BOX, WHITE_SHULKER_BOX,
                        YELLOW_SHULKER_BOX));
                try {
                    shulkerBoxes.add(LIGHT_GRAY_SHULKER_BOX);
                } catch (NoSuchFieldError e) {
                    // old name
                    shulkerBoxes.add(Material.valueOf("SILVER_SHULKER_BOX"));
                }
                try {
                    shulkerBoxes.add(SHULKER_BOX);
                } catch (NoSuchFieldError ignored) {
                    // no standard shulker box in old versions
                }
                if (plugin.getVersionUtil().isOneDotXOrHigher(14)) {
                    shulkerBoxes.add(Material.valueOf("BARREL"));
                }
            } catch (NoSuchFieldError | IllegalArgumentException ignored) {
                // no shulker box support in very old versions
            }
        }
    }

    @Override
    public void onDisable() {
        for (Player p : playerStateInfoMap.keySet()) {
            StateInfo stateInfo = playerStateInfoMap.remove(p);
            if (stateInfo == null) continue;
            restoreState(stateInfo, p);
        }
    }

    @EventHandler(priority = EventPriority.LOWEST)
    public void onSpectatorClick(InventoryClickEvent e) {
        if (!(e.getWhoClicked() instanceof Player))
            return;
        Player p = (Player) e.getWhoClicked();
        if (!plugin.getVanishStateMgr().isVanished(p.getUniqueId())) return;
        if (!playerStateInfoMap.containsKey(p)) return;
        if (p.getGameMode() != GameMode.SURVIVAL && p.getGameMode() != GameMode.ADVENTURE
                && p.getGameMode() != GameMode.CREATIVE) {
            e.setCancelled(false);
        }
    }

    @EventHandler(priority = EventPriority.MONITOR)
    public void onQuit(PlayerQuitEvent e) {
        Player p = e.getPlayer();
        StateInfo stateInfo = playerStateInfoMap.remove(p);
        if (stateInfo == null) return;
        restoreState(stateInfo, p);
    }

    @EventHandler(priority = EventPriority.HIGH)
    public void onTeleport(PlayerTeleportEvent e) {
        Player p = e.getPlayer();
        if (playerStateInfoMap.containsKey(p)
                && e.getCause() == PlayerTeleportEvent.TeleportCause.SPECTATE) {
            e.setCancelled(true);
        }
    }

    @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
    public void onReappear(PlayerShowEvent e) {
        Player p = e.getPlayer();
        StateInfo stateInfo = playerStateInfoMap.remove(p);
        if (stateInfo == null) return;
        restoreState(stateInfo, p);
    }

    @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
    public void onMove(PlayerMoveEvent e) {
        Player p = e.getPlayer();
        if (playerStateInfoMap.containsKey(p)) {
            if (!(p.getOpenInventory().getType() == InventoryType.CHEST ||
                    plugin.getVersionUtil().isOneDotXOrHigher(11)
                            && isShulkerBox(p.getOpenInventory()))) {
                restoreState(playerStateInfoMap.get(p), p);
                playerStateInfoMap.remove(p);
            }
        }
    }

    @EventHandler(priority = EventPriority.HIGH)
    public void onGameModeChange(PlayerGameModeChangeEvent e) {
        Player p = e.getPlayer();
        if (playerStateInfoMap.containsKey(p) && e.getNewGameMode() != GameMode.SPECTATOR) {
            // Don't let low-priority event listeners cancel the gamemode change
            if (e.isCancelled()) e.setCancelled(false);
        }
    }

    @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
    public void onChestInteract(PlayerInteractEvent e) {
        Player p = e.getPlayer();
        if (!plugin.getVanishStateMgr().isVanished(p.getUniqueId())) return;
        if (e.getAction() != Action.RIGHT_CLICK_BLOCK) return;
        if (p.getGameMode() == GameMode.SPECTATOR) return;
        //noinspection deprecation
        if (p.isSneaking() && p.getItemInHand() != null
                && (p.getItemInHand().getType().isBlock() || p.getItemInHand().getType() == ITEM_FRAME)
                && p.getItemInHand().getType() != Material.AIR)
            return;
        Block block = e.getClickedBlock();
        if (block == null) return;
        if (block.getType() == ENDER_CHEST) {
            e.setCancelled(true);
            p.openInventory(p.getEnderChest());
            return;
        }
        if (!(block.getType() == CHEST || block.getType() == TRAPPED_CHEST
                || plugin.getVersionUtil().isOneDotXOrHigher(11) && shulkerBoxes.contains(block.getType())))
            return;
        StateInfo stateInfo = StateInfo.extract(p);
        playerStateInfoMap.put(p, stateInfo);
        p.setGameMode(GameMode.SPECTATOR);
    }

    @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
    public void onChestClose(InventoryCloseEvent e) {
        if (!(e.getPlayer() instanceof Player))
            return;
        final Player p = (Player) e.getPlayer();
        if (!playerStateInfoMap.containsKey(p)) return;
        if (!(p.getInventory().getType() == InventoryType.CHEST ||
                plugin.getVersionUtil().isOneDotXOrHigher(11)
                        && isShulkerBox(p.getInventory()))) {
            return;
        }
        new BukkitRunnable() {
            @Override
            public void run() {
                StateInfo stateInfo = playerStateInfoMap.get(p);
                if (stateInfo == null) return;
                restoreState(stateInfo, p);
                playerStateInfoMap.remove(p);
            }
        }.runTaskLater(plugin, 1);
    }

    private void restoreState(StateInfo stateInfo, Player p) {
        p.setGameMode(stateInfo.gameMode);
        p.setAllowFlight(stateInfo.canFly);
        p.setFlying(stateInfo.isFlying);
    }

    @Override
    public boolean isActive() {
        return plugin.getSettings().getBoolean("InvisibilityFeatures.OpenChestsSilently");
    }

    private boolean isShulkerBox(Inventory inv) {
        try {
            return inv.getType() == InventoryType.SHULKER_BOX;
        } catch (NoSuchFieldError e) {
            return false;
        }
    }

    private boolean isShulkerBox(InventoryView inv) {
        try {
            return inv.getType() == InventoryType.SHULKER_BOX;
        } catch (NoSuchFieldError e) {
            return false;
        }
    }

    public boolean hasSilentlyOpenedChest(Player p) {
        return playerStateInfoMap.containsKey(p);
    }

    @Override
    public void onEnable() {
        ProtocolLibrary.getProtocolManager().addPacketListener(
                new PacketAdapter(plugin, ListenerPriority.LOW, PLAYER_INFO, GAME_STATE_CHANGE, ABILITIES,
                        ENTITY_METADATA) {
                    @Override
                    public void onPacketSending(PacketEvent event) {
                        try {
                            if (event.getPacketType() == PLAYER_INFO) {
                                // multiple events share same packet object
                                event.setPacket(event.getPacket().shallowClone());

                                List<PlayerInfoData> infoDataList = new ArrayList<>(
                                        event.getPacket().getPlayerInfoDataLists().read(0));
                                Player receiver = event.getPlayer();
                                for (PlayerInfoData infoData : ImmutableList.copyOf(infoDataList)) {
                                    if (!SilentOpenChest.this.plugin.getVisibilityChanger().getHider()
                                            .isHidden(infoData.getProfile().getUUID(), receiver)
                                            && SilentOpenChest.this.plugin.getVanishStateMgr()
                                            .isVanished(infoData.getProfile().getUUID())) {
                                        Player vanishedTabPlayer = Bukkit.getPlayer(infoData.getProfile().getUUID());
                                        if (infoData.getGameMode() == EnumWrappers.NativeGameMode.SPECTATOR
                                                && hasSilentlyOpenedChest(vanishedTabPlayer)
                                                && event.getPacket().getPlayerInfoAction().read(0)
                                                == EnumWrappers.PlayerInfoAction.UPDATE_GAME_MODE) {
                                            int latency;
                                            try {
                                                latency = infoData.getLatency();
                                            } catch (NoSuchMethodError e) {
                                                latency = 21;
                                            }
                                            PlayerInfoData newData = new PlayerInfoData(infoData.getProfile(),
                                                    latency, EnumWrappers.NativeGameMode.SURVIVAL,
                                                    infoData.getDisplayName());
                                            infoDataList.remove(infoData);
                                            infoDataList.add(newData);
                                        }
                                    }
                                }
                                event.getPacket().getPlayerInfoDataLists().write(0, infoDataList);
                            } else if (event.getPacketType() == GAME_STATE_CHANGE) {
                                if (SilentOpenChest.this.plugin.getVanishStateMgr().isVanished(
                                        event.getPlayer().getUniqueId())) {
                                    try {
                                        if (event.getPacket().getIntegers().read(0) != 3) return;
                                    } catch (FieldAccessException e) {
                                        // TODO find alternative for newer versions
                                    }
                                    if (!hasSilentlyOpenedChest(event.getPlayer())) return;
                                    event.setCancelled(true);
                                }
                            } else if (event.getPacketType() == ABILITIES) {
                                if (SilentOpenChest.this.plugin.getVanishStateMgr().isVanished(
                                        event.getPlayer().getUniqueId())) {
                                    if (!hasSilentlyOpenedChest(event.getPlayer())) return;
                                    event.setCancelled(true);
                                }
                            } else if (event.getPacketType() == ENTITY_METADATA) {
                                int entityID = event.getPacket().getIntegers().read(0);
                                if (entityID == event.getPlayer().getEntityId()) {
                                    if (SilentOpenChest.this.plugin.getVanishStateMgr().isVanished(
                                            event.getPlayer().getUniqueId())) {
                                        if (!hasSilentlyOpenedChest(event.getPlayer())) return;
                                        event.setCancelled(true);
                                    }
                                }
                            }
                        } catch (Exception e) {
                            if (e.getMessage() == null
                                    || !e.getMessage().endsWith("is not supported for temporary players.")) {
                                SilentOpenChest.this.plugin.logException(e);
                            }
                        }
                    }
                });
    }

    @Data
    private static class StateInfo {

        private final boolean canFly, isFlying;
        private final GameMode gameMode;

        static StateInfo extract(Player p) {
            return new StateInfo(
                    p.getAllowFlight(),
                    p.isFlying(),
                    p.getGameMode()
            );
        }
    }
}