package ca.recrown.islandsurvivalcraft.world;

import java.util.Random;

import org.bukkit.util.noise.SimplexOctaveGenerator;

import ca.recrown.islandsurvivalcraft.caching.Cache;
import ca.recrown.islandsurvivalcraft.caching.CacheValue;
import ca.recrown.islandsurvivalcraft.caching.CoordinateIdentifier;
import ca.recrown.islandsurvivalcraft.pathfinding.CoordinateValidatable;
import ca.recrown.islandsurvivalcraft.pathfinding.DepthFirstSearch;

public class IslandWorldMapper implements CoordinateValidatable {
    private Cache<Double> blockValueCache;

    private SimplexOctaveGenerator noiseGenerator;
    private final int noiseOctaves = 4;
    private final float islandBlockGenerationPercent = 16;
    private final float exaggerationFactor = 1.2f;
    private final float islandValueExaggerationFactor = 0.2f;
    private final double noiseFrequency = 1.95D;
    private final double noiseAmplitude = 0.5D;
    private final double scale = 0.03D;
    private final float shoreFactor = 0.035f;
    private final float shallowPortion = 0.015f;
    private final DepthFirstSearch dfs;

    public IslandWorldMapper(Random random) {
        dfs = new DepthFirstSearch(this);
        this.noiseGenerator = new SimplexOctaveGenerator(random, noiseOctaves);
        noiseGenerator.setScale(scale);
        blockValueCache = new Cache<>(4096);
    }
    
    /**
     * Considers this land if it is greater than sea level.
     * @param worldX the X coordinate of location in world.
     * @param worldZ the Z coordinate of location in world.
     * @return
     */
    public boolean isLand(int worldX, int worldZ) {
        if (getWorldBlockValue(worldX, worldZ) >= 0) {
            return true;
        }
        return false;
    }

    /**
     * Calculates if this is the edge of the island by checking if it's surrounding blocks are part of this island.
     * @param worldX The world X coordinate.
     * @param worldZ The world Z coordinate.
     * @return true if it is the edge of the island and false if it isn't.
     */
    public boolean isEdgeOfIsland(int worldX, int worldZ) {
        return isIsland(worldX, worldZ) &&
        !(isSameIsland(worldX, worldZ, worldX + 1, worldZ) && 
        isSameIsland(worldX, worldZ, worldX - 1, worldZ) && 
        isSameIsland(worldX, worldZ, worldX , worldZ + 1) && 
        isSameIsland(worldX, worldZ, worldX, worldZ - 1));
    }

    /**
     * These coordinates are considered to be the shore if:
     * It is a part of an island,
     * the island value is less than the shore factor.
     * @param worldX the X coordinate of location in world.
     * @param worldZ the Z coordinate of location in world.
     * @return
     */
    public boolean isShore(int worldX, int worldZ) {
        if (!isIsland(worldX, worldZ)) return false;
        if (isLand(worldX, worldZ) && getWorldBlockValue(worldX, worldZ) <= shoreFactor) {
            return true;
        }
        return false;
    }

    /**
     * Considers these coordinates to be part of an island if:
     * It is land, or if it is a shallow portion of land.
     * @param worldX the X coordinate of location in world.
     * @param worldZ the Y coordinate of location in world.
     * @return true if these coordinates represent an island.
     */
    public boolean isIsland(int worldX, int worldZ) {
        return isLand(worldX, worldZ) || isShallowPortion(worldX, worldZ);
    }

    /**
     * This block is considered a shallow portion of the island:
     * The block height is less than the sea level,
     * and that the block height is greater than the shallow portion.
     * If it is land, it is not shallow.
     * @param worldX the X coordinate of location in world.
     * @param worldZ the Z coordinate of location in world.
     * @return true if it is considered the shallow portion.
     */
    public boolean isShallowPortion(int worldX, int worldZ) {
        if (!isLand(worldX, worldZ) && getWorldBlockValue(worldX, worldZ) >= -shallowPortion) {
            return true;
        }
        return false;
    }


    /**
     * World block value will be 0 or positive if it is a part of an island.
     * If less than, it is considered under the sea.
     * Does not factor in a shallow depth.
     * The value is normalized to [0, 1].
     * @param worldX the x world coordinate to obtain this value for.
     * @param worldZ the z world coordinate to obtain this value for.
     * @return a value representing the island at the given point.
     */
    public double getWorldBlockValue(int worldX, int worldZ) {
        CacheValue<Double> cacheVal = blockValueCache.retrieveCache(new CoordinateIdentifier(worldX, worldZ));
        if (cacheVal.isEmpty()) {
            double portionSea = 1f - (this.islandBlockGenerationPercent / 100f);
            double shift = 1f - 2 * portionSea;
            double rawNoise = noiseGenerator.noise(worldX, worldZ, noiseFrequency, noiseAmplitude, true);
            double noise = 0;
            if (rawNoise < 0) {
                noise = ( - Math.pow(- rawNoise, exaggerationFactor) + shift);
            } else {
                noise = Math.pow(rawNoise, islandValueExaggerationFactor) + shift;
            }
            double maxNeg = -1 + shift;
            double maxPos = 1 + shift;
    
            double res = 0;
            if (noise < 0) {
                res = - noise / maxNeg;
            } else {
                res = noise / maxPos;
            }
            cacheVal.setValue(res);
        }

        return cacheVal.getValue();
    }

    /**
     * Using DFS, determines if two given blocks are from the same island.
     * Works by checking if a path can be drawn between these two.
     * @param firstBlockX The x coord of the first block.
     * @param firstBlockZ The z coord of the first block.
     * @param secondBlockX The X coord of the first block.
     * @param secondBlockZ The Z coord of the first block.
     * @return true if the two blocks are on the same island, and false otherwise.
     */
    public boolean isSameIsland(int firstBlockX, int firstBlockZ, int secondBlockX, int secondBlockZ) {
        if (!isIsland(firstBlockX, firstBlockZ)) return false;
        if (!isIsland(secondBlockX, secondBlockZ)) return false;
        dfs.setStartPosition(firstBlockX, firstBlockZ);
        dfs.setEndPosition(secondBlockX, secondBlockZ);
        boolean res = dfs.buildPathToEndNode();
        dfs.deleteTree();
        return res;
    }

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