/*
 * 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.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.bukkit.Bukkit;
import org.bukkit.GameMode;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;

import fr.neatmonster.nocheatplus.actions.ActionList;
import fr.neatmonster.nocheatplus.checks.CheckType;
import fr.neatmonster.nocheatplus.checks.access.ACheckConfig;
import fr.neatmonster.nocheatplus.checks.moving.magic.Magic;
import fr.neatmonster.nocheatplus.checks.moving.model.ModelFlying;
import fr.neatmonster.nocheatplus.checks.moving.player.PlayerSetBackMethod;
import fr.neatmonster.nocheatplus.checks.moving.util.MovingUtil;
import fr.neatmonster.nocheatplus.command.CommandUtil;
import fr.neatmonster.nocheatplus.compat.AlmostBoolean;
import fr.neatmonster.nocheatplus.compat.Bridge1_9;
import fr.neatmonster.nocheatplus.compat.versions.Bugs;
import fr.neatmonster.nocheatplus.compat.versions.ServerVersion;
import fr.neatmonster.nocheatplus.components.config.value.OverrideType;
import fr.neatmonster.nocheatplus.config.ConfPaths;
import fr.neatmonster.nocheatplus.config.ConfigFile;
import fr.neatmonster.nocheatplus.permissions.Permissions;
import fr.neatmonster.nocheatplus.utilities.ColorUtil;
import fr.neatmonster.nocheatplus.utilities.ds.prefixtree.SimpleCharPrefixTree;
import fr.neatmonster.nocheatplus.utilities.location.PlayerLocation;
import fr.neatmonster.nocheatplus.worlds.IWorldData;

/**
 * Configurations specific for the moving checks. Every world gets one of these
 * assigned to it.
 */
public class MovingConfig extends ACheckConfig {

    // Model flying ids.
    public static final String ID_JETPACK_ELYTRA = "jetpack.elytra";
    public static final String ID_POTION_LEVITATION = "potion.levitation";

    // INSTANCE

    public final boolean    ignoreCreative;
    public final boolean    ignoreAllowFlight;

    private final Map<GameMode, ModelFlying> flyingModelGameMode = new HashMap<GameMode, ModelFlying>();
    private final ModelFlying flyingModelElytra;
    private final ModelFlying flyingModelLevitation;
    public final ActionList creativeFlyActions;

    /** Assumed number of packets per second under ideal conditions. */
    public final float      morePacketsEPSIdeal;
    /** The maximum number of packets per second that we accept. */
    public final float      morePacketsEPSMax;
    public final int        morePacketsEPSBuckets;
    public final float		morePacketsBurstPackets;
    public final double		morePacketsBurstDirect;
    public final double		morePacketsBurstEPM;
    public final int        morePacketsSetBackAge;
    public final ActionList morePacketsActions;

    /**
     * Deal damage instead of Minecraft, whenever a player is judged to be on
     * ground.
     */
    public final boolean    noFallDealDamage;
    public final boolean    noFallSkipAllowFlight;
    /**
     * Reset data on violation, i.e. a player taking fall damage without being
     * on ground.
     */
    public final boolean    noFallViolationReset;
    /** Reset data on tp. */
    public final boolean 	noFallTpReset;
    /** Reset if in vehicle. */
    public final boolean noFallVehicleReset;
    /** Reset fd to 0  if on ground (dealdamage only). */
    public final boolean noFallAntiCriticals;
    public final ActionList noFallActions;

    // TODO: passableAccuracy: also use if not using ray-tracing
    public final ActionList passableActions;
    public final boolean    passableUntrackedTeleportCheck;
    public final boolean    passableUntrackedCommandCheck;
    public final boolean    passableUntrackedCommandTryTeleport;
    public final SimpleCharPrefixTree passableUntrackedCommandPrefixes = new SimpleCharPrefixTree();

