text:
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml;
using System.Xml.Serialization;
using System.Xml.XPath;
using System.Xml.Xsl;
using MindTouch.Dream;
using MindTouch.IO;
using MindTouch.Security.Cryptography;

namespace MindTouch.Xml {

    /// <summary>
    /// A fluent interface-style Document Object Model for building, manipulating and ingesting Xml.
    /// </summary>
    public class XDoc : IEnumerable<XDoc>, ICloneable {

        //--- Constants ---

        /// <summary>
        /// RFC DateTime format used by <see cref="XDoc"/>
        /// </summary>
        public const string RFC_DATETIME_FORMAT = "yyyy-MM-dd\\THH:mm:ssZ";

        /// <summary>
        /// Namespace for XDoc digital signatures generated by <see cref="Sign"/>.
        /// </summary>
        public const string NS_DSIG = "http://mindtouch.com/2008/dsig";

        /// <summary>
        /// XmlNs namespace Uri.
        /// </summary>
        public const string NS_XMLNS = "http://www.w3.org/2000/xmlns/";

        /// <summary>
        /// Xml namespace Uri.
        /// </summary>
        public const string NS_XML = "http://www.w3.org/XML/1998/namespace";

        //--- Class Fields ---

        /// <summary>
        /// An Empty XDoc instance.
        /// </summary>
        public static readonly XDoc Empty = new XDoc();

        /// <summary>
        /// Xml Name Table shared by <see cref="XDoc"/> instances
        /// </summary>
        public static readonly XmlNameTable XmlNameTable = SysUtil.NameTable;

        private static readonly log4net.ILog _log = LogUtils.CreateLog();
        private static readonly Regex _elementRegex = new Regex(@"(?<name>.+)\[(?<index>\d+)\]$", RegexOptions.Compiled | RegexOptions.CultureInvariant);

        //--- Class Properties ---
        /// <summary>
        /// Returns the DOCTYPE string to mark an XML document as being XHTML 1.0 Strict compliant.
        /// </summary>
        public static string XhtmlDocType {
            get {
                return "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">";
            }
        }

        //--- Class Operators ---

        /// <summary>
        /// Equality operator to compare content equality of two XDoc instances.
        /// </summary>
        /// <param name="left">XDoc instance.</param>
        /// <param name="right">XDoc instance.</param>
        /// <returns><see langword="true"/></returns>
        public static bool operator ==(XDoc left, XDoc right) {
            if((object)left == null && (object)right == null) {
                return true;
            }
            if((object)left == null || (object)right == null) {
                return false;
            }
            return left.Equals(right);
        }

        /// <summary>
        /// Inequality operator to compare content equality of two XDoc instances.
        /// </summary>
        /// <param name="left">XDoc instance.</param>
        /// <param name="right">XDoc instance.</param>
        /// <returns><see langword="true"/></returns>
        public static bool operator !=(XDoc left, XDoc right) {
            return !(left == right);
        }

        //--- Class Methods ---

        /// <summary>
        /// Removes a collection of XDoc instances from their respective containers.
        /// </summary>
        /// <param name="items">Collection of XDoc instances to remove or Null.</param>
        public static void RemoveAll(ICollection<XDoc> items) {
            if(items != null) {
                foreach(XDoc item in items) {
                    item.Remove();
                }
            }
        }

        /// <summary>
        /// Converts an XmlNodeList instace into an XmlNode array.
        /// </summary>
        /// <param name="list">XmlNodeList to convert to an array. Cannot be null.</param>
        /// <returns>Array of XmlNode instances.</returns>
        public static XmlNode[] NewListXmlNode(XmlNodeList list) {
            XmlNode[] result = new XmlNode[list.Count];
            int index = 0;
            foreach(XmlNode node in list) {
                result[index++] = node;
            }
            return result;
        }

        /// <summary>
        /// Creates a new XDoc instance is a selection of documents.
        /// </summary>
        /// <param name="documents">Array of XDoc instances to convert into a selection.</param>
        /// <returns>Returns an XDoc instance that is a selection of XDoc instances.</returns>
        public static XDoc CreateSelection(params XDoc[] documents) {
            if(documents == null) {
                throw new ArgumentNullException("documents");
            }
            var list = (from doc in documents where !doc.IsEmpty select doc.AsXmlNode).ToArray();
            return (list.Length > 0) ? new XDoc(list, 0, null, null) : Empty;
        }

        /// <summary>
        /// Creates a new XDoc instance is a selection of XmlNodes.
        /// </summary>
        /// <param name="nodes">Array of XmlNodes to convert into a selection.</param>
        /// <returns>Returns an XDoc instance that is a selection of the XmlNodes.</returns>
        public static XDoc CreateNodesSelection(params XmlNode[] nodes) {
            if(nodes == null) {
                throw new ArgumentNullException("nodes");
            }
            if(nodes.Length == 0) {
                return Empty;
            }
            for(int i = 0; i < nodes.Length; ++i) {
                if(nodes[i] == null) {
                    throw new ArgumentNullException("nodes[" + i + "]");
                }
                if(nodes[i].OwnerDocument == null) {
                    throw new ArgumentNullException("nodes[" + i + "] missing owner document");
                }
                if(nodes[i].OwnerDocument.DocumentElement == null) {
                    throw new ArgumentNullException("nodes[" + i + "] missing root element");
                }
            }
            return new XDoc(nodes, 0, null, null);
        }

        /// <summary>
        /// Recursively compares two XmlNode instances. Immediately returns false upon finding the first differnece between the two nodes.
        /// </summary>
        /// <param name="left">The left node. Cannot be null.</param>
        /// <param name="right">The right node. Cannot be null.</param>
        /// <returns>Returns true if the two nodes match, false otherwise.</returns>
        private static bool CompareNode(XmlNode left, XmlNode right) {
            if(left == null) {
                throw new ArgumentNullException("left");
            }
            if(right == null) {
                throw new ArgumentNullException("right");
            }

            // compare node types
            if(left.NodeType != right.NodeType) {
                return false;
            }

            // check if values need to be compared
            switch(left.NodeType) {
            case XmlNodeType.Attribute:
            case XmlNodeType.CDATA:
            case XmlNodeType.Comment:
            case XmlNodeType.ProcessingInstruction:
            case XmlNodeType.SignificantWhitespace:
            case XmlNodeType.Text:
            case XmlNodeType.Whitespace:
            case XmlNodeType.XmlDeclaration:
                if(left.Value != right.Value) {
                    return false;
                }
                break;
            }

            // check if names need to be compared
            switch(left.NodeType) {
            case XmlNodeType.Attribute:
            case XmlNodeType.DocumentType:
            case XmlNodeType.Element:
            case XmlNodeType.Entity:
            case XmlNodeType.EntityReference:
            case XmlNodeType.Notation:
            case XmlNodeType.ProcessingInstruction:
                if(left.NamespaceURI != right.NamespaceURI) {
                    return false;
                }
                if(left.LocalName != right.LocalName) {
                    return false;
                }
                break;
            }

            // check if attributes need to be compared
            switch(left.NodeType) {
            case XmlNodeType.Element:
                if(left.Attributes.Count != right.Attributes.Count) {
                    return false;
                }
                for(int i = 0; i < left.Attributes.Count; ++i) {
                    if(!CompareNode(left.Attributes[i], right.Attributes[i])) {
                        return false;
                    }
                }
                break;
            }

            // check if child nodes need to be compared
            switch(left.NodeType) {
            case XmlNodeType.Element:
            case XmlNodeType.Document:
            case XmlNodeType.DocumentFragment:
                if(left.ChildNodes.Count != right.ChildNodes.Count) {
                    return false;
                }
                for(int i = 0; i < left.ChildNodes.Count; ++i) {
                    if(!CompareNode(left.ChildNodes[i], right.ChildNodes[i])) {
                        return false;
                    }
                }
                break;
            }
            return true;
        }

        internal static XmlDocument NewXmlDocument() {
            XmlDocument result = new XmlDocument(XmlNameTable) { PreserveWhitespace = true, XmlResolver = null };
            return result;
        }

        //--- Fields ---
        private XmlDocument _doc;
        private XmlNode _root;
        private XmlNode[] _list;
        private int _index;
        private XmlNamespaceManager _nsManager;
        private bool _exclusive;

        //--- Constructors ---
        /// <summary>
        /// Creates a new XDoc instance with the given root tag.
        /// </summary>
        /// <param name="tag">Root tag of the new XML document. Cannot be null.</param>
        public XDoc(string tag) : this(tag, null) { }

        /// <summary>
        /// Creates a new XDoc instance with the given root tag and the given implicit namespace.
        /// </summary>
        /// <param name="tag">Root tag of the new XML document. Cannot be null.</param>
        /// <param name="ns">Tag xml namespace.</param>
        public XDoc(string tag, string ns) : this(null, tag, ns) { }

        /// <summary>
        /// Creates a new XDoc instance with the given root tag and the given implicit namespace.
        /// </summary>
        /// <param name="prefix">Prefix for the root tag of the new XML document.</param>
        /// <param name="tag">Root tag of the new XML document. Cannot be null.</param>
        /// <param name="ns">Tag xml namespace.</param>
        public XDoc(string prefix, string tag, string ns) {
            if(tag == null) {
                throw new ArgumentNullException("tag");
            }
            XmlDocument doc = NewXmlDocument();
            _doc = doc;
            _root = doc;
            _nsManager = new XmlNamespaceManager(_doc.NameTable);

            // initialize document
            if(!string.IsNullOrEmpty(ns)) {
                if(string.IsNullOrEmpty(prefix)) {
                    _doc.AppendChild(_doc.CreateElement(tag, ns));
                    _nsManager.AddNamespace(string.Empty, ns);
                    _nsManager.AddNamespace("_", ns);
                } else {
                    _doc.AppendChild(_doc.CreateElement(prefix, tag, ns));
                    _nsManager.AddNamespace(prefix, ns);
                    _nsManager.AddNamespace(string.Empty, string.Empty);
                    _nsManager.AddNamespace("_", string.Empty);
                }
            } else {

                // create the root child node
                _doc.AppendChild(_doc.CreateElement(tag, string.Empty));
                _nsManager.AddNamespace("_", string.Empty);
            }
            _list = new XmlNode[] { doc.DocumentElement };
            _index = 0;
            _root = CurrentNode;
        }

        /// <summary>
        /// Creates a new XDoc instance from an existing XmlDocument instance.
        /// </summary>
        /// <param name="doc">XmlDocument instance with a root element. Cannot be null.</param>
        public XDoc(XmlDocument doc) {
            if(doc == null) {
                throw new ArgumentNullException("doc");
            }
            if(doc.DocumentElement == null) {
                throw new ArgumentException("XmlDocument instance does not have a root element.");
            }
            Initialize(new XmlNode[] { doc.DocumentElement }, 0, doc.DocumentElement, null);
        }

