// DEBUG

// We make sure that diagnostics code will not be compiled nor called into Release configuration.
// In Debug configuration, diagnostics code can be enabled by defining DEBUGNAVIGATOR below,
// but by default nothing is written, unless some lines are un-commented in Debug(...) below.
//
// Beware! Diagnostics are extremely verbose and can overflow logging pretty easily.

#if DEBUG
// define to enable diagnostics code
#undef DEBUGNAVIGATOR
#endif

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Xml;
using System.Xml.XPath;

namespace Umbraco.Cms.Core.Xml.XPath
{
    /// <summary>
    /// Provides a cursor model for navigating Umbraco data as if it were XML.
    /// </summary>
    public class NavigableNavigator : XPathNavigator
    {
        // "The XmlNameTable stores atomized strings of any local name, namespace URI,
        // and prefix used by the XPathNavigator. This means that when the same Name is
        // returned multiple times (like "book"), the same String object is returned for
        // that Name. This makes it possible to write efficient code that does object
        // comparisons on these strings, instead of expensive string comparisons."
        //
        // "When an element or attribute name occurs multiple times in an XML document,
        // it is stored only once in the NameTable. The names are stored as common
        // language runtime (CLR) object types. This enables you to do object comparisons
        // on these strings rather than a more expensive string comparison. These
        // string objects are referred to as atomized strings."
        //
        // But... "Any instance members are not guaranteed to be thread safe."
        //
        // see http://msdn.microsoft.com/en-us/library/aa735772%28v=vs.71%29.aspx
        // see http://www.hanselman.com/blog/XmlAndTheNametable.aspx
        // see http://blogs.msdn.com/b/mfussell/archive/2004/04/30/123673.aspx
        //
        // "Additionally, all LocalName, NameSpaceUri and Prefix strings must be added to
        // a NameTable, given by the NameTable property. When the LocalName, NamespaceURI,
        // and Prefix properties are returned, the string returned should come from the
        // NameTable. Comparisons between names are done by object comparisons rather
        // than by string comparisons, which are significantly slower.""
        //
        // So what shall we do? Well, here we have no namespace, no prefix, and all
        // local names come from cached instances of INavigableContentType or
        // INavigableFieldType and are already unique. So... create a one nametable
        // because we need one, and share it amongst all clones.

        private readonly XmlNameTable _nameTable;
        private readonly INavigableSource _source;
        private readonly int _lastAttributeIndex; // last index of attributes in the fields collection
        private State _state;
        private readonly int _maxDepth;

        #region Constructor

        ///// <summary>
        ///// Initializes a new instance of the <see cref="NavigableNavigator"/> class with a content source.
        ///// </summary>
        ///// <param name="source">The content source.</param>
        ///// <param name="maxDepth">The maximum depth.</param>
        //private NavigableNavigator(INavigableSource source, int maxDepth)
        //{
        //    _source = source;
        //    _lastAttributeIndex = source.LastAttributeIndex;
        //    _maxDepth = maxDepth;
        //}

        /// <summary>
        /// Initializes a new instance of the <see cref="NavigableNavigator"/> class with a content source,
        /// and an optional root content.
        /// </summary>
        /// <param name="source">The content source.</param>
        /// <param name="rootId">The root content identifier.</param>
        /// <param name="maxDepth">The maximum depth.</param>
        /// <remarks>When no root content is supplied then the root of the source is used.</remarks>
        public NavigableNavigator(INavigableSource source, int rootId = 0, int maxDepth = int.MaxValue)
            //: this(source, maxDepth)
        {
            _source = source;
            _lastAttributeIndex = source.LastAttributeIndex;
            _maxDepth = maxDepth;

            _nameTable = new NameTable();
            _lastAttributeIndex = source.LastAttributeIndex;
            var content = rootId <= 0 ? source.Root : source.Get(rootId);
            if (content == null)
                throw new ArgumentException("Not the identifier of a content within the source.", nameof(rootId));
            _state = new State(content, null, null, 0, StatePosition.Root);

            _contents = new ConcurrentDictionary<int, INavigableContent?>();
        }

        ///// <summary>
        ///// Initializes a new instance of the <see cref="NavigableNavigator"/> class with a content source, a name table and a state.
        ///// </summary>
        ///// <param name="source">The content source.</param>
        ///// <param name="nameTable">The name table.</param>
        ///// <param name="state">The state.</param>
        ///// <param name="maxDepth">The maximum depth.</param>
        ///// <remarks>Privately used for cloning a navigator.</remarks>
        //private NavigableNavigator(INavigableSource source, XmlNameTable nameTable, State state, int maxDepth)
        //    : this(source, rootId: 0, maxDepth: maxDepth)
        //{
        //    _nameTable = nameTable;
        //    _state = state;
        //}

