// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Xaml;
using System.Xaml.Replacements;
using MS.Internal.Serialization;

#pragma warning disable 1634, 1691  // suppressing PreSharp warnings

namespace System.Windows.Markup
{
    /// <summary>
    /// ValueSerializer allows a type to declare a serializer to control how the type is serialized to and from strings.
    /// If a TypeConverter is declared for a type that converts to and from a string, a default value serializer will
    /// be created for the type. The string values must be loss-less (i.e. converting to and from a string doesn't loose
    /// data) and must be stable (i.e. returns the same string for the same value). If a type converter doesn't  meet
    /// these requirements, a custom ValueSerializer must be declared that meet the requirements or associate a null
    /// ValueSerializer with the type to indicate the type converter should be ignored. Implementation of ValueSerializer
    /// should avoid throwing exceptions. Any exceptions thrown could possibly terminate serialization.
    /// </summary>
    [TypeForwardedFrom("WindowsBase, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35")]
    public abstract class ValueSerializer
    {
        private static readonly object s_valueSerializersLock = new object();
        private static Hashtable s_valueSerializers = new Hashtable();

        /// <summary>
        /// Constructor for a ValueSerializer
        /// </summary>
        protected ValueSerializer() { }

        /// <summary>
        /// Returns true if the given value can be converted to a string.
        /// </summary>
        /// <param name="value">Value to convert</param>
        /// <param name="context">Context information</param>
        /// <returns>Whether or not the value can be converted to a string</returns>
        public virtual bool CanConvertToString(object? value, IValueSerializerContext? context)
        {
            return false;
        }

        /// <summary>
        /// Returns true if the given value can be converted from a string.
        /// </summary>
        /// <param name="value">The string to convert</param>
        /// <param name="context">Context information</param>
        /// <returns>Whether or not the value can be converted from a string</returns>
        public virtual bool CanConvertFromString(string? value, IValueSerializerContext? context)
        {
            return false;
        }

        /// <summary>
        /// Converts the given value to a string for use in serialization. This method should only be
        /// called if CanConvertToString returns true for the given value.
        /// </summary>
        /// <param name="value">The value to convert to a string</param>
        /// <param name="context">Context information</param>
        /// <returns>A string representation of value</returns>
        public virtual string? ConvertToString(object? value, IValueSerializerContext? context)
        {
            throw GetConvertToException(value, typeof(string));
        }

        /// <summary>
        /// Convert a string to an object. This method should only be called if CanConvertFromString
        /// returns true for the given string.
        /// </summary>
        /// <param name="value">The string value to convert</param>
        /// <param name="context">Context information</param>
        /// <returns>An object corresponding to the string value</returns>
        public virtual object? ConvertFromString(string value, IValueSerializerContext? context)
        {
            throw GetConvertFromException(value);
        }

        /// <summary>
        /// Returns an enumeration of the types referenced by the value serializer. If the value serializer asks for
        /// a value serializer for System.Type, any types it asks to convert should be supplied in the returned
        /// enumeration. This allows a serializer to ensure a de-serializer has enough information about the types
        /// this serializer converts.
        ///
        /// Since a value serializer doesn't exist by default, it is important the value serializer be requested from
        /// the IValueSerializerContext, not ValueSerializer.GetSerializerFor. This allows a serializer to encode
        /// context information (such as xmlns definitions) to the System.Type converter (for example, which prefix
        /// to generate).
        /// </summary>
        /// <param name="value">The value being serialized</param>
        /// <param name="context">Context information</param>
        /// <returns>An enumeration of the types converted by this serializer</returns>
        public virtual IEnumerable<Type> TypeReferences(object? value, IValueSerializerContext? context)
        {
            return Array.Empty<Type>();
        }

        /// <summary>
        /// Get the value serializer declared for the given type.
        /// </summary>
        /// <param name="type">The value type to serialize</param>
        /// <returns>The value serializer associated with the given type</returns>
        public static ValueSerializer? GetSerializerFor(Type type)
        {
            ArgumentNullException.ThrowIfNull(type);

            object? value = s_valueSerializers[type];
            if (value != null)
            {
                // This uses s_valueSerializersLock's instance as a sentinal for null  (as opposed to not attempted yet).
                return value == s_valueSerializersLock ? null : value as ValueSerializer;
            }

            AttributeCollection attributes = TypeDescriptor.GetAttributes(type);
            ValueSerializer? result = null;

            if (attributes[typeof(ValueSerializerAttribute)] is ValueSerializerAttribute attribute)
            {
                result = (ValueSerializer?)Activator.CreateInstance(attribute.ValueSerializerType);
            }

            if (result == null)
            {
                if (type == typeof(string))
                {
                    result = new StringValueSerializer();
                }
                else
                {
                    // Try to use the type converter
                    TypeConverter converter = TypeConverterHelper.GetTypeConverter(type);

                    // DateTime is a special-case. We can't use the DateTimeConverter, because it doesn't
                    // support anything other than user culture and invariant culture, and we need to specify
                    // en-us culture.
                    if (converter.GetType() == typeof(DateTimeConverter2))
                    {
                        result = new DateTimeValueSerializer();
                    }
                    else if (converter.CanConvertTo(typeof(string)) && converter.CanConvertFrom(typeof(string)) &&
                             !(converter is ReferenceConverter))
                    {
                        result = new TypeConverterValueSerializer(converter);
                    }
                }
            }
            lock (s_valueSerializersLock)
            {
                // This uses s_valueSerializersLock's instance as a sentinal for null (as opposed to not attempted yet).
                s_valueSerializers[type] = result ?? s_valueSerializersLock;
            }

            return result;
        }

