using System.Diagnostics.CodeAnalysis;

namespace InnovEnergy.Lib.Utils;

public static class EnumerableUtils
{
    public static T? FirstOrNullableDefault<T>(this IEnumerable<T> ts, Func<T, Boolean> predicate) where T : struct
    {
        return ts
            .Where(predicate)
            .OfType<T?>()
            .FirstOrDefault();
    }

    public static Int32 SequenceHash<T>(this IEnumerable<T> ts)
    {
        var hash = new HashCode();
        hash.Add(typeof(T).GetHashCode());
        foreach (var t in ts)
            hash.Add(t?.GetHashCode());

        return hash.ToHashCode();
    }

    public static IList<T> Push<T>(this IList<T> l, T t)
    {
        l.Add(t);
        return l;
    }

    public static T Pop<T>(this IList<T> l)
    {
        var i = l.Count - 1;
        var t = l[i];
        l.RemoveAt(i);
        return t;
    }

    // public static async Task<IEnumerable<R>> SelectManyAsync<T, R>(this IEnumerable<T> ts, Func<T, Task<IEnumerable<R>>> func)
    // {
    //     var whenAll = await Task.WhenAll(ts.Select(func));
    //     return whenAll.SelectMany(s => s);
    // }


    // public static async Task<IEnumerable<R>> SelectManyAsync<T, R>(this IEnumerable<T> ts,
    //                                                     Func<T, Task<IEnumerable<R>>> func)
    // {
    //     var result = Enumerable.Empty<R>();
    //
    //     foreach (var t in ts)
    //     {
    //         result = result.Concat(await func(t));
    //     }
    //
    //     return result;
    // }


    // public static IEnumerable<Task<T>> SelectAsync<T>(this Task<IEnumerable<T>> ts)
    // {
    //     yield return ts.ContinueWith(r => r.Result.First());
    //
    //     foreach (var t in ts.Result.Skip(1))
    //         yield return Task.FromResult(t);
    // }


    public static Queue<T> ToQueue<T>(this IEnumerable<T> ts)
    {
        var q = new Queue<T>();
        foreach (var t in ts)
            q.Enqueue(t);

        return q;
    }

    public static Stack<T> ToStack<T>(this IEnumerable<T> ts)
    {
        var s = new Stack<T>();
        foreach (var t in ts)
            s.Push(t);

        return s;
    }

    public static async IAsyncEnumerable<R>
        SelectManyAsync<T, R>(this IEnumerable<T> ts, Func<T, Task<IEnumerable<R>>> func)
    {
        foreach (var t in ts)
        {
            var rs = await func(t);
            foreach (var r in rs)
                yield return r;
        }
    }


    public static IEnumerable<T> SelectTuple<L, R, T>(this IEnumerable<(L l, R r)> tuples, Func<L, R, T> map)
    {
        return tuples.Select(tuple => map(tuple.l, tuple.r));
    }


    public static Boolean AllEqual<T, R>(this IEnumerable<T> ts, Func<T, R> map)
    {
        return ts.Select(map).AllEqual();
    }

    public static Boolean AllEqual<T>(this IEnumerable<T> ts)
    {
        return !ts.Distinct().Skip(1).Any();
    }

    public static IReadOnlyList<T> NullableToReadOnlyList<T>(this T? t)
    {
        return t is null ? Array.Empty<T>() : new[] { t };
    }

    public static IEnumerable<T> NullableToEnumerable<T>(this T? t)
    {
        if (t is not null)
            yield return t;
    }

    public static IEnumerable<(T left, T right)> Pairwise<T>(this IEnumerable<T> ts)
    {
        using var e = ts.GetEnumerator();

        if (!e.MoveNext())
            yield break;

        var left = e.Current;

        while (e.MoveNext())
        {
            yield return (left, e.Current);
            left = e.Current;
        }
    }


    public static IEnumerable<(T left, T right)> Pairwise<T>(this IEnumerable<T> ts, T seed)
    {
        using var e = ts.GetEnumerator();

        var left = seed;

        while (e.MoveNext())
        {
            yield return (left, e.Current);
            left = e.Current;
        }
    }


    public static IEnumerable<Int32> To(this Int32 start, Int32 end)
    {
        var d = Math.Sign(end - start);

        for (var i = start; i != end; i += d)
            yield return i;
    }

    public static IEnumerable<UInt32> To(this UInt32 start, UInt32 end)
    {
        var d = Math.Sign(end - start).ConvertTo<UInt32>();

        for (var i = start; i != end; i += d)
            yield return i;
    }

