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

#nullable enable

using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc.Core;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;

namespace Microsoft.AspNetCore.Mvc.ModelBinding
{
    /// <summary>
    /// A context that contains operating information for model binding and validation.
    /// </summary>
    public class DefaultModelBindingContext : ModelBindingContext
    {
        private static readonly IValueProvider EmptyValueProvider = new CompositeValueProvider();

        private IValueProvider _originalValueProvider = default!;
        private ActionContext _actionContext = default!;
        private ModelStateDictionary _modelState = default!;
        private ValidationStateDictionary _validationState = default!;
        private int? _maxModelBindingRecursionDepth;

        private State _state;
        private readonly Stack<State> _stack = new Stack<State>();

        /// <inheritdoc />
        public override ActionContext ActionContext
        {
            get { return _actionContext; }
            set
            {
                if (value == null)
                {
                    throw new ArgumentNullException(nameof(value));
                }
                _actionContext = value;
            }
        }

        /// <inheritdoc />
        public override string FieldName
        {
            get { return _state.FieldName; }
            set
            {
                if (value == null)
                {
                    throw new ArgumentNullException(nameof(value));
                }
                _state.FieldName = value;
            }
        }

        /// <inheritdoc />
        public override object? Model
        {
            get { return _state.Model; }
            set { _state.Model = value; }
        }

        /// <inheritdoc />
        public override ModelMetadata ModelMetadata
        {
            get { return _state.ModelMetadata; }
            set
            {
                if (value == null)
                {
                    throw new ArgumentNullException(nameof(value));
                }
                _state.ModelMetadata = value;
            }
        }

        /// <inheritdoc />
        public override string ModelName
        {
            get { return _state.ModelName; }
            set
            {
                if (value == null)
                {
                    throw new ArgumentNullException(nameof(value));
                }
                _state.ModelName = value;
            }
        }

        /// <inheritdoc />
        public override ModelStateDictionary ModelState
        {
            get { return _modelState; }
            set
            {
                if (value == null)
                {
                    throw new ArgumentNullException(nameof(value));
                }
                _modelState = value;
            }
        }

        /// <inheritdoc />
        public override string? BinderModelName
        {
            get => _state.BinderModelName;
            set => _state.BinderModelName = value;
        }

        /// <inheritdoc />
        public override BindingSource? BindingSource
        {
            get { return _state.BindingSource; }
            set { _state.BindingSource = value; }
        }

        /// <inheritdoc />
        public override bool IsTopLevelObject
        {
            get { return _state.IsTopLevelObject; }
            set { _state.IsTopLevelObject = value; }
        }

        /// <summary>
        /// Gets or sets the original value provider to be used when value providers are not filtered.
        /// </summary>
        public IValueProvider OriginalValueProvider
        {
            get { return _originalValueProvider; }
            set
            {
                if (value == null)
                {
                    throw new ArgumentNullException(nameof(value));
                }
                _originalValueProvider = value;
            }
        }

        /// <inheritdoc />
        public override IValueProvider ValueProvider
        {
            get { return _state.ValueProvider; }
            set
            {
                if (value == null)
                {
                    throw new ArgumentNullException(nameof(value));
                }
                _state.ValueProvider = value;
            }
        }

        /// <inheritdoc />
        public override Func<ModelMetadata, bool>? PropertyFilter
        {
            get { return _state.PropertyFilter; }
            set { _state.PropertyFilter = value; }
        }

        /// <inheritdoc />
        public override ValidationStateDictionary ValidationState
        {
            get { return _validationState; }
            set
            {
                if (value == null)
                {
                    throw new ArgumentNullException(nameof(value));
                }
                _validationState = value;
            }
        }

        /// <inheritdoc />
        public override ModelBindingResult Result
        {
            get
            {
                return _state.Result;
            }
            set
            {
                _state.Result = value;
            }
        }

        private int MaxModelBindingRecursionDepth
        {
            get
            {
                if (!_maxModelBindingRecursionDepth.HasValue)
                {
                    // Ignore incomplete initialization. This must be a test scenario because CreateBindingContext(...)
                    // has not been called or was called without MvcOptions in the service provider.
                    _maxModelBindingRecursionDepth = MvcOptions.DefaultMaxModelBindingRecursionDepth;
                }

                return _maxModelBindingRecursionDepth.Value;
            }
            set
            {
                _maxModelBindingRecursionDepth = value;
            }
        }

