// 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;

namespace System.ComponentModel.Design.Serialization;

public sealed partial class CodeDomComponentSerializationService
{
    private sealed partial class CodeDomSerializationStore
    {
        /// <summary>
        ///  This is a simple code dom serializer that serializes a set of objects as a unit.
        /// </summary>
        private class ComponentListCodeDomSerializer : CodeDomSerializer
        {
            internal static readonly ComponentListCodeDomSerializer s_instance = new();
            private readonly Dictionary<string, OrderedCodeStatementCollection?> _statementsTable = [];
            private readonly Dictionary<string, List<CodeExpression>> _expressions = [];
            private Dictionary<string, CodeDomComponentSerializationState>? _objectState; // only used during deserialization
            private bool _applyDefaults = true;
            private readonly HashSet<string> _nameResolveGuard = [];

            public override object Deserialize(IDesignerSerializationManager manager, object state)
            {
                throw new NotSupportedException();
            }

            private static void PopulateCompleteStatements(object? data, string name, CodeStatementCollection completeStatements, Dictionary<string, List<CodeExpression>> expressions)
            {
                if (data is null)
                {
                    return;
                }

                if (data is CodeStatementCollection statements)
                {
                    completeStatements.AddRange(statements);
                }
                else if (data is CodeStatement statement)
                {
                    completeStatements.Add(statement);
                }
                else if (data is CodeExpression expression)
                {
                    // we handle expressions a little differently since they don't have a LHS or RHS they won't show up correctly in the statement table. We will deserialize them explicitly.
                    if (!expressions.TryGetValue(name, out List<CodeExpression>? exps))
                    {
                        exps = [];
                        expressions[name] = exps;
                    }

                    exps.Add(expression);
                }
                else
                {
                    Debug.Fail($"No case for {data.GetType().Name}");
                }
            }

            /// <summary>
            ///  Deserializes the given object state.  The results are contained within the  serialization manager's name table.  The objectNames list is used to  deserialize in the proper order, as objectState is unordered.
            /// </summary>
            internal void Deserialize(IDesignerSerializationManager manager, Dictionary<string, CodeDomComponentSerializationState> objectState, List<string> objectNames, bool applyDefaults)
            {
                CodeStatementCollection completeStatements = [];
                _expressions.Clear();
                _applyDefaults = applyDefaults;
                foreach (string name in objectNames)
                {
                    if (objectState.TryGetValue(name, out CodeDomComponentSerializationState? state))
                    {
                        PopulateCompleteStatements(state.Code, name, completeStatements, _expressions);
                        PopulateCompleteStatements(state.Context, name, completeStatements, _expressions);
                    }
                }

                CodeStatementCollection mappedStatements = [];
                CodeMethodMap methodMap = new(mappedStatements);

                methodMap.Add(completeStatements);
                methodMap.Combine();
                _statementsTable.Clear();

                // generate statement table keyed on component name
                FillStatementTable(manager, _statementsTable, mappedStatements);

                // We need to also ensure that for every entry in the statement table we have a corresponding entry in objectNames.  Otherwise, we won't deserialize completely.
                HashSet<string> completeNames = new(objectNames);
                completeNames.UnionWith(_statementsTable.Keys);

                _objectState = new(objectState);

                ResolveNameEventHandler resolveNameHandler = new(OnResolveName);
                manager.ResolveName += resolveNameHandler;
                try
                {
                    foreach (string name in completeNames)
                    {
                        ResolveName(manager, name, true);
                    }
                }
                finally
                {
                    _objectState = null;
                    manager.ResolveName -= resolveNameHandler;
                }
            }

            private void OnResolveName(object? sender, ResolveNameEventArgs e)
            {
                string name = e.Name!;
                // note: this recursionguard does not fix the problem, but rather avoids a stack overflow which will bring down VS and cause loss of data.
                if (!_nameResolveGuard.Add(name))
                {
                    return;
                }

                try
                {
                    IDesignerSerializationManager manager = (IDesignerSerializationManager)sender!;
                    if (ResolveName(manager, name, false))
                    {
                        e.Value = manager.GetInstance(name);
                    }
                }
                finally
                {
                    _nameResolveGuard.Remove(name);
                }
            }