        /// <summary>
        /// Creates a new XDoc instance from an existing, non-empty XDoc instance.
        /// </summary>
        /// <param name="doc">Non-empty XDoc instance. Cannot be null.</param>
        public XDoc(XDoc doc) {
            if((doc == null) || doc.IsEmpty) {
                throw new ArgumentNullException("doc");
            }
            XmlNode node = doc.AsXmlNode;
            Initialize(new[] { node }, 0, node, doc._nsManager);
        }

        /// <summary>
        /// Protected constructor for XDoc inheritors for creating a new instance with the state of the old instance.
        /// </summary>
        /// <param name="list">List of nodes for the current cursor into the <see cref="XmlDocument"/>.</param>
        /// <param name="index">Index into the list of nodes providing the cursor pointer.</param>
        /// <param name="root">Root of the XDoc instance.</param>
        /// <param name="nsManager">Namespace manager to use with the instance.</param>
        internal protected XDoc(XmlNode[] list, int index, XmlNode root, XmlNamespaceManager nsManager) {
            Initialize(list, index, root, nsManager);
        }

        private XDoc() { }

        //--- Operators ---
        /// <summary>
        /// Returns the XDoc at the given index of the root XDoc or an empty instance if the index is out of bounds.
        /// </summary>
        /// <param name="index">Index of the child instance of the root XDoc instance.</param>
        /// <returns></returns>
        public XDoc this[int index] {
            get {
                if(!IsEmpty && (index >= 0) && (index < _root.ChildNodes.Count)) {
                    return new XDoc(NewListXmlNode(_root.ChildNodes), index, null, _nsManager);
                }
                return Empty;
            }
        }

        /// <summary>
        /// Returns a new rooted XDoc instance based on the supplied XPath. The selection starts at the first result.
        /// </summary>
        /// <param name="path">XPath 1.0 expression to select XDoc instances in the current XDoc instance.</param>
        /// <returns></returns>
        public XDoc this[string path] {
            get {

                // OPTIMIZATION: check if xpath is just looking for a specific attribute or element
                bool simple = true;
                for(int i = 0; i < path.Length; ++i) {
                    char c = path[i];
                    if(!((i == 0 && c == '@') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || (c == '-') || (c == '.'))) {
                        simple = false;
                        break;
                    }
                }
                if(simple) {
                    XmlElement element = CurrentNode as XmlElement;
                    if(element != null) {
                        if(path[0] == '@') {
                            XmlNode child = element.Attributes[path.Substring(1)];
                            if(child != null) {
                                return new XDoc(new[] { child }, 0, null, _nsManager);
                            }
                        } else {
                            List<XmlNode> children = new List<XmlNode>();
                            for(XmlNode child = element.FirstChild; child != null; child = child.NextSibling) {
                                if((child.NodeType == XmlNodeType.Element) && child.Name.EqualsInvariant(path)) {
                                    children.Add(child);
                                }
                            }
                            if(children.Count > 0) {
                                return new XDoc(children.ToArray(), 0, null, _nsManager);
                            }
                        }
                    }
                    return Empty;
                }
                return AtPath(path, _nsManager);
            }
        }

        /// <summary>
        /// Returns a new rooted XDoc instance based on the supplied XPath. The selection starts at the given index position.
        /// </summary>
        /// <param name="path">XPath 1.0 expression to select XDoc instances in the current XDoc instance.</param>
        /// <param name="namespaces">Namespaces to use for xpath expression</param>
        /// <returns></returns>
        public XDoc AtPath(string path, XmlNamespaceManager namespaces) {
            if(path == null) {
                throw new ArgumentNullException("path");
            }
            if(!IsEmpty) {
                if(namespaces == null) {
                    namespaces = _nsManager;
                }
                try {
                    List<XmlNode> list = null;
                    XPathNavigator navigator = _root.CreateNavigator();
                    if(navigator != null) {
                        XPathExpression expr = XPathExpression.Compile(path, namespaces);
                        XPathNodeIterator iterator = navigator.Select(expr);
                        foreach(XPathNavigator nav in iterator) {
                            if(list == null) {
                                list = new List<XmlNode>();
                            }
                            list.Add(((IHasXmlNode)nav).GetNode());
                        }
                    }
                    if((list != null) && (list.Count > 0)) {
                        return new XDoc(list.ToArray(), 0, null, namespaces);
                    }
                } catch(Exception e) {
                    _log.ErrorExceptionMethodCall(e, "AtPath", path);
                    throw;
                }
            }
            return Empty;
        }

        /// <summary>
        /// Returns a new rooted XDoc instance representing the given XmlNode instance. The XmlNode instance must belong to the same XmlDocument instance as the XDoc instance.
        /// </summary>
        /// <param name="node">XmlNode instance to wrapt. Cannot be null.</param>
        /// <returns></returns>
        public XDoc this[XmlNode node] {
            get {
                if(node == null) {
                    throw new ArgumentNullException("node");
                }
                if(IsEmpty) {
                    return Empty;
                }
                if(!ReferenceEquals(_doc, node.OwnerDocument)) {
                    return Empty;
                }
                return new XDoc(new[] { node }, 0, null, _nsManager);
            }
        }

        //--- Properties ---

        /// <summary>
        /// Returns the XDoc instance representing the root element of XmlDocument instance.
        /// </summary>
        public XDoc Doc {
            get {
                return this[_doc.DocumentElement];
            }
        }

        /// <summary>
        /// Returns the root XDoc instance, which is either the XmlDocument element or the original node returned by an XPath expression.
        /// </summary>
        public XDoc Root {
            get {
                return new XDoc(_list, _index, _root, _nsManager).EndAll();
            }
        }

        /// <summary>
        /// Returns the number of items in the XDoc selection.
        /// </summary>
        public int ListLength {
            get {
                if(IsEmpty) {
                    return 0;
                }
                if(_list == null) {
                    return 1;
                }
                return _list.Length;
            }
        }

        /// <summary>
        /// Returns the textual contents of the XDoc instance, including text and inner XML nodes.
        /// </summary>
        public string Contents {
            get {
                if(_doc != null) {
                    XmlNode current = CurrentNode;
                    if((current.NodeType == XmlNodeType.Element) && (current.ChildNodes.Count == 1) && (current.ChildNodes[0].NodeType == XmlNodeType.Text)) {
                        return current.ChildNodes[0].Value;
                    }
                    if(current.NodeType == XmlNodeType.Text || current.NodeType == XmlNodeType.Attribute) {
                        return current.Value;
                    }
                    return current.InnerXml;
                }
                return string.Empty;
            }
        }

        /// <summary>
        /// Returns an XDoc selection that includes all child elements.
        /// </summary>
        public XDoc Elements {
            get {
                if(_doc != null) {
                    return this["*"];
                }
                return Empty;
            }
        }

        /// <summary>
        /// Returns the first XDoc instance of the selection.
        /// </summary>
        public XDoc First {
            get {
                if(!IsEmpty && (_list != null)) {
                    return new XDoc(_list, 0, null, _nsManager);
                }
                return Empty;
            }
        }

        /// <summary>
        /// Returns the next XDoc instance in the selection.
        /// </summary>
        public XDoc Next {
            get {
                if(!IsEmpty && (_list != null)) {
                    if(_index < (_list.Length - 1)) {
                        return new XDoc(_list, _index + 1, null, _nsManager);
                    }
                }
                return Empty;
            }
        }

        /// <summary>
        /// Returns true if the XDoc instance is empty.
        /// </summary>
        public bool IsEmpty { get { return _doc == null; } }

        /// <summary>
        /// Returns true if the XDoc instance is an XmlText node.
        /// </summary>
        public bool IsText { get { return CurrentNode is XmlText; } }

        /// <summary>
        /// Returns the element or attribute name of the current XDoc instance.
        /// </summary>
        /// <exception cref="InvalidOperationException">XDoc is empty</exception>
        public string Name {
            get {
                if(IsEmpty) {
                    throw new InvalidOperationException("xdoc is empty");
                }
                return CurrentNode.LocalName;
            }
        }

        /// <summary>
        /// Returns the element or attribute namespace URI of the current XDoc instance.
        /// </summary>
        /// <exception cref="InvalidOperationException">XDoc is empty</exception>
        public string NamespaceURI {
            get {
                if(IsEmpty) {
                    throw new InvalidOperationException("xdoc is empty");
                }
                return CurrentNode.NamespaceURI;
            }
        }

        /// <summary>
        /// Returns the element or attribute prefix of the current XDoc instance.
        /// </summary>
        /// <exception cref="InvalidOperationException">XDoc is empty</exception>
        public string Prefix {
            get {
                if(IsEmpty) {
                    throw new InvalidOperationException("xdoc is empty");
                }
                return CurrentNode.Prefix;
            }
        }

        /// <summary>
        /// Returns the element or attribute qualified name containing the namespace URI, prefix, and name of the current XDoc instance.
        /// </summary>
        /// <exception cref="InvalidOperationException">XDoc is empty</exception>
        public Tuplet<string, string, string> QualifiedName {
            get {
                if(IsEmpty) {
                    throw new InvalidOperationException("xdoc is empty");
                }
                XmlNode current = CurrentNode;
                return new Tuplet<string, string, string>(current.NamespaceURI, current.Prefix, current.LocalName);
            }
        }

        /// <summary>
        /// Return the XmlNode wrappter by the XDoc instance. Use with caution and only if absolutely nessecary.
        /// </summary>
        public XmlNode AsXmlNode { get { return CurrentNode; } }

        /// <summary>
        /// Returns the value of all contained text nodes that are immediate children.
        /// </summary>
        public string AsText {
            get {
                if(IsEmpty) {
                    return null;
                }
                XmlNode current = CurrentNode;

                // check if node is an element
                if(current.NodeType == XmlNodeType.Element) {
                    int count = current.ChildNodes.Count;
                    if(count != 0) {

                        // check if we have exactly one #text/cdata node
                        if(count == 1) {
                            var text = GetNodeText(current.ChildNodes[0]);
                            return text;
                        }

                        // concatenate all consecutive #text nodes
                        var result = new StringBuilder();
                        foreach(XmlNode node in current.ChildNodes) {
                            var text = GetNodeText(node);
                            if(text == null) {
                                break;
                            }
                            result.Append(text);
                        }
                        return result.ToString();
                    }
                    return string.Empty;
                }

                // return the nodes text value, if we can
                return GetNodeText(current);
            }
        }

        /// <summary>
        /// Returns the value of all contained text nodes including nested ones.
        /// </summary>
        public string AsInnerText {
            get {
                if(IsEmpty) {
                    return null;
                }
                return AsXmlNode.InnerText;
            }
        }

        /// <summary>
        /// Returns the contents as boolean or null if contents could not be converted.
        /// </summary>
        public bool? AsBool { get { return As<bool>(); } }

        /// <summary>
        /// Returns the contents as byte or null if contents could not be converted.
        /// </summary>
        public byte? AsByte { get { return As<byte>(); } }

        /// <summary>
        /// Returns the contents as signed byte or null if contents could not be converted.
        /// </summary>
        public sbyte? AsSByte { get { return As<sbyte>(); } }

        /// <summary>
        /// Returns the contents as short integer or null if contents could not be converted.
        /// </summary>
        public short? AsShort { get { return As<short>(); } }

