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

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.bukkit.Location;
import org.bukkit.Sound;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;

import com.comphenix.protocol.PacketType;
import com.comphenix.protocol.events.ListenerPriority;
import com.comphenix.protocol.events.PacketContainer;
import com.comphenix.protocol.events.PacketEvent;
import com.comphenix.protocol.reflect.StructureModifier;

import fr.neatmonster.nocheatplus.checks.CheckType;
import fr.neatmonster.nocheatplus.checks.net.NetConfig;
import fr.neatmonster.nocheatplus.compat.versions.ServerVersion;
import fr.neatmonster.nocheatplus.players.DataManager;
import fr.neatmonster.nocheatplus.players.IPlayerData;
import fr.neatmonster.nocheatplus.utilities.location.TrigUtil;

public class SoundDistance extends BaseAdapter {

    // TODO: Will not be effective with 512 radius, if they add the patch by @Amranth.
    // TODO: For lower distances more packets might need to be intercepted.

    /** Partly by debugging, partly from various sources, possibly including wrong spelling. */
    private static final Set<String> effectNames = new HashSet<String>(Arrays.asList(
            ////////////
            // PRE 1.9
            ////////////

            // Weather
            "ambient.weather.thunder",
            // Wither
            "wither-spawn-sound-radius",
            "mob.wither.spawn",
            "mob.wither.shoot",
            "mob.wither.idle",
            "mob.wither.hurt",
            "mob.wither.death",
            // Enderdragon
            "dragon-death-sound-radius",
            "mob.enderdragon.wings",
            "mob.enderdragon.grow",
            "mob.enderdragon.growl",
            "mob.enderdragon.hit",
            "mob.enderdragon.end",
            "game.neutral.die", // Enderdragon 1.8.7 (debug).

            //////////////////
            // 1.9 AND LATER
            //////////////////
            // Weather
            "ENTITY_LIGHTNING_IMPACT",
            "ENTITY_LIGHTNING_THUNDER",
            // Enderdragon
            "ENTITY_ENDERDRAGON_AMBIENT",
            "ENTITY_ENDERDRAGON_DEATH",
            "ENTITY_ENDERDRAGON_FIREBALL_EXPLODE",
            "ENTITY_ENDERDRAGON_FLAP",
            "ENTITY_ENDERDRAGON_GROWL",
            "ENTITY_ENDERDRAGON_HURT",
            "ENTITY_ENDERDRAGON_SHOOT",
            // Wither
            "ENTITY_WITHER_AMBIENT",
            "ENTITY_WITHER_BREAK_BLOCK",
            "ENTITY_WITHER_DEATH",
            "ENTITY_WITHER_HURT",
            "ENTITY_WITHER_SHOOT",
            "ENTITY_WITHER_SPAWN"


            ));

    private final Integer idSoundEffectCancel = counters.registerKey("packet.sound.cancel");
    private final Location useLoc = new Location(null, 0, 0, 0);
    /** Legacy check behavior. */
    private final boolean pre1_9;

    public SoundDistance(Plugin plugin) {
        super(plugin, ListenerPriority.LOW, PacketType.Play.Server.NAMED_SOUND_EFFECT);
        this.checkType = CheckType.NET_SOUNDDISTANCE;
        pre1_9 = ServerVersion.compareMinecraftVersion("1.9") < 0;
        inflateEffectNames();
    }

    /**
     * Ensure both lower and upper case are contained.
     */
    private void inflateEffectNames() {
        final List<String> names = new ArrayList<String>(effectNames);
        for (String name : names) {
            effectNames.add(name.toLowerCase());
            effectNames.add(name.toUpperCase());
        }
    }

    private boolean isSoundMonitoredPre1_9(final PacketContainer packetContainer) {
        //debug(null, packetContainer.getStrings().read(0));
        return effectNames.contains(packetContainer.getStrings().read(0));
    }

    private boolean isSoundMonitoredLatest(final PacketContainer packetContainer) {
        StructureModifier<Sound> sounds = packetContainer.getSoundEffects();
        for (final Sound sound : sounds.getValues()) {
            if (effectNames.contains(sound.name())) {
                //debug(null, "MONITOR SOUND: " + sound);
                return true;
            }
        }
        return false;
    }

    private boolean isSoundMonitored(final PacketContainer packetContainer) {
        if (pre1_9) {
            return isSoundMonitoredPre1_9(packetContainer);
        }
        else {
            return isSoundMonitoredLatest(packetContainer);
        }
    }

    @Override
    public void onPacketSending(final PacketEvent event) {
        final PacketContainer packetContainer = event.getPacket();

        // Compare sound effect name.
        if (!isSoundMonitored(packetContainer)) {
            return;
        }

        final Player player = event.getPlayer();
        final IPlayerData pData = DataManager.getPlayerData(player);
        if (!pData.isCheckActive(CheckType.NET_SOUNDDISTANCE, player)) {
            return;
        }

        // Compare distance of player to the weather location.
        final Location loc = player.getLocation(useLoc);
        final StructureModifier<Integer> ints = packetContainer.getIntegers();
        final double dSq = TrigUtil.distanceSquared(ints.read(0) / 8, ints.read(2) / 8, loc.getX(), loc.getZ());
        //        if (data.debug) {
        //            debug(player, "SoundDistance(" + soundName + "): " + StringUtil.fdec1.format(Math.sqrt(dSq)));
        //        }
        final NetConfig cc = pData.getGenericInstance(NetConfig.class);
        if (dSq > cc.soundDistanceSq) {
            event.setCancelled(true);
            counters.add(idSoundEffectCancel, 1);
        }
        useLoc.setWorld(null);
    }

}
