// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;
using System.ComponentModel.Composition.Hosting;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using Microsoft.Internal;

namespace System.ComponentModel.Composition.Primitives
{
    /// <summary>
    ///     Represents a contract name and metadata-based import
    ///     required by a <see cref="ComposablePart"/> object.
    /// </summary>
    public class ContractBasedImportDefinition : ImportDefinition
    {
        // Unlike contract name, both metadata and required metadata has a sensible default; set it to an empty
        // enumerable, so that derived definitions only need to override ContractName by default.
        private readonly IEnumerable<KeyValuePair<string, Type>> _requiredMetadata = Enumerable.Empty<KeyValuePair<string, Type>>();
        private Expression<Func<ExportDefinition, bool>>? _constraint;
        private readonly CreationPolicy _requiredCreationPolicy = CreationPolicy.Any;
        private readonly string? _requiredTypeIdentity;
        private bool _isRequiredMetadataValidated;

        /// <summary>
        ///     Initializes a new instance of the <see cref="ContractBasedImportDefinition"/> class.
        /// </summary>
        /// <remarks>
        ///     <note type="inheritinfo">
        ///         Derived types calling this constructor can optionally override the
        ///         <see cref="ImportDefinition.ContractName"/>, <see cref="RequiredTypeIdentity"/>,
        ///         <see cref="RequiredMetadata"/>, <see cref="ImportDefinition.Cardinality"/>,
        ///         <see cref="ImportDefinition.IsPrerequisite"/>, <see cref="ImportDefinition.IsRecomposable"/>
        ///         and <see cref="RequiredCreationPolicy"/> properties.
        ///     </note>
        /// </remarks>
        protected ContractBasedImportDefinition()
        {
        }

        /// <summary>
        ///     Initializes a new instance of the <see cref="ContractBasedImportDefinition"/> class
        ///     with the specified contract name, required metadataq, cardinality, value indicating
        ///     if the import definition is recomposable and a value indicating if the import definition
        ///     is a prerequisite.
        /// </summary>
        /// <param name="contractName">
        ///     A <see cref="string"/> containing the contract name of the
        ///     <see cref="Export"/> required by the <see cref="ContractBasedImportDefinition"/>.
        /// </param>
        /// <param name="requiredTypeIdentity">
        ///     The type identity of the export type expected. Use <see cref="AttributedModelServices.GetTypeIdentity(Type)"/>
        ///     to generate a type identity for a given type. If no specific type is required pass <see langword="null"/>.
        /// </param>
        /// <param name="requiredMetadata">
        ///     An <see cref="IEnumerable{T}"/> of <see cref="string"/> objects containing
        ///     the metadata names of the <see cref="Export"/> required by the
        ///     <see cref="ContractBasedImportDefinition"/>; or <see langword="null"/> to
        ///     set the <see cref="RequiredMetadata"/> property to an empty <see cref="IEnumerable{T}"/>.
        /// </param>
        /// <param name="cardinality">
        ///     One of the <see cref="ImportCardinality"/> values indicating the
        ///     cardinality of the <see cref="Export"/> objects required by the
        ///     <see cref="ContractBasedImportDefinition"/>.
        /// </param>
        /// <param name="isRecomposable">
        ///     <see langword="true"/> if the <see cref="ContractBasedImportDefinition"/> can be satisfied
        ///     multiple times throughout the lifetime of a <see cref="ComposablePart"/>, otherwise,
        ///     <see langword="false"/>.
        /// </param>
        /// <param name="isPrerequisite">
        ///     <see langword="true"/> if the <see cref="ContractBasedImportDefinition"/> is required to be
        ///     satisfied before a <see cref="ComposablePart"/> can start producing exported
        ///     objects; otherwise, <see langword="false"/>.
        /// </param>
        /// <param name="requiredCreationPolicy">
        ///     A value indicating that the importer requires a specific <see cref="CreationPolicy"/> for
        ///     the exports used to satisfy this import. If no specific <see cref="CreationPolicy"/> is needed
        ///     pass the default <see cref="CreationPolicy.Any"/>.
        /// </param>
        /// <exception cref="ArgumentNullException">
        ///     <paramref name="contractName"/> is <see langword="null"/>.
        /// </exception>
        /// <exception cref="ArgumentException">
        ///     <paramref name="contractName"/> is an empty string ("").
        ///     <para>
        ///         -or-
        ///     </para>
        ///     <paramref name="requiredMetadata"/> contains an element that is <see langword="null"/>.
        ///     <para>
        ///         -or-
        ///     </para>
        ///     <paramref name="cardinality"/> is not one of the <see cref="ImportCardinality"/>
        ///     values.
        /// </exception>
        public ContractBasedImportDefinition(string contractName, string? requiredTypeIdentity, IEnumerable<KeyValuePair<string, Type>>? requiredMetadata,
            ImportCardinality cardinality, bool isRecomposable, bool isPrerequisite, CreationPolicy requiredCreationPolicy)
            : this(contractName, requiredTypeIdentity, requiredMetadata, cardinality, isRecomposable, isPrerequisite, requiredCreationPolicy, MetadataServices.EmptyMetadata)
        {
        }

