/*
 * This file is part of Hawk Anticheat.
 * Copyright (C) 2018 Hawk Development Team
 *
 * 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 <https://www.gnu.org/licenses/>.
 */

package me.islandscout.hawk.util;

import me.islandscout.hawk.Hawk;
import me.islandscout.hawk.HawkPlayer;
import me.islandscout.hawk.wrap.block.WrappedBlock;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.material.Openable;
import org.bukkit.util.Vector;

import java.util.*;

public class AdjacentBlocks {

    //loop horizontally around nearby blocks about the size of a player's collision box
    public static Set<Block> getBlocksInLocation(Location loc) {
        Location check = loc.clone();
        Set<Block> blocks = new HashSet<>();
        blocks.add(ServerUtils.getBlockAsync(check.add(0, 0, 0.3)));
        blocks.add(ServerUtils.getBlockAsync(check.add(0.3, 0, 0)));
        blocks.add(ServerUtils.getBlockAsync(check.add(0, 0, -0.3)));
        blocks.add(ServerUtils.getBlockAsync(check.add(0, 0, -0.3)));
        blocks.add(ServerUtils.getBlockAsync(check.add(-0.3, 0, 0)));
        blocks.add(ServerUtils.getBlockAsync(check.add(-0.3, 0, 0)));
        blocks.add(ServerUtils.getBlockAsync(check.add(0, 0, 0.3)));
        blocks.add(ServerUtils.getBlockAsync(check.add(0, 0, 0.3)));
        blocks.remove(null);
        return blocks;
    }

    public static boolean matIsAdjacent(Location loc, Material... materials) {
        Location check = loc.clone();
        Set<Block> sample = new HashSet<>();
        sample.add(ServerUtils.getBlockAsync(check.add(0, 0, 0.3)));
        sample.add(ServerUtils.getBlockAsync(check.add(0.3, 0, 0)));
        sample.add(ServerUtils.getBlockAsync(check.add(0, 0, -0.3)));
        sample.add(ServerUtils.getBlockAsync(check.add(0, 0, -0.3)));
        sample.add(ServerUtils.getBlockAsync(check.add(-0.3, 0, 0)));
        sample.add(ServerUtils.getBlockAsync(check.add(-0.3, 0, 0)));
        sample.add(ServerUtils.getBlockAsync(check.add(0, 0, 0.3)));
        sample.add(ServerUtils.getBlockAsync(check.add(0, 0, 0.3)));
        for (Block b : sample) {
            if (b == null)
                continue;
            for (Material mat : materials) {
                if (b.getType() == mat)
                    return true;
            }
        }
        return false;
    }
    /*public static boolean matIsAdjacent(Location loc, Material material) {
        Location check = loc.clone();
        return ServerUtils.getBlockAsync(check.add(0, 0, 0.3)).getType() == material ||
                ServerUtils.getBlockAsync(check.add(0.3, 0, 0)).getType() == material ||
                ServerUtils.getBlockAsync(check.add(0, 0, -0.3)).getType() == material ||
                ServerUtils.getBlockAsync(check.add(0, 0, -0.3)).getType() == material ||
                ServerUtils.getBlockAsync(check.add(-0.3, 0, 0)).getType() == material ||
                ServerUtils.getBlockAsync(check.add(-0.3, 0, 0)).getType() == material ||
                ServerUtils.getBlockAsync(check.add(0, 0, 0.3)).getType() == material ||
                ServerUtils.getBlockAsync(check.add(0, 0, 0.3)).getType() == material;
    }*/

    public static boolean matContainsStringIsAdjacent(Location loc, String name) {
        Location check = loc.clone();
        Set<Block> sample = new HashSet<>();
        sample.add(ServerUtils.getBlockAsync(check.add(0, 0, 0.3)));
        sample.add(ServerUtils.getBlockAsync(check.add(0.3, 0, 0)));
        sample.add(ServerUtils.getBlockAsync(check.add(0, 0, -0.3)));
        sample.add(ServerUtils.getBlockAsync(check.add(0, 0, -0.3)));
        sample.add(ServerUtils.getBlockAsync(check.add(-0.3, 0, 0)));
        sample.add(ServerUtils.getBlockAsync(check.add(-0.3, 0, 0)));
        sample.add(ServerUtils.getBlockAsync(check.add(0, 0, 0.3)));
        sample.add(ServerUtils.getBlockAsync(check.add(0, 0, 0.3)));
        for (Block b : sample) {
            if (b != null && b.getType().name().contains(name))
                return true;
        }
        return false;
    }

