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

//
// Description:
// Holds the data for Avalon BindProducts in the journal
//


using System;
using System.Collections;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Formats.Nrbf;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.Security;
using MS.Internal.AppModel;
using System.Windows;
using System.Windows.Navigation;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;

namespace MS.Internal.AppModel
{
    #region SubStream struct
    [Serializable]
    internal struct SubStream
    {
        internal SubStream(string propertyName, byte[] dataBytes)
        {
            _propertyName = propertyName;
            _data = dataBytes;
        }

        internal string _propertyName;
        internal byte[] _data;
    }
    #endregion SubStream struct
    
    #region DataStreams class
    [Serializable]
    internal class DataStreams
    {
        internal DataStreams()
        {
            // Dummy constructor to keep FxCop Critical rules happy.
        }

        internal bool HasAnyData
        {
            get 
            {
                return _subStreams != null && _subStreams.Count > 0
                    || _customJournaledObjects != null && _customJournaledObjects.Count > 0;
            }
        }

        private bool HasSubStreams(object key)
        {
            return _subStreams != null && _subStreams.Contains(key);
        }

        private ArrayList GetSubStreams(object key)
        {
            ArrayList subStreams = (ArrayList) _subStreams[key];
            if (subStreams == null)
            {
                subStreams = new ArrayList(3);
                _subStreams[key] = subStreams;
            }

            return subStreams;
        }

        private delegate void NodeOperation(object node);

        /// <summary>
        /// Build an ArrayList of SubStreams where each SubStream represents one of the
        /// node's journalable DPs.
        /// </summary>
        /// <param name="element"></param>
        /// <returns>The ArrayList of SubStreams. May be null.</returns>
        private ArrayList SaveSubStreams(UIElement element)
        {
            ArrayList subStreams = null;

#pragma warning disable 618
            if ((element != null) && (element.PersistId != 0))
#pragma warning restore 618
            {
                LocalValueEnumerator dpEnumerator = element.GetLocalValueEnumerator();

                while (dpEnumerator.MoveNext())
                {
                    LocalValueEntry localValueEntry = (LocalValueEntry)dpEnumerator.Current;
                    FrameworkPropertyMetadata metadata = localValueEntry.Property.GetMetadata(element.DependencyObjectType) as FrameworkPropertyMetadata;
                                        
                    if (metadata == null)
                    {
                        continue;
                    }

                    // To be saved, a DP should have the correct metadata and NOT be an expression or data bound.
                    // Since Bind inherits from Expression, the test for Expression will suffice.
                    // NOTE: we do not journal expression. So we should let parser restore it in BamlRecordReader.SetDependencyValue.
                    // Please see Windows OS bug # 1852349 for details.
                    if (metadata.Journal && (!(localValueEntry.Value is Expression)))
                    {
                        // These properties should not be journaled.
                        // There should be a better way to do this, maybe FrameworkPropertyMetadata.CausesNavigation?
                        // Or maybe the Source property should be on INavigatorHost
                        if (object.ReferenceEquals(localValueEntry.Property, Frame.SourceProperty))
                            continue;

                        if (subStreams == null)
                        {
                            subStreams = new ArrayList(3);
                        }

                        object currentValue = element.GetValue(localValueEntry.Property);
                        byte[] bytes = null;

                        if ((currentValue != null) && !(currentValue is Uri))
                        {
                            // Convert the value of the DP into a byte array
                            MemoryStream byteStream = new MemoryStream();

                            bool success = false;
                            try
                            {
                                success = BinaryFormatWriter.TryWriteFrameworkObject(byteStream, currentValue);
                            }
                            catch (Exception ex) when (!ex.IsCriticalException())
                            {
                                // Being extra cautious here, but the Try method above should never throw in normal circumstances.
                                Debug.Fail($"Unexpected exception writing binary formatted data. {ex.Message}");
                            }

                            if(!success)
                            {
                                #pragma warning disable SYSLIB0011 // BinaryFormatter is obsolete 
                                this.Formatter.Serialize(byteStream, currentValue);
                                #pragma warning restore SYSLIB0011 // BinaryFormatter is obsolete 
                            }
                            
                            
                            bytes = byteStream.ToArray();
                            // Dispose the stream
                            ((IDisposable)byteStream).Dispose( );
                        }

                        // Save the byte array by the property name
                        subStreams.Add(new SubStream(localValueEntry.Property.Name, bytes));
                    }
                }
            }

            return subStreams;
        }

