using System.Collections.Generic;
using System.Linq;

using UnityEngine;
using UnityEngine.Events;

using Photon.Pun;

using UPG.Extensions;
using UPG.Debugging;

#region CLASS EVENTS
namespace UnityEngine.Events
{
    [System.Serializable] public class LootChestEvent : UnityEvent<BR.BattleRoyale.Items.LootChest> { };
}
#endregion

namespace BR.BattleRoyale.Items
{
    [RequireComponent(typeof(Animator))]
    [RequireComponent(typeof(PhotonView))]
    public class LootChest : MonoBehaviourPun, IDisplayable
    {
        #region STATIC
        public static List<LootChest> ActiveLootChests = new List<LootChest>();
        #endregion

        #region EVENTS
        public static LootChestEvent OnLootChestSpawned = new LootChestEvent();

        public static LootChestEvent OnChestRangeEntered = new LootChestEvent();
        public IDisplayableEvent OnNoLongerPreviewable =>
            e_onNoLongerPreviewable;
        IDisplayableEvent e_onNoLongerPreviewable = new IDisplayableEvent();

        public static LootChestEvent OnChestOpened = new LootChestEvent();
        [HideInInspector] public UnityEvent OnOpened = new UnityEvent();
        #endregion

        #region COMPONENTS
        Animator m_animator;
        #endregion

        #region STRUCTS
        [System.Serializable]
        public struct LootChestData
        {
            public Vector3 Position { get; private set; }
            public Quaternion Rotation { get; private set; }
            public string DropTableName { get; private set; }
            public int MaxDrops { get; private set; }
            public float[] Chances { get; private set; }

            public LootChestData(Vector3 _position, Quaternion _rotation, string _tableName, int _maxDrops, float[] _chances) =>
                (Position, Rotation, DropTableName, MaxDrops, Chances) = (_position, _rotation, _tableName, _maxDrops, _chances);
            public LootChestData(Vector3 _position, Quaternion _rotation, DropTable _table, int _maxDrops, float[] _chances) =>
                (Position, Rotation, DropTableName, MaxDrops, Chances) = (_position, _rotation, _table?.name, _maxDrops, _chances);
        }
        #endregion

        #region INSPECTOR FIELDS
        [Header("Drop Table")]
        [SerializeField] DropTable m_dropTable;

        [Header("Drop Settings")]
        [SerializeField] [Range(1, 5)] int m_maxDrops;
        [SerializeField] [Range(0, 1)] float m_greyChance = 0.35f;
        [SerializeField] [Range(0, 1)] float m_greenChance = 0.25f;
        [SerializeField] [Range(0, 1)] float m_blueChance = 0.2f;
        [SerializeField] [Range(0, 1)] float m_purpleChance = 0.15f;
        [SerializeField] [Range(0, 1)] float m_goldChance = 0.05f;

        [Header("Debug")]
        [SerializeField] DebugChannel m_debugChannel = null;
        #endregion

        #region PRIVATE FIELDS
        List<Item> m_chestItems = new List<Item>();
        bool m_opened = false;
        #endregion

        #region PUBLIC FIELDS
        float m_greyChanceInternal = -1;
        float m_greenChanceInternal = -1;
        float m_blueChanceInternal = -1;
        float m_purpleChanceInternal = -1;
        float m_goldChanceInternal = -1;

        public bool Opened
        {
            get => m_opened;
            private set
            {
                if (m_opened == value)
                    return;

                if (m_opened = value)
                {
                    OnChestOpened?.Invoke(this);
                    OnOpened?.Invoke();
                }
            }
        }
        #endregion

        #region READABLES
        public LootChestData ToData() =>
            new LootChestData(transform.position, transform.rotation, m_dropTable, m_maxDrops, new float[]  { m_greyChance, m_greenChance, m_blueChance, m_purpleChance, m_goldChance });
        public Vector3 ItemSpawnPoint =>
            transform.position + Vector3.up * 1.75f;
        #endregion

