// 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 MS.Internal.Xaml.Context;
using MS.Utility;
using System.Collections;
using System.Windows.Baml2006;
using System.Windows.Diagnostics;
using System.Windows.Media;
using System.Xaml;
using System.Xaml.Permissions;
using System.Windows.Data;

namespace System.Windows.Markup
{
    internal class WpfXamlLoader
    {
        private static Lazy<XamlMember> XmlSpace = new Lazy<XamlMember>(() => new WpfXamlMember(XmlAttributeProperties.XmlSpaceProperty, true));

        public static object Load(System.Xaml.XamlReader xamlReader, bool skipJournaledProperties, Uri baseUri)
        {
            XamlObjectWriterSettings settings = XamlReader.CreateObjectWriterSettings();
            object result = WpfXamlLoader.Load(xamlReader, null, skipJournaledProperties, null, settings, baseUri);
            EnsureXmlNamespaceMaps(result, xamlReader.SchemaContext);
            return result;
        }

        public static object LoadDeferredContent(System.Xaml.XamlReader xamlReader, IXamlObjectWriterFactory writerFactory,
            bool skipJournaledProperties, Object rootObject, XamlObjectWriterSettings parentSettings, Uri baseUri)
        {
            XamlObjectWriterSettings settings = XamlReader.CreateObjectWriterSettings(parentSettings);
            // Don't set settings.RootObject because this isn't the real root
            return Load(xamlReader, writerFactory, skipJournaledProperties, rootObject, settings, baseUri);
        }

        public static object LoadBaml(System.Xaml.XamlReader xamlReader, bool skipJournaledProperties,
            Object rootObject, XamlAccessLevel accessLevel, Uri baseUri)
        {
            XamlObjectWriterSettings settings = XamlReader.CreateObjectWriterSettingsForBaml();
            settings.RootObjectInstance = rootObject;
            settings.AccessLevel = accessLevel;
            object result = Load(xamlReader, null, skipJournaledProperties, rootObject, settings, baseUri);
            EnsureXmlNamespaceMaps(result, xamlReader.SchemaContext);
            return result;
        }

        internal static void EnsureXmlNamespaceMaps(object rootObject, XamlSchemaContext schemaContext)
        {
            DependencyObject depObj = rootObject as DependencyObject;
            if (depObj == null)
            {
                return;
            }
            // If a user passed in custom mappings via XamlTypeMapper, we need to preserve thos
            // in the XmlNamespaceMaps property. Otherwise, we can just leave it empty and let all
            // type resolutions fall back to the shared SchemaContext.
            var typeMapperContext = schemaContext as XamlTypeMapper.XamlTypeMapperSchemaContext;
            Hashtable namespaceMaps;
            if (typeMapperContext == null)
            {
                namespaceMaps = new Hashtable();
            }
            else
            {
                namespaceMaps = typeMapperContext.GetNamespaceMapHashList();
            }
            depObj.SetValue(XmlAttributeProperties.XmlNamespaceMapsProperty, namespaceMaps);
        }