        /// <summary>
        /// Returns the contents as unsigned short integer or null if contents could not be converted.
        /// </summary>
        public ushort? AsUShort { get { return As<ushort>(); } }

        /// <summary>
        /// Returns the contents as integer or null if contents could not be converted.
        /// </summary>
        public int? AsInt { get { return As<int>(); } }

        /// <summary>
        /// Returns the contents as unsigned integer or null if contents could not be converted.
        /// </summary>
        public uint? AsUInt { get { return As<uint>(); } }

        /// <summary>
        /// Returns the contents as long integer or null if contents could not be converted.
        /// </summary>
        public long? AsLong { get { return As<long>(); } }

        /// <summary>
        /// Returns the contents as unsigned long integer or null if contents could not be converted.
        /// </summary>
        public ulong? AsULong { get { return As<ulong>(); } }

        /// <summary>
        /// Returns the contents as floating-point number or null if contents could not be converted.
        /// </summary>
        public float? AsFloat { get { return As<float>(); } }

        /// <summary>
        /// Returns the contents as double floating-point number or null if contents could not be converted.
        /// </summary>
        public double? AsDouble { get { return As<double>(); } }

        /// <summary>
        /// Returns the contents as decimal number or null if contents could not be converted.
        /// </summary>
        public decimal? AsDecimal { get { return As<decimal>(); } }

        /// <summary>
        /// Returns the contents as date/time or null if contents could not be converted.
        /// </summary>
        public DateTime? AsDate {
            get {
                if(IsEmpty) {
                    return null;
                }
                return DateTimeUtil.ParseInvariant(Contents).ToUniversalTime();
            }
        }

        /// <summary>
        /// Returns the contents as uri or null if contents could not be converted.
        /// </summary>
        public XUri AsUri {
            get {
                if(IsEmpty) {
                    return null;
                }
                XUri result = new XUri(AsText);
                DreamContext context = DreamContext.CurrentOrNull;
                if(context != null) {
                    result = context.AsLocalUri(result);
                }
                return result;
            }
        }

        /// <summary>
        /// Returns the contents as byte array or null if contents could not be converted.
        /// </summary>
        public byte[] AsBytes { get { return IsEmpty ? new byte[0] : Convert.FromBase64String(Contents); } }

        /// <summary>
        /// Returns the parent XDoc instance or Empty if none exists.
        /// </summary>
        public XDoc Parent {
            get {
                if(IsEmpty) {
                    return Empty;
                }
                XmlNode current = CurrentNode;
                XmlNode parent;
                if(current is XmlAttribute) {
                    parent = ((XmlAttribute)current).OwnerElement;
                } else {
                    parent = current.ParentNode;
                }
                if((parent == null) || (parent is XmlDocument)) {
                    return Empty;
                }
                return this[parent];
            }
        }

        /// <summary>
        /// Gets/sets the value of the xml:lang attribute. Returns null if no such attribute exists. Set null to clear out the attribute.
        /// </summary>
        public string Language {
            get {
                return this["@xml:lang"].AsText;
            }
            set {
                if(value != null) {
                    Attr("xml:lang", value);
                } else {
                    RemoveAttr("xml:lang");
                }
            }
        }

        /// <summary>
        /// Gets the namespace URI associated with the empty prefix
        /// </summary>
        public string EmptyNamespaceUri {
            get {
                return _nsManager.LookupNamespace(string.Empty);
            }
        }

        private XmlNode CurrentNode {
            get {
                if(_doc != null) {
                    return (_list != null) ? _list[_index] : _doc;
                }
                return null;
            }
        }

        private XmlNode RootNode { get { return _root; } }

        private XmlElement RootElement {
            get {
                if(_root.NodeType == XmlNodeType.Document) {
                    return ((XmlDocument)_root).DocumentElement;
                }
                return (XmlElement)_root;
            }
        }

        //--- Methods ---
        /// <summary>
        /// Returns XDoc contents converted into given type.
        /// </summary>
        /// <typeparam name="T">Type to convet into (must be struct type)</typeparam>
        /// <returns>Converted value or null if no value is present</returns>
        public T? As<T>() where T : struct {
            if(IsEmpty) {
                return null;
            }
            return (T)SysUtil.ChangeType(Contents, typeof(T));
        }

        /// <summary>
        /// Check the element/attribute name of the current XDoc.
        /// </summary>
        /// <param name="name">Name to compare with</param>
        /// <returns>True if the name matches</returns>
        public bool HasName(string name) {
            return !IsEmpty && Name.EqualsInvariant(name);
        }

        /// <summary>
        /// Check the element/attribute name of the current XDoc.
        /// </summary>
        /// <param name="name">Name to compare with</param>
        /// <param name="namespaceUri">XML namespace to compar with</param>
        /// <returns>True if the name and namespace match</returns>
        public bool HasName(string name, string namespaceUri) {
            return !IsEmpty && Name.EqualsInvariant(name) && NamespaceURI.EqualsInvariant(namespaceUri);
        }

        /// <summary>
        /// Check if current XDoc contains named attribute.
        /// </summary>
        /// <param name="name">Name to compare with</param>
        /// <returns>True if the name matches an attribute</returns>
        public bool HasAttr(string name) {
            return !IsEmpty && (CurrentNode.Attributes != null) && (CurrentNode.Attributes[name] != null);
        }

        /// <summary>
        /// Check if current XDoc contains named attribute.
        /// </summary>
        /// <param name="name">Name to compare with</param>
        /// <param name="namespaceUri">XML namespace to compar with</param>
        /// <returns>True if the name matches an attribute</returns>
        public bool HasAttr(string name, string namespaceUri) {
            return !IsEmpty && (CurrentNode.Attributes != null) && (CurrentNode.Attributes[name, namespaceUri] != null);
        }

        /// <summary>
        /// Returns a deep clone of the XDoc instance starting at the root XDoc instance.
        /// </summary>
        /// <returns>Deep clone of the root XDoc instance</returns>
        public XDoc Clone() {
            if(IsEmpty) {
                return this;
            }
            XmlDocument doc;
            if(_root.ParentNode.NodeType == XmlNodeType.Document) {
                // check if the document is marked as exclusive, in which case we can forgo the cloning
                if(_exclusive) {
                    _exclusive = false;
                    return this;
                }
                doc = (XmlDocument)_root.ParentNode.Clone();
            } else {
                doc = NewXmlDocument();
                XmlNode node = doc.ImportNode(_root, true);
                doc.AppendChild(node);
            }
            return new XDoc(doc);
        }

        /// <summary>
        /// Mark the current XDoc instance as exclusive.  This will skip the next Clone() operation when it occurs.
        /// </summary>
        /// <returns>XDoc instance marked as exclusive</returns>
        public XDoc MarkExclusive() {
            if(!IsEmpty) {
                if(_root.ParentNode.NodeType != XmlNodeType.Document) {
                    throw new InvalidOperationException("only the root node can be marked as exclusive");
                }
                _exclusive = true;
            }
            return this;
        }

        /// <summary>
        /// Returns a shallow copy of current XDoc instance.
        /// </summary>
        /// <returns>Shallow copy of the XDoc instance</returns>
        public XDoc Copy() {
            if(IsEmpty) {
                return this;
            }
            return new XDoc(_list, _index, _root, _nsManager);
        }

        /// <summary>
        /// Returns the XDoc instance at the index position in the XDoc selection.
        /// </summary>
        /// <param name="index">index position</param>
        /// <returns>selected XDoc node or Empty if index is out of bounds</returns>
        public XDoc AtPosition(int index) {
            if(!IsEmpty && (_list != null)) {
                if((index >= 0) && (index < _list.Length)) {
                    return new XDoc(_list, index, null, _nsManager);
                }
            }
            return Empty;
        }

        /// <summary>
        /// Add an attribute to the XDoc instance.
        /// </summary>
        /// <param name="tag">Attribute name</param>
        /// <param name="value">Value of the attribute</param>
        /// <returns>Current XDoc instance</returns>
        public XDoc Attr(string tag, string value) {
            if(IsEmpty) {
                throw new InvalidOperationException("xdoc is empty");
            }
            if(tag == null) {
                throw new ArgumentNullException("tag");
            }
            if(value == null) {
                return this;
            }

            // create attribute
            string ns = GetNamespaceUri(tag);
            XmlAttribute node = _doc.CreateAttribute(tag, ns);
            node.Value = value;
            CurrentNode.Attributes.Append(node);
            return this;
        }

        /// <summary>
        /// Add an attribute to the XDoc instance.
        /// </summary>
        /// <param name="tag">Attribute name</param>
        /// <param name="namespaceUri">Attribute XML namespace</param>
        /// <param name="value">Value of the attribute</param>
        /// <returns>Current XDoc instance</returns>
        public XDoc Attr(string tag, string namespaceUri, string value) {
            if(namespaceUri == null) {
                return Attr(tag, value);
            }
            if(IsEmpty) {
                throw new InvalidOperationException("xdoc is empty");
            }
            if(tag == null) {
                throw new ArgumentNullException("tag");
            }
            if(value == null) {
                return this;
            }

            // create attribute
            XmlAttribute node = _doc.CreateAttribute(tag, NamespaceURI);
            node.Value = value;
            CurrentNode.Attributes.Append(node);
            return this;
        }

        /// <summary>
        /// Add an attribute to the XDoc instance.
        /// </summary>
        /// <param name="tag">Attribute name</param>
        /// <param name="value">Value of the attribute</param>
        /// <returns>Current XDoc instance</returns>
        public XDoc Attr(string tag, XUri value) {
            if(tag == null) {
                throw new ArgumentNullException("tag");
            }
            if(value == null) {
                return this;
            }
            DreamContext context = DreamContext.CurrentOrNull;
            if(context != null) {
                value = context.AsPublicUri(value);
            }
            return Attr(tag, value.ToString());
        }

        /// <summary>
        /// Add an attribute to the XDoc instance.
        /// </summary>
        /// <param name="tag">Attribute name</param>
        /// <param name="value">Value of the attribute</param>
        /// <returns>Current XDoc instance</returns>
        public XDoc Attr(string tag, int value) {
            return Attr(tag, value.ToString());
        }

        /// <summary>
        /// Add an attribute to the XDoc instance.
        /// </summary>
        /// <param name="tag">Attribute name</param>
        /// <param name="value">Value of the attribute</param>
        /// <returns>Current XDoc instance</returns>
        public XDoc Attr(string tag, uint value) {
            return Attr(tag, value.ToString());
        }

        /// <summary>
        /// Add an attribute to the XDoc instance.
        /// </summary>
        /// <param name="tag">Attribute name</param>
        /// <param name="value">Value of the attribute</param>
        /// <returns>Current XDoc instance</returns>
        public XDoc Attr(string tag, long value) {
            return Attr(tag, value.ToString());
        }

        /// <summary>
        /// Add an attribute to the XDoc instance.
        /// </summary>
        /// <param name="tag">Attribute name</param>
        /// <param name="value">Value of the attribute</param>
        /// <returns>Current XDoc instance</returns>
        public XDoc Attr(string tag, ulong value) {
            return Attr(tag, value.ToString());
        }