        /// <summary>
        /// Get the value serializer declared for the given property. ValueSerializer can be overriden by an attribute
        /// on the property declaration.
        /// </summary>
        /// <param name="descriptor">PropertyDescriptor for the property to be serialized</param>
        /// <returns>A value serializer associated with the given property</returns>
        public static ValueSerializer? GetSerializerFor(PropertyDescriptor descriptor)
        {
            ArgumentNullException.ThrowIfNull(descriptor);

            if (descriptor.Attributes[typeof(ValueSerializerAttribute)] is ValueSerializerAttribute serializerAttribute)
            {
                return (ValueSerializer?)Activator.CreateInstance(serializerAttribute.ValueSerializerType);
            }

            ValueSerializer? result = GetSerializerFor(descriptor.PropertyType);
            if (result is null or TypeConverterValueSerializer)
            {
                TypeConverter converter = descriptor.Converter;
                if (converter != null && converter.CanConvertTo(typeof(string)) && converter.CanConvertFrom(typeof(string)) &&
                    converter is not ReferenceConverter)
                {
                    result = new TypeConverterValueSerializer(converter);
                }
            }

            return result;
        }

        /// <summary>
        /// Get the value serializer declared for the given type. This version should be called whenever the caller
        /// has a IValueSerializerContext to ensure that the correct value serializer is returned for the given
        /// context.
        /// </summary>
        /// <param name="type">The value type to serialize</param>
        /// <param name="context">Context information</param>
        /// <returns>The value serializer associated with the given type</returns>
        public static ValueSerializer? GetSerializerFor(Type type, IValueSerializerContext? context)
        {
            if (context != null)
            {
                ValueSerializer result = context.GetValueSerializerFor(type);
                if (result != null)
                {
                    return result;
                }
            }

            return GetSerializerFor(type);
        }

        /// <summary>
        /// Get the value serializer declared for the given property. ValueSerializer can be overriden by an attribute
        /// on the property declaration. This version should be called whenever the caller has a
        /// IValueSerializerContext to ensure that the correct value serializer is returned for the given context.
        /// </summary>
        /// <param name="descriptor">PropertyDescriptor for the property to be serialized</param>
        /// <param name="context">Context information</param>
        /// <returns>A value serializer associated with the given property</returns>
        public static ValueSerializer? GetSerializerFor(PropertyDescriptor descriptor, IValueSerializerContext? context)
        {
            if (context != null)
            {
                ValueSerializer result = context.GetValueSerializerFor(descriptor);
                if (result != null)
                {
                    return result;
                }
            }

            return GetSerializerFor(descriptor);
        }

        /// <summary>
        /// Return a exception to throw if the value cannot be converted
        /// </summary>
        protected Exception GetConvertToException(object? value, Type destinationType)
        {
            ArgumentNullException.ThrowIfNull(destinationType);

            string? text;
            if (value == null)
            {
                text = SR.ToStringNull;
            }
            else
            {
                text = value.GetType().FullName;
            }
            return new NotSupportedException(SR.Format(SR.ConvertToException, base.GetType().Name, text, destinationType.FullName));
        }

        /// <summary>
        /// Return a exception to throw if the string cannot be converted
        /// </summary>
        protected Exception GetConvertFromException(object? value)
        {
            string? text;
            if (value == null)
            {
                text = SR.ToStringNull;
            }
            else
            {
                text = value.GetType().FullName;
            }
            return new NotSupportedException(SR.Format(SR.ConvertFromException, base.GetType().Name, text));
        }

        private static void TypeDescriptorRefreshed(RefreshEventArgs args)
        {
            s_valueSerializers = new Hashtable();
        }

        static ValueSerializer()
        {
            TypeDescriptor.Refreshed += TypeDescriptorRefreshed;
        }
    }
}
