package ca.recrown.islandsurvivalcraft.world.generation.chunks;

import java.util.LinkedList;
import java.util.Random;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;

import org.apache.commons.lang.NotImplementedException;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.block.Biome;
import org.bukkit.block.data.BlockData;
import org.bukkit.event.Listener;
import org.bukkit.generator.ChunkGenerator;

import ca.recrown.islandsurvivalcraft.utilities.GeneralUtilities;
import ca.recrown.islandsurvivalcraft.utilities.caching.Cache;
import ca.recrown.islandsurvivalcraft.utilities.datatypes.Point2;
import ca.recrown.islandsurvivalcraft.world.BiomeMap;
import ca.recrown.islandsurvivalcraft.world.IslandWorldMap;
import ca.recrown.islandsurvivalcraft.world.TemperatureMap;
import ca.recrown.islandsurvivalcraft.world.WorldInfo;
import ca.recrown.islandsurvivalcraft.world.generation.GeneratorModes;
import ca.recrown.islandsurvivalcraft.world.generation.biomes.BiomeGenerator;
import ca.recrown.islandsurvivalcraft.world.generation.biomes.UniqueBiomeGenerator;
import ca.recrown.islandsurvivalcraft.world.generation.shaders.WorldHeightShader;
import ca.recrown.islandsurvivalcraft.world.generation.shaders.WorldLayerShader;

public class IslandWorldChunkGenerator extends ChunkGenerator implements Listener {
    private final int BEDROCK_HEIGHT = 5;
    private volatile GeneratorModes generatorType;
    private volatile boolean initialized = false;
    private volatile WorldInfo worldInfo;
    private volatile IslandWorldMap islandMap;
    private volatile TemperatureMap temperatureMap;
    private volatile BiomeMap biomeMap;
    private volatile BiomeGenerator biomeGenerator;
    private volatile WorldHeightShader heightShader;
    private volatile WorldLayerShader layerShader;
    private volatile Cache<Point2, Biome[]> biomeCache = new Cache<>(131072);
    private volatile Cache<Point2, Boolean> chunkLoadedCache = new Cache<>(4096);
    private volatile ExecutorService exAlpha = GeneralUtilities.ISC_EXECUTOR_ALPHA;
    private volatile ExecutorService exBeta = GeneralUtilities.ISC_EXECUTOR_BETA;

    public void initialize() {
        if (initialized) throw new IllegalStateException("This generator has already been initialized.");
        initialized = true;
        this.islandMap = worldInfo.getIslandMap();
        this.temperatureMap = worldInfo.getTempMap();
        this.biomeMap = worldInfo.getBiomeMap();
        this.heightShader = new WorldHeightShader(worldInfo.getRandom(), islandMap, worldInfo.getSeaLevel(), worldInfo.getWorldHeight(), BEDROCK_HEIGHT + 1);
        this.layerShader = new WorldLayerShader(worldInfo.getRandom(), worldInfo.getSeaLevel(), worldInfo.getWorldHeight(), islandMap);

        if (generatorType == GeneratorModes.UNIQUE) {
            biomeGenerator = new UniqueBiomeGenerator();
        } else {
            throw new NotImplementedException();
        }
    }

    /**
     * @param generatorType the gID to set to.
     */
    public void setGeneratorType(GeneratorModes generatorType) {
        if (initialized) throw new IllegalStateException("This generator has already been initialized.");
        this.generatorType = generatorType;
    }

    public IslandWorldChunkGenerator(WorldInfo worldInfo) {
        this.worldInfo = worldInfo;   
    }