        /// <summary>
        /// Initializes a new instance of the <see cref="NavigableNavigator"/> class as a clone.
        /// </summary>
        /// <param name="orig">The cloned navigator.</param>
        /// <param name="state">The clone state.</param>
        /// <param name="maxDepth">The clone maximum depth.</param>
        /// <remarks>Privately used for cloning a navigator.</remarks>
        private NavigableNavigator(NavigableNavigator orig, State? state = null, int maxDepth = -1)
            : this(orig._source, rootId: 0, maxDepth: orig._maxDepth)
        {
            _nameTable = orig._nameTable;

            _state = state ?? orig._state.Clone();
            if (state != null && maxDepth < 0)
                throw new ArgumentException("Both state and maxDepth are required.");
            _maxDepth = maxDepth < 0 ? orig._maxDepth : maxDepth;

            _contents = orig._contents;
        }

        #endregion

        #region Diagnostics

#if DEBUGNAVIGATOR
        private const string Tabs = "                    ";
        private int _tabs;
        private readonly int _uid = GetUid();
        private static int _uidg;
        private readonly static object Uidl = new object();
        private static int GetUid()
        {
            lock (Uidl)
            {
                return _uidg++;
            }
        }
#endif

        // About conditional methods: marking a method with the [Conditional] attribute ensures
        // that no calls to the method will be generated by the compiler. However, the method
        // does exist. Wrapping the method body with #if/endif ensures that no IL is generated
        // and so it's only an empty method.

        [Conditional("DEBUGNAVIGATOR")]
        void DebugEnter(string name)
        {
#if DEBUGNAVIGATOR
            Debug("");
            DebugState(":");
            Debug(name);
            _tabs = Math.Min(Tabs.Length, _tabs + 2);
#endif
        }

        [Conditional("DEBUGNAVIGATOR")]
        void DebugCreate(NavigableNavigator nav)
        {
#if DEBUGNAVIGATOR
            Debug("Create: [NavigableNavigator::{0}]", nav._uid);
#endif
        }

        [Conditional("DEBUGNAVIGATOR")]
        private void DebugReturn()
        {
#if DEBUGNAVIGATOR
// ReSharper disable IntroduceOptionalParameters.Local
            DebugReturn("(void)");
// ReSharper restore IntroduceOptionalParameters.Local
#endif
        }

        [Conditional("DEBUGNAVIGATOR")]
        private void DebugReturn(bool value)
        {
#if DEBUGNAVIGATOR
            DebugReturn(value ? "true" : "false");
#endif
        }

        [Conditional("DEBUGNAVIGATOR")]
        void DebugReturn(string format, params object[] args)
        {
#if DEBUGNAVIGATOR
            Debug("=> " + format, args);
            if (_tabs > 0) _tabs -= 2;
#endif
        }

        [Conditional("DEBUGNAVIGATOR")]
        void DebugState(string s = " =>")
        {
#if DEBUGNAVIGATOR
            string position;

            switch (_state.Position)
            {
                case StatePosition.Attribute:
                    position = string.Format("At attribute '{0}/@{1}'.",
                        _state.Content.Type.Name,
                        _state.FieldIndex < 0 ? "id" : _state.CurrentFieldType.Name);
                    break;
                case StatePosition.Element:
                    position = string.Format("At element '{0}' (depth={1}).",
                        _state.Content.Type.Name, _state.Depth);
                    break;
                case StatePosition.PropertyElement:
                    position = string.Format("At property '{0}/{1}'.",
                        _state.Content.Type.Name, _state.Content.Type.FieldTypes[this._state.FieldIndex].Name);
                    break;
                case StatePosition.PropertyText:
                    position = string.Format("At property '{0}/{1}' text.",
                        _state.Content.Type.Name, _state.CurrentFieldType.Name);
                    break;
                case StatePosition.PropertyXml:
                    position = string.Format("In property '{0}/{1}' xml fragment.",
                        _state.Content.Type.Name, _state.CurrentFieldType.Name);
                    break;
                case StatePosition.Root:
                    position = string.Format("At root (depth={0}).",
                        _state.Depth);
                    break;
                default:
                    throw new InvalidOperationException("Invalid position.");
            }

            Debug("State{0} {1}", s, position);
#endif
        }

#if DEBUGNAVIGATOR
        void Debug(string format, params object[] args)
        {
            // remove comments to write

            //format = "[" + _uid.ToString("00000") + "] " + Tabs.Substring(0, _tabs) + format;
            //var msg = string.Format(format, args);
        }
#endif

        #endregion

        #region Source management

