using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Sirenix.OdinInspector;
using Sirenix.Utilities;
using UnityEngine;
using VOID.Generic.AI;
using VOID.Generic.Dict;
using VOID.Generic.Navigation;
using VOID.Generic.Objects.Abstract;
using VOID.Generic.Objects.Abstract.Events;
using VOID.Generic.Objects.Unit;
using VOID.Generic.Objects.Unit.Events;
using VOID.Generic.Player;
using VOID.Generic.Util;

namespace VOID.Generic.Turn
{
    /// <summary>
    /// Instantiated object that manages turn based combat in given area.<br/>
    /// - Invokes <see cref="AbstractObjectEvents"/> events <b>onTurn...</b>
    /// - Automatically add any <see cref="AbstractObject"/> when in area<br/>
    /// </summary>
    public class TurnManager : MonoBehaviour
    {
        [InfoBox("<u><b>" + nameof(InitBefore) + "</b></u> Initializing - still looking for objects in range" +
                 "<u><b>" + nameof(InitAfter) + "</b></u> Initializing - found all objects in range" +
                 "<u><b>" + nameof(Running) + "</b></u> Active" +
                 "<u><b>" + nameof(End) + "</b></u> TurnManager finishing its job")]
        private enum TurnManagerState
        {
            InitBefore,
            InitAfter,
            Running,
            End
        }
        
        // State of this instance of TurnManager for internal purposes
        [ShowInInspector, ReadOnly] private TurnManagerState _currentState = TurnManagerState.InitBefore;

        // Participants in Queue - both units and non-units
        [ShowInInspector, ReadOnly] public List<AbstractObject> allNonUnitsInQueue { get; private set; } = new();
        [ShowInInspector, ReadOnly] public List<UnitObject> allUnitsInQueue { get; private set; } = new();
        [ShowInInspector, ReadOnly] public List<UnitObject> unitsInCurrentQueue { get; private set; } = new();
        [ShowInInspector, ReadOnly] public UnitObject currentUnitTurn { get; private set; }

        // Bounds of TurnManager - everything inside should participate in combat
        private readonly Dictionary<UnitObject, TurnManagerArea> _turnManagerAreas = new();

        // Events (for UI purposes)
        public event Action OnNextTurn;
        public event Action OnNextQueue;
        public event Action<AbstractObject> OnObjectEnter;
        public event Action<AbstractObject> OnObjectLeave;
        public event Action<UnitObject, int> OnCurrentQueueOrderChange;
        public event Action<UnitObject, int> OnNextQueueOrderChange;

        // States
        private bool _isOtherToMerge;
        public bool isForcedFight { get; private set; }

        /// <summary>
        /// Starts new TurnManager for given units.<br/>
        /// - All units in range also will be added to turn combat
        /// </summary>
        public static void BuildGameObject(IEnumerable<UnitObject> units)
        {
            new GameObject(nameof(TurnManager)).AddComponent<TurnManager>().Initialize(units);
        }

        /// <inheritdoc cref="BuildGameObject(IEnumerable{UnitObject})"/>
        public static void BuildGameObject(UnitObject unit)
        {
            BuildGameObject(new[] { unit });
        }

        private void Initialize(IEnumerable<UnitObject> units)
        {
            units.ForEach(ObjectEnter);
            StartCoroutine(WaitForFinishInitialize());
        }

        private IEnumerator WaitForFinishInitialize()
        {
            // Because check for collision is after fixed update, we need to wait for that fixed update when there
            // will be no more new obejcts added to turn manager. After that we can start first turn.
            // If any object found within TurnManager bounds then state changes back to InitBefore in ObjectEnter().
            // This process loops untill no more objects can be found!
            while (_currentState is TurnManagerState.InitBefore)
            {
                _currentState = TurnManagerState.InitAfter;
                yield return new WaitForFixedUpdate();
            }

            // Checks if this turn manager was initialized manually or by meeting enemies
            // If fight is started manually it can be ended anytime
            // If fight is started by meeting enemy, then ending is possible only by killing or running
            isForcedFight = IsForcedFight();
            
            UpdateNavMesh();

            _currentState = TurnManagerState.Running;
            NextTurn();
        }

        private void UpdateNavMesh()
        {
            var positions = allUnitsInQueue.Select(unit => unit.transform.position).ToArray();
            var bounds = GeometryUtility.CalculateBounds(positions, transform.localToWorldMatrix);
            NavigationManager.current.UpdateNavMesh(bounds);
        }