        /// <summary>
        /// Add an attribute to the XDoc instance.
        /// </summary>
        /// <param name="tag">Attribute name</param>
        /// <param name="value">Value of the attribute</param>
        /// <returns>Current XDoc instance</returns>
        public XDoc Attr(string tag, float value) {
            return Attr(tag, value.ToString());
        }

        /// <summary>
        /// Add an attribute to the XDoc instance.
        /// </summary>
        /// <param name="tag">Attribute name</param>
        /// <param name="value">Value of the attribute</param>
        /// <returns>Current XDoc instance</returns>
        public XDoc Attr(string tag, double value) {
            return Attr(tag, value.ToString());
        }

        /// <summary>
        /// Add an attribute to the XDoc instance.
        /// </summary>
        /// <param name="tag">Attribute name</param>
        /// <param name="value">Value of the attribute</param>
        /// <returns>Current XDoc instance</returns>
        public XDoc Attr(string tag, DateTime value) {
            return Attr(tag, value.ToUniversalTime().ToString(RFC_DATETIME_FORMAT));
        }

        /// <summary>
        /// Add an attribute to the XDoc instance.
        /// </summary>
        /// <param name="tag">Attribute name</param>
        /// <param name="value">Value of the attribute</param>
        /// <returns>Current XDoc instance</returns>
        public XDoc Attr(string tag, bool value) {
            return Attr(tag, value ? "true" : "false");
        }

        /// <summary>
        /// Starts a new child element. (e.g. <foo>)
        /// </summary>
        /// <param name="tag">Element name</param>
        /// <returns>Created XDoc instance</returns>
        public XDoc Start(string tag) {
            if(IsEmpty) {
                throw new InvalidOperationException("xdoc is empty");
            }
            if(tag == null) {
                throw new ArgumentNullException("tag");
            }
            string ns = GetNamespaceUri(tag) ?? EmptyNamespaceUri;
            XmlNode node = _doc.CreateElement(tag, ns);
            CurrentNode.AppendChild(node);
            _list = new[] { node };
            _index = 0;
            return this;
        }

        /// <summary>
        /// Starts a new child element. (e.g. <foo>)
        /// </summary>
        /// <param name="tag">Element name</param>
        /// <param name="namespaceUri">Element XML namespace</param>
        /// <returns>Created XDoc instance</returns>
        public XDoc Start(string tag, string namespaceUri) {
            if(namespaceUri == null) {
                return Start(tag);
            }
            if(IsEmpty) {
                throw new InvalidOperationException("xdoc is empty");
            }
            if(tag == null) {
                throw new ArgumentNullException("tag");
            }
            XmlNode node = _doc.CreateElement(tag, namespaceUri);
            CurrentNode.AppendChild(node);
            _list = new[] { node };
            _index = 0;
            return this;
        }

        /// <summary>
        /// Adds a complete child element.
        /// </summary>
        /// <param name="tag">Element name</param>
        /// <returns>Current XDoc instance</returns>
        public XDoc Elem(string tag) {
            return Start(tag).End();
        }

        /// <summary>
        /// Adds a complete child element.
        /// </summary>
        /// <param name="tag">Element name</param>
        /// <param name="value">Value to add</param>
        /// <returns>Current XDoc instance</returns>
        public XDoc Elem(string tag, string value) {
            if(value == null) {
                return this;
            }
            return Start(tag).Value(value).End();
        }

        /// <summary>
        /// Adds a complete child element.
        /// </summary>
        /// <param name="tag">Element name</param>
        /// <param name="value">Value to add</param>
        /// <returns>Current XDoc instance</returns>
        public XDoc Elem(string tag, DateTime value) {
            return Start(tag).Value(value).End();
        }

        /// <summary>
        /// Adds a complete child element.
        /// </summary>
        /// <param name="tag">Element name</param>
        /// <param name="value">Value to add</param>
        /// <returns>Current XDoc instance</returns>
        public XDoc Elem(string tag, byte[] value) {
            if(value == null) {
                return this;
            }
            return Start(tag).Value(value).End();
        }

        /// <summary>
        /// Adds a complete child element.
        /// </summary>
        /// <param name="tag">Element name</param>
        /// <param name="value">Value to add</param>
        /// <returns>Current XDoc instance</returns>
        public XDoc Elem(string tag, XUri value) {
            if(value == null) {
                return this;
            }
            return Start(tag).Value(value).End();
        }

        /// <summary>
        /// Adds a complete child.
        /// </summary>
        /// <param name="tag">Element name</param>
        /// <param name="value">Value to add</param>
        /// <returns>Current XDoc instance</returns>
        public XDoc Elem(string tag, object value) {
            if(value == null) {
                return this;
            }
            return Start(tag).Value(value).End();
        }

        /// <summary>
        /// Adds a text node.
        /// </summary>
        /// <param name="value">Value to add</param>
        /// <returns>Current XDoc instance</returns>
        public XDoc Value(string value) {
            if(IsEmpty) {
                throw new InvalidOperationException("xdoc is empty");
            }
            if(value == null) {
                return this;
            }
            XmlNode node = _doc.CreateTextNode(value);
            CurrentNode.AppendChild(node);
            return this;
        }

        /// <summary>
        /// Adds a text node.
        /// </summary>
        /// <param name="value">Value to add</param>
        /// <returns>Current XDoc instance</returns>
        public XDoc Value(DateTime value) {
            return Value(value.ToUniversalTime().ToString(RFC_DATETIME_FORMAT));
        }

        /// <summary>
        /// Adds a text node.
        /// </summary>
        /// <param name="value">Value to add</param>
        /// <returns>Current XDoc instance</returns>
        public XDoc Value(bool value) {
            return Value(value ? "true" : "false");
        }

        /// <summary>
        /// Adds a text node.
        /// </summary>
        /// <param name="value">Value to add</param>
        /// <returns>Current XDoc instance</returns>
        public XDoc Value(byte[] value) {
            if(value == null) {
                return this;
            }
            return Value(Convert.ToBase64String(value));
        }

        /// <summary>
        /// Adds a text node.
        /// </summary>
        /// <param name="value">Value to add</param>
        /// <returns>Current XDoc instance</returns>
        public XDoc Value(XUri value) {
            if(value == null) {
                return this;
            }
            DreamContext context = DreamContext.CurrentOrNull;
            if(context != null) {
                value = context.AsPublicUri(value);
            }
            return Value(value.ToString());
        }

        /// <summary>
        /// Adds a text node.
        /// </summary>
        /// <param name="value">Value to add</param>
        /// <returns>Current XDoc instance</returns>
        public XDoc Value(object value) {
            if(value is DateTime) {
                return Value((DateTime)value);
            }
            if(value is byte[]) {
                return Value((byte[])value);
            }
            if(value is XUri) {
                return Value((XUri)value);
            }
            if(value is XDoc) {
                return Add((XDoc)value);
            }
            if(value is bool) {
                return Value((bool)value);
            }
            if(value != null) {
                return Value(value.ToString());
            }
            return this;
        }

        /// <summary>
        /// Replaces the text node with a new text node.
        /// </summary>
        /// <param name="value">Replacement value</param>
        /// <returns></returns>
        public XDoc ReplaceValue(bool value) {
            return ReplaceValue(value ? "true" : "false");
        }

        /// <summary>
        /// Replaces the text node with a new text node.
        /// </summary>
        /// <param name="value">Replacement value</param>
        /// <returns></returns>
        public XDoc ReplaceValue(byte[] value) {
            if(value == null) {

                // TODO (steveb): how should we handle the null case?

                throw new ArgumentNullException("value");
            }
            return ReplaceValue(Convert.ToBase64String(value));
        }

        /// <summary>
        /// Replaces the text node with a new text node.
        /// </summary>
        /// <param name="value">Replacement value</param>
        /// <returns></returns>
        public XDoc ReplaceValue(DateTime value) {
            return ReplaceValue(value.ToUniversalTime().ToString(RFC_DATETIME_FORMAT));
        }

        /// <summary>
        /// Replaces the text node with a new text node.
        /// </summary>
        /// <param name="value">Replacement value</param>
        /// <returns></returns>
        public XDoc ReplaceValue(object value) {
            if(value is DateTime) {
                return ReplaceValue((DateTime)value);
            }
            if(value is byte[]) {
                return ReplaceValue((byte[])value);
            }
            if(value is XUri) {
                return ReplaceValue((XUri)value);
            }
            if(value is bool) {
                return ReplaceValue((bool)value);
            }
            if(value != null) {
                return ReplaceValue(value.ToString());
            }

            // TODO (steveb): how should we handle the null case?

            throw new ArgumentNullException("value");
        }

        /// <summary>
        /// Replaces the text node with a new text node.
        /// </summary>
        /// <param name="value">Replacement value</param>
        /// <returns></returns>
        public XDoc ReplaceValue(XUri value) {
            if(value == null) {

                // TODO (steveb): how should we handle the null case?

                throw new ArgumentNullException("value");
            }
            DreamContext context = DreamContext.CurrentOrNull;
            if(context != null) {
                value = context.AsPublicUri(value);
            }
            return ReplaceValue(value.ToString());
        }

        /// <summary>
        /// Replaces the text node with a new text node.
        /// </summary>
        /// <param name="value">Replacement value</param>
        /// <returns></returns>
        public XDoc ReplaceValue(string value) {
            if(IsEmpty) {
                throw new InvalidOperationException("xdoc is empty");
            }
            if(value == null) {

                // TODO (steveb): how should we handle the null case?

                throw new ArgumentNullException("value");
            }
            XmlNode current = CurrentNode;
            if(current is XmlElement) {

                // replace element value
                XmlNode node = current.FirstChild;
                while(node != null) {
                    XmlNode next = node.NextSibling;
                    if(node is XmlText) {
                        current.RemoveChild(node);
                    }
                    node = next;
                }
                return Value(value);
            }
            if(current is XmlText) {

                // replace text node
                current.Value = value;
                return this;
            }
            if(current is XmlAttribute) {

                // replace attribute
                current.Value = value;
                return this;
            }

            // unknown content type
            throw new InvalidOperationException("xdoc has no value");
        }

