/*
 * This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package fr.neatmonster.nocheatplus.checks.moving.util;

import java.util.UUID;

import org.bukkit.Chunk;
import org.bukkit.GameMode;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.event.player.PlayerMoveEvent;

import fr.neatmonster.nocheatplus.NCPAPIProvider;
import fr.neatmonster.nocheatplus.checks.CheckType;
import fr.neatmonster.nocheatplus.checks.moving.MovingConfig;
import fr.neatmonster.nocheatplus.checks.moving.MovingData;
import fr.neatmonster.nocheatplus.checks.moving.magic.Magic;
import fr.neatmonster.nocheatplus.checks.moving.model.MoveData;
import fr.neatmonster.nocheatplus.checks.moving.model.PlayerMoveData;
import fr.neatmonster.nocheatplus.checks.moving.player.PlayerSetBackMethod;
import fr.neatmonster.nocheatplus.checks.net.NetData;
import fr.neatmonster.nocheatplus.checks.net.model.CountableLocation;
import fr.neatmonster.nocheatplus.compat.Bridge1_9;
import fr.neatmonster.nocheatplus.compat.BridgeMisc;
import fr.neatmonster.nocheatplus.compat.MCAccess;
import fr.neatmonster.nocheatplus.components.debug.IDebugPlayer;
import fr.neatmonster.nocheatplus.logging.StaticLog;
import fr.neatmonster.nocheatplus.players.DataManager;
import fr.neatmonster.nocheatplus.players.IPlayerData;
import fr.neatmonster.nocheatplus.utilities.CheckUtils;
import fr.neatmonster.nocheatplus.utilities.location.LocUtil;
import fr.neatmonster.nocheatplus.utilities.location.PlayerLocation;
import fr.neatmonster.nocheatplus.utilities.location.RichBoundsLocation;
import fr.neatmonster.nocheatplus.utilities.location.TrigUtil;
import fr.neatmonster.nocheatplus.utilities.map.BlockProperties;
import fr.neatmonster.nocheatplus.utilities.map.MapUtil;

/**
 * Static utility methods.
 * @author dev1mc
 *
 */
public class MovingUtil {

    /**
     * Always set world to null after use, careful with nested methods. Main thread only.
     */
    private static final Location useLoc = new Location(null, 0, 0, 0);

    /**
     * Check if the player is to be checked by the survivalfly check.<br>
     * Primary thread only.
     * 
     * @param player
     * @param fromLoc
     *            The location the player is moving from or just where the
     *            player is.
     * @param data
     * @param cc
     * @return
     */
    public static final boolean shouldCheckSurvivalFly(final Player player, final PlayerLocation fromLocation, 
            final MovingData data, final MovingConfig cc, final IPlayerData pData) {
        final GameMode gameMode = player.getGameMode();
        // (Full activation check - use permission caching for performance rather.)
        return  pData.isCheckActive(CheckType.MOVING_SURVIVALFLY, player) 
                && gameMode != BridgeMisc.GAME_MODE_SPECTATOR
                && (cc.ignoreCreative || gameMode != GameMode.CREATIVE) && !player.isFlying() 
                && (cc.ignoreAllowFlight || !player.getAllowFlight())
                && (
                        !Bridge1_9.isGlidingWithElytra(player) 
                        || !isGlidingWithElytraValid(player, fromLocation, data, cc)
                        )
                && (
                        Double.isInfinite(Bridge1_9.getLevitationAmplifier(player)) 
                        || fromLocation.isInLiquid()
                        )
                ;
    }