    public static IReadOnlyList<T> ToReadOnlyList<T>(this IEnumerable<T> src)
    {
        return src switch
        {
            T[] a => a,
            List<T> l => l,
            _ => src.ToList()
        };
    }

    public static IReadOnlyList<T> ToReadOnlyList<T>(this IEnumerable<T> src, Int32 size) => ToArray(src, size);

    public static List<T> ToList<T>(this IEnumerable<T> src, Int32 initialSize)
    {
        var list = new List<T>(initialSize);
        list.AddRange(src);
        return list;
    }


    public static IEnumerable<T> PadEnd<T>(this IEnumerable<T> src, Int32 length, T padding)
    {
        using var enumerator = src.GetEnumerator();

        while (enumerator.MoveNext() && length-- > 0)
            yield return enumerator.Current;

        while (length-- > 0)
            yield return padding;
    }

    public static IEnumerable<T> PadStart<T>(this IEnumerable<T> src, Int32 length, T padding)
    {
        return src
            .Reverse()
            .PadEnd(length, padding)
            .Reverse();
    }


    public static IEnumerator<T> AsSingleEnumerator<T>(this T t)
    {
        yield return t;
    }

    public static IEnumerable<T> AsSingleEnumerable<T>(this T t)
    {
        yield return t;
    }

    public static IEnumerable<T> Flatten<T>(this IEnumerable<IEnumerable<T>> src) => src.SelectMany(s => s);

    [SuppressMessage("ReSharper", "PossibleMultipleEnumeration")]
    public static IEnumerable<T> ForEach<T>(this IEnumerable<T> enumerable, Action<T> action)
    {
        foreach (var e in enumerable)
            action(e);

        return enumerable;
    }

    public static IEnumerable<T> Do<T>(this IEnumerable<T> enumerable, Action<T> action)
    {
        return enumerable.Select(e =>
        {
            action(e);
            return e;
        });
    }

    public static void ForEach<T, R>(this IEnumerable<T> enumerable, Func<T, R> func)
    {
        foreach (var e in enumerable)
            func(e);
    }

    public static IEnumerable<T> EmptyIfNull<T>(this IEnumerable<T>? ts)
    {
        return ts ?? Enumerable.Empty<T>();
    }

    public static T ElementAtOr<T>(this IEnumerable<T> ts, Int32 index, T defaultValue)
    {
        return ts.ElementAtOrDefault(index) ?? defaultValue;
    }

    public static IEnumerable<T> Unfold<T>(this T? seed, Func<T, T?> next)
    {
        var value = seed;
        while (value is not null)
        {
            yield return value;
            value = next(value);
        }
    }


    public static IEnumerable<T> InfinitelyMany<T>(this T value)
    {
        while (true)
            yield return value;

        // ReSharper disable once IteratorNeverReturns
    }

    public static T[] ToArray<T>(this IEnumerable<T> ts, Int32 n)
    {
        if (ts is T[] ta)
            return ta;
        
        var array = new T[n];
        var i = 0;

        using var enumerator = ts.GetEnumerator();

        while (i < n && enumerator.MoveNext())
            array[i++] = enumerator.Current;

        if (enumerator.MoveNext())
            throw new ArgumentOutOfRangeException(nameof(ts));

        return array;
    }

    public static T[] ToArray<T>(this IReadOnlyList<T> ts)
    {
        return ts as T[] ?? ts.ToArray(ts.Count);
    }

  
    
    public static IEnumerable<T> Concat<T>(this IEnumerable<T> ts, T last)
    {
        foreach (var t in ts)
            yield return t;

        yield return last;
    }

    public static IEnumerable<T> Many<T>(this T value)
    {
        yield return value;
    }

    public static IEnumerable<T> PadWith<T>(this IEnumerable<T> enumerable, T padding)
    {
        return enumerable.Concat(padding.InfinitelyMany());
    }

    public static IEnumerable<T> PadWith<T>(this IEnumerable<T> enumerable, T padding, Int32 maxLength)
    {
        return enumerable
            .PadWith(padding)
            .Take(maxLength);
    }


    public static IEnumerable<T> Unless<T>(this IEnumerable<T> seq, Func<T, Boolean> filter)
    {
        return seq.Where(e => !filter(e));
    }

    public static IEnumerable<T> Intersperse<T>(this IEnumerable<T> enumerable, T interspersed)
    {
        using var e = enumerable.GetEnumerator();

        if (!e.MoveNext()) yield break;

        yield return e.Current;

        while (e.MoveNext())
        {
            yield return interspersed;
            yield return e.Current;
        }
    }

