package com.fastasyncworldedit.core.extent;

import com.fastasyncworldedit.core.extent.filter.block.ExtentFilterBlock;
import com.fastasyncworldedit.core.extent.processor.ProcessorScope;
import com.fastasyncworldedit.core.internal.exception.FaweException;
import com.fastasyncworldedit.core.limit.FaweLimit;
import com.fastasyncworldedit.core.queue.Filter;
import com.fastasyncworldedit.core.queue.IBatchProcessor;
import com.fastasyncworldedit.core.queue.IChunk;
import com.fastasyncworldedit.core.queue.IChunkGet;
import com.fastasyncworldedit.core.queue.IChunkSet;
import com.fastasyncworldedit.core.util.ExtentTraverser;
import com.sk89q.jnbt.CompoundTag;
import com.sk89q.worldedit.MaxChangedBlocksException;
import com.sk89q.worldedit.WorldEditException;
import com.sk89q.worldedit.entity.BaseEntity;
import com.sk89q.worldedit.entity.Entity;
import com.sk89q.worldedit.extent.AbstractDelegateExtent;
import com.sk89q.worldedit.extent.Extent;
import com.sk89q.worldedit.function.mask.Mask;
import com.sk89q.worldedit.function.pattern.Pattern;
import com.sk89q.worldedit.math.BlockVector3;
import com.sk89q.worldedit.regions.Region;
import com.sk89q.worldedit.util.Countable;
import com.sk89q.worldedit.util.Location;
import com.sk89q.worldedit.util.formatting.text.Component;
import com.sk89q.worldedit.world.biome.BiomeType;
import com.sk89q.worldedit.world.biome.BiomeTypes;
import com.sk89q.worldedit.world.block.BaseBlock;
import com.sk89q.worldedit.world.block.BlockState;
import com.sk89q.worldedit.world.block.BlockStateHolder;
import com.sk89q.worldedit.world.block.BlockType;
import com.sk89q.worldedit.world.block.BlockTypes;

import javax.annotation.Nullable;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.function.Consumer;

public class LimitExtent extends AbstractDelegateExtent implements IBatchProcessor {

    private final FaweLimit limit;
    private final boolean[] faweExceptionReasonsUsed = new boolean[FaweException.Type.values().length];
    private final Consumer<Component> onErrorMessage;
    private final int chunk_size;
    private boolean processing;

    /**
     * Create a new instance.
     *
     * @param extent the extent
     * @param limit  the limit
     * @deprecated Use {@link LimitExtent#LimitExtent(Extent, FaweLimit, Consumer, boolean)}
     */
    @Deprecated(forRemoval = true, since = "2.12.0")
    public LimitExtent(Extent extent, FaweLimit limit) {
        this(extent, limit, c -> {
        });
    }

    /**
     * Create a new instance.
     *
     * @param extent         the extent
     * @param limit          the limit
     * @param onErrorMessage consumer to handle a component generated by exceptions
     * @deprecated Use {@link LimitExtent#LimitExtent(Extent, FaweLimit, Consumer, boolean)}
     */
    @Deprecated(forRemoval = true, since = "2.12.0")
    public LimitExtent(Extent extent, FaweLimit limit, Consumer<Component> onErrorMessage) {
        this(extent, limit, onErrorMessage, false);
    }

    /**
     * Create a new instance.
     *
     * @param extent                   the extent
     * @param limit                    the limit
     * @param onErrorMessage           consumer to handle a component generated by exceptions
     * @param processing               if this limit extent is expected to be processing
     * @since 2.12.0
     */
    public LimitExtent(
            Extent extent,
            FaweLimit limit,
            Consumer<Component> onErrorMessage,
            boolean processing
    ) {
        super(extent);
        this.limit = limit;
        this.onErrorMessage = onErrorMessage;
        this.chunk_size = 16 * 16 * (extent.getMaxY() - extent.getMinY());
        this.processing = processing;
    }

    private void handleException(FaweException e) {
        if (e.ignorable() || !limit.MAX_FAILS()) {
            throw e;
        }
        if (!faweExceptionReasonsUsed[e.getType().ordinal()]) {
            faweExceptionReasonsUsed[e.getType().ordinal()] = true;
            onErrorMessage.accept(e.getComponent());
        }
    }

    @Override
    public List<? extends Entity> getEntities(Region region) {
        limit.THROW_MAX_CHECKS(region.getVolume());
        try {
            return extent.getEntities(region);
        } catch (FaweException e) {
            handleException(e);
            return Collections.emptyList();
        }
    }

