// 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;
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;

namespace Microsoft.AspNetCore.Mvc.Internal
{
    /// <summary>
    /// An implementation of <see cref="IValidationStrategy"/> for a collection bound using 'explict indexing'
    /// style keys.
    /// </summary>
    /// <remarks>
    /// This implemenation handles cases like:
    /// <example>
    ///     Model: IList&lt;Student&gt; 
    ///     Query String: ?students.index=Joey,Katherine&amp;students[Joey].Age=8&amp;students[Katherine].Age=9
    /// 
    ///     In this case, 'Joey' and 'Katherine' need to be used in the model prefix keys, but cannot be inferred
    ///     form inspecting the collection. These prefixes are captured during model binding, and mapped to 
    ///     the corresponding ordinal index of a model object in the collection. The enumerator returned from this
    ///     class will yield two 'Student' objects with corresponding keys 'students[Joey]' and 'students[Katherine]'.
    /// </example>
    /// 
    /// Using this key format, the enumerator enumerates model objects of type matching
    /// <see cref="ModelMetadata.ElementMetadata"/>. The keys captured during model binding are mapped to the elements
    /// in the collection to compute the model prefix keys.
    /// </remarks>
    public class ExplicitIndexCollectionValidationStrategy : IValidationStrategy
    {
        /// <summary>
        /// Creates a new <see cref="ExplicitIndexCollectionValidationStrategy"/>.
        /// </summary>
        /// <param name="elementKeys">The keys of collection elements that were used during model binding.</param>
        public ExplicitIndexCollectionValidationStrategy(IEnumerable<string> elementKeys)
        {
            if (elementKeys == null)
            {
                throw new ArgumentNullException(nameof(elementKeys));
            }

            ElementKeys = elementKeys;
        }

        /// <summary>
        /// Gets the keys of collection elements that were used during model binding.
        /// </summary>
        public IEnumerable<string> ElementKeys { get; }

        /// <inheritdoc />
        public IEnumerator<ValidationEntry> GetChildren(
            ModelMetadata metadata,
            string key,
            object model)
        {
            var enumerator = DefaultCollectionValidationStrategy.Instance.GetEnumeratorForElementType(metadata, model);
            return new Enumerator(metadata.ElementMetadata, key, ElementKeys, enumerator);
        }

        private class Enumerator : IEnumerator<ValidationEntry>
        {
            private readonly string _key;
            private readonly ModelMetadata _metadata;
            private readonly IEnumerator _enumerator;
            private readonly IEnumerator<string> _keyEnumerator;

            private ValidationEntry _entry;

            public Enumerator(
                ModelMetadata metadata,
                string key,
                IEnumerable<string> elementKeys,
                IEnumerator enumerator)
            {
                _metadata = metadata;
                _key = key;

                _keyEnumerator = elementKeys.GetEnumerator();
                _enumerator = enumerator;
            }

            public ValidationEntry Current => _entry;

            object IEnumerator.Current => Current;

            public bool MoveNext()
            {
                if (!_keyEnumerator.MoveNext())
                {
                    return false;
                }

                if (!_enumerator.MoveNext())
                {
                    return false;
                }

                var model = _enumerator.Current;
                var key = ModelNames.CreateIndexModelName(_key, _keyEnumerator.Current);

                _entry = new ValidationEntry(_metadata, key, model);

                return true;
            }

            public void Dispose()
            {
            }

            public void Reset()
            {
                throw new NotImplementedException();
            }
        }
    }
}
