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

using System.CodeDom;
using System.Collections;
using System.Globalization;
using System.Reflection;

namespace System.ComponentModel.Design.Serialization;

/// <summary>
///  This serializer serializes collections.  This can either create statements or expressions.  It will create an expression and assign it to the statement in the current context stack if the object is an array.  If it is a collection with an add range or similar method, it will create a statement calling the method.
/// </summary>
public class CollectionCodeDomSerializer : CodeDomSerializer
{
    private static CollectionCodeDomSerializer? s_defaultSerializer;

    /// <summary>
    ///  Retrieves a default static instance of this serializer.
    /// </summary>
    internal static new CollectionCodeDomSerializer Default => s_defaultSerializer ??= new CollectionCodeDomSerializer();

    /// <summary>
    ///  Computes the delta between an existing collection and a modified one. This is for the case of inherited items that have collection properties so we only generate Add/AddRange calls for the items that have been added.  It works by Hashing up the items in the original collection and then walking the modified collection and only returning those items which do not exist in the base collection.
    /// </summary>
    [return: NotNullIfNotNull(nameof(modified))]
    private static ICollection? GetCollectionDelta(ICollection? original, ICollection? modified)
    {
        if (original is null || modified is null || original.Count == 0)
        {
            return modified;
        }

        IEnumerator modifiedEnum = modified.GetEnumerator();
        if (modifiedEnum is null)
        {
            Debug.Fail($"Collection of type {modified.GetType().FullName} doesn't return an enumerator");
            return modified;
        }

        // first hash up the values so we can quickly decide if it's a new one or not
        Dictionary<object, int> originalValues = [];
        foreach (object originalValue in original)
        {
            // the array could contain multiple copies of the same value (think of a string collection), so we need to be sensitive of that.
            if (originalValues.TryGetValue(originalValue, out int count))
            {
                originalValues[originalValue] = count + 1;
            }
            else
            {
                originalValues.Add(originalValue, 1);
            }
        }

        // now walk through and delete existing values
        List<object>? result = null;
        // now compute the delta.
        for (int i = 0; i < modified.Count && modifiedEnum.MoveNext(); i++)
        {
            object value = modifiedEnum.Current!;

            if (originalValues.TryGetValue(value, out int count))
            {
                // we've got one we need to remove, so  create our array list, and push all the values we've passed into it.
                if (result is null)
                {
                    result = [];
                    modifiedEnum.Reset();
                    for (int n = 0; n < i && modifiedEnum.MoveNext(); n++)
                    {
                        result.Add(modifiedEnum.Current!);
                    }

                    // and finally skip the one we're on
                    modifiedEnum.MoveNext();
                }

                // decrement the count if we've got more than one...
                if (--count == 0)
                {
                    originalValues.Remove(value);
                }
                else
                {
                    originalValues[value] = count;
                }
            }
            else                     // this one isn't in the old list, so add it to our  result list.
            {
                result?.Add(value);
            }

            // this item isn't in the list and we haven't yet created our array list so just keep on going.
        }

        return result ?? modified;
    }

    /// <summary>
    ///  Checks the attributes on this method to see if they support serialization.
    /// </summary>
    protected bool MethodSupportsSerialization(MethodInfo method)
    {
        ArgumentNullException.ThrowIfNull(method);

        object[] attributes = method.GetCustomAttributes(typeof(DesignerSerializationVisibilityAttribute), true);
        if (attributes.Length > 0)
        {
            DesignerSerializationVisibilityAttribute visibility = (DesignerSerializationVisibilityAttribute)attributes[0];
            if (visibility is { Visibility: DesignerSerializationVisibility.Hidden })
            {
                return false;
            }
        }

        return true;
    }

