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

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

import ca.recrown.islandsurvivalcraft.utilities.GeneralUtilities;
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.DepthFirstSearch;
import ca.recrown.islandsurvivalcraft.world.BiomeMap;
import ca.recrown.islandsurvivalcraft.world.IslandWorldMap;
import ca.recrown.islandsurvivalcraft.world.TemperatureMap;

public class UniqueBiomeGenerator implements BiomeGenerator {
    @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, Cache<Point2, Boolean> chunkGenCache) {
        int worldX = 16 * chunkX + localX;
        int worldZ = 16 * chunkZ + localZ;
        Point2 chunkCoords = GeneralUtilities.worldToChunkCoordinates(new Point2(worldX, worldZ));
        chunkGenCache.set(chunkCoords, false);

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

        //Fine, check if it's ocean.
        if (!islandMap.isIsland(worldX, worldZ)) {
            biomeSet = chunkBiomeSets[localX][localZ];
            biomeSet[0] = biomeSelector.getOceanBiome(tempGen.getTemperature(worldX, worldZ), islandMap.isDeepOcean(worldX, worldZ), worldX, worldZ);
            setCacheBiome(worldX, worldZ, biomeSet, chunkBiomeSets, biomeCache);
            return;
        }

        //Shoot, looks like it's actually an island.
        DepthFirstSearch search = new DepthFirstSearch();
        search.setValidatable(islandMap);
        
        IslandInfo islandInfo = new IslandInfo(islandMap, world, biomeCache, chunkGenCache);
        search.setStartPosition(worldX, worldZ);
        if (!search.findTarget(islandInfo)) {
            float temp = tempGen.getTemperature(worldX, worldZ);
            if (islandInfo.main == null) islandInfo.main = biomeSelector.getLandBiome(temp, worldX, worldZ);
            if (islandInfo.shore == null) islandInfo.shore = biomeSelector.getShoreBiome(islandInfo.main, temp);
            if (islandInfo.shallow == null) islandInfo.shallow = biomeSelector.getOceanBiome(temp, islandMap.isDeepOcean(worldX, worldZ), worldX, worldZ);
        }

        PropagatorInfo propInfo = new PropagatorInfo(islandInfo, 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, Cache<Point2, Boolean> existenceCache, 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);
        if (res != null) return res;

        Point2 chunkCoords = GeneralUtilities.worldToChunkCoordinates(worldCoords);
        Boolean chunkExists = existenceCache.get(chunkCoords);
        if (chunkExists == null) {
            chunkExists = world.isChunkGenerated(worldX, worldZ);
            existenceCache.set(chunkCoords, chunkExists);
        }

        if (chunkExists) {
            res = new Biome[4];
            res[0] = world.getBiome(worldX, 0, worldZ);
            biomeCache.set(worldCoords, res);
        }
        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 {
        public final IslandWorldMap mapper;
        public final World world;
        private final Cache<Point2, Biome[]> biomeCache;
        private final Cache<Point2, Boolean> chunkGenCache;
        public Biome main, shore, shallow;
        
        public IslandInfo(IslandWorldMap mapper, World world, Cache<Point2, Biome[]> biomeCache, Cache<Point2, Boolean> chunkGenCache) {
            this.mapper = mapper;
            this.world = world;
            this.biomeCache = biomeCache;
            this.chunkGenCache = chunkGenCache;
        }

        @Override
        public boolean isCoordinateTarget(int x, int y) {
            Biome[] biomeSet = attemptGetBiomeSet(world, x, y, biomeCache, chunkGenCache, null);
            if (biomeSet != null) {
                main = biomeSet[1];
                shore = biomeSet[2];
                shallow = biomeSet[3];
                if ((main == null || shore == null) && mapper.isLand(x, y)) {
                    if (shore == null && mapper.isShore(x, y)) {
                        main = biomeSet[0];
                    } else if (main == null && !mapper.isShore(x, y)) {
                        shore = biomeSet[0];
                    }
                } else if (shallow == null) {
                    shallow = biomeSet[0];
                }
            }
            return main != null && shore != null && shallow != null;
        }
    }

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

        public PropagatorInfo(IslandInfo islandInfo, Biome[][][] biomes, Point2 chunkCoords, IslandWorldMap mapper, Cache<Point2, Biome[]> biomeCache) {
            this.shallow = islandInfo.shallow;
            this.shore = islandInfo.shore;
            this.main = islandInfo.main;
            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.validate(point.x, point.y)) return false;
            int x = point.x;
            int y = point.y;
            Biome[] biomeSet = new Biome[4];
            biomeSet[1] = main;
            biomeSet[2] = shore;
            biomeSet[3] = shallow;
            if (mapper.isLand(x, y)) {
                if (mapper.isShore(x, y)) {
                    biomeSet[0] = shore;
                } else {
                    biomeSet[0] = main;
                }
            } else {
                biomeSet[0] = shallow;
            }
            
            setCacheBiome(x, y, biomeSet, biomes, biomeCache);
            return true;
        }

    }
}