using System;
using System.Collections.Generic;
using Animators.Leonid_Animator.Bot;
using Unity.MLAgents;
using Unity.MLAgents.Actuators;
using Unity.MLAgents.Sensors;
using UnityEngine;

[RequireComponent(typeof(MovementController))]
[RequireComponent(typeof(BufferSensorComponent))]
[RequireComponent(typeof(DecisionRequester))]
[RequireComponent(typeof(BotLocomotion))]
public class NPC : Agent, ICharacter
{
    private CharacterCondition _condition;
    private FlagZone _flagZone;
    public INpcBaseState NpcState { get; set; }
    public INpcBaseBodyState NpcBodyState { get; private set; }

    [field: HideInInspector]
    public Character GetCharacter { get; private set; }

    private NpcDirectPointState _directState;
    private NpcInCoverState _coverState;
    private NpcRunningState _runningState;

    private NpcStandingState _standingState;
    private NpcCrouchingState _crouchingState;

    private MovementController _moveController;
    private BufferSensorComponent _bufferSensor;
    private AimAssistant _assistant;
    private BotLocomotion _botLocomotion;
    
    private Dictionary<int, NavPoint> _navPointIdDict;

    public bool IsFiring => _assistant.fireAnimation;

    public void ChangePointState(NpcEnumState state)
    {
        switch (state)
        {
            case NpcEnumState.InCover:
                NpcState = _coverState;
                break;
            case NpcEnumState.InRunning:
                NpcState = _runningState;
                break;
            case NpcEnumState.InDirectPoint:
                NpcState = _directState;
                break;
        }
    }

    public void ChangeBodyState(NpcBodyState state)
    {
        switch (state)
        {
            case global::NpcBodyState.Crouching:
                NpcBodyState = _crouchingState;
                break;
            case global::NpcBodyState.Standing:
                NpcBodyState = _standingState;
                break;
        }
    }
    
    #region UnityEvents and ML
    private void Awake()
    {
        _directState = new NpcDirectPointState();
        _coverState = new NpcInCoverState();
        _runningState = new NpcRunningState();
        NpcState = _directState;

        _crouchingState = new NpcCrouchingState();
        _standingState = new NpcStandingState();
        NpcBodyState = _standingState;

        GetCharacter = new Character();
        _condition = GetCharacter.Condition;

        _moveController = gameObject.GetComponent<MovementController>();
        _bufferSensor = gameObject.GetComponent<BufferSensorComponent>();
        _assistant = gameObject.GetComponent<AimAssistant>();
        _botLocomotion = gameObject.GetComponent<BotLocomotion>();
        
        _flagZone = GameObject.FindObjectOfType<FlagZone>();
        if (_flagZone is null)
            Debug.LogError("Flag Is Not Set");
        
        _navPointIdDict = MapManager.Instance.IDToNavPoint;
        if (_navPointIdDict is null)
            Debug.LogError("Cant Find Nav Point Dictionary");
    }

    private void OnDestroy()
    {
        Debug.LogWarning("Pooled object was destroyed");
    }

    private void Update()
    {
        _botLocomotion.UpdateAnimatorValues();
    }

    public override void OnEpisodeBegin()
    {
        if (_navPointIdDict is null)
            Debug.LogError("Cant Find Nav Point Dictionary");
        
        _flagZone = GameObject.FindObjectOfType<FlagZone>();
    }

