// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ActionConstraints;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Matching;

namespace Microsoft.AspNetCore.Mvc.Routing
{
    // This is a bridge that allows us to execute IActionConstraint instance when
    // used with Matcher.
    internal class ActionConstraintMatcherPolicy : MatcherPolicy, IEndpointSelectorPolicy
    {
        private static readonly IReadOnlyList<Endpoint> EmptyEndpoints = Array.Empty<Endpoint>();

        // We need to be able to run IActionConstraints on Endpoints that aren't associated
        // with an action. This is a sentinel value we use when the endpoint isn't from MVC.
        internal static readonly ActionDescriptor NonAction = new ActionDescriptor();

        private readonly ActionConstraintCache _actionConstraintCache;

        public ActionConstraintMatcherPolicy(ActionConstraintCache actionConstraintCache)
        {
            _actionConstraintCache = actionConstraintCache;
        }

        // Run really late.
        public override int Order => 100000;

        public bool AppliesToEndpoints(IReadOnlyList<Endpoint> endpoints)
        {
            if (endpoints == null)
            {
                throw new ArgumentNullException(nameof(endpoints));
            }

            // We can skip over action constraints when they aren't any for this set
            // of endpoints. This happens once on startup so it removes this component
            // from the code path in most scenarios.
            for (var i = 0; i < endpoints.Count; i++)
            {
                var endpoint = endpoints[i];
                var action = endpoint.Metadata.GetMetadata<ActionDescriptor>();
                if (action?.ActionConstraints?.Count > 0 && HasSignificantActionConstraint(action))
                {
                    // We need to check for some specific action constraint implementations.
                    // We've implemented consumes, and HTTP method support inside endpoint routing, so
                    // we don't need to run an 'action constraint phase' if those are the only constraints.
                    return true;
                }
            }

            return false;

            bool HasSignificantActionConstraint(ActionDescriptor a)
            {
                for (var i = 0; i < a.ActionConstraints.Count; i++)
                {
                    var actionConstraint = a.ActionConstraints[i];
                    if (actionConstraint.GetType() == typeof(HttpMethodActionConstraint))
                    {
                        // This one is OK, we implement this in endpoint routing.
                    }
                    else if (actionConstraint.GetType() == typeof(ConsumesAttribute))
                    {
                        // This one is OK, we implement this in endpoint routing.
                    }
                    else
                    {
                        return true;
                    }
                }

                return false;
            }
        }

        public Task ApplyAsync(HttpContext httpContext, CandidateSet candidateSet)
        {
            var finalMatches = EvaluateActionConstraints(httpContext, candidateSet);

            // We've computed the set of actions that still apply (and their indices)
            // First, mark everything as invalid, and then mark everything in the matching
            // set as valid. This is O(2n) vs O(n**2)
            for (var i = 0; i < candidateSet.Count; i++)
            {
                candidateSet.SetValidity(i, false);
            }

            if (finalMatches != null)
            {
                for (var i = 0; i < finalMatches.Count; i++)
                {
                    candidateSet.SetValidity(finalMatches[i].index, true);
                }
            }

            return Task.CompletedTask;
        }

        // This is almost the same as the code in ActionSelector, but we can't really share the logic
        // because we need to track the index of each candidate - and, each candidate has its own route
        // values.
        private IReadOnlyList<(int index, ActionSelectorCandidate candidate)> EvaluateActionConstraints(
            HttpContext httpContext,
            CandidateSet candidateSet)
        {
            var items = new List<(int index, ActionSelectorCandidate candidate)>();

            // We want to execute a group at a time (based on score) so keep track of the score that we've seen.
            int? score = null;

            // Perf: Avoid allocations
            for (var i = 0; i < candidateSet.Count; i++)
            {
                if (candidateSet.IsValidCandidate(i))
                {
                    ref var candidate = ref candidateSet[i];
                    if (score != null && score != candidate.Score)
                    {
                        // This is the end of a group.
                        var matches = EvaluateActionConstraintsCore(httpContext, candidateSet, items, startingOrder: null);
                        if (matches?.Count > 0)
                        {
                            return matches;
                        }

                        // If we didn't find matches, then reset.
                        items.Clear();
                    }

                    score = candidate.Score;

                    // If we get here, this is either the first endpoint or the we just (unsuccessfully)
                    // executed constraints for a group.
                    //
                    // So keep adding constraints.
                    var endpoint = candidate.Endpoint;
                    var actionDescriptor = endpoint.Metadata.GetMetadata<ActionDescriptor>();

                    IReadOnlyList<IActionConstraint> constraints = Array.Empty<IActionConstraint>();
                    if (actionDescriptor != null)
                    {
                        constraints = _actionConstraintCache.GetActionConstraints(httpContext, actionDescriptor);
                    }

                    // Capture the index. We need this later to look up the endpoint/route values.
                    items.Add((i, new ActionSelectorCandidate(actionDescriptor ?? NonAction, constraints)));
                }
            }

            // Handle residue
            return EvaluateActionConstraintsCore(httpContext, candidateSet, items, startingOrder: null);
        }

