/*
 * 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;

import java.util.Collection;
import java.util.concurrent.Callable;

import org.bukkit.Location;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;

import fr.neatmonster.nocheatplus.NCPAPIProvider;
import fr.neatmonster.nocheatplus.checks.CheckType;
import fr.neatmonster.nocheatplus.checks.access.ACheckData;
import fr.neatmonster.nocheatplus.checks.access.IRemoveSubCheckData;
import fr.neatmonster.nocheatplus.checks.moving.location.setback.DefaultSetBackStorage;
import fr.neatmonster.nocheatplus.checks.moving.location.tracking.LocationTrace;
import fr.neatmonster.nocheatplus.checks.moving.location.tracking.LocationTrace.TraceEntryPool;
import fr.neatmonster.nocheatplus.checks.moving.magic.Magic;
import fr.neatmonster.nocheatplus.checks.moving.model.LiftOffEnvelope;
import fr.neatmonster.nocheatplus.checks.moving.model.MoveConsistency;
import fr.neatmonster.nocheatplus.checks.moving.model.MoveTrace;
import fr.neatmonster.nocheatplus.checks.moving.model.PlayerMoveData;
import fr.neatmonster.nocheatplus.checks.moving.model.VehicleMoveData;
import fr.neatmonster.nocheatplus.checks.moving.velocity.AccountEntry;
import fr.neatmonster.nocheatplus.checks.moving.velocity.FrictionAxisVelocity;
import fr.neatmonster.nocheatplus.checks.moving.velocity.SimpleAxisVelocity;
import fr.neatmonster.nocheatplus.checks.moving.velocity.SimpleEntry;
import fr.neatmonster.nocheatplus.checks.moving.velocity.VelocityFlags;
import fr.neatmonster.nocheatplus.checks.workaround.WRPT;
import fr.neatmonster.nocheatplus.compat.blocks.changetracker.BlockChangeReference;
import fr.neatmonster.nocheatplus.components.entity.IEntityAccessDimensions;
import fr.neatmonster.nocheatplus.components.location.IGetPosition;
import fr.neatmonster.nocheatplus.components.location.IPositionWithLook;
import fr.neatmonster.nocheatplus.players.IPlayerData;
import fr.neatmonster.nocheatplus.utilities.CheckUtils;
import fr.neatmonster.nocheatplus.utilities.TickTask;
import fr.neatmonster.nocheatplus.utilities.ds.count.ActionAccumulator;
import fr.neatmonster.nocheatplus.utilities.ds.count.ActionFrequency;
import fr.neatmonster.nocheatplus.utilities.location.LocUtil;
import fr.neatmonster.nocheatplus.utilities.location.PlayerLocation;
import fr.neatmonster.nocheatplus.utilities.location.RichEntityLocation;
import fr.neatmonster.nocheatplus.utilities.location.TrigUtil;
import fr.neatmonster.nocheatplus.workaround.IWorkaroundRegistry.WorkaroundSet;

/**
 * Player specific data for the moving checks.
 */
public class MovingData extends ACheckData implements IRemoveSubCheckData {

    /*
     * TODO: Handle world unload and other by registration (PlayerDataManager) -
     * might just implement the interfaces and auto-handle at registration.
     */

    //    /**
    //     * Clear data related to the given world.
    //     * @param world The world that gets unloaded.
    //     */
    //    public static void onWorldUnload(final World world) {
    //        // TODO: Register with check (interfaces or just an event listener).
    //        final String worldName = world.getName();
    //        for (final MovingData data : playersMap.values()) {
    //            data.onWorldUnload(worldName);
    //        }
    //    }
    //
    //    public static void onReload() {
    //        // TODO: Register with check (interfaces or just an event listener).
    //        final MovingConfig globalCc = MovingConfig.getConfig((String) null);
    //        final int tick = TickTask.getTick();
    //        for (final MovingData data : playersMap.values()) {
    //            data.adjustOnReload(globalCc, tick);
    //        }
    //    }

    // Check specific.

    /**
     * Default lift-off envelope, used after resetting. <br>
     * TODO: Test, might be better ground.
     */
    private static final LiftOffEnvelope defaultLiftOffEnvelope = LiftOffEnvelope.UNKNOWN;

    public static final int              vehicleMorePacketsBufferDefault = 50;

    /** Tolerance value for using vertical velocity (the client sends different values than received with fight damage). */
    private static final double TOL_VVEL = 0.0625;

    //  private static final long IGNORE_SETBACK_Y = BlockProperties.F_SOLID | BlockProperties.F_GROUND | BlockProperties.F_CLIMBABLE | BlockProperties.F_LIQUID;

    /////////////////
    // Not static.
    /////////////////

    // Violation levels -----
    public double         creativeFlyVL            = 0.0;
    public double         morePacketsVL            = 0.0;
    public double         noFallVL                 = 0.0;
    public double         survivalFlyVL            = 0.0;
    public double         vehicleMorePacketsVL     = 0.0;
    public double         vehicleEnvelopeVL        = 0.0;

    // Data shared between the fly checks -----
    public int            bunnyhopDelay;
    public double         jumpAmplifier = 0;
    /** Last time the player was actually sprinting. */
    public long           timeSprinting = 0;
    public double         multSprinting = 1.30000002; // Multiplier at the last time sprinting.
    /** Compatibility entry for bouncing of slime blocks and the like. */
    public SimpleEntry verticalBounce = null;
    /** Last used block change id (BlockChangeTracker). */
    public final BlockChangeReference blockChangeRef = new BlockChangeReference();

