using System;
using System.Collections.Generic;
using Cybe;
using Sources.Gui;
using Sources.Signals;
using UniRx;
using UnityEngine;
using Zenject;

namespace Sources
{
    public class Player : MonoBehaviour, IDamageable
    {
        [SerializeField] private Animator _animator;
        [SerializeField] private Rigidbody _rb;
        [SerializeField] private Camera _camera;
        [SerializeField] private Transform _head;
        [SerializeField] private Transform _body;
        [SerializeField] private LayerMask _ignoreLayers;

        [Header("Weapons")] 
        [SerializeField] private Sword _sword;
        [SerializeField] private Sword _sword2;
        [SerializeField] private Gun _gun;
        [SerializeField] private Gun _gun2;

        [Header("Balance")] 
        [SerializeField] private float _moveSpeed = 5;
        [SerializeField] private float _attackCooldown = 2;
        [SerializeField] private float _jumpForce = 100;
        [SerializeField] private float _centerHeight = 1;
        [SerializeField] private Level _initialLevel;

        // Skills
        public int SkillPoints;
        public ReactiveProperty<int> HpLevel = new ReactiveProperty<int>(0);
        public ReactiveProperty<int> SwordMastery = new ReactiveProperty<int>(0);
        public ReactiveProperty<int> WeaponModule = new ReactiveProperty<int>(0);
        public ReactiveProperty<int> WeaponPower = new ReactiveProperty<int>(0);
        public ReactiveProperty<int> WeaponCooldown = new ReactiveProperty<int>(0);
        public ReactiveProperty<int> Regeneration = new ReactiveProperty<int>(0);
        public ReactiveProperty<int> PlasmaAccelerator = new ReactiveProperty<int>(0);
        public ReactiveProperty<int> SwordDancer = new ReactiveProperty<int>(0);

        public static Dictionary<string, int> LevelsProgress = new Dictionary<string, int>();
    
        // Main parameters
        public bool IsAlive { get; set; }
        public float HP { get; set; }
        public int CurrentLevel { get; set; } = 1;
        public float CurrentExperience { get; set; }
        public float CurrentLevelMaxExperience => CurrentLevel * Mathf.Pow(CurrentLevel * 10, 1.3f);
        public float MaxHp => 100 * Mathf.Pow(1.3f, CurrentLevel - 1) * (1 + HpLevel.Value / 5f * 2f);
        public Rigidbody Rigidbody => _rb;

        public Action OnDamaged;

        [Inject] private AudioManager _audioManager;
        [Inject] private SignalBus _signalBus;
        [Inject] private GuiController _guiController;
        [Inject] private DiContainer _container;
    
        private const string ANIM_WALK = "Walk";
        private const string ANIM_IDLE = "Idle";
        private const string ANIM_JUMP = "Jump";
        private const string ANIM_DIE = "Die";
        private const string ANIM_SWORD_ATTACK = "SwordHit";

        private PlayerHandling _handling;
        private bool _swordAttackCooldown;
        private bool _jumpCooldown;
        private CompositeDisposable _disposables;
    
        private AnimState _currentAnimState;

        private enum AnimState
        {
            Idle,
            Walking,
            Jumping,
            Die
        }

        private void Start()
        {
            IsAlive = true;
            
            Animate(AnimState.Idle);
            _handling = new PlayerHandling(this, _camera, _ignoreLayers);
            
            _signalBus.Subscribe<EnemyDiedSignal>(OnEnemyDied);
        
            HP = MaxHp;

            WeaponModule.ObserveEveryValueChanged(v => v.Value).Subscribe(value =>
            {
                _guiController.SetCrosshairVisible(value > 0);
                _gun.gameObject.SetActive(value > 0);
            });
            
            SwordDancer.ObserveEveryValueChanged(v => v.Value).Subscribe(value =>
            {
                _sword2.gameObject.SetActive(value > 0);
            });
        
            _sword.SubscribeDamage(SwordMastery);
            _sword2.SubscribeDamage(SwordMastery);
            
            _gun.SubscribeDamage(WeaponPower);
            _gun.SubscribeCooldown(WeaponCooldown);
            _gun.SubscribeFlySpeed(PlasmaAccelerator);
        
            Observable.Interval(TimeSpan.FromSeconds(1)).Subscribe(_ => ProcessRegen());
        }

        private void Update()
        {
            if (!IsAlive)
                return;
            
            ProcessCheats();
        
            if (Math.Abs(Time.timeScale) < 0.001f)
                return;
        
            ProcessMovement();
            ProcessAttacks();

            if (transform.position.y < -2)
            {
                TakeDamage(MaxHp / 2f * Time.deltaTime, null);
            }
        }

        private void ProcessCheats()
        {
            if (!Application.isEditor)
                return;
        
            if (Input.GetKeyDown(KeyCode.P))
                SkillPoints += 10;
        
            if (Input.GetKeyDown(KeyCode.L))
                AddExperience(500);
        }

        private void ProcessRegen()
        {
            if (!IsAlive)
                return;
            
            var maxHp = MaxHp;
            if (HP >= maxHp)
                return;

            HP += 0.5f + Regeneration.Value + (0.2f * CurrentLevel);
            if (HP > maxHp)
                HP = maxHp;
        }

