using System.Collections.Generic;
using UnityEngine;

namespace InfiniteWorld.VoxelWorld
{
    internal static class WorldSpawnPlanner
    {
        public static WorldChunkPlacementPlan PlanChunk(
            int worldSeed,
            Vector2Int chunkCoord,
            int chunkSize,
            IReadOnlyList<WorldPrefabCollectionRuntime> collections,
            System.Func<Vector2Int, int> getBaseHeight)
        {
            if (collections == null || collections.Count == 0)
            {
                return WorldChunkPlacementPlan.Empty;
            }

            List<WorldSpawnPoint> spawnPoints = new List<WorldSpawnPoint>();
            List<WorldTerrainPatch> terrainPatches = new List<WorldTerrainPatch>();
            HashSet<Vector2Int> flattenedCells = new HashSet<Vector2Int>();
            HashSet<Vector2Int> occupiedCells = new HashSet<Vector2Int>();
            int spawnOrdinal = 0;

            for (int collectionIndex = 0; collectionIndex < collections.Count; collectionIndex++)
            {
                WorldPrefabCollectionRuntime collection = collections[collectionIndex];
                if (collection == null || collection.Entries == null || collection.Entries.Count == 0 || collection.MaxPlacementsPerChunk <= 0)
                {
                    continue;
                }

                for (int placementIndex = 0; placementIndex < collection.MaxPlacementsPerChunk; placementIndex++)
                {
                    if (!TryPickEntry(collection, worldSeed, chunkCoord, collectionIndex, placementIndex, out int entryIndex, out WorldPrefabEntryRuntime entry))
                    {
                        continue;
                    }

                    if (!PassesSpawnChance(entry, worldSeed, chunkCoord, collectionIndex, placementIndex))
                    {
                        continue;
                    }

                    if (!TryPlanPlacement(worldSeed, chunkCoord, chunkSize, collection, collectionIndex, placementIndex, entryIndex, entry, spawnOrdinal, occupiedCells, flattenedCells, getBaseHeight, out WorldSpawnPoint spawnPoint, out WorldTerrainPatch patch, out List<Vector2Int> reservedCells))
                    {
                        continue;
                    }

                    spawnPoints.Add(spawnPoint);
                    if (patch != null)
                    {
                        terrainPatches.Add(patch);
                    }

                    for (int i = 0; i < reservedCells.Count; i++)
                    {
                        occupiedCells.Add(reservedCells[i]);
                    }

                    spawnOrdinal++;
                }
            }

            if (spawnPoints.Count == 0 && terrainPatches.Count == 0)
            {
                return WorldChunkPlacementPlan.Empty;
            }

            return new WorldChunkPlacementPlan(spawnPoints, terrainPatches, flattenedCells.Count > 0 ? flattenedCells : null);
        }

        private static bool TryPickEntry(WorldPrefabCollectionRuntime collection, int worldSeed, Vector2Int chunkCoord, int collectionIndex, int placementIndex, out int entryIndex, out WorldPrefabEntryRuntime entry)
        {
            float totalWeight = 0f;
            for (int i = 0; i < collection.Entries.Count; i++)
            {
                WorldPrefabEntryRuntime candidate = collection.Entries[i];
                if (candidate == null || !candidate.HasPrefab || candidate.Weight <= 0f)
                {
                    continue;
                }

                totalWeight += candidate.Weight;
            }

            if (totalWeight <= 0f)
            {
                entryIndex = -1;
                entry = null;
                return false;
            }

            float pick = WorldSeedUtility.Value01(WorldSeedUtility.Hash(worldSeed, chunkCoord.x, chunkCoord.y, collectionIndex, placementIndex, 1)) * totalWeight;
            float accumulated = 0f;
            for (int i = 0; i < collection.Entries.Count; i++)
            {
                WorldPrefabEntryRuntime candidate = collection.Entries[i];
                if (candidate == null || !candidate.HasPrefab || candidate.Weight <= 0f)
                {
                    continue;
                }

                accumulated += candidate.Weight;
                if (pick <= accumulated)
                {
                    entryIndex = i;
                    entry = candidate;
                    return true;
                }
            }

            for (int i = collection.Entries.Count - 1; i >= 0; i--)
            {
                WorldPrefabEntryRuntime candidate = collection.Entries[i];
                if (candidate == null || !candidate.HasPrefab || candidate.Weight <= 0f)
                {
                    continue;
                }

                entryIndex = i;
                entry = candidate;
                return true;
            }

            entryIndex = -1;
            entry = null;
            return false;
        }