    /** Tick at which walk/fly speeds got changed last time. */
    public int speedTick = 0;
    public float walkSpeed = 0.0f;
    public float flySpeed = 0.0f;

    /** Count set back (re-) setting. */
    private int playerMoveCount = 0;
    /**
     * setBackResetCount (incremented) at the time of (re-) setting the ordinary
     * set back.
     */
    private int setBackResetTime = 0;
    /**
     * setBackResetCount (incremented) at the time of (re-) setting the
     * morepackets set back.
     */
    private int morePacketsSetBackResetTime = 0;

    /**
     * Position teleported from into another world. Only used for certain
     * contexts for workarounds.
     */
    public IPositionWithLook crossWorldFrom = null;

    /** Keep track of currently processed (if) and past moves for player moving. */
    public final MoveTrace <PlayerMoveData> playerMoves = new MoveTrace<PlayerMoveData>(new Callable<PlayerMoveData>() {
        @Override
        public PlayerMoveData call() throws Exception {
            return new PlayerMoveData();
        }
    }, 2);

    /** Keep track of currently processed (if) and past moves for vehicle moving. */
    // TODO: There may be need to store such data with vehicles, or detect tandem abuse in a different way.
    public final MoveTrace <VehicleMoveData> vehicleMoves = new MoveTrace<VehicleMoveData>(new Callable<VehicleMoveData>() {
        @Override
        public VehicleMoveData call() throws Exception {
            return new VehicleMoveData();
        }
    }, 2);

    // Velocity handling.
    /** Vertical velocity modeled as an axis (positive and negative possible) */
    private final SimpleAxisVelocity verVel = new SimpleAxisVelocity();

    /** Horizontal velocity modeled as an axis (always positive) */
    private final FrictionAxisVelocity horVel = new FrictionAxisVelocity();

    /** Duration of the boost effect in ticks. */
    public int fireworksBoostDuration = 0;
    /**
     * Expire at this tick.
     */
    public int fireworksBoostTickExpire = 0;

    // Coordinates.
    /** Moving trace (to-positions, use tick as time). This is initialized on "playerJoins, i.e. MONITOR, and set to null on playerLeaves." */
    private final LocationTrace trace;

    // sf rather
    /** Basic envelope constraints for switching into air. */
    public LiftOffEnvelope liftOffEnvelope = defaultLiftOffEnvelope;
    /** Count how many moves have been made inside a medium (other than air). */
    public int insideMediumCount = 0;
    // TODO: Does combinedMedium stuff need resetting on join/teleport/special?
    /** Number of moves for horizontal moving within air + certain medium. */
    public int combinedMediumHCount = 0;
    /** Sum of actual speed / base speed for horizontal moving within air + certain medium. */
    public double combinedMediumHValue = 0.0;

    // Locations shared between all checks.
    private Location    setBack = null;
    private Location    teleported = null;

    // Check specific data -----

    // Data of the more packets check.
    /** Packet frequency count. */
    public final ActionFrequency    morePacketsFreq;
    /** Burst count. */
    public final ActionFrequency    morePacketsBurstFreq;
    private Location                morePacketsSetback = null;

    // Data of the no fall check.
    public float            noFallFallDistance = 0;
    /** Last y coordinate from when the player was on ground. */
    public double           noFallMaxY = 0;
    /** Indicate that NoFall is not to use next damage event for checking on-ground properties. */ 
    public boolean          noFallSkipAirCheck = false;
    // Passable check.
    public double           passableVL;

    // Data of the survival fly check.
    public double       sfHorizontalBuffer = 0.0; // ineffective: SurvivalFly.hBufMax / 2.0;
    /** Event-counter to cover up for sprinting resetting server side only. Set in the FighListener. */
    public int          lostSprintCount = 0;
    public int          sfJumpPhase = 0;
    /**
     * Count how many times in a row v-dist has been zero, only for in-air
     * moves, updated on not cancelled moves (aimed at in-air workarounds).
     */
    public int          sfZeroVdistRepeat = 0;
    /** Only used during processing, to keep track of sub-checks using velocity. Reset in velocityTick, before checks run. */

    /** "Dirty" flag, for receiving velocity and similar while in air. */
    private boolean     sfDirty = false;

    /** Indicate low jumping descending phase (likely cheating). */
    public boolean sfLowJump = false;
    public boolean sfNoLowJump = false; // Hacks.

    /**
     * Counting while the player is not on ground and not moving. A value <0
     * means not hovering at all.
     */
    public int          sfHoverTicks = -1;
    /**
     * First count these down before incrementing sfHoverTicks. Set on join, if
     * configured so.
     */
    public int          sfHoverLoginTicks = 0;
    public int          sfOnIce = 0; // TODO: Replace by allowed speed + friction.
    public long         sfCobwebTime = 0;
    public double       sfCobwebVL = 0;
    public long         sfVLTime = 0;


    // Accounting info.
    public final ActionAccumulator vDistAcc = new ActionAccumulator(3, 3);
    /**
     * Rough friction factor estimate, 0.0 is the reset value (maximum with
     * lift-off/burst speed is used).
     */
    public double lastFrictionHorizontal = 0.0;
    /**
     * Rough friction factor estimate, 0.0 is the reset value (maximum with
     * lift-off/burst speed is used).
     */
    public double lastFrictionVertical = 0.0;
    /** Used during processing, no resetting necessary.*/
    public double nextFrictionHorizontal = 0.0;
    /** Used during processing, no resetting necessary.*/
    public double nextFrictionVertical= 0.0;
    /** Workarounds */
    public final WorkaroundSet ws;