    @Override
    public List<? extends Entity> getEntities() {
        limit.THROW_MAX_CHECKS();
        try {
            return extent.getEntities();
        } catch (FaweException e) {
            handleException(e);
            return Collections.emptyList();
        }
    }

    @Override
    @Nullable
    public Entity createEntity(Location location, BaseEntity entity) {
        limit.THROW_MAX_CHANGES();
        limit.THROW_MAX_ENTITIES();
        try {
            return extent.createEntity(location, entity);
        } catch (FaweException e) {
            handleException(e);
            return null;
        }
    }

    @Override
    @Nullable
    public Entity createEntity(Location location, BaseEntity entity, UUID uuid) {
        limit.THROW_MAX_CHANGES();
        limit.THROW_MAX_ENTITIES();
        try {
            return extent.createEntity(location, entity, uuid);
        } catch (FaweException e) {
            handleException(e);
            return null;
        }
    }

    @Override
    public void removeEntity(int x, int y, int z, UUID uuid) {
        limit.THROW_MAX_CHANGES();
        limit.THROW_MAX_ENTITIES();
        try {
            extent.removeEntity(x, y, z, uuid);
        } catch (FaweException e) {
            handleException(e);
        }
    }

    @Override
    public boolean regenerateChunk(int x, int z, @Nullable BiomeType type, @Nullable Long seed) {
        limit.THROW_MAX_CHANGES(chunk_size);
        try {
            return extent.regenerateChunk(x, z, type, seed);
        } catch (FaweException e) {
            handleException(e);
            return false;
        }
    }

    @Override
    public int getHighestTerrainBlock(int x, int z, int minY, int maxY) {
        limit.THROW_MAX_CHECKS(maxY - minY + 1);
        try {
            return extent.getHighestTerrainBlock(x, z, minY, maxY);
        } catch (FaweException e) {
            handleException(e);
            return minY;
        }
    }

    @Override
    public int getHighestTerrainBlock(int x, int z, int minY, int maxY, Mask filter) {
        limit.THROW_MAX_CHECKS(maxY - minY + 1);
        try {
            return extent.getHighestTerrainBlock(x, z, minY, maxY, filter);
        } catch (FaweException e) {
            handleException(e);
            return minY;
        }
    }

    @Override
    public int getNearestSurfaceLayer(int x, int z, int y, int minY, int maxY) {
        limit.THROW_MAX_CHECKS(maxY - minY + 1);
        try {
            return extent.getNearestSurfaceLayer(x, z, y, minY, maxY);
        } catch (FaweException e) {
            handleException(e);
            return minY;
        }
    }

    @Override
    public int getNearestSurfaceTerrainBlock(int x, int z, int y, int minY, int maxY, boolean ignoreAir) {
        limit.THROW_MAX_CHECKS(maxY - minY + 1);
        try {
            return extent.getNearestSurfaceTerrainBlock(x, z, y, minY, maxY, ignoreAir);
        } catch (FaweException e) {
            handleException(e);
            return minY;
        }
    }

    @Override
    public int getNearestSurfaceTerrainBlock(int x, int z, int y, int minY, int maxY) {
        limit.THROW_MAX_CHECKS(maxY - minY + 1);
        try {
            return extent.getNearestSurfaceTerrainBlock(x, z, y, minY, maxY);
        } catch (FaweException e) {
            handleException(e);
            return minY;
        }
    }

    @Override
    public int getNearestSurfaceTerrainBlock(int x, int z, int y, int minY, int maxY, int failedMin, int failedMax) {
        limit.THROW_MAX_CHECKS(maxY - minY + 1);
        try {
            return extent.getNearestSurfaceTerrainBlock(x, z, y, minY, maxY, failedMin, failedMax);
        } catch (FaweException e) {
            handleException(e);
            return minY;
        }
    }

    @Override
    public int getNearestSurfaceTerrainBlock(int x, int z, int y, int minY, int maxY, int failedMin, int failedMax, Mask mask) {
        limit.THROW_MAX_CHECKS(maxY - minY + 1);
        try {
            return extent.getNearestSurfaceTerrainBlock(x, z, y, minY, maxY, failedMin, failedMax, mask);
        } catch (FaweException e) {
            handleException(e);
            return minY;
        }
    }