        private static bool PassesSpawnChance(WorldPrefabEntryRuntime entry, int worldSeed, Vector2Int chunkCoord, int collectionIndex, int placementIndex)
        {
            if (entry == null)
            {
                return false;
            }

            if (entry.SpawnChancePercent <= 0f)
            {
                return false;
            }

            if (entry.SpawnChancePercent >= 100f)
            {
                return true;
            }

            float roll = WorldSeedUtility.Value01(WorldSeedUtility.Hash(worldSeed, chunkCoord.x, chunkCoord.y, collectionIndex, placementIndex, 11)) * 100f;
            return roll <= entry.SpawnChancePercent;
        }

        private static bool TryPlanPlacement(
            int worldSeed,
            Vector2Int chunkCoord,
            int chunkSize,
            WorldPrefabCollectionRuntime collection,
            int collectionIndex,
            int placementIndex,
            int entryIndex,
            WorldPrefabEntryRuntime entry,
            int spawnOrdinal,
            HashSet<Vector2Int> occupiedCells,
            HashSet<Vector2Int> flattenedCells,
            System.Func<Vector2Int, int> getBaseHeight,
            out WorldSpawnPoint spawnPoint,
            out WorldTerrainPatch patch,
            out List<Vector2Int> reservedCells)
        {
            reservedCells = null;
            spawnPoint = default;
            patch = null;

            Vector2Int chunkOrigin = new Vector2Int(chunkCoord.x * chunkSize, chunkCoord.y * chunkSize);
            int attempts = Mathf.Max(1, collection.AttemptsPerPlacement);
            for (int attempt = 0; attempt < attempts; attempt++)
            {
                int rotationSteps = entry.AllowRotations ? WorldSeedUtility.Range(WorldSeedUtility.Hash(worldSeed, chunkCoord.x, chunkCoord.y, collectionIndex, placementIndex, attempt, 2), 0, 4) : 0;
                Vector2Int footprint = GetFootprint(entry.Footprint, rotationSteps);
                if (footprint.x <= 0 || footprint.y <= 0)
                {
                    continue;
                }

                int localMinX = collection.ChunkEdgePadding;
                int localMinZ = collection.ChunkEdgePadding;
                int localMaxX = chunkSize - collection.ChunkEdgePadding - footprint.x;
                int localMaxZ = chunkSize - collection.ChunkEdgePadding - footprint.y;
                if (localMaxX < localMinX || localMaxZ < localMinZ)
                {
                    continue;
                }

                int localX = WorldSeedUtility.Range(WorldSeedUtility.Hash(worldSeed, chunkCoord.x, chunkCoord.y, collectionIndex, placementIndex, attempt, 3), localMinX, localMaxX + 1);
                int localZ = WorldSeedUtility.Range(WorldSeedUtility.Hash(worldSeed, chunkCoord.x, chunkCoord.y, collectionIndex, placementIndex, attempt, 4), localMinZ, localMaxZ + 1);
                RectInt footprintRect = new RectInt(localX, localZ, footprint.x, footprint.y);
                RectInt reservedRect = ExpandRect(footprintRect, Mathf.Max(0, entry.Clearance), chunkSize);

                if (IntersectsOccupied(chunkOrigin, reservedRect, occupiedCells))
                {
                    continue;
                }

                if (entry.PlacementMode == WorldPlacementMode.GroundOnly)
                {
                    if (!IsGroundArea(chunkOrigin, reservedRect, flattenedCells, getBaseHeight))
                    {
                        continue;
                    }

                    reservedCells = CollectCells(chunkOrigin, reservedRect);
                }
                else
                {
                    if (!TryCreateFlattenPatch(chunkOrigin, chunkSize, footprintRect, reservedRect, entry, occupiedCells, flattenedCells, getBaseHeight, out patch, out reservedCells))
                    {
                        patch = null;
                        continue;
                    }
                }

                float rotationY = rotationSteps * 90f;
                Vector3 position = new Vector3(
                    chunkOrigin.x + footprintRect.x + footprintRect.width * 0.5f,
                    0f,
                    chunkOrigin.y + footprintRect.y + footprintRect.height * 0.5f);
                long spawnId = WorldSeedUtility.ToStableId(
                    WorldSeedUtility.Hash(worldSeed, chunkCoord.x, chunkCoord.y, collectionIndex),
                    WorldSeedUtility.Hash(entryIndex, placementIndex, spawnOrdinal, footprintRect.x, footprintRect.y));
                spawnPoint = new WorldSpawnPoint(spawnId, chunkCoord, spawnOrdinal, collectionIndex, entryIndex, entry.Id, position, Quaternion.Euler(0f, rotationY, 0f));
                return true;
            }

            return false;
        }