        private void SaveState(object node)
        {
            UIElement element = node as UIElement;
            if (element == null)
            {
                return;
            }

            // Due to bug 1282529, PersistId can be null. Only XAML/BAML-loaded elements have it.
            // Besides for PageFunctions journaled by type, the PersistId check below is needed
            // because elements might have been added to the tree after loading from XAML/BAML.
#pragma warning disable 618
            int persistId = element.PersistId;
#pragma warning restore 618

            if (persistId != 0)
            {
                ArrayList subStreams = this.SaveSubStreams(element);
                if (subStreams != null)
                {
                    //
                    // If one element in the tree is replaced with a new element which is created
                    // from a xaml/baml stream programatically, this new element and all its descendent nodes 
                    // would have another set of PersistId starting from 1, it would end up with two or more 
                    // elements in the same page share the same PersistId.  We cannot guarantee to restore 
                    // journaldata for the newly added elements when doing the journaling navigation.
                    //
                    // So to do some level protection to avoid application crash, the code doesn't update the 
                    // journaldata for that element id if the data was set already.
                    //
                    // We cannot track whether the element with the specific PersistId is created from 
                    //       the original xaml/baml stream, or is added later from another xaml/baml stream.
                    //       More efficient solution will be invented in next version.
                    // 
                    if (!_subStreams.Contains(persistId))
                    {
                        _subStreams[persistId] = subStreams;
                    }
                }

                IJournalState customJournalingObject = node as IJournalState;
                if (customJournalingObject != null)
                {
                    object customState = customJournalingObject.GetJournalState(JournalReason.NewContentNavigation);
                    if (customState != null)
                    {
                        if (_customJournaledObjects == null)
                        {
                            _customJournaledObjects = new HybridDictionary(2);
                        }

                        //
                        // Again, We cannot guarantee the PeristId of all elements in the same page are unique.
                        // Some IJouralState aware node such as Frame, FlowDocumentPageViewer could be added 
                        // programatically after the page is created from baml stream,  the new added node could also 
                        // be created from baml/xaml stream by Parser.
                        //
                        if (!_customJournaledObjects.Contains(persistId))
                        {
                            _customJournaledObjects[persistId] = customState;
                        }
                    }
                }
            }
        }

        internal void PrepareForSerialization()
        {
            if (_customJournaledObjects != null)
            {
                foreach (DictionaryEntry entry in _customJournaledObjects)
                {
                    CustomJournalStateInternal cjs = (CustomJournalStateInternal)entry.Value;
                    cjs.PrepareForSerialization();
                }
            }

            // Everything in _subStreams is already binary-serialized.
        }

        private void LoadSubStreams(UIElement element, ArrayList subStreams)
        {
            for (int subStreamIndex = 0; subStreamIndex < subStreams.Count; ++subStreamIndex)
            {
                SubStream subStream = (SubStream)subStreams[subStreamIndex];

                // Restore the value of an individual DP
                DependencyProperty dp = DependencyProperty.FromName(subStream._propertyName, element.GetType());
                // If the dp cannot be found it may mean that we navigated back to a loose file that has been changed.
                if (dp != null)
                {
                    object newValue = null;
                    if (subStream._data != null)
                    {
                        using MemoryStream dataStream = new(subStream._data);

                        try
                        {
                            NrbfDecoder.Decode(dataStream, leaveOpen: true).TryGetFrameworkObject(out object val);
                            newValue = val;
                        }
                        catch (Exception ex) when (!ex.IsCriticalException())
                        {
                            // Being extra cautious here, but the Try method above should never throw in normal circumstances.
                            Debug.Fail($"Unexpected exception reading binary formatted data. {ex.Message}");
                        }

                        //Using Binary formatter
                        if(newValue == null)
                        {
                            dataStream.Position = 0;
                            #pragma warning disable SYSLIB0011 // BinaryFormatter is obsolete 
                            newValue = this.Formatter.Deserialize(dataStream);
                            #pragma warning restore SYSLIB0011 // BinaryFormatter is obsolete 
                        }
                        
                    }
                    element.SetValue(dp, newValue);
                }
            }
        }

