using System;
using System.Collections.Generic;
using System.Linq;
using Sirenix.OdinInspector;
using Sirenix.Utilities;
using UnityEngine;
using VOID.Generic.Dict;
using VOID.Generic.Objects.Abstract.Events;
using VOID.Generic.Objects.Accessory;
using VOID.Generic.Objects.Armor;
using VOID.Generic.Objects.Item.Events;
using VOID.Generic.Objects.Unit.Equipment;
using VOID.Generic.Objects.Unit.Events;
using VOID.Generic.Objects.Weapon;
using VOID.Generic.Objects.Wearable;
using VOID.Generic.Objects.Wearable.Events;
using static VOID.Generic.Util.WearableUtils;

namespace VOID.Generic.Objects.Unit
{
    /// <summary>
    /// Module of <see cref="UnitObject"/> which manage currently equipped items by this unit.<br/>
    /// List of available equip slots: <see cref="EquipmentSlotType"/>
    /// </summary>
    [Serializable]
    public class UnitObjectEquipment : ObjectComponent<UnitObject>
    {
        #region Initialize

        protected override void SelfInitialize()
        {
            // Empty GameObject where all items from backpack are "attached"
            _equipmentGameObject = parent.transform.Find("_equipment")?.gameObject;
            if (_equipmentGameObject == null)
            {
                _equipmentGameObject = new GameObject("_equipment");
                _equipmentGameObject.transform.parent = parent.gameObject.transform;
                _equipmentGameObject.transform.localPosition = Vector3.zero;
            }
        }

        protected override void Initialize()
        {
            // Equip all items pre-equipped from inspector
            Enum.GetValues(typeof(EquipmentSlotType)).OfType<EquipmentSlotType>()
                .Where(slotType => GetEquipped(slotType))
                .ForEach(slotType => FakeEquipOnInitForPrefab(GetEquipped(slotType), slotType));
        }
        
        private WearableObject FakeEquipOnInitForPrefab(WearableObject wearable, EquipmentSlotType slotType)
        {
            // If item is prefab asset then we need to instantiate it first
            if (wearable.gameObject.scene.IsValid() == false)
            {
                wearable = UnityEngine.Object.Instantiate(wearable).GetComponent<WeaponObject>();
            }
            
            var pickEvent = new PickEvent
            {
                item = wearable,
                unit = parent
            };
            var equipEvent = new EquipEvent
            {
                wearable = wearable,
                unit = parent,
                slotIndex = (int)slotType,
                slotType = slotType
            };
            
            wearable.itemEvents.onPickedBefore.Invoke(pickEvent);
            wearable.itemEvents.onPickedAfter.Invoke(pickEvent);
            wearable.wearableEvents.onEquippedBefore.Invoke(equipEvent);
            wearable.wearableEvents.onEquippedAfter.Invoke(equipEvent);
            parent.unitEvents.onEquipBefore.Invoke(equipEvent);
            parent.unitEvents.onEquipAfter.Invoke(equipEvent);

            return wearable;
        }

        #endregion

        /// <summary>
        /// For each <see cref="EquipmentSlotType"/> func checking if given wearable can be equipped
        /// </summary>
        private static readonly Dictionary<EquipmentSlotType, Func<WearableObject, bool>> CanWearInSlotType = new()
        {
            [EquipmentSlotType.MainHand] = wearable => IsWeaponCarryType(wearable, WeaponCarryType.TwoHand, WeaponCarryType.OneHandAny, WeaponCarryType.OneHandMain),
            [EquipmentSlotType.OffHand] = wearable => IsWeaponCarryType(wearable, WeaponCarryType.OneHandAny, WeaponCarryType.OneHandOff),
            [EquipmentSlotType.Helmet] = wearable => IsType(wearable, ArmorType.Helmet),
            [EquipmentSlotType.Armor] = wearable => IsType(wearable, ArmorType.Armor),
            [EquipmentSlotType.Pants] = wearable => IsType(wearable, ArmorType.Pants),
            [EquipmentSlotType.Belt] = wearable => IsType(wearable, ArmorType.Belt),
            [EquipmentSlotType.Boots] = wearable => IsType(wearable, ArmorType.Boots),
            [EquipmentSlotType.Gloves] = wearable => IsType(wearable, ArmorType.Gloves),
            [EquipmentSlotType.Necklace] = wearable => IsType(wearable, AccessoryType.Necklace),
            [EquipmentSlotType.RingOne] = wearable => IsType(wearable, AccessoryType.Ring),
            [EquipmentSlotType.RingTwo] = wearable => IsType(wearable, AccessoryType.Ring),
        };
        
        [ShowInInspector, ReadOnly] private GameObject _equipmentGameObject;