        private static object Load(System.Xaml.XamlReader xamlReader, IXamlObjectWriterFactory writerFactory,
            bool skipJournaledProperties, Object rootObject, XamlObjectWriterSettings settings, Uri baseUri)
        {
            XamlObjectWriter xamlWriter = null;
            XamlContextStack<WpfXamlFrame> stack = new XamlContextStack<WpfXamlFrame>(() => new WpfXamlFrame());
            int persistId = 1;

            settings.AfterBeginInitHandler = delegate (object sender, System.Xaml.XamlObjectEventArgs args)
            {
                if (EventTrace.IsEnabled(EventTrace.Keyword.KeywordXamlBaml | EventTrace.Keyword.KeywordPerf, EventTrace.Level.Verbose))
                {
                    IXamlLineInfo ixli = xamlReader as IXamlLineInfo;

                    int lineNumber = -1;
                    int linePosition = -1;

                    if (ixli != null && ixli.HasLineInfo)
                    {
                        lineNumber = ixli.LineNumber;
                        linePosition = ixli.LinePosition;
                    }

                    EventTrace.EventProvider.TraceEvent(
                        EventTrace.Event.WClientParseXamlBamlInfo,
                        EventTrace.Keyword.KeywordXamlBaml | EventTrace.Keyword.KeywordPerf, EventTrace.Level.Verbose,
                        args.Instance == null ? 0 : PerfService.GetPerfElementID(args.Instance),
                        lineNumber,
                        linePosition);
                }

                UIElement uiElement = args.Instance as UIElement;
                if (uiElement != null)
                {
                    uiElement.SetPersistId(persistId++);
                }

                XamlSourceInfoHelper.SetXamlSourceInfo(args.Instance, args, baseUri);

                DependencyObject dObject = args.Instance as DependencyObject;
                if (dObject != null && stack.CurrentFrame.XmlnsDictionary != null)
                {
                    XmlnsDictionary dictionary = stack.CurrentFrame.XmlnsDictionary;
                    dictionary.Seal();

                    XmlAttributeProperties.SetXmlnsDictionary(dObject, dictionary);
                }

                stack.CurrentFrame.Instance = args.Instance;
                
                if (xamlReader is RestrictiveXamlXmlReader && args != null)
                {
                    if (args.Instance is System.Windows.ResourceDictionary rd)
                    {
                        rd.IsUnsafe = true;
                    }
                    else if (args.Instance is System.Windows.Controls.Frame frame)
                    {
                        frame.NavigationService.IsUnsafe = true;
                    }
                    else if (args.Instance is System.Windows.Navigation.NavigationWindow nw)
                    {
                        nw.NavigationService.IsUnsafe = true;
                    }
                }

            };
            if (writerFactory != null)
            {
                xamlWriter = writerFactory.GetXamlObjectWriter(settings);
            }
            else
            {
                xamlWriter = new System.Xaml.XamlObjectWriter(xamlReader.SchemaContext, settings);
            }

            IXamlLineInfo xamlLineInfo = null;
            try
            {
                //Handle Line Numbers
                xamlLineInfo = xamlReader as IXamlLineInfo;
                IXamlLineInfoConsumer xamlLineInfoConsumer = xamlWriter as IXamlLineInfoConsumer;
                bool shouldPassLineNumberInfo = false;
                if ((xamlLineInfo != null && xamlLineInfo.HasLineInfo)
                    && (xamlLineInfoConsumer != null && xamlLineInfoConsumer.ShouldProvideLineInfo))
                {
                    shouldPassLineNumberInfo = true;
                }

                IStyleConnector styleConnector = rootObject as IStyleConnector;
                TransformNodes(xamlReader, xamlWriter,
                    false /*onlyLoadOneNode*/,
                    skipJournaledProperties,
                    shouldPassLineNumberInfo, xamlLineInfo, xamlLineInfoConsumer,
                    stack, styleConnector);
                xamlWriter.Close();
                return xamlWriter.Result;
            }
            catch (Exception e)
            {
                // Don't wrap critical exceptions or already-wrapped exceptions.
                if (MS.Internal.CriticalExceptions.IsCriticalException(e) || !XamlReader.ShouldReWrapException(e, baseUri))
                {
                    throw;
                }
                XamlReader.RewrapException(e, xamlLineInfo, baseUri);
                return null;    // this should never be executed
            }
        }

