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

#pragma warning disable 1634, 1691
//
// Description:
//     ContentLocatorBase contains an ordered set of ContentLocatorParts which each identify
//     a piece of content within a certain context. Resolving one part
//     provides the context to resolve the next part.  Locators are used 
//     to refer to external data in a structured way.
//
//     Spec: Simplifying Store Cache Model.doc
//

using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics;
using System.Windows.Annotations.Storage;
using System.Windows.Data;
using System.Xml;
using System.Xml.Schema;
using System.Xml.Serialization;
using MS.Internal;
using MS.Internal.Annotations;

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

namespace System.Windows.Annotations
{
    /// <summary>
    ///     ContentLocatorBase contains an ordered set of ContentLocatorParts which each identify
    ///     a piece of content within a certain context. Resolving one part
    ///     provides the context to resolve the next part.  Locators are used
    ///     to refer to external data in a structured way.
    /// </summary>
    [XmlRoot(Namespace = AnnotationXmlConstants.Namespaces.CoreSchemaNamespace, ElementName = AnnotationXmlConstants.Elements.ContentLocator)]
    public sealed class ContentLocator : ContentLocatorBase, IXmlSerializable
    {
        //------------------------------------------------------
        //
        //  Constructors
        //
        //------------------------------------------------------
        
        #region Constructors

        /// <summary>
        ///     Creates an instance of ContentLocator.
        /// </summary>
        public ContentLocator()
        {
            _parts = new AnnotationObservableCollection<ContentLocatorPart>();
            _parts.CollectionChanged += OnCollectionChanged;
        }

        #endregion Constructors

        //------------------------------------------------------
        //
        //  Public Methods
        //
        //------------------------------------------------------
        
        #region Public Methods

        /// <summary>
        ///     Determines if this list begins with the ContentLocatorParts that
        ///     make up matchList.  All ContentLocatorParts in matchList must 
        ///     be present and in the same order in this list for 
        ///     true to be returned.
        /// </summary>
        /// <param name="locator">the list to compare with</param>
        /// <returns>
        ///     true if this list begins with the ContentLocatorParts in locator; 
        ///     false otherwise.  If locator is longer than this locator, will 
        ///     return false as well.
        /// </returns>
        /// <exception cref="ArgumentNullException">locator is null</exception>
        public bool StartsWith(ContentLocator locator)
        {
            ArgumentNullException.ThrowIfNull(locator);

            Invariant.Assert(locator.Parts != null, "Locator has null Parts property.");

            // If this locator is shorter than matchList, then this can't contain matchList.            
            #pragma warning suppress 6506 // Invariant.Assert(locator.Parts != null)
            if (this.Parts.Count < locator.Parts.Count)
            {
                return false;
            }

            for (int locatorPartIndex = 0; locatorPartIndex < locator.Parts.Count; locatorPartIndex++)
            {
                ContentLocatorPart left = locator.Parts[locatorPartIndex];
                ContentLocatorPart right = this.Parts[locatorPartIndex];

                // ContentLocator parts can be null so check for that case here
                if (left == null && right != null)
                {
                    return false;
                }

                if (!left.Matches(right))
                {
                    return false;
                }
            }

            return true;
        }

        /// <summary>
        ///     Creates a deep copy of this list.  A new list with a clone of 
        ///     every ContentLocatorPart in this list, in the same order, is returned.
        ///     Never returns null.
        /// </summary>
        /// <returns>a deep copy of this list</returns>
        public override Object Clone()
        {
            ContentLocator clone = new ContentLocator();
            ContentLocatorPart newPart = null;
            foreach (ContentLocatorPart part in this.Parts)
            {
                if (part != null)
                    newPart = (ContentLocatorPart)part.Clone();
                else
                    newPart = null;

                clone.Parts.Add(newPart);
            }

            return clone;
        }

        #region IXmlSerializable Implementation

        /// <summary>
        ///     Returns the null.  The annotations schema can be found at
        ///     http://schemas.microsoft.com/windows/annotations/2003/11/core.
        /// </summary>
        public XmlSchema GetSchema()
        {
            return null;
        }

