// 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 Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.Extensions.Internal;

namespace Microsoft.AspNetCore.Mvc.ViewFeatures
{
    /// <summary>
    /// Associates a model object with it's corresponding <see cref="ModelMetadata"/>.
    /// </summary>
    [DebuggerDisplay("DeclaredType={Metadata.ModelType.Name} PropertyName={Metadata.PropertyName}")]
    public class ModelExplorer
    {
        private readonly IModelMetadataProvider _metadataProvider;

        private object _model;
        private Func<object, object> _modelAccessor;
        private ModelExplorer[] _properties;

        /// <summary>
        /// Creates a new <see cref="ModelExplorer"/>.
        /// </summary>
        /// <param name="metadataProvider">The <see cref="IModelMetadataProvider"/>.</param>
        /// <param name="metadata">The <see cref="ModelMetadata"/>.</param>
        /// <param name="model">The model object. May be <c>null</c>.</param>
        public ModelExplorer(
            IModelMetadataProvider metadataProvider,
            ModelMetadata metadata,
            object model)
        {
            if (metadataProvider == null)
            {
                throw new ArgumentNullException(nameof(metadataProvider));
            }

            if (metadata == null)
            {
                throw new ArgumentNullException(nameof(metadata));
            }

            _metadataProvider = metadataProvider;
            Metadata = metadata;
            _model = model;
        }

        /// <summary>
        /// Creates a new <see cref="ModelExplorer"/>.
        /// </summary>
        /// <param name="metadataProvider">The <see cref="IModelMetadataProvider"/>.</param>
        /// <param name="container">The container <see cref="ModelExplorer"/>.</param>
        /// <param name="metadata">The <see cref="ModelMetadata"/>.</param>
        /// <param name="modelAccessor">A model accessor function..</param>
        public ModelExplorer(
            IModelMetadataProvider metadataProvider,
            ModelExplorer container,
            ModelMetadata metadata,
            Func<object, object> modelAccessor)
        {
            if (metadataProvider == null)
            {
                throw new ArgumentNullException(nameof(metadataProvider));
            }

            if (container == null)
            {
                throw new ArgumentNullException(nameof(container));
            }

            if (metadata == null)
            {
                throw new ArgumentNullException(nameof(metadata));
            }

            _metadataProvider = metadataProvider;
            Container = container;
            Metadata = metadata;
            _modelAccessor = modelAccessor;
        }

        /// <summary>
        /// Creates a new <see cref="ModelExplorer"/>.
        /// </summary>
        /// <param name="metadataProvider">The <see cref="IModelMetadataProvider"/>.</param>
        /// <param name="container">The container <see cref="ModelExplorer"/>.</param>
        /// <param name="metadata">The <see cref="ModelMetadata"/>.</param>
        /// <param name="model">The model object. May be <c>null</c>.</param>
        public ModelExplorer(
            IModelMetadataProvider metadataProvider,
            ModelExplorer container,
            ModelMetadata metadata,
            object model)
        {
            if (metadataProvider == null)
            {
                throw new ArgumentNullException(nameof(metadataProvider));
            }

            if (metadata == null)
            {
                throw new ArgumentNullException(nameof(metadata));
            }

            _metadataProvider = metadataProvider;
            Container = container;
            Metadata = metadata;
            _model = model;
        }

        /// <summary>
        /// Gets the container <see cref="ModelExplorer"/>.
        /// </summary>
        /// <remarks>
        /// <para>
        /// The <see cref="Container"/> will most commonly be set as a result of calling
        /// <see cref="GetExplorerForProperty(string)"/>. In this case, the returned <see cref="ModelExplorer"/> will
        /// have it's <see cref="Container"/> set to the instance upon which <see cref="GetExplorerForProperty(string)"/>
        /// was called.
        /// </para>
        /// <para>
        /// This however is not a requirement. The <see cref="Container"/> is informational, and may not
        /// represent a type that defines the property represented by <see cref="Metadata"/>. This can
        /// occur when constructing a <see cref="ModelExplorer"/> based on evaluation of a complex
        /// expression.
        /// </para>
        /// <para>
        /// If calling code relies on a parent-child relationship between <see cref="ModelExplorer"/>
        /// instances, then use <see cref="ModelMetadata.ContainerType"/> to validate this assumption.
        /// </para>
        /// </remarks>
        public ModelExplorer Container { get; }

