package tc.oc.commons.bukkit.util;

import java.util.Collection;

import gnu.trove.set.TLongSet;
import gnu.trove.set.hash.TLongHashSet;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.block.BlockState;
import org.bukkit.util.BlockVector;
import org.bukkit.util.Vector;

public class BlockUtils {
    public static BlockVector position(BlockState block) {
        return new BlockVector(block.getX(), block.getY(), block.getZ());
    }

    public static Location center(Location location) {
        Location center = location.clone();
        center.setX(center.getBlockX() + 0.5);
        center.setY(center.getBlockY() + 0.5);
        center.setZ(center.getBlockZ() + 0.5);
        return center;
    }

    public static EntityLocation center(EntityLocation location) {
        EntityLocation center = location.clone();
        center.setX(center.getBlockX() + 0.5);
        center.setY(center.getBlockY() + 0.5);
        center.setZ(center.getBlockZ() + 0.5);
        return center;
    }

    public static Location center(Block block) {
        return center(block.getLocation());
    }

    public static Location center(BlockState state) {
        return center(state.getLocation());
    }

    /**
     * Return the "base" {@link Location} of the block at the given location,
     * which is the bottom center point on the block (i.e. the location of any
     * block-shaped entity that is aligned with the block).
     */
    public static Location base(Location location) {
        Location center = location.clone();
        center.setX(center.getBlockX() + 0.5);
        center.setY(center.getBlockY());
        center.setZ(center.getBlockZ() + 0.5);
        return center;
    }

    public static Location base(Block block) {
        return base(block.getLocation());
    }

    public static Location base(BlockState state) {
        return base(state.getLocation());
    }

    public static boolean isInside(Vector point, Location blockLocation) {
        return blockLocation.getX() <= point.getX() && point.getX() <= blockLocation.getX() + 1 &&
               blockLocation.getY() <= point.getY() && point.getY() <= blockLocation.getY() + 1 &&
               blockLocation.getZ() <= point.getZ() && point.getZ() <= blockLocation.getZ() + 1;
    }

    /**
     *  BlockVector encoding API - pack a BlockVector into a single long
     */

    private static final int SHIFT = 21;
    private static final long MASK = ~(-1 << SHIFT);
    private static final long SIGN_MASK = 1 << (SHIFT - 1);

    /**
     * Decode a single component from the packed coordinates
     */
    private static long unpack(long packed, int shift) {
        packed >>= shift;

        // Sign extension
        if((packed & SIGN_MASK) == 0) {
            packed &= MASK;
        } else {
            packed |= ~MASK;
        }

        return packed;
    }

    public static BlockVector decodePos(long from, BlockVector to) {
        to.setX(unpack(from, 0));
        to.setY(unpack(from, SHIFT));
        to.setZ(unpack(from, SHIFT + SHIFT));
        return to;
    }

    public static BlockVector decodePos(long encoded) {
        return new BlockVector(
            unpack(encoded, 0),
            unpack(encoded, SHIFT),
            unpack(encoded, SHIFT + SHIFT)
        );
    }

    public static final long ENCODED_NULL_POS = Long.MIN_VALUE;

    public static long encodePos(long x, long y, long z) {
        return (x & MASK) | ((y & MASK) << SHIFT) | ((z & MASK) << (SHIFT + SHIFT));
    }

    public static long encodePos(BlockVector vector) {
        return encodePos(vector.getBlockX(), vector.getBlockY(), vector.getBlockZ());
    }

    public static long encodePos(Block block) {
        return encodePos(block.getX(), block.getY(), block.getZ());
    }

    public static long encodePos(BlockState block) {
        return encodePos(block.getX(), block.getY(), block.getZ());
    }

    public static TLongSet encodePosSet(Collection<?> vectors) {
        TLongSet encoded = new TLongHashSet(vectors.size());
        for(Object o : vectors) {
            if(o instanceof BlockVector) {
                encoded.add(encodePos((BlockVector) o));
            }
        }
        return encoded;
    }

    /**
     * Return the encoded location neighboring the given location on the given side.
     * Equivalent to {@link Block#getRelative}.
     */
    public static long neighborPos(long encoded, BlockFace face) {
        return encodePos(
            unpack(encoded, 0) + face.getModX(),
            unpack(encoded, SHIFT) + face.getModY(),
            unpack(encoded, SHIFT + SHIFT) + face.getModZ()
        );
    }

    public static Block blockAt(World world, Vector pos) {
        return world.getBlockAt(pos.getBlockX(),
                                pos.getBlockY(),
                                pos.getBlockZ());
    }

    /**
     * Return the {@link Block} in the given {@link World}, at the given encoded location.
     * This method is more efficient than creating an intermediate {@link BlockVector},
     * and more convenient.
     */
    public static Block blockAt(World world, long encoded) {
        return world.getBlockAt(
            (int) unpack(encoded, 0),
            (int) unpack(encoded, SHIFT),
            (int) unpack(encoded, SHIFT + SHIFT)
        );
    }
}