        private readonly ConcurrentDictionary<int, INavigableContent?> _contents;

        private INavigableContent? SourceGet(int id)
        {
            // original version, would keep creating INavigableContent objects
            //return _source.Get(id);

            // improved version, uses a cache, shared with clones
            return _contents.GetOrAdd(id, x => _source.Get(x));
        }

        #endregion

        /// <summary>
        /// Gets the underlying content object.
        /// </summary>
        public override object? UnderlyingObject => _state.Content;

        /// <summary>
        /// Creates a new XPathNavigator positioned at the same node as this XPathNavigator.
        /// </summary>
        /// <returns>A new XPathNavigator positioned at the same node as this XPathNavigator.</returns>
        public override XPathNavigator Clone()
        {
            DebugEnter("Clone");
            var nav = new NavigableNavigator(this);
            DebugCreate(nav);
            DebugReturn("[XPathNavigator]");
            return nav;
        }

        /// <summary>
        /// Creates a new XPathNavigator using the same source but positioned at a new root.
        /// </summary>
        /// <returns>A new XPathNavigator using the same source and positioned at a new root.</returns>
        /// <remarks>The new root can be above this navigator's root.</remarks>
        public XPathNavigator CloneWithNewRoot(string id, int maxDepth = int.MaxValue)
        {
            int i;
            if (int.TryParse(id, NumberStyles.Integer, CultureInfo.InvariantCulture, out i) == false)
                throw new ArgumentException("Not a valid identifier.", nameof(id));
            return CloneWithNewRoot(id);
        }

        /// <summary>
        /// Creates a new XPathNavigator using the same source but positioned at a new root.
        /// </summary>
        /// <returns>A new XPathNavigator using the same source and positioned at a new root.</returns>
        /// <remarks>The new root can be above this navigator's root.</remarks>
        public XPathNavigator? CloneWithNewRoot(int id, int maxDepth = int.MaxValue)
        {
            DebugEnter("CloneWithNewRoot");

            State? state = null;

            if (id <= 0)
            {
                state = new State(_source.Root, null, null, 0, StatePosition.Root);
            }
            else
            {
                var content = SourceGet(id);
                if (content != null)
                {
                    state = new State(content, null, null, 0, StatePosition.Root);
                }
            }

            NavigableNavigator? clone = null;

            if (state != null)
            {
                clone = new NavigableNavigator(this, state, maxDepth);
                DebugCreate(clone);
                DebugReturn("[XPathNavigator]");
            }
            else
            {
                DebugReturn("[null]");
            }

            return clone;
        }

        /// <summary>
        /// Gets a value indicating whether the current node is an empty element without an end element tag.
        /// </summary>
        public override bool IsEmptyElement
        {
            get
            {
                DebugEnter("IsEmptyElement");
                bool isEmpty;

                switch (_state.Position)
                {
                    case StatePosition.Element:
                        // must go through source because of preview/published ie there may be
                        // ids but corresponding to preview elements that we don't see here
                        var hasContentChild = _state.GetContentChildIds(_maxDepth).Any(x => SourceGet(x) != null);
                        isEmpty = (hasContentChild == false) // no content child
                            && _state.FieldsCount - 1 == _lastAttributeIndex; // no property element child
                        break;
                    case StatePosition.PropertyElement:
                        // value should be
                        // - an XPathNavigator over a non-empty XML fragment
                        // - a non-Xml-whitespace string
                        // - null
                        isEmpty = _state.Content?.Value(_state.FieldIndex) == null;
                        break;
                    case StatePosition.PropertyXml:
                        isEmpty = _state.XmlFragmentNavigator?.IsEmptyElement ?? true;
                        break;
                    case StatePosition.Attribute:
                    case StatePosition.PropertyText:
                    case StatePosition.Root:
                        throw new InvalidOperationException("Not an element.");
                    default:
                        throw new InvalidOperationException("Invalid position.");
                }

                DebugReturn(isEmpty);
                return isEmpty;
            }
        }

        /// <summary>
        /// Determines whether the current XPathNavigator is at the same position as the specified XPathNavigator.
        /// </summary>
        /// <param name="nav">The XPathNavigator to compare to this XPathNavigator.</param>
        /// <returns>true if the two XPathNavigator objects have the same position; otherwise, false.</returns>
        public override bool IsSamePosition(XPathNavigator nav)
        {
            DebugEnter("IsSamePosition");
            bool isSame;

            switch (_state.Position)
            {
                case StatePosition.PropertyXml:
                    isSame = _state.XmlFragmentNavigator?.IsSamePosition(nav) ?? false;
                    break;
                case StatePosition.Attribute:
                case StatePosition.Element:
                case StatePosition.PropertyElement:
                case StatePosition.PropertyText:
                case StatePosition.Root:
                    var other = nav as NavigableNavigator;
                    isSame = other != null && other._source == _source && _state.IsSamePosition(other._state);
                    break;
                default:
                    throw new InvalidOperationException("Invalid position.");
            }

            DebugReturn(isSame);
            return isSame;
        }

