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

using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Reflection;

namespace System.ComponentModel.DataAnnotations
{
    /// <summary>
    ///     Base class for all validation attributes.
    ///     <para>Override <see cref="IsValid(object, ValidationContext)" /> to implement validation logic.</para>
    /// </summary>
    /// <remarks>
    ///     The properties <see cref="ErrorMessageResourceType" /> and <see cref="ErrorMessageResourceName" /> are used to
    ///     provide
    ///     a localized error message, but they cannot be set if <see cref="ErrorMessage" /> is also used to provide a
    ///     non-localized
    ///     error message.
    /// </remarks>
    public abstract class ValidationAttribute : Attribute
    {
        #region Member Fields

        private string? _errorMessage;
        private Func<string>? _errorMessageResourceAccessor;
        private string? _errorMessageResourceName;
        [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)]
        private Type? _errorMessageResourceType;
        private volatile bool _hasBaseIsValid;
        private string? _defaultErrorMessage;
        #endregion

        #region All Constructors

        /// <summary>
        ///     Default constructor for any validation attribute.
        /// </summary>
        /// <remarks>
        ///     This constructor chooses a very generic validation error message.
        ///     Developers subclassing ValidationAttribute should use other constructors
        ///     or supply a better message.
        /// </remarks>
        protected ValidationAttribute()
            : this(() => SR.ValidationAttribute_ValidationError)
        {
        }

        /// <summary>
        ///     Constructor that accepts a fixed validation error message.
        /// </summary>
        /// <param name="errorMessage">A non-localized error message to use in <see cref="ErrorMessageString" />.</param>
        protected ValidationAttribute(string errorMessage)
            : this(() => errorMessage)
        {
        }

        /// <summary>
        ///     Allows for providing a resource accessor function that will be used by the <see cref="ErrorMessageString" />
        ///     property to retrieve the error message.  An example would be to have something like
        ///     CustomAttribute() : base( () =&gt; MyResources.MyErrorMessage ) {}.
        /// </summary>
        /// <param name="errorMessageAccessor">The <see cref="Func{T}" /> that will return an error message.</param>
        protected ValidationAttribute(Func<string> errorMessageAccessor)
        {
            // If null, will later be exposed as lack of error message to be able to construct accessor
            _errorMessageResourceAccessor = errorMessageAccessor;
        }

        /// <summary>
        /// Internal constructor used for delayed population of the error message delegate.
        /// </summary>
        private protected ValidationAttribute(bool populateErrorMessageResourceAccessor)
        {
            Debug.Assert(populateErrorMessageResourceAccessor is false, "Use the default constructor instead");
        }

        #endregion

        #region Internal Properties
        /// <summary>
        /// Sets the default error message string.
        /// This message will be used if the user has not set <see cref="ErrorMessage"/>
        /// or the <see cref="ErrorMessageResourceType"/> and <see cref="ErrorMessageResourceName"/> pair.
        /// This property was added after the public contract for DataAnnotations was created.
        /// It is internal to avoid changing the DataAnnotations contract.
        /// </summary>
        private protected string? DefaultErrorMessage
        {
            init
            {
                _defaultErrorMessage = value;
                _errorMessageResourceAccessor = null;
                CustomErrorMessageSet = true;
            }
        }

        /// <summary>
        /// Sets the delayed resource accessor in cases where we can't pass it directly to the base constructor.
        /// </summary>
        private protected Func<string> ErrorMessageResourceAccessor
        {
            init
            {
                Debug.Assert(_defaultErrorMessage is null && _errorMessageResourceName is null && _errorMessage is null && _errorMessageResourceType is null);
                _errorMessageResourceAccessor = value;
            }
        }

        #endregion

        #region Protected Properties

        /// <summary>
        ///     Gets the localized error message string, coming either from <see cref="ErrorMessage" />, or from evaluating the
        ///     <see cref="ErrorMessageResourceType" /> and <see cref="ErrorMessageResourceName" /> pair.
        /// </summary>
        protected string ErrorMessageString
        {
            get
            {
                SetupResourceAccessor();
                return _errorMessageResourceAccessor!();
            }
        }

        /// <summary>
        ///     A flag indicating whether a developer has customized the attribute's error message by setting any one of
        ///     ErrorMessage, ErrorMessageResourceName, ErrorMessageResourceType or DefaultErrorMessage.
        /// </summary>
        internal bool CustomErrorMessageSet { get; private set; }

        /// <summary>
        ///     A flag indicating that the attribute requires a non-null
        ///     <see cref="ValidationContext" /> to perform validation.
        ///     Base class returns false. Override in child classes as appropriate.
        /// </summary>
        public virtual bool RequiresValidationContext => false;

        #endregion

        #region Public Properties

