#nullable enable
using System.Collections.Generic;
using System.Threading.Tasks;
using AdvChkSys.Chunk;
using AdvChkSys.Events;
using AdvChkSys.Interfaces;
using AdvChkSys.Resources;
using AdvChkSys.Constraints;
using AdvChkSys.Util;
using System;

namespace AdvChkSys.Manager
{
    /// <summary>
    /// Manages 2D chunks in memory. Handles loading, unloading, and access.
    /// </summary>
    public class ChunkManager2D<T> : IChunkManager
    {
        private readonly LRUCache<(int, int), Chunk2D<T>> _chunks;
        private readonly WorldConstraints? _constraints;
        private readonly int _capacity;
        private readonly Dictionary<(int, int), Task<Chunk2D<T>>> _loadingChunks = new();
        private readonly object _lock = new();

        /// <summary>
        /// Delegate for custom air check logic
        /// </summary>
        private Func<int, int, int, int, bool>? _airCheckDelegate;

        /// <summary>
        /// World generator for determining empty regions
        /// </summary>
        private IWorldGenerator? _worldGenerator;

        /// <summary>
        /// Data provider for determining empty regions
        /// </summary>
        private IDataProvider? _dataProvider;

        /// <summary>
        /// Height map for determining sky chunks
        /// </summary>
        private IHeightMap? _heightMap;

        /// <summary>
        /// Initializes a new instance of the 2D chunk manager.
        /// </summary>
        /// <param name="constraints">Optional world constraints</param>
        /// <param name="capacity">Maximum number of chunks to keep in memory (0 for auto-calculation)</param>
        /// <param name="chunkWidth">Default chunk width in cells</param>
        /// <param name="chunkHeight">Default chunk height in cells</param>
        public ChunkManager2D(WorldConstraints? constraints = null, int capacity = 0, int chunkWidth = 32, int chunkHeight = 32)
        {
            _constraints = constraints;
            if (capacity <= 0)
            {
                // Use dynamic calculation
                int bytesPerChunk = chunkWidth * chunkHeight * System.Runtime.InteropServices.Marshal.SizeOf<T>();
                ulong availableBytes = MemoryHelper.GetAvailableMemoryBytes();
                _capacity = (int)(availableBytes * 0.8 / (ulong)bytesPerChunk);
            }
            else
            {
                _capacity = capacity;
            }
            _chunks = new LRUCache<(int, int), Chunk2D<T>>(_capacity);
        }

        /// <summary>
        /// Returns true if the chunk at (x, y) is loaded.
        /// </summary>
        public bool IsChunkLoaded(int x, int y) => _chunks.TryGet((x, y), out _);

        /// <summary>
        /// Gets the chunk at (x, y) if loaded, or null if not.
        /// </summary>
        public Chunk2D<T>? GetChunk(int x, int y)
        {
            _chunks.TryGet((x, y), out var chunk);
            return chunk;
        }

        /// <summary>
        /// Loads or creates a chunk at (x, y) with the given size.
        /// Returns the loaded or newly created chunk.
        /// </summary>
        public Chunk2D<T> LoadOrCreateChunk(int x, int y, int width, int height)
        {
            if (_constraints != null)
            {
                if (!_constraints.IsWithinBounds(x, y))
                    throw new InvalidOperationException("Chunk coordinates out of bounds.");
                if (!_constraints.IsWithinChunkLimit(_chunks.Count + 1))
                    throw new InvalidOperationException("Chunk limit exceeded.");
            }

            if (!_chunks.TryGet((x, y), out var chunk))
            {
                // If this region is all air, use the singleton
                if (ShouldBeAllAir(x, y, width, height))
                    chunk = Chunk2D<T>.AllAir(x, y, width, height);
                else
                    chunk = new Chunk2D<T>(x, y, width, height);

                _chunks.Add((x, y), chunk, OnChunkEvicted);
                ChunkResourceManager.AllocateChunk(chunk);
                ChunkEvents.OnChunkLoaded(chunk);
            }
            return chunk;
        }

        private void OnChunkEvicted((int, int) key, Chunk2D<T> chunk)
        {
            if (chunk == null) return;

            var arr = chunk.GetDataArray();
            if (!chunk.IsAllAir && arr != null)
            {
                Chunk2D<T>.ReturnArray(arr);
            }
            ChunkResourceManager.ReleaseChunk(chunk);
            ChunkEvents.OnChunkUnloaded(chunk);
        }

        /// <summary>
        /// Determines if a region should be represented as an all-air chunk.
        /// </summary>
        private bool ShouldBeAllAir(int x, int y, int width, int height)
        {
            // Check if we have a custom air check delegate
            if (_airCheckDelegate != null)
                return _airCheckDelegate(x, y, width, height);

            // Check if we have a world generator
            if (_worldGenerator != null)
                return _worldGenerator.IsRegionEmpty(x, y, width, height);

            // Check if we have a data provider
            if (_dataProvider != null)
                return _dataProvider.IsEmptyRegion(x, y, width, height);

            // Check height-based air (for sky chunks)
            if (_heightMap != null)
                return y > _heightMap.GetMaxHeight(x, x + width);

            // Default to non-air to ensure data integrity
            return false;
        }