    /**
     * Consistency / cheat check. Prerequisite is
     * Bridge1_9.isGlidingWithElytra(player) having returned true.
     * 
     * @param player
     * @param fromLocation
     * @param data
     * @param cc
     * @return
     */
    public static boolean isGlidingWithElytraValid(final Player player, final PlayerLocation fromLocation, 
            final MovingData data, final MovingConfig cc) {

        // TODO: Configuration for which/if to check on either lift-off / unknown / gliding.
        // TODO: Item durability?
        // TODO: TEST LAVA (ordinary and boost, lift off and other).
        /*
         * TODO: Allow if last move not touched ground (+-) after all the
         * onGround check isn't much needed, if we can test for the relevant stuff (web?).
         */

        // Check start glide conditions.
        final PlayerMoveData firstPastMove = data.playerMoves.getFirstPastMove();
        if (
                // Skip lift-off conditions if the EntityToggleGlideEvent is present (checked there).
                !Bridge1_9.hasEntityToggleGlideEvent()
                // Otherwise only treat as lift-off, if not already gliding.
                && !firstPastMove.toIsValid || firstPastMove.modelFlying == null 
                || !MovingConfig.ID_JETPACK_ELYTRA.equals(firstPastMove.modelFlying.getId())) {
            // Treat as a lift off.
            // TODO: Past map states might allow lift off (...).
            return canLiftOffWithElytra(player, fromLocation, data);
        }

        /*
         * TODO: Test / verify it gets turned off if depleted during gliding,
         * provided the client doesn't help knowing. (Only shortly tested with
         * grep -r "ItemElytra.d" <- looks good.)
         */
        //        // Test late, as lift-off check also tests for this.
        //        if (InventoryUtil.isItemBroken(player.getInventory().getChestplate())) {
        //            return false;
        //        }

        /*
         * TODO: Rather focus on abort conditions (in-medium stay time for
         * special blocks, sleeping / dead / ...)?
         */

        // Only the web can stop a player who isn't propelled by a rocket.
        return data.fireworksBoostDuration > 0 || !BlockProperties.collidesId(fromLocation.getBlockCache(), 
                fromLocation.getMinX(), fromLocation.getMinY(), fromLocation.getMinZ(), 
                fromLocation.getMaxX(), fromLocation.getMinY() + 0.6, fromLocation.getMaxZ(), Material.WEB);
    }

    /**
     * Check lift-off (CB: on ground is done wrongly, inWater probably is
     * correct, web is not checked).
     * 
     * @param fromLocation
     * @param data
     * @return
     */
    public static boolean canLiftOffWithElytra(final Player player, final PlayerLocation loc, 
            final MovingData data) {
        // TODO: Item durability here too?
        // TODO: this/firstPast- Move not touching or not explicitly on ground would be enough?
        return 
                loc.isPassableBox() // Full box as if standing for lift-off.
                && !loc.isInWeb()
                // Durability is checked within PlayerConnection (toggling on).
                // && !InventoryUtil.isItemBroken(player.getInventory().getChestplate())
                /*
                 * TODO: Could be a problem with too high yOnGround. Actual
                 * cheating rather would have to be prevented by monitoring
                 * jumping more tightly (low jump = set back, needs
                 * false-positive-free checking (...)).
                 */
                && !loc.isOnGround(0.001) 
                // Assume water is checked correctly.
                //                && (
                //                        !fromLocation.isInLiquid() // (Needs to check for actual block bounds).
                //                        /*
                //                         * Observed with head free, but feet clearly in water:
                //                         * lift off from water (not 100% easy to do).
                //                         */
                //                        || !BlockProperties.isLiquid(fromLocation.getTypeId())
                //                        || !BlockProperties.isLiquid(fromLocation.getTypeIdAbove())
                //                        )
                ;
    }

    /**
     * Workaround for getEyeHeight not accounting for special conditions like
     * gliding with elytra. (Sleeping is not checked.)
     * 
     * @param player
     * @return
     */
    public static double getEyeHeight(final Player player) {
        // TODO: Need a variant/method to test legitimate state transitions?
        // TODO: EntityToggleGlideEvent
        return Bridge1_9.isGlidingWithElytra(player) ? 0.4 : player.getEyeHeight();
    }

    /**
     * Initialize pLoc with edge data specific to gliding with elytra.
     * 
     * @param player
     * @param pLoc
     * @param loc
     * @param yOnGround
     * @param mcAccess
     */
    public static void setElytraProperties(final Player player, final PlayerLocation pLoc, final Location loc,
            final double yOnGround, final MCAccess mcAccess) {
        pLoc.set(loc, player, mcAccess.getWidth(player), 0.4, 0.6, 0.6, yOnGround);
    }

