using System;
using System.Collections.Generic;
using System.Linq;
using Sirenix.OdinInspector;
using Sirenix.Utilities;
using UnityEngine;
using VOID.Generic.Dict;
using VOID.Generic.Util;
using VOID.ScriptableObjects.World;

namespace VOID.Generic.World
{
    public class WorldGeneratorManager : MonoBehaviour
    {
        public static WorldGeneratorManager current { get; private set; }
        
        [InlineEditor] public WorldGeneratorSettings worldSettings;

        // Temp data used for generation
        private readonly Dictionary<Vector2Int, Area> _mapArea = new();
        private readonly Dictionary<Vector2Int, ComplexArea> _mapComplexArea = new();
        private readonly Dictionary<Vector2Int, List<AreaConnection>> _mapOpenConnections = new();
        private readonly Dictionary<WorldAreaGeneratorSettings, int> _groupCount = new();
        private readonly Dictionary<Vector2Int, int> _distanceToArea = new();

        // Config
        [SerializeField] private Material wallMaterial;

        private void Awake()
        {
            current = this;
        }

        [Button]
        public void Generate(int minAreas = 150, int seed = 2137, bool debugWalls = false, bool debugDistance = false)
        {
            if (worldSettings == null) return;
            
            RandomUtil.InitState(seed);

            // Clear previous data
            Clear();

            GenerateStartingArea();
            GenerateAreas(minAreas);

            CreateComplexAreas();
            if (debugWalls) CreateWalls();
            if (debugDistance) CreateAreaDistanceText();
        }

        private void Clear()
        {
            _mapArea.Clear();
            _mapComplexArea.Clear();
            _mapOpenConnections.Clear();
            _distanceToArea.Clear();

            worldSettings.list.ForEach(areaSettings => _groupCount[areaSettings] = 0);
        }

        /// <summary>
        /// Initializes all prepared <see cref="ComplexArea"/>
        /// </summary>
        private void CreateComplexAreas()
        {
            var areasParentGameObject = new GameObject("areas parent");
            _mapComplexArea.Keys.ForEach(posIndex =>
            {
                var complexArea = _mapComplexArea[posIndex];
                var position = new Vector3(posIndex.x * Area.Size, 0, posIndex.y * Area.Size);
                var areaGameObject = Instantiate(complexArea, position, Quaternion.identity);
                areaGameObject.transform.parent = areasParentGameObject.transform;
            });
        }

        /// <summary>
        /// TODO: DEBUG - creates temporary walls :D
        /// </summary>
        private void CreateWalls()
        {
            var walledPosIndexes = new List<Vector2Int>();
            var wallsParentGameObject = new GameObject("walls parent");

            _mapArea.Keys.ForEach(posIndex =>
            {
                _mapArea[posIndex].GetConnections().ForEach(connection =>
                {
                    var otherPosIndex = GetPosIndexInDirection(posIndex, connection.direction);
                    if (walledPosIndexes.Contains(otherPosIndex)) return;
                    if (connection.type is not AreaConnectionType.None && _mapArea.ContainsKey(otherPosIndex)) return;
                    var wallPosition = Vector3.Lerp(
                        new Vector3(posIndex.x * Area.Size, 0, posIndex.y * Area.Size),
                        new Vector3(otherPosIndex.x * Area.Size, 0, otherPosIndex.y * Area.Size),
                        0.5f);
                    var wallScale = new Vector3(Area.Size, 10, 1);
                    var isVertical = connection.direction is Direction2D.Back or Direction2D.Forward;
                    var wallRotation = Quaternion.Euler(0, isVertical ? 0 : 90, 0);
                    var wallGameObject = GameObject.CreatePrimitive(PrimitiveType.Cube);
                    wallGameObject.transform.position = wallPosition;
                    wallGameObject.transform.localScale = wallScale;
                    wallGameObject.transform.rotation = wallRotation;
                    wallGameObject.GetComponent<MeshRenderer>().material = wallMaterial;
                    wallGameObject.transform.parent = wallsParentGameObject.transform;
                    wallGameObject.layer = LayerManager.StaticIndex;
                });
                
                walledPosIndexes.Add(posIndex);
            });
        }