        /// <summary>
        /// Gets the qualified name of the current node.
        /// </summary>
        public override string Name
        {
            get
            {
                DebugEnter("Name");
                string name;

                switch (_state.Position)
                {
                    case StatePosition.PropertyXml:
                        name = _state.XmlFragmentNavigator?.Name ?? string.Empty;
                        break;
                    case StatePosition.Attribute:
                    case StatePosition.PropertyElement:
                        name = _state.FieldIndex == -1 ? "id" : _state.CurrentFieldType?.Name ?? string.Empty;
                        break;
                    case StatePosition.Element:
                        name = _state.Content?.Type.Name ?? string.Empty;
                        break;
                    case StatePosition.PropertyText:
                        name = string.Empty;
                        break;
                    case StatePosition.Root:
                        name = string.Empty;
                        break;
                    default:
                        throw new InvalidOperationException("Invalid position.");
                }

                DebugReturn("\"{0}\"", name);
                return name;
            }
        }

        /// <summary>
        /// Gets the Name of the current node without any namespace prefix.
        /// </summary>
        public override string LocalName
        {
            get
            {
                DebugEnter("LocalName");
                var name = Name;
                DebugReturn("\"{0}\"", name);
                return name;
            }
        }

        /// <summary>
        /// Moves the XPathNavigator to the same position as the specified XPathNavigator.
        /// </summary>
        /// <param name="nav">The XPathNavigator positioned on the node that you want to move to. </param>
        /// <returns>Returns true if the XPathNavigator is successful moving to the same position as the specified XPathNavigator;
        /// otherwise, false. If false, the position of the XPathNavigator is unchanged.</returns>
        public override bool MoveTo(XPathNavigator nav)
        {
            DebugEnter("MoveTo");

            var other = nav as NavigableNavigator;
            var succ = false;

            if (other != null && other._source == _source)
            {
                _state = other._state.Clone();
                DebugState();
                succ = true;
            }

            DebugReturn(succ);
            return succ;
        }

        /// <summary>
        /// Moves the XPathNavigator to the first attribute of the current node.
        /// </summary>
        /// <returns>Returns true if the XPathNavigator is successful moving to the first attribute of the current node;
        /// otherwise, false. If false, the position of the XPathNavigator is unchanged.</returns>
        public override bool MoveToFirstAttribute()
        {
            DebugEnter("MoveToFirstAttribute");
            bool succ;

            switch (_state.Position)
            {
                case StatePosition.PropertyXml:
                    succ = _state.XmlFragmentNavigator?.MoveToFirstAttribute() ?? false;
                    break;
                case StatePosition.Element:
                    _state.FieldIndex = -1;
                    _state.Position = StatePosition.Attribute;
                    DebugState();
                    succ = true;
                    break;
                case StatePosition.Attribute:
                case StatePosition.PropertyElement:
                case StatePosition.PropertyText:
                case StatePosition.Root:
                    succ = false;
                    break;
                default:
                    throw new InvalidOperationException("Invalid position.");
            }

            DebugReturn(succ);
            return succ;
        }

        /// <summary>
        /// Moves the XPathNavigator to the first child node of the current node.
        /// </summary>
        /// <returns>Returns true if the XPathNavigator is successful moving to the first child node of the current node;
        /// otherwise, false. If false, the position of the XPathNavigator is unchanged.</returns>
        public override bool MoveToFirstChild()
        {
            DebugEnter("MoveToFirstChild");
            bool succ;

            switch (_state.Position)
            {
                case StatePosition.PropertyXml:
                    succ = _state.XmlFragmentNavigator?.MoveToFirstChild() ?? false;
                    break;
                case StatePosition.Attribute:
                case StatePosition.PropertyText:
                    succ = false;
                    break;
                case StatePosition.Element:
                    var firstPropertyIndex = _lastAttributeIndex + 1;
                    if (_state.FieldsCount > firstPropertyIndex)
                    {
                        _state.Position = StatePosition.PropertyElement;
                        _state.FieldIndex = firstPropertyIndex;
                        DebugState();
                        succ = true;
                    }
                    else succ = MoveToFirstChildElement();
                    break;
                case StatePosition.PropertyElement:
                    succ = MoveToFirstChildProperty();
                    break;
                case StatePosition.Root:
                    _state.Position = StatePosition.Element;
                    DebugState();
                    succ = true;
                    break;
                default:
                    throw new InvalidOperationException("Invalid position.");
            }

            DebugReturn(succ);
            return succ;
        }

