using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Threading.Tasks;
using NadekoBot.Common.Collections;
using NadekoBot.Services.Database.Models;

namespace NadekoBot.Extensions
{
    public static class IEnumerableExtensions
    {
        public static string JoinWith<T>(this IEnumerable<T> data, char separator, Func<T, string> func = null)
        {
            if (func is null)
                func = x => x?.ToString() ?? string.Empty;

            return string.Join(separator, data.Select(func));
        }
        
        public static string JoinWith<T>(this IEnumerable<T> data, string separator, Func<T, string> func = null)
        {
            func ??= x => x?.ToString() ?? string.Empty;

            return string.Join(separator, data.Select(func));
        }
        
        public static IEnumerable<T> Distinct<T, U>(this IEnumerable<T> data, Func<T, U> getKey) =>
            data.GroupBy(x => getKey(x))
                .Select(x => x.First());

        /// <summary>
        /// Randomize element order by performing the Fisher-Yates shuffle
        /// </summary>
        /// <typeparam name="T">Item type</typeparam>
        /// <param name="items">Items to shuffle</param>
        public static IReadOnlyList<T> Shuffle<T>(this IEnumerable<T> items)
        {
            using var provider = RandomNumberGenerator.Create();
            var list = items.ToList();
            var n = list.Count;
            while (n > 1)
            {
                var box = new byte[(n / Byte.MaxValue) + 1];
                int boxSum;
                do
                {
                    provider.GetBytes(box);
                    boxSum = box.Sum(b => b);
                } while (!(boxSum < n * ((Byte.MaxValue * box.Length) / n)));

                var k = (boxSum % n);
                n--;
                var value = list[k];
                list[k] = list[n];
                list[n] = value;
            }

            return list;
        }

        public static IEnumerable<T> ForEach<T>(this IEnumerable<T> elems, Action<T> exec)
        {
            var realElems = elems.ToList();
            foreach (var elem in realElems)
            {
                exec(elem);
            }
            
            return realElems;
        }
        
        public static ConcurrentDictionary<TKey, TValue> ToConcurrent<TKey, TValue>(this IEnumerable<KeyValuePair<TKey, TValue>> dict)
            => new ConcurrentDictionary<TKey, TValue>(dict);

        public static IndexedCollection<T> ToIndexed<T>(this IEnumerable<T> enumerable)
            where T : class, IIndexed
            => new IndexedCollection<T>(enumerable);
        
        // Licensed to the .NET Foundation under one or more agreements.
        // The .NET Foundation licenses this file to you under the MIT license.
        
        /// <summary>
        /// Split the elements of a sequence into chunks of size at most <paramref name="size"/>.
        /// </summary>
        /// <remarks>
        /// Every chunk except the last will be of size <paramref name="size"/>.
        /// The last chunk will contain the remaining elements and may be of a smaller size.
        /// </remarks>
        /// <param name="source">
        /// An <see cref="IEnumerable{T}"/> whose elements to chunk.
        /// </param>
        /// <param name="size">
        /// Maximum size of each chunk.
        /// </param>
        /// <typeparam name="TSource">
        /// The type of the elements of source.
        /// </typeparam>
        /// <returns>
        /// An <see cref="IEnumerable{T}"/> that contains the elements the input sequence split into chunks of size <paramref name="size"/>.
        /// </returns>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="source"/> is null.
        /// </exception>
        /// <exception cref="ArgumentOutOfRangeException">
        /// <paramref name="size"/> is below 1.
        /// </exception>
        public static IEnumerable<TSource[]> Chunk<TSource>(this IEnumerable<TSource> source, int size)
        {
            if (source is null)
            {
                throw new ArgumentNullException(nameof(source));
            }
 
            if (size < 1)
            {
                throw new ArgumentOutOfRangeException(nameof(size));
            }
 
            return ChunkIterator(source, size);
        }
 
        private static IEnumerable<TSource[]> ChunkIterator<TSource>(IEnumerable<TSource> source, int size)
        {
            using IEnumerator<TSource> e = source.GetEnumerator();
            while (e.MoveNext())
            {
                TSource[] chunk = new TSource[size];
                chunk[0] = e.Current;
 
                for (int i = 1; i < size; i++)
                {
                    if (!e.MoveNext())
                    {
                        Array.Resize(ref chunk, i);
                        yield return chunk;
                        yield break;
                    }
 
                    chunk[i] = e.Current;
                }
 
                yield return chunk;
            }
        }

        public static Task<TData[]> WhenAll<TData>(this IEnumerable<Task<TData>> items)
            => Task.WhenAll(items);
    }
}