    @Override
    public int getNearestSurfaceTerrainBlock(
            int x,
            int z,
            int y,
            int minY,
            int maxY,
            int failedMin,
            int failedMax,
            boolean ignoreAir
    ) {
        limit.THROW_MAX_CHECKS(maxY - minY + 1);
        try {
            return extent.getNearestSurfaceTerrainBlock(x, z, y, minY, maxY, failedMin, failedMax, ignoreAir);
        } catch (FaweException e) {
            handleException(e);
            return minY;
        }
    }

    @Override
    public List<Countable<BlockType>> getBlockDistribution(Region region) {
        limit.THROW_MAX_CHECKS(region.getVolume());
        return extent.getBlockDistribution(region);
    }

    @Override
    public List<Countable<BlockState>> getBlockDistributionWithData(Region region) {
        limit.THROW_MAX_CHECKS(region.getVolume());
        return extent.getBlockDistributionWithData(region);
    }

    @Override
    public int countBlocks(Region region, Set<BaseBlock> searchBlocks) {
        limit.THROW_MAX_CHECKS(region.getVolume());
        return extent.countBlocks(region, searchBlocks);
    }

    @Override
    public int countBlocks(Region region, Mask searchMask) {
        limit.THROW_MAX_CHECKS(region.getVolume());
        return extent.countBlocks(region, searchMask);
    }

    @Override
    public <B extends BlockStateHolder<B>> int setBlocks(Region region, B block) throws MaxChangedBlocksException {
        limit.THROW_MAX_CHANGES(region.getVolume());
        return extent.setBlocks(region, block);
    }

    @Override
    public int setBlocks(Region region, Pattern pattern) throws MaxChangedBlocksException {
        limit.THROW_MAX_CHANGES(region.getVolume());
        return extent.setBlocks(region, pattern);
    }

    @Override
    public <B extends BlockStateHolder<B>> int replaceBlocks(Region region, Set<BaseBlock> filter, B replacement) throws
            MaxChangedBlocksException {
        limit.THROW_MAX_CHECKS(region.getVolume());
        limit.THROW_MAX_CHANGES(region.getVolume());
        return extent.replaceBlocks(region, filter, replacement);
    }

    @Override
    public int replaceBlocks(Region region, Set<BaseBlock> filter, Pattern pattern) throws MaxChangedBlocksException {
        limit.THROW_MAX_CHECKS(region.getVolume());
        limit.THROW_MAX_CHANGES(region.getVolume());
        return extent.replaceBlocks(region, filter, pattern);
    }

    @Override
    public int replaceBlocks(Region region, Mask mask, Pattern pattern) throws MaxChangedBlocksException {
        limit.THROW_MAX_CHECKS(region.getVolume());
        limit.THROW_MAX_CHANGES(region.getVolume());
        return extent.replaceBlocks(region, mask, pattern);
    }

    @Override
    public int setBlocks(Set<BlockVector3> vset, Pattern pattern) {
        limit.THROW_MAX_CHANGES(vset.size());
        return extent.setBlocks(vset, pattern);
    }

    @Override
    public <T extends Filter> T apply(Region region, T filter, boolean full) {
        limit.THROW_MAX_CHECKS(region.getVolume());
        limit.THROW_MAX_CHANGES(region.getVolume());
        return extent.apply(region, filter, full);
    }

    @Override
    public <T extends Filter> T apply(Iterable<BlockVector3> positions, T filter) {
        int size;
        if (positions instanceof Collection) {
            size = ((Collection<BlockVector3>) positions).size();
        } else if (positions instanceof Region) {
            BlockVector3 dim = ((Region) positions).getDimensions();
            size = dim.x() * dim.y() * dim.z();
        } else if (positions instanceof Extent) {
            BlockVector3 min = ((Extent) positions).getMinimumPoint();
            BlockVector3 max = ((Extent) positions).getMinimumPoint();
            BlockVector3 dim = max.subtract(min).add(BlockVector3.ONE);
            size = dim.x() * dim.y() * dim.z();
        } else {
            ExtentFilterBlock block = new ExtentFilterBlock(this);
            for (BlockVector3 pos : positions) {
                limit.THROW_MAX_CHECKS();
                try {
                    filter.applyBlock(block.init(pos));
                } catch (FaweException e) {
                    handleException(e);
                }
            }
            return filter;
        }
        limit.THROW_MAX_CHECKS(size);
        limit.THROW_MAX_CHANGES(size);
        return extent.apply(positions, filter);
    }