        internal static void TransformNodes(System.Xaml.XamlReader xamlReader, System.Xaml.XamlObjectWriter xamlWriter,
                                         bool onlyLoadOneNode,
                                         bool skipJournaledProperties,
                                         bool shouldPassLineNumberInfo, IXamlLineInfo xamlLineInfo, IXamlLineInfoConsumer xamlLineInfoConsumer,
                                         XamlContextStack<WpfXamlFrame> stack,
                                         IStyleConnector styleConnector)
        {
            while (xamlReader.Read())
            {
                if (shouldPassLineNumberInfo)
                {
                    if (xamlLineInfo.LineNumber != 0)
                    {
                        xamlLineInfoConsumer.SetLineInfo(xamlLineInfo.LineNumber, xamlLineInfo.LinePosition);
                    }
                }

                switch (xamlReader.NodeType)
                {
                    case System.Xaml.XamlNodeType.NamespaceDeclaration:
                        xamlWriter.WriteNode(xamlReader);
                        if (stack.Depth == 0 || stack.CurrentFrame.Type != null)
                        {
                            stack.PushScope();
                            // Need to create an XmlnsDictionary.  
                            // Look up stack to see if we have one earlier
                            //  If so, use that.  Otherwise new a xmlnsDictionary                        
                            WpfXamlFrame iteratorFrame = stack.CurrentFrame;
                            while (iteratorFrame != null)
                            {
                                if (iteratorFrame.XmlnsDictionary != null)
                                {
                                    stack.CurrentFrame.XmlnsDictionary =
                                        new XmlnsDictionary(iteratorFrame.XmlnsDictionary);
                                    break;
                                }
                                iteratorFrame = (WpfXamlFrame)iteratorFrame.Previous;
                            }
                            if (stack.CurrentFrame.XmlnsDictionary == null)
                            {
                                stack.CurrentFrame.XmlnsDictionary =
                                         new XmlnsDictionary();
                            }
                        }
                        stack.CurrentFrame.XmlnsDictionary.Add(xamlReader.Namespace.Prefix, xamlReader.Namespace.Namespace);
                        break;
                    case System.Xaml.XamlNodeType.StartObject:
                        WriteStartObject(xamlReader, xamlWriter, stack);
                        break;
                    case System.Xaml.XamlNodeType.GetObject:
                        xamlWriter.WriteNode(xamlReader);
                        // If there wasn't a namespace node before this get object, need to pushScope.
                        if (stack.CurrentFrame.Type != null)
                        {
                            stack.PushScope();
                        }
                        stack.CurrentFrame.Type = stack.PreviousFrame.Property.Type;
                        break;
                    case System.Xaml.XamlNodeType.EndObject:
                        xamlWriter.WriteNode(xamlReader);
                        // Freeze if required
                        if (stack.CurrentFrame.FreezeFreezable)
                        {
                            Freezable freezable = xamlWriter.Result as Freezable;
                            if (freezable != null && freezable.CanFreeze)
                            {
                                freezable.Freeze();
                            }
                        }
                        DependencyObject dependencyObject = xamlWriter.Result as DependencyObject;
                        if (dependencyObject != null && stack.CurrentFrame.XmlSpace.HasValue)
                        {
                            XmlAttributeProperties.SetXmlSpace(dependencyObject, stack.CurrentFrame.XmlSpace.Value ? "default" : "preserve");
                        }
                        stack.PopScope();
                        break;
                    case System.Xaml.XamlNodeType.StartMember:
                        // ObjectWriter should NOT process PresentationOptions:Freeze directive since it is Unknown
                        // The space directive node stream should not be written because it induces object instantiation,
                        // and the Baml2006Reader can produce space directives prematurely.
                        if (!(xamlReader.Member.IsDirective && xamlReader.Member == XamlReaderHelper.Freeze) &&
                            xamlReader.Member != XmlSpace.Value &&
                            xamlReader.Member != XamlLanguage.Space)
                        {
                            xamlWriter.WriteNode(xamlReader);
                        }

                        stack.CurrentFrame.Property = xamlReader.Member;
                        if (skipJournaledProperties)
                        {
                            if (!stack.CurrentFrame.Property.IsDirective)
                            {
                                System.Windows.Baml2006.WpfXamlMember wpfMember = stack.CurrentFrame.Property as System.Windows.Baml2006.WpfXamlMember;
                                if (wpfMember != null)
                                {
                                    DependencyProperty prop = wpfMember.DependencyProperty;

                                    if (prop != null)
                                    {
                                        FrameworkPropertyMetadata metadata = prop.GetMetadata(stack.CurrentFrame.Type.UnderlyingType) as FrameworkPropertyMetadata;
                                        if (metadata != null && metadata.Journal == true)
                                        {
                                            // Ignore the BAML for this member, unless it declares a value that wasn't journaled - namely a binding or a dynamic resource
                                            int count = 1;
                                            while (xamlReader.Read())
                                            {
                                                switch (xamlReader.NodeType)
                                                {
                                                    case System.Xaml.XamlNodeType.StartMember:
                                                        count++;
                                                        break;
                                                    case System.Xaml.XamlNodeType.StartObject:
                                                        XamlType xamlType = xamlReader.Type;
                                                        XamlType bindingBaseType = xamlType.SchemaContext.GetXamlType(typeof(BindingBase));
                                                        XamlType dynamicResourceType = xamlType.SchemaContext.GetXamlType(typeof(DynamicResourceExtension));
                                                        if (count == 1 && (xamlType.CanAssignTo(bindingBaseType) || xamlType.CanAssignTo(dynamicResourceType)))
                                                        {
                                                            count = 0;
                                                            WriteStartObject(xamlReader, xamlWriter, stack);
                                                        }
                                                        break;
                                                    case System.Xaml.XamlNodeType.EndMember:
                                                        count--;
                                                        if (count == 0)
                                                        {
                                                            xamlWriter.WriteNode(xamlReader);
                                                            stack.CurrentFrame.Property = null;
                                                        }
                                                        break;
                                                    case System.Xaml.XamlNodeType.Value:
                                                        DynamicResourceExtension value = xamlReader.Value as DynamicResourceExtension;
                                                        if (value != null)
                                                        {
                                                            WriteValue(xamlReader, xamlWriter, stack, styleConnector);
                                                        }
                                                        break;
                                                }
                                                if (count == 0)
                                                    break;
                                            }

                                            System.Diagnostics.Debug.Assert(count == 0, "Mismatch StartMember/EndMember");
                                        }
                                    }
                                }
                            }
                        }
                        break;
                    case System.Xaml.XamlNodeType.EndMember:
                        WpfXamlFrame currentFrame = stack.CurrentFrame;
                        XamlMember currentProperty = currentFrame.Property;
                        // ObjectWriter should not process PresentationOptions:Freeze directive nodes since it is unknown
                        // The space directive node stream should not be written because it induces object instantiation,
                        // and the Baml2006Reader can produce space directives prematurely.
                        if (!(currentProperty.IsDirective && currentProperty == XamlReaderHelper.Freeze) &&
                            currentProperty != XmlSpace.Value &&
                            currentProperty != XamlLanguage.Space)
                        {
                            xamlWriter.WriteNode(xamlReader);
                        }
                        currentFrame.Property = null;
                        break;
                    case System.Xaml.XamlNodeType.Value:
                        WriteValue(xamlReader, xamlWriter, stack, styleConnector);
                        break;
                    default:
                        xamlWriter.WriteNode(xamlReader);
                        break;
                }

                //Only do this loop for one node if loadAsync
                if (onlyLoadOneNode)
                {
                    return;
                }
            }
        }