    // HOT FIX / WORKAROUND
    /**
     * Set to true after login/respawn, only if the set back is reset there.
     * Reset in MovingListener after handling PlayerMoveEvent
     */
    public boolean joinOrRespawn = false;
    /**
     * Number of (player/vehicle) move events since set.back. Update after
     * running standard checks on that EventPriority level (not MONITOR).
     */
    public int timeSinceSetBack = 0;
    /**
     * Location hash value of the last (player/vehicle) set back, for checking
     * independently of which set back location had been used.
     */
    public int lastSetBackHash = 0;

    // Vehicles.
    /**
     * Inconsistency-flag. Set on moving inside of vehicles, reset on exiting
     * properly. Workaround for VehicleLeaveEvent missing.
     */
    public boolean wasInVehicle = false; // Workaround
    /**
     * Set to indicate that events happen during a vehicle set back. Allows
     * skipping some resetting.
     */
    public boolean isVehicleSetBack = false; // Workaround
    public MoveConsistency vehicleConsistency = MoveConsistency.INCONSISTENT; // Workaround
    public final DefaultSetBackStorage vehicleSetBacks = new DefaultSetBackStorage();
    // Data of the more packets vehicle check.
    public int              vehicleMorePacketsBuffer = vehicleMorePacketsBufferDefault;
    public long             vehicleMorePacketsLastTime;
    /** Task id of the vehicle set back task. */ 
    public int              vehicleSetBackTaskId = -1;

    private final IPlayerData pData;

    public MovingData(final MovingConfig config, final IPlayerData pData) {
        this.pData = pData;
        morePacketsFreq = new ActionFrequency(config.morePacketsEPSBuckets, 500);
        morePacketsBurstFreq = new ActionFrequency(12, 5000);

        // Location trace.
        trace = new LocationTrace(config.traceMaxAge, config.traceMaxSize, NCPAPIProvider.getNoCheatPlusAPI().getGenericInstance(TraceEntryPool.class));

        // A new set of workaround conters.
        ws = NCPAPIProvider.getNoCheatPlusAPI().getGenericInstance(WRPT.class).getWorkaroundSet(WRPT.WS_MOVING);

    }

    @Override
    public boolean removeSubCheckData(final CheckType checkType) {
        // TODO: LocationTrace stays (leniency for other players!).
        // TODO: Likely more fields left to change.
        switch (checkType) {
            /*
             * TODO: case MOVING: // Remove all in-place (future: data might
             * stay as long as the player is online).
             */
            case MOVING_SURVIVALFLY:
                survivalFlyVL = 0;
                clearFlyData();
                resetSetBack(); // TODO: Not sure this is really best for compatibility.
                // TODO: other?
                return true;
            case MOVING_CREATIVEFLY:
                creativeFlyVL = 0;
                clearFlyData();
                resetSetBack(); // TODO: Not sure this is really best for compatibility.
                // TODO: other?
                return true;
            case MOVING_NOFALL:
                noFallVL = 0;
                clearNoFallData();
                return true;
            case MOVING_MOREPACKETS:
                morePacketsVL = 0;
                clearPlayerMorePacketsData();
                morePacketsSetback = null;
                morePacketsSetBackResetTime = 0;
                return true;
            case MOVING_PASSABLE:
                passableVL = 0;
                return true;
            case MOVING_VEHICLE:
                vehicleEnvelopeVL = 0;
                vehicleMorePacketsVL = 0;
                clearVehicleData();
                return true;
            case MOVING_VEHICLE_ENVELOPE:
                vehicleEnvelopeVL = 0;
                vehicleMoves.invalidate();
                vehicleSetBacks.invalidateAll(); // Also invalidates morepackets set back.
                return true;
            case MOVING_VEHICLE_MOREPACKETS:
                vehicleMorePacketsVL = 0;
                clearVehicleMorePacketsData();
                return true;
            default:
                return false;
        }
    }

    /**
     * Clear fly and more packets check data for both vehicles and players.
     */
    public void clearMostMovingCheckData() {
        clearFlyData();
        clearVehicleData();
        clearAllMorePacketsData();
    }

    /**
     * Clear vehicle related data, except more packets.
     */
    public void clearVehicleData() {
        // TODO: Not entirely sure what to do here.
        vehicleMoves.invalidate();
        vehicleSetBacks.invalidateAll();
    }

    /**
     * Clear the data of the fly checks (not more-packets).
     */
    public void clearFlyData() {
        playerMoves.invalidate();
        bunnyhopDelay = 0;
        sfJumpPhase = 0;
        jumpAmplifier = 0;
        setBack = null;
        sfZeroVdistRepeat = 0;
        clearAccounting();
        clearNoFallData();
        removeAllPlayerSpeedModifiers();
        lostSprintCount = 0;
        sfHoverTicks = sfHoverLoginTicks = -1;
        sfDirty = false;
        sfLowJump = false;
        liftOffEnvelope = defaultLiftOffEnvelope;
        insideMediumCount = 0;
        vehicleConsistency = MoveConsistency.INCONSISTENT;
        lastFrictionHorizontal = lastFrictionVertical = 0.0;
        verticalBounce = null;
        blockChangeRef.valid = false;
    }

