package ca.recrown.islandsurvivalcraft.world;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Random;
import java.util.Map.Entry;

import org.bukkit.block.Biome;
import org.bukkit.util.noise.SimplexOctaveGenerator;

import ca.recrown.islandsurvivalcraft.utilities.GeneralUtilities;

public class BiomeMap {
    private final SimplexOctaveGenerator noise;
    private final HashMap<Biome, Float> landBiomeTemperatures = new HashMap<>();
    private volatile HashMap<Float, ArrayList<Biome>> temperaturesForLand = new HashMap<>();
    private final HashMap<Float, ArrayList<Biome>> temperaturePartitionedLandBiomes = new HashMap<>();
    private final HashMap<Biome, Float> oceanBiomeTemperatures = new HashMap<>();
    private volatile HashMap<Float, ArrayList<Biome>> temperaturesForOcean = new HashMap<>();
    private final HashMap<Float, ArrayList<Biome>> temperaturePartitionedOceanBiomes = new HashMap<>();

    public BiomeMap(Random random) {
        this(random, 0.05d);
    }

    public BiomeMap(Random random, double scale) {
        noise = new SimplexOctaveGenerator(random, 2);
        noise.setScale(scale);
        temperaturePartitionedLandBiomes.put(0.05f, new ArrayList<>());
        temperaturePartitionedLandBiomes.put(0.30f, new ArrayList<>());
        temperaturePartitionedLandBiomes.put(0.95f, new ArrayList<>());
        temperaturePartitionedLandBiomes.put(2f, new ArrayList<>());

        temperaturePartitionedOceanBiomes.put(0.00f, new ArrayList<>());
        temperaturePartitionedOceanBiomes.put(0.3f, new ArrayList<>());
        temperaturePartitionedOceanBiomes.put(0.5f, new ArrayList<>());

        registerLandBiomeTemperatures();
        registerOceanBiomeTemperatures();
    }    

    private void registerLandBiomeTemperatures() {
        landBiomeTemperatures.put(Biome.SNOWY_TUNDRA, 0.0f);
        landBiomeTemperatures.put(Biome.ICE_SPIKES, 0.0f);
        landBiomeTemperatures.put(Biome.SNOWY_TAIGA, -0.5f);
        landBiomeTemperatures.put(Biome.SNOWY_TAIGA_MOUNTAINS, -0.5f);
        landBiomeTemperatures.put(Biome.MOUNTAINS, 0.2f);
        landBiomeTemperatures.put(Biome.GRAVELLY_MOUNTAINS, 0.2f);
        landBiomeTemperatures.put(Biome.WOODED_MOUNTAINS, 0.2f);
        landBiomeTemperatures.put(Biome.MODIFIED_GRAVELLY_MOUNTAINS, 0.2f);
        landBiomeTemperatures.put(Biome.TAIGA, 0.25f);
        landBiomeTemperatures.put(Biome.TAIGA_MOUNTAINS, 0.25f);
        landBiomeTemperatures.put(Biome.GIANT_TREE_TAIGA, 0.3f);
        landBiomeTemperatures.put(Biome.PLAINS, 0.8f);
        landBiomeTemperatures.put(Biome.SUNFLOWER_PLAINS, 0.8f);
        landBiomeTemperatures.put(Biome.FOREST, 0.7f);
        landBiomeTemperatures.put(Biome.FLOWER_FOREST, 0.7f);
        landBiomeTemperatures.put(Biome.BIRCH_FOREST, 0.6f);
        landBiomeTemperatures.put(Biome.TALL_BIRCH_FOREST, 0.7f);
        landBiomeTemperatures.put(Biome.DARK_FOREST, 0.7f);
        landBiomeTemperatures.put(Biome.SWAMP, 0.8f);
        landBiomeTemperatures.put(Biome.JUNGLE, 0.95f);
        landBiomeTemperatures.put(Biome.MODIFIED_JUNGLE, 0.95f);
        landBiomeTemperatures.put(Biome.BAMBOO_JUNGLE, 0.95f);
        landBiomeTemperatures.put(Biome.MUSHROOM_FIELDS, 0.9f);
        landBiomeTemperatures.put(Biome.DESERT, 2.0f);
        landBiomeTemperatures.put(Biome.SAVANNA, 1.2f);
        landBiomeTemperatures.put(Biome.SHATTERED_SAVANNA, 1.1f);
        landBiomeTemperatures.put(Biome.BADLANDS, 2.0f);
        landBiomeTemperatures.put(Biome.ERODED_BADLANDS, 2.0f);
        landBiomeTemperatures.put(Biome.WOODED_BADLANDS_PLATEAU, 2.0f);
        landBiomeTemperatures.put(Biome.MODIFIED_WOODED_BADLANDS_PLATEAU, 2.0f);
        landBiomeTemperatures.put(Biome.SAVANNA_PLATEAU, 1.0f);
        landBiomeTemperatures.put(Biome.BADLANDS_PLATEAU, 2.0f);
        landBiomeTemperatures.put(Biome.MODIFIED_BADLANDS_PLATEAU, 2.0f);
        landBiomeTemperatures.put(Biome.SHATTERED_SAVANNA_PLATEAU, 1.0f);
        temperaturesForLand = GeneralUtilities.invertHashMap(landBiomeTemperatures);

        for (Entry<Float, ArrayList<Biome>> entry : temperaturesForLand.entrySet()) {
            if (entry.getKey() <= 0.05f) {
                temperaturePartitionedLandBiomes.get(0.05f).addAll(entry.getValue());
            } else if (entry.getKey() <= 0.3) {
                temperaturePartitionedLandBiomes.get(0.3f).addAll(entry.getValue());
            } else if (entry.getKey() <= 0.85f) {
                temperaturePartitionedLandBiomes.get(0.95f).addAll(entry.getValue());
            } else {
                temperaturePartitionedLandBiomes.get(2.00f).addAll(entry.getValue());
            }
        }
    }