    /// <summary>
    ///  Serializes the given object into a CodeDom object.
    /// </summary>
    public override object? Serialize(IDesignerSerializationManager manager, object value)
    {
        ArgumentNullException.ThrowIfNull(manager);
        ArgumentNullException.ThrowIfNull(value);

        object? result = null;

        // We serialize collections as follows:
        //      If the collection is an array, we write out the array.
        //      If the collection has a method called AddRange, we will call that, providing an array.
        //      If the collection has an Add method, we will call it repeatedly.
        //      If the collection is an IList, we will cast to IList and add to it.
        //      If the collection has no add method, but is marked with PersistContents, we will enumerate the collection and serialize each element.
        // Check to see if there is a CodePropertyReferenceExpression on the stack.  If there is, we can use it as a guide for serialization.
        CodeExpression? target;
        if (manager.TryGetContext(out ExpressionContext? context) && context.PresetValue == value &&
            manager.TryGetContext(out PropertyDescriptor? property) && property.PropertyType == context.ExpressionType)
        {
            // We only want to give out an expression target if  this is our context (we find this out by comparing types above) and if the context type is not an array.  If it is an array, we will  just return the array create expression.
            target = context.Expression;
        }
        else
        {
            // This context is either the wrong context or doesn't match the property descriptor we found.
            target = null;
            context = null;
            property = null;
        }

        // If we have a target expression see if we can create a delta for the collection. We want to do this only if the
        // property the collection is associated with is inherited, and if the collection is not an array.
        if (value is ICollection collection)
        {
            ICollection subset = collection;
            Type collectionType = context?.ExpressionType ?? collection.GetType();
            bool isArray = typeof(Array).IsAssignableFrom(collectionType);

            // If we don't have a target expression and this isn't an array, let's try to create one.
            if (target is null && !isArray)
            {
                target = SerializeCreationExpression(manager, collection, out bool isComplete);
                if (isComplete)
                {
                    return target;
                }
            }

            if (target is not null || isArray)
            {
                if (property is InheritedPropertyDescriptor inheritedDesc && !isArray)
                {
                    subset = GetCollectionDelta(inheritedDesc.OriginalValue as ICollection, collection);
                }

                result = SerializeCollection(manager, target, collectionType, collection, subset);

                // See if we should emit a clear for this collection.
                if (target is not null && ShouldClearCollection(manager, collection))
                {
                    CodeStatementCollection? resultCollection = result as CodeStatementCollection;

                    // If non empty collection is being serialized, but no statements were generated, there is no need to clear.
                    if (collection.Count > 0 && (result is null || (resultCollection is not null && resultCollection.Count == 0)))
                    {
                        return null;
                    }

                    if (resultCollection is null)
                    {
                        resultCollection = [];
                        if (result is CodeStatement resultStatement)
                        {
                            resultCollection.Add(resultStatement);
                        }

                        result = resultCollection;
                    }

                    CodeMethodInvokeExpression clearMethod = new(target, "Clear");
                    CodeExpressionStatement clearStatement = new(clearMethod);
                    resultCollection.Insert(0, clearStatement);
                }
            }
        }
        else
        {
            Debug.Fail($"Collection serializer invoked for non-collection: {(value is null ? "(null)" : value.GetType().Name)}");
        }

        return result;
    }

    /// <summary>
    ///  Given a set of methods and objects, determines the method with the correct of  parameter type for all objects.
    /// </summary>
    private static MethodInfo? ChooseMethodByType(TypeDescriptionProvider provider, List<MethodInfo> methods, ICollection values)
    {
        // Note that this method uses reflection types which may not be compatible with runtime types. objType must be
        // obtained from the same provider as the methods were to ensure that the reflection types all belong to the
        // same type universe.
        MethodInfo? final = null;
        Type? finalType = null;
        foreach (object obj in values)
        {
            Type objType = provider.GetReflectionType(obj);
            MethodInfo? candidate = null;
            Type? candidateType = null;
            if (final is null || (finalType is not null && !finalType.IsAssignableFrom(objType)))
            {
                foreach (MethodInfo method in methods)
                {
                    ParameterInfo parameter = method.GetParameters()[0];
                    if (parameter is not null)
                    {
                        Type type = parameter.ParameterType;
                        if (type.IsArray)
                        {
                            type = type.GetElementType()!;
                        }

                        if (type.IsAssignableFrom(objType))
                        {
                            if (final is not null)
                            {
                                if (type.IsAssignableFrom(finalType))
                                {
                                    final = method;
                                    finalType = type;
                                    break;
                                }
                            }
                            else if (candidate is null)
                            {
                                candidate = method;
                                candidateType = type;
                            }
                            else
                            {
                                // we found another method.  Pick the one that uses the most derived type.
                                Debug.Assert(candidateType!.IsAssignableFrom(type) || type.IsAssignableFrom(candidateType), "These two types are not related.  how were they chosen based on the base type");
                                bool assignable = candidateType.IsAssignableFrom(type);
                                candidate = assignable ? method : candidate;
                                candidateType = assignable ? type : candidateType;
                            }
                        }
                    }
                }
            }

            if (final is null)
            {
                final = candidate;
                finalType = candidateType;
            }
        }

        return final;
    }