        /// <summary>
        ///     Gets or sets the explicit error message string.
        /// </summary>
        /// <value>
        ///     This property is intended to be used for non-localizable error messages.  Use
        ///     <see cref="ErrorMessageResourceType" /> and <see cref="ErrorMessageResourceName" /> for localizable error messages.
        /// </value>
        public string? ErrorMessage
        {
            // If _errorMessage is not set, return the default. This is done to preserve
            // behavior prior to the fix where ErrorMessage showed the non-null message to use.
            get => _errorMessage ?? _defaultErrorMessage;
            set
            {
                _errorMessage = value;
                _errorMessageResourceAccessor = null;
                CustomErrorMessageSet = true;

                // Explicitly setting ErrorMessage also sets DefaultErrorMessage if null.
                // This prevents subsequent read of ErrorMessage from returning default.
                if (value == null)
                {
                    _defaultErrorMessage = null;
                }
            }
        }

        /// <summary>
        ///     Gets or sets the resource name (property name) to use as the key for lookups on the resource type.
        /// </summary>
        /// <value>
        ///     Use this property to set the name of the property within <see cref="ErrorMessageResourceType" />
        ///     that will provide a localized error message.  Use <see cref="ErrorMessage" /> for non-localized error messages.
        /// </value>
        public string? ErrorMessageResourceName
        {
            get => _errorMessageResourceName;
            set
            {
                _errorMessageResourceName = value;
                _errorMessageResourceAccessor = null;
                CustomErrorMessageSet = true;
            }
        }

