// 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.Linq;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;

namespace Microsoft.AspNetCore.Mvc.ApplicationModels
{
    /// <summary>
    /// Constructs a <see cref="CompiledPageActionDescriptor"/> from an <see cref="PageApplicationModel"/>.
    /// </summary>
    internal static class CompiledPageActionDescriptorBuilder
    {
        /// <summary>
        /// Creates a <see cref="CompiledPageActionDescriptor"/> from the specified <paramref name="applicationModel"/>.
        /// </summary>
        /// <param name="applicationModel">The <see cref="PageApplicationModel"/>.</param>
        /// <param name="globalFilters">Global filters to apply to the page.</param>
        /// <returns>The <see cref="CompiledPageActionDescriptor"/>.</returns>
        public static CompiledPageActionDescriptor Build(
            PageApplicationModel applicationModel,
            FilterCollection globalFilters)
        {
            var boundProperties = CreateBoundProperties(applicationModel);
            var filters = Enumerable.Concat(
                    globalFilters.Select(f => new FilterDescriptor(f, FilterScope.Global)),
                    applicationModel.Filters.Select(f => new FilterDescriptor(f, FilterScope.Action)))
                .ToArray();
            var handlerMethods = CreateHandlerMethods(applicationModel);

            if (applicationModel.ModelType != null && applicationModel.DeclaredModelType != null &&
                !applicationModel.DeclaredModelType.IsAssignableFrom(applicationModel.ModelType))
            {
                var message = Resources.FormatInvalidActionDescriptorModelType(
                    applicationModel.ActionDescriptor.DisplayName,
                    applicationModel.ModelType.Name,
                    applicationModel.DeclaredModelType.Name);

                throw new InvalidOperationException(message);
            }

            var actionDescriptor = applicationModel.ActionDescriptor;
            return new CompiledPageActionDescriptor(actionDescriptor)
            {
                ActionConstraints = actionDescriptor.ActionConstraints,
                AttributeRouteInfo = actionDescriptor.AttributeRouteInfo,
                BoundProperties = boundProperties,
                EndpointMetadata = CreateEndPointMetadata(applicationModel),
                FilterDescriptors = filters,
                HandlerMethods = handlerMethods,
                HandlerTypeInfo = applicationModel.HandlerType,
                DeclaredModelTypeInfo = applicationModel.DeclaredModelType,
                ModelTypeInfo = applicationModel.ModelType,
                RouteValues = actionDescriptor.RouteValues,
                PageTypeInfo = applicationModel.PageType,
                Properties = applicationModel.Properties,
            };
        }

        private static IList<object> CreateEndPointMetadata(PageApplicationModel applicationModel)
        {
            var handlerMetatdata = applicationModel.HandlerTypeAttributes;
            var endpointMetadata = applicationModel.EndpointMetadata;

            // It is criticial to get the order in which metadata appears in endpoint metadata correct. More significant metadata
            // must appear later in the sequence.
            // In this case, handlerMetadata is attributes on the Page \ PageModel, and endPointMetadata is configured via conventions. and
            // We consider the latter to be more significant.
            return Enumerable.Concat(handlerMetatdata, endpointMetadata).ToList();
        }

        // Internal for unit testing
        internal static HandlerMethodDescriptor[] CreateHandlerMethods(PageApplicationModel applicationModel)
        {
            var handlerModels = applicationModel.HandlerMethods;
            var handlerDescriptors = new HandlerMethodDescriptor[handlerModels.Count];

            for (var i = 0; i < handlerDescriptors.Length; i++)
            {
                var handlerModel = handlerModels[i];

                handlerDescriptors[i] = new HandlerMethodDescriptor
                {
                    HttpMethod = handlerModel.HttpMethod,
                    Name = handlerModel.HandlerName,
                    MethodInfo = handlerModel.MethodInfo,
                    Parameters = CreateHandlerParameters(handlerModel),
                };
            }

            return handlerDescriptors;
        }

        // internal for unit testing
        internal static HandlerParameterDescriptor[] CreateHandlerParameters(PageHandlerModel handlerModel)
        {
            var methodParameters = handlerModel.Parameters;
            var parameters = new HandlerParameterDescriptor[methodParameters.Count];

            for (var i = 0; i < parameters.Length; i++)
            {
                var parameterModel = methodParameters[i];

                parameters[i] = new HandlerParameterDescriptor
                {
                    BindingInfo = parameterModel.BindingInfo,
                    Name = parameterModel.ParameterName,
                    ParameterInfo = parameterModel.ParameterInfo,
                    ParameterType = parameterModel.ParameterInfo.ParameterType,
                };
            }

            return parameters;
        }

        // internal for unit testing
        internal static PageBoundPropertyDescriptor[] CreateBoundProperties(PageApplicationModel applicationModel)
        {
            var results = new List<PageBoundPropertyDescriptor>();
            var properties = applicationModel.HandlerProperties;
            for (var i = 0; i < properties.Count; i++)
            {
                var propertyModel = properties[i];

                // Only add properties which are explicitly marked to bind.
                if (propertyModel.BindingInfo == null)
                {
                    continue;
                }

                var descriptor = new PageBoundPropertyDescriptor
                {
                    Property = propertyModel.PropertyInfo,
                    Name = propertyModel.PropertyName,
                    BindingInfo = propertyModel.BindingInfo,
                    ParameterType = propertyModel.PropertyInfo.PropertyType,
                };

                results.Add(descriptor);
            }

            return results.ToArray();
        }
    }
}