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

import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.block.BlockState;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.entity.Projectile;
import org.bukkit.event.Event.Result;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.block.Action;
import org.bukkit.event.block.BlockPlaceEvent;
import org.bukkit.event.block.SignChangeEvent;
import org.bukkit.event.entity.ProjectileLaunchEvent;
import org.bukkit.event.player.PlayerAnimationEvent;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.inventory.ItemStack;

import fr.neatmonster.nocheatplus.NCPAPIProvider;
import fr.neatmonster.nocheatplus.checks.CheckListener;
import fr.neatmonster.nocheatplus.checks.CheckType;
import fr.neatmonster.nocheatplus.checks.blockinteract.BlockInteractData;
import fr.neatmonster.nocheatplus.checks.blockinteract.BlockInteractListener;
import fr.neatmonster.nocheatplus.checks.combined.Combined;
import fr.neatmonster.nocheatplus.checks.combined.CombinedConfig;
import fr.neatmonster.nocheatplus.checks.combined.Improbable;
import fr.neatmonster.nocheatplus.checks.moving.MovingConfig;
import fr.neatmonster.nocheatplus.checks.moving.util.MovingUtil;
import fr.neatmonster.nocheatplus.checks.net.FlyingQueueHandle;
import fr.neatmonster.nocheatplus.checks.net.model.DataPacketFlying;
import fr.neatmonster.nocheatplus.compat.Bridge1_9;
import fr.neatmonster.nocheatplus.compat.BridgeMisc;
import fr.neatmonster.nocheatplus.permissions.Permissions;
import fr.neatmonster.nocheatplus.players.DataManager;
import fr.neatmonster.nocheatplus.players.IPlayerData;
import fr.neatmonster.nocheatplus.stats.Counters;
import fr.neatmonster.nocheatplus.utilities.InventoryUtil;
import fr.neatmonster.nocheatplus.utilities.ReflectionUtil;
import fr.neatmonster.nocheatplus.utilities.TickTask;
import fr.neatmonster.nocheatplus.utilities.map.BlockProperties;

/**
 * Central location to listen to events that are relevant for the block place checks.
 * 
 * @see BlockPlaceEvent
 */
public class BlockPlaceListener extends CheckListener {

    private static final int p1 = 73856093;
    private static final int p2 = 19349663;
    private static final int p3 = 83492791;

    private static final int getHash(final int x, final int y, final int z) {
        return p1 * x ^ p2 * y ^ p3 * z;
    }

    public static int getCoordHash(final Block block) {
        return getHash(block.getX(), block.getY(), block.getZ());
    }

    public static int getBlockPlaceHash(final Block block, final Material mat) {
        int hash = getCoordHash(block);
        if (mat != null) {
            hash |= mat.name().hashCode();
        }
        hash |= block.getWorld().getName().hashCode();
        return hash;
    }

    /** Against. */
    private final Against against = addCheck(new Against());

    /** AutoSign. */
    private final AutoSign autoSign = addCheck(new AutoSign());

    /** The direction check. */
    private final Direction direction = addCheck(new Direction());

    /** The fast place check. */
    private final FastPlace fastPlace = addCheck(new FastPlace());

    /** The no swing check. */
    private final NoSwing   noSwing   = addCheck(new NoSwing());

    /** The reach check. */
    private final Reach     reach     = addCheck(new Reach());

    /** The speed check. */
    private final Speed     speed     = addCheck(new Speed());

    /** For temporary use: LocUtil.clone before passing deeply, call setWorld(null) after use. */
    private final Location useLoc = new Location(null, 0, 0, 0);

    private final Counters counters = NCPAPIProvider.getNoCheatPlusAPI().getGenericInstance(Counters.class);
    private final int idBoatsAnywhere = counters.registerKey("boatsanywhere");
    private final int idEnderPearl = counters.registerKey("throwenderpearl");

    private final Class<?> blockMultiPlaceEvent = ReflectionUtil.getClass("org.bukkit.event.block.BlockMultiPlaceEvent");
    private final boolean hasGetReplacedState = ReflectionUtil.getMethodNoArgs(BlockPlaceEvent.class, "getReplacedState", BlockState.class) != null;

    public BlockPlaceListener() {
        super(CheckType.BLOCKPLACE);
    }