    /**
     * On confirming a set back (teleport monitor / move start point): Mildly
     * reset the flying data without losing any important information. Past move
     * is adjusted to the given setBack, internal setBack is only updated, if
     * none is set.
     * 
     * @param setBack
     */
    public void onSetBack(final PlayerLocation setBack) {
        // Reset positions (a teleport should follow, though).
        this.morePacketsSetback = null;
        clearAccounting(); // Might be more safe to do this.
        // Keep no-fall data.
        // Fly data: problem is we don't remember the settings for the set back location.
        // Assume the player to start falling from there rather, or be on ground.
        // TODO: Check if to adjust some counters to state before setback? 
        // Keep jump amplifier
        // Keep bunny-hop delay (?)
        // keep jump phase.
        lostSprintCount = 0;
        sfHoverTicks = -1; // 0 ?
        sfDirty = false;
        sfLowJump = false;
        liftOffEnvelope = defaultLiftOffEnvelope;
        insideMediumCount = 0;
        removeAllPlayerSpeedModifiers();
        vehicleConsistency = MoveConsistency.INCONSISTENT; // Not entirely sure here.
        lastFrictionHorizontal = lastFrictionVertical = 0.0;
        verticalBounce = null;
        timeSinceSetBack = 0;
        lastSetBackHash = setBack == null ? 0 : setBack.hashCode();
        // Reset to setBack.
        resetPlayerPositions(setBack);
        adjustMediumProperties(setBack);
        // Only setSetBack if no set back location is there.
        if (setBack == null) {
            setSetBack(setBack);
        }
        // vehicleSetBacks.resetAllLazily(setBack); // Not good: Overrides older set back locations.
    }

    /**
     * Move event: Mildly reset some data, prepare setting a new to-Location.
     */
    public void prepareSetBack(final Location loc) {
        playerMoves.invalidate();
        vehicleMoves.invalidate();
        clearAccounting();
        sfJumpPhase = 0;
        sfZeroVdistRepeat = 0;
        verticalBounce = null;
        // Remember where we send the player to.
        setTeleported(loc);
        // TODO: sfHoverTicks ?
    }

    /**
     * Adjust properties that relate to the medium, called on set back and
     * similar. <br>
     * Currently: liftOffEnvelope, nextFriction.
     * 
     * @param loc
     */
    public void adjustMediumProperties(final PlayerLocation loc) {
        // Ensure block flags have been collected.
        loc.collectBlockFlags();
        // Simplified.
        if (loc.isInWeb()) {
            liftOffEnvelope = LiftOffEnvelope.NO_JUMP;
            nextFrictionHorizontal = nextFrictionVertical = 0.0;
        }
        else if (loc.isInLiquid()) {
            // TODO: Distinguish strong limit.
            liftOffEnvelope = LiftOffEnvelope.LIMIT_LIQUID;
            if (loc.isInLava()) {
                nextFrictionHorizontal = nextFrictionVertical = Magic.FRICTION_MEDIUM_LAVA;
            } else {
                nextFrictionHorizontal = nextFrictionVertical = Magic.FRICTION_MEDIUM_WATER;
            }
        }
        else if (loc.isOnGround()) {
            liftOffEnvelope = LiftOffEnvelope.NORMAL;
            nextFrictionHorizontal = nextFrictionVertical = Magic.FRICTION_MEDIUM_AIR;
        }
        else {
            liftOffEnvelope = LiftOffEnvelope.UNKNOWN;
            nextFrictionHorizontal = nextFrictionVertical = Magic.FRICTION_MEDIUM_AIR;
        }
        insideMediumCount = 0;
    }


    /**
     * Called when a player leaves the server.
     */
    public void onPlayerLeave() {
        removeAllPlayerSpeedModifiers();
        trace.reset();
        playerMoves.invalidate();
        vehicleMoves.invalidate();
    }

    /**
     * Clean up data related to worlds with the given name (not case-sensitive).
     * @param worldName
     */
    public void onWorldUnload(final String worldName) {
        // TODO: Unlink world references.
        if (teleported != null && worldName.equalsIgnoreCase(teleported.getWorld().getName())) {
            resetTeleported();
        }
        if (setBack != null && worldName.equalsIgnoreCase(setBack.getWorld().getName())) {
            clearFlyData();
        }
        if (morePacketsSetback != null && worldName.equalsIgnoreCase(morePacketsSetback.getWorld().getName())) {
            clearPlayerMorePacketsData();
            clearNoFallData(); // just in case.
        }
        // (Assume vehicle data needn't really reset here.)
        vehicleSetBacks.resetByWorldName(worldName);
    }

    /**
     * Invalidate all past player moves data and set last position if not null.
     * 
     * @param loc
     */
    public void resetPlayerPositions(final PlayerLocation loc) {
        resetPlayerPositions();
        if (loc != null) {
            final PlayerMoveData lastMove = playerMoves.getFirstPastMove();
            // Always set with extra properties.
            lastMove.setWithExtraProperties(loc);
        }
    }

    /**
     * Invalidate all past moves data (player).
     */
    private void resetPlayerPositions() {
        playerMoves.invalidate();
        sfZeroVdistRepeat = 0;
        sfDirty = false;
        sfLowJump = false;
        liftOffEnvelope = defaultLiftOffEnvelope;
        insideMediumCount = 0;
        lastFrictionHorizontal = lastFrictionVertical = 0.0;
        verticalBounce = null;
        blockChangeRef.valid = false;
        // TODO: other buffers ?
        // No reset of vehicleConsistency.
    }

    /**
     * Invalidate all past vehicle moves data and set last position if not null.
     * 
     * @param loc
     */
    public void resetVehiclePositions(final RichEntityLocation loc) {
        // TODO: Other properties (convenience, e.g. set back?) ?
        vehicleMoves.invalidate();
        if (loc != null) {
            final VehicleMoveData lastMove = vehicleMoves.getFirstPastMove();
            // Always set with extra properties.
            lastMove.setWithExtraProperties(loc);
            final Entity entity = loc.getEntity();
            lastMove.vehicleId = entity.getUniqueId();
            lastMove.vehicleType = entity.getType();
        }
    }