        /// <summary>
        ///     Writes the internal data for this ContentLocator to the writer.  This 
        ///     method is used by an XmlSerializer to serialize a ContentLocator.  Don't 
        ///     use this method directly, to serialize a ContentLocator to Xml, use an 
        ///     XmlSerializer.
        /// </summary>
        /// <param name="writer">the writer to write internal data to</param>
        /// <exception cref="ArgumentNullException">writer is null</exception>
        public void WriteXml(XmlWriter writer)
        {
            ArgumentNullException.ThrowIfNull(writer);

            string prefix = writer.LookupPrefix(AnnotationXmlConstants.Namespaces.CoreSchemaNamespace);
            if (prefix == null)
            {
                writer.WriteAttributeString(AnnotationXmlConstants.Prefixes.XmlnsPrefix, AnnotationXmlConstants.Prefixes.CoreSchemaPrefix, null, AnnotationXmlConstants.Namespaces.CoreSchemaNamespace);
            }
            prefix = writer.LookupPrefix(AnnotationXmlConstants.Namespaces.BaseSchemaNamespace);
            if (prefix == null)
            {
                writer.WriteAttributeString(AnnotationXmlConstants.Prefixes.XmlnsPrefix, AnnotationXmlConstants.Prefixes.BaseSchemaPrefix, null, AnnotationXmlConstants.Namespaces.BaseSchemaNamespace);
            }

            // Write each ContentLocatorPart as its own element
            foreach (ContentLocatorPart part in _parts)
            {
                prefix = writer.LookupPrefix(part.PartType.Namespace);
                if (String.IsNullOrEmpty(prefix))
                {
                    prefix = "tmp";                    
                }

                // ContentLocatorParts cannot write themselves out becuase the element
                // name is based on the part's type.  The ContentLocatorPart instance
                // has no way (through normal serialization) to change the element
                // name it writes out at runtime.
                writer.WriteStartElement(prefix, part.PartType.Name, part.PartType.Namespace);

                // Each name/value pair for the ContentLocatorPart becomes an attribute
                foreach (KeyValuePair<string, string> pair in part.NameValuePairs)
                {
                    writer.WriteStartElement(AnnotationXmlConstants.Elements.Item, AnnotationXmlConstants.Namespaces.CoreSchemaNamespace);
                    writer.WriteAttributeString(AnnotationXmlConstants.Attributes.ItemName, pair.Key);
                    writer.WriteAttributeString(AnnotationXmlConstants.Attributes.ItemValue, pair.Value);
                    writer.WriteEndElement();
                }

                writer.WriteEndElement();
            }
        }