        public void TakeDamage(float damage, Transform damageDealer, DamageType type = DamageType.Melee)
        {
            if (!IsAlive)
                return;
            
            HP -= damage;
            OnDamaged?.Invoke();

            if (HP <= 0)
            {
                HP = 0;
                Die();
            }
        }

        private void Die()
        {
            IsAlive = false;
            Animate(AnimState.Die);
        
            Debug.Log("Player died!");

            Observable.Timer(TimeSpan.FromSeconds(5)).Subscribe(_ =>
            {
                _guiController.FadeInAndOut(onFaded: () =>
                {
                    Level.DestroyCurrentLevel();
                
                    var level = _container.InstantiatePrefabForComponent<Level>(_initialLevel);
                    transform.position = level.SpawnPoint;

                    HP = MaxHp / 2f;
                    Animate(AnimState.Idle);
                    IsAlive = true;
                });
            });
        }
    
        private void ProcessAttacks()
        {
            if (_handling.IsAttackingMain)
            {
                if (!_swordAttackCooldown)
                {
                    _sword.StartAttack();

                    if (SwordDancer.Value > 0)
                    { 
                        Observable.Timer(TimeSpan.FromSeconds(0.2f)).Subscribe(_ => _sword2.StartAttack());
                    }

                    _animator.SetTrigger(ANIM_SWORD_ATTACK);
                    _swordAttackCooldown = true;
                    Observable.Timer(TimeSpan.FromSeconds(_attackCooldown)).Subscribe(_ => _swordAttackCooldown = false);
                }
            }

            if (WeaponModule.Value > 0 && _handling.IsAttackingSecondary)
            {
                _gun.Attack(_handling.Target);
            }
        }

        private void ProcessMovement()
        {
            var v = Input.GetAxis("Vertical");
            var h = Input.GetAxis("Horizontal");

            var move = new Vector3(h, 0, v);
            var lookDirection = _handling.Target - _rb.position;
            lookDirection.y = 0;


            var isOnGround = Physics.Raycast(
                _rb.position, Vector3.down * _centerHeight, out var hit, _centerHeight, LayerMask.GetMask("Ground"));

            _head.LookAt(_handling.Target);
        
            if (_gun.gameObject.activeSelf)
                _gun.transform.LookAt(_handling.Target);

            if (move.sqrMagnitude > 0.0002)
            {
                if (isOnGround && !_jumpCooldown)
                    Animate(AnimState.Walking);

                move = Quaternion.LookRotation(lookDirection) * move;
                _rb.MovePosition(_rb.position + Time.deltaTime * _moveSpeed * move.normalized);

                var bodyRotation = Quaternion.LookRotation(lookDirection);
                bodyRotation.eulerAngles = new Vector3(0, bodyRotation.eulerAngles.y, 0);
                _body.rotation = bodyRotation;
            }
            else
            {
                if (isOnGround && !_jumpCooldown)
                    Animate(AnimState.Idle);
            }


            if (Input.GetKeyDown(KeyCode.Space))
            {
                if (isOnGround && !_jumpCooldown)
                {
                    Animate(AnimState.Jumping);
                    _rb.AddForce(_jumpForce * Vector3.up);

                    _jumpCooldown = true;
                    Observable.Timer(TimeSpan.FromSeconds(0.3f)).Subscribe(_ => _jumpCooldown = false);
                }
            }
        }

        private void OnEnemyDied(EnemyDiedSignal signal)
        {
            AddExperience(signal.Enemy.ExpReward);
        }

        private void AddExperience(float experience)
        {
            CurrentExperience += experience;
        
            if (CurrentExperience > CurrentLevelMaxExperience)
            {
                CurrentExperience -= CurrentLevelMaxExperience;
                LevelUp();
                    
                if (CurrentExperience > CurrentLevelMaxExperience)
                    AddExperience(0);
            }
        }

        private void LevelUp()
        {
            CurrentLevel++;
            SkillPoints += 2;
            HP = MaxHp;
        
            _signalBus.Fire(new LevelUpSignal{Level = CurrentLevel});
        }

        private void Animate(AnimState state)
        {
            if (_currentAnimState == state)
                return;

            switch (state)
            {
                case AnimState.Idle:
                    _animator.SetTrigger(ANIM_IDLE);
                    break;
                case AnimState.Walking:
                    _animator.SetTrigger(ANIM_WALK);
                    break;
                case AnimState.Jumping:
                    _animator.SetTrigger(ANIM_JUMP);
                    break;
                case AnimState.Die:
                    _animator.SetTrigger(ANIM_DIE);
                    break;
                default:
                    throw new ArgumentOutOfRangeException(nameof(state), state, null);
            }

            _currentAnimState = state;
        }

        public void OnStep()
        {
            _audioManager.PlayFx(SoundType.Step);
        }
    
        private void OnDrawGizmos()
        {
            Debug.DrawRay(_rb.position, Vector3.down * _centerHeight);
        }
    }
}