    /// <summary>
    ///  Serializes the given collection.  targetExpression will refer to the expression used to rever to the  collection, but it can be null.
    /// </summary>
    protected virtual object? SerializeCollection(IDesignerSerializationManager manager, CodeExpression? targetExpression, Type targetType, ICollection originalCollection, ICollection valuesToSerialize)
    {
        ArgumentNullException.ThrowIfNull(manager);
        ArgumentNullException.ThrowIfNull(targetType);
        ArgumentNullException.ThrowIfNull(originalCollection);
        ArgumentNullException.ThrowIfNull(valuesToSerialize);

        object? result = null;
        bool serialized = false;
        if (typeof(Array).IsAssignableFrom(targetType))
        {
            CodeArrayCreateExpression? arrayCreate = SerializeArray(manager, targetType, (Array)originalCollection, valuesToSerialize);
            if (arrayCreate is not null)
            {
                if (targetExpression is not null)
                {
                    result = new CodeAssignStatement(targetExpression, arrayCreate);
                }
                else
                {
                    result = arrayCreate;
                }
            }
        }
        else if (valuesToSerialize.Count > 0)
        {
            // Use the TargetFrameworkProviderService to create a provider, or use the default for the collection if the
            // service is not available. Since TargetFrameworkProvider reflection types are not compatible with RuntimeTypes,
            // they can only be used with other reflection types from the same provider.
            TypeDescriptionProvider? provider = GetTargetFrameworkProvider(manager, originalCollection);
            provider ??= TypeDescriptor.GetProvider(originalCollection);

            MethodInfo[] methods = provider.GetReflectionType(originalCollection).GetMethods(BindingFlags.Public | BindingFlags.Instance);
            List<MethodInfo> addRangeMethods = [];
            List<MethodInfo> addMethods = [];
            foreach (MethodInfo method in methods)
            {
                switch (method.Name)
                {
                    case "AddRange":
                        ParameterInfo[] parameters = method.GetParameters();
                        if (parameters is [{ ParameterType.IsArray: true }] && MethodSupportsSerialization(method))
                        {
                            addRangeMethods.Add(method);
                        }

                        break;

                    case "Add":
                        if (method.GetParameters().Length == 1 && MethodSupportsSerialization(method))
                        {
                            addMethods.Add(method);
                        }

                        break;
                }
            }

            MethodInfo? addRangeMethodToUse = ChooseMethodByType(provider, addRangeMethods, valuesToSerialize);
            if (addRangeMethodToUse is not null)
            {
                Type elementType = provider.GetRuntimeType(addRangeMethodToUse.GetParameters()[0].ParameterType.GetElementType()!);
                result = SerializeViaAddRange(manager, targetExpression, elementType, valuesToSerialize);
                serialized = true;
            }
            else
            {
                MethodInfo? addMethodToUse = ChooseMethodByType(provider, addMethods, valuesToSerialize);
                if (addMethodToUse is not null)
                {
                    Type elementType = provider.GetRuntimeType(addMethodToUse.GetParameters()[0].ParameterType);
                    result = SerializeViaAdd(manager, targetExpression, elementType, valuesToSerialize);
                    serialized = true;
                }
            }

#pragma warning disable SYSLIB0050 // Type or member is obsolete
            if (!serialized && originalCollection.GetType().IsSerializable)
            {
                result = SerializeToResourceExpression(manager, originalCollection, false);
            }
#pragma warning restore SYSLIB0050 // Type or member is obsolete
        }

        return result;
    }

