// 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.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Security.Claims;
using System.Threading;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Http.Features.Authentication;
using Microsoft.Extensions.DependencyInjection;

namespace Microsoft.AspNetCore.Http
{
    /// <summary>
    /// Represents an implementation of the HTTP Context class.
    /// </summary>
    public sealed class DefaultHttpContext : HttpContext
    {
        // The initial size of the feature collection when using the default constructor; based on number of common features
        // https://github.com/dotnet/aspnetcore/issues/31249
        private const int DefaultFeatureCollectionSize = 10;

        // Lambdas hoisted to static readonly fields to improve inlining https://github.com/dotnet/roslyn/issues/13624
        private static readonly Func<IFeatureCollection, IItemsFeature> _newItemsFeature = f => new ItemsFeature();
        private static readonly Func<DefaultHttpContext, IServiceProvidersFeature> _newServiceProvidersFeature = context => new RequestServicesFeature(context, context.ServiceScopeFactory);
        private static readonly Func<IFeatureCollection, IHttpAuthenticationFeature> _newHttpAuthenticationFeature = f => new HttpAuthenticationFeature();
        private static readonly Func<IFeatureCollection, IHttpRequestLifetimeFeature> _newHttpRequestLifetimeFeature = f => new HttpRequestLifetimeFeature();
        private static readonly Func<IFeatureCollection, ISessionFeature> _newSessionFeature = f => new DefaultSessionFeature();
        private static readonly Func<IFeatureCollection, ISessionFeature?> _nullSessionFeature = f => null;
        private static readonly Func<IFeatureCollection, IHttpRequestIdentifierFeature> _newHttpRequestIdentifierFeature = f => new HttpRequestIdentifierFeature();

        private FeatureReferences<FeatureInterfaces> _features;

        private readonly DefaultHttpRequest _request;
        private readonly DefaultHttpResponse _response;

        private DefaultConnectionInfo? _connection;
        private DefaultWebSocketManager? _websockets;

        // This is field exists to make analyzing memory dumps easier.
        // https://github.com/dotnet/aspnetcore/issues/29709
        internal bool _active;