    private void registerOceanBiomeTemperatures() {
        oceanBiomeTemperatures.put(Biome.WARM_OCEAN, 0.5f);
        oceanBiomeTemperatures.put(Biome.LUKEWARM_OCEAN, 0.5f);
        oceanBiomeTemperatures.put(Biome.OCEAN, 0.3f);
        oceanBiomeTemperatures.put(Biome.COLD_OCEAN, 0.3f);
        oceanBiomeTemperatures.put(Biome.FROZEN_OCEAN, 0f);
        temperaturesForOcean = GeneralUtilities.invertHashMap(oceanBiomeTemperatures);
        for (Entry<Float, ArrayList<Biome>> entry : temperaturesForOcean.entrySet()) {
            if (entry.getKey() <= 0.00f) {
                temperaturePartitionedOceanBiomes.get(0.0f).addAll(entry.getValue());
            } else if (entry.getKey() <= 0.3f) {
                temperaturePartitionedOceanBiomes.get(0.3f).addAll(entry.getValue());
            } else {
                temperaturePartitionedOceanBiomes.get(0.5f).addAll(entry.getValue());
            }
        }
    }

    /**
     * Obtains the normal temperature for the given biome.
     * @param biome The given biome.
     * @return the Minecraft temperature.
     */
    public float getBiomeTemperature(Biome biome) {
        String biomeName = biome.name().toLowerCase();
        if (biomeName.endsWith("hills") || biomeName.endsWith("beach") || biomeName.endsWith("shore")) {
            String biomeTypeName = biomeName;
            biomeTypeName = biomeTypeName.replace("_hills", "");
            biomeTypeName = biomeTypeName.replace("_beach", "");
            biomeTypeName = biomeTypeName.replace("_shore", "");

            for (Entry<Biome, Float> entry : landBiomeTemperatures.entrySet()) {
                if (entry.getKey().name().toLowerCase().startsWith(biomeTypeName)) {
                    return entry.getValue();
                }
            }
        }
        return landBiomeTemperatures.get(biome);
    }

    /**
     * Attempts to procure a biome that helps transitionfrom one biome to another biome
     * for given biome.
     * @param from the biome to transition from.
     * @return the resulting transition biome.
     */
    public Biome getTransitionalBiome(Biome from) {
        String biomeName = from.name().toLowerCase();
        if (biomeName.contains("jungle")) {
            if (biomeName.contains("modified")) {
                return Biome.MODIFIED_JUNGLE_EDGE;
            }
            return Biome.JUNGLE_EDGE;
        } else if (biomeName.contains("mountain")) {
            //return Biome.MOUNTAIN_EDGE;
            return null;
        }
        return null;
    }

    /**
     * Finds the shore biome from the given information.
     * @param from land biome that connects to it.
     * @param temperature the Minecraft temperature of this shore.
     * @return the shore biome associated with it.
     */
    public Biome getShoreBiome(Biome from, float temperature) {
        String biomeName = from.name().toLowerCase();
        if (biomeName.contains("mushroom")) {
            return Biome.MUSHROOM_FIELD_SHORE;
        } else if (biomeName.contains("mountains")) {
            return Biome.STONE_SHORE;
        }
        if (temperature < 0.05) {
            return Biome.SNOWY_BEACH;
        }
        return Biome.BEACH;
    }

    public boolean isOceanBiome(Biome biome) {
        return oceanBiomeTemperatures.containsKey(biome);
    }

    public boolean isLandBiome(Biome biome) {
        return !isOceanBiome(biome);
    }

    /**
     * Randomly selects a land biome that fits in given temperature.
     * @param temperature Minecraft temperature to select biome from.
     * @param seed The seed to use to select the biome.
     * @return The randomly selected biome.
     */
    public Biome getLandBiome(float temperature, int worldX, int worldZ) {
        ArrayList<Biome> biomes = null;
        if (temperature <= 0.05f) {
            biomes = temperaturePartitionedLandBiomes.get(0.05f);
        } else if (temperature <= 0.3) {
            biomes = temperaturePartitionedLandBiomes.get(0.3f);
        } else if (temperature <= 0.85f) {
            biomes = temperaturePartitionedLandBiomes.get(0.95f);
        } else {
            biomes = temperaturePartitionedLandBiomes.get(2.00f);
        }
        return biomes.get((int) ((noise.noise(worldX, worldZ, 0.5d, 0.5d, true) + 1d) / 2d) * biomes.size());
    }

    /**
     * Randomly selects an ocean biome that fits in given temperature.
     * 
     * @param temperature Minecraft temperature to select biome from.
     * @param isDeep If this ocean should be of the deep variant.
     * @param seed The seed to use to select the biome.
     * @return The randomly selected biome.
     */
    public Biome getOceanBiome(float temperature, boolean isDeep, int worldX, int worldZ) {
        ArrayList<Biome> biomes = null;
        if (temperature <= 0.00f) {
            biomes = temperaturePartitionedOceanBiomes.get(0.0f);
        } else if (temperature <= 0.3) {
            biomes = temperaturePartitionedOceanBiomes.get(0.3f);
        } else {
            biomes = temperaturePartitionedOceanBiomes.get(0.5f);
        }
        Biome oceanBiome = biomes.get((int) ((noise.noise(worldX, worldZ, 0.5d, 0.5d, true) + 1d) / 2d) * biomes.size());
        if (isDeep) oceanBiome = Biome.valueOf("DEEP_" + oceanBiome.name());
        return oceanBiome;
    }

}