        /// <summary>
        ///     Initializes a new instance of the <see cref="ContractBasedImportDefinition"/> class
        ///     with the specified contract name, required metadataq, cardinality, value indicating
        ///     if the import definition is recomposable and a value indicating if the import definition
        ///     is a prerequisite.
        /// </summary>
        /// <param name="contractName">
        ///     A <see cref="string"/> containing the contract name of the
        ///     <see cref="Export"/> required by the <see cref="ContractBasedImportDefinition"/>.
        /// </param>
        /// <param name="requiredTypeIdentity">
        ///     The type identity of the export type expected. Use <see cref="AttributedModelServices.GetTypeIdentity(Type)"/>
        ///     to generate a type identity for a given type. If no specific type is required pass <see langword="null"/>.
        /// </param>
        /// <param name="requiredMetadata">
        ///     An <see cref="IEnumerable{T}"/> of <see cref="string"/> objects containing
        ///     the metadata names of the <see cref="Export"/> required by the
        ///     <see cref="ContractBasedImportDefinition"/>; or <see langword="null"/> to
        ///     set the <see cref="RequiredMetadata"/> property to an empty <see cref="IEnumerable{T}"/>.
        /// </param>
        /// <param name="cardinality">
        ///     One of the <see cref="ImportCardinality"/> values indicating the
        ///     cardinality of the <see cref="Export"/> objects required by the
        ///     <see cref="ContractBasedImportDefinition"/>.
        /// </param>
        /// <param name="isRecomposable">
        ///     <see langword="true"/> if the <see cref="ContractBasedImportDefinition"/> can be satisfied
        ///     multiple times throughout the lifetime of a <see cref="ComposablePart"/>, otherwise,
        ///     <see langword="false"/>.
        /// </param>
        /// <param name="isPrerequisite">
        ///     <see langword="true"/> if the <see cref="ContractBasedImportDefinition"/> is required to be
        ///     satisfied before a <see cref="ComposablePart"/> can start producing exported
        ///     objects; otherwise, <see langword="false"/>.
        /// </param>
        /// <param name="requiredCreationPolicy">
        ///     A value indicating that the importer requires a specific <see cref="CreationPolicy"/> for
        ///     the exports used to satisfy this import. If no specific <see cref="CreationPolicy"/> is needed
        ///     pass the default <see cref="CreationPolicy.Any"/>.
        /// </param>
        /// <param name="metadata">The metadata associated with this import.</param>
        /// <exception cref="ArgumentNullException">
        ///     <paramref name="contractName"/> is <see langword="null"/>.
        /// </exception>
        /// <exception cref="ArgumentException">
        ///     <paramref name="contractName"/> is an empty string ("").
        ///     <para>
        ///         -or-
        ///     </para>
        ///     <paramref name="requiredMetadata"/> contains an element that is <see langword="null"/>.
        ///     <para>
        ///         -or-
        ///     </para>
        ///     <paramref name="cardinality"/> is not one of the <see cref="ImportCardinality"/>
        ///     values.
        /// </exception>
        public ContractBasedImportDefinition(string contractName, string? requiredTypeIdentity, IEnumerable<KeyValuePair<string, Type>>? requiredMetadata,
            ImportCardinality cardinality, bool isRecomposable, bool isPrerequisite, CreationPolicy requiredCreationPolicy, IDictionary<string, object?> metadata)
            : base(contractName, cardinality, isRecomposable, isPrerequisite, metadata)
        {
            Requires.NotNullOrEmpty(contractName, nameof(contractName));

            _requiredTypeIdentity = requiredTypeIdentity;

            if (requiredMetadata != null)
            {
                _requiredMetadata = requiredMetadata;
            }

            _requiredCreationPolicy = requiredCreationPolicy;
        }