            private void DeserializeDefaultProperties(IDesignerSerializationManager manager, string name, List<string>? defProps)
            {
                // Next, default properties, but only if we successfully  resolved.
                if (defProps is null || !_applyDefaults)
                {
                    return;
                }

                object? comp = manager.GetInstance(name);
                if (comp is null)
                {
                    return;
                }

                PropertyDescriptorCollection props = TypeDescriptor.GetProperties(comp);
                foreach (string propName in defProps)
                {
                    PropertyDescriptor? prop = props[propName];
                    if (prop is not null && prop.CanResetValue(comp))
                    {
                        // If there is a member relationship setup for this property, we should disconnect it first. This makes sense, since if there was a previous relationship, we would have serialized it and not come here at all.
                        if (manager.TryGetService(out MemberRelationshipService? relationships) && relationships[comp, prop] != MemberRelationship.Empty)
                        {
                            relationships[comp, prop] = MemberRelationship.Empty;
                        }

                        prop.ResetValue(comp);
                    }
                }
            }

            private static void DeserializeDesignTimeProperties(IDesignerSerializationManager manager, string name, Dictionary<string, object?>? state)
            {
                if (state is null)
                {
                    return;
                }

                object? comp = manager.GetInstance(name);
                if (comp is null)
                {
                    return;
                }

                PropertyDescriptorCollection props = TypeDescriptor.GetProperties(comp);

                foreach (KeyValuePair<string, object?> stateEntry in state)
                {
                    PropertyDescriptor? prop = props[stateEntry.Key];
                    prop?.SetValue(comp, stateEntry.Value);
                }
            }

            /// <summary>
            ///  This is used to resolve nested component references.  NestedComponents don't exist as sited components within the DesignerHost, they are actually sited within a parent component.  This method takes the FullName defined on INestedSite and returns the component which matches it. outerComponent is the name of the topmost component which does exist in the DesignerHost
            ///  This code also exists in VSCodeDomDesignerLoader -- please keep them in sync.
            /// </summary>
            private static IComponent? ResolveNestedName(IDesignerSerializationManager? manager, string name, [NotNullIfNotNull(nameof(manager))] out string? outerComponent)
            {
                if (manager is null)
                {
                    outerComponent = null;
                    return null;
                }

                bool moreChunks;
                // We need to resolve the first chunk using the manager. other chunks will be resolved within the nested containers.
                int curIndex = name.IndexOf('.');
                Debug.Assert(curIndex > 0, "ResolvedNestedName accepts only nested names!");
                outerComponent = name[..curIndex];
                IComponent? curComp = manager.GetInstance(outerComponent) as IComponent;

                do
                {
                    int prevIndex = curIndex;
                    curIndex = name.IndexOf('.', curIndex + 1);

                    moreChunks = curIndex != -1;
                    string compName = moreChunks
                        ? name.Substring(prevIndex + 1, curIndex)
                        : name[(prevIndex + 1)..];

                    if (string.IsNullOrEmpty(compName))
                    {
                        return null;
                    }

                    ISite? site = curComp?.Site;
                    if (!site.TryGetService(out INestedContainer? container))
                    {
                        return null;
                    }

                    curComp = container.Components[compName];
                }
                while (moreChunks);

                return curComp;
            }