        private bool MoveToFirstChildElement()
        {
            var children = _state.GetContentChildIds(_maxDepth);

            if (children.Count > 0)
            {
                // children may contain IDs that does not correspond to some content in source
                // because children contains all child IDs including unpublished children - and
                // then if we're not previewing, the source will return null.
                var child = children.Select(id => SourceGet(id)).FirstOrDefault(c => c != null);
                if (child != null)
                {
                    _state.Position = StatePosition.Element;
                    _state.FieldIndex = -1;
                    _state = new State(child, _state, children, 0, StatePosition.Element);
                    DebugState();
                    return true;
                }
            }

            return false;
        }

        private bool MoveToFirstChildProperty()
        {
            var valueForXPath = _state.Content?.Value(_state.FieldIndex);

            // value should be
            // - an XPathNavigator over a non-empty XML fragment
            // - a non-Xml-whitespace string
            // - null

            var nav = valueForXPath as XPathNavigator;
            if (nav != null)
            {
                nav = nav.Clone(); // never use the one we got
                nav.MoveToFirstChild();
                _state.XmlFragmentNavigator = nav;
                _state.Position = StatePosition.PropertyXml;
                DebugState();
                return true;
            }

            if (valueForXPath == null)
                return false;

            if (valueForXPath is string)
            {
                _state.Position = StatePosition.PropertyText;
                DebugState();
                return true;
            }

            throw new InvalidOperationException("XPathValue must be an XPathNavigator or a string.");
        }

        /// <summary>
        /// Moves the XPathNavigator to the first namespace node that matches the XPathNamespaceScope specified.
        /// </summary>
        /// <param name="namespaceScope">An XPathNamespaceScope value describing the namespace scope. </param>
        /// <returns>Returns true if the XPathNavigator is successful moving to the first namespace node;
        /// otherwise, false. If false, the position of the XPathNavigator is unchanged.</returns>
        public override bool MoveToFirstNamespace(XPathNamespaceScope namespaceScope)
        {
            DebugEnter("MoveToFirstNamespace");
            DebugReturn(false);
            return false;
        }

        /// <summary>
        /// Moves the XPathNavigator to the next namespace node matching the XPathNamespaceScope specified.
        /// </summary>
        /// <param name="namespaceScope">An XPathNamespaceScope value describing the namespace scope. </param>
        /// <returns>Returns true if the XPathNavigator is successful moving to the next namespace node;
        /// otherwise, false. If false, the position of the XPathNavigator is unchanged.</returns>
        public override bool MoveToNextNamespace(XPathNamespaceScope namespaceScope)
        {
            DebugEnter("MoveToNextNamespace");
            DebugReturn(false);
            return false;
        }

        /// <summary>
        /// Moves to the node that has an attribute of type ID whose value matches the specified String.
        /// </summary>
        /// <param name="id">A String representing the ID value of the node to which you want to move.</param>
        /// <returns>true if the XPathNavigator is successful moving; otherwise, false.
        /// If false, the position of the navigator is unchanged.</returns>
        public override bool MoveToId(string id)
        {
            DebugEnter("MoveToId");
            var succ = false;

            // don't look into fragments, just look for element identifiers
            // not sure we actually need to implement it... think of it as
            // as exercise of style, always better than throwing NotImplemented.

            // navigator may be rooted below source root
            // find the navigator root id
            var state = _state;
            while (state.Parent != null) // root state has no parent
                state = state.Parent;
            var navRootId = state.Content?.Id;

            int contentId;
            if (int.TryParse(id, NumberStyles.Integer, CultureInfo.InvariantCulture, out contentId))
            {
                if (contentId == navRootId)
                {
                    _state = new State(state.Content, null, null, 0, StatePosition.Element);
                    succ = true;
                }
                else
                {
                    var content = SourceGet(contentId);
                    if (content != null)
                    {
                        // walk up to the navigator's root - or the source's root
                        var s = new Stack<INavigableContent>();
                        while (content != null && content.ParentId != navRootId)
                        {
                            s.Push(content);
                            content = SourceGet(content.ParentId);
                        }

                        if (content != null && s.Count < _maxDepth)
                        {
                            _state = new State(state.Content, null, null, 0, StatePosition.Element);
                            while (content != null)
                            {
                                _state = new State(content, _state, _state.Content?.ChildIds, _state.Content?.ChildIds?.IndexOf(content.Id) ?? -1, StatePosition.Element);
                                content = s.Count == 0 ? null : s.Pop();
                            }
                            DebugState();
                            succ = true;
                        }
                    }
                }
            }

            DebugReturn(succ);
            return succ;
        }