        /// <summary>
        /// Ends current turn and proceed to start next one.<br/>
        /// - Ends whole TurnManager if needed<br/>
        /// - Manages events for units<br/>
        /// - Restarts queue if all units had their turns
        /// </summary>
        public void NextTurn()
        {
            // If somehow whole queue is empty - Destroy current turn manager
            if (allUnitsInQueue.IsNullOrEmpty())
            {
                EndTurnManager();
                return;
            }

            // Get next unit in queue
            var previousUnit = currentUnitTurn;
            unitsInCurrentQueue.Remove(previousUnit);
            currentUnitTurn = unitsInCurrentQueue.FirstOrDefault();

            // Nobody did their turn - usually when starting new turn manager
            if (previousUnit)
            {
                var turnEvent = new TurnEvent
                {
                    obj = previousUnit,
                    turnManager = previousUnit.basicState.turnManager
                };
                previousUnit.basicEvents.onTurnSelfEnd.Invoke(turnEvent);
            }

            // That was last unit in current queue, start next queue
            if (!currentUnitTurn)
            {
                NextQueue();
                currentUnitTurn = unitsInCurrentQueue.First();
            }

            OnNextTurn?.Invoke();

            currentUnitTurn.basicEvents.onTurnSelfStart.Invoke(new TurnEvent
                { obj = currentUnitTurn, turnManager = this });
            
            UpdateNavMesh();

            StartCoroutine(SkipTurnIfNeeded());
        }

        /// <summary>
        /// Skips <see cref="UnitObject"/>'s turn if it cant end itself.
        /// Usually that means that its not controlled by Player nor AI.
        /// </summary>
        private IEnumerator SkipTurnIfNeeded()
        {
            if (!currentUnitTurn) yield break;
            var isPlayerUnit = currentUnitTurn == PlayerController.current.mainUnit || currentUnitTurn == PlayerController.current.subUnit;
            var isAIUnit = currentUnitTurn.GetComponent<UnitAI>();
            if (isPlayerUnit || isAIUnit) yield break;
            yield return new WaitForSeconds(0.5f);
            NextTurn();
        }

        /// <summary>
        /// Starts next Queue for this <see cref="TurnManager"/> and invokes events for all participants.
        /// </summary>
        private void NextQueue()
        {
            allNonUnitsInQueue.ForEach(obj =>
            {
                var turnEvent = new TurnEvent { obj = obj, turnManager = this };
                obj.basicEvents.onTurnSelfStart.Invoke(turnEvent);
                obj.basicEvents.onTurnSelfEnd.Invoke(turnEvent);
                obj.basicEvents.onTurnQueueStart.Invoke(turnEvent);
                obj.basicEvents.onTurnQueueEnd.Invoke(turnEvent);
            });
            allUnitsInQueue.ForEach(unit =>
            {
                var turnEvent = new TurnEvent { obj = unit, turnManager = this };
                unit.basicEvents.onTurnQueueStart.Invoke(turnEvent);
                unit.basicEvents.onTurnQueueEnd.Invoke(turnEvent);
            });

            unitsInCurrentQueue = allUnitsInQueue.ToList();
            OnNextQueue?.Invoke();
        }

        /// <summary>
        /// Try to end this TurnManager if possible.
        /// If participing units are hostile to each other then ending is not possible.
        /// </summary>
        public void EndTurnManager()
        {
            if (isForcedFight) return;

            _currentState = TurnManagerState.End;
            allNonUnitsInQueue.ForEach(obj =>
                obj.basicEvents.onTurnQueueLeave.Invoke(new TurnEvent { obj = obj, turnManager = this }));
            allUnitsInQueue.ForEach(unit =>
                unit.basicEvents.onTurnQueueLeave.Invoke(new TurnEvent { obj = unit, turnManager = this }));
            Destroy(gameObject);
        }

        /// <summary>
        /// Checks if there is any units with enemy attitude towards any other unit.
        /// </summary>
        /// <returns>TRUE if anyone is enemy</returns>
        private bool IsForcedFight()
        {
            for (var i = 0; i < allUnitsInQueue.Count - 1; i++)
            for (var j = i + 1; j < allUnitsInQueue.Count; j++)
                if (allUnitsInQueue[i].unitAttitude.GetAttitudeWith(allUnitsInQueue[j])
                    is AttitudeType.Hostile or AttitudeType.Cautious)
                    return true;

            return false;
        }