    /**
     * Handle an illegal move by a player, attempt to restore a valid location.
     * <br>
     * NOTE: event.setTo is used to not leave a gap.
     * 
     * @param event
     * @param player
     * @param data
     * @param cc
     */
    public static void handleIllegalMove(final PlayerMoveEvent event, final Player player, 
            final MovingData data, final MovingConfig cc)
    {
        // This might get extended to a check-like thing.
        boolean restored = false;
        final PlayerLocation pLoc = new PlayerLocation(NCPAPIProvider.getNoCheatPlusAPI().getGenericInstanceHandle(MCAccess.class), null);
        // (Mind that we don't set the block cache here).
        final Location loc = player.getLocation();
        if (!restored && data.hasSetBack()) {
            final Location setBack = data.getSetBack(loc); 
            pLoc.set(setBack, player);
            if (!pLoc.hasIllegalCoords() && (cc.ignoreStance || !pLoc.hasIllegalStance())) {
                event.setFrom(setBack);
                event.setTo(setBack);
                restored = true;
            }
            else {
                data.resetSetBack();
            }
        } 
        if (!restored) {
            pLoc.set(loc, player);
            if (!pLoc.hasIllegalCoords() && (cc.ignoreStance || !pLoc.hasIllegalStance())) {
                event.setFrom(loc);
                event.setTo(loc);
                restored = true;
            }
        }
        pLoc.cleanup();
        if (!restored) {
            // TODO: reset the bounding box of the player ?
            if (cc.tempKickIllegal) {
                NCPAPIProvider.getNoCheatPlusAPI().denyLogin(player.getName(), 24L * 60L * 60L * 1000L);
                StaticLog.logSevere("[NCP] could not restore location for " + player.getName() + ", kicking them and deny login for 24 hours");
            } else {
                StaticLog.logSevere("[NCP] could not restore location for " + player.getName() + ", kicking them.");
            }
            CheckUtils.kickIllegalMove(player, cc);
        }
    }

    /**
     * Used for a workaround that resets the set back for the case of jumping on just placed blocks.
     * @param id
     * @return
     */
    public static boolean canJumpOffTop(final Material blockType) {
        return BlockProperties.isGround(blockType) || BlockProperties.isSolid(blockType);
    }

    /**
     * Check the context-independent pre-conditions for checking for untracked
     * locations (not the world spawn, location is not passable, passable is
     * enabled for the player).
     * 
     * @param player
     * @param loc
     * @return
     */
    public static boolean shouldCheckUntrackedLocation(final Player player, 
            final Location loc, final IPlayerData pData) {
        return !TrigUtil.isSamePos(loc, loc.getWorld().getSpawnLocation()) 
                && !BlockProperties.isPassable(loc)
                && pData.isCheckActive(CheckType.MOVING_PASSABLE, player);
    }

    /**
     * Detect if the given location is an untracked spot. This is spots for
     * which a player is at the location, but the moving data has another
     * "last to" position set for that player. Note that one matching player
     * with "last to" being consistent is enough to let this return null, world spawn is exempted.
     * <hr>
     * Pre-conditions:<br>
     * <li>Context-specific (e.g. activation flags for command, teleport).</li>
     * <li>See MovingUtils.shouldCheckUntrackedLocation.</li>
     * 
     * @param loc
     * @return Corrected location, if loc is an "untracked location".
     */
    public static Location checkUntrackedLocation(final Location loc) {
        // TODO: More efficient method to get entities at the same position (might use MCAccess).
        final Chunk toChunk = loc.getChunk();
        final Entity[] entities = toChunk.getEntities();
        MovingData untrackedData = null;
        for (int i = 0; i < entities.length; i++) {
            final Entity entity = entities[i];
            if (entity.getType() != EntityType.PLAYER) {
                continue;
            }
            final Location refLoc = entity.getLocation(useLoc);
            // Exempt world spawn.
            // TODO: Exempt other warps -> HASH based exemption (expire by time, keep high count)?
            if (TrigUtil.isSamePos(loc, refLoc) && (entity instanceof Player)) {
                final Player other = (Player) entity;
                final IPlayerData otherPData = DataManager.getPlayerData(other);
                final MovingData otherData = otherPData.getGenericInstance(MovingData.class);
                final PlayerMoveData otherLastMove = otherData.playerMoves.getFirstPastMove();
                if (!otherLastMove.toIsValid) {
                    // Data might have been removed.
                    // TODO: Consider counting as tracked?
                    continue;
                }
                else if (TrigUtil.isSamePos(refLoc, otherLastMove.to.getX(), otherLastMove.to.getY(), otherLastMove.to.getZ())) {
                    // Tracked.
                    return null;
                }
                else {
                    // Untracked location.
                    // TODO: Discard locations in the same block, if passable.
                    // TODO: Sanity check distance?
                    // More leniency: allow moving inside of the same block.
                    if (TrigUtil.isSameBlock(loc, otherLastMove.to.getX(), otherLastMove.to.getY(), otherLastMove.to.getZ()) && !BlockProperties.isPassable(refLoc.getWorld(), otherLastMove.to.getX(), otherLastMove.to.getY(), otherLastMove.to.getZ())) {
                        continue;
                    }
                    untrackedData = otherData;
                }
            }
        }
        useLoc.setWorld(null); // Cleanup.
        if (untrackedData == null) {
            return null;
        }
        else {
            // TODO: Count and log to TRACE_FILE, if multiple locations would match (!).
            final PlayerMoveData lastMove = untrackedData.playerMoves.getFirstPastMove();
            return new Location(loc.getWorld(), lastMove.to.getX(), lastMove.to.getY(), lastMove.to.getZ(), loc.getYaw(), loc.getPitch());
        }
    }

