using System;
using System.Collections;
using System.Collections.Generic;
using KitsuneCafe.System;
using ObservableCollections;
using UnityEngine;
using Unit = KitsuneCafe.System.Unit;

namespace KitsuneCafe.ItemSystem
{
    [Serializable]
    public struct InventoryItem : IEquatable<Item>, ICountable
    {
        public Item Item;
        [SerializeField]
        private int count;
        public readonly int Count => count;

        private InventoryItem(Item item, int count)
        {
            Item = item;
            this.count = count;
        }

        public static int Create(Item item, int count, out InventoryItem? inventoryItem)
        {
            int quantity = Math.Clamp(count, 0, item.MaxStackCount);
            inventoryItem = quantity > 0 ? new InventoryItem(item, quantity) : null;
            return quantity;
        }

        public readonly int IncreaseCount(int count, out InventoryItem? item)
        {
            return Create(Item, Count + count, out item);
        }

        public readonly int ReduceCount(int count, out InventoryItem? item)
        {
            return Create(Item, Count - count, out item);
        }

        public readonly bool Equals(Item other)
        {
            return Item == other;
        }

        public override readonly bool Equals(object obj)
        {
            return obj is InventoryItem other
                && Item == other.Item
                && Count == other.Count;
        }

        public override int GetHashCode()
        {
            return HashCode.Combine(Item, Count);
        }
    }

    public enum InventoryError
    {
        None,
        InventoryFull,
        ItemNotFound,
        NotEnoughQuantity,
        ItemNotStackable,
        InvalidQuantity
    }

    public interface ICountable
    {
        int Count { get; }
    }

    public interface IInventory<T> where T : IEquatable<Item>, ICountable
    {
        int Capacity { get; }
        int Count { get; }

        IResult<Unit, InventoryError> Add(Item item, int count = 1);
        IResult<Unit, InventoryError> Remove(Item item, int count = 1);
        IEnumerable<T> Find(Func<T, bool> predicate);
        bool Has(Item item);
    }

    [CreateAssetMenu(menuName = KitsuneCafeMenu.Item + "Inventory")]
    public class Inventory : ScriptableObject, IInventory<InventoryItem>, IEnumerable<InventoryItem>, IObservableCollection<InventoryItem>, ISerializationCallbackReceiver
    {
        [SerializeField]
        private List<InventoryItem> serializableItems = new();

        private ObservableList<InventoryItem> runtimeItems;

        [SerializeField]
        private int capacity = 8;

        public event NotifyCollectionChangedEventHandler<InventoryItem> CollectionChanged = default;

        public int Capacity => capacity;

        public int Count => runtimeItems?.Count ?? 0;

        public bool IsEmpty => Count == 0;
        public bool IsFull => Count == Capacity;

        public object SyncRoot => runtimeItems.SyncRoot;

        public InventoryItem this[int index]
        {
            get => runtimeItems[index];
            set => runtimeItems[index] = value;
        }

        private void OnEnable()
        {
            if (runtimeItems == null)
            {
                runtimeItems = new ObservableList<InventoryItem>();
            }

            runtimeItems.CollectionChanged -= OnCollectionChanged;
            runtimeItems.CollectionChanged += OnCollectionChanged;
        }

        private void OnDisable()
        {
            if (runtimeItems != null)
            {
                runtimeItems.CollectionChanged -= OnCollectionChanged;
            }
        }

        private void OnCollectionChanged(in NotifyCollectionChangedEventArgs<InventoryItem> e)
        {
            CollectionChanged?.Invoke(e);
        }

        public void OnBeforeSerialize()
        {
            if (runtimeItems != null)
            {
                serializableItems.Clear();
                foreach (var item in runtimeItems)
                {
                    serializableItems.Add(item);
                }
            }
        }

        public void OnAfterDeserialize()
        {
            if (runtimeItems == null)
            {
                runtimeItems = new ObservableList<InventoryItem>();
            }
            else
            {
                runtimeItems.Clear();
            }

            foreach (var item in serializableItems)
            {
                runtimeItems.Add(item);
            }
        }

        public IEnumerable<InventoryItem> Find(Func<InventoryItem, bool> predicate)
        {
            for (int i = 0; i < Count; i++)
            {
                var item = runtimeItems[i];

                if (predicate(item))
                {
                    yield return item;
                }
            }
        }

        private IResult<int, InventoryError> AddToExisting(Item item, int count)
        {
            if (count <= 0)
            {
                return Result.Err<int, InventoryError>(InventoryError.InvalidQuantity);
            }

            for (int i = 0; i < Count; i++)
            {
                var existingItem = runtimeItems[i];

                if (existingItem.Equals(item))
                {
                    if (!item.IsStackable)
                    {
                        return Result.Err<int, InventoryError>(InventoryError.ItemNotStackable);
                    }

                    var consumedAmount = existingItem.IncreaseCount(count, out var updatedItem);
                    runtimeItems[i] = updatedItem.Value;

                    count -= consumedAmount;
                    if (count <= 0)
                    {
                        return Result.Ok<int, InventoryError>(0);
                    }
                }
            }

            return Result.Ok<int, InventoryError>(count);
        }

        private IResult<Unit, InventoryError> AddNew(Item item, int count)
        {
            while (count > 0)
            {
                if (IsFull)
                {
                    return Result.Err<Unit, InventoryError>(InventoryError.InventoryFull);
                }

                count -= InventoryItem.Create(item, count, out var newItem);
                runtimeItems.Add(newItem.Value);

            }

            return Result.Ok<Unit, InventoryError>(default);
        }

        public IResult<Unit, InventoryError> Add(Item item, int count = 1)
        {
            return AddToExisting(item, count)
                .Where(count => count > 0)
                .SelectMany(count => AddNew(item, count)) switch
            {
                Ok<Unit, InventoryError> ok => ok,
                Err<Unit, InventoryError>(InventoryError.None) => Result.Ok<Unit, InventoryError>(default),
                var err => err
            };
        }

        public IResult<Unit, InventoryError> Remove(Item item, int count = 1)
        {
            if (count <= 0)
            {
                return Result.Err<Unit, InventoryError>(InventoryError.InvalidQuantity);
            }

            for (int i = runtimeItems.Count - 1; i >= 0; i--)
            {
                var existingItem = runtimeItems[i];

                if (existingItem.Item == item)
                {
                    if (existingItem.Count > count)
                    {
                        existingItem.ReduceCount(count, out var updatedItem);
                        runtimeItems[i] = updatedItem.Value;
                        return Result.Ok<Unit, InventoryError>(Unit.Default);
                    }
                    else
                    {
                        count -= existingItem.Count;
                        runtimeItems.RemoveAt(i);

                        if (count == 0)
                        {
                            return Result.Ok<Unit, InventoryError>(Unit.Default);
                        }
                    }
                }
            }

            if (count > 0)
            {
                return Result.Err<Unit, InventoryError>(InventoryError.NotEnoughQuantity);
            }

            return Result.Ok<Unit, InventoryError>(Unit.Default);
        }

        public bool Has(Item item)
        {
            for (int i = 0; i < Count; i++)
            {
                if (runtimeItems[i].Equals(item))
                {
                    return true;
                }
            }

            return false;
        }

        public IEnumerator<InventoryItem> GetEnumerator()
        {
            return runtimeItems.GetEnumerator();
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }

        public ISynchronizedView<InventoryItem, TView> CreateView<TView>(Func<InventoryItem, TView> transform)
        {
            return runtimeItems.CreateView(transform);
        }
    }
}