        /// <summary>
        /// Moves the XPathNavigator to the next sibling node of the current node.
        /// </summary>
        /// <returns>true if the XPathNavigator is successful moving to the next sibling node;
        /// otherwise, false if there are no more siblings or if the XPathNavigator is currently
        /// positioned on an attribute node. If false, the position of the XPathNavigator is unchanged.</returns>
        public override bool MoveToNext()
        {
            DebugEnter("MoveToNext");
            bool succ;

            switch (_state.Position)
            {
                case StatePosition.PropertyXml:
                    succ = _state.XmlFragmentNavigator?.MoveToNext() ?? false;
                    break;
                case StatePosition.Element:
                    succ = false;
                    while (_state.Siblings != null && _state.SiblingIndex < _state.Siblings.Count - 1)
                    {
                        // Siblings may contain IDs that does not correspond to some content in source
                        // because children contains all child IDs including unpublished children - and
                        // then if we're not previewing, the source will return null.
                        var node = SourceGet(_state.Siblings[++_state.SiblingIndex]);
                        if (node == null) continue;

                        _state.Content = node;
                        DebugState();
                        succ = true;
                        break;
                    }
                    break;
                case StatePosition.PropertyElement:
                    if (_state.FieldIndex == _state.FieldsCount - 1)
                    {
                        // after property elements may come some children elements
                        // if successful, will push a new state
                        succ = MoveToFirstChildElement();
                    }
                    else
                    {
                        ++_state.FieldIndex;
                        DebugState();
                        succ = true;
                    }
                    break;
                case StatePosition.PropertyText:
                case StatePosition.Attribute:
                case StatePosition.Root:
                    succ = false;
                    break;
                default:
                    throw new InvalidOperationException("Invalid position.");
            }

            DebugReturn(succ);
            return succ;
        }

        /// <summary>
        /// Moves the XPathNavigator to the previous sibling node of the current node.
        /// </summary>
        /// <returns>Returns true if the XPathNavigator is successful moving to the previous sibling node;
        /// otherwise, false if there is no previous sibling node or if the XPathNavigator is currently
        /// positioned on an attribute node. If false, the position of the XPathNavigator is unchanged.</returns>
        public override bool MoveToPrevious()
        {
            DebugEnter("MoveToPrevious");
            bool succ;

            switch (_state.Position)
            {
                case StatePosition.PropertyXml:
                    succ = _state.XmlFragmentNavigator?.MoveToPrevious() ?? false;
                    break;
                case StatePosition.Element:
                    succ = false;
                    while (_state.Siblings != null && _state.SiblingIndex > 0)
                    {
                        // children may contain IDs that does not correspond to some content in source
                        // because children contains all child IDs including unpublished children - and
                        // then if we're not previewing, the source will return null.
                        var content = SourceGet(_state.Siblings[--_state.SiblingIndex]);
                        if (content == null) continue;

                        _state.Content = content;
                        DebugState();
                        succ = true;
                        break;
                    }
                    if (succ == false && _state.SiblingIndex == 0 && _state.FieldsCount - 1 > _lastAttributeIndex)
                    {
                        // before children elements may come some property elements
                        if (MoveToParentElement()) // pops the state
                        {
                            _state.FieldIndex = _state.FieldsCount - 1;
                            DebugState();
                            succ = true;
                        }
                    }
                    break;
                case StatePosition.PropertyElement:
                    succ = false;
                    if (_state.FieldIndex > _lastAttributeIndex)
                    {
                        --_state.FieldIndex;
                        DebugState();
                        succ = true;
                    }
                    break;
                case StatePosition.Attribute:
                case StatePosition.PropertyText:
                case StatePosition.Root:
                    succ = false;
                    break;
                default:
                    throw new InvalidOperationException("Invalid position.");
            }

            DebugReturn(succ);
            return succ;
        }

        /// <summary>
        /// Moves the XPathNavigator to the next attribute.
        /// </summary>
        /// <returns>Returns true if the XPathNavigator is successful moving to the next attribute;
        /// false if there are no more attributes. If false, the position of the XPathNavigator is unchanged.</returns>
        public override bool MoveToNextAttribute()
        {
            DebugEnter("MoveToNextAttribute");
            bool succ;

            switch (_state.Position)
            {
                case StatePosition.PropertyXml:
                    succ = _state.XmlFragmentNavigator?.MoveToNextAttribute() ?? false;
                    break;
                case StatePosition.Attribute:
                    if (_state.FieldIndex == _lastAttributeIndex)
                        succ = false;
                    else
                    {
                        ++_state.FieldIndex;
                        DebugState();
                        succ = true;
                    }
                    break;
                case StatePosition.Element:
                case StatePosition.PropertyElement:
                case StatePosition.PropertyText:
                case StatePosition.Root:
                    succ = false;
                    break;
                default:
                    throw new InvalidOperationException("Invalid position.");
            }

            DebugReturn(succ);
            return succ;
        }

