using System;
using Sirenix.OdinInspector;
using UnityEngine;
using VOID.Generic.Dict;
using VOID.Generic.Objects.Abstract.Events;
using VOID.Generic.Objects.Item;
using VOID.Generic.Objects.Item.Events;
using VOID.Generic.Objects.Unit.Events;
using VOID.Generic.Util;

namespace VOID.Generic.Objects.Unit
{
    /// <summary>
    /// Module of <see cref="UnitObject"/> which manage currently carried items by this unit.
    /// </summary>
    [Serializable]
    public class UnitObjectBackpack : ObjectComponent<UnitObject>
    {
        #region Initialize

        protected override void SelfInitialize()
        {
            // Get empty GameObject where all items from backpack are "attached" - OR - create if missing
            _backpackGameObject = parent.transform.Find("_backpack")?.gameObject;
            if (_backpackGameObject == null)
            {
                _backpackGameObject = new GameObject("_backpack");
                _backpackGameObject.transform.parent = parent.gameObject.transform;
                _backpackGameObject.transform.localPosition = Vector3.zero;
            }
        }

        protected override void Initialize()
        {
            // Instantiate all prefabs
            for (var i = 0; i < items.Length; i++)
            {
                if (items[i] == null) continue;

                // Instantiate prefab and get references to its GameObject and ItemObject
                FakePickOnInit(i, items[i]);
            }
            
            Resize();
        }

        /// <summary>
        /// Only when <see cref="UnitObject"/> is initializing:<br/>
        /// - Instantiate item if it is prefab asset<br/>
        /// - Inform it by events that it has been picked<br/>
        /// - Finally just resume with <see cref="Take(int,VOID.Generic.Objects.Item.ItemObject)"/><br/>
        /// </summary>
        private void FakePickOnInit(int itemIndex, ItemObject item)
        {
            // If item is prefab asset then we need to instantiate it first
            if (!item.gameObject.scene.IsValid())
                item = UnityEngine.Object.Instantiate(item.gameObject).GetComponent<ItemObject>();
            
            var pickEvent = new PickEvent
            {
                item = item,
                unit = parent
            };

            item.itemEvents.onPickedBefore.Invoke(pickEvent);
            item.itemEvents.onPickedAfter.Invoke(pickEvent);

            // Make this slot empty so we can rightfully start whole "Take" machine
            items[itemIndex] = null;
            
            Take(itemIndex, item);
        }

        #endregion

        [ShowInInspector, ReadOnly] private GameObject _backpackGameObject;

        [SerializeField, DisableInPlayMode] private ItemObject[] _items = Array.Empty<ItemObject>();
        public ItemObject[] items => _items;

        /// <summary>
        /// Puts given item into given backpack's slot, if slot is occupied then puts it into first empty.
        /// </summary>
        public void Take(int itemIndex, ItemObject item)
        {
            if (items.Length <= itemIndex)
                Resize(itemIndex);
            
            if (items[itemIndex])
            {
                Take(item);
                return;
            }
            
            items[itemIndex] = item;

            var backpackPickEvent = new BackpackPickEvent
            {
                unit = parent,
                item = item,
                slotIndex = itemIndex,
            };
            
            parent.unitEvents.onBackpackPick.Invoke(backpackPickEvent);

            var itemGameObject = item.gameObject;
            itemGameObject.transform.parent = _backpackGameObject.transform;
            itemGameObject.transform.localPosition = Vector3.zero;
            itemGameObject.transform.localRotation = new Quaternion();
            
            item.basicEvents.onRemove.AddListener(OnItemRemove);
            
            Resize();
        }

        /// <summary>
        /// Puts given item into first empty slot.
        /// </summary>
        public void Take(ItemObject item)
        {
            // Find first free slot in backpack
            var firstFreeIndex = Array.IndexOf(items, null);

            // No free slots, we need to resize array
            if (firstFreeIndex == -1)
            {
                firstFreeIndex = items.Length;
                Resize();
            }
            
            Take(firstFreeIndex, item);
        }