        /// <summary>
        /// Gets the <see cref="ModelMetadata"/>.
        /// </summary>
        public ModelMetadata Metadata { get; }

        /// <summary>
        /// Gets the model object.
        /// </summary>
        /// <remarks>
        /// Retrieving the <see cref="Model"/> object will execute the model accessor function if this
        /// <see cref="ModelExplorer"/> was provided with one.
        /// </remarks>
        public object Model
        {
            get
            {
                if (_model == null && _modelAccessor != null)
                {
                    Debug.Assert(Container != null);
                    _model = _modelAccessor(Container.Model);

                    // Null-out the accessor so we don't invoke it repeatedly if it returns null.
                    _modelAccessor = null;
                }

                return _model;
            }
        }

        /// <remarks>
        /// Retrieving the <see cref="ModelType"/> will execute the model accessor function if this
        /// <see cref="ModelExplorer"/> was provided with one.
        /// </remarks>
        public Type ModelType
        {
            get
            {
                if (Metadata.IsNullableValueType)
                {
                    // We have a model, but if it's a nullable value type, then Model.GetType() will return
                    // the non-nullable type (int? -> int). Since it's a value type, there's no subclassing,
                    // just go with the declared type.
                    return Metadata.ModelType;
                }

                if (Model == null)
                {
                    // If the model is null, then use the declared model type;
                    return Metadata.ModelType;
                }

                // We have a model, and it's not a nullable, so use the runtime type to handle
                // cases where the model is a subclass of the declared type and has extra data.
                return Model.GetType();
            }
        }

        /// <summary>
        /// Gets the properties.
        /// </summary>
        /// <remarks>
        /// Includes a <see cref="ModelExplorer"/> for each property of the <see cref="ModelMetadata"/>
        /// for <see cref="ModelType"/>.
        /// </remarks>
        public IEnumerable<ModelExplorer> Properties => PropertiesInternal;

        internal ModelExplorer[] PropertiesInternal
        {
            get
            {
                if (_properties == null)
                {
                    var metadata = GetMetadataForRuntimeType();
                    var properties = metadata.Properties;
                    var propertyHelpers = PropertyHelper.GetProperties(ModelType);

                    _properties = new ModelExplorer[properties.Count];
                    for (var i = 0; i < properties.Count; i++)
                    {
                        var propertyMetadata = properties[i];
                        PropertyHelper propertyHelper = null;
                        for (var j = 0; j < propertyHelpers.Length; j++)
                        {
                            if (string.Equals(
                                propertyMetadata.PropertyName,
                                propertyHelpers[j].Property.Name,
                                StringComparison.Ordinal))
                            {
                                propertyHelper = propertyHelpers[j];
                                break;
                            }
                        }

                        Debug.Assert(propertyHelper != null);
                        _properties[i] = CreateExplorerForProperty(propertyMetadata, propertyHelper);
                    }
                }

                return _properties;
            }
        }

        /// <summary>
        /// Gets a <see cref="ModelExplorer"/> for the given <paramref name="model"/> value.
        /// </summary>
        /// <param name="model">The model value.</param>
        /// <returns>A <see cref="ModelExplorer"/>.</returns>
        public ModelExplorer GetExplorerForModel(object model)
        {
            if (Container == null)
            {
                return new ModelExplorer(_metadataProvider, Metadata, model);
            }
            else
            {
                return new ModelExplorer(_metadataProvider, Container, Metadata, model);
            }
        }

