using System;
using System.Collections;
using System.Collections.Generic;
using KitsuneCafe.Sys;
using ObservableCollections;
using UnityEngine;
using Unit = KitsuneCafe.Sys.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, IList, IInventory<InventoryItem>, IEnumerable<InventoryItem>
  {
    [SerializeField]
    private List<InventoryItem> items = new();

    [SerializeField]
    private int capacity = 8;

    public int Capacity => capacity;

    public int Count => items.Count;

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

    public object SyncRoot => ((IList)items).SyncRoot;

    public bool IsFixedSize => ((IList)items).IsFixedSize;

    public bool IsReadOnly => ((IList)items).IsReadOnly;

    public bool IsSynchronized => ((ICollection)items).IsSynchronized;

    object IList.this[int index] { get => items[index]; set => items[index] = (InventoryItem)value; }

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

    public IEnumerable<InventoryItem> Find(Func<InventoryItem, bool> predicate)
    {
      for (int i = 0; i < Count; i++)
      {
        var item = items[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 = items[i];

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

          var consumedAmount = existingItem.IncreaseCount(count, out var updatedItem);
          items[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);
        items.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 = items.Count - 1; i >= 0; i--)
      {
        var existingItem = items[i];

        if (existingItem.Item == item)
        {
          if (existingItem.Count > count)
          {
            existingItem.ReduceCount(count, out var updatedItem);
            items[i] = updatedItem.Value;
            return Result.Ok<Unit, InventoryError>(Unit.Default);
          }
          else
          {
            count -= existingItem.Count;
            items.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 (items[i].Equals(item))
        {
          return true;
        }
      }

      return false;
    }

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

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

    public int Add(object value)
    {
      return ((IList)items).Add(value);
    }

    public void Clear()
    {
      ((IList)items).Clear();
    }

    public bool Contains(object value)
    {
      return ((IList)items).Contains(value);
    }

    public int IndexOf(object value)
    {
      return ((IList)items).IndexOf(value);
    }

    public void Insert(int index, object value)
    {
      ((IList)items).Insert(index, value);
    }

    public void Remove(object value)
    {
      ((IList)items).Remove(value);
    }

    public void RemoveAt(int index)
    {
      ((IList)items).RemoveAt(index);
    }

    public void CopyTo(Array array, int index)
    {
      ((ICollection)items).CopyTo(array, index);
    }
  }
}