        /// <summary>
        /// TODO: DEBUG - area distance from starting area
        /// </summary>
        private void CreateAreaDistanceText()
        {
            var distanceParentGameObject = new GameObject("distance to areas");
            _mapArea.Keys.ForEach(posIndex =>
            {
                var position = new Vector3(posIndex.x * Area.Size, 25, posIndex.y * Area.Size);
                var rotation = new Vector3(90, 0, 0);
                
                var textGameObject = new GameObject($"{_distanceToArea} ({posIndex})");
                textGameObject.transform.parent = distanceParentGameObject.transform;
                textGameObject.transform.position = position;
                textGameObject.transform.rotation = Quaternion.Euler(rotation);
                var textMesh = textGameObject.AddComponent<TextMesh>();
                textMesh.color = Color.red;
                textMesh.text = _distanceToArea[posIndex].ToString();
                textMesh.fontSize = 100;
                textMesh.alignment = TextAlignment.Center;
                textMesh.anchor = TextAnchor.MiddleCenter;
            });
        }

        /// <summary>
        /// Selects random starting area at position 0,0.
        /// </summary>
        private void GenerateStartingArea()
        {
            var startingArea = RandomUtil.RandomElement(WithVariants(worldSettings.startingComplexAreas));
            UpdateMap(startingArea, Vector2Int.zero);
            RecreateAreasDistance();
        }

        /// <summary>
        /// Alias to <see cref="AddRandomValidComplexArea(Vector2Int, Direction2D)"/>, but runs it multiple times
        /// </summary>
        private void GenerateAreas(int count)
        {
            while (_mapArea.Count <= count && _mapOpenConnections.Count > 0)
            {
                FindClosestConnection(out var posIndex, out var direction);
                AddRandomValidComplexArea(posIndex, direction);
            }
        }

        /// <summary>
        /// Returns in out parameters closest and open connection at position index and direction
        /// </summary>
        /// <param name="posIndex">Position index where open connection exists</param>
        /// <param name="direction">In which direction connection is open</param>
        private void FindClosestConnection(out Vector2Int posIndex, out Direction2D direction)
        {
            posIndex = default;
            direction = default;
            var minDistance = float.MaxValue;
            foreach (var pair in _mapOpenConnections)
            {
                var distance = Vector2Int.Distance(Vector2Int.zero, pair.Key);
                if (!(distance < minDistance)) continue;
                minDistance = distance;
                posIndex = pair.Key;
                direction = RandomUtil.RandomElement(pair.Value).direction;
            }
        }

