package cn.citycraft.AuthMe.listener;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.PatternSyntaxException;

import org.bukkit.Bukkit;
import org.bukkit.GameMode;
import org.bukkit.Location;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.block.SignChangeEvent;
import org.bukkit.event.entity.EntityDamageByEntityEvent;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.inventory.InventoryOpenEvent;
import org.bukkit.event.player.AsyncPlayerChatEvent;
import org.bukkit.event.player.AsyncPlayerPreLoginEvent;
import org.bukkit.event.player.PlayerBedEnterEvent;
import org.bukkit.event.player.PlayerCommandPreprocessEvent;
import org.bukkit.event.player.PlayerDropItemEvent;
import org.bukkit.event.player.PlayerFishEvent;
import org.bukkit.event.player.PlayerGameModeChangeEvent;
import org.bukkit.event.player.PlayerInteractEntityEvent;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.event.player.PlayerItemConsumeEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerKickEvent;
import org.bukkit.event.player.PlayerLoginEvent;
import org.bukkit.event.player.PlayerMoveEvent;
import org.bukkit.event.player.PlayerPickupItemEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.event.player.PlayerRespawnEvent;
import org.bukkit.event.player.PlayerShearEntityEvent;

import com.google.common.io.ByteArrayDataOutput;
import com.google.common.io.ByteStreams;

import cn.citycraft.AuthMe.AuthMe;
import cn.citycraft.AuthMe.ConsoleLogger;
import cn.citycraft.AuthMe.Utils;
import cn.citycraft.AuthMe.cache.auth.PlayerAuth;
import cn.citycraft.AuthMe.cache.auth.PlayerCache;
import cn.citycraft.AuthMe.cache.limbo.LimboCache;
import cn.citycraft.AuthMe.cache.limbo.LimboPlayer;
import cn.citycraft.AuthMe.settings.Messages;
import cn.citycraft.AuthMe.settings.Settings;

public class AuthMePlayerListener implements Listener {

	public static ConcurrentHashMap<String, Boolean> causeByAuthMe = new ConcurrentHashMap<>();
	public static ConcurrentHashMap<String, GameMode> gameMode = new ConcurrentHashMap<>();
	public static ConcurrentHashMap<String, String> joinMessage = new ConcurrentHashMap<>();
	public AuthMe plugin;
	private final List<String> antibot = new ArrayList<>();
	private final Messages m = Messages.getInstance();

	public AuthMePlayerListener(final AuthMe plugin) {
		this.plugin = plugin;
	}

