// 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.Diagnostics;
using System.Linq;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ActionConstraints;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Routing;
using Resources = Microsoft.AspNetCore.Mvc.Core.Resources;

namespace Microsoft.AspNetCore.Mvc.ApplicationModels
{
    /// <summary>
    /// Creates instances of <see cref="ControllerActionDescriptor"/> from application model
    /// types.
    /// </summary>
    internal static class ControllerActionDescriptorBuilder
    {
        public static IList<ControllerActionDescriptor> Build(ApplicationModel application)
        {
            return ApplicationModelFactory.Flatten(application, CreateActionDescriptor);
        }

        private static ControllerActionDescriptor CreateActionDescriptor(
            ApplicationModel application,
            ControllerModel controller,
            ActionModel action,
            SelectorModel selector)
        {
            var actionDescriptor = new ControllerActionDescriptor
            {
                ActionName = action.ActionName,
                MethodInfo = action.ActionMethod,
            };

            actionDescriptor.ControllerName = controller.ControllerName;
            actionDescriptor.ControllerTypeInfo = controller.ControllerType;
            AddControllerPropertyDescriptors(actionDescriptor, controller);

            AddActionConstraints(actionDescriptor, selector);
            AddEndpointMetadata(actionDescriptor, selector);
            AddAttributeRoute(actionDescriptor, selector);
            AddParameterDescriptors(actionDescriptor, action);
            AddActionFilters(actionDescriptor, action.Filters, controller.Filters, application.Filters);
            AddApiExplorerInfo(actionDescriptor, application, controller, action);
            AddRouteValues(actionDescriptor, controller, action);
            AddProperties(actionDescriptor, action, controller, application);

            return actionDescriptor;
        }

        private static void AddControllerPropertyDescriptors(ActionDescriptor actionDescriptor, ControllerModel controller)
        {
            actionDescriptor.BoundProperties = controller.ControllerProperties
                .Where(p => p.BindingInfo != null)
                .Select(CreateParameterDescriptor)
                .ToList();
        }

        private static void AddParameterDescriptors(ActionDescriptor actionDescriptor, ActionModel action)
        {
            var parameterDescriptors = new List<ParameterDescriptor>(action.Parameters.Count);
            foreach (var parameter in action.Parameters)
            {
                var parameterDescriptor = CreateParameterDescriptor(parameter);
                parameterDescriptors.Add(parameterDescriptor);
            }

            actionDescriptor.Parameters = parameterDescriptors;
        }

        private static ParameterDescriptor CreateParameterDescriptor(ParameterModel parameterModel)
        {
            var parameterDescriptor = new ControllerParameterDescriptor()
            {
                Name = parameterModel.ParameterName,
                ParameterType = parameterModel.ParameterInfo.ParameterType,
                BindingInfo = parameterModel.BindingInfo,
                ParameterInfo = parameterModel.ParameterInfo,
            };

            return parameterDescriptor;
        }

        private static ParameterDescriptor CreateParameterDescriptor(PropertyModel propertyModel)
        {
            var parameterDescriptor = new ControllerBoundPropertyDescriptor()
            {
                BindingInfo = propertyModel.BindingInfo,
                Name = propertyModel.PropertyName,
                ParameterType = propertyModel.PropertyInfo.PropertyType,
                PropertyInfo = propertyModel.PropertyInfo,
            };

            return parameterDescriptor;
        }

        private static void AddApiExplorerInfo(
            ControllerActionDescriptor actionDescriptor,
            ApplicationModel application,
            ControllerModel controller,
            ActionModel action)
        {
            var isVisible =
                action.ApiExplorer?.IsVisible ??
                controller.ApiExplorer?.IsVisible ??
                application.ApiExplorer?.IsVisible ??
                false;

            var isVisibleSetOnActionOrController =
                action.ApiExplorer?.IsVisible ??
                controller.ApiExplorer?.IsVisible ??
                false;

            // ApiExplorer isn't supported on conventional-routed actions, but we still allow you to configure
            // it at the application level when you have a mix of controller types. We'll just skip over enabling
            // ApiExplorer for conventional-routed controllers when this happens.
            var isVisibleSetOnApplication = application.ApiExplorer?.IsVisible ?? false;

            if (isVisibleSetOnActionOrController && !IsAttributeRouted(actionDescriptor))
            {
                // ApiExplorer is only supported on attribute routed actions.
                throw new InvalidOperationException(Resources.FormatApiExplorer_UnsupportedAction(
                    actionDescriptor.DisplayName));
            }
            else if (isVisibleSetOnApplication && !IsAttributeRouted(actionDescriptor))
            {
                // This is the case where we're going to be lenient, just ignore it.
            }
            else if (isVisible)
            {
                Debug.Assert(IsAttributeRouted(actionDescriptor));

                var apiExplorerActionData = new ApiDescriptionActionData()
                {
                    GroupName = action.ApiExplorer?.GroupName ?? controller.ApiExplorer?.GroupName,
                };

                actionDescriptor.SetProperty(apiExplorerActionData);
            }
        }