        /// <summary>
        /// Merges two <see cref="TurnManager"/>.
        /// </summary>
        private void MergeWith(TurnManager otherTurnManager)
        {
            if (!otherTurnManager) throw new Exception($"{nameof(TurnManager)}: Nothing to merge with!");
            if (otherTurnManager == this) throw new Exception($"{nameof(TurnManager)}: Cant merge with itself!");

            // First manager that will come here will flag other one for merging, so other one will do nothing
            if (_isOtherToMerge) return;
            otherTurnManager._isOtherToMerge = true;

            // Disable other so it wont process anything more - current one will do it instead
            otherTurnManager.gameObject.SetActive(false);

            // Get all units and objects from other turn manager and put them into current one
            otherTurnManager.unitsInCurrentQueue.Skip(1).ForEach(PutUnitInCurrentQueue);
            otherTurnManager.allUnitsInQueue.ForEach(PutUnitInQueue);
            otherTurnManager.allNonUnitsInQueue.ForEach(allNonUnitsInQueue.Add);

            // After merging two queues, there will be two units with their turn, need to decide who will be first
            var thisUnit = unitsInCurrentQueue.First();
            var otherUnit = otherTurnManager.unitsInCurrentQueue.First();
            if (thisUnit.unitData.subAttributes.initiative >= otherUnit.unitData.subAttributes.initiative)
            {
                // Other unit is second, need to update other unit
                unitsInCurrentQueue.Insert(1, otherUnit);
                otherUnit.OrderStop();
                otherUnit.unitState.isSelfTurn = false;
                currentUnitTurn = unitsInCurrentQueue.First();
            }
            else
            {
                // Other unit is first, need to update this unit
                unitsInCurrentQueue.Insert(0, otherUnit);
                thisUnit.OrderStop();
                thisUnit.unitState.isSelfTurn = false;
                currentUnitTurn = unitsInCurrentQueue.First();
            }

            // Also we need to inform all participants that their turn manager changed
            allUnitsInQueue.ForEach(unit =>
                unit.basicEvents.onTurnQueueSwap.Invoke(new TurnEvent { obj = unit, turnManager = this }));
            allNonUnitsInQueue.ForEach(obj =>
                obj.basicEvents.onTurnQueueSwap.Invoke(new TurnEvent { obj = obj, turnManager = this }));

            // Move all trigger areas from other to this manager
            otherTurnManager._turnManagerAreas.Values.ForEach(turnArea => turnArea.ChangeTurnManager(this));
            otherTurnManager._turnManagerAreas.ForEach(other => _turnManagerAreas.Add(other.Key, other.Value));

            // After merging destroy other turn manager
            Destroy(otherTurnManager.gameObject, 1f);
        }

        /// <summary>
        /// Given object will be added to this TurnManager.
        /// If this is UnitObject then TurnManagerArea will be expanded.
        /// Also entering object events will be handled.
        /// </summary>
        public void ObjectEnter(AbstractObject obj)
        {
            // If this object exists in CURRENT turn manager - do nothing 
            if (allNonUnitsInQueue.Contains(obj) || allUnitsInQueue.Contains(obj)) return;

            // If this object exists in OTHER turn manager - merge with it
            if (obj.basicState.turnManager)
            {
                MergeWith(obj.basicState.turnManager);
                return;
            }

            // For initialize purpose - if any object found, wait for one more fixed update to probably get more
            if (_currentState is TurnManagerState.InitAfter) _currentState = TurnManagerState.InitBefore;

            if (obj is UnitObject unit)
            {
                PutUnitInCurrentQueue(unit);
                PutUnitInQueue(unit);
                CreateNewArea(unit);
                
                // Listen to unit's change of initiative - it can change queue order
                unit.unitEvents.onSubAttributeChange.AddListener(OnInitiativeChange);
            }
            else
            {
                allNonUnitsInQueue.Add(obj);
            }

            OnObjectEnter?.Invoke(obj);

            // Everytime someone enters fight check if whole turn manager can be ended manually or by killing/running
            if (_currentState is TurnManagerState.InitAfter && !isForcedFight) isForcedFight = !IsForcedFight();

            obj.basicEvents.onTurnQueueEnter.Invoke(new TurnEvent { obj = obj, turnManager = this });
        }