        /// <summary>
        ///     Reads the internal data for this ContentLocator from the reader.  This 
        ///     method is used by an XmlSerializer to deserialize a ContentLocator.  Don't 
        ///     use this method directly, to deserialize a ContentLocator from Xml, use an 
        ///     XmlSerializer.
        /// </summary>
        /// <param name="reader">the reader to read internal data from</param>
        /// <exception cref="ArgumentNullException">reader is null</exception>
        public void ReadXml(XmlReader reader)
        {
            ArgumentNullException.ThrowIfNull(reader);

            // We expect no attributes on a "ContentLocator", 
            // so throw using the name of one of the unexpected attributes
            Annotation.CheckForNonNamespaceAttribute(reader, AnnotationXmlConstants.Elements.ContentLocator);

            if (!reader.IsEmptyElement)
            {
                reader.Read();  // Reads the start of the "ContentLocator" element

                // ContentLocatorParts cannot write themselves out (see above).  They could read
                // themselves in but instead of having write code in one place and read 
                // code somewhere else - we keep it together in this class.
                while (!(AnnotationXmlConstants.Elements.ContentLocator == reader.LocalName && XmlNodeType.EndElement == reader.NodeType))
                {
                    if (XmlNodeType.Element != reader.NodeType)
                    {
                        throw new XmlException(SR.Format(SR.InvalidXmlContent, AnnotationXmlConstants.Elements.ContentLocator));
                    }

                    ContentLocatorPart part = new ContentLocatorPart(new XmlQualifiedName(reader.LocalName, reader.NamespaceURI));
                    
                    // Read each of the Item elements within the ContentLocatorPart
                    if (!reader.IsEmptyElement)
                    {
                        // We expect no attributes on a locator part tag, 
                        // so throw using the name of one of the unexpected attributes
                        Annotation.CheckForNonNamespaceAttribute(reader, part.PartType.Name);

                        reader.Read(); // Read the start of the locator part tag

                        while (!(XmlNodeType.EndElement == reader.NodeType && part.PartType.Name == reader.LocalName))
                        {
                            if (AnnotationXmlConstants.Elements.Item == reader.LocalName && reader.NamespaceURI == AnnotationXmlConstants.Namespaces.CoreSchemaNamespace)
                            {
                                string name = null;
                                string value = null;

                                while (reader.MoveToNextAttribute())
                                {
                                    switch (reader.LocalName)
                                    {
                                        case AnnotationXmlConstants.Attributes.ItemName:
                                            name = reader.Value;
                                            break;
                                        case AnnotationXmlConstants.Attributes.ItemValue:
                                            value = reader.Value;
                                            break;
                                        default:
                                            if (!Annotation.IsNamespaceDeclaration(reader))
                                                throw new XmlException(SR.Format(SR.UnexpectedAttribute, reader.LocalName, AnnotationXmlConstants.Elements.Item));
                                            break;
                                    }
                                }

                                if (name == null)
                                {
                                    throw new XmlException(SR.Format(SR.RequiredAttributeMissing, AnnotationXmlConstants.Attributes.ItemName, AnnotationXmlConstants.Elements.Item));
                                }
                                if (value == null)
                                {
                                    throw new XmlException(SR.Format(SR.RequiredAttributeMissing, AnnotationXmlConstants.Attributes.ItemValue, AnnotationXmlConstants.Elements.Item));
                                }

                                reader.MoveToContent();

                                part.NameValuePairs.Add(name, value);

                                bool isEmpty = reader.IsEmptyElement;

                                reader.Read();  // Read the beginning of the complete Item tag

                                if (!isEmpty)
                                {
                                    if (!(XmlNodeType.EndElement == reader.NodeType && AnnotationXmlConstants.Elements.Item == reader.LocalName))
                                    {
                                        // Should not contain any content, only attributes
                                        throw new XmlException(SR.Format(SR.InvalidXmlContent, AnnotationXmlConstants.Elements.Item));
                                    }
                                    else
                                    {
                                        reader.Read(); // Read the end of the Item tag
                                    }
                                }
                            }
                            else
                            {
                                // The locator part contains data other than just "Item" tags
                                throw new XmlException(SR.Format(SR.InvalidXmlContent, part.PartType.Name));
                            }
                        }
                    }

                    _parts.Add(part);

                    reader.Read();  // Read the ContentLocatorPart element                
                }
            }

            reader.Read(); // Reads the end of the "ContentLocator" element (or whole element if empty)
        }

        #endregion IXmlSerializable Implementation

        #endregion Public Methods

        //------------------------------------------------------
        //
        //  Public Operators
        //
        //------------------------------------------------------

        //------------------------------------------------------
        //
        //  Public Events
        //
        //------------------------------------------------------

        //------------------------------------------------------
        //
        //  Public Properties
        //
        //------------------------------------------------------

        #region Public Properties

        /// <summary>
        /// 
        /// </summary>
        public Collection<ContentLocatorPart> Parts
        {
            get
            {
                return _parts;
            }
        }

        #endregion Public Properties

        //------------------------------------------------------
        //
        //  Internal Methods
        //
        //------------------------------------------------------

        #region InternalMethods