        [SerializeField, DisableInPlayMode] private EquippedItem mainHand;
        [SerializeField, DisableInPlayMode] private EquippedItem offHand;
        [SerializeField, DisableInPlayMode] private EquippedItem helmet;
        [SerializeField, DisableInPlayMode] private EquippedItem armor;
        [SerializeField, DisableInPlayMode] private EquippedItem pants;
        [SerializeField, DisableInPlayMode] private EquippedItem belt;
        [SerializeField, DisableInPlayMode] private EquippedItem boots;
        [SerializeField, DisableInPlayMode] private EquippedItem gloves;
        [SerializeField, DisableInPlayMode] private EquippedItem necklace;
        [SerializeField, DisableInPlayMode] private EquippedItem ringOne;
        [SerializeField, DisableInPlayMode] private EquippedItem ringTwo;

        /// <summary>
        /// Returns equipped WearableObject in given slot.
        /// </summary>
        public WearableObject GetEquipped(EquipmentSlotType equipmentSlotType)
        {
            var equipItem = GetEquippedItemByType(equipmentSlotType);
            return equipItem.wearable ? equipItem.wearable : null;
        }
        
        /// <summary>
        /// Returns equopped WearableObject in given slot index. By enum: <see cref="EquipmentSlotType"/>
        /// </summary>
        public WearableObject GetEquipped(int equipmentSlotIndex)
        {
            return GetEquipped((EquipmentSlotType)equipmentSlotIndex);
        }

        /// <summary>
        /// Checks if given WearableObject can be equipped in given Slot.
        /// </summary>
        public bool CanEquipInSlot(EquipmentSlotType equipmentSlotType, WearableObject wearable)
        {
            return CanWearInSlotType.GetValueOrDefault(equipmentSlotType).Invoke(wearable);
        }

        /// <summary>
        /// Checks if given WearableObject can be equipped in given Slot index. By enum: <see cref="EquipmentSlotType"/>
        /// </summary>
        public bool CanEquipInSlot(int equipmentSlotIndex, WearableObject wearable)
        {
            return CanEquipInSlot((EquipmentSlotType)equipmentSlotIndex, wearable);
        }
        
        /// <summary>
        /// Equip given WearableObject in first valid slot.
        /// </summary>
        public void Equip(WearableObject wearable)
        {
            var equipRingOne = !ringOne.wearable || ringTwo.wearable;
            var equipRingTwo = ringOne.wearable && !ringTwo.wearable;
            var equipmentSlotType = wearable switch
            {
                ArmorObject when IsType(wearable, ArmorType.Armor) => EquipmentSlotType.Armor,
                ArmorObject when IsType(wearable, ArmorType.Belt) => EquipmentSlotType.Belt,
                ArmorObject when IsType(wearable, ArmorType.Boots) => EquipmentSlotType.Boots,
                ArmorObject when IsType(wearable, ArmorType.Gloves) => EquipmentSlotType.Gloves,
                ArmorObject when IsType(wearable, ArmorType.Helmet) => EquipmentSlotType.Helmet,
                ArmorObject when IsType(wearable, ArmorType.Pants) => EquipmentSlotType.Pants,
                AccessoryObject when IsType(wearable, AccessoryType.Necklace) => EquipmentSlotType.Necklace,
                AccessoryObject when IsType(wearable, AccessoryType.Ring) && equipRingOne => EquipmentSlotType.RingOne,
                AccessoryObject when IsType(wearable, AccessoryType.Ring) && equipRingTwo => EquipmentSlotType.RingTwo,
                WeaponObject when IsWeaponCarryType(wearable, WeaponCarryType.TwoHand, WeaponCarryType.OneHandAny, WeaponCarryType.OneHandMain) => EquipmentSlotType.MainHand,
                WeaponObject when IsWeaponCarryType(wearable, WeaponCarryType.OneHandAny, WeaponCarryType.OneHandOff) => EquipmentSlotType.OffHand,
                _ => throw new ArgumentOutOfRangeException(nameof(wearable))
            };
            
            Equip(equipmentSlotType, wearable);
        }
        
        /// <summary>
        /// Equip given WearableObject in given slot index. By enum: <see cref="EquipmentSlotType"/>
        /// </summary>
        public void Equip(int equipmentSlotIndex, WearableObject wearable)
        {
            Equip((EquipmentSlotType)equipmentSlotIndex, wearable);
        }