        private static void AddProperties(
            ControllerActionDescriptor actionDescriptor,
            ActionModel action,
            ControllerModel controller,
            ApplicationModel application)
        {
            foreach (var item in application.Properties)
            {
                actionDescriptor.Properties[item.Key] = item.Value;
            }

            foreach (var item in controller.Properties)
            {
                actionDescriptor.Properties[item.Key] = item.Value;
            }

            foreach (var item in action.Properties)
            {
                actionDescriptor.Properties[item.Key] = item.Value;
            }
        }

        private static void AddActionFilters(
            ControllerActionDescriptor actionDescriptor,
            IEnumerable<IFilterMetadata> actionFilters,
            IEnumerable<IFilterMetadata> controllerFilters,
            IEnumerable<IFilterMetadata> globalFilters)
        {
            actionDescriptor.FilterDescriptors =
                actionFilters.Select(f => new FilterDescriptor(f, FilterScope.Action))
                .Concat(controllerFilters.Select(f => new FilterDescriptor(f, FilterScope.Controller)))
                .Concat(globalFilters.Select(f => new FilterDescriptor(f, FilterScope.Global)))
                .OrderBy(d => d, FilterDescriptorOrderComparer.Comparer)
                .ToList();
        }

        private static void AddActionConstraints(ControllerActionDescriptor actionDescriptor, SelectorModel selectorModel)
        {
            if (selectorModel.ActionConstraints?.Count > 0)
            {
                actionDescriptor.ActionConstraints = new List<IActionConstraintMetadata>(selectorModel.ActionConstraints);
            }
        }

        private static void AddEndpointMetadata(ControllerActionDescriptor actionDescriptor, SelectorModel selectorModel)
        {
            if (selectorModel.EndpointMetadata?.Count > 0)
            {
                actionDescriptor.EndpointMetadata = new List<object>(selectorModel.EndpointMetadata);
            }
        }

        private static void AddAttributeRoute(ControllerActionDescriptor actionDescriptor, SelectorModel selectorModel)
        {
            if (selectorModel.AttributeRouteModel != null)
            {
                actionDescriptor.AttributeRouteInfo = new AttributeRouteInfo
                {
                    Template = selectorModel.AttributeRouteModel.Template,
                    Order = selectorModel.AttributeRouteModel.Order ?? 0,
                    Name = selectorModel.AttributeRouteModel.Name,
                    SuppressLinkGeneration = selectorModel.AttributeRouteModel.SuppressLinkGeneration,
                    SuppressPathMatching = selectorModel.AttributeRouteModel.SuppressPathMatching,
                };
            }
        }

        public static void AddRouteValues(
            ControllerActionDescriptor actionDescriptor,
            ControllerModel controller,
            ActionModel action)
        {
            // Apply all the constraints defined on the action, then controller (for example, [Area])
            // to the actions. Also keep track of all the constraints that require preventing actions
            // without the constraint to match. For example, actions without an [Area] attribute on their
            // controller should not match when a value has been given for area when matching a url or
            // generating a link.
            foreach (var kvp in action.RouteValues)
            {
                // Skip duplicates
                if (!actionDescriptor.RouteValues.ContainsKey(kvp.Key))
                {
                    actionDescriptor.RouteValues.Add(kvp.Key, kvp.Value);
                }
            }

            foreach (var kvp in controller.RouteValues)
            {
                // Skip duplicates - this also means that a value on the action will take precedence
                if (!actionDescriptor.RouteValues.ContainsKey(kvp.Key))
                {
                    actionDescriptor.RouteValues.Add(kvp.Key, kvp.Value);
                }
            }

            // Lastly add the 'default' values
            if (!actionDescriptor.RouteValues.ContainsKey("action"))
            {
                actionDescriptor.RouteValues.Add("action", action.ActionName ?? string.Empty);
            }

            if (!actionDescriptor.RouteValues.ContainsKey("controller"))
            {
                actionDescriptor.RouteValues.Add("controller", controller.ControllerName);
            }
        }

        private static bool IsAttributeRouted(ActionDescriptor actionDescriptor)
        {
            return actionDescriptor.AttributeRouteInfo != null;
        }
    }
}