        /// <summary>
        /// Gets a <see cref="ModelExplorer"/> for the property with given <paramref name="name"/>, or <c>null</c> if
        /// the property cannot be found.
        /// </summary>
        /// <param name="name">The property name.</param>
        /// <returns>A <see cref="ModelExplorer"/>, or <c>null</c>.</returns>
        public ModelExplorer GetExplorerForProperty(string name)
        {
            if (name == null)
            {
                throw new ArgumentNullException(nameof(name));
            }

            for (var i = 0; i < PropertiesInternal.Length; i++)
            {
                var property = PropertiesInternal[i];
                if (string.Equals(name, property.Metadata.PropertyName, StringComparison.Ordinal))
                {
                    return property;
                }
            }

            return null;
        }

        /// <summary>
        /// Gets a <see cref="ModelExplorer"/> for the property with given <paramref name="name"/>, or <c>null</c> if
        /// the property cannot be found.
        /// </summary>
        /// <param name="name">The property name.</param>
        /// <param name="modelAccessor">An accessor for the model value.</param>
        /// <returns>A <see cref="ModelExplorer"/>, or <c>null</c>.</returns>
        /// <remarks>
        /// As this creates a model explorer with a specific model accessor function, the result is not cached.
        /// </remarks>
        public ModelExplorer GetExplorerForProperty(string name, Func<object, object> modelAccessor)
        {
            if (name == null)
            {
                throw new ArgumentNullException(nameof(name));
            }

            var metadata = GetMetadataForRuntimeType();

            var propertyMetadata = metadata.Properties[name];
            if (propertyMetadata == null)
            {
                return null;
            }

            return new ModelExplorer(_metadataProvider, this, propertyMetadata, modelAccessor);
        }

        /// <summary>
        /// Gets a <see cref="ModelExplorer"/> for the property with given <paramref name="name"/>, or <c>null</c> if
        /// the property cannot be found.
        /// </summary>
        /// <param name="name">The property name.</param>
        /// <param name="model">The model value.</param>
        /// <returns>A <see cref="ModelExplorer"/>, or <c>null</c>.</returns>
        /// <remarks>
        /// As this creates a model explorer with a specific model value, the result is not cached.
        /// </remarks>
        public ModelExplorer GetExplorerForProperty(string name, object model)
        {
            if (name == null)
            {
                throw new ArgumentNullException(nameof(name));
            }

            var metadata = GetMetadataForRuntimeType();

            var propertyMetadata = metadata.Properties[name];
            if (propertyMetadata == null)
            {
                return null;
            }

            return new ModelExplorer(_metadataProvider, this, propertyMetadata, model);
        }

        /// <summary>
        /// Gets a <see cref="ModelExplorer"/> for the provided model value and model <see cref="Type"/>.
        /// </summary>
        /// <param name="modelType">The model <see cref="Type"/>.</param>
        /// <param name="model">The model value.</param>
        /// <returns>A <see cref="ModelExplorer"/>.</returns>
        /// <remarks>
        /// <para>
        /// A <see cref="ModelExplorer"/> created by <see cref="GetExplorerForExpression(Type, object)"/>
        /// represents the result of executing an arbitrary expression against the model contained
        /// in the current <see cref="ModelExplorer"/> instance.
        /// </para>
        /// <para>
        /// The returned <see cref="ModelExplorer"/> will have the current instance set as its <see cref="Container"/>.
        /// </para>
        /// </remarks>
        public ModelExplorer GetExplorerForExpression(Type modelType, object model)
        {
            if (modelType == null)
            {
                throw new ArgumentNullException(nameof(modelType));
            }

            var metadata = _metadataProvider.GetMetadataForType(modelType);
            return GetExplorerForExpression(metadata, model);
        }