    public static boolean blockAdjacentIsSolid(Location loc) {
        Location check = loc.clone();
        Set<Block> sample = new HashSet<>();
        sample.add(ServerUtils.getBlockAsync(check.add(0, 0, 0.3)));
        sample.add(ServerUtils.getBlockAsync(check.add(0.3, 0, 0)));
        sample.add(ServerUtils.getBlockAsync(check.add(0, 0, -0.3)));
        sample.add(ServerUtils.getBlockAsync(check.add(0, 0, -0.3)));
        sample.add(ServerUtils.getBlockAsync(check.add(-0.3, 0, 0)));
        sample.add(ServerUtils.getBlockAsync(check.add(-0.3, 0, 0)));
        sample.add(ServerUtils.getBlockAsync(check.add(0, 0, 0.3)));
        sample.add(ServerUtils.getBlockAsync(check.add(0, 0, 0.3)));
        for (Block b : sample) {
            if (b != null && b.getType().isSolid())
                return true;
        }
        return false;
    }

    public static boolean blockAdjacentIsLiquid(Location loc) {
        Location check = loc.clone();
        Set<Block> sample = new HashSet<>();
        sample.add(ServerUtils.getBlockAsync(check.add(0, 0, 0.3)));
        sample.add(ServerUtils.getBlockAsync(check.add(0.3, 0, 0)));
        sample.add(ServerUtils.getBlockAsync(check.add(0, 0, -0.3)));
        sample.add(ServerUtils.getBlockAsync(check.add(0, 0, -0.3)));
        sample.add(ServerUtils.getBlockAsync(check.add(-0.3, 0, 0)));
        sample.add(ServerUtils.getBlockAsync(check.add(-0.3, 0, 0)));
        sample.add(ServerUtils.getBlockAsync(check.add(0, 0, 0.3)));
        sample.add(ServerUtils.getBlockAsync(check.add(0, 0, 0.3)));
        for (Block b : sample) {
            if (b != null && b.isLiquid())
                return true;
        }
        return false;
    }

    /**
     * Checks if the location is on ground. Good replacement for Entity#isOnGround()
     * since that flag can be spoofed by the client. Hawk's definition of being on
     * ground: yVelocity must not exceed 0.6, player's feet must not be inside
     * a solid block's AABB, and there must be at least 1 solid block AABB collision
     * with the AABB defining the thin area below the location (represents below the
     * player's feet).
     *
     * @param loc            Test location
     * @param yVelocity      Y-velocity
     * @param ignoreInGround return false if location is inside something
     * @param feetDepth      Don't set this too low. The client doesn't like to send moves unless they are significant enough.
     * @param pp
     * @return boolean
     */
    //if not sure what your velocity is, just put -1 for velocity
    //if you just want to check for location, just put -1 for velocity
    public static boolean onGroundReally(Location loc, double yVelocity, boolean ignoreInGround, double feetDepth, HawkPlayer pp) {
        if (yVelocity > 0.6) //allows stepping up short blocks, but not full blocks
            return false;
        Location check = loc.clone();
        Set<Block> blocks = new HashSet<>();
        blocks.addAll(AdjacentBlocks.getBlocksInLocation(check));
        blocks.addAll(AdjacentBlocks.getBlocksInLocation(check.add(0, -1, 0)));

        AABB underFeet = new AABB(loc.toVector().add(new Vector(-0.3, -feetDepth, -0.3)), loc.toVector().add(new Vector(0.3, 0, 0.3)));
        for (Block block : blocks) {
            WrappedBlock bNMS = WrappedBlock.getWrappedBlock(block, pp.getClientVersion());
            if (block.isLiquid() || (!bNMS.isSolid() && Hawk.getServerVersion() == 8))
                continue;
            if (bNMS.isColliding(underFeet)) {

                //almost done. gotta do one more check... Check if their foot ain't in a block. (stops checkerclimb)
                if (ignoreInGround) {
                    AABB topFeet = underFeet.clone();
                    topFeet.translate(new Vector(0, feetDepth + 0.00001, 0));
                    for (Block block1 : AdjacentBlocks.getBlocksInLocation(loc)) {
                        WrappedBlock bNMS1 = WrappedBlock.getWrappedBlock(block1, pp.getClientVersion());
                        if (block1.isLiquid() || (!bNMS1.isSolid() && Hawk.getServerVersion() == 8) ||
                                block1.getState().getData() instanceof Openable ||
                                pp.getIgnoredBlockCollisions().contains(block1.getLocation()))
                            continue;
                        if (bNMS1.isColliding(topFeet))
                            return false;
                    }
                }

                return true;
            }
        }
        return false;
    }