        private static bool TryCreateFlattenPatch(
            Vector2Int chunkOrigin,
            int chunkSize,
            RectInt footprintRect,
            RectInt reservedRect,
            WorldPrefabEntryRuntime entry,
            HashSet<Vector2Int> occupiedCells,
            HashSet<Vector2Int> flattenedCells,
            System.Func<Vector2Int, int> getBaseHeight,
            out WorldTerrainPatch patch,
            out List<Vector2Int> reservedCells)
        {
            int flattenPadding = Mathf.Max(entry.Clearance, entry.FlattenPadding);
            RectInt flattenRect = ExpandRect(footprintRect, flattenPadding, chunkSize);
            List<Vector2Int> flattened = CollectCells(chunkOrigin, flattenRect);
            if (IntersectsOccupied(flattened, occupiedCells))
            {
                patch = null;
                reservedCells = null;
                return false;
            }

            List<Vector2Int> corridor = TryBuildAccessCorridor(chunkOrigin, chunkSize, flattenRect, Mathf.Max(1, entry.FlattenSearchRadius), occupiedCells, flattenedCells, getBaseHeight);
            if (corridor == null)
            {
                patch = null;
                reservedCells = null;
                return false;
            }

            for (int i = 0; i < corridor.Count; i++)
            {
                flattened.Add(corridor[i]);
            }

            HashSet<Vector2Int> uniqueFlattened = new HashSet<Vector2Int>(flattened);
            foreach (Vector2Int worldCell in uniqueFlattened)
            {
                flattenedCells.Add(worldCell);
            }

            patch = new WorldTerrainPatch(flattened);
            reservedCells = CollectCells(chunkOrigin, reservedRect);
            reservedCells.AddRange(corridor);
            return true;
        }

        private static List<Vector2Int> TryBuildAccessCorridor(
            Vector2Int chunkOrigin,
            int chunkSize,
            RectInt flattenRect,
            int searchRadius,
            HashSet<Vector2Int> occupiedCells,
            HashSet<Vector2Int> flattenedCells,
            System.Func<Vector2Int, int> getBaseHeight)
        {
            if (HasAccessibleGroundNeighbor(chunkOrigin, flattenRect, flattenedCells, getBaseHeight))
            {
                return new List<Vector2Int>();
            }

            Vector2Int center = new Vector2Int(flattenRect.x + flattenRect.width / 2, flattenRect.y + flattenRect.height / 2);
            Vector2Int? target = FindNearestGroundCell(chunkOrigin, chunkSize, center, flattenRect, searchRadius, occupiedCells, flattenedCells, getBaseHeight);
            if (!target.HasValue)
            {
                return null;
            }

            Vector2Int entryCell = new Vector2Int(
                Mathf.Clamp(target.Value.x, chunkOrigin.x + flattenRect.xMin, chunkOrigin.x + flattenRect.xMax - 1),
                Mathf.Clamp(target.Value.y, chunkOrigin.y + flattenRect.yMin, chunkOrigin.y + flattenRect.yMax - 1));

            List<Vector2Int> corridor = new List<Vector2Int>();
            Vector2Int cursor = target.Value;
            while (cursor.x != entryCell.x)
            {
                if (!occupiedCells.Contains(cursor))
                {
                    corridor.Add(cursor);
                }

                cursor.x += cursor.x < entryCell.x ? 1 : -1;
            }

            while (cursor.y != entryCell.y)
            {
                if (!occupiedCells.Contains(cursor))
                {
                    corridor.Add(cursor);
                }

                cursor.y += cursor.y < entryCell.y ? 1 : -1;
            }

            return corridor;
        }

        private static bool HasAccessibleGroundNeighbor(Vector2Int chunkOrigin, RectInt flattenRect, HashSet<Vector2Int> flattenedCells, System.Func<Vector2Int, int> getBaseHeight)
        {
            for (int z = flattenRect.yMin - 1; z <= flattenRect.yMax; z++)
            {
                Vector2Int left = new Vector2Int(chunkOrigin.x + flattenRect.xMin - 1, chunkOrigin.y + z);
                Vector2Int right = new Vector2Int(chunkOrigin.x + flattenRect.xMax, chunkOrigin.y + z);
                if (GetPlannedHeight(left, flattenedCells, getBaseHeight) == 0 || GetPlannedHeight(right, flattenedCells, getBaseHeight) == 0)
                {
                    return true;
                }
            }

            for (int x = flattenRect.xMin; x < flattenRect.xMax; x++)
            {
                Vector2Int bottom = new Vector2Int(chunkOrigin.x + x, chunkOrigin.y + flattenRect.yMin - 1);
                Vector2Int top = new Vector2Int(chunkOrigin.x + x, chunkOrigin.y + flattenRect.yMax);
                if (GetPlannedHeight(bottom, flattenedCells, getBaseHeight) == 0 || GetPlannedHeight(top, flattenedCells, getBaseHeight) == 0)
                {
                    return true;
                }
            }

            return false;
        }