            private bool ResolveName(IDesignerSerializationManager manager, string name, bool canInvokeManager)
            {
                bool resolved = false;
                // Check for a nested name. Components that are sited within NestedContainers need to be looked up in their nested container, and won't be resolvable directly via the manager.
                if (name.IndexOf('.') > 0)
                {
                    IComponent? nestedComp = ResolveNestedName(manager, name, out string? parentName);
                    if (nestedComp is not null && parentName is not null)
                    {
                        manager.SetName(nestedComp, name);
                        // What is the point of this?  Well, the nested components won't be in the statement table with its nested name.  However, their most parent component will be, so forcing a resolve of their name will actually deserialize the nested statements.
                        ResolveName(manager, parentName, canInvokeManager);
                    }
                    else
                    {
                        Debug.Fail($"Unable to resolve nested component: {name}");
                    }
                }

                // First we check to see if the statements table contains an OrderedCodeStatementCollection for this name.  If it does this means we have not resolved this name yet, so we grab its OrderedCodeStatementCollection and deserialize that, along with any default properties and design-time properties.
                // If it doesn't contain an OrderedCodeStatementsCollection this means one of two things:
                // 1. We already resolved this name and shoved an instance in there.  In this case we just return the instance
                // 2. There are no statements corresponding to this name, but there might be expressions that have never been deserialized, so we check for that and deserialize those.
                _statementsTable.TryGetValue(name, out OrderedCodeStatementCollection? statements);
                if (statements is not null)
                {
                    _statementsTable[name] = null; // prevent recursion
                    // we look through the statements to find the variableRef or fieldRef that matches this name
                    string? typeName = null;
                    foreach (CodeStatement statement in statements)
                    {
                        if (statement is CodeVariableDeclarationStatement codeVariableDeclaration)
                        {
                            typeName = codeVariableDeclaration.Type.BaseType;
                            break;
                        }
                    }

                    // next, invoke the serializer for this component
                    if (typeName is not null)
                    {
                        Type? type = manager.GetType(typeName);
                        if (type is null)
                        {
                            manager.ReportError(new CodeDomSerializerException(string.Format(SR.SerializerTypeNotFound, typeName), manager));
                        }
                        else if (statements.Count > 0)
                        {
                            CodeDomSerializer? serializer = GetSerializer(manager, type);
                            if (serializer is null)
                            {
                                // We report this as an error.  This indicates that there are code statements in initialize component that we do not know how to load.
                                manager.ReportError(new CodeDomSerializerException(
                                    string.Format(SR.SerializerNoSerializerForComponent, type.FullName), manager));
                            }
                            else
                            {
                                try
                                {
                                    object? instance = serializer.Deserialize(manager, statements);
                                    resolved = instance is not null;
                                    if (resolved)
                                    {
                                        _statementsTable[name] = (OrderedCodeStatementCollection?)instance;
                                    }
                                }
                                catch (Exception ex)
                                {
                                    manager.ReportError(ex);
                                }
                            }
                        }
                    }

                    // if we can't find a typeName to get a serializer with we fallback to deserializing each statement individually using the default serializer.
                    else
                    {
                        foreach (CodeStatement cs in statements)
                        {
                            DeserializeStatement(manager, cs);
                        }

                        resolved = true;
                    }

                    if (_objectState!.Remove(name, out CodeDomComponentSerializationState? state))
                    {
                        DeserializeState(manager, name, state);
                    }

                    if (_expressions.Remove(name, out List<CodeExpression>? exps))
                    {
                        foreach (CodeExpression exp in exps)
                        {
                            DeserializeExpression(manager, name, exp);
                        }

                        resolved = true;
                    }
                }
                else
                {
                    resolved = ((IDictionary)_statementsTable)[name] is not null;
                    if (!resolved)
                    {
                        // this is condition 2 of the comment at the start of this method.
                        if (_expressions.TryGetValue(name, out List<CodeExpression>? exps))
                        {
                            foreach (CodeExpression exp in exps)
                            {
                                object? exValue = DeserializeExpression(manager, name, exp);
                                if (exValue is not null && !resolved && canInvokeManager && manager.GetInstance(name) is null)
                                {
                                    manager.SetName(exValue, name);
                                    resolved = true;
                                }
                            }
                        }

                        // Sometimes components won't be in either the statements table or the expressions table, for example, this occurs for resources  during undo/redo. In these cases the component should be resolvable by the manager. Never do this when we have been asked by the serialization manager to resolve the name;  otherwise we may infinitely recurse.
                        if (!resolved && canInvokeManager)
                        {
                            resolved = manager.GetInstance(name) is not null;
                        }

                        // In this case we still need to correctly deserialize default properties &  design-time only properties.
                        if (resolved && _objectState!.TryGetValue(name, out CodeDomComponentSerializationState? state))
                        {
                            DeserializeState(manager, name, state);
                        }
                    }

                    if (!resolved && canInvokeManager)
                    {
                        manager.ReportError(new CodeDomSerializerException(string.Format(SR.CodeDomComponentSerializationServiceDeserializationError, name), manager));
                        Debug.Fail($"No statements or instance for name and no lone expressions: {name}");
                    }
                }

                return resolved;
            }

