using System;
using System.Collections.Generic;
using System.Linq;
using Sirenix.OdinInspector;
using UnityEngine;
using UnityEngine.Rendering;
using VOID.Generic.Dict;
using VOID.Generic.Objects.Abstract.Events;
using VOID.Generic.Objects.Unit;
using VOID.Generic.Objects.Unit.Events;
using VOID.Generic.Player.Input;
using VOID.Generic.Player.Util;
using VOID.UserInterface;
using VOID.Generic.Util;

namespace VOID.Generic.Player
{
    [DefaultExecutionOrder(-1)]
    public class PathVisualizer : MonoBehaviour
    {
        public static PathVisualizer current { get; private set; }
        
        // Configuration
        [SerializeField] private float redrawTimer = 0.1f;
        [SerializeField] private float lineSize = 0.05f; 
        [SerializeField] private float verticalOffset = 0.1f;
        [SerializeField, Required] private Material reachableMaterial; 
        [SerializeField, Required] private Material unreachableMaterial; 
        [SerializeField, Required] private Material dangerousMaterial;
        [SerializeField, Required] private Material neutralMaterial;
        
        // References
        private UnitObject _activeUnit;
        private readonly List<LineRenderer> _pathLineRenderers = new();

        private bool _autoRedraw;
        private float _lastRedrawTimer;

        private void Awake()
        {
            current = this;
        }

        private void Start()
        {
            PlayerController.current.OnActiveUnitChange += OnActiveUnitChange;
            PlayerInputManager.current.OnForceMoveStart += ShouldAutoRedraw;
            PlayerInputManager.current.OnForceMoveEnd += ShouldAutoRedraw;
        }

        private void FixedUpdate()
        {
            if (_autoRedraw == false) return;
            if (GameUI.current.isCursorOverUI)
            {
                HidePath();
                return;
            }
            
            _lastRedrawTimer += Time.fixedDeltaTime;
            if (_lastRedrawTimer < redrawTimer) return;
            _lastRedrawTimer = 0f;
            
            _activeUnit.unitNavAgent.CalculatePathTo(SelectUtil.SelectPosition());
            UpdatePath(_activeUnit.unitNavAgent.GetCorrectedPath().ToArray());
        }

        private void ShouldAutoRedraw()
        {
            // No player's active unit!
            if (!_activeUnit)
            {
                _autoRedraw = false;
                return;
            }
            
            // Unit is moving on drawn path - don't draw another one
            if (_activeUnit.unitState.isMoving)
            {
                _autoRedraw = false;
                return;
            }
            
            // Is something forcing path drawing?
            var isForcedMove = PlayerInputManager.current.isForceMove;
            var isActiveUnitTurn = _activeUnit.basicState.turnManager && _activeUnit.basicState.turnManager.currentUnitTurn == _activeUnit;
            if (!isForcedMove && !isActiveUnitTurn)
            {
                _autoRedraw = false;
                HidePath();
                return;
            }

            // Is unit casting ability?
            if (_activeUnit.unitState.isCasting) {
                _autoRedraw = false;
                HidePath();
                return;
            }

            _autoRedraw = true;
        }

        private void AddLineRenderers(int needed)
        {
            // Add missing LineRenderers to current poll
            for (var i = 0; i < needed - _pathLineRenderers.Count; i++)
            {
                var lineRendererGameObject = new GameObject($"Path Renderer - part {_pathLineRenderers.Count+1}");
                lineRendererGameObject.transform.parent = gameObject.transform;
                var lineRenderer = lineRendererGameObject.AddComponent<LineRenderer>();
                lineRenderer.enabled = false;
                lineRenderer.startWidth = lineSize;
                lineRenderer.endWidth = lineSize;
                lineRenderer.shadowCastingMode = ShadowCastingMode.Off;
                lineRenderer.numCapVertices = 3;
                lineRenderer.numCornerVertices = 3;
                
                _pathLineRenderers.Add(lineRenderer);
            }
        }

