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

using System.Buffers;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using Microsoft.AspNetCore.Components.Endpoints.FormMapping;
using Microsoft.AspNetCore.Components.Forms.Mapping;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;

namespace Microsoft.AspNetCore.Components.Endpoints;

internal sealed class HttpContextFormValueMapper : IFormValueMapper
{
    private readonly HttpContextFormDataProvider _formData;
    private readonly FormDataMapperOptions _options;
    private static readonly ConcurrentDictionary<Type, FormValueSupplier> _cache = new();

    public HttpContextFormValueMapper(
        HttpContextFormDataProvider formData,
        IOptions<RazorComponentsServiceOptions> options)
    {
        _formData = formData;
        _options = options.Value._formMappingOptions;
    }

    public bool CanMap(Type valueType, string scopeName, string? formName)
    {
        // We must always match on scope
        if (!_formData.TryGetIncomingHandlerName(out var incomingScopeQualifiedFormName)
            || !MatchesScope(incomingScopeQualifiedFormName, scopeName, out var incomingFormName))
        {
            return false;
        }

        // Matching on formname is optional, enforced only if a nonempty form name was demanded by the receiver
        if (formName is not null && !incomingFormName.Equals(formName, StringComparison.Ordinal))
        {
            return false;
        }

        return _options.ResolveConverter(valueType) is not null;
    }

    private static bool MatchesScope(string incomingScopeQualifiedFormName, string currentMappingScopeName, out ReadOnlySpan<char> incomingFormName)
    {
        if (incomingScopeQualifiedFormName.StartsWith('['))
        {
            // The scope-qualified name is in the form "[scopename]formname", so validate that the [scopename]
            // prefix matches and return the formname part
            var incomingScopeQualifiedFormNameSpan = incomingScopeQualifiedFormName.AsSpan();
            if (incomingScopeQualifiedFormNameSpan[1..].StartsWith(currentMappingScopeName, StringComparison.Ordinal)
                && incomingScopeQualifiedFormName.Length >= currentMappingScopeName.Length + 2
                && incomingScopeQualifiedFormName[currentMappingScopeName.Length + 1] == ']')
            {
                incomingFormName = incomingScopeQualifiedFormNameSpan[(currentMappingScopeName.Length + 2)..];
                return true;
            }
        }
        else
        {
            // The scope-qualified name is in the form "formname", so validating that the scopename matches
            // means checking that it's empty
            if (string.IsNullOrEmpty(currentMappingScopeName))
            {
                incomingFormName = incomingScopeQualifiedFormName;
                return true;
            }
        }

        incomingFormName = default;
        return false;
    }

    public void Map(FormValueMappingContext context)
    {
        // This will func to a proper binder
        if (!CanMap(context.ValueType, context.AcceptMappingScopeName, context.AcceptFormName))
        {
            context.SetResult(null);
        }

        var deserializer = _cache.GetOrAdd(context.ValueType, CreateDeserializer);
        Debug.Assert(deserializer != null);
        deserializer.Deserialize(context, _options, _formData.Entries, _formData.FormFiles);
    }

    private FormValueSupplier CreateDeserializer(Type type) =>
        (FormValueSupplier)Activator.CreateInstance(typeof(FormValueSupplier<>)
        .MakeGenericType(type))!;

    internal abstract class FormValueSupplier
    {
        [RequiresDynamicCode(FormMappingHelpers.RequiresDynamicCodeMessage)]
        [RequiresUnreferencedCode(FormMappingHelpers.RequiresUnreferencedCodeMessage)]
        public abstract void Deserialize(
            FormValueMappingContext context,
            FormDataMapperOptions options,
            IReadOnlyDictionary<string, StringValues> form,
            IFormFileCollection formFiles);
    }

    internal class FormValueSupplier<T> : FormValueSupplier
    {
        [RequiresDynamicCode(FormMappingHelpers.RequiresDynamicCodeMessage)]
        [RequiresUnreferencedCode(FormMappingHelpers.RequiresUnreferencedCodeMessage)]
        public override void Deserialize(
            FormValueMappingContext context,
            FormDataMapperOptions options,
            IReadOnlyDictionary<string, StringValues> form,
            IFormFileCollection formFiles)
        {
            if (form.Count == 0)
            {
                return;
            }

            char[]? buffer = null;
            try
            {
                var dictionary = new Dictionary<FormKey, StringValues>();
                foreach (var (key, value) in form)
                {
                    dictionary.Add(new FormKey(key.AsMemory()), value);
                }
                buffer = ArrayPool<char>.Shared.Rent(options.MaxKeyBufferSize);

                using var reader = new FormDataReader(
                    dictionary,
                    CultureInfo.InvariantCulture,
                    buffer.AsMemory(0, options.MaxKeyBufferSize),
                    formFiles)
                {
                    ErrorHandler = context.OnError,
                    AttachInstanceToErrorsHandler = context.MapErrorToContainer,
                    MaxRecursionDepth = options.MaxRecursionDepth,
                    MaxErrorCount = options.MaxErrorCount,
                    MaxCollectionSize = options.MaxCollectionSize
                };

                reader.PushPrefix(context.ParameterName);
                var result = FormDataMapper.Map<T>(reader, options);
                context.SetResult(result);
            }
            finally
            {
                if (buffer != null)
                {
                    ArrayPool<char>.Shared.Return(buffer);
                }
            }
        }
    }
}
