using System;
using System.Collections.Generic;
using UnityEngine;

namespace InfiniteWorld.VoxelWorld
{
    public enum WorldPlacementMode : byte
    {
        GroundOnly,
        FlattenTerrain
    }

    [CreateAssetMenu(menuName = "Infinite World/World Prefab Collection", fileName = "WorldPrefabCollection")]
    public sealed class WorldPrefabCollection : ScriptableObject
    {
        [Tooltip("Maximum number of spawn points this collection may contribute to a single chunk.")]
        [Min(0)] public int maxPlacementsPerChunk = 2;
        [Tooltip("How many deterministic candidate positions are tested for each spawn slot before it is skipped.")]
        [Min(1)] public int attemptsPerPlacement = 8;
        [Tooltip("How many cells near the chunk border are reserved so large prefabs do not clip outside the chunk.")]
        [Min(0)] public int chunkEdgePadding = 1;
        public List<WorldPrefabEntry> entries = new List<WorldPrefabEntry>();

        public IReadOnlyList<WorldPrefabEntry> Entries => entries;
    }

    [Serializable]
    public sealed class WorldPrefabEntry
    {
        [Tooltip("Stable logical identifier for this entry. Use it later for save data, analytics, or seeded config generation.")]
        public string id = "entry";

        [Tooltip("Prefab that will be instantiated when this entry wins placement generation.")]
        public GameObject prefab;

        [Tooltip("Relative weight inside the collection. Higher values make this entry more likely to be selected compared to others.")]
        [Min(0f)] public float weight = 1f;

        [Tooltip("Percent chance from 0 to 100 that the selected entry will actually spawn in its slot after being chosen by weight.")]
        [Range(0f, 100f)] public float spawnChancePercent = 100f;

        [Tooltip("GroundOnly requires an already flat area. FlattenTerrain carves the area to ground level and ensures an approach path.")]
        public WorldPlacementMode placementMode;

        [Tooltip("Required placement size in world cells. Rotations may swap X and Y if enabled.")]
        public Vector2Int footprint = Vector2Int.one;

        [Tooltip("Extra reserved cells around the footprint so other generated placements do not overlap too closely.")]
        [Min(0)] public int clearance;

        [Tooltip("Extra cells around the footprint that will also be flattened when using FlattenTerrain.")]
        [Min(0)] public int flattenPadding = 1;

        [Tooltip("Maximum search radius in cells for finding existing ground and cutting an access corridor to the flattened area.")]
        [Min(1)] public int flattenSearchRadius = 6;

        [Tooltip("Allows deterministic 90-degree rotations so the same prefab can fit in more layout variations.")]
        public bool allowRotations = true;
    }

    internal sealed class WorldPrefabCollectionRuntime
    {
        public WorldPrefabCollectionRuntime(int sourceIndex, int maxPlacementsPerChunk, int attemptsPerPlacement, int chunkEdgePadding, IReadOnlyList<WorldPrefabEntryRuntime> entries)
        {
            SourceIndex = sourceIndex;
            MaxPlacementsPerChunk = maxPlacementsPerChunk;
            AttemptsPerPlacement = attemptsPerPlacement;
            ChunkEdgePadding = chunkEdgePadding;
            Entries = entries ?? Array.Empty<WorldPrefabEntryRuntime>();
        }

        public int SourceIndex { get; }
        public int MaxPlacementsPerChunk { get; }
        public int AttemptsPerPlacement { get; }
        public int ChunkEdgePadding { get; }
        public IReadOnlyList<WorldPrefabEntryRuntime> Entries { get; }
    }

    internal sealed class WorldPrefabEntryRuntime
    {
        public WorldPrefabEntryRuntime(int sourceIndex, string id, float weight, float spawnChancePercent, WorldPlacementMode placementMode, Vector2Int footprint, int clearance, int flattenPadding, int flattenSearchRadius, bool allowRotations, bool hasPrefab)
        {
            SourceIndex = sourceIndex;
            Id = string.IsNullOrWhiteSpace(id) ? $"entry_{sourceIndex}" : id;
            Weight = Mathf.Max(0f, weight);
            SpawnChancePercent = Mathf.Clamp(spawnChancePercent, 0f, 100f);
            PlacementMode = placementMode;
            Footprint = new Vector2Int(Mathf.Max(1, footprint.x), Mathf.Max(1, footprint.y));
            Clearance = Mathf.Max(0, clearance);
            FlattenPadding = Mathf.Max(0, flattenPadding);
            FlattenSearchRadius = Mathf.Max(1, flattenSearchRadius);
            AllowRotations = allowRotations;
            HasPrefab = hasPrefab;
        }

        public int SourceIndex { get; }
        public string Id { get; }
        public float Weight { get; }
        public float SpawnChancePercent { get; }
        public WorldPlacementMode PlacementMode { get; }
        public Vector2Int Footprint { get; }
        public int Clearance { get; }
        public int FlattenPadding { get; }
        public int FlattenSearchRadius { get; }
        public bool AllowRotations { get; }
        public bool HasPrefab { get; }
    }