        /// <summary>
        /// Select random <see cref="ComplexArea"/> from given list that can be connected to given position.
        /// </summary>
        /// <param name="thisPosIndex">To which position new area will be connected</param>
        /// <param name="thisDir">From which direction new area will be connected</param>
        private void AddRandomValidComplexArea(Vector2Int thisPosIndex, Direction2D thisDir)
        {
            var otherPosIndex = GetPosIndexInDirection(thisPosIndex, thisDir);
            var otherPosDistance = _distanceToArea[thisPosIndex] + 1;
            
            // 1. Get all Area that can be placed
            // - Check group limits
            // - Check if group can be placed in this distance
            // - Include rotated variants if needed
            // - Check if it would collide others
            var validAreas = worldSettings.list
                .Where(areaSettings => _groupCount[areaSettings] < areaSettings.limit)
                .Where(areaSettings => !areaSettings.requireDistance || (otherPosDistance >= areaSettings.distance.x && otherPosDistance <= areaSettings.distance.y))
                .SelectMany(areaSettings => WithVariants(areaSettings.group, areaSettings.useRotatedVariants))
                .SelectMany(complexArea => complexArea.areas)
                .Where(area => IsComplexAreaValid(area, otherPosIndex)).ToList();
            
            // 2. No available Area can be placed here!
            if (!validAreas.Any())
            {
                // No valid areas found
                CloseConnection(thisPosIndex, _mapArea[thisPosIndex].GetConnection(thisDir));
                return;
            }
            
            // 3. Get all ComplexArea, by found validated Area
            var validComplexAreas = validAreas.Select(area => area.complexArea).Distinct().ToList();
            
            // 4. Get all groups from WorldGeneratorSettings that contain ANY validated ComplexArea
            // - Include rotated variants if needed
            var validWorldAreaSettings = worldSettings.list
                .Where(areaSettings => validComplexAreas.Intersect(WithVariants(areaSettings.group)).Any()).ToList();
            
            // 5. Get random group
            var worldAreaSettingsToAdd = RandomUtil.RandomElementByWeight(validWorldAreaSettings, wags => wags.weight);
            
            // 6. Randomized group still can contain few ComplexArea, so we need to get only valid ones
            var areasToAdd = WithVariants(worldAreaSettingsToAdd.group, worldAreaSettingsToAdd.useRotatedVariants)
                .Where(complexArea => validComplexAreas.Contains(complexArea))
                .SelectMany(complexArea => complexArea.areas)
                .Where(area => validAreas.Contains(area)).ToList();
            
            // 7. Randomize again to get final ComplexArea from group
            var areaToAdd = RandomUtil.RandomElement(areasToAdd);
            
            // 8. Add ComplexArea with correct offset to its Area
            UpdateMap(areaToAdd.complexArea, otherPosIndex - areaToAdd.position);
            _groupCount[worldAreaSettingsToAdd]++;
            
            // 9. Recreate Distances to areas
            RecreateAreasDistance();
        }

        private List<ComplexArea> WithVariants(List<ComplexArea> complexAreas, bool includeRotatedVariants = true)
        {
            if (!includeRotatedVariants) return complexAreas;
            return complexAreas.Concat(complexAreas.SelectMany(complexArea => complexArea.variants)).ToList();
        }

        /// <summary>
        /// Checks if whole <see cref="ComplexArea"/> which given <see cref="Area"/> belongs to can be placed at given position index
        /// </summary>
        private bool IsComplexAreaValid(Area area, Vector2Int posIndex)
        {
            return IsComplexAreaValid(area.complexArea, posIndex - area.position);
        }

        /// <summary>
        /// Checks if whole <see cref="ComplexArea"/> can be placed at given position index
        /// </summary>
        private bool IsComplexAreaValid(ComplexArea complexArea, Vector2Int posIndex)
        {
            return complexArea.areas.All(area => IsAreaValid(area, posIndex + area.position));
        }

        /// <summary>
        /// Checks if given <see cref="Area"/> can be placed at position index
        /// </summary>
        private bool IsAreaValid(Area area, Vector2Int posIndex)
        {
            // Position already occupied
            if (_mapArea.ContainsKey(posIndex)) return false;
            
            return area.GetConnections().All(connection => IsConnectionValid(connection, posIndex));
        }

        /// <summary>
        /// Checks if <see cref="AreaConnection"/> can be placed at given position index
        /// Its valid when:
        /// - Neighbor is empty
        /// - Neighbor connection is also open AND have identical tag
        /// </summary>
        private bool IsConnectionValid(AreaConnection thisConnection, Vector2Int thisPosIndex)
        {
            var otherPosIndex = GetPosIndexInDirection(thisPosIndex, thisConnection.direction);
            var otherConnection = _mapArea.GetValueOrDefault(otherPosIndex)?.GetConnection(GetReverseDirection(thisConnection.direction));
            
            // No area in that direction - VALID
            if (otherConnection == null) return true;
            
            // Open connection leads to closed connection - INVALID
            if (IsConnectionOpen(thisConnection) != IsConnectionOpen(otherConnection)) return false;
            
            // Connections have mismatched tags - INVALID
            if (thisConnection.tag != otherConnection.tag) return false;

            return true;
        }

        private void UpdateMap(ComplexArea complexArea, Vector2Int posIndex)
        {
            _mapComplexArea.Add(posIndex, complexArea);
            complexArea.areas.ForEach(area =>
            {
                var localPosIndex = posIndex + area.position;
                var connections = area.GetConnections().Where(IsConnectionOpen).ToList();
                _mapArea.Add(localPosIndex, area);
                _mapOpenConnections.Add(localPosIndex, connections);
                UpdateMapConnections(localPosIndex);
            });
        }

