package tc.oc.pgm.regions;

import java.util.Iterator;
import java.util.Optional;
import java.util.Random;
import java.util.function.Function;
import java.util.stream.IntStream;
import java.util.stream.Stream;

import com.google.common.collect.Iterators;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.BlockState;
import org.bukkit.entity.Entity;
import org.bukkit.util.BlockVector;
import org.bukkit.util.Vector;
import tc.oc.commons.bukkit.util.BlockUtils;
import tc.oc.commons.bukkit.util.ChunkPosition;
import tc.oc.commons.core.util.Streams;
import tc.oc.pgm.features.FeatureDefinition;
import tc.oc.pgm.features.FeatureInfo;
import tc.oc.pgm.filters.matcher.TypedFilter;
import tc.oc.pgm.filters.query.ILocationQuery;

/**
 * Represents an arbitrary region in a Bukkit world.
 */
@FeatureInfo(name = "region")
public interface Region extends TypedFilter<ILocationQuery> {

    /**
     * @return The smallest cuboid that entirely contains this region
     */
    Cuboid getBounds();

    /**
     * True only if this region contains no points at all times.
     *
     * Note that a zero-volume region is not necessarily empty.
     */
    default boolean isEmpty() {
        return false;
    }

    /**
     * True only if this region contains all points at all times.
     */
    default boolean isEverywhere() {
        return false;
    }

    @Override
    default boolean matches(ILocationQuery query) {
        return contains(query.blockCenter());
    }

    @Override
    default boolean isDynamic() {
        return true;
    }

    /**
     * Test if the region contains the given point
     */
    boolean contains(Vector point);

    default boolean contains(Vec3 point) {
        return contains(ImVector.copyOf(point.isFine() ? point.fineCopy()
                                                       : point.blockCenter()));
    }

    default boolean contains(BlockVector blockPos) {
        return contains(blockPos.blockCenter());
    }

    default boolean contains(Block block) {
        return contains(ImVector.centerOf(block));
    }

    default boolean contains(BlockState block) {
        return contains(ImVector.centerOf(block));
    }

    default boolean contains(Location point) {
        return contains(point.position());
    }

    default boolean contains(Entity entity) {
        return contains(entity.getLocation());
    }

    /**
     * Test if moving from the first point to the second crosses into the region
     */
    default boolean enters(BlockVector from, BlockVector to) {
        return !contains(from) && contains(to);
    }

    default boolean enters(Optional<BlockVector> from, BlockVector to) {
        return from.isPresent() ? enters(from.get(), to)
                                : contains(to);
    }

    /**
     * Test if moving from the first point to the second crosses out of the region
     */
    default boolean exits(BlockVector from, BlockVector to) {
        return contains(from) && !contains(to);
    }

    default boolean exits(Optional<BlockVector> from, BlockVector to) {
        return from.isPresent() ? exits(from.get(), to)
                                : !contains(to);
    }

    default boolean canGetRandom() {
        return false;
    }

    default Vector getRandom(Random random) {
        throw new UnsupportedOperationException("Cannot generate a random point in " + this.getClass().getSimpleName());
    }

    default boolean isBlockBounded() {
        return false;
    }

    default org.bukkit.region.BlockRegion blockRegion() {
        return getBounds().blockRegion().filter(this::contains);
    }

    @Deprecated
    default Iterator<BlockVector> getBlockVectorIterator() {
        return Iterators.transform(
            blockRegion().mutableIterator(),
            BlockVector::new
        );
    }

    @Deprecated
    default Iterable<BlockVector> getBlockVectors() {
        return this::getBlockVectorIterator;
    }

    @Deprecated
    default Stream<BlockVector> blockPositions() {
        return Streams.of(getBlockVectorIterator());
    }

    default Iterable<Block> getBlocks(World world) {
        return () -> Iterators.transform(
            blockRegion().mutableIterator(),
            world::getBlockAt
        );
    }

    default Stream<Block> blocks(World world) {
        return blockPositions().map(pos -> BlockUtils.blockAt(world, pos));
    }

    default long blockVolume() {
        return blockPositions().count();
    }

    default Stream<ChunkPosition> chunkPositions() {
        final Cuboid bounds = getBounds();
        if(!bounds.isBlockFinite()) {
            throw new UnsupportedOperationException("Cannot enumerate chunks in unbounded region type " + getClass().getSimpleName());
        }

        final ChunkPosition
            min = ChunkPosition.ofBlock(bounds.minimumBlockInside()),
            max = ChunkPosition.ofBlock(bounds.maximumBlockInside());
        return IntStream.rangeClosed(min.x(), max.x())
                        .mapToObj(x -> IntStream.rangeClosed(min.z(), max.z())
                                                .mapToObj(z -> new ChunkPosition(x, z)))
                        .flatMap(Function.identity());
    }

    default Stream<BlockState> tileEntities(World world) {
        return chunkPositions().flatMap(cp -> Stream.of(cp.getChunk(world).getTileEntities()))
                               .filter(this::contains);
    }

    default Stream<Entity> entities(World world) {
        return chunkPositions().flatMap(cp -> Stream.of(cp.getChunk(world).getEntities()))
                               .filter(this::contains);
    }

    abstract class Impl extends FeatureDefinition.Impl implements Region {}
}