        /// <summary>
        /// Moves the XPathNavigator to the parent node of the current node.
        /// </summary>
        /// <returns>Returns true if the XPathNavigator is successful moving to the parent node of the current node;
        /// otherwise, false. If false, the position of the XPathNavigator is unchanged.</returns>
        public override bool MoveToParent()
        {
            DebugEnter("MoveToParent");
            bool succ;

            switch (_state.Position)
            {
                case StatePosition.Attribute:
                case StatePosition.PropertyElement:
                    _state.Position = StatePosition.Element;
                    _state.FieldIndex = -1;
                    DebugState();
                    succ = true;
                    break;
                case StatePosition.Element:
                    succ = MoveToParentElement();
                    if (succ == false)
                    {
                        _state.Position = StatePosition.Root;
                        succ = true;
                    }
                    break;
                case StatePosition.PropertyText:
                    _state.Position = StatePosition.PropertyElement;
                    DebugState();
                    succ = true;
                    break;
                case StatePosition.PropertyXml:
                    if (_state.XmlFragmentNavigator?.MoveToParent() == false)
                        throw new InvalidOperationException("Could not move to parent in fragment.");
                    if (_state.XmlFragmentNavigator?.NodeType == XPathNodeType.Root)
                    {
                        _state.XmlFragmentNavigator = null;
                        _state.Position = StatePosition.PropertyElement;
                        DebugState();
                    }
                    succ = true;
                    break;
                case StatePosition.Root:
                    succ = false;
                    break;
                default:
                    throw new InvalidOperationException("Invalid position.");
            }

            DebugReturn(succ);
            return succ;
        }

        private bool MoveToParentElement()
        {
            var p = _state.Parent;
            if (p != null)
            {
                _state = p;
                DebugState();
                return true;
            }

            return false;
        }

        /// <summary>
        /// Moves the XPathNavigator to the root node that the current node belongs to.
        /// </summary>
        public override void MoveToRoot()
        {
            DebugEnter("MoveToRoot");

            while (_state.Parent != null)
                _state = _state.Parent;
            DebugState();

            DebugReturn();
        }

        /// <summary>
        /// Gets the base URI for the current node.
        /// </summary>
        public override string BaseURI => string.Empty;

        /// <summary>
        /// Gets the XmlNameTable of the XPathNavigator.
        /// </summary>
        public override XmlNameTable NameTable => _nameTable;

        /// <summary>
        /// Gets the namespace URI of the current node.
        /// </summary>
        public override string NamespaceURI => string.Empty;

        /// <summary>
        /// Gets the XPathNodeType of the current node.
        /// </summary>
        public override XPathNodeType NodeType
        {
            get
            {
                DebugEnter("NodeType");
                XPathNodeType type;

                switch (_state.Position)
                {
                    case StatePosition.PropertyXml:
                        type = _state.XmlFragmentNavigator?.NodeType ?? XPathNodeType.Root;
                        break;
                    case StatePosition.Attribute:
                        type = XPathNodeType.Attribute;
                        break;
                    case StatePosition.Element:
                    case StatePosition.PropertyElement:
                        type = XPathNodeType.Element;
                        break;
                    case StatePosition.PropertyText:
                        type = XPathNodeType.Text;
                        break;
                    case StatePosition.Root:
                        type = XPathNodeType.Root;
                        break;
                    default:
                        throw new InvalidOperationException("Invalid position.");
                }

                DebugReturn("\'{0}\'", type);
                return type;
            }
        }

        /// <summary>
        /// Gets the namespace prefix associated with the current node.
        /// </summary>
        public override string Prefix => string.Empty;