    @Override
    public ChunkData generateChunkData(World world, Random random, int chunkX, int chunkZ, BiomeGrid biomeGrid) {
        if (!worldInfo.isInitialized()) {
            worldInfo.initialize(world, this);
        }
        if (!initialized) throw new IllegalStateException("This generator has not been initialized.");

        LinkedList<Future<Boolean>> tasks = new LinkedList<>();
        ChunkData chunkData = createChunkData(world);
        Future<Boolean> preLoader = exAlpha.submit(() -> {
            for (int x = GeneralUtilities.CHUNK_SIZE - 1; x >= 0; x--) {
                for (int z = GeneralUtilities.CHUNK_SIZE - 1; z >= 0; z--) {
                    if (Thread.currentThread().isInterrupted()) return false;
                    islandMap.getWorldValue(GeneralUtilities.CHUNK_SIZE * chunkX + x, GeneralUtilities.CHUNK_SIZE * chunkZ + z);
                }
            }
            return true;
        });

        Biome[][][] biomeArraySet = new Biome[GeneralUtilities.CHUNK_SIZE][GeneralUtilities.CHUNK_SIZE][4];
        for (int x = 0; x < GeneralUtilities.CHUNK_SIZE; x++) {
            for (int z = 0; z < GeneralUtilities.CHUNK_SIZE; z++) {
                final int localX = x;
                final int localZ = z;
                final int worldX = GeneralUtilities.CHUNK_SIZE * chunkX + localX;
                final int worldZ = GeneralUtilities.CHUNK_SIZE * chunkZ + localZ;
                if (biomeArraySet[localX][localZ][0] == null) {
                    biomeGenerator.generateBiomeColumn(biomeArraySet, world, chunkX, chunkZ, localX, localZ, islandMap, biomeMap,
                            temperatureMap, biomeCache, chunkLoadedCache);
                }
                if (biomeArraySet[localX][localZ][0] == null) throw new IllegalStateException("Biome was null.");
                tasks.add(exBeta.submit(() -> {
                    for (int y = 0; y < worldInfo.getWorldHeight(); y++) {
                        biomeGrid.setBiome(localX, y, localZ, biomeArraySet[localX][localZ][0]);
                    }
                    return true;
                }));

                Biome[] currentBiomeSet = biomeArraySet[localX][localZ];
                int terrainHeight = heightShader.getTerrainHeight(worldX, worldZ, currentBiomeSet);
                int currentTerrainHeight = terrainHeight - 1;
                int bedrockHeight = random.nextInt(5) + 1;
                if (layerShader.hasSpecialLayers(currentBiomeSet[0])) {
                    BlockData currentMaterial = layerShader.getMaterialForHeight(worldInfo.getSeed(), worldX, worldZ, currentTerrainHeight, terrainHeight - 1, currentBiomeSet);
                    while (currentMaterial != null) {
                        chunkData.setBlock(localX, currentTerrainHeight, localZ, currentMaterial);
                        currentTerrainHeight--;
                        currentMaterial = layerShader.getMaterialForHeight(worldInfo.getSeed(), worldX, worldZ, currentTerrainHeight, terrainHeight - 1, currentBiomeSet);
                    }
                } else {
                    int surfaceThickness = layerShader.getSurfaceThickness(worldX, worldZ, currentBiomeSet[0]);
                    currentTerrainHeight = currentTerrainHeight - surfaceThickness;
                    chunkData.setRegion(localX, currentTerrainHeight, localZ, localX + 1, currentTerrainHeight + surfaceThickness + 1, localZ + 1, layerShader.getSurfaceMaterial(currentBiomeSet[0]));
                    int transitionThickness = layerShader.getTransitionMaterialThickness(worldX, worldZ, currentBiomeSet[0]);
                    currentTerrainHeight = currentTerrainHeight - transitionThickness;
                    chunkData.setRegion(localX, currentTerrainHeight, localZ, localX + 1, currentTerrainHeight + transitionThickness + 1, localZ + 1, layerShader.getTransitionMaterial(currentBiomeSet[0]));
                }
                chunkData.setRegion(localX, bedrockHeight, localZ, localX + 1, currentTerrainHeight + 1, localZ + 1, Material.STONE);
                if (terrainHeight < worldInfo.getSeaLevel()) {
                    chunkData.setRegion(localX, terrainHeight, localZ, localX + 1, worldInfo.getSeaLevel(), localZ + 1, Material.WATER);
                }
                chunkData.setRegion(localX, 0, localZ, localX + 1, bedrockHeight, localZ + 1, Material.BEDROCK);
            }
        }

        preLoader.cancel(false);
        try {
            while (!tasks.isEmpty()) {
                tasks.poll().get();
            }
        } catch (ExecutionException | InterruptedException e) {
            e.printStackTrace();
            throw new IllegalStateException(e.getCause().getMessage());
        }
        return chunkData;
    }

    /**
     * @return if this generator has been initialized.
     */
    public boolean isInitialized() {
        return initialized;
    }

    @Override
    public boolean canSpawn(World world, int x, int z) {
        return islandMap.isLand(x, z);
    }

    @Override
    public Location getFixedSpawnLocation(World world, Random random) {
        return null;
    }

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

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

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

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

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

    public void updateChunkLoadedCache(int chunkX, int chunkZ) {
        this.chunkLoadedCache.set(new Point2(chunkX, chunkZ), true);
    }
}