    public override void CollectObservations(VectorSensor sensor)
    {
        _navPointIdDict = MapManager.Instance.IDToNavPoint;
        if (_navPointIdDict is null)
            Debug.LogError("Cant Find Nav Point Dictionary");
        var candidates = _moveController.GetPointsCandidate();

        //common sensors
        sensor.AddObservation(GameManager.IsHaveSeenByEnemy(GetCharacter.Team.GetOppositeTeam(),
                              NpcBodyState.GetPointToHit(gameObject)).ToInt());
        sensor.AddObservation(GetCharacter.LastTimeHit);
        sensor.AddObservation((!_flagZone.IsNotOccup).ToInt());
        sensor.AddObservation(_condition.GetHealthPointsInQuantile());
        sensor.AddObservation(_condition.GetArmourPointsInQuantile());
        sensor.AddObservation(candidates.Count);
        sensor.AddObservation(_moveController.PointStartID);
        sensor.AddObservation(_moveController.PointEndID);
        //state sensors
        sensor.AddObservation((int)NpcState.State);
        sensor.AddObservation((int)NpcBodyState.State);
        sensor.AddObservation(GameManager.IsEnemyNearby(gameObject.transform.position, GetCharacter.Team));
        sensor.AddObservation(_moveController.FlagDistance);

        //point sensors
        foreach (var point in candidates)
        {
            var position = transform.position;
            _bufferSensor.AppendObservation(new float[] {
                (int)point.navType,
                GameManager.IsCloserToFlagFromNextNavPoint(point, position).ToInt(),
                GameManager.IsCloserToEnemyThanToNextNavPoint(point, position, GetCharacter.Team.GetOppositeTeam()).ToInt(),
                GameManager.IsHaveSeenByEnemy(GetCharacter.Team.GetOppositeTeam(),
                              point.Position).ToInt()
        });
        } ;
    }

    public override void WriteDiscreteActionMask(IDiscreteActionMask actionMask)
    {
        if (!NpcState.InCover)
            actionMask.SetActionEnabled(0,0,false);
        if (!NpcState.InDirectPoint)
            actionMask.SetActionEnabled(0,1,false);
        if (!NpcState.IsRunning)
            actionMask.SetActionEnabled(0,2,false);
        base.WriteDiscreteActionMask(actionMask);
    }
    

    public override void OnActionReceived(ActionBuffers actions)
    { 
        var result = actions.DiscreteActions;
        if (result[0] == 0)
        {
            switch (result[1])
            {
                case 0: Peek(); break;
                case 1: Cover(); break;
                case 2: Peek();
                    _moveController.GoToNextNavPoint(_navPointIdDict[result[2]]);
                    break;
                case 3: break;
                default: throw new ArgumentException("Undefined Action received");
            }
        }
        if (result[0] == 1)
        {
            switch (result[1])
            {
                case 0:
                    _moveController.GoToNextNavPoint(_navPointIdDict[result[2]]);break;
                case 1: break;
                case 2: break;
                case 3: break;
                default: throw new ArgumentException("Undefined Action received");
            }
        }
        
        if (result[0] == 2)
        {     
            switch (result[1])
            {
                case 0: _moveController.StopOnPath(); break;
                case 1: _moveController.ReturnToStartPoint(); break;
                case 2: break;
                case 3: break;
                default: throw new ArgumentException("Undefined Action received");
            }
        }
        AddReward(-0.001f);
    }
    #endregion

    public event Action<NpcBodyState> OnChangePosition;
    private void Peek()
    {
        OnChangePosition?.Invoke(global::NpcBodyState.Standing);
        NpcBodyState = _standingState;
    }

    private void Cover()
    {
        OnChangePosition?.Invoke(global::NpcBodyState.Crouching);
        NpcBodyState = _crouchingState;
    }

    public event Action<bool> OnDeathEvent;
    public event Action<int, Team> OnDamageReceived;
    public void GetDamage(int damage)
    {
        GetCharacter.LastTimeHit = TimeManager.Instance.CurrentTime;
        _condition.GiveHealth(-Mathf.RoundToInt(damage * (1 - _condition.ArmourPoints * 0.5f)));
        _condition.GiveArmour(-Mathf.RoundToInt(Mathf.Sqrt(damage) * 5));
        OnDamageReceived?.Invoke(damage, GetCharacter.Team);
        AddReward(-0.03f);
        if (_condition.HealthPoints < 1)
        {
            Die();
        }
    }

    private void Die()
    {
        AddReward(-0.2f);
        OnDeathEvent?.Invoke(true);
        MapManager.AddDeathAttributeToPoints(_moveController.PointStartID, _moveController.PointEndID,
            _moveController.DistanceToGo, _moveController.RemainingDistance);
        CharacterFactory.Instance.ReSpawn(this, gameObject);
    }

    public void ResetCharacter()
    {
        _condition.Reset();
        EndEpisode();
    }
}