    internal static class WorldPlacementValidation
    {
        public static bool TryGetCollectionBlockerReason(WorldPrefabCollection collection, out string reason)
        {
            if (collection == null)
            {
                reason = "collection reference is missing.";
                return true;
            }

            if (collection.maxPlacementsPerChunk <= 0)
            {
                reason = "max placements per chunk must be greater than 0.";
                return true;
            }

            if (collection.attemptsPerPlacement < 1)
            {
                reason = "attempts per placement must be at least 1.";
                return true;
            }

            if (collection.chunkEdgePadding < 0)
            {
                reason = "chunk edge padding cannot be negative.";
                return true;
            }

            if (collection.entries == null || collection.entries.Count == 0)
            {
                reason = "collection has no entries configured.";
                return true;
            }

            reason = null;
            return false;
        }

        public static bool TryGetEntryBlockerReason(WorldPrefabEntry entry, HashSet<string> usedIds, out string reason)
        {
            if (entry == null)
            {
                reason = "entry reference is missing.";
                return true;
            }

            if (string.IsNullOrWhiteSpace(entry.id))
            {
                reason = "id is empty. A stable id is required for deterministic spawns and save data.";
                return true;
            }

            if (usedIds != null && !usedIds.Add(entry.id))
            {
                reason = $"duplicate id '{entry.id}' in the same collection.";
                return true;
            }

            if (entry.prefab == null)
            {
                reason = "prefab is missing.";
                return true;
            }

            if (entry.weight <= 0f)
            {
                reason = "weight must be greater than 0.";
                return true;
            }

            if (entry.spawnChancePercent <= 0f)
            {
                reason = "spawn chance must be greater than 0%.";
                return true;
            }

            if (entry.footprint.x <= 0 || entry.footprint.y <= 0)
            {
                reason = "footprint must be at least 1x1.";
                return true;
            }

            if (entry.clearance < 0)
            {
                reason = "clearance cannot be negative.";
                return true;
            }

            if (entry.flattenPadding < 0)
            {
                reason = "flatten padding cannot be negative.";
                return true;
            }

            if (entry.placementMode == WorldPlacementMode.FlattenTerrain && entry.flattenSearchRadius < 1)
            {
                reason = "flatten search radius must be at least 1 for FlattenTerrain mode.";
                return true;
            }

            reason = null;
            return false;
        }
    }

    public sealed class WorldTerrainPatch
    {
        private readonly List<Vector2Int> flattenedCells;

        public WorldTerrainPatch(IReadOnlyList<Vector2Int> cells)
        {
            flattenedCells = cells != null ? new List<Vector2Int>(cells) : new List<Vector2Int>();
        }

        public IReadOnlyList<Vector2Int> FlattenedCells => flattenedCells;
    }

    public readonly struct WorldSpawnPoint
    {
        public WorldSpawnPoint(long spawnId, Vector2Int chunkCoord, int spawnOrdinalInChunk, int collectionIndex, int entryIndex, string entryId, Vector3 position, Quaternion rotation)
        {
            SpawnId = spawnId;
            ChunkCoord = chunkCoord;
            SpawnOrdinalInChunk = spawnOrdinalInChunk;
            CollectionIndex = collectionIndex;
            EntryIndex = entryIndex;
            EntryId = entryId;
            Position = position;
            Rotation = rotation;
        }

        public long SpawnId { get; }
        public Vector2Int ChunkCoord { get; }
        public int SpawnOrdinalInChunk { get; }
        public int CollectionIndex { get; }
        public int EntryIndex { get; }
        public string EntryId { get; }
        public Vector3 Position { get; }
        public Quaternion Rotation { get; }
    }

    public sealed class WorldChunkPlacementPlan
    {
        private static readonly IReadOnlyList<WorldSpawnPoint> EmptySpawnPoints = Array.Empty<WorldSpawnPoint>();
        private static readonly IReadOnlyList<WorldTerrainPatch> EmptyTerrainPatches = Array.Empty<WorldTerrainPatch>();

        private readonly IReadOnlyList<WorldSpawnPoint> spawnPoints;
        private readonly IReadOnlyList<WorldTerrainPatch> terrainPatches;
        private readonly HashSet<Vector2Int> flattenedCells;

        public static WorldChunkPlacementPlan Empty { get; } = new WorldChunkPlacementPlan(EmptySpawnPoints, EmptyTerrainPatches, null);

        public WorldChunkPlacementPlan(IReadOnlyList<WorldSpawnPoint> spawnPoints, IReadOnlyList<WorldTerrainPatch> terrainPatches, HashSet<Vector2Int> flattenedCells)
        {
            this.spawnPoints = spawnPoints ?? EmptySpawnPoints;
            this.terrainPatches = terrainPatches ?? EmptyTerrainPatches;
            this.flattenedCells = flattenedCells;
        }

        public IReadOnlyList<WorldSpawnPoint> SpawnPoints => spawnPoints;
        public IReadOnlyList<WorldTerrainPatch> TerrainPatches => terrainPatches;
        public bool HasFlattening => flattenedCells != null && flattenedCells.Count > 0;

        public int GetFinalHeight(Vector2Int worldCell, int baseHeight)
        {
            return flattenedCells != null && flattenedCells.Contains(worldCell) ? 0 : baseHeight;
        }
    }
}