    /**
     * Clear accounting data.
     */
    public void clearAccounting() {
        vDistAcc.clear();
    }

    /**
     * Clear the data of the more packets checks, both for players and vehicles.
     */
    public void clearAllMorePacketsData() {
        clearPlayerMorePacketsData();
        clearVehicleMorePacketsData();
    }

    public void clearPlayerMorePacketsData() {
        morePacketsSetback = null;
        final long now = System.currentTimeMillis();
        morePacketsFreq.clear(now);
        morePacketsBurstFreq.clear(now);
        // TODO: Also reset other data ?
    }

    /**
     * Reduce the morepackets frequency counters by the given amount, capped at
     * a minimum of 0.
     * 
     * @param amount
     */
    public void reducePlayerMorePacketsData(final float amount) {
        ActionFrequency.reduce(System.currentTimeMillis(), amount, morePacketsFreq, morePacketsBurstFreq);
    }

    public void clearVehicleMorePacketsData() {
        vehicleMorePacketsLastTime = 0;
        vehicleMorePacketsBuffer = vehicleMorePacketsBufferDefault;
        vehicleSetBacks.getMidTermEntry().setValid(false); // TODO: Will have other resetting conditions later on.
        // TODO: Also reset other data ?
    }

    /**
     * Clear the data of the new fall check.
     */
    public void clearNoFallData() {
        noFallFallDistance = 0;
        noFallMaxY = 0.0;
        noFallSkipAirCheck = false;
    }

    /**
     * Set the set back location, this will also adjust the y-coordinate for some block types (at least air).
     * @param loc
     */
    public void setSetBack(final PlayerLocation loc) {
        if (setBack == null) {
            setBack = loc.getLocation();
        }
        else{
            LocUtil.set(setBack, loc);
        }
        // TODO: Consider adjusting the set back-y here. Problem: Need to take into account for bounding box (collect max-ground-height needed).
        setBackResetTime = playerMoveCount;
    }

    /**
     * Convenience method.
     * @param loc
     */
    public void setSetBack(final Location loc) {
        if (setBack == null) {
            setBack = LocUtil.clone(loc);
        }
        else{
            LocUtil.set(setBack, loc);
        }
        setBackResetTime = playerMoveCount;
    }

    /**
     * Get the set back location with yaw and pitch set form ref.
     * @param ref
     * @return
     */
    public Location getSetBack(final Location ref) {
        return LocUtil.clone(setBack, ref);
    }

    /**
     * Get the set back location with yaw and pitch set from ref.
     * @param ref
     * @return
     */
    public Location getSetBack(final PlayerLocation ref) {
        return LocUtil.clone(setBack, ref);
    }

    public boolean hasSetBack() {
        return setBack != null;
    }

    public boolean hasSetBackWorldChanged(final Location loc) {
        if (setBack == null) {
            return true;
        }
        else {
            return setBack.getWorld().equals(loc.getWorld());
        }
    }


    public double getSetBackX() {
        return setBack.getX();
    }

    public double getSetBackY() {
        return setBack.getY();
    }

    public double getSetBackZ() {
        return setBack.getZ();
    }

    public void setSetBackY(final double y) {
        setBack.setY(y);
        // (Skip setting/increasing the reset count.)
    }

    /**
     * Test, if the 'teleported' location is set, e.g. on a scheduled set back.
     * 
     * @return
     */
    public boolean hasTeleported() {
        return teleported != null;
    }

    /**
     * Return a copy of the teleported-to Location.
     * @return
     */
    public final Location getTeleported() {
        // TODO: here a reference might do.
        return teleported == null ? teleported : LocUtil.clone(teleported);
    }

    /**
     * Check if the given location equals to the 'teleported' (set back)
     * location.
     * 
     * @param loc
     * @return In case of either loc or teleported being null, false is
     *         returned, otherwise teleported.equals(loc).
     */
    public boolean isTeleported(final Location loc) {
        return loc != null && teleported != null && teleported.equals(loc);
    }

    /**
     * Check if the given location has the same coordinates like the
     * 'teleported' (set back) location. This is more light-weight and more
     * lenient than isTeleported, because world and yaw and pitch are all
     * ignored.
     * 
     * @param loc
     * @return In case of either loc or teleported being null, false is
     *         returned, otherwise TrigUtil.isSamePos(teleported, loc).
     */
    public boolean isTeleportedPosition(final Location loc) {
        return loc != null && teleported != null && TrigUtil.isSamePos(teleported, loc);
    }

    /**
     * Check if the given location has the same coordinates like the
     * 'teleported' (set back) location. This is more light-weight and more
     * lenient than isTeleported, because world and yaw and pitch are all
     * ignored.
     * 
     * @param loc
     * @return In case of either loc or teleported being null, false is
     *         returned, otherwise TrigUtil.isSamePos(pos, teleported).
     */
    public boolean isTeleportedPosition(final IGetPosition pos) {
        return pos != null && teleported != null && TrigUtil.isSamePos(pos, teleported);
    }

    /**
     * Set teleport-to location to recognize NCP set backs. This copies the coordinates and world.
     * @param loc
     */
    public final void setTeleported(final Location loc) {
        teleported = LocUtil.clone(loc); // Always overwrite.
    }

    public boolean hasMorePacketsSetBack() {
        return morePacketsSetback != null;
    }

    /**
     * Test if the morepackets set back is older than the ordinary set back.
     * Does not check for existence of either.
     * 
     * @return
     */
    public boolean isMorePacketsSetBackOldest() {
        return morePacketsSetBackResetTime < setBackResetTime;
    }

