/// <summary>
    /// <para>Notify the chunk loading. If is a parallel execution, is added to the ChunksLoaded list in order to be build at BuildParallelyLoadedChunks method.</para>
    /// <para>If the chunk is for map, it is added to ChunksForMap list.</para>
    /// <para>Otherwise, the chunk is build.</para>
    /// <para>IMPORTANT: If is a parallel execution, this method can't instanciate any child of GameObject.</para>
    /// </summary>
    /// <param name="chunk">Chunk instance.</param>
    /// <param name="isParellel">If the chunk is loading in parallel.</param>
    /// <param name="isForMap">If the chunk is loading for the terrain mapping.</param>
    protected virtual void ChunkLoaded(Chunk chunk, bool isParallel, bool isForMap)
    {
        if (isParallel)
        {
            Monitor.Enter(ChunksLoadingLock);
        }

        if (isForMap)
        {
            ChunksLoadingForMap.Remove(chunk.Position);
            ChunksForMap.Add(chunk.Position, chunk);
        }
        else if (isParallel)
        {
            ChunksLoading.Remove(chunk.Position);
            ChunksLoaded.Add(chunk.Position, chunk);
        }

        if (isParallel)
        {
            Monitor.Exit(ChunksLoadingLock);
        }
        else if (!isForMap)
        {
            BuildChunk(chunk);
            CurrentChunks.Add(chunk.Position, chunk);
        }
    }
    /// <summary>
    /// Equivalent to DynamicChunksUpdate but for terrain mapping. Loads the chunks required for the map and unloads the unrequired ones.
    /// </summary>
    /// <param name="bottomLeftPos">Bottom left corner of the map.</param>
    /// <param name="topRightPos">Top right corner of the map.</param>
    /// <param name="inMaxUpdateTime">Function from the mapping class that checks if the corresponding MaxUpdateTime is exceeded.</param>
    public virtual void UpdateChunksForMap(Vector2Int bottomLeftPos, Vector2Int topRightPos, System.Func <bool> inMaxUpdateTime)
    {
        Vector2Int bottomLeftChunkPos = TerrainPosToChunk(bottomLeftPos);
        Vector2Int topRightChunkPos   = TerrainPosToChunk(topRightPos);
        Vector2Int mapSize            = topRightChunkPos - bottomLeftChunkPos;

        // Destroy the don't required chunks
        List <Vector2Int> chunksToDestroy = new List <Vector2Int>();

        foreach (KeyValuePair <Vector2Int, Chunk> entry in ChunksForMap)
        {
            if (entry.Key.x < bottomLeftChunkPos.x || entry.Key.x > topRightChunkPos.x ||
                entry.Key.y < bottomLeftChunkPos.y || entry.Key.y > topRightChunkPos.y)
            {
                entry.Value.Destroy();
                chunksToDestroy.Add(entry.Key);
            }
        }

        foreach (Vector2Int pos in chunksToDestroy)
        {
            ChunksForMap.Remove(pos);
        }

        // Load the chunks required for map
        Vector2Int chunkPos = new Vector2Int();

        for (int x = 0; x <= mapSize.x; x++)
        {
            for (int y = 0; y <= mapSize.y; y++)
            {
                chunkPos.x = bottomLeftChunkPos.x + x;
                chunkPos.y = bottomLeftChunkPos.y + y;

                // If no loaded neither loading
                if (!CurrentChunks.ContainsKey(chunkPos) &&
                    !ChunksLoaded.ContainsKey(chunkPos) &&
                    !ChunksLoading.ContainsKey(chunkPos) &&
                    !ChunksForMap.ContainsKey(chunkPos) &&
                    !ChunksLoadingForMap.ContainsKey(chunkPos))
                {
                    LoadChunk(chunkPos, ParallelChunkLoading, true);
                }
            }
        }
    }
    /// <summary>
    /// Builds the chunks loaded parallely. For each chunk it uses the InMaxUpdateTime method, breaking the loop if the MaxUpdateTime is exceeded.
    /// </summary>
    protected virtual void BuildParallelyLoadedChunks()
    {
        if (ChunksLoaded.Count > 0)
        {
            lock ( ChunksLoadingLock )
            {
                Chunk             chunk;
                List <Vector2Int> chunksBuilt          = new List <Vector2Int>(ChunksLoaded.Count);
                float             loopStartTime        = Time.realtimeSinceStartup;
                float             numIterations        = 0;
                float             averageIterationTime = 0;
                foreach (KeyValuePair <Vector2Int, Chunk> entry in ChunksLoaded)
                {
                    if (InMaxUpdateTime(averageIterationTime))
                    {
                        chunk = entry.Value;
                        if (IsChunkRequired(chunk.Position))
                        {
                            BuildChunk(chunk);
                            CurrentChunks.Add(chunk.Position, chunk);
                        }
                        else
                        {
                            chunk.Destroy();
                        }

                        chunksBuilt.Add(entry.Key);

                        numIterations++;
                        averageIterationTime = (Time.realtimeSinceStartup - loopStartTime) / numIterations;
                    }
                    else
                    {
                        break;
                    }
                }

                // Delete chunks already built
                foreach (Vector2Int key in chunksBuilt)
                {
                    ChunksLoaded.Remove(key);
                }
            }
        }
    }
    /// <summary>
    /// <para>Checks the reference current chunk, that always has to be build.</para>
    /// <para>If is loading, waits for it to load and then build it.</para>
    /// <para>If it isn't loaded, loads it.</para>
    /// </summary>
    protected virtual void CheckReferenceCurrentChunk()
    {
        Monitor.Enter(ChunksLoadingLock);

        if (!CurrentChunks.ContainsKey(ReferenceChunkPos) && !ChunksLoaded.ContainsKey(ReferenceChunkPos))
        {
            bool isLoading = ChunksLoading.TryGetValue(ReferenceChunkPos, out Chunk referenceChunk);
            Monitor.Exit(ChunksLoadingLock);
            if (isLoading)
            {
                referenceChunk.ParallelTask.Wait();              // Wait parallel loading to end
                BuildParallelyLoadedChunks();                    // Built the chunk (and the others if are required)
            }
            else
            {
                LoadChunk(ReferenceChunkPos, true);                      // Ignore parallel because is impossible continue playing without this chunk
            }
        }
        else
        {
            Monitor.Exit(ChunksLoadingLock);
        }
    }
    public virtual Chunk GetChunk(Vector2Int terrainPos)
    {
        Vector2Int chunkPos = TerrainPosToChunk(terrainPos);

        return(CurrentChunks.ContainsKey(chunkPos) ? CurrentChunks[chunkPos] : null);
    }
    /// <summary>
    /// <para>Manages the chunks required and the current chunks using the reference position.</para>
    /// <para>If a chunk is required and not loaded, it loads it (or starts the load if ParallelChunkLoading is enabled).</para>
    /// <para>If a chunk is not required, destroy it.</para>
    /// <para>Moreover, when is loading the chunks required, it uses the InMaxUpdateTime method, breaking the loop if the MaxUpdateTime is exceeded.</para>
    /// </summary>
    protected virtual void DynamicChunksUpdate()
    {
        Dictionary <Vector2Int, object> chunkRequired = DynamicChunksRequired();        // Use a dictionary for faster searchs

        // Check chunks already created
        List <Vector2Int> chunksToDestroy = new List <Vector2Int>();

        foreach (KeyValuePair <Vector2Int, Chunk> entry in CurrentChunks)
        {
            // If already created, don't reload
            if (chunkRequired.ContainsKey(entry.Key))
            {
                chunkRequired.Remove(entry.Key);
            }
            // If don't required, unload
            else
            {
                chunksToDestroy.Add(entry.Key);
                entry.Value.Destroy();
            }
        }

        // Remove chunks don't required
        foreach (Vector2Int chunkPos in chunksToDestroy)
        {
            CurrentChunks.Remove(chunkPos);
        }

        if (InMaxUpdateTime())
        {
            lock ( ChunksLoadingLock )
            {
                // Ignore chunks that are loading
                foreach (KeyValuePair <Vector2Int, Chunk> entry in ChunksLoading)
                {
                    if (chunkRequired.ContainsKey(entry.Key))
                    {
                        chunkRequired.Remove(entry.Key);
                    }
                }

                // Ignore chunks that are already loaded
                foreach (KeyValuePair <Vector2Int, Chunk> entry in ChunksLoaded)
                {
                    if (chunkRequired.ContainsKey(entry.Key))
                    {
                        chunkRequired.Remove(entry.Key);
                    }
                }

                // Load the other chunks
                float loopStartTime        = Time.realtimeSinceStartup;
                float numIterations        = 0;
                float averageIterationTime = 0;
                foreach (KeyValuePair <Vector2Int, object> entry in chunkRequired)
                {
                    if (InMaxUpdateTime(averageIterationTime))
                    {
                        LoadChunk(entry.Key);

                        numIterations++;
                        averageIterationTime = (Time.realtimeSinceStartup - loopStartTime) / numIterations;
                    }
                    else
                    {
                        break;
                    }
                }
            }
        }
    }
    /// <summary>
    /// Destroys all the chunks of the terrain, including all GameObjects and ParallelTasks.
    /// </summary>
    public virtual void DestroyTerrain()
    {
        IsGenerated = false;

        foreach (Transform child in transform)
        {
            Destroy(child.gameObject);
        }

        if (CurrentChunks != null)
        {
            foreach (KeyValuePair <Vector2Int, Chunk> entry in CurrentChunks)
            {
                entry.Value.Destroy();
            }

            CurrentChunks.Clear();
        }

        if (ChunksForMap != null)
        {
            foreach (KeyValuePair <Vector2Int, Chunk> entry in ChunksForMap)
            {
                entry.Value.Destroy();
            }

            ChunksForMap.Clear();
        }

        lock ( ChunksLoadingLock )
        {
            if (ChunksLoading != null)
            {
                foreach (KeyValuePair <Vector2Int, Chunk> entry in ChunksLoading)
                {
                    entry.Value.Destroy();
                }

                ChunksLoading.Clear();
            }

            if (ChunksLoaded != null)
            {
                foreach (KeyValuePair <Vector2Int, Chunk> entry in ChunksLoaded)
                {
                    entry.Value.Destroy();
                }

                ChunksLoaded.Clear();
            }

            if (ChunksLoadingForMap != null)
            {
                foreach (KeyValuePair <Vector2Int, Chunk> entry in ChunksLoadingForMap)
                {
                    entry.Value.Destroy();
                }

                ChunksLoadingForMap.Clear();
            }
        }
    }