        /// <summary>
        /// Gets the string value of the item.
        /// </summary>
        /// <remarks>Does not fully behave as per the specs, as we report empty value on content elements, and we start
        /// reporting values only on property elements. This is because, otherwise, we would dump the whole database
        /// and it probably does not make sense at Umbraco level.</remarks>
        public override string Value
        {
            get
            {
                DebugEnter("Value");
                string value;

                switch (_state.Position)
                {
                    case StatePosition.PropertyXml:
                        value = _state.XmlFragmentNavigator?.Value ?? string.Empty;
                        break;
                    case StatePosition.Attribute:
                    case StatePosition.PropertyText:
                    case StatePosition.PropertyElement:
                        if (_state.FieldIndex == -1)
                        {
                            value = _state.Content?.Id.ToString(CultureInfo.InvariantCulture) ?? string.Empty;
                        }
                        else
                        {
                            var valueForXPath = _state.Content?.Value(_state.FieldIndex);

                            // value should be
                            // - an XPathNavigator over a non-empty XML fragment
                            // - a non-Xml-whitespace string
                            // - null

                            var nav = valueForXPath as XPathNavigator;
                            var s = valueForXPath as string;
                            if (valueForXPath == null)
                            {
                                value = string.Empty;
                            }
                            else if (nav != null)
                            {
                                nav = nav.Clone(); // never use the one we got
                                value = nav.Value;
                            }
                            else if (s != null)
                            {
                                value = s;
                            }
                            else
                            {
                                throw new InvalidOperationException("XPathValue must be an XPathNavigator or a string.");
                            }
                        }
                        break;
                    case StatePosition.Element:
                    case StatePosition.Root:
                        value = string.Empty;
                        break;
                    default:
                        throw new InvalidOperationException("Invalid position.");
                }

                DebugReturn("\"{0}\"", value);
                return value;
            }
        }

        #region State management

        // the possible state positions
        public enum StatePosition
        {
            Root,
            Element,
            Attribute,
            PropertyElement,
            PropertyText,
            PropertyXml
        };

        // gets the state
        // for unit tests only
        public State InternalState => _state;

        // represents the XPathNavigator state
        public class State
        {
            public StatePosition Position { get; set; }

            // initialize a new state
            private State(StatePosition position)
            {
                Position = position;
                FieldIndex = -1;
            }

            // initialize a new state
            // used for creating the very first state
            // and also when moving to a child element
            public State(INavigableContent? content, State? parent, IList<int>? siblings, int siblingIndex, StatePosition position)
                : this(position)
            {
                Content = content;
                Parent = parent;
                Depth = parent?.Depth + 1 ?? 0;
                Siblings = siblings;
                SiblingIndex = siblingIndex;
            }

            // initialize a clone state
            private State(State other, bool recurse = false)
            {
                Position = other.Position;

                _content = other._content;
                SiblingIndex = other.SiblingIndex;
                Siblings = other.Siblings;
                FieldsCount = other.FieldsCount;
                FieldIndex = other.FieldIndex;
                Depth = other.Depth;

                if (Position == StatePosition.PropertyXml)
                    XmlFragmentNavigator = other.XmlFragmentNavigator?.Clone();

                // NielsK did
                //Parent = other.Parent;
                // but that creates corrupted stacks of states when cloning
                // because clones share the parents : have to clone the whole
                // stack of states. Avoid recursion.

                if (recurse) return;

                var clone = this;
                while (other.Parent != null)
                {
                    clone.Parent = new State(other.Parent, true);
                    clone = clone.Parent;
                    other = other.Parent;
                }
            }

            public State Clone()
            {
                return new State(this);
            }

            // the parent state
            public State? Parent { get; private set; }

            // the depth
            public int Depth { get; }

            // the current content
            private INavigableContent? _content;

            // the current content
            public INavigableContent? Content
            {
                get
                {
                    return _content;
                }
                set
                {
                    FieldsCount = value?.Type.FieldTypes.Length ?? 0;
                    _content = value;
                }
            }

            private static readonly int[] NoChildIds = new int[0];

            // the current content child ids
            public IList<int> GetContentChildIds(int maxDepth)
            {
                return Depth < maxDepth && _content?.ChildIds != null ? _content.ChildIds : NoChildIds;
            }

            // the index of the current content within Siblings
            public int SiblingIndex { get; set; }

            // the list of content identifiers for all children of the current content's parent
            public IList<int>? Siblings { get; }

            // the number of fields of the current content
            // properties include attributes and properties
            public int FieldsCount { get; private set; }

            // the index of the current field
            // index -1 means special attribute "id"
            public int FieldIndex { get; set; }

            // the current field type
            // beware, no check on the index
            public INavigableFieldType? CurrentFieldType => Content?.Type.FieldTypes[FieldIndex];

            // gets or sets the xml fragment navigator
            public XPathNavigator? XmlFragmentNavigator { get; set; }

            // gets a value indicating whether this state is at the same position as another one.
            public bool IsSamePosition(State other)
            {
                if (other.XmlFragmentNavigator is null || XmlFragmentNavigator is null)
                {
                    return false;
                }
                return other.Position == Position
                    && (Position != StatePosition.PropertyXml || other.XmlFragmentNavigator.IsSamePosition(XmlFragmentNavigator))
                    && other.Content == Content
                    && other.FieldIndex == FieldIndex;
            }
        }

        #endregion
    }
}