    /**
     * Convenience method for the case that the server has already reset the
     * fall distance, e.g. with micro moves.
     * 
     * @param player
     * @param fromY
     * @param toY
     * @param data
     * @return
     */
    public static double getRealisticFallDistance(final Player player, 
            final double fromY, final double toY, 
            final MovingData data, final IPlayerData pData) {
        if (pData.isCheckActive(CheckType.MOVING_NOFALL, player)) {
            // (NoFall will not be checked, if this method is called.)
            if (data.noFallMaxY >= fromY ) {
                return Math.max(0.0, data.noFallMaxY - toY);
            } else {
                return Math.max(0.0, fromY - toY); // Skip to avoid exploits: + player.getFallDistance()
            }
        } else {
            // TODO: This would ignore the first split move, if this is the second one.
            return (double) player.getFallDistance() + Math.max(0.0, fromY - toY);
        }
    }

    /**
     * Ensure we have a set back location set, plus allow moving from upwards
     * with respawn/login. Intended for MovingListener (pre-checks).
     * 
     * @param player
     * @param from
     * @param data
     */
    public static void checkSetBack(final Player player, final PlayerLocation from, 
            final MovingData data, final IPlayerData pData, final IDebugPlayer idp) {
        if (!data.hasSetBack()) {
            data.setSetBack(from);
        }
        else if (data.joinOrRespawn && from.getY() > data.getSetBackY() && 
                TrigUtil.isSamePos(from.getX(), from.getZ(), data.getSetBackX(), data.getSetBackZ()) &&
                (from.isOnGround() || from.isResetCond())) {
            // TODO: Move most to a method?
            // TODO: Is a margin needed for from.isOnGround()? [bukkitapionly]
            if (pData.isDebugActive(CheckType.MOVING)) {
                // TODO: Should this be info?
                idp.debug(player, "Adjust set back after join/respawn: " + from.getLocation());
            }
            data.setSetBack(from);
            data.resetPlayerPositions(from);
        }
    }

    public static double getJumpAmplifier(final Player player, final MCAccess mcAccess) {
        final double amplifier = mcAccess.getJumpAmplifier(player);
        if (Double.isInfinite(amplifier)) {
            return 0.0;
        }
        else {
            return 1.0 + amplifier;
        }
    }

    public static void prepareFullCheck(final RichBoundsLocation from, final RichBoundsLocation to, final MoveData thisMove, final double yOnGround) {
        // Collect block flags.
        from.collectBlockFlags(yOnGround);
        if (from.isSamePos(to)) {
            // TODO: Could consider pTo = pFrom, set pitch / yaw elsewhere.
            // Sets all properties, but only once.
            to.prepare(from);
        }
        else {
            // Might collect block flags for small distances with the containing bounds for both. 
            to.collectBlockFlags(yOnGround);
        }

        // Set basic properties for past move bookkeeping.
        thisMove.setExtraProperties(from, to);
    }

