using System.Collections.Generic;
using System.Linq;
using Sirenix.OdinInspector;
using Unity.AI.Navigation;
using UnityEngine;
using UnityEngine.AI;
using VOID.Generic.Objects.Traversable;

namespace VOID.Generic.Navigation
{
    [RequireComponent(typeof(NavMeshSurface))]
    [DefaultExecutionOrder(-1)]
    public class NavigationManager : MonoBehaviour
    {
        public static NavigationManager current { get; private set; }
        
        private const float NavMeshLinkThreshold = 0.01f;

        // NavMesh data
        private NavMeshSurface _navMeshSurface;
        private NavMeshBuildSettings _navMeshBuildSettings;
        
        // Cached links
        private Dictionary<Vector2Int, Dictionary<Vector3, TraversableObject>> _linksPerTile;
        private Dictionary<Vector3, bool> _isPositionStart;

        private void Awake()
        {
            current = this;
            _navMeshSurface = gameObject.GetComponent<NavMeshSurface>();
            _navMeshBuildSettings = _navMeshSurface.GetBuildSettings();
            _navMeshBuildSettings.preserveTilesOutsideBounds = true;
            _linksPerTile = new Dictionary<Vector2Int, Dictionary<Vector3, TraversableObject>>();
            _isPositionStart = new Dictionary<Vector3, bool>();
        }

        public void BuildNavMesh()
        {
            _navMeshSurface.BuildNavMesh();
        }

        [Button]
        public void UpdateNavMesh(Bounds bounds)
        {
            bounds.extents += Vector3.one * GetTileSize();
            NavMeshBuilder.UpdateNavMeshData(
                _navMeshSurface.navMeshData,
                _navMeshBuildSettings,
                GetSources(bounds),
                bounds
            );
        }

        private static List<NavMeshBuildSource> GetSources(Bounds bounds)
        {
            var sources = new List<NavMeshBuildSource>();
            NavMeshBuilder.CollectSources(
                bounds,
                LayerManager.Static | LayerManager.NavModifier,
                NavMeshCollectGeometry.RenderMeshes,
                0,
                GetMarkups(),
                sources
            );
            AppendSourcesWithModifierVolume(ref sources);
            return sources;
        }

        private static List<NavMeshBuildMarkup> GetMarkups()
        {
            var markups = new List<NavMeshBuildMarkup>();
            foreach (var modifier in NavMeshModifier.activeModifiers)
            {
                var markup = new NavMeshBuildMarkup();
                markup.root = modifier.transform;
                markup.overrideArea = modifier.overrideArea;
                markup.area = modifier.area;
                markup.ignoreFromBuild = modifier.ignoreFromBuild;
                markups.Add(markup);
            }
            return markups;
        }

        private static void AppendSourcesWithModifierVolume(ref List<NavMeshBuildSource> sources)
        {
            foreach (var modifier in NavMeshModifierVolume.activeModifiers)
            {
                var mcenter = modifier.transform.TransformPoint(modifier.center);
                var scale = modifier.transform.lossyScale;
                var msize = new Vector3(modifier.size.x * Mathf.Abs(scale.x), modifier.size.y * Mathf.Abs(scale.y), modifier.size.z * Mathf.Abs(scale.z));

                var src = new NavMeshBuildSource();
                src.shape = NavMeshBuildSourceShape.ModifierBox;
                src.transform = Matrix4x4.TRS(mcenter, modifier.transform.rotation, Vector3.one);
                src.size = msize;
                src.area = modifier.area;
                sources.Add(src);
            }
        }
        
        /// <summary>
        /// Gets cached TraversableObject at given point.
        /// </summary>
        /// <param name="pos">Check point for TraversableObject</param>
        /// <param name="traversable">returns found traversable</param>
        /// <param name="isReverse">returns bool if this is point is from link's end</param>
        /// <returns>Boolean if traversable is found.</returns>
        public bool GetCachedTraversable(Vector3 pos, out TraversableObject traversable, out bool isReverse)
        {
            var index = GetTileIndex(pos);
            if (!_linksPerTile.ContainsKey(index))
            {
                traversable = null;
                isReverse = false;
                return false;
            }
            var foundPair = _linksPerTile[index].FirstOrDefault(pair =>
            {
                var position = pair.Key;
                return Vector3.Distance(position, pos) < NavMeshLinkThreshold;
            });
            traversable = foundPair.Value;
            isReverse = !_isPositionStart.GetValueOrDefault(foundPair.Key);
            return traversable;
        }

        /// <summary>
        /// Cache both points of TraversableObject. Alias for:
        /// <see cref="RegisterTraversable(Vector3, TraversableObject, bool)" />
        /// </summary>
        public void RegisterTraversable(TraversableObject traversable)
        {
            var startPosition = traversable.traversableData.linkStartPosition;
            var endPosition = traversable.traversableData.linkEndPosition;
            RegisterTraversable(startPosition, traversable, true);
            RegisterTraversable(endPosition, traversable, false);
        }

        /// <summary>
        /// Cache given TraversableObject at given point.
        /// </summary>
        /// <param name="position">At which position, also it calculates into position index.</param>
        /// <param name="traversable">Which traversable will be cached.</param>
        /// <param name="isStart">Is given point start link? Otherwise it is end.</param>
        public void RegisterTraversable(Vector3 position, TraversableObject traversable, bool isStart)
        {
            var index = GetTileIndex(position);
            if (!_linksPerTile.ContainsKey(index))
                _linksPerTile[index] = new Dictionary<Vector3, TraversableObject>();
            _linksPerTile[index].Add(position, traversable);
            _isPositionStart.Add(position, isStart);
        }

        private float GetTileSize()
        {
            return _navMeshBuildSettings.tileSize * _navMeshBuildSettings.voxelSize;
        }

        private Vector2Int GetTileIndex(Vector3 pos)
        {
            return new Vector2Int()
            {
                x = Mathf.FloorToInt(pos.x / GetTileSize()),
                y = Mathf.FloorToInt(pos.z / GetTileSize()),
            };
        }
    }
}