        /// <summary>
        ///     The type identity of the export type expected.
        /// </summary>
        /// <value>
        ///     A <see cref="string"/> that is generated by <see cref="AttributedModelServices.GetTypeIdentity(Type)"/>
        ///     on the type that this import expects. If the value is <see langword="null"/> then this import
        ///     doesn't expect a particular type.
        /// </value>
        public virtual string? RequiredTypeIdentity
        {
            get { return _requiredTypeIdentity; }
        }

        /// <summary>
        ///     Gets the metadata names of the export required by the import definition.
        /// </summary>
        /// <value>
        ///     An <see cref="IEnumerable{T}"/> of pairs of metadata keys and types of the <see cref="Export"/> required by the
        ///     <see cref="ContractBasedImportDefinition"/>. The default is an empty
        ///     <see cref="IEnumerable{T}"/>.
        /// </value>
        /// <remarks>
        ///     <note type="inheritinfo">
        ///         Overriders of this property should never return <see langword="null"/>
        ///         or return an <see cref="IEnumerable{T}"/> that contains an element that is
        ///         <see langword="null"/>. If the definition does not contain required metadata,
        ///         return an empty <see cref="IEnumerable{T}"/> instead.
        ///     </note>
        /// </remarks>
        public virtual IEnumerable<KeyValuePair<string, Type>> RequiredMetadata
        {
            get
            {
                // NOTE : unlike other arguments, we validate this one as late as possible, because its validation may lead to type loading
                ValidateRequiredMetadata();

                Debug.Assert(_requiredMetadata != null);
                return _requiredMetadata;
            }
        }

        private void ValidateRequiredMetadata()
        {
            if (!_isRequiredMetadataValidated)
            {
                foreach (KeyValuePair<string, Type> metadataItem in _requiredMetadata)
                {
                    if ((metadataItem.Key == null) || (metadataItem.Value == null))
                    {
                        throw new InvalidOperationException(
                            SR.Format(SR.Argument_NullElement, "requiredMetadata"));
                    }
                }
                _isRequiredMetadataValidated = true;
            }
        }

        /// <summary>
        ///     Gets or sets a value indicating that the importer requires a specific
        ///     <see cref="CreationPolicy"/> for the exports used to satisfy this import. T
        /// </summary>
        /// <value>
        ///     <see cref="CreationPolicy.Any"/> - default value, used if the importer doesn't
        ///         require a specific <see cref="CreationPolicy"/>.
        ///
        ///     <see cref="CreationPolicy.Shared"/> - Requires that all exports used should be shared
        ///         by everyone in the container.
        ///
        ///     <see cref="CreationPolicy.NonShared"/> - Requires that all exports used should be
        ///         non-shared in a container and thus everyone gets their own instance.
        /// </value>
        public virtual CreationPolicy RequiredCreationPolicy
        {
            get { return _requiredCreationPolicy; }
        }

        /// <summary>
        ///     Gets an expression that defines conditions that must be matched for the import
        ///     described by the import definition to be satisfied.
        /// </summary>
        /// <returns>
        ///     A <see cref="Expression{TDelegate}"/> containing a <see cref="Func{T, TResult}"/>
        ///     that defines the conditions that must be matched for the
        ///     <see cref="ImportDefinition"/> to be satisfied by an <see cref="Export"/>.
        /// </returns>
        /// <remarks>
        ///     <para>
        ///         This property returns an expression that defines conditions based on the
        ///         <see cref="ImportDefinition.ContractName"/>, <see cref="RequiredTypeIdentity"/>,
        ///         <see cref="RequiredMetadata"/>, and <see cref="RequiredCreationPolicy"/>
        ///         properties.
        ///     </para>
        /// </remarks>
        public override Expression<Func<ExportDefinition, bool>> Constraint =>
            _constraint ??= ConstraintServices.CreateConstraint(ContractName, RequiredTypeIdentity, RequiredMetadata, RequiredCreationPolicy);