        /// <summary>
        /// Loads or creates a chunk at (x, y) with the given size asynchronously.
        /// Returns the loaded or newly created chunk.
        /// </summary>
        public async Task<Chunk2D<T>> LoadOrCreateChunkAsync(int x, int y, int width, int height)
        {
            var key = (x, y);
            Task<Chunk2D<T>>? task = null;

            // First check if we already have a task
            lock (_lock)
            {
                if (_loadingChunks.TryGetValue(key, out task))
                {
                    // We found an existing task, no need to create a new one
                }
            }

            // If we found a task, await it outside the lock
            if (task != null)
            {
                return await task;
            }

            // Otherwise, create a new task outside the lock
            task = Task.Run(() => LoadOrCreateChunk(x, y, width, height));

            // Register the task
            lock (_lock)
            {
                // Check again in case another thread created the task while we were creating ours
                if (_loadingChunks.TryGetValue(key, out var existingTask))
                {
                    // Use the existing task instead
                    task = existingTask;
                }
                else
                {
                    // Register our task
                    _loadingChunks[key] = task;
                }
            }

            try
            {
                return await task;
            }
            finally
            {
                lock (_lock)
                {
                    _loadingChunks.Remove(key);
                }
            }
        }

        /// <summary>
        /// Unloads (removes) the chunk at (x, y) if loaded.
        /// </summary>
        public bool UnloadChunk(int x, int y)
        {
            if (_chunks.Remove((x, y), out var chunk))
            {
                if (chunk != null) // null check
                {
                    var arr = chunk.GetDataArray();
                    if (!chunk.IsAllAir && arr != null)
                    {
                        Chunk2D<T>.ReturnArray(arr);
                    }
                    ChunkResourceManager.ReleaseChunk(chunk);
                    ChunkEvents.OnChunkUnloaded(chunk);
                }
                return true;
            }
            return false;
        }

        /// <summary>
        /// Unloads (removes) the chunk at (x, y) if loaded asynchronously.
        /// </summary>
        public async Task<bool> UnloadChunkAsync(int x, int y)
        {
            return await Task.Run(() => UnloadChunk(x, y));
        }

        /// <summary>
        /// Enumerates all loaded chunks.
        /// </summary>
        public IEnumerable<Chunk2D<T>> GetAllChunks() => _chunks.Values;

        // IChunkManager interface compatibility
        bool IChunkManager.IsChunkLoaded(int x, int y) => IsChunkLoaded(x, y);
        IChunk? IChunkManager.GetChunk(int x, int y) => GetChunk(x, y);
        IChunk IChunkManager.LoadOrCreateChunk(int x, int y, int width, int height) => LoadOrCreateChunk(x, y, width, height);
        bool IChunkManager.UnloadChunk(int x, int y) => UnloadChunk(x, y);
        IEnumerable<IChunk> IChunkManager.GetAllChunks() => _chunks.Values;

        /// <summary>
        /// Sets a custom delegate for determining if a region should be all air.
        /// </summary>
        public void SetAirCheckDelegate(Func<int, int, int, int, bool> airCheckDelegate)
        {
            _airCheckDelegate = airCheckDelegate;
        }

        /// <summary>
        /// Sets a world generator for determining empty regions.
        /// </summary>
        public void SetWorldGenerator(IWorldGenerator worldGenerator)
        {
            _worldGenerator = worldGenerator;
        }

        /// <summary>
        /// Sets a data provider for determining empty regions.
        /// </summary>
        public void SetDataProvider(IDataProvider dataProvider)
        {
            _dataProvider = dataProvider;
        }

        /// <summary>
        /// Sets a height map for determining sky chunks.
        /// </summary>
        public void SetHeightMap(IHeightMap heightMap)
        {
            _heightMap = heightMap;
        }
    }

    /// <summary>
    /// Interface for world generators that can determine if regions are empty.
    /// </summary>
    public interface IWorldGenerator
    {
        /// <summary>
        /// Determines if a region is empty (all air).
        /// </summary>
        bool IsRegionEmpty(int x, int y, int width, int height);
    }

    /// <summary>
    /// Interface for data providers that can determine if regions are empty.
    /// </summary>
    public interface IDataProvider
    {
        /// <summary>
        /// Determines if a region is empty (all air).
        /// </summary>
        bool IsEmptyRegion(int x, int y, int width, int height);
    }

    /// <summary>
    /// Interface for height maps that can determine the maximum height at a position.
    /// </summary>
    public interface IHeightMap
    {
        /// <summary>
        /// Gets the maximum height at a position range.
        /// </summary>
        int GetMaxHeight(int startX, int endX);
    }
}