        /// <summary>
        /// Initializes a new instance of the <see cref="DefaultHttpContext"/> class.
        /// </summary>
        public DefaultHttpContext()
            : this(new FeatureCollection(DefaultFeatureCollectionSize))
        {
            Features.Set<IHttpRequestFeature>(new HttpRequestFeature());
            Features.Set<IHttpResponseFeature>(new HttpResponseFeature());
            Features.Set<IHttpResponseBodyFeature>(new StreamResponseBodyFeature(Stream.Null));
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="DefaultHttpContext"/> class with provided features.
        /// </summary>
        /// <param name="features">Initial set of features for the <see cref="DefaultHttpContext"/>.</param>
        public DefaultHttpContext(IFeatureCollection features)
        {
            _features.Initalize(features);
            _request = new DefaultHttpRequest(this);
            _response = new DefaultHttpResponse(this);
        }

        /// <summary>
        /// Reinitialize  the current instant of the class with features passed in.
        /// </summary>
        /// <remarks>
        /// This method allows the consumer to re-use the <see cref="DefaultHttpContext" /> for another request, rather than having to allocate a new instance.
        /// </remarks>
        /// <param name="features">The new set of features for the <see cref="DefaultHttpContext" />.</param>
        public void Initialize(IFeatureCollection features)
        {
            var revision = features.Revision;
            _features.Initalize(features, revision);
            _request.Initialize(revision);
            _response.Initialize(revision);
            _connection?.Initialize(features, revision);
            _websockets?.Initialize(features, revision);
            _active = true;
        }

        /// <summary>
        /// Uninitialize all the features in the <see cref="DefaultHttpContext" />.
        /// </summary>
        public void Uninitialize()
        {
            _features = default;
            _request.Uninitialize();
            _response.Uninitialize();
            _connection?.Uninitialize();
            _websockets?.Uninitialize();
            _active = false;
        }

        /// <summary>
        /// Gets or set the <see cref="FormOptions" /> for this instance.
        /// </summary>
        /// <returns>
        /// <see cref="FormOptions"/>
        /// </returns>
        public FormOptions FormOptions { get; set; } = default!;

        /// <summary>
        /// Gets or sets the <see cref="IServiceScopeFactory" /> for this instance.
        /// </summary>
        /// <returns>
        /// <see cref="IServiceScopeFactory"/>
        /// </returns>
        public IServiceScopeFactory ServiceScopeFactory { get; set; } = default!;

        private IItemsFeature ItemsFeature =>
            _features.Fetch(ref _features.Cache.Items, _newItemsFeature)!;

        private IServiceProvidersFeature ServiceProvidersFeature =>
            _features.Fetch(ref _features.Cache.ServiceProviders, this, _newServiceProvidersFeature)!;

        private IHttpAuthenticationFeature HttpAuthenticationFeature =>
            _features.Fetch(ref _features.Cache.Authentication, _newHttpAuthenticationFeature)!;

        private IHttpRequestLifetimeFeature LifetimeFeature =>
            _features.Fetch(ref _features.Cache.Lifetime, _newHttpRequestLifetimeFeature)!;

        private ISessionFeature SessionFeature =>
            _features.Fetch(ref _features.Cache.Session, _newSessionFeature)!;

        private ISessionFeature? SessionFeatureOrNull =>
            _features.Fetch(ref _features.Cache.Session, _nullSessionFeature);

        private IHttpRequestIdentifierFeature RequestIdentifierFeature =>
            _features.Fetch(ref _features.Cache.RequestIdentifier, _newHttpRequestIdentifierFeature)!;

        /// <inheritdoc/>
        public override IFeatureCollection Features => _features.Collection ?? ContextDisposed();

        /// <inheritdoc/>
        public override HttpRequest Request => _request;

        /// <inheritdoc/>
        public override HttpResponse Response => _response;

        /// <inheritdoc/>
        public override ConnectionInfo Connection => _connection ?? (_connection = new DefaultConnectionInfo(Features));

        /// <inheritdoc/>
        public override WebSocketManager WebSockets => _websockets ?? (_websockets = new DefaultWebSocketManager(Features));

        /// <inheritdoc/>
        public override ClaimsPrincipal User
        {
            get
            {
                var user = HttpAuthenticationFeature.User;
                if (user == null)
                {
                    user = new ClaimsPrincipal(new ClaimsIdentity());
                    HttpAuthenticationFeature.User = user;
                }
                return user;
            }
            set { HttpAuthenticationFeature.User = value; }
        }

        /// <inheritdoc/>
        public override IDictionary<object, object?> Items
        {
            get { return ItemsFeature.Items; }
            set { ItemsFeature.Items = value; }
        }

        /// <inheritdoc/>
        public override IServiceProvider RequestServices
        {
            get { return ServiceProvidersFeature.RequestServices; }
            set { ServiceProvidersFeature.RequestServices = value; }
        }

        /// <inheritdoc/>
        public override CancellationToken RequestAborted
        {
            get { return LifetimeFeature.RequestAborted; }
            set { LifetimeFeature.RequestAborted = value; }
        }

        /// <inheritdoc/>
        public override string TraceIdentifier
        {
            get { return RequestIdentifierFeature.TraceIdentifier; }
            set { RequestIdentifierFeature.TraceIdentifier = value; }
        }

        /// <inheritdoc/>
        public override ISession Session
        {
            get
            {
                var feature = SessionFeatureOrNull;
                if (feature == null)
                {
                    throw new InvalidOperationException("Session has not been configured for this application " +
                        "or request.");
                }
                return feature.Session;
            }
            set
            {
                SessionFeature.Session = value;
            }
        }

        // This property exists because of backwards compatibility.
        // We send an anonymous object with an HttpContext property
        // via DiagnosticListener in various events throughout the pipeline. Instead
        // we just send the HttpContext to avoid extra allocations
        /// <summary>
        /// This API is used by ASP.NET Core's infrastructure and should not be used by application code.
        /// </summary>
        [EditorBrowsable(EditorBrowsableState.Never)]
        public HttpContext HttpContext => this;

        /// <inheritdoc/>
        public override void Abort()
        {
            LifetimeFeature.Abort();
        }

        private static IFeatureCollection ContextDisposed()
        {
            ThrowContextDisposed();
            return null;
        }

        [DoesNotReturn]
        private static void ThrowContextDisposed()
        {
            throw new ObjectDisposedException(nameof(HttpContext), $"Request has finished and {nameof(HttpContext)} disposed.");
        }

        struct FeatureInterfaces
        {
            public IItemsFeature? Items;
            public IServiceProvidersFeature? ServiceProviders;
            public IHttpAuthenticationFeature? Authentication;
            public IHttpRequestLifetimeFeature? Lifetime;
            public ISessionFeature? Session;
            public IHttpRequestIdentifierFeature? RequestIdentifier;
        }
    }
}