        /// <summary>
        /// Gets a <see cref="ModelExplorer"/> for the provided model value and model <see cref="Type"/>.
        /// </summary>
        /// <param name="metadata">The <see cref="ModelMetadata"/> associated with the model.</param>
        /// <param name="model">The model value.</param>
        /// <returns>A <see cref="ModelExplorer"/>.</returns>
        /// <remarks>
        /// <para>
        /// A <see cref="ModelExplorer"/> created by
        /// <see cref="GetExplorerForExpression(ModelMetadata, object)"/>
        /// represents the result of executing an arbitrary expression against the model contained
        /// in the current <see cref="ModelExplorer"/> instance.
        /// </para>
        /// <para>
        /// The returned <see cref="ModelExplorer"/> will have the current instance set as its <see cref="Container"/>.
        /// </para>
        /// </remarks>
        public ModelExplorer GetExplorerForExpression(ModelMetadata metadata, object model)
        {
            if (metadata == null)
            {
                throw new ArgumentNullException(nameof(metadata));
            }

            return new ModelExplorer(_metadataProvider, this, metadata, model);
        }

        /// <summary>
        /// Gets a <see cref="ModelExplorer"/> for the provided model value and model <see cref="Type"/>.
        /// </summary>
        /// <param name="modelType">The model <see cref="Type"/>.</param>
        /// <param name="modelAccessor">A model accessor function.</param>
        /// <returns>A <see cref="ModelExplorer"/>.</returns>
        /// <remarks>
        /// <para>
        /// A <see cref="ModelExplorer"/> created by
        /// <see cref="GetExplorerForExpression(Type, Func{object, object})"/>
        /// represents the result of executing an arbitrary expression against the model contained
        /// in the current <see cref="ModelExplorer"/> instance.
        /// </para>
        /// <para>
        /// The returned <see cref="ModelExplorer"/> will have the current instance set as its <see cref="Container"/>.
        /// </para>
        /// </remarks>
        public ModelExplorer GetExplorerForExpression(Type modelType, Func<object, object> modelAccessor)
        {
            if (modelType == null)
            {
                throw new ArgumentNullException(nameof(modelType));
            }

            var metadata = _metadataProvider.GetMetadataForType(modelType);
            return GetExplorerForExpression(metadata, modelAccessor);
        }

        /// <summary>
        /// Gets a <see cref="ModelExplorer"/> for the provided model value and model <see cref="Type"/>.
        /// </summary>
        /// <param name="metadata">The <see cref="ModelMetadata"/> associated with the model.</param>
        /// <param name="modelAccessor">A model accessor function.</param>
        /// <returns>A <see cref="ModelExplorer"/>.</returns>
        /// <remarks>
        /// <para>
        /// A <see cref="ModelExplorer"/> created by
        /// <see cref="GetExplorerForExpression(ModelMetadata, Func{object, object})"/>
        /// represents the result of executing an arbitrary expression against the model contained
        /// in the current <see cref="ModelExplorer"/> instance.
        /// </para>
        /// <para>
        /// The returned <see cref="ModelExplorer"/> will have the current instance set as its <see cref="Container"/>.
        /// </para>
        /// </remarks>
        public ModelExplorer GetExplorerForExpression(ModelMetadata metadata, Func<object, object> modelAccessor)
        {
            if (metadata == null)
            {
                throw new ArgumentNullException(nameof(metadata));
            }

            return new ModelExplorer(_metadataProvider, this, metadata, modelAccessor);
        }

        private ModelMetadata GetMetadataForRuntimeType()
        {
            // We want to make sure we're looking at the runtime properties of the model, and for
            // that we need the model metadata of the runtime type.
            var metadata = Metadata;
            if (Metadata.ModelType != ModelType)
            {
                metadata = _metadataProvider.GetMetadataForType(ModelType);
            }

            return metadata;
        }

        private ModelExplorer CreateExplorerForProperty(
            ModelMetadata propertyMetadata,
            PropertyHelper propertyHelper)
        {
            if (propertyHelper == null)
            {
                return new ModelExplorer(_metadataProvider, this, propertyMetadata, modelAccessor: null);
            }

            var modelAccessor = new Func<object, object>((c) =>
            {
                return c == null ? null : propertyHelper.GetValue(c);
            });

            return new ModelExplorer(_metadataProvider, this, propertyMetadata, modelAccessor);
        }
    }
}