        /// <summary>
        /// Inserts a text or attribute node at the given XPath expression, creating elements as needed.
        /// </summary>
        /// <param name="xpath">XPath expression</param>
        /// <param name="value">Value to insert</param>
        /// <returns></returns>
        public XDoc InsertValueAt(string xpath, string value) {
            if(xpath == null) {
                throw new ArgumentNullException("xpath");
            }
            if(value == null) {
                return this;
            }

            // check if path is empty
            XDoc cursor = Copy();
            if(xpath.Length > 0) {

                // split path
                string[] path = xpath.Trim('/').Split('/');
                for(int i = 0; i < path.Length; i++) {
                    string token = path[i];
                    int index = -1;

                    // check if token contains index
                    Match m = _elementRegex.Match(token);
                    if(m.Success) {
                        index = int.Parse(m.Groups["index"].Value) - 1;
                        token = m.Groups["name"].Value;
                    }
                    if(token.Length == 0) {
                        throw new ArgumentException("xpath");
                    }

                    // check if this is the last token in the path
                    if(i == (path.Length - 1)) {

                        // check if last token is an attribute
                        if(token[0] == '@') {

                            // insert attribute
                            cursor.Attr(token.Substring(1), value);
                            return this;
                        }
                        if(token[0] != '#') {

                            // always create the last token (unless it's a '#text' node)
                            cursor = cursor.Start(token);
                        }
                    } else if(index >= 0) {

                        // add as many new nodes as required
                        int count = index;
                        XDoc current = cursor[token];
                        while(--count >= 0) {
                            if(current.IsEmpty) {
                                current = cursor.Start(token).Copy();
                                cursor.End();
                            }
                            current = current.Next;
                        }
                        if(current.IsEmpty) {
                            current = cursor.Start(token).Copy();
                            cursor.End();
                        }

                        // select token at index position
                        cursor = current;
                    } else {
                        XDoc current = cursor[token + "[last()]"];

                        // add one node
                        if(current.IsEmpty) {
                            current = cursor.Start(token).Copy();
                            cursor.End();
                        }
                        cursor = current;
                    }
                }
            }

            // add value to current position
            if(value.Length != 0) {
                cursor.Value(value);
            }
            return this;
        }

        /// <summary>
        /// Adds a CDATA section.
        /// </summary>
        /// <param name="value">Contents of the CDATA section.</param>
        /// <returns></returns>
        public XDoc CDataSection(string value) {
            if(IsEmpty) {
                throw new InvalidOperationException("xdoc is empty");
            }
            if(value == null) {
                return this;
            }
            XmlNode node = _doc.CreateCDataSection(value);
            CurrentNode.AppendChild(node);
            return this;
        }

        /// <summary>
        /// Adds an XML comment node.
        /// </summary>
        /// <param name="value">Comment text.</param>
        /// <returns></returns>
        public XDoc Comment(string value) {
            if(IsEmpty) {
                throw new InvalidOperationException("xdoc is empty");
            }
            if(value == null) {
                return this;
            }
            XmlNode node = _doc.CreateComment(value);
            CurrentNode.AppendChild(node);
            return this;
        }

        /// <summary>
        /// Adds a conditional XML comment node.
        /// </summary>
        /// <param name="condition">Condition expression.</param>
        /// <param name="contents">Comment text.</param>
        /// <returns></returns>
        public XDoc ConditionalComment(string condition, XDoc contents) {
            if(IsEmpty) {
                throw new InvalidOperationException("xdoc is empty");
            }
            if(string.IsNullOrEmpty(condition)) {
                throw new ArgumentException("condition");
            }
            if((contents == null) || contents.IsEmpty) {
                return this;
            }
            XmlNode node = _doc.CreateComment(string.Format("[{0}]>{1}<![endif]", condition, contents.Contents));
            CurrentNode.AppendChild(node);
            return this;
        }

        /// <summary>
        /// Ends a child element. (e.g. </foo>)
        /// </summary>
        /// <returns></returns>
        /// <exception cref="InvalidOperationException">Thrown if the markerNode is not found before reaching the root node or the document is empty</exception>
        public XDoc End() {
            if(IsEmpty) {
                throw new InvalidOperationException("xdoc is empty");
            }
            if(ReferenceEquals(CurrentNode, _root)) {
                throw new InvalidOperationException("xdoc is at root position");
            }
            XmlNode parent = CurrentNode.ParentNode;
            if(ReferenceEquals(parent, _doc)) {
                _list = null;
                _index = -1;
            } else {
                _list = new[] { parent };
                _index = 0;
            }
            return this;
        }

        /// <summary>
        /// Ends child elements until the current node is the same as the marker node. Will throw an <see cref="InvalidOperationException"/> if
        /// the marker isn't encountered before the root. It is however fine to use the root as the marker node.
        /// </summary>
        /// <param name="markerNode">Xml node to use a marker of where the document was before the target <see cref="Start(string)"/> was called.</param>
        /// <returns></returns>
        /// <exception cref="InvalidOperationException">Thrown if the markerNode is not found before reaching the root node or the document is empty</exception>
        public XDoc End(XmlNode markerNode) {
            if(IsEmpty) {
                throw new InvalidOperationException("xdoc is empty");
            }
            var currentNode = AsXmlNode;
            var root = Root.AsXmlNode;
            while(currentNode != markerNode) {
                End();
                currentNode = AsXmlNode;
            }
            return this;
        }

        /// <summary>
        /// Ends all child elements until the root XDoc instance is reached.
        /// </summary>
        /// <returns></returns>
        public XDoc EndAll() {
            if(IsEmpty) {
                throw new InvalidOperationException("xdoc is empty");
            }

            // close tags until we reach teh root
            while(!ReferenceEquals(CurrentNode, _root)) {
                End();
            }
            return this;
        }

        /// <summary>
        /// Removes this XDoc instance from the containing document.
        /// </summary>
        /// <returns></returns>
        public XDoc Remove() {
            if(!IsEmpty) {
                XmlNode current = CurrentNode;
                if(current is XmlAttribute) {
                    XmlAttribute attribute = (XmlAttribute)current;
                    if(attribute.OwnerElement != null) {
                        attribute.OwnerElement.Attributes.Remove(attribute);
                    }
                } else if(current.ParentNode != null) {
                    current.ParentNode.RemoveChild(CurrentNode);
                }
                _list = null;
            }
            return Empty;
        }

        /// <summary>
        /// Removes an attriute.
        /// </summary>
        /// <param name="name">Name of attribute to remove.</param>
        /// <returns></returns>
        public XDoc RemoveAttr(string name) {
            if(IsEmpty) {
                return this;
            }
            if(name == null) {
                throw new ArgumentNullException("name");
            }
            CurrentNode.Attributes.RemoveNamedItem(name);
            return this;
        }

        /// <summary>
        /// Removes all XDoc instances in current selection.
        /// </summary>
        public void RemoveAll() {
            foreach(XDoc doc in ToList()) {
                doc.Remove();
            }
        }

        /// <summary>
        /// Removes all child nodes.
        /// </summary>
        /// <returns></returns>
        public XDoc RemoveNodes() {
            if(IsEmpty) {
                return this;
            }
            XmlNode current = CurrentNode;
            while(current.FirstChild != null) {
                current.RemoveChild(current.FirstChild);
            }
            return this;
        }

        /// <summary>
        /// Replaces this XDoc instance with another one.
        /// </summary>
        /// <param name="doc">Replacement XDoc instance.</param>
        /// <returns></returns>
        public XDoc Replace(XDoc doc) {
            if(IsEmpty) {
                throw new InvalidOperationException("xdoc is empty");
            }
            if((doc == null) || doc.IsEmpty) {
                return Remove();
            } else {

                // fetch top element node of document to add
                XmlElement docnode = doc.RootElement;

                // BUG #832: is this necessary when the namespace match?

                // check if current document has a namespace
                string defaultNs = _doc.DocumentElement.NamespaceURI;
                if((defaultNs.Length != 0) && (docnode.NamespaceURI.Length == 0)) {

                    // BUGBUGBUG (steveb): we can't modify the document we're importing; can this be done later?

                    docnode.SetAttribute("xmlns", defaultNs);
                }
                XmlNode node = _doc.ImportNode(docnode, true);

                // replace node
                return ReplaceCurrentNode(node);
            }
        }

        /// <summary>
        /// Replaces this XDoc instance with the child nodes of another one.
        /// </summary>
        /// <param name="doc">Container of replacement nodes.</param>
        /// <returns></returns>
        public XDoc ReplaceWithNodes(XDoc doc) {
            if(IsEmpty) {
                throw new InvalidOperationException("xdoc is empty");
            }
            if((doc == null) || doc.IsEmpty || (doc.AsXmlNode.FirstChild == null)) {
                return Remove();
            }
            XmlNode importNode = doc.RootElement.FirstChild;
            XmlNode insertNode = CurrentNode;
            XmlNode parentNode = insertNode.ParentNode;
            while(importNode != null) {
                insertNode = parentNode.InsertAfter(_doc.ImportNode(importNode, true), insertNode);
                importNode = importNode.NextSibling;
            }
            parentNode.RemoveChild(CurrentNode);
            return this[insertNode];
        }

        /// <summary>
        /// Replaces this XDoc instance with a text node.
        /// </summary>
        /// <param name="value">Text value.</param>
        /// <returns></returns>
        public XDoc Replace(string value) {
            if(IsEmpty) {
                throw new InvalidOperationException("xdoc is empty");
            }
            if(value == null) {
                return Remove();
            }

            // replace node
            return ReplaceCurrentNode(_doc.CreateTextNode(value));
        }

        /// <summary>
        /// Replaces this XDoc instance with a text node.
        /// </summary>
        /// <param name="value">Text value.</param>
        /// <returns></returns>
        public XDoc Replace(DateTime value) {
            return Replace(value.ToUniversalTime().ToString(RFC_DATETIME_FORMAT));
        }

        /// <summary>
        /// Replaces this XDoc instance with a text node.
        /// </summary>
        /// <param name="value">Text value.</param>
        /// <returns></returns>
        public XDoc Replace(byte[] value) {
            if(value == null) {
                return Remove();
            }
            return Replace(Convert.ToBase64String(value));
        }

        /// <summary>
        /// Replaces this XDoc instance with a text node.
        /// </summary>
        /// <param name="value">Text value.</param>
        /// <returns></returns>
        public XDoc Replace(object value) {
            if(value is XDoc) {
                return Replace((XDoc)value);
            }
            if(value is DateTime) {
                return Replace((DateTime)value);
            }
            if(value is byte[]) {
                return Replace((byte[])value);
            }
            if(value != null) {
                return Replace(value.ToString());
            }
            return Remove();
        }

        /// <summary>
        /// Change the name of the current element node.
        /// </summary>
        /// <param name="name">New element name.</param>
        /// <returns></returns>
        public XDoc Rename(string name) {
            if(IsEmpty) {
                throw new InvalidOperationException("xdoc is empty");
            }
            if(name == null) {
                throw new ArgumentNullException("name");
            }
            if(name == CurrentNode.Name) {

                // nothing to do
                return this;
            }

            // TODO (steveb): allow renaming of attribute nodes (see http://youtrack.developer.mindtouch.com/issue/MT-9661)

            XmlNode current = CurrentNode;
            XmlElement node = current.OwnerDocument.CreateElement(name);

            // move over the attributes
            List<XmlAttribute> list = new List<XmlAttribute>(current.Attributes.Count);
            foreach(XmlAttribute attribute in current.Attributes) {
                list.Add(attribute);
            }
            foreach(XmlAttribute attribute in list) {
                node.SetAttribute(attribute.LocalName, attribute.NamespaceURI, attribute.Value);
            }

            // move over the child nodes
            foreach(XmlNode child in NewListXmlNode(current.ChildNodes)) {
                node.AppendChild(child);
            }

            // replace current node with new node
            return ReplaceCurrentNode(node);
        }