    @Override
    public BlockState getBlock(BlockVector3 position) {
        limit.THROW_MAX_CHECKS();
        try {
            return extent.getBlock(position);
        } catch (FaweException e) {
            handleException(e);
            return BlockTypes.AIR.getDefaultState();
        }
    }

    @Override
    public BlockState getBlock(int x, int y, int z) {
        limit.THROW_MAX_CHECKS();
        try {
            return extent.getBlock(x, y, z);
        } catch (FaweException e) {
            handleException(e);
            return BlockTypes.AIR.getDefaultState();
        }
    }

    @Override
    public BaseBlock getFullBlock(BlockVector3 position) {
        limit.THROW_MAX_CHECKS();
        try {
            return extent.getFullBlock(position);
        } catch (FaweException e) {
            handleException(e);
            return BlockTypes.AIR.getDefaultState().toBaseBlock();
        }
    }

    @Override
    public BaseBlock getFullBlock(int x, int y, int z) {
        limit.THROW_MAX_CHECKS();
        try {
            return extent.getFullBlock(x, y, z);
        } catch (FaweException e) {
            handleException(e);
            return BlockTypes.AIR.getDefaultState().toBaseBlock();
        }
    }

    @Override
    public BiomeType getBiome(BlockVector3 position) {
        limit.THROW_MAX_CHECKS();
        try {
            return extent.getBiome(position);
        } catch (FaweException e) {
            handleException(e);
            return BiomeTypes.FOREST;
        }
    }

    @Override
    public BiomeType getBiomeType(int x, int y, int z) {
        limit.THROW_MAX_CHECKS();
        try {
            return extent.getBiomeType(x, y, z);
        } catch (FaweException e) {
            handleException(e);
            return BiomeTypes.FOREST;
        }
    }

    @Override
    @Deprecated
    public <T extends BlockStateHolder<T>> boolean setBlock(BlockVector3 position, T block) throws WorldEditException {
        limit.THROW_MAX_CHANGES();
        if (block.getNbtData() != null || block.getBlockType().getMaterial().isTile()) {
            limit.THROW_MAX_BLOCKSTATES();
        }
        try {
            return extent.setBlock(position, block);
        } catch (FaweException e) {
            handleException(e);
            return false;
        }
    }

    @Override
    public <T extends BlockStateHolder<T>> boolean setBlock(int x, int y, int z, T block) throws WorldEditException {
        limit.THROW_MAX_CHANGES();
        if (block.getNbtData() != null || block.getBlockType().getMaterial().isTile()) {
            limit.THROW_MAX_BLOCKSTATES();
        }
        try {
            return extent.setBlock(x, y, z, block);
        } catch (FaweException e) {
            handleException(e);
            return false;
        }
    }

    @Override
    public boolean setTile(int x, int y, int z, CompoundTag tile) throws WorldEditException {
        limit.THROW_MAX_CHANGES();
        limit.THROW_MAX_BLOCKSTATES();
        try {
            return extent.setTile(x, y, z, tile);
        } catch (FaweException e) {
            handleException(e);
            return false;
        }
    }

    @Override
    public boolean setBiome(BlockVector3 position, BiomeType biome) {
        limit.THROW_MAX_CHANGES();
        try {
            return extent.setBiome(position, biome);
        } catch (FaweException e) {
            handleException(e);
            return false;
        }
    }

    @Override
    public boolean setBiome(int x, int y, int z, BiomeType biome) {
        limit.THROW_MAX_CHANGES();
        try {
            return extent.setBiome(x, y, z, biome);
        } catch (FaweException e) {
            handleException(e);
            return false;
        }
    }

    public void setProcessing(boolean processing) {
        this.processing = processing;
    }

    @Override
    public IChunkSet processSet(IChunk chunk, IChunkGet get, IChunkSet set) {
        if (!processing) {
            return set;
        }
        int tiles = set.tiles().size();
        int ents = set.entities().size() + set.getEntityRemoves().size();
        limit.THROW_MAX_CHANGES(tiles + ents);
        limit.THROW_MAX_BLOCKSTATES(tiles);
        limit.THROW_MAX_ENTITIES(ents);
        return set;
    }

    @Override
    public Extent construct(final Extent child) {
        if (extent != child) {
            new ExtentTraverser<Extent>(this).setNext(child);
        }
        return this;
    }

    @Override
    public ProcessorScope getScope() {
        return ProcessorScope.READING_SET_BLOCKS;
    }

}