    public final int        survivalFlyBlockingSpeed;
    public final int        survivalFlySneakingSpeed;
    public final int        survivalFlySpeedingSpeed;
    public final int        survivalFlySprintingSpeed;
    public final int        survivalFlySwimmingSpeed;
    public final int        survivalFlyWalkingSpeed;
    public final boolean    survivalFlyCobwebHack;
    public final boolean    sfSlownessSprintHack;
    /**
     * If true, will allow moderate bunny hop without lift off. Applies for
     * normal speed on 1.6.4 and probably below.
     */
    public final boolean    sfGroundHop;
    public final boolean    survivalFlyAccountingH;
    public final boolean    survivalFlyAccountingV;
    public final boolean    sfSetBackPolicyVoid;
    public final boolean    sfSetBackPolicyFallDamage;
    public final double     sfStepHeight;
    public final long       survivalFlyVLFreeze;
    public final ActionList survivalFlyActions;

    public final boolean 	sfHoverCheck; // TODO: Sub check ?
    public final int 		sfHoverTicks;
    public final int		sfHoverLoginTicks;
    public final boolean    sfHoverFallDamage;
    public final double		sfHoverViolation;

    // Special tolerance values:
    /** Horizontal buffer (rather sf), after failure leniency. */
    public final double hBufMax;
    /**
     * Number of moving packets until which a velocity entry must be activated,
     * in order to not be removed.
     */
    public final int		velocityActivationCounter;
    /** Server ticks until invalidating queues velocity. */
    public final int		velocityActivationTicks;
    public final boolean	velocityStrictInvalidation;
    public final double     noFallyOnGround;
    public final double     yOnGround;

    // General things.
    /**
     * If to allow splitting moves, due to player.getLocation reflecting
     * something else than from/to.
     */
    public final boolean splitMoves;
    public final boolean ignoreStance;
    public final boolean tempKickIllegal;
    public final boolean loadChunksOnJoin;
    public final boolean loadChunksOnMove;
    public final boolean loadChunksOnTeleport;
    public final boolean loadChunksOnWorldChange;
    public final long sprintingGrace;
    public final boolean assumeSprint;
    public final int speedGrace;
    public final boolean enforceLocation;
    public final boolean trackBlockMove;
    public final PlayerSetBackMethod playerSetBackMethod;

    // Vehicles
    public final boolean vehicleEnforceLocation;
    public final boolean vehiclePreventDestroyOwn;
    public final boolean scheduleVehicleSetBacks;

    public final Set<EntityType> ignoredVehicles = new HashSet<EntityType>();

    public final ActionList vehicleMorePacketsActions;

    public final HashMap<EntityType, Double> vehicleEnvelopeHorizontalSpeedCap = new HashMap<EntityType, Double>();
    public final ActionList vehicleEnvelopeActions;

    // Trace
    public final int traceMaxAge;
    public final int traceMaxSize;

    // Messages.
    public final String msgKickIllegalMove;
    public final String msgKickIllegalVehicleMove;