    /**
     * We listen to BlockPlace events for obvious reasons.
     * 
     * @param event
     *            the event
     */
    @EventHandler(
            ignoreCancelled = true, priority = EventPriority.LOWEST)
    public void onBlockPlace(final BlockPlaceEvent event) {

        final Block block = event.getBlockPlaced();
        final Block blockAgainst = event.getBlockAgainst();
        // Skip any null blocks.
        if (block == null || blockAgainst == null) {
            return;
        }
        // TODO: What if same block?

        // TODO: Revise material use (not block.get... ?)
        //final Material mat = block.getType();
        final Player player = event.getPlayer();
        final Material placedMat;
        if (hasGetReplacedState) {
            placedMat = event.getBlockPlaced().getType();
        }
        else if (Bridge1_9.hasGetItemInOffHand()) {
            final ItemStack stack = event.getItemInHand();
            placedMat = BlockProperties.isAir(stack) ? Material.AIR : stack.getType();
        }
        else {
            placedMat = Bridge1_9.getItemInMainHand(player).getType(); // Safety first.
        }
        boolean cancelled = false;

        final IPlayerData pData = DataManager.getPlayerData(player); // TODO: Use for data + config getting etc.
        final BlockPlaceData data = pData.getGenericInstance(BlockPlaceData.class);
        final BlockPlaceConfig cc = pData.getGenericInstance(BlockPlaceConfig.class);
        final BlockInteractData bdata = pData.getGenericInstance(BlockInteractData.class);
        final int tick = TickTask.getTick();
        // isInteractBlock - the block placed against is the block last interacted with.
        final boolean isInteractBlock = !bdata.getLastIsCancelled() && bdata.matchesLastBlock(tick, blockAgainst);
        int skippedRedundantChecks = 0;
        final boolean debug = pData.isDebugActive(CheckType.BLOCKPLACE);

        final boolean shouldSkipSome;
        if (blockMultiPlaceEvent != null && event.getClass() == blockMultiPlaceEvent) {
            if (placedMat == Material.BEDROCK 
                    || Bridge1_9.hasEndCrystalItem() && placedMat == Bridge1_9.END_CRYSTAL_ITEM) {
                shouldSkipSome = true;
            }
            else {
                if (debug) {
                    debug(player, "Block place " + event.getClass().getName() + " " + placedMat);
                }
                shouldSkipSome = false;
            }
        }
        else {
            shouldSkipSome = false;
        }

        if (placedMat == Material.SIGN) {
            // Might move to MONITOR priority.
            data.autoSignPlacedTime = System.currentTimeMillis();
            // Always hash as sign post for improved compatibility with Lockette etc.
            data.autoSignPlacedHash = getBlockPlaceHash(block, Material.SIGN);
        }

        // Don't run checks, if a set back is scheduled.
        if (!cancelled && pData.isPlayerSetBackScheduled()) {
            cancelled = true;
        }

        // Fast place check.
        if (!cancelled && fastPlace.isEnabled(player, pData)) {
            if (fastPlace.check(player, block, tick, data, cc, pData)) {
                cancelled = true;
            }
            else {
                // Feed the improbable.
                Improbable.feed(player, 0.5f, System.currentTimeMillis(), pData);
            }
        }

        // No swing check (player doesn't swing their arm when placing a lily pad).
        if (!cancelled && !cc.noSwingExceptions.contains(placedMat) 
                && noSwing.isEnabled(player, pData) && noSwing.check(player, data, cc)) {
            // Consider skipping all insta placables or using simplified version (true or true within time frame).
            cancelled = true;
        }

        final FlyingQueueHandle flyingHandle;
        final boolean reachCheck = pData.isCheckActive(CheckType.BLOCKPLACE_REACH, player);
        final boolean directionCheck = pData.isCheckActive(CheckType.BLOCKPLACE_DIRECTION, player);
        if (reachCheck || directionCheck) {
            flyingHandle = new FlyingQueueHandle(pData);
            final Location loc = player.getLocation(useLoc);
            final double eyeHeight = MovingUtil.getEyeHeight(player);
            // Reach check (distance).
            if (!cancelled && !shouldSkipSome) {
                if (isInteractBlock && bdata.isPassedCheck(CheckType.BLOCKINTERACT_REACH)) {
                    skippedRedundantChecks ++;
                }
                else if (reachCheck && reach.check(player, eyeHeight, block, data, cc)) {
                    cancelled = true;
                }
            }

            // Direction check.
            if (!cancelled && !shouldSkipSome) {
                if (isInteractBlock && bdata.isPassedCheck(CheckType.BLOCKINTERACT_DIRECTION)) {
                    skippedRedundantChecks ++;
                }
                else if (directionCheck && direction.check(player, loc, eyeHeight, block, 
                        flyingHandle, data, cc, pData)) {
                    cancelled = true;
                }
            }
            useLoc.setWorld(null);
        }
        else {
            flyingHandle = null;
        }

        // Surrounding material.
        if (!cancelled && against.isEnabled(player, pData) && against.check(player, block, placedMat, blockAgainst, 
                isInteractBlock, data, cc, pData)) {
            cancelled = true;
        }

        // If one of the checks requested to cancel the event, do so.
        if (cancelled) {
            event.setCancelled(cancelled);
        }
        else {
            // Debug log (only if not cancelled, to avoid spam).
            if (debug) {
                debugBlockPlace(player, placedMat, block, blockAgainst, 
                        skippedRedundantChecks, flyingHandle, pData);
            }
        }
        // Cleanup
        // Reminder(currently unused): useLoc.setWorld(null);
    }