        /// <summary>
        /// Prepend child nodes from another XDoc instance.
        /// </summary>
        /// <param name="doc">Container of prepended nodes.</param>
        /// <returns></returns>
        public XDoc AddNodesInFront(XDoc doc) {
            if(IsEmpty) {
                throw new InvalidOperationException("xdoc is empty");
            }
            if(doc == null) {
                return this;
            }
            if(!(_root is XmlElement)) {
                throw new InvalidOperationException("xdoc is not an element");
            }
            if(!doc.IsEmpty) {
                XmlNode insertNode = null;
                foreach(XmlNode node in doc.RootElement.ChildNodes) {
                    insertNode = CurrentNode.InsertAfter(_doc.ImportNode(node, true), insertNode);
                }
            }
            return this;
        }

        /// <summary>
        /// Add child nodes from another XDoc instance before this one.
        /// </summary>
        /// <param name="doc">Container of prepended nodes.</param>
        /// <returns></returns>
        public XDoc AddNodesBefore(XDoc doc) {
            if(IsEmpty) {
                throw new InvalidOperationException("xdoc is empty");
            }
            if(doc == null) {
                return this;
            }
            if(!(_root is XmlElement)) {
                throw new InvalidOperationException("xdoc is not an element");
            }
            if(!doc.IsEmpty) {
                XmlNode parent = CurrentNode.ParentNode;
                if(parent == null) {
                    throw new InvalidOperationException("xdoc is top node");
                }
                XmlNode marker = parent.InsertBefore(_doc.CreateTextNode("#"), CurrentNode);
                XmlNode insertNode = marker;
                foreach(XmlNode node in doc.RootElement.ChildNodes) {
                    insertNode = parent.InsertAfter(_doc.ImportNode(node, true), insertNode);
                }
                parent.RemoveChild(marker);
            }
            return this;
        }

        /// <summary>
        /// Add child nodes from another XDoc instance after this one.
        /// </summary>
        /// <param name="doc">Container of prepended nodes.</param>
        /// <returns></returns>
        public XDoc AddNodesAfter(XDoc doc) {
            if(IsEmpty) {
                throw new InvalidOperationException("xdoc is empty");
            }
            if(doc == null) {
                return this;
            }
            if(!(_root is XmlElement)) {
                throw new InvalidOperationException("xdoc is not an element");
            }
            if(!doc.IsEmpty) {
                XmlNode parent = CurrentNode.ParentNode;
                if(parent == null) {
                    throw new InvalidOperationException("xdoc is top node");
                }
                XmlNode insertNode = CurrentNode;
                foreach(XmlNode node in doc.RootElement.ChildNodes) {
                    insertNode = parent.InsertAfter(_doc.ImportNode(node, true), insertNode);
                }
            }
            return this;
        }

        /// <summary>
        /// Adds child nodes from another XDoc instance.
        /// </summary>
        /// <param name="doc">Container of added nodes.</param>
        /// <returns></returns>
        public XDoc AddNodes(XDoc doc) {
            if(IsEmpty) {
                throw new InvalidOperationException("xdoc is empty");
            }
            if(doc == null) {
                return this;
            }
            if(!doc.IsEmpty) {
                foreach(XmlNode node in doc.RootElement.ChildNodes) {
                    CurrentNode.AppendChild(_doc.ImportNode(node, true));
                }
            }
            return this;
        }

        /// <summary>
        /// Adds an XDoc instance.
        /// </summary>
        /// <param name="doc">XDoc instance to add.</param>
        /// <returns></returns>
        public XDoc Add(XDoc doc) {
            if(IsEmpty) {
                throw new InvalidOperationException("xdoc is empty");
            }
            if(doc == null) {
                return this;
            }
            if(!doc.IsEmpty) {
                CurrentNode.AppendChild(_doc.ImportNode(doc.RootNode, true));
            }
            return this;
        }

        /// <summary>
        /// Adds a value after this XDoc instance.
        /// </summary>
        /// <param name="value">Value to add.</param>
        /// <returns></returns>
        public XDoc AddAfter(string value) {
            if(IsEmpty) {
                throw new InvalidOperationException("xdoc is empty");
            }
            if(string.IsNullOrEmpty(value)) {
                return this;
            }
            XmlNode parent = CurrentNode.ParentNode;
            if(parent == null) {
                throw new InvalidOperationException("xdoc is top node");
            }
            parent.InsertAfter(_doc.CreateTextNode(value), CurrentNode);
            return this;
        }

        /// <summary>
        /// Adds a value after this XDoc instance.
        /// </summary>
        /// <param name="value">Value to add.</param>
        /// <returns></returns>
        public XDoc AddAfter(DateTime value) {
            return AddAfter(value.ToUniversalTime().ToString(RFC_DATETIME_FORMAT));
        }

        /// <summary>
        /// Adds a value after this XDoc instance.
        /// </summary>
        /// <param name="value">Value to add.</param>
        /// <returns></returns>
        public XDoc AddAfter(bool value) {
            return AddAfter(value ? "true" : "false");
        }

        /// <summary>
        /// Adds a value after this XDoc instance.
        /// </summary>
        /// <param name="value">Value to add.</param>
        /// <returns></returns>
        public XDoc AddAfter(byte[] value) {
            if(value == null) {
                return this;
            }
            return AddAfter(Convert.ToBase64String(value));
        }

        /// <summary>
        /// Adds a value after this XDoc instance.
        /// </summary>
        /// <param name="value">Value to add.</param>
        /// <returns></returns>
        public XDoc AddAfter(XUri value) {
            if(value == null) {
                return this;
            }
            DreamContext context = DreamContext.CurrentOrNull;
            if(context != null) {
                value = context.AsPublicUri(value);
            }
            return AddAfter(value.ToString());
        }

        /// <summary>
        /// Adds a value after this XDoc instance.
        /// </summary>
        /// <param name="value">Value to add.</param>
        /// <returns></returns>
        public XDoc AddAfter(object value) {
            if(value is DateTime) {
                return AddAfter((DateTime)value);
            }
            if(value is byte[]) {
                return AddAfter((byte[])value);
            }
            if(value is XUri) {
                return AddAfter((XUri)value);
            }
            if(value is XDoc) {
                return AddAfter((XDoc)value);
            }
            if(value is bool) {
                return AddAfter((bool)value);
            }
            if(value != null) {
                return AddAfter(value.ToString());
            }
            return this;
        }

        /// <summary>
        /// Adds an XDoc instance after this one.
        /// </summary>
        /// <param name="doc">XDoc instance to add.</param>
        /// <returns></returns>
        public XDoc AddAfter(XDoc doc) {
            if(IsEmpty) {
                throw new InvalidOperationException("xdoc is empty");
            }
            XmlNode parent = CurrentNode.ParentNode;
            if(parent == null) {
                throw new InvalidOperationException("xdoc is top node");
            }
            if((doc != null) && !doc.IsEmpty) {
                parent.InsertAfter(_doc.ImportNode(doc.RootNode, true), CurrentNode);
            }
            return this;
        }

        /// <summary>
        /// Adds a value before this XDoc instance.
        /// </summary>
        /// <param name="value">Value to add.</param>
        /// <returns></returns>
        public XDoc AddBefore(string value) {
            if(IsEmpty) {
                throw new InvalidOperationException("xdoc is empty");
            }
            if(string.IsNullOrEmpty(value)) {
                return this;
            }
            XmlNode parent = CurrentNode.ParentNode;
            if(parent == null) {
                throw new InvalidOperationException("xdoc is top node");
            }
            parent.InsertBefore(_doc.CreateTextNode(value), CurrentNode);
            return this;
        }

        /// <summary>
        /// Adds a value before this XDoc instance.
        /// </summary>
        /// <param name="value">Value to add.</param>
        /// <returns></returns>
        public XDoc AddBefore(DateTime value) {
            return AddBefore(value.ToUniversalTime().ToString(RFC_DATETIME_FORMAT));
        }

        /// <summary>
        /// Adds a value before this XDoc instance.
        /// </summary>
        /// <param name="value">Value to add.</param>
        /// <returns></returns>
        public XDoc AddBefore(bool value) {
            return AddBefore(value ? "true" : "false");
        }

        /// <summary>
        /// Adds a value before this XDoc instance.
        /// </summary>
        /// <param name="value">Value to add.</param>
        /// <returns></returns>
        public XDoc AddBefore(byte[] value) {
            if(value == null) {
                return this;
            }
            return AddBefore(Convert.ToBase64String(value));
        }

        /// <summary>
        /// Adds a value before this XDoc instance.
        /// </summary>
        /// <param name="value">Value to add.</param>
        /// <returns></returns>
        public XDoc AddBefore(XUri value) {
            if(value == null) {
                return this;
            }
            DreamContext context = DreamContext.CurrentOrNull;
            if(context != null) {
                value = context.AsPublicUri(value);
            }
            return AddBefore(value.ToString());
        }

        /// <summary>
        /// Adds a value before this XDoc instance.
        /// </summary>
        /// <param name="value">Value to add.</param>
        /// <returns></returns>
        public XDoc AddBefore(object value) {
            if(value is DateTime) {
                return AddBefore((DateTime)value);
            }
            if(value is byte[]) {
                return AddBefore((byte[])value);
            }
            if(value is XUri) {
                return AddBefore((XUri)value);
            }
            if(value is XDoc) {
                return AddBefore((XDoc)value);
            }
            if(value is bool) {
                return AddBefore((bool)value);
            }
            if(value != null) {
                return AddBefore(value.ToString());
            }
            return this;
        }

        /// <summary>
        /// Adds an XDoc instance before this one.
        /// </summary>
        /// <param name="doc">XDoc instance to add.</param>
        /// <returns></returns>
        public XDoc AddBefore(XDoc doc) {
            if(IsEmpty) {
                throw new InvalidOperationException("xdoc is empty");
            }
            XmlNode parent = CurrentNode.ParentNode;
            if(parent == null) {
                throw new InvalidOperationException("xdoc is top node");
            }
            if((doc != null) && !doc.IsEmpty) {
                parent.InsertBefore(_doc.ImportNode(doc.RootNode, true), CurrentNode);
            }
            return this;
        }

        /// <summary>
        /// Adds all XDoc instances in selection to this one.
        /// </summary>
        /// <param name="doc">XDoc selection.</param>
        /// <returns></returns>
        public XDoc AddAll(XDoc doc) {
            if(IsEmpty) {
                throw new InvalidOperationException("xdoc is empty");
            }
            if(doc != null) {
                foreach(XDoc item in doc) {
                    Add(item);
                }
            }
            return this;
        }

        /// <summary>
        /// Adds all XDoc instances in selection before this one.
        /// </summary>
        /// <param name="doc">XDoc selection.</param>
        /// <returns></returns>
        public XDoc AddAllBefore(XDoc doc) {
            if(IsEmpty) {
                throw new InvalidOperationException("xdoc is empty");
            }
            if(doc != null) {
                foreach(XDoc item in doc) {
                    AddBefore(item);
                }
            }
            return this;
        }