            private void DeserializeState(IDesignerSerializationManager manager, string name, CodeDomComponentSerializationState state)
            {
                DeserializeDefaultProperties(manager, name, state.Properties);
                DeserializeDesignTimeProperties(manager, name, state.Resources);
                DeserializeEventResets(manager, name, state.Events);
                DeserializeModifier(manager, name, state.Modifier);
            }

            private static void DeserializeEventResets(IDesignerSerializationManager? manager, string name, List<string>? eventNames)
            {
                if (eventNames is not null && manager is not null && !string.IsNullOrEmpty(name))
                {
                    object? comp = manager.GetInstance(name);
                    if (comp is not null && manager.GetService(typeof(IEventBindingService)) is IEventBindingService ebs)
                    {
                        PropertyDescriptorCollection eventProps = ebs.GetEventProperties(TypeDescriptor.GetEvents(comp));
                        if (eventProps is not null)
                        {
                            foreach (string eventName in eventNames)
                            {
                                PropertyDescriptor? prop = eventProps[eventName];

                                prop?.SetValue(comp, null);
                            }
                        }
                    }
                }
            }

            private static void DeserializeModifier(IDesignerSerializationManager manager, string name, object? state)
            {
                if (state is null)
                {
                    return;
                }

                Debug.Assert(state is MemberAttributes, "Attempting to deserialize a null modifier");
                object? comp = manager.GetInstance(name);
                if (comp is not null)
                {
                    MemberAttributes modifierValue = (MemberAttributes)state;
                    PropertyDescriptor? modifierProp = TypeDescriptor.GetProperties(comp)["Modifiers"];
                    modifierProp?.SetValue(comp, modifierValue);
                }
            }

            public override object Serialize(IDesignerSerializationManager manager, object state)
            {
                throw new NotSupportedException();
            }

            /// <summary>
            ///  For everything in the serialization manager's container, we need a variable ref,
            ///  just in case something that has changed has a reference to another object. We also
            ///  must do this for everything that we are serializing that is not marked as EntireObject.
            ///  Otherwise reference could leak and cause the entire object to be serialized.
            /// </summary>
            internal void SetupVariableReferences(IDesignerSerializationManager manager, IContainer container, Dictionary<object, ObjectData> objectData, IList shimObjectNames)
            {
                foreach (IComponent c in container.Components)
                {
                    string? name = TypeDescriptor.GetComponentName(c);
                    if (!string.IsNullOrEmpty(name))
                    {
                        bool needVar = !(objectData.TryGetValue(c, out ObjectData? data) && data.EntireObject);

                        if (needVar)
                        {
                            CodeVariableReferenceExpression var = new(name);
                            SetExpression(manager, c, var);
                            if (!shimObjectNames.Contains(name))
                            {
                                shimObjectNames.Add(name);
                            }

                            if (c.Site.TryGetService(out INestedContainer? nested) && nested.Components.Count > 0)
                            {
                                SetupVariableReferences(manager, nested, objectData, shimObjectNames);
                            }
                        }
                    }
                }
            }