    public static T Next<T>(this IEnumerator<T> enumerator)
    {
        enumerator.MoveNext();
        return enumerator.Current;
    }

    public static IEnumerable<T> Scan<T>(this IEnumerable<T> src, Func<T, T, T> next)
    {
        using var e = src.GetEnumerator();

        if (!e.MoveNext())
            yield break;

        var state = e.Current;
        yield return state;
        
        while(e.MoveNext())
        {
            state = next(state, e.Current);
            yield return state;
        }
        
    }
    
    public static IEnumerable<R> Scan<T, R>(this IEnumerable<T> src, R seed, Func<R, T, R> next)
    {
        var current = seed;

        foreach (var t in src)
        {
            yield return current;
            current = next(current, t);
        }
    }


    [SuppressMessage("ReSharper", "AccessToDisposedClosure")]
    public static IEnumerable<IEnumerable<T>> GroupUntil<T>(this IEnumerable<T> sequence, Func<T, T, Boolean> splitBetween)
    {
        using var e = sequence.GetEnumerator();

        if (!e.MoveNext())
            yield break; // empty sequence

        var moreInSeq = false;
        var moreInGroup = false;

        do
        {
            yield return GetGroup();
            while (moreInGroup)
                MoveNext();
        }
        while (moreInSeq);

        void MoveNext()
        {
            var prev = e.Current;
            moreInSeq = e.MoveNext();
            moreInGroup = moreInSeq && !splitBetween(prev, e.Current);
        }

        IEnumerable<T> GetGroup()
        {
            do
            {
                yield return e.Current;
                MoveNext();
            }
            while (moreInGroup);
        }
    }

    public static IEnumerable<T> NotNull<T>(this IEnumerable<T?> ts)
    {
        return ts.Where(t => t != null)!;
    }


    public static IEnumerable<Double> RandomWalk()
    {
        var rng = new Random();
        var x = NextDx();

        while (true)
        {
            yield return x;

            x = (x + NextDx()) / 2;
        }

        Double NextDx() => rng.NextDouble() * 2 - 1;
    }


    public static IEnumerable<Double> Integrate(this IEnumerable<Double> source, Double initConst = 0)
    {
        return source.Scan(initConst, (x, y) => x + y);
    }

    public static IEnumerable<Double> IntegrateNormalize(this IEnumerable<Double> source, Double initConst = 0)
    {
        return source.Scan(initConst, (x, y) => Math.Tanh(x + y));
    }


    public static IEnumerable<Double> MovingAverage(this IEnumerable<Double> source, Int32 windowSize)
    {
        if (windowSize < 1)
            throw new ArgumentException(nameof(windowSize) + " must be positive", nameof(windowSize));

        using var e = source.GetEnumerator();

        if (!e.MoveNext()) yield break;

        var state = e.Current;

        do
        {
            state = ((windowSize - 1) * state + e.Current) / windowSize;
            yield return state;
        }
        while (e.MoveNext());
    }


    public static IEnumerable<T> ZeroOrOne<T>(this T? t)
    {
        if (t is not null)
            yield return t;
    }


    public static IEnumerable<IEnumerable<T>> Transpose<T>(this IEnumerable<IEnumerable<T>> src)
    {
        return src
            .SelectMany(line => line.Select((element, column) => (element, column)))
            .GroupBy(i => i.column, i => i.element);
    }

    public static T GetNext<T>(this IReadOnlyList<T> ts, T current)
    {
        return ts
            .Concat(ts)
            .SkipWhile(t => !t!.Equals(current))
            .Skip(1)
            .First();
    }

    public static T GetPrevious<T>(this IReadOnlyList<T> ts, T current)
    {
        return GetNext(ts.Reverse().ToArray(ts.Count), current);
    }


    public static IEnumerable<R> Memoize<T, R>(this IEnumerable<T> src, Func<T, R> map)
    {
        var cache = new List<R>();

        return src
              .Select((e, i) => i < cache.Count
                              ? cache[i]
                              : map(e).Apply(cache.Add));
    }


    public static HashSet<T> ToHashSet<T>(this IEnumerable<T> source, IEqualityComparer<T>? comparer = null)
    {
        return new HashSet<T>(source, comparer);
    }

    public static IEnumerable<Int32> Times(this Int32 n)
    {
        return Enumerable.Range(0, n);
    }
    
}