    /// <summary>
    ///  Serializes the given array.
    /// </summary>
    private CodeArrayCreateExpression? SerializeArray(IDesignerSerializationManager manager, Type targetType, Array array, ICollection valuesToSerialize)
    {
        CodeArrayCreateExpression? result = null;

        if (array.Rank != 1)
        {
            manager.ReportError(string.Format(SR.SerializerInvalidArrayRank, array.Rank.ToString(CultureInfo.InvariantCulture)));
        }
        else
        {
            // For an array, we need an array create expression.  First, get the array type
            Type elementType = targetType.GetElementType()!;
            CodeTypeReference elementTypeRef = new(elementType);

            // Now create an ArrayCreateExpression, and fill its initializers.
            CodeArrayCreateExpression arrayCreate = new CodeArrayCreateExpression
            {
                CreateType = elementTypeRef
            };

            bool arrayOk = true;
            foreach (object o in valuesToSerialize)
            {
                // If this object is being privately inherited, it cannot be inside this collection.  Since we're writing an entire array here, we cannot write any of it.
                if (o is IComponent && TypeDescriptor.GetAttributes(o).Contains(InheritanceAttribute.InheritedReadOnly))
                {
                    arrayOk = false;
                    break;
                }

                CodeExpression? expression = null;
                // If there is an expression context on the stack at this point, we need to fix up the ExpressionType on it to be the array element type.
                ExpressionContext? newContext = null;
                if (manager.TryGetContext(out ExpressionContext? context))
                {
                    newContext = new ExpressionContext(context.Expression, elementType, context.Owner);
                    manager.Context.Push(newContext);
                }

                try
                {
                    expression = SerializeToExpression(manager, o);
                }
                finally
                {
                    if (newContext is not null)
                    {
                        Debug.Assert(manager.Context.Current == newContext, "Context stack corrupted.");
                        manager.Context.Pop();
                    }
                }

                if (expression is not null)
                {
                    if (o is not null && o.GetType() != elementType)
                    {
                        expression = new CodeCastExpression(elementType, expression);
                    }

                    arrayCreate.Initializers.Add(expression);
                }
                else
                {
                    arrayOk = false;
                    break;
                }
            }

            if (arrayOk)
            {
                result = arrayCreate;
            }
        }

        return result;
    }

    /// <summary>
    ///  Serializes the given collection by creating multiple calls to an Add method.
    /// </summary>
    private CodeStatementCollection SerializeViaAdd(
        IDesignerSerializationManager manager,
        CodeExpression? targetExpression,
        Type elementType,
        ICollection valuesToSerialize)
    {
        CodeStatementCollection statements = [];

        // Here we need to invoke Add once for each and every item in the collection. We can re-use the property
        // reference and method reference, but we will need to recreate the invoke statement each time.
        CodeMethodReferenceExpression methodRef = new(targetExpression!, "Add");

        if (valuesToSerialize.Count == 0)
        {
            return statements;
        }

        foreach (object o in valuesToSerialize)
        {
            // If this object is being privately inherited, it cannot be inside this collection.
            bool genCode = o is not IComponent;
            if (!genCode)
            {
                if (TypeDescriptorHelper.TryGetAttribute(o, out InheritanceAttribute? ia))
                {
                    genCode = ia.InheritanceLevel != InheritanceLevel.InheritedReadOnly;
                }
                else
                {
                    genCode = true;
                }
            }

            Debug.Assert(genCode, "Why didn't GetCollectionDelta calculate the same thing?");
            if (genCode)
            {
                CodeMethodInvokeExpression statement = new CodeMethodInvokeExpression
                {
                    Method = methodRef
                };

                CodeExpression? serializedObject = null;

                // If there is an expression context on the stack at this point,
                // we need to fix up the ExpressionType on it to be the element type.
                ExpressionContext? newCtx = null;

                if (manager.TryGetContext(out ExpressionContext? ctx))
                {
                    newCtx = new ExpressionContext(ctx.Expression, elementType, ctx.Owner);
                    manager.Context.Push(newCtx);
                }

                try
                {
                    serializedObject = SerializeToExpression(manager, o);
                }
                finally
                {
                    if (newCtx is not null)
                    {
                        Debug.Assert(manager.Context.Current == newCtx, "Context stack corrupted.");
                        manager.Context.Pop();
                    }
                }

                if (o is not null && !elementType.IsAssignableFrom(o.GetType()) && o.GetType().IsPrimitive)
                {
                    serializedObject = new CodeCastExpression(elementType, serializedObject!);
                }

                if (serializedObject is not null)
                {
                    statement.Parameters.Add(serializedObject);
                    statements.Add(statement);
                }
            }
        }

        return statements;
    }