        private void UpdateLineRenderers(List<PathType> pathTypes, List<Vector3[]> paths)
        {
            AddLineRenderers(paths.Count);

            for (var i = 0; i < paths.Count; i++)
            {
                var lineRenderer = _pathLineRenderers[i];
                var pathType = pathTypes[i];
                var path = paths[i];
                var material = pathType switch
                {
                    PathType.Reachable => reachableMaterial,
                    PathType.UnReachable => unreachableMaterial,
                    PathType.Dangerous => dangerousMaterial,
                    PathType.Neutral => neutralMaterial,
                    _ => throw new ArgumentOutOfRangeException(nameof(pathType), pathType, null)
                };

                var offsetPath = path.Select(point => point + new Vector3(0, verticalOffset, 0)).ToArray();
                lineRenderer.positionCount = path.Length;
                lineRenderer.SetPositions(offsetPath);
                lineRenderer.sharedMaterial = material;
                lineRenderer.enabled = true;
            }
        }

        private void OnActiveUnitChange(UnitObject unitObject)
        {
            if (_activeUnit)
            {
                _activeUnit.unitEvents.onMoveStart.RemoveListener(OnUnitMoveStart);
                _activeUnit.unitEvents.onMoveEnd.RemoveListener(OnUnitMoveEnd);
                _activeUnit.basicEvents.onTurnSelfStart.RemoveListener(OnTurnSelfStart);
                _activeUnit.basicEvents.onTurnSelfEnd.RemoveListener(OnTurnSelfEnd);
                _activeUnit.basicEvents.onTurnQueueEnter.RemoveListener(OnTurnQueueEnter);
                _activeUnit.basicEvents.onTurnQueueLeave.RemoveListener(OnTurnQueueLeave);
                _activeUnit.unitEvents.onStartCasting.RemoveListener(OnStartCasting);
                _activeUnit.unitEvents.onEndCasting.RemoveListener(OnEndCasting);
            }
            HidePath();
            ShouldAutoRedraw();
            _activeUnit = unitObject;
            _activeUnit.unitEvents.onMoveStart.AddListener(OnUnitMoveStart);
            _activeUnit.unitEvents.onMoveEnd.AddListener(OnUnitMoveEnd);
            _activeUnit.basicEvents.onTurnSelfStart.AddListener(OnTurnSelfStart);
            _activeUnit.basicEvents.onTurnSelfEnd.AddListener(OnTurnSelfEnd);
            _activeUnit.basicEvents.onTurnQueueEnter.AddListener(OnTurnQueueEnter);
            _activeUnit.basicEvents.onTurnQueueLeave.AddListener(OnTurnQueueLeave);
            _activeUnit.unitEvents.onStartCasting.AddListener(OnStartCasting);
            _activeUnit.unitEvents.onEndCasting.AddListener(OnEndCasting);
        }

        private void OnUnitMoveStart(MoveEvent moveEvent)
        {
            _autoRedraw = false;
            UpdatePath(moveEvent.pathCorrected.path.ToArray());
        }

        private void OnUnitMoveEnd(MoveEvent moveEvent)
        {
            HidePath();
            ShouldAutoRedraw();
        }

        private void OnTurnSelfStart(TurnEvent turnEvent)
        {
            ShouldAutoRedraw();
        }

        private void OnTurnSelfEnd(TurnEvent turnEvent)
        {
            ShouldAutoRedraw();
        }

        private void OnTurnQueueEnter(TurnEvent turnEvent)
        {
            ShouldAutoRedraw();
        }

        private void OnTurnQueueLeave(TurnEvent turnEvent)
        {
            ShouldAutoRedraw();
        }

        private void OnStartCasting(CastingEvent castingEvent) {
            ShouldAutoRedraw();
        }

        private void OnEndCasting(CastingEvent castingEvent) {
            ShouldAutoRedraw();
        }

        private void UpdatePath(Vector3[] path)
        {
            HidePath();

            Vector3[] reachablePath;
            Vector3[] unreachablePath;

            if (_activeUnit.basicState.turnManager)
            {
                // Inside combat - some part of path can be unreachable (not enough move points)
                var movePoints = _activeUnit.unitData.currentResources.movePoints;
                NavMeshPathUtil.SplitPath(path, movePoints, out reachablePath, out unreachablePath);
            }
            else
            {
                // Outside combat - whole path is reachable
                reachablePath = path;
                unreachablePath = null;
            }

            NavMeshPathUtil.SplitReachablePath(reachablePath, out var pathTypes, out var paths);

            if (unreachablePath is not null)
            {
                pathTypes.Add(PathType.UnReachable);
                paths.Add(unreachablePath);
            }

            UpdateLineRenderers(pathTypes, paths);
        }

        private void HidePath()
        {
            _pathLineRenderers.ForEach(lineRenderer => lineRenderer.enabled = false);
        }
    }
}