    @SuppressWarnings("BooleanMethodIsAlwaysInverted")
    public static boolean blockNearbyIsSolid(Location loc, boolean hawkDefinition) {
        Location check = loc.clone();
        Set<Block> sample = new HashSet<>();
        sample.add(ServerUtils.getBlockAsync(check.add(0, 0, 1)));
        sample.add(ServerUtils.getBlockAsync(check.add(1, 0, 0)));
        sample.add(ServerUtils.getBlockAsync(check.add(0, 0, -1)));
        sample.add(ServerUtils.getBlockAsync(check.add(0, 0, -1)));
        sample.add(ServerUtils.getBlockAsync(check.add(-1, 0, 0)));
        sample.add(ServerUtils.getBlockAsync(check.add(-1, 0, 0)));
        sample.add(ServerUtils.getBlockAsync(check.add(0, 0, 1)));
        sample.add(ServerUtils.getBlockAsync(check.add(0, 0, 1)));
        for (Block b : sample) {
            if (b == null)
                continue;
            if (hawkDefinition) {
                if (WrappedBlock.getWrappedBlock(b, Hawk.getServerVersion()).isSolid())
                    return true;
            } else if (b.getType().isSolid())
                return true;
        }
        return false;
    }

    public static Set<Direction> checkTouchingBlock(AABB boundingBox, World world, double borderSize, int clientVersion) {
        AABB bigBox = boundingBox.clone();
        Vector min = bigBox.getMin().add(new Vector(-borderSize, -borderSize, -borderSize));
        Vector max = bigBox.getMax().add(new Vector(borderSize, borderSize, borderSize));
        Set<Direction> directions = EnumSet.noneOf(Direction.class);
        //The coordinates should be floored, but this works too.
        for (int x = (int) (min.getX() < 0 ? min.getX() - 1 : min.getX()); x <= max.getX(); x++) {
            for (int y = (int) min.getY() - 1; y <= max.getY(); y++) { //always subtract 1 so that fences/walls can be checked
                for (int z = (int) (min.getZ() < 0 ? min.getZ() - 1 : min.getZ()); z <= max.getZ(); z++) {
                    Block b = ServerUtils.getBlockAsync(new Location(world, x, y, z));
                    if (b != null) {
                        WrappedBlock bNMS = WrappedBlock.getWrappedBlock(b, clientVersion);
                        for (AABB blockBox : bNMS.getCollisionBoxes()) {
                            if (blockBox.getMin().getX() > boundingBox.getMax().getX() && blockBox.getMin().getX() < bigBox.getMax().getX()) {
                                directions.add(Direction.EAST);
                            }
                            if (blockBox.getMin().getY() > boundingBox.getMax().getY() && blockBox.getMin().getY() < bigBox.getMax().getY()) {
                                directions.add(Direction.TOP);
                            }
                            if (blockBox.getMin().getZ() > boundingBox.getMax().getZ() && blockBox.getMin().getZ() < bigBox.getMax().getZ()) {
                                directions.add(Direction.SOUTH);
                            }
                            if (blockBox.getMax().getX() > bigBox.getMin().getX() && blockBox.getMax().getX() < boundingBox.getMin().getX()) {
                                directions.add(Direction.WEST);
                            }
                            if (blockBox.getMax().getY() > bigBox.getMin().getY() && blockBox.getMax().getY() < boundingBox.getMin().getY()) {
                                directions.add(Direction.BOTTOM);
                            }
                            if (blockBox.getMax().getZ() > bigBox.getMin().getZ() && blockBox.getMax().getZ() < boundingBox.getMin().getZ()) {
                                directions.add(Direction.NORTH);
                            }
                        }
                    }
                }
            }
        }
        return directions;
    }
}