/*
 * This file is part of GriefPrevention, licensed under the MIT License (MIT).
 *
 * Copyright (c) Ryan Hamshire
 * Copyright (c) bloodmc
 * Copyright (c) contributors
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package me.ryanhamshire.griefprevention.task;

import me.ryanhamshire.griefprevention.GriefPreventionPlugin;
import me.ryanhamshire.griefprevention.util.BlockUtils;
import net.minecraft.block.state.IBlockState;
import org.spongepowered.api.Sponge;
import org.spongepowered.api.block.BlockSnapshot;
import org.spongepowered.api.block.BlockType;
import org.spongepowered.api.block.BlockTypes;
import org.spongepowered.api.block.trait.EnumTraits;
import org.spongepowered.api.entity.living.player.Player;
import org.spongepowered.api.world.DimensionType;
import org.spongepowered.api.world.DimensionTypes;
import org.spongepowered.api.world.Location;
import org.spongepowered.api.world.World;
import org.spongepowered.api.world.biome.BiomeType;
import org.spongepowered.api.world.biome.BiomeTypes;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

//non-main-thread task which processes world data to repair the unnatural
//after processing is complete, creates a main thread task to make the necessary changes to the world
public class RestoreNatureProcessingTask implements Runnable {

    // world information captured from the main thread
    // will be updated and sent back to main thread to be applied to the world
    private BlockSnapshot[][][] snapshots;

    // other information collected from the main thread.
    // not to be updated, only to be passed back to main thread to provide some
    // context about the operation
    private int miny;
    private DimensionType environment;
    private Location<World> lesserBoundaryCorner;
    private Location<World> greaterBoundaryCorner;
    // absolutely must not be accessed. not thread safe.
    private Player player;
    private BiomeType biome;
    private boolean creativeMode;
    private int seaLevel;
    private boolean aggressiveMode;

    // two lists of materials
    // natural blocks which don't naturally hang in their air
    private ArrayList<BlockType> notAllowedToHang;

    // a "complete" list of player-placed blocks. MUST BE MAINTAINED as patches introduce more
    private ArrayList<BlockType> playerBlocks;

    public RestoreNatureProcessingTask(BlockSnapshot[][][] snapshots, int miny, DimensionType environment, BiomeType biome,
            Location<World> lesserBoundaryCorner, Location<World> greaterBoundaryCorner, int seaLevel, boolean aggressiveMode, boolean creativeMode,
            Player player) {
        this.snapshots = snapshots;
        this.miny = miny;
        if (this.miny < 0) {
            this.miny = 0;
        }
        this.environment = environment;
        this.lesserBoundaryCorner = lesserBoundaryCorner;
        this.greaterBoundaryCorner = greaterBoundaryCorner;
        this.biome = biome;
        this.seaLevel = seaLevel;
        this.aggressiveMode = aggressiveMode;
        this.player = player;
        this.creativeMode = creativeMode;

        this.notAllowedToHang = new ArrayList<BlockType>();
        this.notAllowedToHang.add(BlockTypes.DIRT);
        this.notAllowedToHang.add(BlockTypes.TALLGRASS);
        this.notAllowedToHang.add(BlockTypes.SNOW);
        this.notAllowedToHang.add(BlockTypes.LOG);

        if (this.aggressiveMode) {
            this.notAllowedToHang.add(BlockTypes.GRASS);
            this.notAllowedToHang.add(BlockTypes.STONE);
        }

        this.playerBlocks = new ArrayList<BlockType>();
        this.playerBlocks.addAll(RestoreNatureProcessingTask.getPlayerBlocks(this.environment, this.biome));

        // in aggressive or creative world mode, also treat these blocks as user placed, to be removed
        // this is helpful in the few cases where griefers intentionally use natural blocks to grief,
        // like a single-block tower of iron ore or a giant penis constructed with melons
        if (this.aggressiveMode || this.creativeMode) {
            this.playerBlocks.add(BlockTypes.IRON_ORE);
            this.playerBlocks.add(BlockTypes.GOLD_ORE);
            this.playerBlocks.add(BlockTypes.DIAMOND_ORE);
            this.playerBlocks.add(BlockTypes.MELON_BLOCK);
            this.playerBlocks.add(BlockTypes.MELON_STEM);
            this.playerBlocks.add(BlockTypes.BEDROCK);
            this.playerBlocks.add(BlockTypes.COAL_ORE);
            this.playerBlocks.add(BlockTypes.PUMPKIN);
            this.playerBlocks.add(BlockTypes.PUMPKIN_STEM);
        }

        if (this.aggressiveMode) {
            this.playerBlocks.add(BlockTypes.LEAVES);
            this.playerBlocks.add(BlockTypes.LOG);
            this.playerBlocks.add(BlockTypes.LOG2);
            this.playerBlocks.add(BlockTypes.VINE);
        }
    }

    @Override
    public void run() {
        // order is important!

        // remove sandstone which appears to be unnatural
        this.removeSandstone();

        // remove any blocks which are definitely player placed
        this.removePlayerBlocks();

        // reduce large outcroppings of stone, sandstone
        this.reduceStone();

        // reduce logs, except in jungle biomes
        this.reduceLogs();

        // remove natural blocks which are unnaturally hanging in the air
        this.removeHanging();

        // remove natural blocks which are unnaturally stacked high
        this.removeWallsAndTowers();

        // fill unnatural thin trenches and single-block potholes
        this.fillHolesAndTrenches();

        // fill water depressions and fix unnatural surface ripples
        this.fixWater();

        // remove water/lava above sea level
        this.removeDumpedFluids();

        // cover surface stone and gravel with sand or grass, as the biome requires
        this.coverSurfaceStone();

        // remove any player-placed leaves
        this.removePlayerLeaves();

        // schedule main thread task to apply the result to the world
        RestoreNatureExecutionTask task =
                new RestoreNatureExecutionTask(this.snapshots, this.miny, this.lesserBoundaryCorner, this.greaterBoundaryCorner, this.player);
        Sponge.getGame().getScheduler().createTaskBuilder().execute(task).submit(GriefPreventionPlugin.instance);
    }

    private void removePlayerLeaves() {
        if (this.seaLevel < 1) {
            return;
        }

        for (int x = 1; x < snapshots.length - 1; x++) {
            for (int z = 1; z < snapshots[0][0].length - 1; z++) {
                for (int y = this.seaLevel - 1; y < snapshots[0].length; y++) {
                    // note: see minecraft wiki data values for leaves
                    BlockSnapshot block = snapshots[x][y][z];
                    if (block.getState().getType() == BlockTypes.LEAVES && (BlockUtils.getBlockStateMeta(block.getState()) & 0x4) != 0) {
                        snapshots[x][y][z] = block.withState(BlockTypes.AIR.getDefaultState());
                    }
                }
            }
        }
    }

    // converts sandstone adjacent to sand to sand, and any other sandstone to
    // air
    private void removeSandstone() {
        for (int x = 1; x < snapshots.length - 1; x++) {
            for (int z = 1; z < snapshots[0][0].length - 1; z++) {
                for (int y = snapshots[0].length - 2; y > miny; y--) {
                    if (snapshots[x][y][z].getState().getType() != BlockTypes.SANDSTONE) {
                        continue;
                    }

                    BlockSnapshot leftBlock = this.snapshots[x + 1][y][z];
                    BlockSnapshot rightBlock = this.snapshots[x - 1][y][z];
                    BlockSnapshot upBlock = this.snapshots[x][y][z + 1];
                    BlockSnapshot downBlock = this.snapshots[x][y][z - 1];
                    BlockSnapshot underBlock = this.snapshots[x][y - 1][z];
                    BlockSnapshot aboveBlock = this.snapshots[x][y + 1][z];

                    // skip blocks which may cause a cave-in
                    if (aboveBlock.getState().getType() == BlockTypes.SAND && underBlock.getState().getType() == BlockTypes.AIR) {
                        continue;
                    }

                    // count adjacent non-air/non-leaf blocks
                    if (leftBlock.getState().getType() == BlockTypes.SAND ||
                            rightBlock.getState().getType() == BlockTypes.SAND ||
                            upBlock.getState().getType() == BlockTypes.SAND ||
                            downBlock.getState().getType() == BlockTypes.SAND ||
                            aboveBlock.getState().getType() == BlockTypes.SAND ||
                            underBlock.getState().getType() == BlockTypes.SAND) {
                        snapshots[x][y][z] = snapshots[x][y][z].withState(BlockTypes.SAND.getDefaultState());
                    } else {
                        snapshots[x][y][z] = snapshots[x][y][z].withState(BlockTypes.AIR.getDefaultState());
                    }
                }
            }
        }
    }

    private void reduceStone() {
        if (this.seaLevel < 1) {
            return;
        }

        for (int x = 1; x < snapshots.length - 1; x++) {
            for (int z = 1; z < snapshots[0][0].length - 1; z++) {
                int thisy = this.highestY(x, z, true);

                while (thisy > this.seaLevel - 1 && (this.snapshots[x][thisy][z].getState().getType() == BlockTypes.STONE
                        || this.snapshots[x][thisy][z].getState().getType() == BlockTypes.SANDSTONE)) {
                    BlockSnapshot leftBlock = this.snapshots[x + 1][thisy][z];
                    BlockSnapshot rightBlock = this.snapshots[x - 1][thisy][z];
                    BlockSnapshot upBlock = this.snapshots[x][thisy][z + 1];
                    BlockSnapshot downBlock = this.snapshots[x][thisy][z - 1];

                    // count adjacent non-air/non-leaf blocks
                    byte adjacentBlockCount = 0;
                    if (leftBlock.getState().getType() != BlockTypes.AIR && leftBlock.getState().getType() != BlockTypes.LEAVES
                            && leftBlock.getState().getType() != BlockTypes.VINE) {
                        adjacentBlockCount++;
                    }
                    if (rightBlock.getState().getType() != BlockTypes.AIR && rightBlock.getState().getType() != BlockTypes.LEAVES
                            && rightBlock.getState().getType() != BlockTypes.VINE) {
                        adjacentBlockCount++;
                    }
                    if (downBlock.getState().getType() != BlockTypes.AIR && downBlock.getState().getType() != BlockTypes.LEAVES
                            && downBlock.getState().getType() != BlockTypes.VINE) {
                        adjacentBlockCount++;
                    }
                    if (upBlock.getState().getType() != BlockTypes.AIR && upBlock.getState().getType() != BlockTypes.LEAVES
                            && upBlock.getState().getType() != BlockTypes.VINE) {
                        adjacentBlockCount++;
                    }

                    if (adjacentBlockCount < 3) {
                        this.snapshots[x][thisy][z] = this.snapshots[x][thisy][z].withState(BlockTypes.AIR.getDefaultState());
                    }

                    thisy--;
                }
            }
        }
    }

    private void reduceLogs() {
        if (this.seaLevel < 1) {
            return;
        }

        boolean jungleBiome = this.biome == BiomeTypes.JUNGLE || this.biome == BiomeTypes.JUNGLE_HILLS;

        // scan all blocks above sea level
        for (int x = 1; x < snapshots.length - 1; x++) {
            for (int z = 1; z < snapshots[0][0].length - 1; z++) {
                for (int y = this.seaLevel - 1; y < snapshots[0].length; y++) {
                    BlockSnapshot block = snapshots[x][y][z];

                    // skip non-logs
                    if (block.getState().getType() != BlockTypes.LOG) {
                        continue;
                    }
                    if (block.getState().getType() != BlockTypes.LOG2) {
                        continue;
                    }

                    // if in jungle biome, skip jungle logs
                    Optional<? extends Enum<?>> enumProperty = block.getState().getTraitValue(EnumTraits.LOG_VARIANT);
                    if (jungleBiome && enumProperty.isPresent() && enumProperty.get().name().equalsIgnoreCase("jungle")) {
                        continue;
                    }

                    // examine adjacent blocks for logs
                    BlockSnapshot leftBlock = this.snapshots[x + 1][y][z];
                    BlockSnapshot rightBlock = this.snapshots[x - 1][y][z];
                    BlockSnapshot upBlock = this.snapshots[x][y][z + 1];
                    BlockSnapshot downBlock = this.snapshots[x][y][z - 1];

                    // if any, remove the log
                    if (leftBlock.getState().getType() == BlockTypes.LOG || rightBlock.getState().getType() == BlockTypes.LOG
                            || upBlock.getState().getType() == BlockTypes.LOG || downBlock.getState().getType() == BlockTypes.LOG) {
                        this.snapshots[x][y][z] = this.snapshots[x][y][z].withState(BlockTypes.AIR.getDefaultState());
                    }
                }
            }
        }
    }

    private void removePlayerBlocks() {
        int miny = this.miny;
        if (miny < 1) {
            miny = 1;
        }

        // remove all player blocks
        for (int x = 1; x < snapshots.length - 1; x++) {
            for (int z = 1; z < snapshots[0][0].length - 1; z++) {
                for (int y = miny; y < snapshots[0].length - 1; y++) {
                    BlockSnapshot block = snapshots[x][y][z];
                    if (this.playerBlocks.contains(block.getState().getType())) {
                        snapshots[x][y][z] = block.withState(BlockTypes.AIR.getDefaultState());
                    }
                }
            }
        }
    }

    private void removeHanging() {
        int miny = this.miny;
        if (miny < 1) {
            miny = 1;
        }

        for (int x = 1; x < snapshots.length - 1; x++) {
            for (int z = 1; z < snapshots[0][0].length - 1; z++) {
                for (int y = miny; y < snapshots[0].length - 1; y++) {
                    BlockSnapshot block = snapshots[x][y][z];
                    BlockSnapshot underBlock = snapshots[x][y - 1][z];

                    if (underBlock.getState().getType() == BlockTypes.AIR || underBlock.getState().getType() == BlockTypes.WATER
                            || underBlock.getState().getType() == BlockTypes.LAVA || underBlock.getState().getType() == BlockTypes.LEAVES) {
                        if (this.notAllowedToHang.contains(block.getState().getType())) {
                            snapshots[x][y][z] = block.withState(BlockTypes.AIR.getDefaultState());
                        }
                    }
                }
            }
        }
    }

    private void removeWallsAndTowers() {
        List<BlockType> excludedBlocksArray = new ArrayList<BlockType>();
        excludedBlocksArray.add(BlockTypes.CACTUS);
        excludedBlocksArray.add(BlockTypes.TALLGRASS);
        excludedBlocksArray.add(BlockTypes.RED_MUSHROOM);
        excludedBlocksArray.add(BlockTypes.BROWN_MUSHROOM);
        excludedBlocksArray.add(BlockTypes.DEADBUSH);
        excludedBlocksArray.add(BlockTypes.SAPLING);
        excludedBlocksArray.add(BlockTypes.YELLOW_FLOWER);
        excludedBlocksArray.add(BlockTypes.RED_FLOWER);
        excludedBlocksArray.add(BlockTypes.REEDS);
        excludedBlocksArray.add(BlockTypes.VINE);
        excludedBlocksArray.add(BlockTypes.PUMPKIN);
        excludedBlocksArray.add(BlockTypes.WATERLILY);
        excludedBlocksArray.add(BlockTypes.LEAVES);

        boolean changed;
        do {
            changed = false;
            for (int x = 1; x < snapshots.length - 1; x++) {
                for (int z = 1; z < snapshots[0][0].length - 1; z++) {
                    int thisy = this.highestY(x, z, false);
                    if (excludedBlocksArray.contains(this.snapshots[x][thisy][z].getState().getType())) {
                        continue;
                    }

                    int righty = this.highestY(x + 1, z, false);
                    int lefty = this.highestY(x - 1, z, false);
                    while (lefty < thisy && righty < thisy) {
                        this.snapshots[x][thisy--][z] = this.snapshots[x][thisy--][z].withState(BlockTypes.AIR.getDefaultState());
                        changed = true;
                    }

                    int upy = this.highestY(x, z + 1, false);
                    int downy = this.highestY(x, z - 1, false);
                    while (upy < thisy && downy < thisy) {
                        this.snapshots[x][thisy--][z] = this.snapshots[x][thisy--][z].withState(BlockTypes.AIR.getDefaultState());
                        changed = true;
                    }
                }
            }
        } while (changed);
    }

    private void coverSurfaceStone() {
        for (int x = 1; x < snapshots.length - 1; x++) {
            for (int z = 1; z < snapshots[0][0].length - 1; z++) {
                int y = this.highestY(x, z, true);
                BlockSnapshot block = snapshots[x][y][z];

                if (block.getState().getType() == BlockTypes.STONE || block.getState().getType() == BlockTypes.GRAVEL
                        || block.getState().getType() == BlockTypes.FARMLAND
                        || block.getState().getType() == BlockTypes.DIRT || block.getState().getType() == BlockTypes.SANDSTONE) {
                    if (this.biome == BiomeTypes.DESERT || this.biome == BiomeTypes.DESERT_HILLS || this.biome == BiomeTypes.BEACH) {
                        this.snapshots[x][y][z] = this.snapshots[x][y][z].withState(BlockTypes.SAND.getDefaultState());
                    } else {
                        this.snapshots[x][y][z] = this.snapshots[x][y][z].withState(BlockTypes.GRASS.getDefaultState());
                    }
                }
            }
        }
    }

    private void fillHolesAndTrenches() {
        ArrayList<BlockType> fillableBlocks = new ArrayList<BlockType>();
        fillableBlocks.add(BlockTypes.AIR);
        fillableBlocks.add(BlockTypes.WATER);
        fillableBlocks.add(BlockTypes.LAVA);
        fillableBlocks.add(BlockTypes.TALLGRASS);

        ArrayList<BlockType> notSuitableForFillBlocks = new ArrayList<BlockType>();
        notSuitableForFillBlocks.add(BlockTypes.TALLGRASS);
        notSuitableForFillBlocks.add(BlockTypes.CACTUS);
        notSuitableForFillBlocks.add(BlockTypes.WATER);
        notSuitableForFillBlocks.add(BlockTypes.LAVA);
        notSuitableForFillBlocks.add(BlockTypes.LOG);
        notSuitableForFillBlocks.add(BlockTypes.LOG2);

        boolean changed;
        do {
            changed = false;
            for (int x = 1; x < snapshots.length - 1; x++) {
                for (int z = 1; z < snapshots[0][0].length - 1; z++) {
                    for (int y = 0; y < snapshots[0].length - 1; y++) {
                        BlockSnapshot block = this.snapshots[x][y][z];
                        if (!fillableBlocks.contains(block.getState().getType())) {
                            continue;
                        }

                        BlockSnapshot leftBlock = this.snapshots[x + 1][y][z];
                        BlockSnapshot rightBlock = this.snapshots[x - 1][y][z];

                        if (!fillableBlocks.contains(leftBlock.getState().getType()) && !fillableBlocks.contains(rightBlock.getState().getType())) {
                            if (!notSuitableForFillBlocks.contains(rightBlock.getState().getType())) {
                                this.snapshots[x][y][z] = block.withState(rightBlock.getState().getType().getDefaultState());
                                changed = true;
                            }
                        }

                        BlockSnapshot upBlock = this.snapshots[x][y][z + 1];
                        BlockSnapshot downBlock = this.snapshots[x][y][z - 1];

                        if (!fillableBlocks.contains(upBlock.getState().getType()) && !fillableBlocks.contains(downBlock.getState().getType())) {
                            if (!notSuitableForFillBlocks.contains(downBlock.getState().getType())) {
                                this.snapshots[x][y][z] = block.withState(downBlock.getState().getType().getDefaultState());
                                changed = true;
                            }
                        }
                    }
                }
            }
        } while (changed);
    }

    private void fixWater() {
        int miny = this.miny;
        if (miny < 1) {
            miny = 1;
        }

        boolean changed;

        // remove hanging water or lava
        for (int x = 1; x < snapshots.length - 1; x++) {
            for (int z = 1; z < snapshots[0][0].length - 1; z++) {
                for (int y = miny; y < snapshots[0].length - 1; y++) {
                    BlockSnapshot block = this.snapshots[x][y][z];
                    BlockSnapshot underBlock = this.snapshots[x][y - 1][z];
                    if (block.getState().getType() == BlockTypes.WATER || block.getState().getType() == BlockTypes.LAVA) {
                        if (underBlock.getState().getType() == BlockTypes.AIR || (((IBlockState) underBlock.getState()).getBlock().getMetaFromState((IBlockState) underBlock.getState()) != 0)) {
                            this.snapshots[x][y][z] = block.withState(BlockTypes.AIR.getDefaultState());
                        }
                    }
                }
            }
        }

        // fill water depressions
        do {
            changed = false;
            for (int y = Math.max(this.seaLevel - 10, 0); y <= this.seaLevel; y++) {
                for (int x = 1; x < snapshots.length - 1; x++) {
                    for (int z = 1; z < snapshots[0][0].length - 1; z++) {
                        BlockSnapshot block = snapshots[x][y][z];

                        // only consider air blocks and flowing water blocks for upgrade to water source blocks
                        if (block.getState().getType() == BlockTypes.AIR || (block.getState().getType() == BlockTypes.WATER
                                && (((IBlockState) block.getState()).getBlock().getMetaFromState((IBlockState) block.getState())) != 0)) {
                            BlockSnapshot leftBlock = this.snapshots[x + 1][y][z];
                            BlockSnapshot rightBlock = this.snapshots[x - 1][y][z];
                            BlockSnapshot upBlock = this.snapshots[x][y][z + 1];
                            BlockSnapshot downBlock = this.snapshots[x][y][z - 1];
                            BlockSnapshot underBlock = this.snapshots[x][y - 1][z];

                            // block underneath MUST be source water
                            if (underBlock.getState().getType() != BlockTypes.WATER
                                    || (((IBlockState) underBlock.getState()).getBlock().getMetaFromState((IBlockState) underBlock.getState())) != 0) {
                                continue;
                            }

                            // count adjacent source water blocks
                            byte adjacentSourceWaterCount = 0;
                            if (leftBlock.getState().getType() == BlockTypes.WATER
                                    && (((IBlockState) leftBlock.getState()).getBlock().getMetaFromState((IBlockState) leftBlock.getState())) == 0) {
                                adjacentSourceWaterCount++;
                            }
                            if (rightBlock.getState().getType() == BlockTypes.WATER
                                    && (((IBlockState) rightBlock.getState()).getBlock().getMetaFromState((IBlockState) rightBlock.getState())) == 0) {
                                adjacentSourceWaterCount++;
                            }
                            if (upBlock.getState().getType() == BlockTypes.WATER && (((IBlockState) upBlock.getState()).getBlock().getMetaFromState((IBlockState) upBlock.getState())) == 0) {
                                adjacentSourceWaterCount++;
                            }
                            if (downBlock.getState().getType() == BlockTypes.WATER
                                    && (((IBlockState) downBlock.getState()).getBlock().getMetaFromState((IBlockState) downBlock.getState())) == 0) {
                                adjacentSourceWaterCount++;
                            }

                            // at least two adjacent blocks must be source water
                            if (adjacentSourceWaterCount >= 2) {
                                snapshots[x][y][z] = block.withState(BlockTypes.WATER.getDefaultState());
                                changed = true;
                            }
                        }
                    }
                }
            }
        } while (changed);
    }

    private void removeDumpedFluids() {
        if (this.seaLevel < 1) {
            return;
        }

        // remove any surface water or lava above sea level, presumed to be
        // placed by players
        // sometimes, this is naturally generated. but replacing it is very easy
        // with a bucket, so overall this is a good plan
        if (this.environment.equals(DimensionTypes.NETHER)) {
            return;
        }
        for (int x = 1; x < snapshots.length - 1; x++) {
            for (int z = 1; z < snapshots[0][0].length - 1; z++) {
                for (int y = this.seaLevel - 1; y < snapshots[0].length - 1; y++) {
                    BlockSnapshot block = snapshots[x][y][z];
                    if (block.getState().getType() == BlockTypes.WATER || block.getState().getType() == BlockTypes.LAVA ||
                            block.getState().getType() == BlockTypes.WATER || block.getState().getType() == BlockTypes.LAVA) {
                        snapshots[x][y][z] = block.withState(BlockTypes.AIR.getDefaultState());
                    }
                }
            }
        }
    }

    private int highestY(int x, int z, boolean ignoreLeaves) {
        int y;
        for (y = snapshots[0].length - 1; y > 0; y--) {
            BlockSnapshot block = this.snapshots[x][y][z];
            if (block.getState().getType() != BlockTypes.AIR &&
                    !(ignoreLeaves && block.getState().getType() == BlockTypes.SNOW) &&
                    !(ignoreLeaves && block.getState().getType() == BlockTypes.LEAVES) &&
                    !(block.getState().getType() == BlockTypes.WATER) &&
                    !(block.getState().getType() == BlockTypes.FLOWING_WATER) &&
                    !(block.getState().getType() == BlockTypes.LAVA) &&
                    !(block.getState().getType() == BlockTypes.FLOWING_LAVA)) {
                return y;
            }
        }

        return y;
    }

    public static ArrayList<BlockType> getPlayerBlocks(DimensionType environment, BiomeType biome) {
        // NOTE on this list. why not make a list of natural blocks?
        // answer: better to leave a few player blocks than to remove too many
        // natural blocks. remember we're "restoring nature"
        // a few extra player blocks can be manually removed, but it will be
        // impossible to guess exactly which natural materials to use in manual
        // repair of an overzealous block removal

        // TODO : add mod support
        ArrayList<BlockType> playerBlocks = new ArrayList<BlockType>();

        playerBlocks.add(BlockTypes.FIRE);
        playerBlocks.add(BlockTypes.BED);
        playerBlocks.add(BlockTypes.PLANKS);
        playerBlocks.add(BlockTypes.BOOKSHELF);
        playerBlocks.add(BlockTypes.BREWING_STAND);
        playerBlocks.add(BlockTypes.BRICK_BLOCK);
        playerBlocks.add(BlockTypes.COBBLESTONE);
        playerBlocks.add(BlockTypes.GLASS);
        playerBlocks.add(BlockTypes.LAPIS_BLOCK);
        playerBlocks.add(BlockTypes.DISPENSER);
        playerBlocks.add(BlockTypes.NOTEBLOCK);
        playerBlocks.add(BlockTypes.RAIL);
        playerBlocks.add(BlockTypes.DETECTOR_RAIL);
        playerBlocks.add(BlockTypes.STICKY_PISTON);
        playerBlocks.add(BlockTypes.PISTON);
        playerBlocks.add(BlockTypes.PISTON_EXTENSION);
        playerBlocks.add(BlockTypes.WOOL);
        playerBlocks.add(BlockTypes.PISTON_HEAD);
        playerBlocks.add(BlockTypes.GOLD_BLOCK);
        playerBlocks.add(BlockTypes.IRON_BLOCK);
        playerBlocks.add(BlockTypes.DOUBLE_STONE_SLAB);
        playerBlocks.add(BlockTypes.STONE_SLAB);
        playerBlocks.add(BlockTypes.WHEAT);
        playerBlocks.add(BlockTypes.TNT);
        playerBlocks.add(BlockTypes.MOSSY_COBBLESTONE);
        playerBlocks.add(BlockTypes.TORCH);
        playerBlocks.add(BlockTypes.FIRE);
        playerBlocks.add(BlockTypes.OAK_STAIRS);
        playerBlocks.add(BlockTypes.CHEST);
        playerBlocks.add(BlockTypes.REDSTONE_WIRE);
        playerBlocks.add(BlockTypes.DIAMOND_BLOCK);
        playerBlocks.add(BlockTypes.CRAFTING_TABLE);
        playerBlocks.add(BlockTypes.FURNACE);
        playerBlocks.add(BlockTypes.LIT_FURNACE);
        playerBlocks.add(BlockTypes.WOODEN_DOOR);
        playerBlocks.add(BlockTypes.STANDING_SIGN);
        playerBlocks.add(BlockTypes.LADDER);
        playerBlocks.add(BlockTypes.RAIL);
        playerBlocks.add(BlockTypes.STONE_STAIRS);
        playerBlocks.add(BlockTypes.WALL_SIGN);
        playerBlocks.add(BlockTypes.STONE_PRESSURE_PLATE);
        playerBlocks.add(BlockTypes.LEVER);
        playerBlocks.add(BlockTypes.IRON_DOOR);
        playerBlocks.add(BlockTypes.WOODEN_PRESSURE_PLATE);
        playerBlocks.add(BlockTypes.REDSTONE_TORCH);
        playerBlocks.add(BlockTypes.UNLIT_REDSTONE_TORCH);
        playerBlocks.add(BlockTypes.STONE_BUTTON);
        playerBlocks.add(BlockTypes.SNOW);
        playerBlocks.add(BlockTypes.JUKEBOX);
        playerBlocks.add(BlockTypes.FENCE);
        playerBlocks.add(BlockTypes.PORTAL);
        playerBlocks.add(BlockTypes.LIT_PUMPKIN);
        playerBlocks.add(BlockTypes.CAKE);
        playerBlocks.add(BlockTypes.UNPOWERED_REPEATER);
        playerBlocks.add(BlockTypes.POWERED_REPEATER);
        playerBlocks.add(BlockTypes.TRAPDOOR);
        playerBlocks.add(BlockTypes.STONEBRICK);
        playerBlocks.add(BlockTypes.BROWN_MUSHROOM_BLOCK);
        playerBlocks.add(BlockTypes.RED_MUSHROOM_BLOCK);
        playerBlocks.add(BlockTypes.IRON_BARS);
        playerBlocks.add(BlockTypes.GLASS_PANE);
        playerBlocks.add(BlockTypes.MELON_STEM);
        playerBlocks.add(BlockTypes.FENCE_GATE);
        playerBlocks.add(BlockTypes.BRICK_STAIRS);
        playerBlocks.add(BlockTypes.STONE_BRICK_STAIRS);
        playerBlocks.add(BlockTypes.ENCHANTING_TABLE);
        playerBlocks.add(BlockTypes.BREWING_STAND);
        playerBlocks.add(BlockTypes.CAULDRON);
        playerBlocks.add(BlockTypes.WEB);
        playerBlocks.add(BlockTypes.SPONGE);
        playerBlocks.add(BlockTypes.GRAVEL);
        playerBlocks.add(BlockTypes.EMERALD_BLOCK);
        playerBlocks.add(BlockTypes.SANDSTONE);
        playerBlocks.add(BlockTypes.WOODEN_SLAB);
        playerBlocks.add(BlockTypes.DOUBLE_WOODEN_SLAB);
        playerBlocks.add(BlockTypes.ENDER_CHEST);
        playerBlocks.add(BlockTypes.SANDSTONE_STAIRS);
        playerBlocks.add(BlockTypes.SPRUCE_STAIRS);
        playerBlocks.add(BlockTypes.JUNGLE_STAIRS);
        playerBlocks.add(BlockTypes.COMMAND_BLOCK);
        playerBlocks.add(BlockTypes.BEACON);
        playerBlocks.add(BlockTypes.COBBLESTONE_WALL);
        playerBlocks.add(BlockTypes.FLOWER_POT);
        playerBlocks.add(BlockTypes.CARROTS);
        playerBlocks.add(BlockTypes.POTATOES);
        playerBlocks.add(BlockTypes.WOODEN_BUTTON);
        playerBlocks.add(BlockTypes.SKULL);
        playerBlocks.add(BlockTypes.ANVIL);

        // these are unnatural in the standard world, but not in the nether
        if (environment.equals(DimensionTypes.NETHER)) {
            playerBlocks.add(BlockTypes.NETHERRACK);
            playerBlocks.add(BlockTypes.SOUL_SAND);
            playerBlocks.add(BlockTypes.GLOWSTONE);
            playerBlocks.add(BlockTypes.NETHER_BRICK);
            playerBlocks.add(BlockTypes.NETHER_BRICK_FENCE);
            playerBlocks.add(BlockTypes.NETHER_BRICK_STAIRS);
        }

        // these are unnatural in the standard and nether worlds, but not in the end
        if (environment.equals(DimensionTypes.THE_END)) {
            playerBlocks.add(BlockTypes.OBSIDIAN);
            playerBlocks.add(BlockTypes.END_STONE);
            playerBlocks.add(BlockTypes.END_PORTAL_FRAME);
        }

        //these are unnatural in sandy biomes, but not elsewhere 
        if (biome == BiomeTypes.DESERT || biome == BiomeTypes.DESERT_HILLS || biome == BiomeTypes.BEACH ||
                !environment.equals(DimensionTypes.OVERWORLD)) {
            playerBlocks.add(BlockTypes.LEAVES);
            playerBlocks.add(BlockTypes.LOG);
            playerBlocks.add(BlockTypes.LOG2);
        }

        return playerBlocks;
    }
}