        /// <summary>
        /// Drops item from given slot, BUT! Unit still carry that.<br/>
        /// This is purely for moving item between modules in UnitObject or preparing to drop it on scene.
        /// </summary>
        public void Drop(int itemIndex)
        {
            var tempDroppedItem = items[itemIndex];
            
            items[itemIndex].basicEvents.onRemove.RemoveListener(OnItemRemove);
            
            items[itemIndex] = null;

            var backpackDropEvent = new BackpackDropEvent
            {
                unit = parent,
                item = tempDroppedItem,
                slotIndex = itemIndex,
            };
            
            parent.unitEvents.onBackpackDrop.Invoke(backpackDropEvent);
            
            var tempItemGameObject = tempDroppedItem.gameObject;
            tempItemGameObject.gameObject.transform.parent = null;
            tempItemGameObject.gameObject.transform.position = parent.transform.position;
            
            Resize();
        }
        
        /// <summary>
        /// Finds given item's index and then run: <see cref="Drop(int)"/>
        /// <br/><br/>
        /// <see cref="Drop(int)"/>:
        /// <inheritdoc cref="Drop(int)"/>
        /// </summary>
        public void Drop(ItemObject item)
        {
            Drop(Array.IndexOf(items, item));
        }

        /// <summary>
        /// Swaps content of two slots by given slot indexes.
        /// </summary>
        public void Swap(int firstItemIndex, int secondItemIndex)
        {
            Resize(Math.Max(firstItemIndex, secondItemIndex));
            
            (items[firstItemIndex], items[secondItemIndex]) = (items[secondItemIndex], items[firstItemIndex]);

            var backpackSwapEvent = new BackpackSwapEvent
            {
                unit = parent,
                firstSlotIndex = firstItemIndex,
                firstItem = items[firstItemIndex],
                secondSlotIndex = secondItemIndex,
                secondItem = items[secondItemIndex],
            };
            
            parent.unitEvents.onBackpackSwap.Invoke(backpackSwapEvent);
            
            Resize();
        }

        public void Sort(Func<ItemObject, IComparable> valueGetter, SortDirectionType sortDirection) {
            ItemObjectSortUtil.Sort(ref _items, valueGetter, sortDirection);

            var backpackSortEvent = new BackpackSortEvent {
                unit = parent,
            };

            parent.unitEvents.onBackpackSort.Invoke(backpackSortEvent);

            Resize();
        }

        /// <summary>
        /// Executed when ItemObject is removed. Removes item's reference from backpack.
        /// </summary>
        private void OnItemRemove(RemoveEvent removeEvent)
        {
            var removedItem = removeEvent.obj as ItemObject;
            Drop(removedItem);
        }
        
        /// <summary>
        /// Resizes backpack with rule: last slots row must be empty AND each row have 10 slots
        /// </summary>
        public void Resize(int minSize = 0, int rowSize = 10)
        {
            var beforeSlotCount = items.Length;
            var lastItemIndex = Array.FindLastIndex(items, item => item);
            var afterSlotCount = Mathf.CeilToInt((Mathf.Max(lastItemIndex, minSize) + rowSize) / (float)rowSize) * rowSize;

            // Size not changed, do nothing
            if (beforeSlotCount == afterSlotCount) return;
            
            // Resize backpack's arrays
            var tempItems = items;
            Array.Resize(ref tempItems, afterSlotCount);
            _items = tempItems;

            var backpackResizeEvent = new BackpackResizeEvent
            {
                unit = parent,
                beforeSlotCount = beforeSlotCount,
                afterSlotCount = afterSlotCount,
                lastItemIndex = lastItemIndex,
            };
            
            parent.unitEvents.onBackpackResize.Invoke(backpackResizeEvent);
        }
    }
}