    /**
     * Instantiates a new moving configuration.
     * 
     * @param config
     *            the data
     */
    public MovingConfig(final IWorldData worldData) {
        super(worldData);
        final ConfigFile config = worldData.getRawConfiguration();

        ignoreCreative = config.getBoolean(ConfPaths.MOVING_CREATIVEFLY_IGNORECREATIVE);
        ignoreAllowFlight = config.getBoolean(ConfPaths.MOVING_CREATIVEFLY_IGNOREALLOWFLIGHT);

        final ModelFlying defaultModel = new ModelFlying("gamemode.creative", config, 
                ConfPaths.MOVING_CREATIVEFLY_MODEL + "creative.", new ModelFlying().lock());
        for (final GameMode gameMode : GameMode.values()) {
            flyingModelGameMode.put(gameMode, new ModelFlying("gamemode." + gameMode.name().toLowerCase(), config, 
                    ConfPaths.MOVING_CREATIVEFLY_MODEL + (gameMode.name().toLowerCase()) + ".", defaultModel).lock());
        }
        flyingModelLevitation = new ModelFlying(ID_POTION_LEVITATION, config, 
                ConfPaths.MOVING_CREATIVEFLY_MODEL + "levitation.", 
                new ModelFlying(null, defaultModel).scaleLevitationEffect(true).lock());
        flyingModelElytra = new ModelFlying(ID_JETPACK_ELYTRA, config, 
                ConfPaths.MOVING_CREATIVEFLY_MODEL + "elytra.", 
                new ModelFlying(null, defaultModel).verticalAscendGliding(true).lock());
        creativeFlyActions = config.getOptimizedActionList(ConfPaths.MOVING_CREATIVEFLY_ACTIONS, 
                Permissions.MOVING_CREATIVEFLY);

        morePacketsEPSIdeal = config.getInt(ConfPaths.MOVING_MOREPACKETS_EPSIDEAL);
        morePacketsEPSMax = Math.max(morePacketsEPSIdeal, config.getInt(ConfPaths.MOVING_MOREPACKETS_EPSMAX));
        morePacketsEPSBuckets = 2 * Math.max(1, Math.min(60, config.getInt(ConfPaths.MOVING_MOREPACKETS_SECONDS)));
        morePacketsBurstPackets = config.getInt(ConfPaths.MOVING_MOREPACKETS_BURST_EPM);
        morePacketsBurstDirect = config.getInt(ConfPaths.MOVING_MOREPACKETS_BURST_DIRECT);
        morePacketsBurstEPM = config.getInt(ConfPaths.MOVING_MOREPACKETS_BURST_EPM);
        morePacketsSetBackAge = config.getInt(ConfPaths.MOVING_MOREPACKETS_SETBACKAGE);
        morePacketsActions = config.getOptimizedActionList(ConfPaths.MOVING_MOREPACKETS_ACTIONS, Permissions.MOVING_MOREPACKETS);

        noFallDealDamage = config.getBoolean(ConfPaths.MOVING_NOFALL_DEALDAMAGE);
        noFallSkipAllowFlight = config.getBoolean(ConfPaths.MOVING_NOFALL_SKIPALLOWFLIGHT);
        noFallViolationReset = config.getBoolean(ConfPaths.MOVING_NOFALL_RESETONVL);
        noFallTpReset = config.getBoolean(ConfPaths.MOVING_NOFALL_RESETONTP);
        noFallVehicleReset = config.getBoolean(ConfPaths.MOVING_NOFALL_RESETONVEHICLE);
        noFallAntiCriticals = config.getBoolean(ConfPaths.MOVING_NOFALL_ANTICRITICALS);
        noFallActions = config.getOptimizedActionList(ConfPaths.MOVING_NOFALL_ACTIONS, Permissions.MOVING_NOFALL);

        passableActions = config.getOptimizedActionList(ConfPaths.MOVING_PASSABLE_ACTIONS, Permissions.MOVING_PASSABLE);
        passableUntrackedTeleportCheck = config.getBoolean(ConfPaths.MOVING_PASSABLE_UNTRACKED_TELEPORT_ACTIVE);
        passableUntrackedCommandCheck = config.getBoolean(ConfPaths.MOVING_PASSABLE_UNTRACKED_CMD_ACTIVE);
        passableUntrackedCommandTryTeleport = config.getBoolean(ConfPaths.MOVING_PASSABLE_UNTRACKED_CMD_TRYTELEPORT);
        CommandUtil.feedCommands(passableUntrackedCommandPrefixes, config, ConfPaths.MOVING_PASSABLE_UNTRACKED_CMD_PREFIXES, true);

        // Default values are specified here because this settings aren't showed by default into the configuration file.
        survivalFlyBlockingSpeed = config.getInt(ConfPaths.MOVING_SURVIVALFLY_BLOCKINGSPEED, 100);
        survivalFlySneakingSpeed = config.getInt(ConfPaths.MOVING_SURVIVALFLY_SNEAKINGSPEED, 100);
        survivalFlySpeedingSpeed = config.getInt(ConfPaths.MOVING_SURVIVALFLY_SPEEDINGSPEED, 200);
        survivalFlySprintingSpeed = config.getInt(ConfPaths.MOVING_SURVIVALFLY_SPRINTINGSPEED, 100);
        survivalFlySwimmingSpeed = config.getInt(ConfPaths.MOVING_SURVIVALFLY_SWIMMINGSPEED, 100);
        survivalFlyWalkingSpeed = config.getInt(ConfPaths.MOVING_SURVIVALFLY_WALKINGSPEED, 100);
        survivalFlyCobwebHack = config.getBoolean(ConfPaths.MOVING_SURVIVALFLY_COBWEBHACK, true);
        sfSlownessSprintHack = config.getAlmostBoolean(ConfPaths.MOVING_SURVIVALFLY_SLOWNESSSPRINTHACK, AlmostBoolean.MAYBE).decideOptimistically();
        sfGroundHop = config.getBoolean(ConfPaths.MOVING_SURVIVALFLY_GROUNDHOP, ServerVersion.compareMinecraftVersion("1.7") == -1);
        survivalFlyAccountingH = config.getBoolean(ConfPaths.MOVING_SURVIVALFLY_EXTENDED_HACC, false);
        survivalFlyAccountingV = config.getBoolean(ConfPaths.MOVING_SURVIVALFLY_EXTENDED_VACC);
        sfSetBackPolicyFallDamage = config.getBoolean(ConfPaths.MOVING_SURVIVALFLY_SETBACKPOLICY_FALLDAMAGE);
        sfSetBackPolicyVoid = config.getBoolean(ConfPaths.MOVING_SURVIVALFLY_SETBACKPOLICY_VOIDTOVOID);
        final double sfStepHeight = config.getDouble(ConfPaths.MOVING_SURVIVALFLY_STEPHEIGHT, Double.MAX_VALUE);
        if (sfStepHeight == Double.MAX_VALUE) {
            final String ref;
            if (Bukkit.getVersion().toLowerCase().indexOf("spigot") != -1) {
                // Assume 1.8 clients being supported.
                ref = "1.7.10";
            } else {
                ref = "1.8";
            }
            this.sfStepHeight = ServerVersion.select(ref, 0.5, 0.6, 0.6, 0.5).doubleValue();
        } else {
            this.sfStepHeight = sfStepHeight;
        }
        survivalFlyVLFreeze = config.getLong(ConfPaths.MOVING_SURVIVALFLY_VLFREEZE, 2000L);
        survivalFlyActions = config.getOptimizedActionList(ConfPaths.MOVING_SURVIVALFLY_ACTIONS, Permissions.MOVING_SURVIVALFLY);

        sfHoverCheck = config.getBoolean(ConfPaths.MOVING_SURVIVALFLY_HOVER_CHECK);
        sfHoverTicks = config.getInt(ConfPaths.MOVING_SURVIVALFLY_HOVER_TICKS);
        sfHoverLoginTicks = Math.max(0, config.getInt(ConfPaths.MOVING_SURVIVALFLY_HOVER_LOGINTICKS));
        sfHoverFallDamage = config.getBoolean(ConfPaths.MOVING_SURVIVALFLY_HOVER_FALLDAMAGE);
        sfHoverViolation = config.getDouble(ConfPaths.MOVING_SURVIVALFLY_HOVER_SFVIOLATION);

        hBufMax = config.getDouble(ConfPaths.MOVING_SURVIVALFLY_HBUFMAX);
        velocityActivationCounter = config.getInt(ConfPaths.MOVING_VELOCITY_ACTIVATIONCOUNTER);
        velocityActivationTicks = config.getInt(ConfPaths.MOVING_VELOCITY_ACTIVATIONTICKS);
        velocityStrictInvalidation = config.getBoolean(ConfPaths.MOVING_VELOCITY_STRICTINVALIDATION);
        yOnGround = config.getDouble(ConfPaths.MOVING_YONGROUND, Magic.Y_ON_GROUND_MIN, Magic.Y_ON_GROUND_MAX, Magic.Y_ON_GROUND_DEFAULT); // sqrt(1/256), see: NetServerHandler.
        noFallyOnGround = config.getDouble(ConfPaths.MOVING_NOFALL_YONGROUND, Magic.Y_ON_GROUND_MIN, Magic.Y_ON_GROUND_MAX, yOnGround);

        AlmostBoolean refSplitMoves = config.getAlmostBoolean(ConfPaths.MOVING_SPLITMOVES, AlmostBoolean.MAYBE);
        //splitMoves = refSplitMoves == AlmostBoolean.MAYBE ? ServerVersion.compareMinecraftVersion("1.9") == -1 : refSplitMoves.decide();
        splitMoves = refSplitMoves.decideOptimistically();
        // TODO: Ignore the stance, once it is known that the server catches such.
        AlmostBoolean refIgnoreStance = config.getAlmostBoolean(ConfPaths.MOVING_IGNORESTANCE, AlmostBoolean.MAYBE);
        ignoreStance = refIgnoreStance == AlmostBoolean.MAYBE ? ServerVersion.compareMinecraftVersion("1.8") >= 0 : refIgnoreStance.decide();
        tempKickIllegal = config.getBoolean(ConfPaths.MOVING_TEMPKICKILLEGAL);
        loadChunksOnJoin = config.getBoolean(ConfPaths.MOVING_LOADCHUNKS_JOIN);
        loadChunksOnMove = config.getBoolean(ConfPaths.MOVING_LOADCHUNKS_MOVE);
        loadChunksOnTeleport = config.getBoolean(ConfPaths.MOVING_LOADCHUNKS_TELEPORT);
        loadChunksOnWorldChange = config.getBoolean(ConfPaths.MOVING_LOADCHUNKS_WORLDCHANGE);
        sprintingGrace = Math.max(0L, (long) (config.getDouble(ConfPaths.MOVING_SPRINTINGGRACE) * 1000.0)); // Config: seconds.
        assumeSprint = config.getBoolean(ConfPaths.MOVING_ASSUMESPRINT);
        speedGrace = Math.max(0, (int) Math.round(config.getDouble(ConfPaths.MOVING_SPEEDGRACE) * 20.0)); // Config: seconds
        AlmostBoolean ref = config.getAlmostBoolean(ConfPaths.MOVING_ENFORCELOCATION, AlmostBoolean.MAYBE);
        if (ref == AlmostBoolean.MAYBE) {
            enforceLocation = Bugs.shouldEnforceLocation();
        } else {
            enforceLocation = ref.decide();
        }
        // TODO: Rename overall flag to trackBlockChanges. Create a sub-config rather.
        trackBlockMove = config.getBoolean(ConfPaths.COMPATIBILITY_BLOCKS_CHANGETRACKER_ACTIVE) 
                && (config.getBoolean(ConfPaths.COMPATIBILITY_BLOCKS_CHANGETRACKER_PISTONS
                        // TODO: || other activation flags.
                        ));
        final PlayerSetBackMethod playerSetBackMethod = PlayerSetBackMethod.fromString(
                "extern.fromconfig", config.getString(ConfPaths.MOVING_SETBACK_METHOD));
        if (playerSetBackMethod.doesThisMakeSense()) {
            // (Might info/warn if legacy is used without setTo and without SCHEDULE and similar?)
            this.playerSetBackMethod = playerSetBackMethod;
        }
        else if (ServerVersion.compareMinecraftVersion("1.9") < 0) {
            this.playerSetBackMethod = PlayerSetBackMethod.LEGACY;
        }
        else {
            // Latest.
            this.playerSetBackMethod = PlayerSetBackMethod.MODERN;
        }

        traceMaxAge = config.getInt(ConfPaths.MOVING_TRACE_MAXAGE, 200);
        traceMaxSize = config.getInt(ConfPaths.MOVING_TRACE_MAXSIZE, 200);

        ref = config.getAlmostBoolean(ConfPaths.MOVING_VEHICLE_ENFORCELOCATION, AlmostBoolean.MAYBE);
        vehicleEnforceLocation = ref.decideOptimistically(); // Currently rather enabled.
        vehiclePreventDestroyOwn = config.getBoolean(ConfPaths.MOVING_VEHICLE_PREVENTDESTROYOWN);
        scheduleVehicleSetBacks = config.getAlmostBoolean(ConfPaths.MOVING_VEHICLE_SCHEDULESETBACKS, AlmostBoolean.MAYBE).decide();
        vehicleMorePacketsActions = config.getOptimizedActionList(ConfPaths.MOVING_VEHICLE_MOREPACKETS_ACTIONS, Permissions.MOVING_MOREPACKETS);
        ref = config.getAlmostBoolean(ConfPaths.MOVING_VEHICLE_ENVELOPE_ACTIVE, AlmostBoolean.MAYBE);
        if (ServerVersion.compareMinecraftVersion("1.9") < 0) {
            worldData.overrideCheckActivation(CheckType.MOVING_VEHICLE_ENVELOPE, 
                    AlmostBoolean.NO, OverrideType.PERMANENT, true);
        }
        config.readDoubleValuesForEntityTypes(ConfPaths.MOVING_VEHICLE_ENVELOPE_HSPEEDCAP, vehicleEnvelopeHorizontalSpeedCap, 4.0, true);
        vehicleEnvelopeActions = config.getOptimizedActionList(ConfPaths.MOVING_VEHICLE_ENVELOPE_ACTIONS, Permissions.MOVING_VEHICLE_ENVELOPE);
        // Ignored vehicle types (ignore mostly, no checks run).
        List<String> types;
        if (config.get(ConfPaths.MOVING_VEHICLE_IGNOREDVEHICLES) == null) { // Hidden setting for now.
            // Use defaults.
            types = Arrays.asList("arrow", "spectral_arrow", "tipped_arrow");
        }
        else {
            types = config.getStringList(ConfPaths.MOVING_VEHICLE_IGNOREDVEHICLES);
        }
        for (String stype : types) {
            try {
                EntityType type = EntityType.valueOf(stype.toUpperCase());
                if (type != null) {
                    ignoredVehicles.add(type);
                }
            }
            catch (IllegalArgumentException e) {}
        }


        // Messages.
        msgKickIllegalMove = ColorUtil.replaceColors(config.getString(ConfPaths.MOVING_MESSAGE_ILLEGALPLAYERMOVE));
        msgKickIllegalVehicleMove = ColorUtil.replaceColors(config.getString(ConfPaths.MOVING_MESSAGE_ILLEGALVEHICLEMOVE));
    }

