package tc.oc.pgm.regions;

import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.Random;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.util.BlockVector;
import org.bukkit.util.Vector;
import tc.oc.api.docs.SemanticVersion;
import tc.oc.commons.bukkit.util.BlockVectorSet;
import tc.oc.pgm.filters.Filter;
import tc.oc.pgm.filters.query.BlockQuery;
import tc.oc.pgm.utils.MaterialPattern;

import static tc.oc.pgm.map.ProtoVersions.REGION_FIX_VERSION;

/**
 * Region represented by a list of single blocks.  This will check if a point
 * is inside the block at all.
 */
public class FiniteBlockRegion extends Region.Impl {
    private final BlockVectorSet positions;
    private final Cuboid bounds;

    public FiniteBlockRegion(Collection<BlockVector> positions) {
        this.positions = BlockVectorSet.of(positions);

        // calculate AABB
        final Vector min = new Vector(Double.MAX_VALUE);
        final Vector max = new Vector(-Double.MAX_VALUE);

        for(BlockVector pos : this.positions) {
            min.setX(Math.min(min.getX(), pos.getBlockX()));
            min.setY(Math.min(min.getY(), pos.getBlockY()));
            min.setZ(Math.min(min.getZ(), pos.getBlockZ()));

            max.setX(Math.max(max.getX(), pos.getBlockX() + 1));
            max.setY(Math.max(max.getY(), pos.getBlockY() + 1));
            max.setZ(Math.max(max.getZ(), pos.getBlockZ() + 1));
        }

        this.bounds = Cuboid.between(min, max);
    }

    @Inspect
    public int size() {
        return positions.size();
    }

    @Override
    public boolean contains(Vector point) {
        return bounds.contains(point) && positions.contains(point.toBlockVector());
    }

    @Override
    public boolean canGetRandom() {
        return true;
    }

    @Override
    public boolean isBlockBounded() {
        return true;
    }

    @Override
    public Cuboid getBounds() {
        return this.bounds;
    }

    @Override
    public Vector getRandom(Random random) {
        final BlockVector randomBlock = positions.chooseRandom(random);
        double dx = random.nextDouble();
        double dy = random.nextDouble();
        double dz = random.nextDouble();
        return randomBlock.add(dx, dy, dz);
    }

    @Override
    public Iterator<BlockVector> getBlockVectorIterator() {
        return positions.mutableIterator();
    }

    @Override
    public Set<BlockVector> getBlockVectors() {
        return positions;
    }

    @Override
    public Stream<BlockVector> blockPositions() {
        return positions.stream();
    }

    public static class Factory {
        private final SemanticVersion mapProto;

        public Factory(SemanticVersion mapProto) {
            this.mapProto = mapProto;
        }

        public FiniteBlockRegion fromWorld(Region region, World world, MaterialPattern...materials) {
            return fromWorld(region, world, Arrays.asList(materials));
        }

        public FiniteBlockRegion fromWorld(Region region, World world, Collection<MaterialPattern> materials) {
            return fromWorld(region, world, (Predicate<Block>) block -> materials.stream().anyMatch(m -> m.matches(block.getState().getMaterialData())));
        }

        public FiniteBlockRegion fromWorld(Region region, World world, Filter filter) {
            return fromWorld(region, world, (Predicate<Block>) block -> filter.query(new BlockQuery(block)).isAllowed());
        }

        public FiniteBlockRegion fromWorld(Region region, World world, Predicate<Block> filter) {
            if(region instanceof CuboidRegion && mapProto.isOlderThan(REGION_FIX_VERSION)) {
                // Due to an old bug, legacy maps have cuboids that are one block too big
                region = new CuboidRegion(region.getBounds().minimum(), region.getBounds().maximum().plus(1, 1, 1));
            }

            return new FiniteBlockRegion(region.blockPositions()
                                               .filter(pos -> filter.test(world.getBlockAt(pos.getBlockX(), pos.getBlockY(), pos.getBlockZ())))
                                               .collect(Collectors.toCollection(BlockVectorSet::new)));
        }
    }
}