        private IReadOnlyList<(int index, ActionSelectorCandidate candidate)> EvaluateActionConstraintsCore(
            HttpContext httpContext,
            CandidateSet candidateSet,
            IReadOnlyList<(int index, ActionSelectorCandidate candidate)> items,
            int? startingOrder)
        {
            // Find the next group of constraints to process. This will be the lowest value of
            // order that is higher than startingOrder.
            int? order = null;

            // Perf: Avoid allocations
            for (var i = 0; i < items.Count; i++)
            {
                var item = items[i];
                var constraints = item.candidate.Constraints;
                if (constraints != null)
                {
                    for (var j = 0; j < constraints.Count; j++)
                    {
                        var constraint = constraints[j];
                        if ((startingOrder == null || constraint.Order > startingOrder) &&
                            (order == null || constraint.Order < order))
                        {
                            order = constraint.Order;
                        }
                    }
                }
            }

            // If we don't find a next then there's nothing left to do.
            if (order == null)
            {
                return items;
            }

            // Since we have a constraint to process, bisect the set of endpoints into those with and without a
            // constraint for the current order.
            var endpointsWithConstraint = new List<(int index, ActionSelectorCandidate candidate)>();
            var endpointsWithoutConstraint = new List<(int index, ActionSelectorCandidate candidate)>();

            var constraintContext = new ActionConstraintContext
            {
                Candidates = items.Select(i => i.candidate).ToArray()
            };

            // Perf: Avoid allocations
            for (var i = 0; i < items.Count; i++)
            {
                var item = items[i];
                var isMatch = true;
                var foundMatchingConstraint = false;

                var constraints = item.candidate.Constraints;
                if (constraints != null)
                {
                    constraintContext.CurrentCandidate = item.candidate;
                    for (var j = 0; j < constraints.Count; j++)
                    {
                        var constraint = constraints[j];
                        if (constraint.Order == order)
                        {
                            foundMatchingConstraint = true;

                            // Before we run the constraint, we need to initialize the route values.
                            // In endpoint routing, the route values are per-endpoint.
                            constraintContext.RouteContext = new RouteContext(httpContext)
                            {
                                RouteData = new RouteData(candidateSet[item.index].Values),
                            };
                            if (!constraint.Accept(constraintContext))
                            {
                                isMatch = false;
                                break;
                            }
                        }
                    }
                }

                if (isMatch && foundMatchingConstraint)
                {
                    endpointsWithConstraint.Add(item);
                }
                else if (isMatch)
                {
                    endpointsWithoutConstraint.Add(item);
                }
            }

            // If we have matches with constraints, those are better so try to keep processing those
            if (endpointsWithConstraint.Count > 0)
            {
                var matches = EvaluateActionConstraintsCore(httpContext, candidateSet, endpointsWithConstraint, order);
                if (matches?.Count > 0)
                {
                    return matches;
                }
            }

            // If the set of matches with constraints can't work, then process the set without constraints.
            if (endpointsWithoutConstraint.Count == 0)
            {
                return null;
            }
            else
            {
                return EvaluateActionConstraintsCore(httpContext, candidateSet, endpointsWithoutConstraint, order);
            }
        }
    }
}
