package us.talabrek.ultimateskyblock.player;

import io.papermc.lib.PaperLib;
import org.apache.commons.lang.Validate;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerMoveEvent;
import org.bukkit.scheduler.BukkitTask;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import us.talabrek.ultimateskyblock.Settings;
import us.talabrek.ultimateskyblock.uSkyBlock;
import us.talabrek.ultimateskyblock.util.LocationUtil;
import dk.lockfuglsang.minecraft.util.TimeUtil;

import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;

import static dk.lockfuglsang.minecraft.po.I18nUtil.tr;

/**
 * Responsible for teleporting (and cancelling teleporting) of players.
 */
public class TeleportLogic implements Listener {
    private static final Logger log = Logger.getLogger(TeleportLogic.class.getName());

    private final uSkyBlock plugin;
    private final int teleportDelay;
    private final Map<UUID, PendingTeleport> pendingTeleports = new ConcurrentHashMap<>();
    private final double cancelDistance;

    public TeleportLogic(uSkyBlock plugin) {
        this.plugin = plugin;
        teleportDelay = plugin.getConfig().getInt("options.island.islandTeleportDelay", 2);
        cancelDistance = plugin.getConfig().getDouble("options.island.teleportCancelDistance", 0.2);
        plugin.getServer().getPluginManager().registerEvents(this, plugin);
    }

    /**
     * Teleport the given {@link Player} to his island home.
     * @param player Player to teleport
     * @param force True to override teleport delay, false otherwise.
     */
    public void homeTeleport(@NotNull Player player, boolean force) {
        Validate.notNull(player, "Player cannot be null");

        Location homeLocation = null;
        PlayerInfo playerInfo = plugin.getPlayerLogic().getPlayerInfo(player);

        if (playerInfo != null) {
            homeLocation = plugin.getSafeHomeLocation(playerInfo);
        }

        if (homeLocation == null) {
            player.sendMessage(tr("\u00a74Unable to find a safe home-location on your island!"));
            if (player.isFlying()) {
                player.sendMessage(tr("\u00a7cWARNING: \u00a7eTeleporting you to mid-air."));
                safeTeleport(player, playerInfo.getIslandLocation(), true);
            }
            return;
        }

        plugin.getWorldManager().removeCreatures(homeLocation);
        player.sendMessage(tr("\u00a7aTeleporting you to your island."));
        safeTeleport(player, homeLocation, force);
    }

    /**
     * Teleport the given {@link Player} to the given {@link Location}, loading the {@link org.bukkit.Chunk} before
     * teleporting and with the configured teleport delay if applicable.
     * @param player Player to teleport.
     * @param targetLocation Location to teleport the player to.
     * @param force True to override teleport delay, false otherwise.
     */
    public void safeTeleport(@NotNull Player player, @NotNull Location targetLocation, boolean force) {
        Validate.notNull(player, "Player cannot be null");
        Validate.notNull(targetLocation, "TargetLocation cannot be null");

        log.log(Level.FINER, "safeTeleport " + player + " to " + targetLocation + (force ? " with force" : ""));
        final Location targetLoc = LocationUtil.centerOnBlock(targetLocation.clone());
        if (player.hasPermission("usb.mod.bypassteleport") || (teleportDelay == 0) || force) {
            PaperLib.teleportAsync(player, targetLoc);
        } else {
            player.sendMessage(tr("\u00a7aYou will be teleported in {0} seconds.", teleportDelay));
            BukkitTask tpTask = plugin.sync(() -> {
                pendingTeleports.remove(player.getUniqueId());
                PaperLib.teleportAsync(player, targetLoc);
            }, TimeUtil.secondsAsMillis(teleportDelay));
            pendingTeleports.put(player.getUniqueId(), new PendingTeleport(player.getLocation(), tpTask));
        }
    }

    /**
     * Teleport the given {@link Player} to the spawn in the island world, loading the {@link org.bukkit.Chunk} before
     * teleporting and with the configured teleport delay if applicable.
     * @param player Player to teleport.
     * @param force True to override teleport delay, false otherwise.
     */
    public void spawnTeleport(@NotNull Player player, boolean force) {
        Validate.notNull(player, "Player cannot be null");

        Location spawnLocation = LocationUtil.centerOnBlock(plugin.getWorldManager().getWorld().getSpawnLocation());
        if (player.hasPermission("usb.mod.bypassteleport") || (teleportDelay == 0) || force) {
            if (Settings.extras_sendToSpawn) {
                plugin.execCommand(player, "op:spawn", false);
            } else {
                PaperLib.teleportAsync(player, spawnLocation);
            }
        } else {
            player.sendMessage(tr("\u00a7aYou will be teleported in {0} seconds.", teleportDelay));
            BukkitTask tpTask = plugin.sync(() -> {
                pendingTeleports.remove(player.getUniqueId());
                if (Settings.extras_sendToSpawn) {
                    plugin.execCommand(player, "op:spawn", false);
                } else {
                    PaperLib.teleportAsync(player, spawnLocation);
                }
            }, TimeUtil.secondsAsMillis(teleportDelay));
            pendingTeleports.put(player.getUniqueId(), new PendingTeleport(player.getLocation(), tpTask));
        }
    }

    /**
     * Teleport the given {@link Player} to the warp location for the given {@link PlayerInfo}.
     * @param player Player to teleport.
     * @param playerInfo PlayerInfo to lookup the target warp location.
     * @param force True to override teleport delay, false otherwise.
     */
    public void warpTeleport(@NotNull Player player, @Nullable PlayerInfo playerInfo, boolean force) {
        Validate.notNull(player, "Player cannot be null");

        Location warpLocation = null;
        if (playerInfo == null) {
            player.sendMessage(tr("\u00a74That player does not exist!"));
            return;
        }

        warpLocation = plugin.getSafeWarpLocation(playerInfo);
        if (warpLocation == null) {
            player.sendMessage(tr("\u00a74Unable to warp you to that player''s island!"));
            return;
        }
        player.sendMessage(tr("\u00a7aTeleporting you to {0}''s island.", playerInfo.getDisplayName()));
        safeTeleport(player, warpLocation, force);
    }

    @EventHandler(priority = EventPriority.HIGHEST,  ignoreCancelled = true)
    @SuppressWarnings("unused")
    public void onPlayerMove(PlayerMoveEvent e) {
        PendingTeleport pendingTeleport = pendingTeleports.get(e.getPlayer().getUniqueId());
        if (pendingTeleport != null) {
            pendingTeleport.playerMoved(e.getPlayer());
        }
    }

    private class PendingTeleport {
        private final Location location;
        private final BukkitTask task;

        private PendingTeleport(Location location, BukkitTask task) {
            this.location = location != null ? location.clone() : null;
            this.task = task;
        }

        public Location getLocation() {
            return location;
        }

        public BukkitTask getTask() {
            return task;
        }

        void playerMoved(Player player) {
            Location newLocation = player.getLocation();
            if (location != null && location.getWorld() != null && location.getWorld().equals(newLocation.getWorld())) {
                double distance = location.distance(newLocation);
                if (distance > cancelDistance) {
                    task.cancel();
                    pendingTeleports.remove(player.getUniqueId());
                    player.sendMessage(tr("\u00a77Teleport cancelled"));
                }
            }
        }
    }
}