    public final void setMorePacketsSetBack(final PlayerLocation loc) {
        if (morePacketsSetback == null) {
            morePacketsSetback = loc.getLocation();
        }
        else {
            LocUtil.set(morePacketsSetback, loc);
        }
        morePacketsSetBackResetTime = playerMoveCount;
    }

    public final void setMorePacketsSetBack(final Location loc) {
        if (morePacketsSetback == null) {
            morePacketsSetback = LocUtil.clone(loc);
        }
        else {
            LocUtil.set(morePacketsSetback, loc);
        }
        morePacketsSetBackResetTime = playerMoveCount;
    }

    public Location getMorePacketsSetBack() {
        return LocUtil.clone(morePacketsSetback);
    }

    public final void resetTeleported() {
        teleported = null;
    }

    /**
     * Set set back location to null.
     */
    public final void resetSetBack() {
        setBack = null;
    }

    /**
     * Add velocity to internal book-keeping.
     * 
     * @param player
     * @param data
     * @param cc
     * @param vx
     * @param vy
     * @param vz
     */
    public void addVelocity(final Player player, final MovingConfig cc, 
            final double vx, final double vy, final double vz) {
        addVelocity(player, cc, vx, vy, vz, 0L);
    }

    /**
     * Add velocity to internal book-keeping.
     * 
     * @param player
     * @param data
     * @param cc
     * @param vx
     * @param vy
     * @param vz
     * @param flags
     *            Flags to use with velocity entries.
     */
    public void addVelocity(final Player player, final MovingConfig cc, 
            final double vx, final double vy, final double vz, final long flags) {
        final int tick = TickTask.getTick();
        // TODO: Slightly odd to call this each time, might switch to a counter-strategy (move - remove). 
        removeInvalidVelocity(tick  - cc.velocityActivationTicks);

        if (pData.isDebugActive(CheckType.MOVING)) {
            CheckUtils.debug(player, CheckType.MOVING, "New velocity: " + vx + ", " + vy + ", " + vz);
        }

        // Always add vertical velocity.
        verVel.add(new SimpleEntry(tick, vy, flags, cc.velocityActivationCounter));

        // TODO: Should also switch to adding always.
        if (vx != 0.0 || vz != 0.0) {
            final double newVal = Math.sqrt(vx * vx + vz * vz);
            horVel.add(new AccountEntry(tick, newVal, cc.velocityActivationCounter, getHorVelValCount(newVal)));
        }

        // Set dirty flag here.
        sfDirty = true; // TODO: Set on using the velocity, due to latency !
        sfNoLowJump = true; // TODO: Set on using the velocity, due to latency !

    }

    /**
     * Std. value counter for horizontal velocity, based on the vlaue.
     * 
     * @param velocity
     * @return
     */
    public static int getHorVelValCount(double velocity) {
        return Math.max(20,  1 + (int) Math.round(velocity * 10.0));
    }

    public void prependVerticalVelocity(final SimpleEntry entry) {
        verVel.addToFront(entry);
    }

    /**
     * Get the first element without using it.
     * @param amount
     * @param minActCount
     * @param maxActCount
     * @return
     */
    public SimpleEntry peekVerticalVelocity(final double amount, final int minActCount, final int maxActCount) {
        return verVel.peek(amount, minActCount, maxActCount, TOL_VVEL);
    }

    public void addVerticalVelocity(final SimpleEntry entry) {
        verVel.add(entry);
    }

    /**
     * Add horizontal velocity directly to horizontal-only bookkeeping.
     * 
     * @param vel
     *            Assumes positive values always.
     */
    public void addHorizontalVelocity(final AccountEntry vel) {
        horVel.add(vel);
    }

    /**
     * Remove/reset all speed modifier tracking, like vertical and horizontal
     * velocity, elytra boost, buffer.
     */
    private void removeAllPlayerSpeedModifiers() {
        // Velocity
        removeAllVelocity();
        // Elytra boost best fits velocity / effects.
        fireworksBoostDuration = 0; 
        fireworksBoostTickExpire = 0;
        // Horizontal buffer.
        sfHorizontalBuffer = 0.0;
    }

    /**
     * Reset velocity tracking (h+v).
     */
    public void removeAllVelocity() {
        horVel.clear();
        verVel.clear();
        sfDirty = false;
    }

    /**
     * Remove all velocity entries that are invalid. Checks both active and queued.
     * <br>(This does not catch invalidation by speed / direction changing.)
     * @param tick All velocity added before this tick gets removed.
     */
    public void removeInvalidVelocity(final int tick) {
        horVel.removeInvalid(tick);
        verVel.removeInvalid(tick);
    }

    /**
     * Clear only active horizontal velocity.
     */
    public void clearActiveHorVel() {
        horVel.clearActive();
    }

    public boolean hasActiveHorVel() {
        return horVel.hasActive();
    }

    public boolean hasQueuedHorVel() {
        return horVel.hasQueued();
    }

    /**
     * Active or queued.
     * @return
     */
    public boolean hasAnyHorVel() {
        return horVel.hasAny();
    }

    /**
     * Queued velocity only.
     * @return
     */
    public boolean hasAnyVerVel() {
        return verVel.hasQueued();
    }

    //    public boolean hasActiveVerVel() {
    //        return verVel.hasActive();
    //    }

    //    public boolean hasQueuedVerVel() {
    //        return verVel.hasQueued();
    //    }

    /**
     * Called for moving events. Remove invalid entries, increase age of velocity, decrease amounts, check which entries are invalid. Both horizontal and vertical.
     */
    public void velocityTick(final int invalidateBeforeTick) {
        // Remove invalid velocity.
        removeInvalidVelocity(invalidateBeforeTick);

        // Horizontal velocity (intermediate concept).
        horVel.tick();

        // (Vertical velocity does not tick.)

        // Renew the dirty phase.
        if (!sfDirty && (horVel.hasActive() || horVel.hasQueued())) {
            sfDirty = true;
        }
    }