        /// <summary>
        ///     Executes an optimized version of the constraint given by the <see cref="Constraint"/> property
        /// </summary>
        /// <param name="exportDefinition">
        ///     A definition for a <see cref="Export"/> used to determine if it satisfies the
        ///     requirements for this <see cref="ImportDefinition"/>.
        /// </param>
        /// <returns>
        ///     <see langword="True"/> if the <see cref="Export"/> satisfies the requirements for
        ///     this <see cref="ImportDefinition"/>, otherwise returns <see langword="False"/>.
        /// </returns>
        /// <remarks>
        ///     <note type="inheritinfo">
        ///         Overrides of this method can provide a more optimized execution of the
        ///         <see cref="Constraint"/> property but the result should remain consistent.
        ///     </note>
        /// </remarks>
        /// <exception cref="ArgumentNullException">
        ///     <paramref name="exportDefinition"/> is <see langword="null"/>.
        /// </exception>
        public override bool IsConstraintSatisfiedBy(ExportDefinition exportDefinition)
        {
            Requires.NotNull(exportDefinition, nameof(exportDefinition));

            if (!StringComparers.ContractName.Equals(ContractName, exportDefinition.ContractName))
            {
                return false;
            }

            return MatchRequiredMetadata(exportDefinition);
        }

        private bool MatchRequiredMetadata(ExportDefinition definition)
        {
            if (!string.IsNullOrEmpty(RequiredTypeIdentity))
            {
                string? exportTypeIdentity = definition.Metadata.GetValue<string>(CompositionConstants.ExportTypeIdentityMetadataName);

                if (!StringComparers.ContractName.Equals(RequiredTypeIdentity, exportTypeIdentity))
                {
                    return false;
                }
            }

            foreach (KeyValuePair<string, Type> metadataItem in RequiredMetadata)
            {
                string metadataKey = metadataItem.Key;
                Type metadataValueType = metadataItem.Value;

                if (!definition.Metadata.TryGetValue(metadataKey, out object? metadataValue))
                {
                    return false;
                }

                if (metadataValue != null)
                {
                    // the metadata value is not null, we can rely on IsInstanceOfType to do the right thing
                    if (!metadataValueType.IsInstanceOfType(metadataValue))
                    {
                        return false;
                    }
                }
                else
                {
                    // this is an unfortunate special case - typeof(object).IsInstanceofType(null) == false
                    // basically nulls are not considered valid values for anything
                    // We want them to match anything that is a reference type
                    if (metadataValueType.IsValueType)
                    {
                        // this is a pretty expensive check, but we only invoke it when metadata values are null, which is very rare
                        return false;
                    }
                }
            }

            if (RequiredCreationPolicy == CreationPolicy.Any)
            {
                return true;
            }

            CreationPolicy exportPolicy = definition.Metadata.GetValue<CreationPolicy>(CompositionConstants.PartCreationPolicyMetadataName);
            return exportPolicy == CreationPolicy.Any ||
                   exportPolicy == RequiredCreationPolicy;
        }

        public override string ToString()
        {
            var sb = new StringBuilder();

            sb.Append("\n\tContractName\t").Append(ContractName);
            sb.Append("\n\tRequiredTypeIdentity\t").Append(RequiredTypeIdentity);
            if (_requiredCreationPolicy != CreationPolicy.Any)
            {
                sb.Append("\n\tRequiredCreationPolicy\t").Append(RequiredCreationPolicy);
            }

            if (_requiredMetadata.Any())
            {
                sb.Append("\n\tRequiredMetadata");
                foreach (KeyValuePair<string, Type> metadataItem in _requiredMetadata)
                {
                    sb.Append("\n\t\t").Append(metadataItem.Key).Append("\t(").Append(metadataItem.Value).Append(')');
                }
            }
            return sb.ToString();
        }
    }
}
