using System.Collections.Generic;
using System.Linq;
using Sirenix.OdinInspector;
using UnityEngine;
using UnityEngine.AI;
using VOID.Generic.Objects.Abstract;
using VOID.Generic.Objects.Abstract.Events;
using VOID.Generic.Objects.Container;
using VOID.Generic.Objects.Item;
using VOID.Generic.Objects.Item.Events;
using VOID.Generic.Objects.Unit;
using VOID.Generic.Util;

namespace VOID.Generic.DropTable
{
    public class DropTableComponent : MonoBehaviour
    {
        // Serialized properties
        [InfoBox("<u><b>OnSpawn</b></u> gives items instantly to target, but works only on Unit and Container\n" +
                 "<u><b>OnDeath</b></u> gives or spawns when target dies, works on any object")]
        [SerializeField, EnumToggleButtons] private DropTableTrigger _trigger;
        [SerializeField, HideLabel, Title("Drop Table")] private MultiSourceWrapper<DropTable> _dropTable;
        
        // Public accessors
        public DropTableTrigger trigger => _trigger;
        public DropTable dropTable => _dropTable.value;

        // Cached object attached to
        private AbstractObject _obj;

        /// <summary>
        /// Start listening for attached object events.
        /// If object already spawned and OnSpawn selected then run immediately
        /// </summary>
        private void Start()
        {
            _obj = GetComponent<AbstractObject>();
            if (_trigger is DropTableTrigger.OnSpawn)
            {
                if (_obj.didStart)
                {
                    OnSpawn();
                }
                else
                {
                    _obj.basicEvents.onSpawn.AddListener(OnSpawn);
                }
            }

            if (_trigger is DropTableTrigger.OnDeath)
            {
                _obj.basicEvents.onDeathAfter.AddListener(OnDeath);
            }
        }

        /// <summary>
        /// Return randomized items from given DropTable
        /// </summary>
        /// <returns></returns>
        private List<ItemObject> GetRandomItemsFromDropTable()
        {
            var itemList = new List<ItemObject>();
            
            var groups = _dropTable.value.groups.ToList();
            var groupCounter = new Dictionary<DropTableGroup, int>();
            
            for (var i = 0; i < _dropTable.value.count; i++)
            {
                var randomGroup = RandomUtil.RandomElementByWeight(groups, group => group.weight);
                itemList.AddRange(randomGroup.items);

                if (!randomGroup.isLimited) continue;
                groupCounter.TryAdd(randomGroup, 0);
                groupCounter[randomGroup]++;
                if (groupCounter[randomGroup] < randomGroup.limit) continue;
                groups.Remove(randomGroup);
            }

            return itemList;
        }

        /// <summary>
        /// Run when attached object spawn
        /// </summary>
        private void OnSpawn()
        {
            _obj.basicEvents.onSpawn.RemoveListener(OnSpawn);

            if (_obj is UnitObject unitObject)
            {
                GetRandomItemsFromDropTable().ForEach(item => GiveItemToUnit(item, unitObject));
                return;
            }
            
            if (_obj is ContainerObject containerObject)
            {
                GetRandomItemsFromDropTable().ForEach(item => GiveItemToContainer(item, containerObject));
                return;
            }

            Debug.LogError($"{_obj.GetType()} can't contain items! Selected trigger won't work!");
        }

        /// <summary>
        /// Run when attached object die
        /// </summary>
        private void OnDeath(DeathEvent deathEvent)
        {
            _obj.basicEvents.onDeathAfter.RemoveListener(OnDeath);
            
            if (_obj is UnitObject unitObject)
            {
                GetRandomItemsFromDropTable().ForEach(item => GiveItemToUnit(item, unitObject));
                return;
            }
            
            GetRandomItemsFromDropTable().ForEach(item => DropItemFromObject(item, _obj));
        }

        /// <summary>
        /// Instantiate given item prefab and put directly to attached unit's backpack
        /// </summary>
        private void GiveItemToUnit(ItemObject item, UnitObject unit)
        {
            var newItemGameObject = Instantiate(item.gameObject);
            var newItem = newItemGameObject.GetComponent<ItemObject>();
            
            var pickEvent = new PickEvent
            {
                item = newItem,
                unit = unit
            };

            newItem.itemEvents.onPickedBefore.Invoke(pickEvent);
            newItem.itemEvents.onPickedAfter.Invoke(pickEvent);
            
            unit.unitBackpack.Take(newItem);
        }

        /// <summary>
        /// Instantiate given item prefab and put directly to attached container's content
        /// </summary>
        private void GiveItemToContainer(ItemObject item, ContainerObject container)
        {
            var newItemGameObject = Instantiate(item.gameObject);
            var newItem = newItemGameObject.GetComponent<ItemObject>();
            container.containerContent.MoveIn(newItem);
        }

        /// <summary>
        /// Instantiate given item prefab and "pop" it randomly near attached object
        /// </summary>
        private void DropItemFromObject(ItemObject item, AbstractObject obj)
        {
            const float dropDistance = 1f;
            var newItemGameObject = Instantiate(item, obj.transform.position, obj.transform.rotation);
            var newItem = newItemGameObject.GetComponent<ItemObject>();
            NavMesh.SamplePosition(obj.transform.position + Random.insideUnitSphere, out var hit, dropDistance, NavAreaManager.Walkable);
            if (hit.hit) newItem.Move(hit.position);
        }
    }
}