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

import java.util.Arrays;

import org.bukkit.World;
import org.bukkit.block.Biome;

import ca.recrown.islandsurvivalcraft.utilities.GeneralUtilities;
import ca.recrown.islandsurvivalcraft.utilities.biomes.LandBiomeInfo;
import ca.recrown.islandsurvivalcraft.utilities.biomes.OceanBiomeInfo;
import ca.recrown.islandsurvivalcraft.utilities.caching.Cache;
import ca.recrown.islandsurvivalcraft.utilities.datatypes.Point2;
import ca.recrown.islandsurvivalcraft.utilities.floodfill.Floodable;
import ca.recrown.islandsurvivalcraft.utilities.floodfill.Flooder;
import ca.recrown.islandsurvivalcraft.utilities.pathfinding.CoordinateTargetValidatable;
import ca.recrown.islandsurvivalcraft.utilities.pathfinding.CoordinateValidatable;
import ca.recrown.islandsurvivalcraft.world.BiomeMap;
import ca.recrown.islandsurvivalcraft.world.IslandWorldMap;
import ca.recrown.islandsurvivalcraft.world.TemperatureMap;

public class UniqueBiomeGenerator implements BiomeGenerator {
    private final double DEEP_OCEAN_PORTION = 0.5d;
    private final double HILL_PORTION = 0.4;

    @Override
    public void generateBiomeColumn(Biome[][][] chunkBiomeSets, World world, int chunkX, int chunkZ, int localX, int localZ, IslandWorldMap islandMap, BiomeMap biomeSelector, TemperatureMap tempGen, Cache<Point2, Biome[]> biomeCache) {
        int worldX = 16 * chunkX + localX;
        int worldZ = 16 * chunkZ + localZ;

        //Check if we can just give it something in cache.
        Biome[] biomeSet = attemptGetBiomeSet(world, worldX, worldZ, biomeCache, null);
        if (biomeSet != null) {
            chunkBiomeSets[localX][localZ] = biomeSet;
            return;
        }

        //Fine, check if it's ocean.
        if (!islandMap.isIsland(worldX, worldZ)) {
            biomeSet = chunkBiomeSets[localX][localZ];
            OceanBiomeInfo ocean = biomeSelector.getOceanBiome(tempGen.getTemperature(worldX, worldZ));
            biomeSet[0] = islandMap.getWorldValue(worldX, worldZ) >= -DEEP_OCEAN_PORTION ? ocean.getOcean() : ocean.getDeepAlternative();
            setCacheBiome(worldX, worldZ, biomeSet, chunkBiomeSets, biomeCache);
            return;
        }

        //Shoot, looks like it's actually an island.
        IslandInfo islandInfo = new IslandInfo(islandMap, world, biomeCache);
        Point2 islandOrigin = islandMap.findIslandOrigin(worldX, worldZ, islandInfo);
        if (!islandInfo.isComplete()) {
            float temp = tempGen.getTemperature(islandOrigin.x, islandOrigin.y);
            LandBiomeInfo biomeInfo = biomeSelector.getLandBiomeInfo(temp, islandOrigin.x, islandOrigin.y);
            islandInfo.biomeset = new Biome[5];
            islandInfo.biomeset[1] = biomeInfo.getBiome();
            islandInfo.biomeset[2] = biomeInfo.getPreferredShore() == null ? Biome.BEACH : biomeInfo.getPreferredShore();
            islandInfo.biomeset[4] = biomeInfo.getHillBiome() == null ? islandInfo.biomeset[1] : biomeInfo.getHillBiome();
            islandInfo.biomeset[3] = islandInfo.biomeset[2];
        }

        PropagatorInfo propInfo = new PropagatorInfo(islandInfo.biomeset, chunkBiomeSets, new Point2(chunkX, chunkZ), islandMap, biomeCache);
        Flooder flooder = new Flooder(new Point2(worldX, worldZ), propInfo);
        flooder.start();
    }

    private Biome[] attemptGetBiomeSet(World world, int worldX, int worldZ, Cache<Point2, Biome[]> biomeCache, Biome[][][] chunkBiomeSets) {
        Point2 worldCoords = new Point2(worldX, worldZ);
        Biome[] res = null;

        if (chunkBiomeSets != null) {
            Point2 localCoords = GeneralUtilities.worldToLocalChunkCoordinates(worldCoords);
            return chunkBiomeSets[localCoords.x][localCoords.y];
        }

        res = biomeCache.get(worldCoords);
        return res;
    }

    private void setCacheBiome(int worldX, int worldZ, Biome[] biomeSet, Biome[][][] localBiomes, Cache<Point2, Biome[]> biomeCache) {
        Point2 worldCoords = new Point2(worldX, worldZ);
        if (localBiomes != null) {
            Point2 localCoords = GeneralUtilities.worldToLocalChunkCoordinates(worldCoords);
            localBiomes[localCoords.x][localCoords.y] = biomeSet;
        }
        biomeCache.set(worldCoords, biomeSet);
    }

    private class IslandInfo implements CoordinateTargetValidatable, CoordinateValidatable {
        public final IslandWorldMap mapper;
        public final World world;
        private final Cache<Point2, Biome[]> biomeCache;
        private Biome[] biomeset;
        
        public IslandInfo(IslandWorldMap mapper, World world, Cache<Point2, Biome[]> biomeCache) {
            this.mapper = mapper;
            this.world = world;
            this.biomeCache = biomeCache;
        }

        @Override
        public boolean isCoordinateTarget(int x, int y) {
            biomeset = attemptGetBiomeSet(world, x, y, biomeCache, null);
            if (biomeset != null) {
                if (!isComplete()) throw new IllegalStateException("There was an incomplete biome set.");
                return true;
            }
            return false;
        }

        @Override
        public boolean validate(int x, int y) {
            return mapper.isIsland(x, y);
        }

        public boolean isComplete() {
            return !(biomeset == null || biomeset[1] == null || biomeset[2] == null || biomeset[3] == null || biomeset[4] == null);
        }
    }

    private class PropagatorInfo implements Floodable {
        private final Biome[] biomeset;
        private final Biome[][][] biomes;
        private final Point2 chunkCoords;
        private final IslandWorldMap mapper;
        private final Cache<Point2, Biome[]> biomeCache;

        public PropagatorInfo(Biome[] biomeset, Biome[][][] biomes, Point2 chunkCoords, IslandWorldMap mapper, Cache<Point2, Biome[]> biomeCache) {
            this.biomeset = biomeset;
            this.biomes = biomes;
            this.chunkCoords = chunkCoords;
            this.mapper = mapper;
            this.biomeCache = biomeCache;
        }

        @Override
        public boolean flood(Point2 point) {
            Point2 chunkCoords = GeneralUtilities.worldToChunkCoordinates(point);
            if (!this.chunkCoords.fastEquals(chunkCoords) || !mapper.isIsland(point.x, point.y)) return false;
            int x = point.x;
            int y = point.y;
            Biome[] biomeSet = Arrays.copyOf(biomeset, biomeset.length);
            if (mapper.isLand(x, y)) {
                if (mapper.isShore(x, y)) {
                    biomeSet[0] = biomeset[2];
                } else if (mapper.getWorldValue(x, y) >= HILL_PORTION) {
                    biomeSet[0] = biomeset[4];
                } else {
                    biomeSet[0] = biomeset[1];
                }
            } else {
                biomeSet[0] = biomeset[3];
            }
            
            setCacheBiome(x, y, biomeSet, biomes, biomeCache);
            return true;
        }

    }
}