    /// <summary>
    ///  Serializes the given collection by creating an array and passing it to the AddRange method.
    /// </summary>
    private CodeStatementCollection SerializeViaAddRange(
        IDesignerSerializationManager manager,
        CodeExpression? targetExpression,
        Type elementType,
        ICollection valuesToSerialize)
    {
        CodeStatementCollection statements = [];

        if (valuesToSerialize.Count == 0)
        {
            return statements;
        }

        List<CodeExpression> arrayList = new(valuesToSerialize.Count);
        foreach (object o in valuesToSerialize)
        {
            // If this object is being privately inherited, it cannot be inside this collection.
            bool genCode = o is not IComponent;
            if (!genCode)
            {
                if (TypeDescriptorHelper.TryGetAttribute(o, out InheritanceAttribute? ia))
                {
                    genCode = ia.InheritanceLevel != InheritanceLevel.InheritedReadOnly;
                }
                else
                {
                    genCode = true;
                }
            }

            Debug.Assert(genCode, "Why didn't GetCollectionDelta calculate the same thing?");
            if (genCode)
            {
                CodeExpression? expression = null;
                // If there is an expression context on the stack at this point, we need to fix up the ExpressionType on it to be the element type.
                ExpressionContext? newContext = null;

                if (manager.TryGetContext(out ExpressionContext? ctx))
                {
                    newContext = new ExpressionContext(ctx.Expression, elementType, ctx.Owner);
                    manager.Context.Push(newContext);
                }

                try
                {
                    expression = SerializeToExpression(manager, o);
                }
                finally
                {
                    if (newContext is not null)
                    {
                        Debug.Assert(manager.Context.Current == newContext, "Context stack corrupted.");
                        manager.Context.Pop();
                    }
                }

                if (expression is not null)
                {
                    // Check to see if we need a cast
                    if (o is not null && !elementType.IsAssignableFrom(o.GetType()))
                    {
                        expression = new CodeCastExpression(elementType, expression);
                    }

                    arrayList.Add(expression);
                }
            }
        }

        if (arrayList.Count > 0)
        {
            // Now convert the array list into an array create expression.
            CodeTypeReference elementTypeRef = new(elementType);

            // Now create an ArrayCreateExpression, and fill its initializers.
            CodeArrayCreateExpression arrayCreate = new CodeArrayCreateExpression
            {
                CreateType = elementTypeRef
            };

            foreach (CodeExpression expression in arrayList)
            {
                arrayCreate.Initializers.Add(expression);
            }

            CodeMethodReferenceExpression methodRef = new(targetExpression!, "AddRange");
            CodeMethodInvokeExpression methodInvoke = new CodeMethodInvokeExpression
            {
                Method = methodRef
            };

            methodInvoke.Parameters.Add(arrayCreate);
            statements.Add(new CodeExpressionStatement(methodInvoke));
        }

        return statements;
    }

    /// <summary>
    ///  Returns true if we should clear the collection contents.
    /// </summary>
    private bool ShouldClearCollection(IDesignerSerializationManager manager, ICollection collection)
    {
        bool shouldClear = false;
        PropertyDescriptor? clearProperty = manager.Properties["ClearCollections"];
        if (clearProperty is not null && clearProperty.TryGetValue(manager, out bool b) && b)
        {
            shouldClear = true;
        }

        if (!shouldClear)
        {
            PropertyDescriptor? property = manager.GetContext<PropertyDescriptor>();
            if (manager.TryGetContext(out SerializeAbsoluteContext? absolute) && absolute.ShouldSerialize(property))
            {
                shouldClear = true;
            }
        }

        if (shouldClear)
        {
            MethodInfo? clearMethod = TypeDescriptor.GetReflectionType(collection).GetMethod("Clear", BindingFlags.Public | BindingFlags.Instance, null, [], null);
            if (clearMethod is null || !MethodSupportsSerialization(clearMethod))
            {
                shouldClear = false;
            }
        }

        return shouldClear;
    }
}
