using DigitalData.Core.Abstraction.Application.Repository;
using DigitalData.Core.Infrastructure.Factory;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using System.ComponentModel.DataAnnotations.Schema;
using System.Reflection;
#if NETFRAMEWORK
using System.Collections.Generic;
using System;
using System.Linq;
#endif

namespace DigitalData.Core.Infrastructure
#if NET
    ;
#elif NETFRAMEWORK
    {
#endif

public static class DependencyInjection
{
    public static IServiceCollection AddDbRepository(this IServiceCollection services, Action<RepositoryConfiguration> options)
    {
        // register services from configuration
        var cfg = new RepositoryConfiguration();
        options.Invoke(cfg);
        cfg.RegisterAllServices(services);

        return services;
    }

    public class RepositoryConfiguration
    {
        // 1. register from assembly
        private readonly Queue<Action<IServiceCollection>> RegsFromAssembly = new Queue<Action<IServiceCollection>>();

        // 2. register entities (can overwrite)
        private readonly Queue<Action<IServiceCollection>> RegsEntity = new Queue<Action<IServiceCollection>>();

        // 3. register db set factories (can overwrite)
        private readonly Queue<Action<IServiceCollection>> RegsDbSetFactory = new Queue<Action<IServiceCollection>>();

        internal void RegisterAllServices(IServiceCollection services)
        {
            // 1. register from assembly
            RegsFromAssembly.InvokeAll(services);

            // 2. register entities (can overwrite)
            RegsEntity.InvokeAll(services);

            // 3. register db set factories (can overwrite)
            RegsDbSetFactory.InvokeAll(services);
        }

        internal RepositoryConfiguration() { }

        public void RegisterFromAssembly<TDbContext>(Assembly assembly) where TDbContext : DbContext
        {
            void reg(IServiceCollection services)
            {
                // scan all types in the Assembly
                var entityTypes = assembly.GetTypes()
                    .Where(t => t.IsClass && !t.IsAbstract && t.GetCustomAttribute<TableAttribute>() != null);

                foreach (var entityType in entityTypes)
                {
                    #region Repository
                    /// register repository
                    // create generic DbRepository<DbContext, TEntity> type
                    var repositoryType = typeof(DbRepository<,>).MakeGenericType(typeof(TDbContext), entityType);
                    var interfaceType = typeof(IRepository<>).MakeGenericType(entityType);

                    // add into DI container as Scoped
                    services.AddScoped(interfaceType, repositoryType);
                    #endregion Repository

                    #region DbSetFactory
                    /// register DbSetFactory
                    var addDbSetFactoryMethod = typeof(DependencyInjection)
                        .GetMethod(nameof(AddDbSetFactory),
                                   BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public);

                    var genericMethod = addDbSetFactoryMethod
#if NET
                                        !
#endif
                                        .MakeGenericMethod(typeof(TDbContext), entityType);
                    genericMethod.Invoke(null, new [] { services, null });
                    #endregion DbSetFactory
                }
            }

            RegsFromAssembly.Enqueue(reg);
        }

        public void RegisterEntity<TDbContext, TEntity>(Func<TDbContext, DbSet<TEntity>>
#if NET
            ?
#endif
            dbSetFactory = null)
            where TDbContext : DbContext
            where TEntity : class
        {
            void reg(IServiceCollection services)
                => services
                    .AddScoped<IRepository<TEntity>, DbRepository<TDbContext, TEntity>>()
                    .AddDbSetFactory(dbSetFactory);

            RegsEntity.Enqueue(reg);
        }

        public void RegisterDbSetFactory<TDbContext, TEntity>(Func<TDbContext, DbSet<TEntity>> dbSetFactory)
            where TDbContext : DbContext
            where TEntity : class
            => RegsDbSetFactory.Enqueue(s => s.AddDbSetFactory(dbSetFactory));
    }

    private static void InvokeAll<T>(this Queue<Action<T>> queue, T services)
    {
        while (queue.Count > 0)
            queue.Dequeue().Invoke(services);
    }

    internal static IServiceCollection AddDbSetFactory<TDbContext, TEntity>(this IServiceCollection services, Func<TDbContext, DbSet<TEntity>>
#if NET
        ?
#endif
        create = null)
        where TDbContext : DbContext
        where TEntity : class
    {
#if NET
        create ??= ctx => ctx.Set<TEntity>();
#elif NETFRAMEWORK
        if(create is null)
            create = ctx => ctx.Set<TEntity>();
#endif
        services.AddSingleton(_ => new DbSetFactory<TDbContext, TEntity>(create));
        return services;
    }
}

#if NETFRAMEWORK
    }
#endif