        #region DEFAULT METHODS
        void OnValidate()
        {
            if (m_maxDrops > 5 || m_maxDrops < 2)
                m_maxDrops = Mathf.Clamp(m_maxDrops, 2, 5);

            if (m_greyChanceInternal == -1 || m_greenChanceInternal == -1 || m_blueChanceInternal == -1 || m_purpleChanceInternal == -1 || m_goldChanceInternal == -1)
            {
                m_greyChanceInternal = m_greyChance = 0.2f;

                m_greenChanceInternal = m_greenChance = 0.2f;

                m_blueChanceInternal = m_blueChance = 0.2f;

                m_purpleChanceInternal = m_purpleChance = 0.2f;

                m_goldChanceInternal = m_goldChance = 0.2f;
            }

            if (m_greyChanceInternal != m_greyChance || m_greenChanceInternal != m_greenChance || m_blueChanceInternal != m_blueChance || m_purpleChanceInternal != m_purpleChance || m_goldChanceInternal != m_goldChance)
            {
                float _aggregate = 0;
                float _remainder = 1;

                bool _greyHasChanged = m_greyChanceInternal != m_greyChance;
                bool _greenHasChanged = m_greenChanceInternal != m_greenChance;
                bool _blueHasChanged = m_blueChanceInternal != m_blueChance;
                bool _purpleHasChanged = m_purpleChanceInternal != m_purpleChance;
                bool _goldHasChanged = m_goldChanceInternal != m_goldChance;

                if (_greyHasChanged)
                {
                    _remainder -= m_greyChance;

                    m_greyChanceInternal = m_greyChance;
                }
                else
                    _aggregate += m_greyChance;

                if (_greenHasChanged)
                {
                    _remainder -= m_greenChance;

                    m_greenChanceInternal = m_greenChance;
                }
                else
                    _aggregate += m_greenChance;

                if (_blueHasChanged)
                {
                    _remainder -= m_blueChance;

                    m_blueChanceInternal = m_blueChance;
                }
                else
                    _aggregate += m_blueChance;

                if (_purpleHasChanged)
                {
                    _remainder -= m_purpleChance;

                    m_purpleChanceInternal = m_purpleChance;
                }
                else
                    _aggregate += m_purpleChance;

                if (_goldHasChanged)
                {
                    _remainder -= m_goldChance;

                    m_goldChanceInternal = m_goldChance;
                }
                else
                    _aggregate += m_goldChance;

                if (!_greyHasChanged)
                    m_greyChanceInternal = m_greyChance = _aggregate == 0 ? 0.2f : Mathf.Clamp01(_remainder * (m_greyChance / _aggregate));

                if (!_greenHasChanged)
                    m_greenChanceInternal = m_greenChance = _aggregate == 0 ? 0.2f : Mathf.Clamp01(_remainder * (m_greenChance / _aggregate));

                if (!_blueHasChanged)
                    m_blueChanceInternal = m_blueChance = _aggregate == 0 ? 0.2f : Mathf.Clamp01(_remainder * (m_blueChance / _aggregate));

                if (!_purpleHasChanged)
                    m_purpleChanceInternal = m_purpleChance = _aggregate == 0 ? 0.2f : Mathf.Clamp01(_remainder * (m_purpleChance / _aggregate));

                if (!_goldHasChanged)
                    m_goldChanceInternal = m_goldChance = _aggregate == 0 ? 0.2f : Mathf.Clamp01(_remainder * (m_goldChance / _aggregate));
            }
        }
        void Awake()
        {
#if UNITY_EDITOR
            if (!UnityEditor.EditorApplication.isPlaying)
                return;
#endif

            if (!gameObject.TryGetComponent(out m_animator))
            {
                Destroy(gameObject);

                return;
            }

            Opened = false;

            SetAllMeshCollidersToIgnoreRaycastsLayer(transform);
            MakeChildOfNearestLevelPrefab();

            ActiveLootChests.AddIfNew(this);

            OnLootChestSpawned?.Invoke(this);
        }
        void Update()
        {
            if (transform.lossyScale != Vector3.one * 1f)
                transform.SetLossyScale(Vector3.one * 1f);
        }
        void OnAnimatorIK()
        {
            
        }
        void OnDestroy()
        {
            ActiveLootChests.TryRemove(this);
        }
#if UNITY_EDITOR
        void OnDrawGizmos()
        {
            if (transform.lossyScale != Vector3.one * 1f)
                transform.SetLossyScale(Vector3.one * 1f);
        }
#endif
        void OnTriggerEnter(Collider other)
        {
            if (Opened)
                return;

            if (!other.gameObject.TryGetComponent(out Player _player))
                return;

            if (!_player.IsLocal)
                return;

            m_debugChannel?.Raise(this, gameObject.name + " is in proximity of local player");

            OnChestRangeEntered?.Invoke(this);

            InputManager.OnInteract.AddListener(LocalOpen);
        }
        void OnTriggerExit(Collider other)
        {
            if (m_opened)
                return;

            if (!other.gameObject.TryGetComponent(out Player _player))
                return;

            if (!_player.IsLocal)
                return;

            m_debugChannel?.Raise(this, gameObject.name + " is no longer in proximity of local player");

            OnNoLongerPreviewable?.Invoke(this);

            InputManager.OnInteract.RemoveListener(LocalOpen);
        }
        #endregion