    /**
     * Get effective amount of all used velocity. Non-destructive.
     * @return
     */
    public double getHorizontalFreedom() {
        return horVel.getFreedom();
    }

    /**
     * Use all queued velocity until at least amount is matched.
     * Amount is the horizontal distance that is to be covered by velocity (active has already been checked).
     * <br>
     * If the modeling changes (max instead of sum or similar), then this will be affected.
     * @param amount The amount demanded, must be positive.
     * @return
     */
    public double useHorizontalVelocity(final double amount) {
        final double available = horVel.use(amount);
        if (available >= amount) {
            sfDirty = true;
        }
        return available;
    }

    /**
     * Debugging.
     * @param builder
     */
    public void addHorizontalVelocity(final StringBuilder builder) {
        if (horVel.hasActive()) {
            builder.append("\n" + " horizontal velocity (active):");
            horVel.addActive(builder);
        }
        if (horVel.hasQueued()) {
            builder.append("\n" + " horizontal velocity (queued):");
            horVel.addQueued(builder);
        }
    }

    /**
     * Get the first matching velocity entry (invalidate others). Sets
     * verVelUsed if available.
     * 
     * @param amount
     * @return
     */
    public SimpleEntry useVerticalVelocity(final double amount) {
        final SimpleEntry available = verVel.use(amount, TOL_VVEL);
        if (available != null) {
            playerMoves.getCurrentMove().verVelUsed = available;
            sfDirty = true;
            // TODO: Consider sfNoLowJump = true;
        }
        return available;
    }

    /**
     * Use the verVelUsed field, if it matches. Otherwise call
     * useVerticalVelocity(amount).
     * 
     * @param amount
     * @return
     */
    public SimpleEntry getOrUseVerticalVelocity(final double amount) {
        final SimpleEntry verVelUsed = playerMoves.getCurrentMove().verVelUsed;
        if (verVelUsed != null) {
            if (verVel.matchesEntry(verVelUsed, amount, TOL_VVEL)) {
                return verVelUsed;
            }
        }
        return useVerticalVelocity(amount);
    }

    /**
     * Debugging.
     * @param builder
     */
    public void addVerticalVelocity(final StringBuilder builder) {
        if (verVel.hasQueued()) {
            builder.append("\n" + " vertical velocity (queued):");
            verVel.addQueued(builder);
        }
    }

    /**
     * Test if the location is the same, ignoring pitch and yaw.
     * @param loc
     * @return
     */
    public boolean isSetBack(final Location loc) {
        if (loc == null || setBack == null) {
            return false;
        }
        if (!loc.getWorld().getName().equals(setBack.getWorld().getName())) {
            return false;
        }
        return loc.getX() == setBack.getX() && loc.getY() == setBack.getY() && loc.getZ() == setBack.getZ();
    }

    public void adjustWalkSpeed(final float walkSpeed, final int tick, final int speedGrace) {
        if (walkSpeed > this.walkSpeed) {
            this.walkSpeed = walkSpeed;
            this.speedTick = tick;
        } else if (walkSpeed < this.walkSpeed) {
            if (tick - this.speedTick > speedGrace) {
                this.walkSpeed = walkSpeed;
                this.speedTick = tick;
            }
        } else {
            this.speedTick = tick;
        }
    }

    public void adjustFlySpeed(final float flySpeed, final int tick, final int speedGrace) {
        if (flySpeed > this.flySpeed) {
            this.flySpeed = flySpeed;
            this.speedTick = tick;
        } else if (flySpeed < this.flySpeed) {
            if (tick - this.speedTick > speedGrace) {
                this.flySpeed = flySpeed;
                this.speedTick = tick;
            }
        } else {
            this.speedTick = tick;
        }
    }

    /**
     * This tests for a LocationTrace instance being set at all, not for locations having been added.
     * @return
     */
    public boolean hasTrace() {
        return trace != null;
    }

    /**
     * Convenience: Access method to simplify coding, being aware of some plugins using Player implementations as NPCs, leading to traces not being present.
     * @return
     */
    public LocationTrace getTrace(final Player player) {
        return trace;
    }

    /**
     * Ensure to have a LocationTrace instance with the given parameters.
     * 
     * @param maxAge
     * @param maxSize
     * @return
     */
    private LocationTrace getTrace(final int maxAge, final int maxSize) {
        if (trace.getMaxSize() != maxSize || trace.getMaxAge() != maxAge) {
            // TODO: Might want to have tick passed as argument?
            trace.adjustSettings(maxAge, maxSize, TickTask.getTick());
        } 
        return trace;
    }

    /**
     * Convenience method to add a location to the trace, creates the trace if
     * necessary.
     * 
     * @param player
     * @param loc
     * @param time
     * @param iead
     *            If null getEyeHeight and 0.3 are used (assume fake player).
     * @return Updated LocationTrace instance, for convenient use, without
     *         sticking too much to MovingData.
     */
    public LocationTrace updateTrace(final Player player, final Location loc, final long time, 
            final IEntityAccessDimensions iead) {
        final LocationTrace trace = getTrace(player);
        if (iead == null) {
            // TODO: 0.3 from bukkit based default heights (needs extra registered classes).
            trace.addEntry(time, loc.getX(), loc.getY(), loc.getZ(), 0.3, player.getEyeHeight());
        }
        else {
            trace.addEntry(time, loc.getX(), loc.getY(), loc.getZ(), iead.getWidth(player) / 2.0, Math.max(player.getEyeHeight(), iead.getHeight(player)));
        }
        return trace;
    }

