package ca.recrown.islandsurvivalcraft.world;

import java.util.Random;

import org.bukkit.util.noise.SimplexOctaveGenerator;

import ca.recrown.islandsurvivalcraft.utilities.caching.Cache;
import ca.recrown.islandsurvivalcraft.utilities.datatypes.Point2;
import ca.recrown.islandsurvivalcraft.utilities.pathfinding.CoordinateValidatable;
import ca.recrown.islandsurvivalcraft.utilities.pathfinding.DepthFirstSearch;

public class IslandWorldMap implements CoordinateValidatable {
    private final Cache<Point2, Double> blockValueCache;

    private final SimplexOctaveGenerator noiseGenerator;
    private final int NOISE_OCTAVES = 4;
    private final float ISLAND_PERCENT = 0.36f;
    private final double NOISE_FREQ = 1.78D;
    private final double NOISE_AMP = 0.47D;
    private final float SHORE_PORTION = 0.095f;
    private final float SHALLOW_PORTION = 0.07f;
    private final double SCALE = 0.005D;
    private final double DEEP_OCEAN_PORTION = 0.6d;
    private final DepthFirstSearch dfs;

    public IslandWorldMap(Random random) {
        dfs = new DepthFirstSearch(this);
        this.noiseGenerator = new SimplexOctaveGenerator(random, NOISE_OCTAVES);
        noiseGenerator.setScale(SCALE);
        this.blockValueCache = new Cache<>(131072);
    }
    
    /**
     * Considers this land if the world block value is greater than or equal to 0.
     * @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 (getWorldValue(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) && getWorldValue(worldX, worldZ) <= SHORE_PORTION) {
            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) && getWorldValue(worldX, worldZ) >= -SHALLOW_PORTION) {
            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 [-1, 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 getWorldValue(int worldX, int worldZ) {
        Point2 p = new Point2(worldX, worldZ);

        Double res = blockValueCache.get(p);
        if (res == null) {
            double shift = 1f - 2f * ISLAND_PERCENT;
            double rawNoise = noiseGenerator.noise(worldX, worldZ, NOISE_FREQ, NOISE_AMP, true) - shift;
            double maxNeg = -1 - shift;
            double maxPos = 1 - shift;
            
            if (rawNoise < 0) {
                res = - rawNoise / maxNeg;
            } else {
                res = rawNoise / maxPos;
            }
            blockValueCache.set(p, res);
            return res;
        }

        return res;
    }

    /**
     * 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;
    }

    public boolean isDeepOcean(int worldX, int worldZ) {
        return getWorldValue(worldX, worldZ) <= -DEEP_OCEAN_PORTION;
    }

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