            /// <summary>
            ///  Serializes the given set of objects (contained in objectData) into the given object state dictionary.
            /// </summary>
            internal void Serialize(IDesignerSerializationManager manager, Dictionary<object, ObjectData> objectData, Dictionary<string, CodeDomComponentSerializationState> objectState, IList shimObjectNames)
            {
                if (manager.GetService<IContainer>() is { } container)
                {
                    SetupVariableReferences(manager, container, objectData, shimObjectNames);
                }

                // Next, save a statement collection for each object.
                StatementContext statementCtx = new();
                statementCtx.StatementCollection.Populate(objectData.Keys);
                manager.Context.Push(statementCtx);
                try
                {
                    foreach (ObjectData data in objectData.Values)
                    {
                        object? code = null;
                        CodeStatementCollection? ctxStatements = null;
                        Dictionary<string, object?>? resources = null;
                        CodeStatementCollection extraStatements = [];
                        manager.Context.Push(extraStatements);
                        if (manager.TryGetSerializer(data._value.GetType(), out CodeDomSerializer? serializer))
                        {
                            if (data.EntireObject)
                            {
                                if (!IsSerialized(manager, data._value))
                                {
                                    code = data.Absolute
                                        ? serializer.SerializeAbsolute(manager, data._value)
                                        : serializer.Serialize(manager, data._value);

                                    ctxStatements = statementCtx.StatementCollection[data._value];
                                    if (ctxStatements?.Count == 0)
                                    {
                                        ctxStatements = null;
                                    }

                                    if (extraStatements.Count > 0 && code is CodeStatementCollection existingStatements)
                                    {
                                        existingStatements.AddRange(extraStatements);
                                    }
                                }
                                else
                                {
                                    code = statementCtx.StatementCollection[data._value];
                                }
                            }
                            else
                            {
                                CodeStatementCollection codeStatements = [];
                                foreach (MemberData md in data.Members)
                                {
                                    if (md._member.Attributes.Contains(DesignOnlyAttribute.Yes))
                                    {
                                        // For design time properties, we write their value into a resource blob.
#pragma warning disable SYSLIB0050 // Type or member is obsolete
                                        if (md._member is PropertyDescriptor prop && prop.PropertyType.IsSerializable)
                                        {
                                            resources ??= [];

                                            resources[prop.Name] = prop.GetValue(data._value);
                                        }
#pragma warning restore SYSLIB0050 // Type or member is obsolete
                                    }
                                    else
                                    {
                                        codeStatements.AddRange(md._absolute
                                            ? serializer.SerializeMemberAbsolute(manager, data._value, md._member)
                                            : serializer.SerializeMember(manager, data._value, md._member));
                                    }
                                }

                                code = codeStatements;
                            }
                        }

                        if (extraStatements.Count > 0)
                        {
                            if (code is CodeStatementCollection existingStatements)
                            {
                                existingStatements.AddRange(extraStatements);
                            }
                        }

                        manager.Context.Pop();
                        // And now search for default properties and events
                        List<string>? defaultPropList = null;
                        List<string>? defaultEventList = null;
                        IEventBindingService? ebs = manager.GetService<IEventBindingService>();
                        if (data.EntireObject)
                        {
                            PropertyDescriptorCollection props = TypeDescriptor.GetProperties(data._value);
                            foreach (PropertyDescriptor prop in props)
                            {
                                if (!prop.ShouldSerializeValue(data._value)
                                    && !prop.Attributes.Contains(DesignerSerializationVisibilityAttribute.Hidden))
                                {
                                    if (!prop.IsReadOnly || prop.Attributes.Contains(DesignerSerializationVisibilityAttribute.Content))
                                    {
                                        defaultPropList ??= new(data.Members.Count);
                                        defaultPropList.Add(prop.Name);
                                    }
                                }
                            }

                            if (ebs is not null)
                            {
                                PropertyDescriptorCollection events = ebs.GetEventProperties(TypeDescriptor.GetEvents(data._value));
                                foreach (PropertyDescriptor eventProp in events)
                                {
                                    if (eventProp is null || eventProp.IsReadOnly)
                                    {
                                        continue;
                                    }

                                    if (eventProp.GetValue(data._value) is null)
                                    {
                                        defaultEventList ??= [];

                                        defaultEventList.Add(eventProp.Name);
                                    }
                                }
                            }
                        }
                        else
                        {
                            foreach (MemberData md in data.Members)
                            {
                                if (md._member is PropertyDescriptor prop && !prop.ShouldSerializeValue(data._value))
                                {
                                    if (ebs?.GetEvent(prop) is not null)
                                    {
                                        Debug.Assert(prop.GetValue(data._value) is null, "ShouldSerializeValue and GetValue are differing");
                                        defaultEventList ??= [];

                                        defaultEventList.Add(prop.Name);
                                    }
                                    else
                                    {
                                        defaultPropList ??= new(data.Members.Count);
                                        defaultPropList.Add(prop.Name);
                                    }
                                }
                            }
                        }

                        // Check for non-default modifiers property
                        object? modifier = TypeDescriptor.GetProperties(data._value)["Modifiers"]?.GetValue(data._value);

                        if (code is not null || defaultPropList is not null)
                        {
                            objectState[data._name] = new CodeDomComponentSerializationState(code, ctxStatements, defaultPropList, resources, defaultEventList, modifier);
                        }
                    }
                }
                finally
                {
                    manager.Context.Pop();
                }
            }
        }
    }
}