        #region DATA METHODS
        public void SetData(LootChestData _data)
        {
            transform.position = _data.Position;
            transform.rotation = _data.Rotation;
            
            m_dropTable = DropTable.GetTableByName(_data.DropTableName);

            m_maxDrops = _data.MaxDrops;

            if (_data.Chances.Length > 0)
                m_greyChance = _data.Chances[0];

            if (_data.Chances.Length > 1)
                m_greenChance = _data.Chances[1];

            if (_data.Chances.Length > 2)
                m_blueChance = _data.Chances[2];

            if (_data.Chances.Length > 3)
                m_purpleChance = _data.Chances[3];

            if (_data.Chances.Length > 4)
                m_goldChance = _data.Chances[4];

            m_debugChannel?.Raise(this, "Spawned at " + transform.position);

            if (photonView.IsMine)
                photonView.RPC(nameof(SetData), RpcTarget.Others, _data.Position, _data.Rotation, _data.DropTableName, _data.MaxDrops, _data.Chances);
        }
        #endregion

        #region OPEN/DROP METHODS
        void LocalOpen()
        {
            if (Opened)
                return;

            if (PhotonNetwork.IsConnected && PhotonNetwork.InRoom)
                photonView?.RPC(nameof(Open), RpcTarget.All);
            else
                Open();
        }
        public GameObject[] GenerateDrops()
        {
            List<GameObject> _objects = new List<GameObject>();

            if (!m_dropTable || m_dropTable.Wands.Count == 0 && m_dropTable.Potions.Count == 0)
                return _objects.ToArray();

            for (int i = 0; i < Random.Range(1, m_maxDrops); i++)
            {
                float _selection = Random.Range(0f, 1);

                Rarity _rarity = (Rarity)(_selection * (System.Enum.GetValues(typeof(Rarity)).Length - 1));

                GameObject _selectedObject = m_dropTable.GetRandomDropByRarity(_rarity);

                if (!_selectedObject)
                {
                    m_debugChannel?.Raise(this, "Failed to acquire valid drop from " + m_dropTable.name);

                    continue;
                }

                m_debugChannel?.Raise(this, "Added " + _selectedObject + " to drop pool");

                _objects.Add(_selectedObject);
            }

            return _objects.ToArray();
        }
        public void LaunchItems()
        {
            if (PhotonNetwork.IsConnected && PhotonNetwork.InRoom && photonView && !photonView.IsMine)
                return;

            GameObject[] _drops = GenerateDrops();

            float _anglePerDrop = 45f / _drops.Length;

            for (int i = 0; i < _drops.Length; i++)
            {
                GameObject _drop = null;

                if (PhotonNetwork.IsConnected && PhotonNetwork.InRoom)
                {
                    string _filePath = 
                        (_drops[i].GetComponent<Wand>() ? "Prefabs/Wands" : "Prefabs/Potions") + 
                        "/" + _drops[i].name;

                    if (!Resources.Load<GameObject>(_filePath))
                        continue;

                    if (PhotonNetwork.IsMasterClient)
                        _drop = PhotonNetwork.InstantiateSceneObject(_filePath, ItemSpawnPoint, transform.rotation * Quaternion.Euler(0, (i * (45f / _drops.Length)) - 22.5f, 0), 0, new object[] { photonView.ViewID });
                }
                else
                    _drop = Instantiate(_drops[i], ItemSpawnPoint, transform.rotation * Quaternion.Euler(0, (i * _anglePerDrop) - 22.5f, 0));

                if (_drop.TryGetComponent(out Item _item))
                    _item.BounceTowards(Quaternion.Euler(0, (i * (45f / _drops.Length)) - 22.5f, 0) * transform.forward * 5);
            }

            OnChestOpened?.Invoke(this);
        }
        #endregion

        #region MISC METHODS
        void SetAllMeshCollidersToIgnoreRaycastsLayer(Transform t)
        {
            for (int i = 0; i < t.childCount; i++)
                SetAllMeshCollidersToIgnoreRaycastsLayer(t.GetChild(i));

            if (t.gameObject.TryGetComponent(out MeshCollider _))
                t.gameObject.layer = LayerMask.NameToLayer("Ignore Raycast");
        }
        void MakeChildOfNearestLevelPrefab()
        {
            if (GetComponentInParent<Level>())
                return;

            List<Level> _levels = FindObjectsOfType<Level>().ToList();

            if (_levels == null || _levels.Count == 0)
                return;

            transform.parent = _levels.OrderBy(x => x.transform.position.DistanceTo(x.transform.position)).ToArray()[0].transform;
        }
        #endregion

        #region RPC METHODS
        [PunRPC]
        public void SetData(Vector3 _position, Quaternion _rotation, string _dropTableName, int _maxDrops, float[] _chances) =>
            SetData(new LootChestData(_position, _rotation, _dropTableName, _maxDrops, _chances));
        [PunRPC]
        public void Open()
        {
            if (!m_opened)
                m_opened = true;

            m_animator.Play("Open");

            OnOpened?.Invoke();
        }
        #endregion

        #region CONTEXT MENU
        [ContextMenu("Reset Values")]
        void ResetValues()
        {
            m_greyChance = -1;
            m_greenChance = -1;
            m_blueChance = -1;
            m_purpleChance = -1;
            m_goldChance = -1;
        }
        #endregion
    }
}