    private void debugBlockPlace(final Player player, final Material placedMat, 
            final Block block, final Block blockAgainst, 
            final int skippedRedundantChecks, final FlyingQueueHandle flyingHandle,
            final IPlayerData pData) {
        debug(player, "Block place(" + placedMat + "): " + block.getX() + ", " + block.getY() + ", " + block.getZ());
        BlockInteractListener.debugBlockVSBlockInteract(player, checkType, 
                blockAgainst, "onBlockPlace(blockAgainst)", Action.RIGHT_CLICK_BLOCK,
                pData);
        if (skippedRedundantChecks > 0) {
            debug(player, "Skipped redundant checks: " + skippedRedundantChecks);
        }
        if (flyingHandle != null && flyingHandle.isFlyingQueueFetched()) {
            final int flyingIndex = flyingHandle.getFirstIndexWithContentIfFetched();
            final DataPacketFlying packet = flyingHandle.getIfFetched(flyingIndex);
            if (packet != null) {
                debug(player, "Flying packet queue used at index " + flyingIndex + ": pitch=" + packet.getPitch() + ",yaw=" + packet.getYaw());
                return;
            }
        }
    }

    @EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true)
    public void onSignChange(final SignChangeEvent event) {
        if (event.getClass() != SignChangeEvent.class) {
            // Built in plugin compatibility.
            // TODO: Don't understand why two consecutive events editing the same block are a problem.
            return;
        }
        final Player player = event.getPlayer();
        final Block block = event.getBlock();
        final String[] lines = event.getLines();
        if (block == null || lines == null || player == null) {
            // Somewhat defensive.
            return;
        }
        final IPlayerData pData = DataManager.getPlayerData(player);
        if (autoSign.isEnabled(player, pData) && autoSign.check(player, block, lines, pData)) {
            event.setCancelled(true);
        }
    }

    /**
     * We listen to PlayerAnimation events because it is (currently) equivalent to "player swings arm" and we want to
     * check if they did that between block breaks.
     * 
     * @param event
     *            the event
     */
    @EventHandler(
            priority = EventPriority.MONITOR)
    public void onPlayerAnimation(final PlayerAnimationEvent event) {
        // Just set a flag to true when the arm was swung.
        DataManager.getPlayerData(event.getPlayer()).getGenericInstance(BlockPlaceData.class).noSwingArmSwung = true;
    }

    /**
     * We listener to PlayerInteract events to prevent players from spamming the server with monster eggs.
     * 
     * @param event
     *            the event
     */
    @EventHandler(ignoreCancelled = false, priority = EventPriority.LOWEST)
    public void onPlayerInteract(final PlayerInteractEvent event) {
        if (event.isCancelled()) {
            // TODO: Might run checks if (event.useInteractedBlock()) ...
            return;
        }
        if (event.getAction() != Action.RIGHT_CLICK_BLOCK) {
            return;
        }
        final Player player = event.getPlayer();

        final ItemStack stack = Bridge1_9.getUsedItem(player, event);
        if (stack == null) {
            return;
        }

        final IPlayerData pData = DataManager.getPlayerData(player);
        final BlockPlaceConfig cc = pData.getGenericInstance(BlockPlaceConfig.class);
        final Material type = stack.getType();
        if (InventoryUtil.isBoat(type)) {
            if (cc.preventBoatsAnywhere) {
                // TODO: Alter config (activation, allow on top of ground).
                // TODO: Version/plugin specific alteration for 'default'.
                checkBoatsAnywhere(player, event, cc, pData);
            }
        }
        else if (type == Material.MONSTER_EGG) {
            // Check blockplace.speed.
            if (speed.isEnabled(player, pData) && speed.check(player, cc, pData)) {
                // If the check was positive, cancel the event.
                event.setCancelled(true);
            }
        }
    }

    private void checkBoatsAnywhere(final Player player, final PlayerInteractEvent event, 
            final BlockPlaceConfig cc, final IPlayerData pData) {
        // Check boats-anywhere.
        final Block block = event.getClickedBlock();
        final Material mat = block.getType();

        // TODO: allow lava ?
        if (BlockProperties.isWater(mat)) {
            return;
        }

        // TODO: Shouldn't this be the opposite face?
        final BlockFace blockFace = event.getBlockFace();
        final Block relBlock = block.getRelative(blockFace);
        final Material relMat = relBlock.getType();

        // TODO: Placing inside of water, but not "against" ?
        if (BlockProperties.isWater(relMat)) {
            return;
        }

        // TODO: Add a check type for exemption?
        if (!pData.hasPermission(Permissions.BLOCKPLACE_BOATSANYWHERE, player)) {
            final Result previousUseBlock = event.useInteractedBlock();
            event.setCancelled(true);
            event.setUseItemInHand(Result.DENY);
            event.setUseInteractedBlock(previousUseBlock == Result.DEFAULT ? Result.ALLOW : previousUseBlock);
            counters.addPrimaryThread(idBoatsAnywhere, 1);
        }
    }

    /**
     * We listen to ProjectileLaunch events to prevent players from launching projectiles too quickly.
     * 
     * @param event
     *            the event
     */
    @EventHandler(
            ignoreCancelled = true, priority = EventPriority.LOWEST)
    public void onProjectileLaunch(final ProjectileLaunchEvent event) {
        // The shooter needs to be a player.
        final Projectile projectile = event.getEntity();
        final Player player = BridgeMisc.getShooterPlayer(projectile);
        if (player == null) {
            return;
        }

        if (MovingUtil.hasScheduledPlayerSetBack(player)) {
            // TODO: Should log.
            event.setCancelled(true);
            return;
        }

        // And the projectile must be one the following:
        EntityType type = event.getEntityType();
        switch (type) {
            case ENDER_PEARL:
                break;
            case ENDER_SIGNAL:
                break;
            case EGG:
                break;
            case SNOWBALL:
                break;
            case THROWN_EXP_BOTTLE:
                break;
            case SPLASH_POTION:
                break;
            default:
                return;
        }

        // Do the actual check...
        final IPlayerData pData = DataManager.getPlayerData(player);
        final BlockPlaceConfig cc = pData.getGenericInstance(BlockPlaceConfig.class);
        boolean cancel = false;
        if (speed.isEnabled(player, pData)) {
            final long now = System.currentTimeMillis();
            final Location loc = player.getLocation(useLoc);
            if (Combined.checkYawRate(player, loc.getYaw(), now, loc.getWorld().getName(), pData)) {
                // Yawrate (checked extra).
                cancel = true;
            }
            if (speed.check(player, cc, pData)) {
                // If the check was positive, cancel the event.
                cancel = true;
            }
            else if (Improbable.check(player, 0.6f, now, "blockplace.speed", pData)) {
                // Combined fighting speed.
                cancel = true;
            }
        }

        // Ender pearl glitch (ab-) use.
        if (!cancel && type == EntityType.ENDER_PEARL) {
            if (!pData.getGenericInstance(CombinedConfig.class).enderPearlCheck) {
                // Do nothing !
                // TODO: Might have further flags?
            }
            else if (!BlockProperties.isPassable(projectile.getLocation(useLoc))) {
                // Launch into a block.
                // TODO: This might be a general check later.       		
                cancel = true;
            }
            else {
                if (!BlockProperties.isPassable(player.getEyeLocation(), projectile.getLocation(useLoc))) {
                    // (Spare a useLoc2, for this is seldom rather.)
                    // Something between player 
                    // TODO: This might be a general check later.
                    cancel = true;
                }
                else {
                    final Material mat = player.getLocation(useLoc).getBlock().getType();
                    final long flags = BlockProperties.F_CLIMBABLE | BlockProperties.F_LIQUID | BlockProperties.F_IGN_PASSABLE;
                    if (!BlockProperties.isAir(mat) && (BlockProperties.getBlockFlags(mat) & flags) == 0 && !mcAccess.getHandle().hasGravity(mat)) {
                        // Still fails on piston traps etc.
                        if (!BlockProperties.isPassable(player.getLocation(), projectile.getLocation()) 
                                && !BlockProperties.isOnGroundOrResetCond(player, player.getLocation(), 
                                        pData.getGenericInstance(MovingConfig.class).yOnGround)) {
                            cancel = true;
                        }
                    }
                }
            }
            if (cancel) {
                counters.addPrimaryThread(idEnderPearl, 1);
            }
        }

        // Cancelled ?
        if (cancel) {
            event.setCancelled(true);
        }
        // Cleanup.
        useLoc.setWorld(null);
    }
}
