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)
    {
        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<R> Scan<T, R>(this IEnumerable<T> src, R seed, Func<R, T, R> next)
    {
        var current = seed;

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


    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);
    }
}