    /**
     * Ensure nearby chunks are loaded so that the move can be processed at all.
     * Assume too big moves to be cancelled anyway and/or checks like passable
     * accounting for chunk load. Further skip chunk loading if the latest
     * stored past move has extra properties set and is close by.
     * 
     * @param from
     * @param to
     * @param lastMove
     * @param tag
     *            The type of context/event for debug logging.
     * @param data
     * @param cc
     */
    public static void ensureChunksLoaded(final Player player, 
            final Location from, final Location to, final PlayerMoveData lastMove, 
            final String tag, final MovingConfig cc, final IPlayerData pData) {
        // (Worlds must be equal. Ensured in player move handling.)
        final double x0 = from.getX();
        final double z0 = from.getZ();
        final double x1 = to.getX();
        final double z1 = to.getZ();
        if (TrigUtil.distanceSquared(x0, z0, x1, z1) > 2.0 * Magic.CHUNK_LOAD_MARGIN_MIN) {
            // Assume extreme move to trigger.
            return;
        }
        boolean loadFrom = true;
        boolean loadTo = true;
        double margin = Magic.CHUNK_LOAD_MARGIN_MIN;
        // Heuristic for if loading may be necessary at all.
        if (lastMove.toIsValid && lastMove.to.extraPropertiesValid) {
            if (TrigUtil.distanceSquared(lastMove.to, x0, z0) < 1.0) {
                loadFrom = false;
            }
            if (TrigUtil.distanceSquared(lastMove.to, x1, z1) < 1.0) {
                loadTo = false;
            }
        }
        else if (lastMove.valid && lastMove.from.extraPropertiesValid
                && cc.loadChunksOnJoin) {
            // TODO: Might need to distinguish join/teleport/world-change later.
            if (TrigUtil.distanceSquared(lastMove.from, x0, z0) < 1.0) {
                loadFrom = false;
            }
            if (TrigUtil.distanceSquared(lastMove.from, x1, z1) < 1.0) {
                loadTo = false;
            }
        }
        int loaded = 0;
        if (loadFrom) {
            loaded += MapUtil.ensureChunksLoaded(from.getWorld(), x0, z0, margin);
            if (TrigUtil.distanceSquared(x0, z0, x1, z1) < 1.0) {
                loadTo = false;
            }
        }
        if (loadTo) {
            loaded += MapUtil.ensureChunksLoaded(to.getWorld(), x1, z1, margin);
        }
        if (loaded > 0 && pData.isDebugActive(CheckType.MOVING)) {
            StaticLog.logInfo("Player " + tag + ": Loaded " + loaded + " chunk" + (loaded == 1 ? "" : "s") + " for the world " + from.getWorld().getName() +  " for player: " + player.getName());
        }
    }

    /**
     * Ensure nearby chunks are loaded. Further skip chunk loading if the latest
     * stored past move has extra properties set and is close by.
     * 
     * @param player
     * @param loc
     * @param tag
     *            The type of context/event for debug logging.
     * @param data
     * @param cc
     */
    public static void ensureChunksLoaded(final Player player, 
            final Location loc, final String tag, 
            final MovingData data, final MovingConfig cc, 
            final IPlayerData pData) {
        final PlayerMoveData lastMove = data.playerMoves.getFirstPastMove();
        final double x0 = loc.getX();
        final double z0 = loc.getZ();
        // Heuristic for if loading may be necessary at all.
        if (lastMove.toIsValid && lastMove.to.extraPropertiesValid) {
            if (TrigUtil.distanceSquared(lastMove.to, x0, z0) < 1.0) {
                return;
            }
        }
        else if (lastMove.valid && lastMove.from.extraPropertiesValid
                && cc.loadChunksOnJoin) {
            // TODO: Might need to distinguish join/teleport/world-change later.
            if (TrigUtil.distanceSquared(lastMove.from, x0, z0) < 1.0) {
                return;
            }
        }
        int loaded = MapUtil.ensureChunksLoaded(loc.getWorld(), loc.getX(), loc.getZ(), Magic.CHUNK_LOAD_MARGIN_MIN);
        if (loaded > 0 && pData.isDebugActive(CheckType.MOVING)) {
            StaticLog.logInfo("Player " + tag + ": Loaded " + loaded + " chunk" + (loaded == 1 ? "" : "s") + " for the world " + loc.getWorld().getName() +  " for player: " + player.getName());
        }
    }