        private static Vector2Int? FindNearestGroundCell(
            Vector2Int chunkOrigin,
            int chunkSize,
            Vector2Int localCenter,
            RectInt excludedRect,
            int searchRadius,
            HashSet<Vector2Int> occupiedCells,
            HashSet<Vector2Int> flattenedCells,
            System.Func<Vector2Int, int> getBaseHeight)
        {
            for (int radius = 1; radius <= searchRadius; radius++)
            {
                for (int dz = -radius; dz <= radius; dz++)
                {
                    for (int dx = -radius; dx <= radius; dx++)
                    {
                        if (Mathf.Max(Mathf.Abs(dx), Mathf.Abs(dz)) != radius)
                        {
                            continue;
                        }

                        int localX = localCenter.x + dx;
                        int localZ = localCenter.y + dz;
                        if (localX < 0 || localZ < 0 || localX >= chunkSize || localZ >= chunkSize)
                        {
                            continue;
                        }

                        if (excludedRect.Contains(new Vector2Int(localX, localZ)))
                        {
                            continue;
                        }

                        Vector2Int worldCell = new Vector2Int(chunkOrigin.x + localX, chunkOrigin.y + localZ);
                        if (occupiedCells.Contains(worldCell))
                        {
                            continue;
                        }

                        if (GetPlannedHeight(worldCell, flattenedCells, getBaseHeight) == 0)
                        {
                            return worldCell;
                        }
                    }
                }
            }

            return null;
        }

        private static int GetPlannedHeight(Vector2Int worldCell, HashSet<Vector2Int> flattenedCells, System.Func<Vector2Int, int> getBaseHeight)
        {
            return flattenedCells.Contains(worldCell) ? 0 : getBaseHeight(worldCell);
        }

        private static bool IsGroundArea(Vector2Int chunkOrigin, RectInt rect, HashSet<Vector2Int> flattenedCells, System.Func<Vector2Int, int> getBaseHeight)
        {
            for (int z = rect.yMin; z < rect.yMax; z++)
            {
                for (int x = rect.xMin; x < rect.xMax; x++)
                {
                    if (GetPlannedHeight(new Vector2Int(chunkOrigin.x + x, chunkOrigin.y + z), flattenedCells, getBaseHeight) > 0)
                    {
                        return false;
                    }
                }
            }

            return true;
        }

        private static bool IntersectsOccupied(Vector2Int chunkOrigin, RectInt rect, HashSet<Vector2Int> occupiedCells)
        {
            for (int z = rect.yMin; z < rect.yMax; z++)
            {
                for (int x = rect.xMin; x < rect.xMax; x++)
                {
                    if (occupiedCells.Contains(new Vector2Int(chunkOrigin.x + x, chunkOrigin.y + z)))
                    {
                        return true;
                    }
                }
            }

            return false;
        }

        private static bool IntersectsOccupied(List<Vector2Int> cells, HashSet<Vector2Int> occupiedCells)
        {
            for (int i = 0; i < cells.Count; i++)
            {
                if (occupiedCells.Contains(cells[i]))
                {
                    return true;
                }
            }

            return false;
        }

        private static List<Vector2Int> CollectCells(Vector2Int chunkOrigin, RectInt rect)
        {
            List<Vector2Int> result = new List<Vector2Int>(rect.width * rect.height);
            for (int z = rect.yMin; z < rect.yMax; z++)
            {
                for (int x = rect.xMin; x < rect.xMax; x++)
                {
                    result.Add(new Vector2Int(chunkOrigin.x + x, chunkOrigin.y + z));
                }
            }

            return result;
        }

        private static RectInt ExpandRect(RectInt rect, int amount, int chunkSize)
        {
            return new RectInt(
                Mathf.Max(0, rect.xMin - amount),
                Mathf.Max(0, rect.yMin - amount),
                Mathf.Min(chunkSize, rect.xMax + amount) - Mathf.Max(0, rect.xMin - amount),
                Mathf.Min(chunkSize, rect.yMax + amount) - Mathf.Max(0, rect.yMin - amount));
        }

        private static Vector2Int GetFootprint(Vector2Int footprint, int rotationSteps)
        {
            footprint.x = Mathf.Max(1, footprint.x);
            footprint.y = Mathf.Max(1, footprint.y);
            return rotationSteps % 2 == 0 ? footprint : new Vector2Int(footprint.y, footprint.x);
        }
    }
}