        /// <summary>
        /// Adds all XDoc instances in selection after this one.
        /// </summary>
        /// <param name="doc">XDoc selection.</param>
        /// <returns></returns>
        public XDoc AddAllAfter(XDoc doc) {
            if(IsEmpty) {
                throw new InvalidOperationException("xdoc is empty");
            }
            if(doc == null) {
                return this;
            }

            // reverse the list of nodes
            List<XDoc> list = new List<XDoc>();
            foreach(XDoc item in doc) {
                list.Add(item);
            }
            list.Reverse();

            // add reverted list
            foreach(XDoc item in list) {
                AddAfter(item);
            }
            return this;
        }

        /// <summary>
        /// Adds all XDoc instances in collection to this one.
        /// </summary>
        /// <param name="collection">XDoc instance collection.</param>
        /// <returns></returns>
        public XDoc AddAll(IEnumerable<XDoc> collection) {
            if(IsEmpty) {
                throw new InvalidOperationException("xdoc is empty");
            }
            if(collection != null) {
                foreach(XDoc item in collection) {
                    Add(item);
                }
            }
            return this;
        }

        /// <summary>
        /// Returns the contents text for each XDoc instance in the selection.
        /// </summary>
        /// <returns></returns>
        public ICollection<string> GetContentsAsStrings() {
            IList<string> result = new List<string>();
            foreach(XDoc doc in First) {
                result.Add(doc.Contents);
            }
            return result;
        }

        /// <summary>
        /// Invokes the given method for each XDoc instance in the selection.
        /// </summary>
        /// <param name="call">Action delegate.</param>
        public void ForEach(Action<XDoc> call) {
            if(call == null) {
                throw new ArgumentNullException("call");
            }
            foreach(XDoc item in this) {
                call(item);
            }
        }

        /// <summary>
        /// Sorts the child elements using the given comparer.
        /// </summary>
        /// <param name="comparer">Comparison delegate.</param>
        /// <returns></returns>
        public XDoc Sort(Comparison<XDoc> comparer) {
            if(comparer == null) {
                throw new ArgumentNullException("comparer");
            }
            List<XDoc> items = new List<XDoc>(Elements);
            items.Sort(comparer);
            RemoveAll(items);
            AddAll(items);
            return this;
        }

        /// <summary>
        /// Filters the child nodes using the given filter.
        /// </summary>
        /// <param name="filter">Predicate delegate.</param>
        /// <returns></returns>
        public XDoc Filter(Predicate<XDoc> filter) {
            if(filter == null) {
                throw new ArgumentNullException("filter");
            }
            XDoc list = Elements;
            List<XDoc> items = new List<XDoc>();
            foreach(XDoc item in list) {
                if(filter(item)) {
                    items.Add(item);
                }
            }
            RemoveAll(new List<XDoc>(list));
            AddAll(items);
            return this;
        }

        /// <summary>
        /// Adds prefix-namespace combination to the XML namespace manager.
        /// </summary>
        /// <param name="prefix">Prefix name.</param>
        /// <param name="namespace">Namespace URI.</param>
        /// <returns></returns>
        public XDoc UsePrefix(string prefix, string @namespace) {
            if(IsEmpty) {
                throw new InvalidOperationException("xdoc is empty");
            }
            _nsManager.AddNamespace(prefix, @namespace);
            return this;
        }

        /// <summary>
        /// Adds an XML stylesheet processing node.
        /// </summary>
        /// <param name="href">XSL tranform URI.</param>
        /// <returns></returns>
        public XDoc WithXslTransform(string href) {
            if(IsEmpty) {
                throw new InvalidOperationException("xdoc is empty");
            }
            _doc.InsertBefore(_doc.CreateProcessingInstruction("xml-stylesheet", string.Format("type='text/xsl' href='{0}'", href)), _doc.DocumentElement);
            return this;
        }

        /// <summary>
        /// Converts this XDoc instance into a string using the given XSLT.
        /// </summary>
        /// <param name="xslt">XSLT instance.</param>
        /// <returns></returns>
        public string TransformAsText(XslCompiledTransform xslt) {
            if(IsEmpty) {
                return string.Empty;
            }
            StringWriter result = new StringWriter();
            xslt.Transform(_doc, null, result);
            return result.ToString();
        }

        /// <summary>
        /// Converts this XDoc instance into a new XDoc instances using the givne XSLT.
        /// </summary>
        /// <param name="xslt">XSLT instance.</param>
        /// <returns></returns>
        public XDoc TransformAsXml(XslCompiledTransform xslt) {
            if(IsEmpty) {
                return XDoc.Empty;
            }
            XmlDocument result = NewXmlDocument();
            using(XmlWriter writer = result.CreateNavigator().AppendChild()) {
                xslt.Transform(_doc, writer);
            }
            return new XDoc(result);
        }

        /// <summary>
        /// Stores this XDoc instance into a file.
        /// </summary>
        /// <param name="filename">File path.</param>
        /// <param name="pretty">Use pretty formatting when saving.</param>
        public void Save(string filename, bool pretty) {
            using(TextWriter writer = new StreamWriter(filename)) {
                writer.Write(pretty ? ToPrettyString() : ToString());
            }
        }

        /// <summary>
        /// Stores this XDoc instance into a file.
        /// </summary>
        /// <param name="filename">File path.</param>
        public void Save(string filename) {
            Save(filename, false);
        }

        /// <summary>
        /// Creates a copy as an XmlDocument instance.
        /// </summary>
        /// <returns></returns>
        public XmlDocument ToXml() {
            if(!IsEmpty) {
                return (XmlDocument)_doc.Clone();
            }
            return NewXmlDocument();
        }

        /// <summary>
        /// Renders the XDoc instance using XHTML compatible format.
        /// </summary>
        /// <returns></returns>
        public string ToXHtml() {
            return ToXHtml(true);
        }

        /// <summary>
        /// Renders the XDoc instance using XHTML compatible format.
        /// </summary>
        /// <returns></returns>
        public string ToXHtml(bool useEntityNames) {
            if(IsEmpty) {
                return string.Empty;
            }
            StringWriter result = new StringWriter();
            XHtmlTextWriter writer = new XHtmlTextWriter(result, useEntityNames);
            XmlNode current = CurrentNode;

            // check if we need to import the current node into a separate document for rendering
            XmlDocument document = current.ParentNode as XmlDocument;
            if(document == null) {
                document = NewXmlDocument();
                document.AppendChild(document.ImportNode(current, true));
            }

            // render document
            document.Save(writer);
            writer.Flush();
            return result.ToString();
        }

        /// <summary>
        /// Renders the XDoc instance using XHTML entities, but omitting the outermost element.
        /// </summary>
        /// <returns></returns>
        public string ToInnerXHtml() {
            string result = ToXHtml();

            // remove opening and closing tags
            int first = result.IndexOf(">");
            int last = result.LastIndexOf("<");
            if((first >= 0) && (last >= 0)) {
                return result.Substring(first + 1, last - (first + 1));
            }
            return result;
        }

        /// <summary>
        /// Converts the XDoc instance into an object instance using the given XmlSerializer instance.
        /// </summary>
        /// <param name="serializer">XmlSerialize instance.</param>
        /// <returns></returns>
        public object ToObject(XmlSerializer serializer) {
            if(IsEmpty) {
                return null;
            }
            XmlNodeReader reader = new XmlNodeReader(_root);
            object result = serializer.Deserialize(reader);
            return result;
        }

        /// <summary>
        /// Renders the XDoc instance as an XML document using UTF-8 encoding.
        /// </summary>
        /// <returns></returns>
        public override string ToString() {
            return ToString(Encoding.UTF8, false);
        }

        /// <summary>
        /// Renders the XDoc instance as an XML document using the given encoding.
        /// </summary>
        /// <param name="encoding">Text encoding.</param>
        /// <returns></returns>
        public string ToString(Encoding encoding) {
            return ToString(encoding, false);
        }

        /// <summary>
        /// Renders the XDoc instance as an XML document using the given encoding.
        /// </summary>
        /// <param name="encoding">Text encoding.</param>
        /// <param name="declaration">Include XML declaration.</param>
        /// <returns></returns>
        public string ToString(Encoding encoding, bool declaration) {
            if(IsEmpty) {
                return string.Empty;
            }
            StringBuilder result = new StringBuilder();
            if(ReferenceEquals(_root.ParentNode, _doc)) {
                XmlDeclaration decl = _doc.FirstChild as XmlDeclaration;
                if(decl != null) {
                    decl.Encoding = encoding.BodyName;
                    if(declaration) {
                        result.Append(decl.OuterXml);
                    }
                }
                foreach(XmlNode node in _doc.ChildNodes) {
                    if(node is XmlProcessingInstruction) {
                        result.Append(node.OuterXml);
                    }
                }
            }
            result.Append(_root.OuterXml);
            return result.ToString();
        }

        /// <summary>
        /// Renders the XDoc instance as an XML document using UTF-8 encoding.
        /// </summary>
        /// <param name="stream">Stream to render the instance on.</param>
        public void WriteTo(Stream stream) {
            WriteTo(stream, Encoding.UTF8, false);
        }

        /// <summary>
        /// Renders the XDoc instance as an XML document using the given encoding.
        /// </summary>
        /// <param name="stream">Stream to render the instance on.</param>
        /// <param name="encoding">Text encoding.</param>
        public void WriteTo(Stream stream, Encoding encoding) {
            WriteTo(stream, encoding, false);
        }

        /// <summary>
        /// Renders the XDoc instance as an XML document using the given encoding.
        /// </summary>
        /// <param name="stream">Stream to render the instance on.</param>
        /// <param name="encoding">Text encoding.</param>
        /// <param name="declaration">Include XML declaration.</param>
        public void WriteTo(Stream stream, Encoding encoding, bool declaration) {
            if(!IsEmpty) {

                // we don't want to write the BOM bytes
                byte[] bom = encoding.GetPreamble();
                if(bom.Length > 0) {
                    stream = new SkipWriteStream(stream, bom.Length);
                }

                // serialize XML document to writer
                XmlWriter writer = new XmlTextWriter(stream, encoding);
                if(ReferenceEquals(_root.ParentNode, _doc)) {
                    var decl = _doc.FirstChild as XmlDeclaration;
                    if(decl != null) {
                        decl.Encoding = encoding.BodyName;
                        if(declaration) {
                            decl.WriteTo(writer);
                        }
                    }
                    foreach(XmlNode node in _doc.ChildNodes) {
                        if(node is XmlProcessingInstruction) {
                            node.WriteTo(writer);
                        }
                    }
                }
                _root.WriteTo(writer);
                writer.Flush();
            }
        }

        /// <summary>
        /// Renders the XDoc instance as an indented XML document using UTF-8 encoding.
        /// </summary>
        /// <returns></returns>
        public string ToPrettyString() {
            if(IsEmpty) {
                return string.Empty;
            }
            bool top = ReferenceEquals(_root.ParentNode, _doc);
            if(top) {
                XmlDeclaration decl = _doc.FirstChild as XmlDeclaration;
                if(decl != null) {
                    decl.Encoding = Encoding.UTF8.BodyName;
                }
            }
            StringWriter result = new StringWriter();
            using(XmlTextWriter xmlWriter = new XmlTextWriter(result)) {
                xmlWriter.Formatting = Formatting.Indented;
                xmlWriter.WriteNode(new XmlNodeReader(top ? _root.OwnerDocument : _root), true);
            }
            return result.ToString();
        }