        /// <summary>
        /// Creates a new <see cref="DefaultModelBindingContext"/> for top-level model binding operation.
        /// </summary>
        /// <param name="actionContext">
        /// The <see cref="ActionContext"/> associated with the binding operation.
        /// </param>
        /// <param name="valueProvider">The <see cref="IValueProvider"/> to use for binding.</param>
        /// <param name="metadata"><see cref="ModelMetadata"/> associated with the model.</param>
        /// <param name="bindingInfo"><see cref="BindingInfo"/> associated with the model.</param>
        /// <param name="modelName">The name of the property or parameter being bound.</param>
        /// <returns>A new instance of <see cref="DefaultModelBindingContext"/>.</returns>
        public static ModelBindingContext CreateBindingContext(
            ActionContext actionContext,
            IValueProvider valueProvider,
            ModelMetadata metadata,
            BindingInfo? bindingInfo,
            string modelName)
        {
            if (actionContext == null)
            {
                throw new ArgumentNullException(nameof(actionContext));
            }

            if (valueProvider == null)
            {
                throw new ArgumentNullException(nameof(valueProvider));
            }

            if (metadata == null)
            {
                throw new ArgumentNullException(nameof(metadata));
            }

            if (modelName == null)
            {
                throw new ArgumentNullException(nameof(modelName));
            }

            var binderModelName = bindingInfo?.BinderModelName ?? metadata.BinderModelName;
            var bindingSource = bindingInfo?.BindingSource ?? metadata.BindingSource;
            var propertyFilterProvider = bindingInfo?.PropertyFilterProvider ?? metadata.PropertyFilterProvider;

            var bindingContext = new DefaultModelBindingContext()
            {
                ActionContext = actionContext,
                BinderModelName = binderModelName,
                BindingSource = bindingSource,
                PropertyFilter = propertyFilterProvider?.PropertyFilter,
                ValidationState = new ValidationStateDictionary(),

                // Because this is the top-level context, FieldName and ModelName should be the same.
                FieldName = binderModelName ?? modelName,
                ModelName = binderModelName ?? modelName,
                OriginalModelName = binderModelName ?? modelName,

                IsTopLevelObject = true,
                ModelMetadata = metadata,
                ModelState = actionContext.ModelState,

                OriginalValueProvider = valueProvider,
                ValueProvider = FilterValueProvider(valueProvider, bindingSource),
            };

            // mvcOptions may be null when this method is called in test scenarios.
            var mvcOptions = actionContext.HttpContext.RequestServices?.GetService<IOptions<MvcOptions>>();
            if (mvcOptions != null)
            {
                bindingContext.MaxModelBindingRecursionDepth = mvcOptions.Value.MaxModelBindingRecursionDepth;
            }

            return bindingContext;
        }

        /// <inheritdoc />
        public override NestedScope EnterNestedScope(
            ModelMetadata modelMetadata,
            string fieldName,
            string modelName,
            object? model)
        {
            if (modelMetadata == null)
            {
                throw new ArgumentNullException(nameof(modelMetadata));
            }

            if (fieldName == null)
            {
                throw new ArgumentNullException(nameof(fieldName));
            }

            if (modelName == null)
            {
                throw new ArgumentNullException(nameof(modelName));
            }

            var scope = EnterNestedScope();

            // Only filter if the new BindingSource affects the value providers. Otherwise we want
            // to preserve the current state.
            if (modelMetadata.BindingSource != null && !modelMetadata.BindingSource.IsGreedy)
            {
                ValueProvider = FilterValueProvider(OriginalValueProvider, modelMetadata.BindingSource);
            }

            Model = model;
            ModelMetadata = modelMetadata;
            ModelName = modelName;
            FieldName = fieldName;
            BinderModelName = modelMetadata.BinderModelName;
            BindingSource = modelMetadata.BindingSource;
            PropertyFilter = modelMetadata.PropertyFilterProvider?.PropertyFilter;

            IsTopLevelObject = false;

            return scope;
        }

        /// <inheritdoc />
        public override NestedScope EnterNestedScope()
        {
            _stack.Push(_state);

            // Would this new scope (which isn't in _stack) exceed the allowed recursion depth? That is, has the model
            // binding system already nested MaxModelBindingRecursionDepth binders?
            if (_stack.Count >= MaxModelBindingRecursionDepth)
            {
                // Find the root of this deeply-nested model.
                var states = _stack.ToArray();
                var rootModelType = states[states.Length - 1].ModelMetadata.ModelType;

                throw new InvalidOperationException(Resources.FormatModelBinding_ExceededMaxModelBindingRecursionDepth(
                    nameof(MvcOptions),
                    nameof(MvcOptions.MaxModelBindingRecursionDepth),
                    MaxModelBindingRecursionDepth,
                    rootModelType));
            }

            Result = default;

            return new NestedScope(this);
        }

        /// <inheritdoc />
        protected override void ExitNestedScope()
        {
            _state = _stack.Pop();
        }

        private static IValueProvider FilterValueProvider(IValueProvider valueProvider, BindingSource? bindingSource)
        {
            if (bindingSource == null || bindingSource.IsGreedy)
            {
                return valueProvider;
            }

            if (valueProvider is not IBindingSourceValueProvider bindingSourceValueProvider)
            {
                return valueProvider;
            }

            return bindingSourceValueProvider.Filter(bindingSource) ?? EmptyValueProvider;
        }

        private struct State
        {
            public string FieldName;
            public object? Model;
            public ModelMetadata ModelMetadata;
            public string ModelName;

            public IValueProvider ValueProvider;
            public Func<ModelMetadata, bool>? PropertyFilter;

            public string? BinderModelName;
            public BindingSource? BindingSource;
            public bool IsTopLevelObject;

            public ModelBindingResult Result;
        }
    }
}