        private void LoadState(object node)
        {
            UIElement element = node as UIElement;
            if (element == null)
            {
                return;
            }

#pragma warning disable 618
            int persistId = element.PersistId;
#pragma warning restore 618

            // Due to bug 1282529, PersistId can be null. Only XAML/BAML-loaded elements have it.
            if (persistId != 0)
            {
                if (this.HasSubStreams(persistId))
                {
                    // Get the properties to restore
                    ArrayList properties = this.GetSubStreams(persistId);
                    LoadSubStreams(element, properties);
                }

                if (_customJournaledObjects != null && _customJournaledObjects.Contains(persistId))
                {
                    CustomJournalStateInternal state = 
                        (CustomJournalStateInternal)_customJournaledObjects[persistId];
                    Debug.Assert(state != null);
                    IJournalState customJournalingObject = node as IJournalState;

                    //
                    // For below two scenarios, JournalData cannot be restored successfully. For now, we just
                    // simply ignore it and don't throw exception.
                    //
                    //  A. After the tree was created from xaml/baml stream,  some elements might be replaced 
                    //     programatically with new elements which could be created from other xaml/baml by Parser.
                    //
                    //  B. If the loose xaml file has been changed since the journal data was created
                    //
                    //
                    if (customJournalingObject != null)
                    {
                        customJournalingObject.RestoreJournalState(state);
                    }
                }
            }
        }

        /// <summary>
        /// Walk the logical tree, and perform an operation on all nodes and children of nodes.
        /// This is slightly more complex than it might otherwise be because it makes no assumptions
        /// that the Logical Tree is strongly typed. It is the responsibility of the operation to make sure
        /// that it can handle the node it receives.
        /// </summary>
        /// <param name="node"></param>
        /// <param name="operation"></param>
        private void WalkLogicalTree(object node, NodeOperation operation)
        {
            if (node != null)
            {
                operation(node);
            }

            DependencyObject treeNode = node as DependencyObject;
            if (treeNode == null)
            {
                return;
            }

            IEnumerator e = LogicalTreeHelper.GetChildren(treeNode).GetEnumerator();
            if (e == null)
            {
                return;
            }

            while (e.MoveNext())
            {
                WalkLogicalTree(e.Current, operation);
            }
        }

        internal void Save(Object root)
        {
            if (_subStreams == null)
            {
                _subStreams = new HybridDictionary(3);
            }
            else
            {
                _subStreams.Clear();
            }
            WalkLogicalTree(root, new NodeOperation(this.SaveState));            
        }

        internal void Load(Object root)
        {
            if (this.HasAnyData)
            {
                WalkLogicalTree(root, new NodeOperation(this.LoadState));               
            }
        }

        internal void Clear()
        {
            _subStreams = null;
            _customJournaledObjects = null; 
        }
#pragma warning disable SYSLIB0011 // Type or member is obsolete
        #region Private and internal fields and properties
        private BinaryFormatter Formatter
        {
            get
            {
                if (_formatter == null)
                {
                    _formatter = new BinaryFormatter();
                }

                return _formatter;
            }
        }

        [ThreadStatic]
        static private BinaryFormatter _formatter;

        private HybridDictionary _subStreams = new HybridDictionary(3);

        /// <summary>
        /// PersistId->CustomJournalStateInternal map
        /// <see cref="SaveState"/>.
        /// </summary>
        private HybridDictionary _customJournaledObjects;

        #endregion Private and internal fields and properties
    }
    #endregion DataStreams class
#pragma warning restore SYSLIB0011 // Type or member is obsolete
}