        private void RecreateAreasDistance()
        {
            // Clear all distances and start from beginning
            // Just to be sure that connected branches of generated world will have correct distance from start
            _distanceToArea.Clear();
            _distanceToArea.Add(Vector2Int.zero, 0);

            var toUpdate = new List<Vector2Int> { Vector2Int.zero };
            var distance = 0;
            
            while (toUpdate.Count > 0)
            {
                distance++;
                toUpdate = toUpdate
                    // Get all neighbours...
                    .SelectMany(GetNeighbourPositions)
                    // ...but only those without distance calculated already
                    .Where(posIndex => !_distanceToArea.ContainsKey(posIndex))
                    .ToList();
                toUpdate.ForEach(posIndex => _distanceToArea.TryAdd(posIndex, distance));
            }
        }
        
        /// <summary>
        /// Returns all index positions of existing and connected neighbours
        /// </summary>
        private List<Vector2Int> GetNeighbourPositions(Vector2Int posIndex)
        {
            return Enum.GetValues(typeof(Direction2D)).Cast<Direction2D>()
                // Only directions which area is open to
                .Where(direction => IsConnectionAny(_mapArea[posIndex].GetConnection(direction)))
                // Get neighbour index positions
                .Select(direction => GetPosIndexInDirection(posIndex, direction))
                // Neighbour need to exists
                .Where(otherPosIndex => _mapArea.ContainsKey(otherPosIndex))
                .ToList();
        }

        private void UpdateMapConnections(Vector2Int thisPosIndex)
        {
            if (!_mapOpenConnections.ContainsKey(thisPosIndex)) return;
            
            if (_mapOpenConnections[thisPosIndex].IsNullOrEmpty())
            {
                _mapOpenConnections.Remove(thisPosIndex);
                return;
            }
            
            _mapOpenConnections[thisPosIndex].ToList().ForEach(thisConnection =>
            {
                var otherPosIndex = GetPosIndexInDirection(thisPosIndex, thisConnection.direction);
                var otherConnection = _mapArea.GetValueOrDefault(otherPosIndex)?.GetConnection(GetReverseDirection(thisConnection.direction));

                if (otherConnection == null) return;
                CloseConnection(thisPosIndex, thisConnection);
                CloseConnection(otherPosIndex, otherConnection);
            });
        }

        private void CloseConnection(Vector2Int posIndex, AreaConnection connection)
        {
            var connections = _mapOpenConnections.GetValueOrDefault(posIndex);
            connections?.Remove(connection);
            if (connections.IsNullOrEmpty()) _mapOpenConnections.Remove(posIndex);
        }

        private Vector2Int GetPosIndexInDirection(Vector2Int fromPosIndex, Direction2D direction)
        {
            return direction switch
            {
                Direction2D.Forward => fromPosIndex + Vector2Int.up,
                Direction2D.Right => fromPosIndex + Vector2Int.right,
                Direction2D.Back => fromPosIndex + Vector2Int.down,
                Direction2D.Left => fromPosIndex + Vector2Int.left,
                _ => throw new ArgumentOutOfRangeException(nameof(direction), direction, null)
            };
        }

        private Direction2D GetReverseDirection(Direction2D direction)
        {
            return direction switch
            {
                Direction2D.Forward => Direction2D.Back,
                Direction2D.Right => Direction2D.Left,
                Direction2D.Back => Direction2D.Forward,
                Direction2D.Left => Direction2D.Right,
                _ => throw new ArgumentOutOfRangeException(nameof(direction), direction, null)
            };
        }

        private bool IsConnectionOpen(AreaConnection connection)
        {
            return connection.type is AreaConnectionType.Tunnel or AreaConnectionType.Whole;
        }
        
        private bool IsConnectionAny(AreaConnection connection)
        {
            return connection.type is not AreaConnectionType.None;
        }
    }
}