// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Owin;

namespace Microsoft.AspNetCore.Builder
{
    using AddMiddleware = Action<Func<
          Func<IDictionary<string, object>, Task>,
          Func<IDictionary<string, object>, Task>
        >>;
    using AppFunc = Func<IDictionary<string, object>, Task>;
    using CreateMiddleware = Func<
          Func<IDictionary<string, object>, Task>,
          Func<IDictionary<string, object>, Task>
        >;

    /// <summary>
    /// Extension methods to add OWIN to an HTTP application pipeline.
    /// </summary>
    public static class OwinExtensions
    {
        /// <summary>
        /// Adds an OWIN pipeline to the specified <see cref="IApplicationBuilder"/>.
        /// </summary>
        /// <param name="builder">The <see cref="IApplicationBuilder"/> to add the pipeline to.</param>
        /// <returns>An action used to create the OWIN pipeline.</returns>
        public static AddMiddleware UseOwin(this IApplicationBuilder builder)
        {
            if (builder == null)
            {
                throw new ArgumentNullException(nameof(builder));
            }

            AddMiddleware add = middleware =>
            {
                Func<RequestDelegate, RequestDelegate> middleware1 = next1 =>
                {
                    AppFunc exitMiddleware = env =>
                    {
                        return next1((HttpContext)env[typeof(HttpContext).FullName]);
                    };
                    var app = middleware(exitMiddleware);
                    return httpContext =>
                    {
                        // Use the existing OWIN env if there is one.
                        IDictionary<string, object> env;
                        var owinEnvFeature = httpContext.Features.Get<IOwinEnvironmentFeature>();
                        if (owinEnvFeature != null)
                        {
                            env = owinEnvFeature.Environment;
                            env[typeof(HttpContext).FullName] = httpContext;
                        }
                        else
                        {
                            env = new OwinEnvironment(httpContext);
                        }
                        return app.Invoke(env);
                    };
                };
                builder.Use(middleware1);
            };
            // Adapt WebSockets by default.
            add(WebSocketAcceptAdapter.AdaptWebSockets);
            return add;
        }

        /// <summary>
        /// Adds OWIN middleware pipeline to the specified <see cref="IApplicationBuilder"/>.
        /// </summary>
        /// <param name="builder">The <see cref="IApplicationBuilder"/> to add the middleware to.</param>
        /// <param name="pipeline">A delegate which can specify the OWIN pipeline.</param>
        /// <returns>The original <see cref="IApplicationBuilder"/>.</returns>
        public static IApplicationBuilder UseOwin(this IApplicationBuilder builder, Action<AddMiddleware> pipeline)
        {
            if (builder == null)
            {
                throw new ArgumentNullException(nameof(builder));
            }
            if (pipeline == null)
            {
                throw new ArgumentNullException(nameof(pipeline));
            }

            pipeline(builder.UseOwin());
            return builder;
        }

        /// <summary>
        /// Creates an <see cref="IApplicationBuilder"/> for an OWIN pipeline.
        /// </summary>
        /// <param name="app">The OWIN pipeline.</param>
        /// <returns>An <see cref="IApplicationBuilder"/></returns>
        public static IApplicationBuilder UseBuilder(this AddMiddleware app)
        {
            return app.UseBuilder(serviceProvider: null);
        }

        /// <summary>
        /// Creates an <see cref="IApplicationBuilder"/> for an OWIN pipeline.
        /// </summary>
        /// <param name="app">The OWIN pipeline.</param>
        /// <param name="serviceProvider">A service provider for <see cref="IApplicationBuilder.ApplicationServices"/>.</param>
        /// <returns>An <see cref="IApplicationBuilder"/>.</returns>
        public static IApplicationBuilder UseBuilder(this AddMiddleware app, IServiceProvider serviceProvider)
        {
            if (app == null)
            {
                throw new ArgumentNullException(nameof(app));
            }

            // Do not set ApplicationBuilder.ApplicationServices to null. May fail later due to missing services but
            // at least that results in a more useful Exception than a NRE.
            if (serviceProvider == null)
            {
                serviceProvider = new EmptyProvider();
            }

            // Adapt WebSockets by default.
            app(OwinWebSocketAcceptAdapter.AdaptWebSockets);
            var builder = new ApplicationBuilder(serviceProvider: serviceProvider);

            var middleware = CreateMiddlewareFactory(exit =>
            {
                builder.Use(ignored => exit);
                return builder.Build();
            }, builder.ApplicationServices);

            app(middleware);
            return builder;
        }

        private static CreateMiddleware CreateMiddlewareFactory(Func<RequestDelegate, RequestDelegate> middleware, IServiceProvider services)
        {
            return next =>
            {
                var app = middleware(httpContext =>
                {
                    return next(httpContext.Features.Get<IOwinEnvironmentFeature>().Environment);
                });

                return env =>
                {
                    // Use the existing HttpContext if there is one.
                    HttpContext context;
                    object obj;
                    if (env.TryGetValue(typeof(HttpContext).FullName, out obj))
                    {
                        context = (HttpContext)obj;
                        context.Features.Set<IOwinEnvironmentFeature>(new OwinEnvironmentFeature() { Environment = env });
                    }
                    else
                    {
                        context = new DefaultHttpContext(
                                    new FeatureCollection(
                                        new OwinFeatureCollection(env)));
                        context.RequestServices = services;
                    }

                    return app.Invoke(context);
                };
            };
        }

        /// <summary>
        /// Creates an <see cref="IApplicationBuilder"/> for an OWIN pipeline.
        /// </summary>
        /// <param name="app">The OWIN pipeline.</param>
        /// <param name="pipeline">A delegate used to configure a middleware pipeline.</param>
        /// <returns>An <see cref="IApplicationBuilder"/>.</returns>
        public static AddMiddleware UseBuilder(this AddMiddleware app, Action<IApplicationBuilder> pipeline)
        {
            return app.UseBuilder(pipeline, serviceProvider: null);
        }

        /// <summary>
        /// Creates an <see cref="IApplicationBuilder"/> for an OWIN pipeline.
        /// </summary>
        /// <param name="app">The OWIN pipeline.</param>
        /// <param name="pipeline">A delegate used to configure a middleware pipeline.</param>
        /// <param name="serviceProvider">A service provider for <see cref="IApplicationBuilder.ApplicationServices"/>.</param>
        /// <returns>An <see cref="IApplicationBuilder"/>.</returns>
        public static AddMiddleware UseBuilder(this AddMiddleware app, Action<IApplicationBuilder> pipeline, IServiceProvider serviceProvider)
        {
            if (app == null)
            {
                throw new ArgumentNullException(nameof(app));
            }
            if (pipeline == null)
            {
                throw new ArgumentNullException(nameof(pipeline));
            }

            var builder = app.UseBuilder(serviceProvider);
            pipeline(builder);
            return app;
        }

        private class EmptyProvider : IServiceProvider
        {
            public object GetService(Type serviceType)
            {
                return null;
            }
        }
    }
}
