using System;
using System.Collections.Generic;
using LiteDB;

namespace Dacryptero.Search
{
    public struct SearchIndex : ICloneable
    {
        /// <summary>
        /// if false this means the search result must be one of the <see cref="Ids" />. 
        /// If false this means all <see cref="Ids" /> are excluded from search.
        /// </summary>
        public bool Exclude { get; private set; }

        /// <summary>
        /// The list of Ids that have to be excluded or the search base. If this field is null this
        /// has the same meaning as if this is empty.
        /// </summary>
        public HashSet<ObjectId>? Ids { get; private set; }

        public SearchIndex(bool exclude, IEnumerable<ObjectId> ids)
            : this(exclude, new HashSet<ObjectId>(ids))
        {}

        private SearchIndex(bool exclude, HashSet<ObjectId>? ids)
        {
            Exclude = exclude;
            Ids = ids;
        }

        public static SearchIndex Empty => new SearchIndex(false, null);

        public static SearchIndex All => new SearchIndex(true, null);

        /// <summary>
        /// Keep only the ids in this <see cref="SearchIndex" /> that appear in both. This will
        /// change this object. Some conditions will also change <seeparam cref="other" />.
        /// </summary>
        /// <param name="other">the other <see cref="SearchIndex" /></param>
        public void IntersectWith(SearchIndex other)
        {
            var ids = Ids ??= new HashSet<ObjectId>();
            // 4-way
            switch ((Exclude, other.Exclude))
            {
                case (true, true):
                    if (other.Ids is not null)
                        Ids.UnionWith(other.Ids);
                    break;
                case (true, false):
                {
                    Exclude = false;
                    var otherIds = other.Ids ?? new HashSet<ObjectId>();
                    otherIds.RemoveWhere(x => ids!.Contains(x));
                    Ids = ids = otherIds;
                    break;
                }
                case (false, true):
                {
                    var otherIds = other.Ids ?? new HashSet<ObjectId>();
                    ids.RemoveWhere(x => otherIds.Contains(x));
                    break;
                }
                case (false, false):
                    if (other.Ids is not null)
                        ids.IntersectWith(other.Ids);
                    else ids.Clear();
                    break;
                default: throw new NotSupportedException();
            }
        }

        /// <summary>
        /// Keep the ids that apear in this <see cref="SearchIndex" /> and in <seeparam cref="other"
        /// />. This will change this object. Some conditions will also change <seeparam
        /// cref="other" />.
        /// </summary>
        /// <param name="other">the other <see cref="SearchIndex" /></param>
        public void UnionWith(SearchIndex other)
        {
            var ids = Ids ??= new HashSet<ObjectId>();
            switch ((Exclude, other.Exclude))
            {
                case (true, true):
                    if (other.Ids is not null)
                        ids.IntersectWith(other.Ids);
                    else ids.Clear();
                    break;
                case (true, false):
                {
                    var otherIds = other.Ids ?? new HashSet<ObjectId>();
                    ids.RemoveWhere(x => otherIds.Contains(x));
                    break;
                }
                case (false, true):
                {
                    Exclude = true;
                    var otherIds = other.Ids ?? new HashSet<ObjectId>();
                    otherIds.RemoveWhere(x => ids.Contains(x));
                    Ids = ids = otherIds;
                    break;
                }
                case (false, false):
                    if (other.Ids is not null)
                        Ids.UnionWith(other.Ids);
                    break;
                default: throw new NotSupportedException();
            }
        }
    
        /// <summary>
        /// Negates the search logic.
        /// </summary>
        public void Negate()
        {
            Exclude = !Exclude;
        }

        public SearchIndex Clone()
        {
            return new SearchIndex(Exclude,
                Ids as IEnumerable<ObjectId> ?? System.Linq.Enumerable.Empty<ObjectId>()
            );
        }

        object ICloneable.Clone()
            => Clone();

        public override string ToString()
        {
            return $"{(Exclude ? "Exclude" : "Include")} {Ids?.Count ?? 0} Items";
        }
    }
}