        private static void WriteValue(Xaml.XamlReader xamlReader, XamlObjectWriter xamlWriter, XamlContextStack<WpfXamlFrame> stack, IStyleConnector styleConnector)
        {
            if (stack.CurrentFrame.Property.IsDirective && stack.CurrentFrame.Property == XamlLanguage.Shared)
            {
                bool isShared;
                if (bool.TryParse(xamlReader.Value as string, out isShared))
                {
                    if (!isShared)
                    {
                        if (!(xamlReader is Baml2006Reader))
                        {
                            throw new XamlParseException(SR.SharedAttributeInLooseXaml);
                        }
                    }
                }
            }
            // ObjectWriter should not process PresentationOptions:Freeze directive nodes since it is unknown
            if (stack.CurrentFrame.Property.IsDirective && stack.CurrentFrame.Property == XamlReaderHelper.Freeze)
            {
                bool freeze = Convert.ToBoolean(xamlReader.Value, TypeConverterHelper.InvariantEnglishUS);
                stack.CurrentFrame.FreezeFreezable = freeze;
                var bamlReader = xamlReader as System.Windows.Baml2006.Baml2006Reader;
                if (bamlReader != null)
                {
                    bamlReader.FreezeFreezables = freeze;
                }
            }
            // The space directive node stream should not be written because it induces object instantiation,
            // and the Baml2006Reader can produce space directives prematurely.
            else if (stack.CurrentFrame.Property == XmlSpace.Value || stack.CurrentFrame.Property == XamlLanguage.Space)
            {
                if (typeof(DependencyObject).IsAssignableFrom(stack.CurrentFrame.Type.UnderlyingType))
                {
                    System.Diagnostics.Debug.Assert(xamlReader.Value is string, "XmlAttributeProperties.XmlSpaceProperty has the type string.");
                    stack.CurrentFrame.XmlSpace = (string)xamlReader.Value == "default";
                }
            }
            else
            {
                // Ideally we should check if we're inside FrameworkTemplate's Content and not register those.
                // However, checking if the instance is null accomplishes the same with a much smaller perf impact.
                if (styleConnector != null &&
                    stack.CurrentFrame.Instance != null &&
                    stack.CurrentFrame.Property == XamlLanguage.ConnectionId &&
                    typeof(Style).IsAssignableFrom(stack.CurrentFrame.Type.UnderlyingType))
                {
                    styleConnector.Connect((int)xamlReader.Value, stack.CurrentFrame.Instance);
                }

                xamlWriter.WriteNode(xamlReader);
            }
        }

        private static void WriteStartObject(Xaml.XamlReader xamlReader, XamlObjectWriter xamlWriter, XamlContextStack<WpfXamlFrame> stack)
        {
            xamlWriter.WriteNode(xamlReader);
            // If there's a frame but no Type, that means there
            // was a namespace. Just set the Type
            if (stack.Depth != 0 && stack.CurrentFrame.Type == null)
            {
                stack.CurrentFrame.Type = xamlReader.Type;
            }
            else
            {
                // Propagate the FreezeFreezable property from the current stack frame
                stack.PushScope();
                stack.CurrentFrame.Type = xamlReader.Type;
                if (stack.PreviousFrame.FreezeFreezable)
                {
                    stack.CurrentFrame.FreezeFreezable = true;
                }
            }
        }
    }
}
