using System;
using System.Collections.Generic;
using System.Linq;

namespace Intrepid.Utilities.Structures.Traits
{
    public sealed class TraitsQuery<T> where T : ITrait
    {
        public IReadOnlyTraits<T> Source { get; }
        private List<Func<IEnumerable<T>, IEnumerable<T>>> Operations { get; } = new();
        private Func<T, float> OrderKey { get; set; }
        private bool OrderDesc { get; set; } = true;
        private int? _skip;
        private int? _take;

        public TraitsQuery(IReadOnlyTraits<T> source)
        {
            Source = source;
        }

        public TraitsQuery<T> Has<TT>() where TT : T
        {
            Operations.Add(seq => seq.Where(_ => Source.Has<TT>()));
            return this;
        }
        
        public TraitsQuery<T> Where<TT>(Func<TT, bool> filter = null) where TT : T
        {
            filter ??= _ => true;
            Operations.Add(seq => seq.Where(_ => Source.TryGet<TT>(out var tt) && filter(tt)));
            return this;
        }

        public TraitsQuery<T> OrderBy(Func<T, float> keySelector, bool descending = true)
        {
            OrderKey = keySelector;
            OrderDesc = descending;
            return this;
        }

        public TraitsQuery<T> Skip(int count)
        {
            if (count < 0)
            {
                return this;
            }

            _skip = count;
            return this;
        }

        public TraitsQuery<T> Take(int count)
        {
            if (count < 0)
            {
                return this;
            }

            _take = count;
            return this;
        }

        public IReadOnlyList<T> ToList() => Materialize().ToList();
        public T FirstOrDefault() => Materialize().FirstOrDefault();
        public bool Any() => Materialize().Any();
        
        public bool TryGet<TT>(out TT trait) where TT : T
        {
            return Source.TryGet(out trait);
        }
        
        private IEnumerable<T> Materialize()
        {
            var query = Source.AllTraits;
            foreach (var operation in Operations)
            {
                query = operation(query);
            }

            if (OrderKey is not null)
            {
                query = OrderDesc
                    ? query.OrderByDescending(OrderKey)
                    : query.OrderBy(OrderKey);
            }

            if (_skip is > 0)
            {
                query = query.Skip(_skip.Value);
            }

            if (_take is > 0)
            {
                query = query.Take(_take.Value);
            }

            return query;
        }
    }
}