        /// <summary>
        /// Renders the XDoc instance omitting whitespace where possible using UTF-8 encoding.
        /// </summary>
        /// <returns></returns>
        public string ToCompactString() {
            if(IsEmpty) {
                return string.Empty;
            }
            bool top = ReferenceEquals(_root.ParentNode, _doc);
            if(top) {
                XmlDeclaration decl = _doc.FirstChild as XmlDeclaration;
                if(decl != null) {
                    decl.Encoding = Encoding.UTF8.BodyName;
                }
            }
            var compact = new XmlDocument();
            compact.PreserveWhitespace = false;
            compact.Load(new XmlNodeReader(top ? _root.OwnerDocument : _root));
            return compact.OuterXml;
        }

        /// <summary>
        /// Converts the XDoc selection into a list of XDoc instances.
        /// </summary>
        /// <returns></returns>
        public List<XDoc> ToList() {
            if(IsEmpty || (_list == null)) {
                return new List<XDoc>(0);
            }
            List<XDoc> result = new List<XDoc>(_list.Length);
            for(int i = 0; i < _list.Length; ++i) {
                result.Add(new XDoc(_list, i, null, _nsManager));
            }
            return result;
        }

        /// <summary>
        /// Converts the XDoc instance into a byte array using UTF-8 encoding.
        /// </summary>
        /// <returns></returns>
        public byte[] ToBytes() {
            return ToBytes(Encoding.UTF8);
        }

        /// <summary>
        /// Converts the XDoc instance into a byte array using the given encoding.
        /// </summary>
        /// <param name="encoding"></param>
        /// <returns></returns>
        public byte[] ToBytes(Encoding encoding) {
            if(encoding == null) {
                throw new ArgumentNullException("encoding");
            }
            return encoding.GetBytes(ToString(encoding));
        }

        /// <summary>
        /// Converts the XDoc instance into an array of XPath expressions and text values.
        /// </summary>
        /// <returns></returns>
        public KeyValuePair<string, string>[] ToKeyValuePairs() {
            return XPostUtil.ToXPathValuePairs(this);
        }

        /// <summary>
        /// Compares this XDoc instance to another one.
        /// </summary>
        /// <param name="other"></param>
        /// <returns></returns>
        public override bool Equals(object other) {
            if(other is XDoc) {
                return CompareNode(this._root, ((XDoc)other)._root);
            }
            return false;
        }

        /// <summary>
        /// Computes the hashcode.
        /// </summary>
        /// <returns></returns>
        public override int GetHashCode() {

            //Note (arnec): We consider them all potentially equal, if their root matches. This is a very cheap approximation,
            //              but it is still more correct than the opposite. We should probably amend this with a bit deeper
            //              consideration of child nodes
            return IsEmpty ? 1 : Name.GetHashCode();
        }

        /// <summary>
        /// Append XML digital signature to XDoc instance.
        /// </summary>
        /// <param name="rsa">RSA key to use for digital signature.</param>
        /// <returns>XDoc instance with appended XML digital signature.</returns>
        public XDoc Sign(System.Security.Cryptography.RSACryptoServiceProvider rsa) {
            if(rsa == null) {
                throw new ArgumentNullException("rsa");
            }
            if(IsEmpty) {
                throw new InvalidOperationException("xdoc is empty");
            }

            // check if current node is an element
            XmlElement node = CurrentNode as XmlElement;
            if(node != null) {

                // remove previous signature, if any
                Unsign();

                // compute signature
                string signature = ToBytes().SignData(rsa);

                // add signature with namespace
                node.SetAttribute("xmlns:dsig", NS_DSIG);
                node.SetAttribute("dsig", NS_DSIG, signature);
            }
            return this;
        }

        /// <summary>
        /// Check if XDoc instance has an XML digital signature.
        /// </summary>
        /// <param name="rsa">RSA key to use for digital signature.</param>
        /// <returns>True if digital signature is present and valid.</returns>
        public bool HasValidSignature(System.Security.Cryptography.RSACryptoServiceProvider rsa) {
            if(rsa == null) {
                throw new ArgumentNullException("rsa");
            }
            if(IsEmpty) {
                return false;
            }

            // check if current node is an element
            XmlElement node = CurrentNode as XmlElement;
            if(node != null) {

                // retrieve element signature
                string attr = node.GetAttribute("dsig", NS_DSIG);
                if(!string.IsNullOrEmpty(attr)) {

                    // copy element
                    XDoc doc = Clone();

                    // remove existing signature
                    doc.Unsign();

                    // validate signature against document
                    return doc.ToBytes().VerifySignature(attr, rsa);
                }
            }
            return false;
        }

        /// <summary>
        /// Remove XML digital signature from XDoc instance.
        /// </summary>
        /// <returns>XDoc instance without a digital signature.</returns>
        public XDoc Unsign() {
            if(!IsEmpty) {

                // check if current node is an element
                XmlElement node = AsXmlNode as XmlElement;
                if(node != null) {

                    // remove existing signature
                    node.RemoveAttribute("dsig", NS_DSIG);

                    // remove signature namespace, if any
                    string ns = node.GetPrefixOfNamespace(NS_DSIG);
                    if(!string.IsNullOrEmpty(ns)) {
                        node.RemoveAttribute(ns, NS_XMLNS);
                    }
                }
            }
            return this;
        }

        /// <summary>
        /// Return a new selection in reverse order.
        /// </summary>
        /// <returns>XDoc instance with elements listed in reverse order.</returns>
        public XDoc ReverseList() {
            if(!IsEmpty) {
                int count = _list.Length;
                XmlNode[] newList = new XmlNode[count];
                for(int i = count - 1; i >= 0; --i) {
                    newList[i] = _list[count - i - 1];
                }
                return new XDoc(newList, 0, null, _nsManager);
            }
            return this;
        }

        /// <summary>
        /// Return an enumerable that visits all nodes recursively.
        /// </summary>
        /// <returns>A recursive enumerable for the XDoc instance.</returns>
        public IEnumerable<XDoc> VisitAll() {
            return VisitAll(null);
        }

        /// <summary>
        /// Return an enumerable that visits all nodes recursively.
        /// </summary>
        /// <param name="nodeExitCallback">Callback called with the visited XDoc after it and its children have been visited.</param>
        /// <returns>A recursive enumerable for the XDoc instance.</returns>
        public IEnumerable<XDoc> VisitAll(Action<XDoc> nodeExitCallback) {
            return new XDocRecursiveEnumerable(CurrentNode, _nsManager, null, nodeExitCallback);
        }

        /// <summary>
        /// Return an enumerable that visits nodes recursively under condition.
        /// </summary>
        /// <param name="enumerateChildren">Test function to determine if the children of the current XDoc instance should be enumerated.</param>
        /// <returns>A recursive enumerable for the XDoc instance.</returns>
        public IEnumerable<XDoc> VisitOnly(Predicate<XDoc> enumerateChildren) {
            return VisitOnly(enumerateChildren, null);
        }

        /// <summary>
        /// Return an enumerable that visits nodes recursively under condition.
        /// </summary>
        /// <param name="enumerateChildren">Test function to determine if the children of the current XDoc instance should be enumerated.</param>
        /// <param name="nodeExitCallback">Callback called with the visited XDoc after it and its children have been visited.</param>
        /// <returns>A recursive enumerable for the XDoc instance.</returns>
        public IEnumerable<XDoc> VisitOnly(Predicate<XDoc> enumerateChildren, Action<XDoc> nodeExitCallback) {
            return new XDocRecursiveEnumerable(CurrentNode, _nsManager, enumerateChildren, nodeExitCallback);
        }

        /// <summary>
        /// Protected initializer for XDoc inheritors for setting the state of an existing instance.
        /// </summary>
        /// <param name="list">List of nodes for the current cursor into the <see cref="XmlDocument"/>.</param>
        /// <param name="index">Index into the list of nodes providing the cursor pointer.</param>
        /// <param name="root">Root of the XDoc instance.</param>
        /// <param name="nsManager">Namespace manager to use with the instance.</param>
        protected void Initialize(XmlNode[] list, int index, XmlNode root, XmlNamespaceManager nsManager) {
            if(list == null) {
                throw new ArgumentNullException("list");
            }
            if(list.Length == 0) {
                throw new ArgumentException("list of nodes is empty", "list");
            }
            if((index < 0) || (index >= list.Length)) {
                throw new ArgumentOutOfRangeException("index");
            }
            _list = list;
            _index = index;
            _root = root ?? _list[_index];
            _doc = _root.OwnerDocument;
            foreach(XmlNode node in list) {
                node.OwnerDocument.XmlResolver = null;
            }

            // set default xml namespace manager
            if(nsManager == null) {
                nsManager = new XmlNamespaceManager(_doc.NameTable);
                string uri = _doc.DocumentElement.NamespaceURI;
                if(uri != null) {
                    nsManager.AddNamespace(string.Empty, uri);
                    nsManager.AddNamespace("_", uri);
                } else {
                    nsManager.AddNamespace("_", string.Empty);
                }
            }
            _nsManager = nsManager;
        }

        private XDoc ReplaceCurrentNode(XmlNode newNode) {
            XmlNode current = CurrentNode;
            XmlNode parent = current.ParentNode;
            parent.ReplaceChild(newNode, current);
            _list[_index] = newNode;
            if(ReferenceEquals(current, _root)) {
                _root = newNode;
                _doc = _root.OwnerDocument;
            }
            return this;
        }

        private string GetNamespaceUri(string tag) {
            if(tag == null) {
                throw new ArgumentNullException("tag");
            }
            string namespaceUri = null;
            if(tag.EqualsInvariant("xmlns")) {

                // NOTE (steveb): see http://msdn2.microsoft.com/en-us/library/system.xml.xmlattribute.prefix(VS.71).aspx

                namespaceUri = NS_XMLNS;
            } else {
                int index = tag.IndexOf(':');
                if(index >= 0) {
                    namespaceUri = _nsManager.LookupNamespace(tag.Substring(0, index));
                }
            }
            return namespaceUri;
        }

        private string GetNodeText(XmlNode node) {
            switch(node.NodeType) {
            case XmlNodeType.Text:
            case XmlNodeType.Whitespace:
            case XmlNodeType.SignificantWhitespace:
            case XmlNodeType.CDATA:
            case XmlNodeType.Attribute:
                return node.Value;
            }
            return null;
        }

        //--- > IEnumerable<XDoc> Members ---
        IEnumerator<XDoc> IEnumerable<XDoc>.GetEnumerator() {
            return ToList().GetEnumerator();
        }

        //--- IEnumerable Members ---
        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() {
            return ((IEnumerable<XDoc>)this).GetEnumerator();
        }

        //--- ICloneable Members ---
        object ICloneable.Clone() {
            return this.Clone();
        }

    }
}