	@EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)
	public void onPlayerBedEnter(final PlayerBedEnterEvent event) {
		if (Utils.checkAuth(event.getPlayer())) {
			return;
		}
		event.setCancelled(true);
	}

	@EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
	public void onPlayerChat(final AsyncPlayerChatEvent event) {
		handleChat(event);
	}

	@EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)
	public void onPlayerCommandPreprocess(final PlayerCommandPreprocessEvent event) {
		final String msg = event.getMessage();
		if (msg.equalsIgnoreCase("/worldedit cui")) {
			return;
		}

		final String cmd = msg.split(" ")[0];
		if (cmd.equalsIgnoreCase("/login") || cmd.equalsIgnoreCase("/register") || cmd.equalsIgnoreCase("/l") || cmd.equalsIgnoreCase("/reg") || cmd.equalsIgnoreCase("/email")
				|| cmd.equalsIgnoreCase("/captcha")) {
			return;
		}
		if (Settings.useEssentialsMotd && cmd.equalsIgnoreCase("/motd")) {
			return;
		}
		if (Settings.allowCommands.contains(cmd)) {
			return;
		}

		if (!Utils.checkAuth(event.getPlayer())) {
			event.setMessage("/notloggedin");
			event.setCancelled(true);
		}
	}

	@EventHandler(ignoreCancelled = true, priority = EventPriority.NORMAL)
	public void onPlayerConsumeItem(final PlayerItemConsumeEvent event) {
		if (Utils.checkAuth(event.getPlayer())) {
			return;
		}
		event.setCancelled(true);
	}

	@EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)
	public void onPlayerDropItem(final PlayerDropItemEvent event) {
		if (Utils.checkAuth(event.getPlayer())) {
			return;
		}
		event.setCancelled(true);
	}

	@EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)
	public void onPlayerEarlyChat(final AsyncPlayerChatEvent event) {
		handleChat(event);
	}

	@EventHandler(ignoreCancelled = true, priority = EventPriority.NORMAL)
	public void onPlayerFish(final PlayerFishEvent event) {
		final Player player = event.getPlayer();
		if (player == null || Utils.checkAuth(player)) {
			return;
		}
		event.setCancelled(true);
	}

	@EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST)
	public void onPlayerGameModeChange(final PlayerGameModeChangeEvent event) {
		final Player player = event.getPlayer();
		if (player == null) {
			return;
		}
		if (plugin.authmePermissible(player, "authme.bypassforcesurvival")) {
			return;
		}
		if (Utils.checkAuth(player)) {
			return;
		}

		final String name = player.getName().toLowerCase();
		if (causeByAuthMe.containsKey(name)) {
			causeByAuthMe.remove(name);
			return;
		}
		event.setCancelled(true);
	}

	@EventHandler(ignoreCancelled = true, priority = EventPriority.HIGH)
	public void onPlayerHighChat(final AsyncPlayerChatEvent event) {
		handleChat(event);
	}

	@EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST)
	public void onPlayerHighestChat(final AsyncPlayerChatEvent event) {
		handleChat(event);
	}

	@EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST)
	public void onPlayerInteract(final PlayerInteractEvent event) {
		final Player player = event.getPlayer();
		if (player == null || Utils.checkAuth(player)) {
			return;
		}
		event.setCancelled(true);
	}

	@EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)
	public void onPlayerInteractEntity(final PlayerInteractEntityEvent event) {
		final Player player = event.getPlayer();
		if (player == null || Utils.checkAuth(player)) {
			return;
		}
		event.setCancelled(true);
	}

	@EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)
	public void onPlayerInventoryClick(final InventoryClickEvent event) {
		if (event.getWhoClicked() == null) {
			return;
		}
		if (!(event.getWhoClicked() instanceof Player)) {
			return;
		}
		if (Utils.checkAuth((Player) event.getWhoClicked())) {
			return;
		}
		event.setCancelled(true);
	}

	@EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST)
	public void onPlayerInventoryOpen(final InventoryOpenEvent event) {
		final Player player = (Player) event.getPlayer();
		if (Utils.checkAuth(player)) {
			return;
		}
		event.setCancelled(true);

		/*
		 * @note little hack cause InventoryOpenEvent cannot be cancelled for
		 * real, cause no packet is send to server by client for the main inv
		 */
		Bukkit.getScheduler().scheduleSyncDelayedTask(plugin, new Runnable() {

			@Override
			public void run() {
				player.closeInventory();
			}
		}, 1);
	}

	@EventHandler(priority = EventPriority.HIGHEST)
	public void onPlayerJoin(final PlayerJoinEvent event) {
		if (event.getPlayer() == null) {
			return;
		}

		// Shedule login task so works after the prelogin
		// (Fix found by Koolaid5000)
		Bukkit.getScheduler().runTask(plugin, new Runnable() {
			@Override
			public void run() {
				final Player player = event.getPlayer();
				final String name = player.getName().toLowerCase();

				plugin.management.performJoin(player);

				// Remove the join message while the player isn't logging in
				if ((Settings.enableProtection || Settings.delayJoinMessage) && event.getJoinMessage() != null) {
					joinMessage.put(name, event.getJoinMessage());
					event.setJoinMessage(null);
				}
			}
		});
	}

	@EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
	public void onPlayerKick(final PlayerKickEvent event) {
		if (event.getPlayer() == null) {
			return;
		}

		if ((!Settings.isForceSingleSessionEnabled) && (event.getReason().contains(m.getString("same_nick")))) {
			event.setCancelled(true);
			return;
		}

		final Player player = event.getPlayer();
		plugin.management.performQuit(player, true);
	}

	@EventHandler(priority = EventPriority.HIGHEST)
	public void onPlayerLogin(final PlayerLoginEvent event) {
		final Player player = event.getPlayer();
		if (player == null) {
			return;
		}
		final String name = player.getName().toLowerCase();
		final boolean isAuthAvailable = plugin.database.isAuthAvailable(name);

		if (Utils.isNPC(player) || Utils.isUnrestricted(player)) {
			return;
		}

		if (event.getResult() != PlayerLoginEvent.Result.ALLOWED) {
			return;
		}

		if (!Settings.countriesBlacklist.isEmpty() && !isAuthAvailable && !plugin.authmePermissible(player, "authme.bypassantibot")) {
			final String code = Utils.getCountryCode(event.getAddress().getHostAddress());
			if (((code == null) || Settings.countriesBlacklist.contains(code))) {
				event.setKickMessage(m.send("country_banned")[0]);
				event.setResult(PlayerLoginEvent.Result.KICK_OTHER);
				return;
			}
		}
		if (Settings.enableProtection && !Settings.countries.isEmpty() && !isAuthAvailable && !plugin.authmePermissible(player, "authme.bypassantibot")) {
			final String code = Utils.getCountryCode(event.getAddress().getHostAddress());
			if (((code == null) || !Settings.countries.contains(code))) {
				event.setKickMessage(m.send("country_banned")[0]);
				event.setResult(PlayerLoginEvent.Result.KICK_OTHER);
				return;
			}
		}

		// TODO: Add message to the messages file!!!
		if (Settings.isKickNonRegisteredEnabled && !isAuthAvailable) {
			if (Settings.antiBotInAction) {
				event.setKickMessage("AntiBot service in action! You actually need to be registered!");
				event.setResult(PlayerLoginEvent.Result.KICK_OTHER);
				return;
			} else {
				event.setKickMessage(m.send("reg_only")[0]);
				event.setResult(PlayerLoginEvent.Result.KICK_OTHER);
				return;
			}
		}

		final int min = Settings.getMinNickLength;
		final int max = Settings.getMaxNickLength;
		final String regex = Settings.getNickRegex;

		if (name.length() > max || name.length() < min) {
			event.setKickMessage(Arrays.toString(m.send("name_len")));
			event.setResult(PlayerLoginEvent.Result.KICK_OTHER);
			return;
		}
		try {
			if (!player.getName().matches(regex) || name.equalsIgnoreCase("Player")) {
				try {
					event.setKickMessage(m.send("regex")[0].replace("REG_EX", regex));
					event.setResult(PlayerLoginEvent.Result.KICK_OTHER);
				} catch (final Exception exc) {
					event.setKickMessage("allowed char : " + regex);
					event.setResult(PlayerLoginEvent.Result.KICK_OTHER);
				}
				return;
			}
		} catch (final PatternSyntaxException pse) {
			if (regex == null || regex.isEmpty()) {
				event.setKickMessage("Your nickname do not match");
				event.setResult(PlayerLoginEvent.Result.KICK_OTHER);
				return;
			}
			try {
				event.setKickMessage(m.send("regex")[0].replace("REG_EX", regex));
				event.setResult(PlayerLoginEvent.Result.KICK_OTHER);
			} catch (final Exception exc) {
				event.setKickMessage("allowed char : " + regex);
				event.setResult(PlayerLoginEvent.Result.KICK_OTHER);
			}
			return;
		}

		if (event.getResult() == PlayerLoginEvent.Result.ALLOWED) {
			checkAntiBotMod(player);
			if (Settings.bungee) {
				final ByteArrayDataOutput out = ByteStreams.newDataOutput();
				out.writeUTF("IP");
				player.sendPluginMessage(plugin, "BungeeCord", out.toByteArray());
			}
			return;
		}
		if (event.getResult() != PlayerLoginEvent.Result.KICK_FULL) {
			return;
		}
		if (!plugin.authmePermissible(player, "authme.vip")) {
			event.setKickMessage(m.send("kick_fullserver")[0]);
			event.setResult(PlayerLoginEvent.Result.KICK_FULL);
			return;
		}

		final int playersOnline = Utils.getOnlinePlayers().size();
		if (playersOnline > plugin.getServer().getMaxPlayers()) {
			event.allow();
		} else {
			final Player pl = plugin.generateKickPlayer(Utils.getOnlinePlayers());
			if (pl != null) {
				pl.kickPlayer(m.send("kick_forvip")[0]);
				event.allow();
			} else {
				ConsoleLogger.info("The player " + player.getName() + " tryed to join, but the server was full");
				event.setKickMessage(m.send("kick_fullserver")[0]);
				event.setResult(PlayerLoginEvent.Result.KICK_FULL);
			}
		}
	}

	@EventHandler(ignoreCancelled = true, priority = EventPriority.LOW)
	public void onPlayerLowChat(final AsyncPlayerChatEvent event) {
		handleChat(event);
	}

	@EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
	public void onPlayerMove(final PlayerMoveEvent event) {
		final Player player = event.getPlayer();
		if (Utils.checkAuth(player)) {
			return;
		}

		if (!Settings.isMovementAllowed) {
			final Location from = event.getFrom();
			from.setPitch(event.getTo().getPitch());
			from.setYaw(event.getTo().getYaw());

			event.setTo(from);
			return;
		}

		if (Settings.getMovementRadius == 0) {
			return;
		}

		final int radius = Settings.getMovementRadius;
		final Location spawn = plugin.getSpawnLocation(player);
		if (spawn != null && spawn.getWorld() != null) {
			if (!event.getPlayer().getWorld().equals(spawn.getWorld())) {
				event.getPlayer().teleport(spawn);
				return;
			}
			if ((spawn.distance(player.getLocation()) > radius)) {
				event.getPlayer().teleport(spawn);
			}
		}
	}

	@EventHandler(ignoreCancelled = true, priority = EventPriority.NORMAL)
	public void onPlayerNormalChat(final AsyncPlayerChatEvent event) {
		handleChat(event);
	}

	@EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST)
	public void onPlayerPickupItem(final PlayerPickupItemEvent event) {
		if (Utils.checkAuth(event.getPlayer())) {
			return;
		}
		event.setCancelled(true);
	}

	@EventHandler(priority = EventPriority.MONITOR)
	public void onPlayerQuit(final PlayerQuitEvent event) {
		if (event.getPlayer() == null) {
			return;
		}

		final Player player = event.getPlayer();
		final String name = player.getName().toLowerCase();

		plugin.management.performQuit(player, false);

		if (!PlayerCache.getInstance().isAuthenticated(name) && Settings.enableProtection) {
			event.setQuitMessage(null);
		}
	}

	@EventHandler(priority = EventPriority.HIGHEST)
	public void onPlayerRespawn(final PlayerRespawnEvent event) {
		final Player player = event.getPlayer();
		if (player == null || Utils.checkAuth(player)) {
			return;
		}
		final String name = player.getName().toLowerCase();
		final Location spawn = plugin.getSpawnLocation(player);
		if (Settings.isSaveQuitLocationEnabled && plugin.database.isAuthAvailable(name)) {
			final PlayerAuth auth = new PlayerAuth(name, spawn.getX(), spawn.getY(), spawn.getZ(), spawn.getWorld().getName(), player.getName());
			plugin.database.updateQuitLoc(auth);
		}
		if (spawn != null && spawn.getWorld() != null) {
			event.setRespawnLocation(spawn);
		}
	}

	@EventHandler(ignoreCancelled = true, priority = EventPriority.NORMAL)
	public void onPlayerShear(final PlayerShearEntityEvent event) {
		final Player player = event.getPlayer();
		if (player == null || Utils.checkAuth(player)) {
			return;
		}
		event.setCancelled(true);
	}

	@EventHandler(priority = EventPriority.HIGHEST)
	public void onPreLogin(final AsyncPlayerPreLoginEvent event) {
		final String name = event.getName().toLowerCase();
		final Player player = Bukkit.getServer().getPlayer(name);
		if (player == null) {
			return;
		}

		// Check if forceSingleSession is set to true, so kick player that has
		// joined with same nick of online player
		if (Settings.isForceSingleSessionEnabled && plugin.dataManager.isOnline(player, name)) {
			event.setKickMessage(m.send("same_nick")[0]);
			event.setLoginResult(AsyncPlayerPreLoginEvent.Result.KICK_OTHER);
			if (LimboCache.getInstance().hasLimboPlayer(name)) {
				Bukkit.getScheduler().runTaskAsynchronously(plugin, new Runnable() {

					@Override
					public void run() {
						final LimboPlayer limbo = LimboCache.getInstance().getLimboPlayer(player.getName().toLowerCase());
						if (limbo != null && PlayerCache.getInstance().isAuthenticated(player.getName().toLowerCase())) {
							Utils.addNormal(player, limbo.getGroup());
							LimboCache.getInstance().deleteLimboPlayer(player.getName().toLowerCase());
						}
					}
				});
			}
		}
	}

	@EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)
	public void onSignChange(final SignChangeEvent event) {
		if (Utils.checkAuth(event.getPlayer())) {
			return;
		}
		event.setCancelled(true);
	}

	@EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)
	public void playerHitPlayerEvent(final EntityDamageByEntityEvent event) {
		final Entity damager = event.getDamager();
		if (!(damager instanceof Player)) {
			return;
		}
		if (Utils.checkAuth((Player) damager)) {
			return;
		}

		event.setCancelled(true);
	}

	private void checkAntiBotMod(final Player player) {
		if (plugin.delayedAntiBot || plugin.antibotMod) {
			return;
		}
		if (plugin.authmePermissible(player, "authme.bypassantibot")) {
			return;
		}
		if (antibot.size() > Settings.antiBotSensibility) {
			plugin.switchAntiBotMod(true);
			for (final String s : m.send("antibot_auto_enabled")) {
				Bukkit.broadcastMessage(s);
			}
			Bukkit.getScheduler().scheduleSyncDelayedTask(plugin, new Runnable() {

				@Override
				public void run() {
					if (plugin.antibotMod) {
						plugin.switchAntiBotMod(false);
						antibot.clear();
						for (final String s : m.send("antibot_auto_disabled")) {
							Bukkit.broadcastMessage(s.replace("%m", "" + Settings.antiBotDuration));
						}
					}
				}
			}, Settings.antiBotDuration * 1200);
			return;
		}
		antibot.add(player.getName().toLowerCase());
		Bukkit.getScheduler().scheduleSyncDelayedTask(plugin, new Runnable() {

			@Override
			public void run() {
				antibot.remove(player.getName().toLowerCase());
			}
		}, 300);
	}

	private void handleChat(final AsyncPlayerChatEvent event) {
		final Player player = event.getPlayer();
		if (!Utils.checkAuth(player)) {
			final String cmd = event.getMessage().split(" ")[0];
			if (!Settings.isChatAllowed && !(Settings.allowCommands.contains(cmd))) {
				event.setCancelled(true);
			}
			if (plugin.database.isAuthAvailable(player.getName().toLowerCase())) {
				m.send(player, "login_msg");
			} else {
				if (Settings.emailRegistration) {
					m.send(player, "reg_email_msg");
				} else {
					m.send(player, "reg_msg");
				}
			}
		}
	}

}
