using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;

namespace Dacryptero
{
    public static class Extensions
    {
        public static Memory<U> Select<T, U>(this ReadOnlyMemory<T> memory, Func<T, U> fn)
            => Select(memory.Span, fn);
        public static Memory<U> Select<T, U>(this Memory<T> memory, Func<T, U> fn)
            => Select(memory.Span, fn);
        public static Memory<U> Select<T, U>(this Span<T> span, Func<T, U> fn)
            => Select((ReadOnlySpan<T>)span, fn);
        public static Memory<U> Select<T, U>(this ReadOnlySpan<T> span, Func<T, U> fn)
        {
            Memory<U> result = new U[span.Length];
            for (int i = 0; i < span.Length; ++i)
                result.Span[i] = fn(span[i]);
            return result;
        }

        public static LiteDB.ObjectId? ToObjectId(this string id)
        {
            try
            {
                return new LiteDB.ObjectId(id);
            }
            catch (ArgumentException)
            {
                return null;
            }
        }
    
        public static async IAsyncEnumerable<ReadOnlyMemory<T>> Chunks<T>(
            this IAsyncEnumerable<T> data,
            int chunkSize,
            [EnumeratorCancellation] CancellationToken cancellationToken)
        {
            if (chunkSize <= 0)
                throw new ArgumentOutOfRangeException(nameof(chunkSize));
            Memory<T> chunk = new T[chunkSize];
            int offset = 0;
            var @enum = data.GetAsyncEnumerator(cancellationToken);
            try
            {
                while (true)
                {
                    cancellationToken.ThrowIfCancellationRequested();
                    var nextTask = @enum.MoveNextAsync().AsTask();
                    if (offset > 0)
                    {
                        await Task.WhenAny(
                            nextTask,
                            Task.Delay(1000, cancellationToken)
                        ).ConfigureAwait(false);
                    }
                    else
                    {
                        await nextTask;
                    }
                    if (nextTask.IsCompleted)
                    {
                        if (nextTask.Result)
                            chunk.Span[offset++] = @enum.Current;
                        else break;
                    }
                    if (!nextTask.IsCompleted || offset >= chunk.Length)
                    {
                        yield return chunk[..offset];
                        chunk = new T[chunkSize];
                        offset = 0;
                    }
                }
                if (offset > 0)
                    yield return chunk[..offset];
            }
            finally
            {
                await @enum.DisposeAsync();
            }
        }
    
        public static void EnsureCapacity<T>(this List<T> list, int additionalElements)
        {
            list.Capacity = Math.Max(list.Capacity, list.Count + additionalElements);
        }

        public static void WriteBson(this System.Text.Json.Utf8JsonWriter writer, 
            string propertyName, LiteDB.BsonValue value)
        {
            writer.WritePropertyName(propertyName);
            writer.WriteBsonValue(value);
        }

        public static void WriteBsonValue(this System.Text.Json.Utf8JsonWriter writer,
            LiteDB.BsonValue value)
        {
            switch (value.Type)
            {
                case LiteDB.BsonType.Null:
                    writer.WriteNullValue();
                    break;
                case LiteDB.BsonType.Int32:
                    writer.WriteNumberValue(value.AsInt32);
                    break;
                case LiteDB.BsonType.Int64:
                    writer.WriteNumberValue(value.AsInt64);
                    break;
                case LiteDB.BsonType.Double:
                    writer.WriteNumberValue(value.AsDouble);
                    break;
                case LiteDB.BsonType.Decimal:
                    writer.WriteNumberValue(value.AsDecimal);
                    break;
                case LiteDB.BsonType.String:
                    writer.WriteStringValue(value.AsString);
                    break;
                case LiteDB.BsonType.Document:
                    writer.WriteStartObject();
                    foreach (var (key, val) in value.AsDocument)
                    {
                        writer.WritePropertyName(key);
                        writer.WriteBsonValue(val);
                    }
                    writer.WriteEndObject();
                    break;
                case LiteDB.BsonType.Array:
                    writer.WriteStartArray();
                    foreach (var val in value.AsArray)
                        writer.WriteBsonValue(val);
                    writer.WriteEndArray();
                    break;
                case LiteDB.BsonType.Binary:
                    writer.WriteBase64StringValue(value.AsBinary);
                    break;
                case LiteDB.BsonType.ObjectId:
                    writer.WriteStringValue(value.AsObjectId.ToString());
                    break;
                case LiteDB.BsonType.Guid:
                    writer.WriteStringValue(value.AsGuid);
                    break;
                case LiteDB.BsonType.Boolean:
                    writer.WriteBooleanValue(value.AsBoolean);
                    break;
                case LiteDB.BsonType.DateTime:
                    writer.WriteStringValue(value.AsDateTime);
                    break;
                default:
                    throw new InvalidOperationException($"Invalid bson type {value.Type}");
            }
        }

        public static LiteDB.ILiteQueryableResult<T> Where<T>(this LiteDB.ILiteQueryable<T> query,
            Search.SearchIndex index
        )
            where T : Data.Base
        {
            if (index.Ids is null || index.Ids.Count == 0)
            {
                if (index.Exclude)
                    return query;
                else return query.Limit(0);
                // else return query.Where(_ => false);
            }
            else
            {
                if (index.Exclude)
                    return query.Where(x => !index.Ids.Contains(x.Id));
                else return query.Where(x => index.Ids.Contains(x.Id));
            }
        }

        public static bool ToSuccess(this DB.DBValue<bool>? value)
        {
            return value is not null && value.Value.Value;
        }
    }
}