    public ModelFlying getModelFlying(final Player player, final PlayerLocation fromLocation,
            final MovingData data, final MovingConfig cc) {
        final GameMode gameMode = player.getGameMode();
        final ModelFlying modelGameMode = flyingModelGameMode.get(gameMode);
        switch(gameMode) {
            case SURVIVAL:
            case ADVENTURE:
            case CREATIVE:
                // Specific checks.
                break;
            default:
                // Default by game mode (spectator, yet unknown).
                return modelGameMode;
        }
        final boolean isGlidingWithElytra = Bridge1_9.isGlidingWithElytra(player)
                && MovingUtil.isGlidingWithElytraValid(player, fromLocation, data, cc);
        // Actual flying (ignoreAllowFlight is a legacy option for rocket boots like flying).
        if (player.isFlying() || !isGlidingWithElytra && !ignoreAllowFlight && player.getAllowFlight()) {
            return modelGameMode;
        }
        // Elytra.
        if (isGlidingWithElytra) { // Defensive: don't demand isGliding.
            return flyingModelElytra;
        }
        // Levitation.
        if (gameMode != GameMode.CREATIVE && !Double.isInfinite(Bridge1_9.getLevitationAmplifier(player))
                && !fromLocation.isInLiquid()) {
            return flyingModelLevitation;
        }
        // Default by game mode.
        return modelGameMode;
    }

}