        /// <summary>
        /// Equip given WearableObject in given slot.
        /// </summary>
        public void Equip(EquipmentSlotType equipmentSlotType, WearableObject wearable)
        {
            if (!CanEquipInSlot(equipmentSlotType, wearable))
            {
                Debug.LogError($"Given wearableItem '{wearable.name}' cannot be equipped in that slot", wearable);
                return;
            }
            
            // Someone already equipped that item
            if (wearable.wearableState.wearerUnit && wearable.wearableState.wearerUnit != parent)
            {
                Debug.LogError($"Given WearableItem '${wearable.name}' is already equipped by someone else!", wearable);
                return;
            }

            // Someone else already carrying that item
            if (wearable.itemState.carriedBy && wearable.itemState.carriedBy != parent)
            {
                Debug.LogError($"Given WearableItem '{wearable.name}' is already carried by someone else!", wearable);
                return;
            }
            
            // If item remove itself then we need to remove it from
            wearable.basicEvents.onRemove.AddListener(OnItemRemove);
            
            // Update equipped item
            var equippedItem = GetEquippedItemByType(equipmentSlotType);
            equippedItem.wearable = wearable;
            equippedItem.gameObject.transform.SetParent(_equipmentGameObject.transform);
            equippedItem.gameObject.transform.SetLocalPositionAndRotation(Vector3.zero, new Quaternion());
            
            parent.unitEvents.onEquipmentEquip.Invoke(new EquipmentEquipEvent
            {
                unit = parent,
                wearable = wearable,
                equipmentSlot = equipmentSlotType
            });
        }

        /// <summary>
        /// Unequip WearableObject from given slot index. By enum: <see cref="EquipmentSlotType"/>
        /// </summary>
        public void UnEquip(int index)
        {
            UnEquip((EquipmentSlotType)index);
        }
        
        /// <summary>
        /// Unequip WearableObject.
        /// </summary>
        public void UnEquip(WearableObject wearable)
        {
            var equipmentSlotType = wearable switch
            {
                WeaponObject when wearable == mainHand.wearable => EquipmentSlotType.MainHand,
                WeaponObject when wearable == offHand.wearable => EquipmentSlotType.OffHand,
                ArmorObject when wearable == helmet.wearable => EquipmentSlotType.Helmet,
                ArmorObject when wearable == armor.wearable => EquipmentSlotType.Armor,
                ArmorObject when wearable == pants.wearable => EquipmentSlotType.Pants,
                ArmorObject when wearable == belt.wearable => EquipmentSlotType.Belt,
                ArmorObject when wearable == boots.wearable => EquipmentSlotType.Boots,
                ArmorObject when wearable == gloves.wearable => EquipmentSlotType.Gloves,
                AccessoryObject when wearable == necklace.wearable => EquipmentSlotType.Necklace,
                AccessoryObject when wearable == ringOne.wearable => EquipmentSlotType.RingOne,
                AccessoryObject when wearable == ringTwo.wearable => EquipmentSlotType.RingTwo,
                _ => throw new ArgumentOutOfRangeException(nameof(wearable))
            };
            
            UnEquip(equipmentSlotType);
        }

        /// <summary>
        /// Unequip WearableObject from given slot.
        /// </summary>
        public void UnEquip(EquipmentSlotType equipmentSlotType)
        {
            var equippedItem = GetEquippedItemByType(equipmentSlotType);
            var tempWearable = equippedItem.wearable;
            
            tempWearable.basicEvents.onRemove.RemoveListener(OnItemRemove);
            
            // Remove reference from equipped slot
            equippedItem.wearable = null;
            
            parent.unitEvents.onEquipmentUnEquip.Invoke(new EquipmentUnEquipEvent
            {
                unit = parent,
                wearable = tempWearable,
                equipmentSlot = equipmentSlotType
            });
        }

        /// <summary>
        /// Returns <see cref="EquippedItem"/> for given <see cref="EquipmentSlotType"/>.
        /// </summary>
        private EquippedItem GetEquippedItemByType(EquipmentSlotType equipmentSlot)
        {
            return equipmentSlot switch
            {
                EquipmentSlotType.MainHand => mainHand,
                EquipmentSlotType.OffHand => offHand,
                EquipmentSlotType.Helmet => helmet,
                EquipmentSlotType.Armor => armor,
                EquipmentSlotType.Pants => pants,
                EquipmentSlotType.Belt => belt,
                EquipmentSlotType.Boots => boots,
                EquipmentSlotType.Gloves => gloves,
                EquipmentSlotType.Necklace => necklace,
                EquipmentSlotType.RingOne => ringOne,
                EquipmentSlotType.RingTwo => ringTwo,
                _ => throw new ArgumentOutOfRangeException(nameof(equipmentSlot), equipmentSlot, null)
            };
        }

        /// <summary>
        /// When item remove itself then also remove it from equipment.
        /// </summary>
        private void OnItemRemove(RemoveEvent removeEvent)
        {
            UnEquip(removeEvent.obj as WearableObject);
        }
    }
}