        /// <summary>
        ///     Gets or sets the resource type to use for error message lookups.
        /// </summary>
        /// <value>
        ///     Use this property only in conjunction with <see cref="ErrorMessageResourceName" />.  They are
        ///     used together to retrieve localized error messages at runtime.
        ///     <para>
        ///         Use <see cref="ErrorMessage" /> instead of this pair if error messages are not localized.
        ///     </para>
        /// </value>
        [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)]
        public Type? ErrorMessageResourceType
        {
            get => _errorMessageResourceType;
            set
            {
                _errorMessageResourceType = value;
                _errorMessageResourceAccessor = null;
                CustomErrorMessageSet = true;
            }
        }

        #endregion

        #region Private Methods

        /// <summary>
        ///     Validates the configuration of this attribute and sets up the appropriate error string accessor.
        ///     This method bypasses all verification once the ResourceAccessor has been set.
        /// </summary>
        /// <exception cref="InvalidOperationException"> is thrown if the current attribute is malformed.</exception>
        private void SetupResourceAccessor()
        {
            if (_errorMessageResourceAccessor == null)
            {
                string? localErrorMessage = ErrorMessage;
                bool resourceNameSet = !string.IsNullOrEmpty(_errorMessageResourceName);
                bool errorMessageSet = !string.IsNullOrEmpty(_errorMessage);
                bool resourceTypeSet = _errorMessageResourceType != null;
                bool defaultMessageSet = !string.IsNullOrEmpty(_defaultErrorMessage);

                // The following combinations are illegal and throw InvalidOperationException:
                //   1) Both ErrorMessage and ErrorMessageResourceName are set, or
                //   2) None of ErrorMessage, ErrorMessageResourceName, and DefaultErrorMessage are set.
                if ((resourceNameSet && errorMessageSet) || !(resourceNameSet || errorMessageSet || defaultMessageSet))
                {
                    throw new InvalidOperationException(
                        SR.ValidationAttribute_Cannot_Set_ErrorMessage_And_Resource);
                }

                // Must set both or neither of ErrorMessageResourceType and ErrorMessageResourceName
                if (resourceTypeSet != resourceNameSet)
                {
                    throw new InvalidOperationException(
                        SR.ValidationAttribute_NeedBothResourceTypeAndResourceName);
                }

                // If set resource type (and we know resource name too), then go setup the accessor
                if (resourceNameSet)
                {
                    SetResourceAccessorByPropertyLookup();
                }
                else
                {
                    // Here if not using resource type/name -- the accessor is just the error message string,
                    // which we know is not empty to have gotten this far.
                    // We captured error message to local in case it changes before accessor runs
                    _errorMessageResourceAccessor = () => localErrorMessage!;
                }
            }
        }

        private void SetResourceAccessorByPropertyLookup()
        {
            Debug.Assert(_errorMessageResourceType != null);
            Debug.Assert(!string.IsNullOrEmpty(_errorMessageResourceName));
            var property = _errorMessageResourceType
                .GetProperty(_errorMessageResourceName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.DeclaredOnly);

            if (property != null)
            {
                var propertyGetter = property.GetMethod;

                // We only support internal and public properties
                if (propertyGetter == null || (!propertyGetter.IsAssembly && !propertyGetter.IsPublic))
                {
                    // Set the property to null so the exception is thrown as if the property wasn't found
                    property = null;
                }
            }

            if (property == null)
            {
                throw new InvalidOperationException(SR.Format(SR.ValidationAttribute_ResourceTypeDoesNotHaveProperty,
                                                    _errorMessageResourceType.FullName,
                                                    _errorMessageResourceName));
            }

            if (property.PropertyType != typeof(string))
            {
                throw new InvalidOperationException(SR.Format(SR.ValidationAttribute_ResourcePropertyNotStringType,
                                                    property.Name,
                                                    _errorMessageResourceType.FullName));
            }

            _errorMessageResourceAccessor = () => (string)property.GetValue(null, null)!;
        }

        private protected ValidationResult CreateFailedValidationResult(ValidationContext validationContext)
        {
            string[]? memberNames = validationContext.MemberName is { } memberName
                ? new[] { memberName }
                : null;

            return new ValidationResult(FormatErrorMessage(validationContext.DisplayName), memberNames);
        }

        #endregion

        #region Protected & Public Methods

        /// <summary>
        ///     Formats the error message to present to the user.
        /// </summary>
        /// <remarks>
        ///     The error message will be re-evaluated every time this function is called.
        ///     It applies the <paramref name="name" /> (for example, the name of a field) to the formatted error message, resulting
        ///     in something like "The field 'name' has an incorrect value".
        ///     <para>
        ///         Derived classes can override this method to customize how errors are generated.
        ///     </para>
        ///     <para>
        ///         The base class implementation will use <see cref="ErrorMessageString" /> to obtain a localized
        ///         error message from properties within the current attribute.  If those have not been set, a generic
        ///         error message will be provided.
        ///     </para>
        /// </remarks>
        /// <param name="name">The user-visible name to include in the formatted message.</param>
        /// <returns>The localized string describing the validation error</returns>
        /// <exception cref="InvalidOperationException"> is thrown if the current attribute is malformed.</exception>
        public virtual string FormatErrorMessage(string name) =>
            string.Format(CultureInfo.CurrentCulture, ErrorMessageString, name);

        /// <summary>
        ///     Gets the value indicating whether or not the specified <paramref name="value" /> is valid
        ///     with respect to the current validation attribute.
        ///     <para>
        ///         Derived classes should not override this method as it is only available for backwards compatibility.
        ///         Instead, implement <see cref="IsValid(object, ValidationContext)" />.
        ///     </para>
        /// </summary>
        /// <remarks>
        ///     The preferred public entry point for clients requesting validation is the <see cref="GetValidationResult" />
        ///     method.
        /// </remarks>
        /// <param name="value">The value to validate</param>
        /// <returns><c>true</c> if the <paramref name="value" /> is acceptable, <c>false</c> if it is not acceptable</returns>
        /// <exception cref="InvalidOperationException"> is thrown if the current attribute is malformed.</exception>
        /// <exception cref="NotImplementedException">
        ///     is thrown when neither overload of IsValid has been implemented
        ///     by a derived class.
        /// </exception>
        public virtual bool IsValid(object? value)
        {
            if (!_hasBaseIsValid)
            {
                // track that this method overload has not been overridden.
                _hasBaseIsValid = true;
            }

            // call overridden method.
            // The IsValid method without a validationContext predates the one accepting the context.
            // This is theoretically unreachable through normal use cases.
            // Instead, the overload using validationContext should be called.
            return IsValid(value, null!) == ValidationResult.Success;
        }

        /// <summary>
        ///     Protected virtual method to override and implement validation logic.
        ///     <para>
        ///         Derived classes should override this method instead of <see cref="IsValid(object)" />, which is deprecated.
        ///     </para>
        /// </summary>
        /// <param name="value">The value to validate.</param>
        /// <param name="validationContext">
        ///     A <see cref="ValidationContext" /> instance that provides
        ///     context about the validation operation, such as the object and member being validated.
        /// </param>
        /// <returns>
        ///     When validation is valid, <see cref="ValidationResult.Success" />.
        ///     <para>
        ///         When validation is invalid, an instance of <see cref="ValidationResult" />.
        ///     </para>
        /// </returns>
        /// <exception cref="InvalidOperationException"> is thrown if the current attribute is malformed.</exception>
        /// <exception cref="NotImplementedException">
        ///     is thrown when <see cref="IsValid(object, ValidationContext)" />
        ///     has not been implemented by a derived class.
        /// </exception>
        protected virtual ValidationResult? IsValid(object? value, ValidationContext validationContext)
        {
            if (_hasBaseIsValid)
            {
                // this means neither of the IsValid methods has been overridden, throw.
                throw NotImplemented.ByDesignWithMessage(
                    SR.ValidationAttribute_IsValid_NotImplemented);
            }

            // call overridden method.
            return IsValid(value)
                ? ValidationResult.Success
                : CreateFailedValidationResult(validationContext);
        }

        /// <summary>
        ///     Tests whether the given <paramref name="value" /> is valid with respect to the current
        ///     validation attribute without throwing a <see cref="ValidationException" />
        /// </summary>
        /// <remarks>
        ///     If this method returns <see cref="ValidationResult.Success" />, then validation was successful, otherwise
        ///     an instance of <see cref="ValidationResult" /> will be returned with a guaranteed non-null
        ///     <see cref="ValidationResult.ErrorMessage" />.
        /// </remarks>
        /// <param name="value">The value to validate</param>
        /// <param name="validationContext">
        ///     A <see cref="ValidationContext" /> instance that provides
        ///     context about the validation operation, such as the object and member being validated.
        /// </param>
        /// <returns>
        ///     When validation is valid, <see cref="ValidationResult.Success" />.
        ///     <para>
        ///         When validation is invalid, an instance of <see cref="ValidationResult" />.
        ///     </para>
        /// </returns>
        /// <exception cref="InvalidOperationException"> is thrown if the current attribute is malformed.</exception>
        /// <exception cref="ArgumentNullException">When <paramref name="validationContext" /> is null.</exception>
        /// <exception cref="NotImplementedException">
        ///     is thrown when <see cref="IsValid(object, ValidationContext)" />
        ///     has not been implemented by a derived class.
        /// </exception>
        public ValidationResult? GetValidationResult(object? value, ValidationContext validationContext)
        {
            ArgumentNullException.ThrowIfNull(validationContext);

            var result = IsValid(value, validationContext);

            // If validation fails, we want to ensure we have a ValidationResult that guarantees it has an ErrorMessage
            if (result != null)
            {
                if (string.IsNullOrEmpty(result.ErrorMessage))
                {
                    var errorMessage = FormatErrorMessage(validationContext.DisplayName);
                    result = new ValidationResult(errorMessage, result.MemberNames);
                }
            }

            return result;
        }

        /// <summary>
        ///     Validates the specified <paramref name="value" /> and throws <see cref="ValidationException" /> if it is not.
        ///     <para>
        ///         The overloaded <see cref="Validate(object, ValidationContext)" /> is the recommended entry point as it
        ///         can provide additional context to the <see cref="ValidationAttribute" /> being validated.
        ///     </para>
        /// </summary>
        /// <remarks>
        ///     This base method invokes the <see cref="IsValid(object)" /> method to determine whether or not the
        ///     <paramref name="value" /> is acceptable.  If <see cref="IsValid(object)" /> returns <c>false</c>, this base
        ///     method will invoke the <see cref="FormatErrorMessage" /> to obtain a localized message describing
        ///     the problem, and it will throw a <see cref="ValidationException" />
        /// </remarks>
        /// <param name="value">The value to validate</param>
        /// <param name="name">The string to be included in the validation error message if <paramref name="value" /> is not valid</param>
        /// <exception cref="ValidationException">
        ///     is thrown if <see cref="IsValid(object)" /> returns <c>false</c>.
        /// </exception>
        /// <exception cref="InvalidOperationException"> is thrown if the current attribute is malformed.</exception>
        public void Validate(object? value, string name)
        {
            if (!IsValid(value))
            {
                throw new ValidationException(FormatErrorMessage(name), this, value);
            }
        }

        /// <summary>
        ///     Validates the specified <paramref name="value" /> and throws <see cref="ValidationException" /> if it is not.
        /// </summary>
        /// <remarks>
        ///     This method invokes the <see cref="IsValid(object, ValidationContext)" /> method
        ///     to determine whether or not the <paramref name="value" /> is acceptable given the
        ///     <paramref name="validationContext" />.
        ///     If that method doesn't return <see cref="ValidationResult.Success" />, this base method will throw
        ///     a <see cref="ValidationException" /> containing the <see cref="ValidationResult" /> describing the problem.
        /// </remarks>
        /// <param name="value">The value to validate</param>
        /// <param name="validationContext">Additional context that may be used for validation.  It cannot be null.</param>
        /// <exception cref="ValidationException">
        ///     is thrown if <see cref="IsValid(object, ValidationContext)" />
        ///     doesn't return <see cref="ValidationResult.Success" />.
        /// </exception>
        /// <exception cref="InvalidOperationException"> is thrown if the current attribute is malformed.</exception>
        /// <exception cref="NotImplementedException">
        ///     is thrown when <see cref="IsValid(object, ValidationContext)" />
        ///     has not been implemented by a derived class.
        /// </exception>
        public void Validate(object? value, ValidationContext validationContext)
        {
            ArgumentNullException.ThrowIfNull(validationContext);

            ValidationResult? result = GetValidationResult(value, validationContext);

            if (result != null)
            {
                // Convenience -- if implementation did not fill in an error message,
                throw new ValidationException(result, this, value);
            }
        }
        #endregion
    }
}