        private void OnInitiativeChange(SubAttributeChangeEvent subAttributeChangeEvent)
        {
            // We care only about initiative - it defines queue order
            if (subAttributeChangeEvent.type is not SubAttributeType.Initiative) return;
            
            // Even after initiative change there is possibility that someone else will have identical value...
            var beforeUnit = subAttributeChangeEvent.changedValue > 0
                // ...when unit LOST initiative and someone else have same value then place it BEFORE them
                ? allUnitsInQueue.FirstOrDefault(unit =>
                    unit.unitData.subAttributes.initiative <= subAttributeChangeEvent.newValue)
                // ...when unit GAIN initiative and someone else have same value then place it AFTER them
                : allUnitsInQueue.FirstOrDefault(unit =>
                    unit.unitData.subAttributes.initiative < subAttributeChangeEvent.newValue);

            // NEXT QUEUE
            // Update unit's position in NEXT queue
            var nextQueueIndex = beforeUnit ? allUnitsInQueue.IndexOf(beforeUnit) : allUnitsInQueue.Count - 1;
            allUnitsInQueue.Remove(subAttributeChangeEvent.unit);
            allUnitsInQueue.Insert(nextQueueIndex, subAttributeChangeEvent.unit);
            OnNextQueueOrderChange?.Invoke(subAttributeChangeEvent.unit, nextQueueIndex);

            // That unit already done or is doing his turn - too late to update current queue
            if (!unitsInCurrentQueue.Contains(subAttributeChangeEvent.unit)) return;
            if (currentUnitTurn == subAttributeChangeEvent.unit) return;

            // CURRENT QUEUE
            // Update unit's position in CURRENT queue - never disturb turn of unit who currently do his turn
            var currentQueueIndex = beforeUnit ? Mathf.Max(unitsInCurrentQueue.IndexOf(beforeUnit), 1) : unitsInCurrentQueue.Count - 1;
            unitsInCurrentQueue.Remove(subAttributeChangeEvent.unit);
            unitsInCurrentQueue.Insert(currentQueueIndex, subAttributeChangeEvent.unit);
            OnCurrentQueueOrderChange?.Invoke(subAttributeChangeEvent.unit, currentQueueIndex);
        }

        private void PutUnitInCurrentQueue(UnitObject unit)
        {
            var currentQueueIndex = unitsInCurrentQueue.FindIndex(other =>
                other.unitData.subAttributes.initiative < unit.unitData.subAttributes.initiative);
            if (currentQueueIndex is - 1) currentQueueIndex = unitsInCurrentQueue.Count;
            else if (currentQueueIndex is 0 && _currentState is not TurnManagerState.InitBefore) currentQueueIndex = 1;
            unitsInCurrentQueue.Insert(currentQueueIndex, unit);
        }

        private void PutUnitInQueue(UnitObject unit)
        {
            var queueIndex = allUnitsInQueue.FindIndex(other =>
                other.unitData.subAttributes.initiative < unit.unitData.subAttributes.initiative);
            if (queueIndex is - 1) queueIndex = allUnitsInQueue.Count;
            allUnitsInQueue.Insert(queueIndex, unit);
        }

        private void CreateNewArea(UnitObject unit)
        {
            var range = unit.unitData.subAttributes.sight;
            _turnManagerAreas.Add(unit, TurnManagerArea.BuildGameObject(this, unit.transform, range));
        }

        /// <summary>
        /// Given object will be taken out of this TurnManager.
        /// - TurnManagerArea attached to that unit will be removed.
        /// - Leaving object events will be handled.
        /// - Checks if this fight can be ended
        /// - If given unit had current turn then its turn will be skipped
        /// </summary>
        public void ObjectExit(AbstractObject obj)
        {
            obj.basicEvents.onTurnQueueLeave.Invoke(new TurnEvent { obj = obj, turnManager = this });
            OnObjectLeave?.Invoke(obj);
            
            if (obj is UnitObject unit)
            {
                unitsInCurrentQueue.Remove(unit);
                allUnitsInQueue.Remove(unit);

                // Destroy turn collider attached to unit
                _turnManagerAreas[unit].gameObject.SetActive(false);
                Destroy(_turnManagerAreas[unit].gameObject);
                _turnManagerAreas.Remove(unit);
                
                // Stop listining to this unit
                unit.unitEvents.onSubAttributeChange.AddListener(OnInitiativeChange);

                // If fight was forced THEN check if there are units still willing to fight - if not end whole fight 
                if (isForcedFight && !IsForcedFight())
                {
                    isForcedFight = false;
                    EndTurnManager();
                    return;
                }

                if (unit == currentUnitTurn)
                {
                    currentUnitTurn = null;
                    NextTurn();
                }
            }
            else
            {
                allNonUnitsInQueue.Remove(obj);
            }
        }

        public bool CanObjectExit(AbstractObject obj)
        {
            if (obj is not UnitObject unit) return true;
            return !unit.basicState.turnManager.allUnitsInQueue
                .Where(otherUnit => RangeUtil.IsInRange(unit, otherUnit, otherUnit.unitData.subAttributes.sight * 2))
                .Any(otherUnit => otherUnit.unitAttitude.GetAttitudeWith(unit) <= AttitudeType.Cautious);
        }

        [Button]
        private void DEBUG_NEXT_TURN()
        {
            NextTurn();
        }
    }
}