    /**
     * Test if a set back is set in data and scheduled. <br>
     * Primary thread only.
     * 
     * @param player
     * @return
     */
    public static boolean hasScheduledPlayerSetBack(final Player player) {
        return hasScheduledPlayerSetBack(player.getUniqueId(), 
                DataManager.getGenericInstance(player, MovingData.class));
    }

    /**
     * Test if a set back is set in data and scheduled. <br>
     * Primary thread only.
     * @param playerId
     * @param data
     * @return
     */
    public static boolean hasScheduledPlayerSetBack(final UUID playerId, final MovingData data) {
        return data.hasTeleported() && isPlayersetBackScheduled(playerId);
    }

    private static boolean isPlayersetBackScheduled(final UUID playerId) {
        final IPlayerData pd = DataManager.getPlayerData(playerId);
        return pd != null  && pd.isPlayerSetBackScheduled();
    }

    /**
     * 
     * @param player
     * @param debugMessagePrefix
     * @return True, if the teleport has been successful.
     */
    public static boolean processStoredSetBack(final Player player, 
            final String debugMessagePrefix, final IPlayerData pData) {
        final MovingData data = pData.getGenericInstance(MovingData.class);
        final boolean debug = pData.isDebugActive(CheckType.MOVING);
        if (!data.hasTeleported()) {
            if (debug) {
                CheckUtils.debug(player, CheckType.MOVING, debugMessagePrefix + "No stored location available.");
            }
            return false;
        }
        // (teleported is set.).

        final Location loc = player.getLocation(useLoc);
        if (data.isTeleportedPosition(loc)) {
            // Skip redundant teleport.
            if (debug) {
                CheckUtils.debug(player, CheckType.MOVING, debugMessagePrefix + "Skip teleport, player is there, already.");
            }
            data.resetTeleported(); // Not necessary to keep.
            useLoc.setWorld(null);
            return false;
        }
        useLoc.setWorld(null);
        // (player is somewhere else.)

        // Post-1.9 packet level workaround.
        final MovingConfig cc = pData.getGenericInstance(MovingConfig.class);
        // TODO: Consider to skip checking for packet level, if not available (plus optimize access).
        // TODO: Consider a config flag, so this can be turned off (set back method).
        final PlayerSetBackMethod method = cc.playerSetBackMethod;
        if (!method.shouldNoRisk() 
                && (method.shouldCancel() || method.shouldSetTo()) && method.shouldUpdateFrom()) {
            /*
             * Another leniency option: Skip, if we have already received an ACK
             * for this position on packet level - typically the next move would
             * confirm the set-back, but a redundant teleport would freeze the
             * player for a slightly longer time. This could happen with the set
             * back being at the coordinates the player had just been at, but
             * between set back and on-tick there has been a micro move (not
             * firing a PlayerMoveEvent) - similarly observed on a local test
             * server once, HOWEVER there the micro move had been a look-only
             * packet, not explaining why the position of the player wasn't
             * reflecting the outgoing position. So here remains the uncertainty
             * concerning the question if a (silent) Minecraft entity teleport
             * always follows a cancelled PlayerMoveEvent (!), and a thinkable
             * potential for abuse.
             */
            // (CANCEL + UPDATE_FROM mean a certain teleport to the set back, still could be repeated tp.)
            // TODO: Better method, full sync reference?
            final CountableLocation cl = pData.getGenericInstance(NetData.class).teleportQueue.getLastAck();
            if (data.isTeleportedPosition(cl)) {
                if (debug) {
                    CheckUtils.debug(player, CheckType.MOVING, debugMessagePrefix + "Skip teleport, having received an ACK for the teleport on packet level. Player is at: " + LocUtil.simpleFormat(loc));
                }
                // Keep teleported in data. Subject to debug logs and/or discussion.
                return false;
            }
        }
        // (No ACK received yet.)

        // Attempt to teleport.
        final Location teleported = data.getTeleported();
        // (Data resetting is done during PlayerTeleportEvent handling.)
        if (player.teleport(teleported, BridgeMisc.TELEPORT_CAUSE_CORRECTION_OF_POSITION)) {
            return true;
        }
        else {
            if (debug) {
                CheckUtils.debug(player, CheckType.MOVING, "Player set back on tick: Teleport failed.");
            }
            return false;
        }
    }

}