        /// <summary>
        ///     Creates the dot product of this ContentLocator and the list of
        ///     ContentLocatorParts.  The result is n Locators where n is the number of
        ///     ContentLocatorParts passed in.
        ///     One of the resulting Locators is this ContentLocator.  If there are no
        ///     additional ContentLocatorParts a list with just this ContentLocator (unmodified)
        ///     is returned.
        /// </summary>
        /// <param name="additionalLocatorParts">array of ContentLocatorParts</param>
        /// <returns>array of Locators (one for each additional ContentLocatorPart)</returns>
        internal IList<ContentLocatorBase> DotProduct(IList<ContentLocatorPart> additionalLocatorParts)
        {
            List<ContentLocatorBase> results = null;

            // If there aren't any additional locator parts - this is basically a no-op
            if (additionalLocatorParts == null || additionalLocatorParts.Count == 0)
            {
                results = new List<ContentLocatorBase>(1);
                results.Add(this);
            }
            else
            {
                results = new List<ContentLocatorBase>(additionalLocatorParts.Count);

                for (int i = 1; i < additionalLocatorParts.Count; i++)
                {
                    ContentLocator loc = (ContentLocator)this.Clone();
                    loc.Parts.Add(additionalLocatorParts[i]);
                    results.Add(loc);
                }

                this.Parts.Add(additionalLocatorParts[0]);
                results.Insert(0, this);
            }
            return results;
        }

        /// <summary>
        ///     Merges this ContentLocator with a ContentLocatorBase.  If other is a 
        ///     ContentLocatorGroup, each of its Locators are added to clones of 
        ///     this ContentLocatorBase and are added to a new ContentLocatorGroup which is 
        ///     returned.  If other is a ContentLocatorBase, its appended to this 
        ///     ContentLocatorBase and this ContentLocatorBase is returned.
        ///     Both operation modify this ContentLocatorBase.
        /// </summary>
        /// <param name="other">the ContentLocatorBase to merge with</param>
        /// <returns>a ContentLocatorBase containing the final merged product</returns>
        internal override ContentLocatorBase Merge(ContentLocatorBase other)
        {
            if (other == null)
                return this;

            ContentLocatorGroup locatorGroup = other as ContentLocatorGroup;
            if (locatorGroup != null)
            {
                ContentLocatorGroup newGroup = new ContentLocatorGroup();

                ContentLocator temp = null;
                // Create n-1 clones of this LPS and append all but one
                // LPSs in the set to the clones, adding the clones to a
                // new ContentLocatorGroup
                foreach (ContentLocator loc in locatorGroup.Locators)
                {
                    if (temp == null)
                    {
                        temp = loc;
                    }
                    else
                    {
                        ContentLocator clone = (ContentLocator)this.Clone();
                        clone.Append(loc);
                        newGroup.Locators.Add(clone);
                    }
                }

                // Finally, add the remaining LPS in the set to this LPS
                // and add this to the new ContentLocatorGroup
                if (temp != null)
                {
                    this.Append(temp);
                    newGroup.Locators.Add(this);
                }

                if (newGroup.Locators.Count == 0)
                    return this;
                else
                    return newGroup;
            }
            else
            {
                // Safe cast - ContentLocator only has two subclasses
                this.Append((ContentLocator)other);
                return this;
            }
        }

        /// <summary>
        ///     Appends a ContentLocator to this ContentLocator.  The passed in 
        ///     ContentLocator is not modified in anyway.  Its ContentLocatorParts are cloned.
        /// </summary>
        /// <param name="other">locator to append</param>
        internal void Append(ContentLocator other)
        {
            Invariant.Assert(other != null, "Parameter 'other' is null.");

            foreach(ContentLocatorPart part in other.Parts)
            {
                this.Parts.Add((ContentLocatorPart)part.Clone());
            }
        }

        #endregion Internal Methods

        //------------------------------------------------------
        //
        //  Private Methods
        //
        //------------------------------------------------------

        #region Private Methods

        /// <summary>
        /// Listens for change events from the list of parts.  Fires a change event
        /// for this ContentLocator when an event is received.
        /// </summary>
        private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            FireLocatorChanged("Parts");
        }

        #endregion Private Methods

        //------------------------------------------------------
        //
        //  Private Fields
        //
        //------------------------------------------------------

        #region Private Fields

        /// <summary>
        ///     List of ContentLocatorParts in this locator.
        /// </summary>
        private AnnotationObservableCollection<ContentLocatorPart> _parts;

        #endregion Private Fields
    }
}