    /**
     * Convenience.
     * @param loc
     * @param time
     * @param cc
     */
    public void resetTrace(final Player player, final Location loc, final long time, 
            final IEntityAccessDimensions iead, final MovingConfig cc) {
        resetTrace(player, loc, time, cc.traceMaxAge, cc.traceMaxSize, iead);
    }

    /**
     * Convenience: Create or just reset the trace, add the current location.
     * @param loc 
     * @param size
     * @param mergeDist
     * @param traceMergeDist 
     */
    public void resetTrace(final Player player, final Location loc, final long time, 
            final int maxAge, final int maxSize, final IEntityAccessDimensions iead) {
        if (trace != null) {
            trace.reset();
        }
        getTrace(maxAge, maxSize).addEntry(time, loc.getX(), loc.getY(), loc.getZ(), 
                iead.getWidth(player) / 2.0, Math.max(player.getEyeHeight(), iead.getHeight(player)));
    }

    /**
     * Adjust to new parameters.
     */
    protected void adjustOnReload(final MovingConfig cc, final int tick) {
        trace.adjustSettings(cc.traceMaxAge, cc.traceMaxSize, tick);
    }

    /**
     * Test if velocity has affected the in-air jumping phase. Keeps set until
     * reset on-ground or otherwise. Use clearActiveVerVel to force end velocity
     * jump phase. Use hasAnyVerVel() to test if active or queued vertical
     * velocity should still be able to influence the in-air jump phase.
     * 
     * @return
     */
    public boolean isVelocityJumpPhase() {
        return sfDirty;
    }

    /**
     * Refactoring stage: Test which value sfDirty should have and set
     * accordingly. This should only be called, if the player reached ground.
     * 
     * @return If the velocity jump phase is still active (sfDirty).
     */
    public boolean resetVelocityJumpPhase() {
        return resetVelocityJumpPhase(null);
    }

    /**
     * See {@link #resetVelocityJumpPhase()}.
     * @param tags
     * @return
     */
    public boolean resetVelocityJumpPhase(final Collection<String> tags) {
        if (horVel.hasActive() || horVel.hasQueued() 
                || sfDirty && shouldRetainSFDirty(tags)) {
            // TODO: What with vertical ?
            return sfDirty = true;
        }
        else {
            return sfDirty = false;
        }
    }

    private final boolean shouldRetainSFDirty(final Collection<String> tags) {
        final PlayerMoveData thisMove = playerMoves.getLatestValidMove();
        if (thisMove == null || !thisMove.toIsValid || thisMove.yDistance >= 0.0) {
            final SimpleEntry entry = verVel.peek(
                    thisMove == null ? 0.05 : thisMove.yDistance, 0, 4, 0.0);
            if (entry != null && entry.hasFlag(VelocityFlags.ORIGIN_BLOCK_BOUNCE)
                    || thisMove != null && thisMove.verVelUsed != null 
                    && thisMove.verVelUsed.hasFlag(VelocityFlags.ORIGIN_BLOCK_BOUNCE)) {
                // TODO: Strictly, pastground_from/to should rather be skipped instead of this.
                if (tags != null) {
                    tags.add("retain_dirty_bounce"); // +- block/push
                }
                return true;
            }
        }
        return false;
    }

    /**
     * Force set the move to be affected by previous speed. Currently
     * implemented as setting velocity jump phase.
     */
    public void setFrictionJumpPhase() {
        // TODO: Better and more reliable modeling.
        sfDirty = true;
    }

    public void useVerticalBounce(final Player player) {
        // CHEATING: Ensure fall distance is reset.
        player.setFallDistance(0f);
        noFallMaxY = 0.0;
        noFallFallDistance = 0f;
        noFallSkipAirCheck = true;
        prependVerticalVelocity(verticalBounce);
        verticalBounce = null;
    }

    public void handleTimeRanBackwards() {
        final long time = System.currentTimeMillis();
        timeSprinting = Math.min(timeSprinting, time);
        vehicleMorePacketsLastTime = Math.min(vehicleMorePacketsLastTime, time);
        sfCobwebTime = Math.min(sfCobwebTime, time);
        sfVLTime = Math.min(sfVLTime, time);
        clearAccounting(); // Not sure: adding up might not be nice.
        removeAllPlayerSpeedModifiers(); // TODO: This likely leads to problems.
        // (ActionFrequency can handle this.)
    }

    /**
     * Get the y-axis velocity tracker. Rather for testing purposes.
     * 
     * @return
     */
    public SimpleAxisVelocity getVerticalVelocityTracker() {
        return verVel;
    }

    /**
     * The number of move events received.
     * 
     * @return
     */
    public int getPlayerMoveCount() {
        return playerMoveCount;
    }

    /**
     * Called with player move events.
     */
    public void increasePlayerMoveCount() {
        playerMoveCount++;
        if (playerMoveCount == Integer.MAX_VALUE) {
            playerMoveCount = 0;
            morePacketsSetBackResetTime = 0;
            setBackResetTime = 0;
        }
    }

    /**
     * Age in move events.
     * @return
     */
    public int getMorePacketsSetBackAge() {
        return playerMoveCount - morePacketsSetBackResetTime;
    }

    /**
     * Remove from start while the flag is present.
     * @param originBlockBounce
     */
    public void removeLeadingQueuedVerticalVelocityByFlag(final long flag) {
        verVel.removeLeadingQueuedVerticalVelocityByFlag(flag);
    }

}
