/* -*- Mode: vala; indent-tabs-mode: t; c-basic-offset: 2; tab-width: 2 -*- */
/* xDocument.vala
 *
 * Copyright (C) 2011-2013  Richard Schwarting <aquarichy@gmail.com>
 * Copyright (C) 2011-2015  Daniel Espinosa <esodan@gmail.com>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.

 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.

 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, see <http://www.gnu.org/licenses/>.
 *
 * Authors:
 *      Richard Schwarting <aquarichy@gmail.com>
 *      Daniel Espinosa <esodan@gmail.com>
 */


/* TODO:
 * * later on, go over libxml2 docs for Tree and xNode and xDocument, etc., and see if we're missing anything significant
 * * compare performance between libxml2 and GXml (should be a little different, but not too much)
 */

/* TODO:IMPORTANT: don't use GLib collections, use Libgee! */

/**
 * The XML Document Object Model.
 *
 * GXml provides a DOM Level 1 Core API in a GObject framework.
 */
namespace GXml {
	/**
	 * Represents an XML xDocument as a tree of {@link GXml.xNode}s.
	 *
	 * The xDocument has a root document element {@link GXml.xElement}.
	 * A xDocument's schema can be defined through its
	 * {@link GXml.xDocumentType}.
	 *
	 * Version: DOM Level 1 Core<<BR>>
	 * URL: [[http://www.w3.org/TR/DOM-Level-1/level-one-core.html#i-xDocument]]
	 */
	public class xDocument : xNode, GXml.Document {
		/* *** Private properties *** */

		/**
		 * This contains a map of Xml.Nodes that have been
		 * accessed and the GXml xNode we created to represent
		 * them on-demand.  That way, we don't create an xNode
		 * for EVERY node, even if the user never actually
		 * accesses it.
		 */
		internal HashTable<Xml.Node*, xNode> node_dict = new HashTable<Xml.Node*, xNode> (GLib.direct_hash, GLib.direct_equal);
		// We don't want want to use Node's Xml.Node or its dict
		// internal HashTable<Xml.Attr*, Attr> attr_dict = new HashTable<Xml.Attr*, Attr> (null, null);

		/**
		 * This contains a list of elements whose attributes
		 * may have been modified within GXml, and whose modified
		 * attributes need to be saved back to the underlying
		 * libxml2 structure when we save.  (Necessary because
		 * the user can obtain a HashTable and modify that in a
		 * way that we can't follow unless we check ourselves.)
		 * Perhaps I really should implement a NamedNodeMap :|
		 * TODO: do that
		 */
		internal GLib.List<xElement> dirty_elements = new GLib.List<xElement> ();

		/* TODO: for future reference, find out if internals
		   are only accessible by children when they're
		   compiled together.  I have a test that had a
		   separately compiled TestDocument : xDocument class,
		   and it couldn't access the internal xmldoc. */
		internal Xml.Doc *xmldoc;
		internal NodeChildNodeList _node_list;

		/* *** Private methods *** */
		internal unowned xAttr? lookup_attr (Xml.Attr *xmlattr) {
			// Xml.Attr and Xml.Node are intentionally compatible
			return (xAttr)this.lookup_node ((Xml.Node*)xmlattr);
		}

		internal unowned xNode? lookup_node (Xml.Node *xmlnode) {
			unowned xNode domnode;

			if (xmlnode == null) {
				return null; // TODO: consider throwing an error instead
			}

			domnode = this.node_dict.lookup (xmlnode);
			if (domnode == null) {
				// If we don't have a cached the appropriate Node for a given Xml.Node* yet, create it (type matters)
				// TODO: see if we can attach logic to the enum {} to handle this
				NodeType nodetype = (NodeType)xmlnode->type;
				switch (nodetype) {
				case NodeType.ELEMENT:
					new xElement (xmlnode, this);
					break;
				case NodeType.TEXT:
					new xText (xmlnode, this);
					break;
				case NodeType.CDATA_SECTION:
					new xCDATASection (xmlnode, this);
					break;
				case NodeType.COMMENT:
					new xComment (xmlnode, this);
					break;
				case NodeType.DOCUMENT_FRAGMENT:
					new DocumentFragment (xmlnode, this);
					break;
				case NodeType.ATTRIBUTE:
					new xAttr ((Xml.Attr*)xmlnode, this);
					break;
					/* TODO: These are not yet implemented (but we won't support xDocument */
				case NodeType.ENTITY_REFERENCE:
				case NodeType.ENTITY:
				case NodeType.PROCESSING_INSTRUCTION:
				case NodeType.DOCUMENT_TYPE:
				case NodeType.NOTATION:
				case NodeType.DOCUMENT:
					GLib.warning (_("Looking up %s from an xmlNode* is not supported"), nodetype.to_string ());
					break;
				}

				domnode = this.node_dict.lookup (xmlnode);
				// TODO: threadsafety?
			}

			return domnode;
		}

		/* Public properties */

		/**
		 * Provides the name for this node. For documents, it is always "#document".
		 */
		public override string node_name {
			get {
				return "#document"; // TODO: wish I could return "#" + base.node_name
			}
			private set {
			}
		}

		// TODO: DTD, sort of works
		/**
		 * The xDocument Type Definition (DTD) defining this document. This may be %NULL.
		 *
		 * Version: DOM Level 1 Core<<BR>>
		 * URL: [[http://www.w3.org/TR/REC-DOM-Level-1/level-one-core.html#attribute-doctype]]
		 */
		public xDocumentType? doctype {
			// either null, or a xDocumentType object
			get;
			private set;
		}
		/**
		 * Describes the features of the DOM implementation behind this document.
		 *
		 * Version: DOM Level 1 Core<<BR>>
		 * URL: [[http://www.w3.org/TR/REC-DOM-Level-1/level-one-core.html#attribute-implementation]]
		 */
		public Implementation implementation {
			// set in constructor
			get;
			private set;
		}
		/**
		 * The root node of the document's node tree.
		 *
		 * Version: DOM Level 1 Core<<BR>>
		 * URL: [[http://www.w3.org/TR/REC-DOM-Level-1/level-one-core.html#attribute-documentElement]]
		 */
		public xElement document_element {
			// TODO: should children work at all on xDocument, or just this, to get root?
			get {
				return (xElement)this.lookup_node (this.xmldoc->get_root_element ());
			}
			private set {
			}
		}

		/* A list of strong references to all GXml.Nodes that this xDocument has created  */
		private GLib.List<GXml.xNode> nodes_to_free = new GLib.List<GXml.xNode> ();
		/* A list of references to Xml.Nodes that were created, and may require freeing */
		private GLib.List<Xml.Node*> new_nodes = new GLib.List<Xml.Node*> ();

		~xDocument () {
			GLib.List<Xml.Node*> to_free = new GLib.List<Xml.Node*> ();

			/* we use two separate loops, because freeing
			   a node frees its descendants, and we might
			   have a branch with children that might be
			   visited after their root ancestor
			*/
			foreach (Xml.Node *new_node in new_nodes) {
				if (new_node->parent == null) {
					to_free.append (new_node);
				}
			}
			foreach (Xml.Node *freeable in to_free) {
				delete freeable;
			}

			delete this.xmldoc;
		}

		/** Constructors */

		/**
		 * Creates a xDocument from a given Implementation, supporting
		 * the {@ GXml.Implementation.create_document} method.
		 *
		 * Version: DOM Level 3 Core<<BR>>
		 * URL: [[http://www.w3.org/TR/DOM-Level-3-Core/core.html#Level-2-Core-DOM-createDocument]]
		 *
		 * @param impl Implementation creating this xDocument
		 * @param namespace_uri URI for the namespace in which this xDocument belongs, or %NULL
		 * @param qualified_name A qualified name for the xDocument, or %NULL
		 * @param doctype The type of the document, or %NULL
		 *
		 * @return The new document; this must be freed with {@link GLib.Object.unref}
		 */
		internal xDocument.with_implementation (Implementation impl, string? namespace_uri, string? qualified_name, xDocumentType? doctype) {
			this ();
			this.implementation = impl;

			xNode root;
			root = (xNode) this.create_element (qualified_name); // TODO: we do not currently support namespaces, but when we do, this new node will want one
			this.append_child (root);

			this.namespace_uri = namespace_uri;
			/* TODO: find out what should be set to qualified_name; perhaps this.node_name, but then that's supposed
			   to be "#document" according to NodeType definitions in http://www.w3.org/TR/DOM-Level-3-Core/core.html */
			this.doctype = doctype;
		}

		/**
		 * Creates a xDocument based on a libxml2 Xml.Doc* object.
		 *
		 * @param doc A {@link Xml.Doc} from libxml2
		 * @param require_root A flag to indicate whether we should require a root node, which the DOM normally expects
		 *
		 * @return A new {@link GXml.xDocument} wrapping the provided {@link Xml.Doc}; this must be freed with {@link GLib.Object.unref}
		 */
		public xDocument.from_libxml2 (Xml.Doc *doc, bool require_root = true) {
			/* All other constructors should call this one,
			   passing it a Xml.Doc* object */

			Xml.Node *root;

			if (doc == null) // should be impossible
				GXml.exception (DomException.INVALID_DOC, "Failed to parse document, xmlDoc* was NULL");

			if (require_root) {
				root = doc->get_root_element ();
				if (root == null) {
					GXml.exception (DomException.INVALID_ROOT, "Could not obtain a valid root for the document; xmlDoc*'s root was NULL");
				}
			}

			// TODO: consider passing root as a base node?
			base.for_document ();

			this.owner_document = this; // must come after base ()
			this.xmldoc = doc;
			if (doc->int_subset == null && doc->ext_subset == null) {
				this.doctype = null;
			} else {
				// TODO: make sure libxml2 binding patch for this makes it through
				this.doctype = new xDocumentType (doc->int_subset, doc->ext_subset, this);
			}
			this.implementation = new Implementation ();
			_node_list = new NodeChildNodeList ((Xml.Node*) this.xmldoc, this.owner_document);
		}

		/**
		 * Creates a xDocument from the file at file_path.
		 *
		 * @param file_path A path to an XML document
		 *
		 * @return A {@link GXml.xDocument} for the given `file_path`; this must be freed with {@link GLib.Object.unref}
		 *
		 * @throws GXml.Error A {@link GXml.Error} if an error occurs while loading
		 */
		public xDocument.from_path (string file_path) throws GXml.Error {
			Xml.ParserCtxt ctxt;
			Xml.Doc *doc;
			Xml.Error *e;
			ctxt = new Xml.ParserCtxt ();
			Xmlx.context_reset_last_error (ctxt);
			doc = ctxt.read_file (file_path, null /* encoding */, 0 /* options */);

			if (doc == null) {
				e = Xmlx.context_get_last_error (ctxt);
				GXml.exception (DomException.INVALID_DOC, _("Could not load document from path: %s").printf (e->message));
				throw new GXml.Error.PARSER (GXml.libxml2_error_to_string (e));
			}

			this.from_libxml2 (doc);
		}

		/**
		 * Creates a xDocument for the {@link GLib.File} `fin`.
		 *
		 * @param fin The {@link GLib.File} containing the document
		 * @param can A {@link GLib.Cancellable} to let you cancel opening the file, or %NULL
		 *
		 * @return A new {@link GXml.xDocument} for `fin`; this must be freed with {@link GLib.Object.unref}
		 *
		 * @throws GLib.Error A {@link GLib.Error} if an error cocurs while reading the {@link GLib.File}
		 * @throws GXml.Error A {@link GXml.Error} if an error occurs while reading the file as a stream
		 */
		public xDocument.from_gfile (File fin, Cancellable? can = null) throws GXml.Error, GLib.Error
			requires (fin.query_exists ()) {
			InputStream instream;

			try {
				instream = fin.read (can);
				this.from_stream (instream, can);
				instream.close ();
			} catch (GLib.Error e) {
				GXml.exception (DomException.INVALID_DOC, "Could not load document from GFile: " + e.message);
				throw e;
			}
		}

		/**
		 * Creates a {@link GXml.xDocument} from data provided
		 * through a {@link GLib.InputStream}.
		 *
		 * @param instream A {@link GLib.InputStream} providing our document
		 * @param can      A {@link GLib.Cancellable} object allowing the caller
		 *                 to interrupt and cancel this operation, or %NULL
		 *
		 * @return A new {@link GXml.xDocument} built from the contents of instream;
		 *         this must be freed with {@link GLib.Object.unref}
		 *
		 * @throws GXml.Error A {@link GXml.Error} if an error occurs while reading the stream
		 */
		public xDocument.from_stream (InputStream instream, Cancellable? can = null) throws GLib.Error {
			Xml.Error* e = null;
			string errmsg = null;
			var ostream = new MemoryOutputStream.resizable ();
			ostream.splice (instream, GLib.OutputStreamSpliceFlags.NONE);
			if (ostream.data == null) {
				errmsg = "Document is empty";
				GXml.exception (DomException.INVALID_DOC, errmsg);
				throw new GXml.Error.PARSER (errmsg);
			}
			Xmlx.reset_last_error ();
			var doc = Xml.Parser.parse_memory ((string) ostream.data, ((string) ostream.data).length);
			if (doc != null) {
				this.from_libxml2 (doc);
			} else {
				errmsg = "Parser Error";
				e = Xmlx.get_last_error ();
				if (e != null) {
					string s = libxml2_error_to_string (e);
					if (s != null)
						errmsg += s;
				}
				GXml.exception (DomException.INVALID_DOC, errmsg);
				throw new GXml.Error.PARSER (errmsg);
			}
		}

		/**
		 * Creates a xDocument from data found in memory.
		 *
		 * @param xml A string representing an XML document
		 *
		 * @return A new {@link GXml.xDocument} from `memory`; this must be freed with {@link GLib.Object.unref}
		 */
		public xDocument.from_string (string xml) {
			Xml.Doc *doc;
			Xmlx.reset_last_error ();
			doc = Xml.Parser.parse_memory (xml, (int)xml.length);
			if (doc == null)
				doc = new Xml.Doc ();
			this.from_libxml2 (doc);
		}
		/**
		 * Creates a xDocument from data found in memory using options.
		 *
		 * @param xml A string representing an XML document
		 * @param url the base URL to use for the document
		 * @param encoding the document encoding
		 * @param options a combination of {@link Xml.ParserOption}
		 *
		 * @return A new {@link GXml.xDocument} from `memory`; this must be freed with {@link GLib.Object.unref}
		 */
		public xDocument.from_string_with_options (string xml, string? url = null,
		                                          string? encoding = null,
		                                          int options = 0)
		                                          throws GLib.Error
		{
		  Xml.Doc *doc;
			Xmlx.reset_last_error ();
		  doc = Xml.Parser.read_memory (xml, (int)xml.length, url, encoding, options);
		  this.from_libxml2 (doc);
			var e = Xmlx.get_last_error ();
			if (e != null) {
				var errmsg = "Parser Error for string";
				string s = libxml2_error_to_string (e);
				if (s != null)
					errmsg = ".  ";
				GXml.exception (DomException.INVALID_DOC, errmsg);
				throw new GXml.Error.PARSER (errmsg);
			}
		}

		/**
		 * Creates an empty document.
		 *
		 * @return A new, empty {@link GXml.xDocument}; this must be freed with {@link GLib.Object.unref}
		 */
		public xDocument () {
			Xml.Doc *doc;

			doc = new Xml.Doc ();
			this.from_libxml2 (doc, false);
			_node_list = new NodeChildNodeList ((Xml.Node*) this.xmldoc, this.owner_document);
		}

		/**
		 * Saves a xDocument to the file at path file_path
		 *
		 * @param file_path A path on the local system to save the document to
		 *
		 * @throws GXml.Error A {@link GXml.Error} if an error occurs while writing
		 */
		// TODO: is this a simple Unix file path, or does libxml2 do networks, too?
		public void save_to_path (string file_path) throws GXml.Error {
			string errmsg;
			Xml.Error *e;

			// TODO: change this to a GIO file so we can save to in a cool way

			if (-1 == this.xmldoc->save_file (file_path)) {
				errmsg = "Failed to write file to path '%s'".printf (file_path);
			} else {
				// yay!
				return;
			}

			// uh oh
			e = Xmlx.get_last_error ();
			if (e != null) {
				errmsg = "Error to save to path";
				string s =  libxml2_error_to_string (e);
				if (s != null)
					errmsg += ".  ";
			}

			// TODO: use xmlGetLastError to get the real error message
			GXml.exception (DomException.X_OTHER, errmsg);
			throw new GXml.Error.WRITER (errmsg);
		}

		/* TODO: consider adding a save_to_file, but then we
		 * need to figure out which flags to accept.  For now
		 * they can just figure it out themselves.
		 */

		/**
		 * Saves a xDocument to the OutputStream outstream.
		 *
		 * @param outstream A destination {@link GLib.OutputStream} to save the XML file to
		 * @param can A {@link GLib.Cancellable} to cancel saving with, or %NULL
		 *
		 * @throws GXml.Error A {@link GXml.Error} is thrown if saving encounters an error
		 */
		public void save_to_stream (OutputStream outstream, Cancellable? can = null) throws GLib.Error {
			try {
				var s = new GLib.StringBuilder ();
				s.append (to_string ());
				var istream = new GLib.MemoryInputStream.from_data (s.data, null);
				outstream.splice (istream, GLib.OutputStreamSpliceFlags.NONE);
			} catch (GLib.Error e) {
				GXml.exception (DomException.X_OTHER, e.message);
				throw new GXml.Error.WRITER (e.message);
			}
		}

		/* Public Methods */

		/**
		 * Creates an empty {@link GXml.xElement} node with the tag name
		 * `tag_name`, which must be a
		 * [[http://www.w3.org/TR/REC-xml/#NT-Name|valid XML name]].
		 * Its memory is freed when its owner document is
		 * freed.
		 *
		 * XML example: {{{<Person></Person>}}}
		 *
		 * Version: DOM Level 1 Core<<BR>>
		 * URL: [[http://www.w3.org/TR/REC-DOM-Level-1/level-one-core.html#method-createElement]]

		 * @param tag_name The name of the new {@link GXml.xElement}
		 *
		 * @return A new {@link GXml.xElement}; this should not be freed
		 */
		public GXml.Node create_element (string tag_name) {
			// TODO: what should we be passing for ns other than old_ns?  Figure it out; needed for level 2+ support
			Xml.Node *xmlelem;

			check_invalid_characters (tag_name, "element");

			xmlelem = this.xmldoc->new_node (null, tag_name, null);
			this.new_nodes.append (xmlelem);

			xElement new_elem = new xElement (xmlelem, this);
			this.nodes_to_free.append (new_elem);
			unowned xElement ret = new_elem;

			return ret;
		}
		/**
		 * Creates a {@link GXml.DocumentFragment}.
		 *
		 * xDocument fragments do not can contain a subset of a
		 * document, without being a complete tree.  Its
		 * memory is freed when its owner document is freed.
		 *
		 * Version: DOM Level 1 Core<<BR>>
		 * URL: [[http://www.w3.org/TR/REC-DOM-Level-1/level-one-core.html#method-createDocumentFragment]]
		 *
		 * @return A {@link GXml.DocumentFragment}; this should not be freed
		 */
		public unowned DocumentFragment create_document_fragment () {
			DocumentFragment fragment = new DocumentFragment (this.xmldoc->new_fragment (), this);
			unowned DocumentFragment ret = fragment;
			this.nodes_to_free.append (fragment);
			return ret;
		}

		/**
		 * Creates a {@link GXml.Text} node containing the text in data.
		 * Its memory is freed when its owner document is freed.
		 *
		 * XML example:
		 * {{{<someElement>Text is contained here.</someElement>}}}
		 *
		 * Version: DOM Level 1 Core<<BR>>
		 * URL: [[http://www.w3.org/TR/REC-DOM-Level-1/level-one-core.html#method-createTextNode]]
		 *
		 * @param text_data The textual data for the {@link GXml.Text} node
		 *
		 * @return A new {@link GXml.Text} node containing
		 * the supplied data; this should not be freed
		 */
		public unowned xText create_text_node (string text_data) {
			xText text = new xText (this.xmldoc->new_text (text_data), this);
			unowned xText ret = text;
			this.nodes_to_free.append (text);
			return ret;
		}

		/**
		 * Creates an XML comment with data.  Its memory is
		 * freed when its owner document is freed.
		 *
		 * XML example:
		 *
		 * {{{
		 *  <!-- data -->
		 * }}}
		 *
		 * Version: DOM Level 1 Core<<BR>>
		 * URL: [[http://www.w3.org/TR/REC-DOM-Level-1/level-one-core.html#method-createComment]]
		 *
		 * @param comment_data The content of the comment
		 *
		 * @return A new {@link GXml.Comment} containing the
		 * supplied data; this should not be freed
		 */
		public unowned xComment create_managed_comment (string comment_data) {
			// TODO: should we be passing around Xml.Node* like this?
			xComment comment = new xComment (this.xmldoc->new_comment (comment_data), this);
			unowned xComment ret = comment;
			this.nodes_to_free.append (comment);
			return ret;
		}

		/**
		 * Creates a CDATA section containing data.
		 *
		 * These do not apply to HTML doctype documents.  Its
		 * memory is freed when its owner document is freed.
		 *
		 * XML example:
		 * {{{ <![CDATA[Here contains non-XML data, like code, or something that requires a lot of special XML entities.]]>. }}}
		 *
		 * Version: DOM Level 1 Core<<BR>>
		 * URL: [[http://www.w3.org/TR/REC-DOM-Level-1/level-one-core.html#method-createCDATASection]]
		 *
		 * @param cdata_data The content for the CDATA section
		 *
		 * @return A new {@link GXml.xCDATASection} with the
		 * supplied data; this should not be freed
		 */
		public unowned xCDATASection create_cdata_section (string cdata_data) {
			check_not_supported_html ("CDATA section");

			xCDATASection cdata = new xCDATASection (this.xmldoc->new_cdata_block (cdata_data, (int)cdata_data.length), this);
			unowned xCDATASection ret = cdata;
			this.nodes_to_free.append (cdata);
			return ret;
		}

		/**
		 * Creates a new {@link GXml.ProcessingInstruction}.
		 *
		 * Its memory is freed when its owner document is
		 * freed.
		 *
		 * Version: DOM Level 1 Core<<BR>>
		 * URL: [[http://www.w3.org/TR/REC-DOM-Level-1/level-one-core.html#method-createProcessingInstruction]]
		 *
		 * @param target The target of the instruction
		 * @param data The content of the instruction
		 *
		 * @return A new {@link GXml.ProcessingInstruction}
		 * for the given target; this should not be freed
		 */
		public xProcessingInstruction create_processing_instruction (string target, string data) {
			/* TODO: this is not backed by a libxml2 structure,
			   and is not stored in the NodeDict, so we don't know
			   when it will be freed :( Figure it out.

			   It looks like so far this GXmlProcessingInstruction node doesn't
			   get recorded by its owner_document at all, so the reference
			   is probably lost.

			   We want to manage it with the GXmlDocument, though, and not
			   make the developer manage it, because that would be inconsistent
			   with the rest of the tree (even if the user doesn't insert
			   this PI into a xDocument at all.  */
			check_not_supported_html ("processing instructions");
			check_invalid_characters (target, "processing instruction");

			// TODO: want to see whether we can find a libxml2 structure for this
			xProcessingInstruction pi = new xProcessingInstruction (target, data, this);

			return pi;
		}

		/**
		 * Creates an {@link GXml.Attribute} attribute with `name`, usually to be associated with an xElement.
		 *
		 * XML example: {{{<element attributename="attributevalue">content</element>}}}
		 *
		 * Version: DOM Level 1 Core<<BR>>
		 * URL: [[http://www.w3.org/TR/REC-DOM-Level-1/level-one-core.html#method-createAttribute]]
		 *
		 * @param name The `name` of the attribute
		 *
		 * @return A new {@link GXml.Attribute} with the given `name`; this should not be freed
		 */
		public xAttr create_attribute (string name) {
			/* TODO: figure out memory for this; its a
			 * Node, not a BackedNode and thus not in
			 * nodedict.  It's like Processing Instruction
			 * in that regard.
			 *
			 * That said, we might be able to make it a
			 * BackedNode after all depending on how
			 * comfortable we are treating libxml2
			 * xmlAttrs as xmlNodes. :D
			 */
			check_invalid_characters (name, "attribute");

			return new xAttr (this.xmldoc->new_prop (name, ""), this);

			/* TODO: should we pass something other than
			   "" for the unspecified value?  probably
			   not, "" is working fine so far.

			   Actually, this introduces troublesome
			   compatibility issues when porting libxml2
			   code to GXml, because in GXml, an
			   unspecified value is also "" (because of
			   the spec) whereas in libxml2 it is NULL. */

			/* TODO: want to create a convenience method
			   to create a new Attr with name and value
			   spec'd, like create_attribute_with_value
			   (), make sure that's not already spec'd in
			   later DOM levels. */
		}

		/**
		 * Creates an entity reference.
		 *
		 * XML example: {{{&name;}}}, for example an apostrophe has the name 'apos', so in XML it appears as {{{&apos;}}}
		 *
		 * Version: DOM Level 1 Core<<BR>>
		 * URL: [[http://www.w3.org/TR/REC-DOM-Level-1/level-one-core.html#method-createEntityReference]]
		 *
		 * @param name The 'name' of the entity reference
		 *
		 * @return An {@link GXml.EntityReference} for `name`; this should not be freed
		 */
		public EntityReference create_entity_reference (string name) {
			check_not_supported_html ("entity reference");
			check_invalid_characters (name, "entity reference");

			return new EntityReference (name, this);
			// TODO: doublecheck that libxml2 doesn't have a welldefined ER
		}

		/**
		 * Obtains a list of {@link GXml.xElement}s, each with
		 * the given tag name `tag_name`, contained within
		 * this document.
		 *
		 * Note that the list is live, updated as new elements
		 * are added to the document.
		 *
		 * Unlike a {@link GXml.xNode} and its subclasses,
		 * {@link GXml.NodeList} are not part of the document
		 * tree, and thus their memory is not managed for the
		 * user, so the user must explicitly free them.
		 *
		 * Version: DOM Level 1 Core<<BR>>
		 * URL: [[http://www.w3.org/TR/REC-DOM-Level-1/level-one-core.html#method-getElementsByTagName]]
		 *
		 * @param tag_name The {@link GXml.xElement} tag name we matching for
		 *
		 * @return A {@link GXml.NodeList} of
		 * {@link GXml.xElement}s; this must be freed with
		 * {@link GLib.Object.unref}.
		 */
		public xNodeList get_elements_by_tag_name (string tag_name) {
			// TODO: verify that it is still live :D
			// TODO: does this ensure that the root element is also included?
			// TODO: determine whether the use needs to free these lists
			return this.document_element.get_elements_by_tag_name (tag_name);
		}

		/**
		 * Feature should be something like "processing instructions"
		 */
		private void check_not_supported_html (string feature) {
			if (this.doctype != null && (this.doctype.name.casefold () == "html".casefold ())) {
				GXml.exception (DomException.NOT_SUPPORTED, "HTML documents do not support '%s'".printf (feature)); // TODO: i18n
			}
		}

		/**
		 * Subject should be something like "element" or "processing instruction"
		 */
		internal static bool check_invalid_characters (string name, string subject) {
			/* TODO: use Xml.validate_name instead  */
			if (Xmlx.validate_name (name, 0) != 0) { // TODO: define validity
				GXml.exception (DomException.INVALID_CHARACTER, "Provided name '%s' for '%s' is not a valid XML name".printf (name, subject));
				return false;
			}

			return true;
		}

		/**
		 * {@inheritDoc}
		 */
		public override string stringify (bool format = true, int level = 0) {
			string str;
			int len;

			this.xmldoc->dump_memory_format (out str, out len, format);

			return str;
		}

		/*** Node methods ***/

		/**
		 * {@inheritDoc}
		 */
		public override xNodeList? child_nodes {
			get {
				// TODO: always create a new one?
				// TODO: xmlDoc and xmlNode are very similar, but perhaps we shouldn't do this :D
				return _node_list;
			}
			internal set {
			}
		}


		/**
		 * Replaces `old_child` with `new_child` in this node's list of children.
		 *
		 * Version: DOM Level 1 Core<<BR>>
		 * URL: [[http://www.w3.org/TR/REC-DOM-Level-1/level-one-core.html#method-replaceChild]]
		 *
		 * @param new_child The child we will replace `old_child` with
		 * @param old_child The child being replaced
		 *
		 * @return The removed node `old_child`; this should not be freed
		 */
		public override unowned xNode? replace_child (xNode new_child, xNode old_child) {
			if (new_child.node_type == NodeType.ELEMENT ||
			    new_child.node_type == NodeType.DOCUMENT_TYPE) {
				/* let append_child do it with its error handling, since
				   we don't consider position with libxml2 */
				return this.append_child (new_child);
			} else {
				return this.replace_child (new_child, old_child);
			}
		}

		/**
		 * Removes `old_child` from this document's list of
		 * children.
		 *
		 * Version: DOM Level 1 Core<<BR>>
		 * URL: [[http://www.w3.org/TR/REC-DOM-Level-1/level-one-core.html#method-removeChild]]
		 *
		 * @param old_child The child we wish to remove
		 *
		 * @return The removed node `old_child`; this should not be freed
		 */
		public override unowned xNode? remove_child (xNode old_child) {
			return this.child_nodes.remove_child (old_child);
		}

		/**
		 * Inserts `new_child` into this document before
		 * `ref_child`, an existing child of this
		 * {@link GXml.xDocument}. A document can only have one
		 * {@link GXml.xElement} child (the root element) and
		 * one {@link GXml.xDocumentType}.
		 *
		 * Version: DOM Level 1 Core<<BR>>
		 * URL: [[http://www.w3.org/TR/DOM-Level-3-Core/core.html#ID-952280727]]
		 *
		 * @param new_child The new node to insert into the document
		 * @param ref_child The existing child of the document that new_child will precede, or %NULL
		 *
		 * @return The newly inserted child; this should not be freed
		 */
		public override unowned xNode? insert_before (xNode new_child, xNode? ref_child) {
			if (new_child.node_type == NodeType.ELEMENT ||
			    new_child.node_type == NodeType.DOCUMENT_TYPE) {
				/* let append_child do it with its error handling, since
				   we don't consider position with libxml2 */
				return this.append_child (new_child);
			} else {
				return this.child_nodes.insert_before (new_child, ref_child);
			}
		}

		/**
		 * Appends new_child to this document, appearing at
		 * the end of its list of children.  A document can
		 * only have one {@link GXml.xElement} child, the root
		 * element, and one {@link GXml.xDocumentType}.
		 *
		 * Version: DOM Level 1 Core<<BR>>
		 * URL: [[http://www.w3.org/TR/REC-DOM-Level-1/level-one-core.html#method-appendChild]]
		 *
		 * @param new_child The child we're appending
		 *
		 * @return The newly added child; this should not be freed
		 */
		public override unowned xNode? append_child (xNode new_child) {
			this.check_wrong_document (new_child);
			this.check_read_only ();

			if (new_child.node_type == NodeType.ELEMENT) {
				if (xmldoc->get_root_element () == null) {
					xmldoc->set_root_element (((xElement)new_child).node);
				} else {
					GXml.exception (DomException.HIERARCHY_REQUEST, "Document already has a root element.  Could not add child element with name '%s'".printf (new_child.node_name));
				}
			} else if (new_child.node_type == NodeType.DOCUMENT_TYPE) {
				if (this.doctype == null) {
					this.doctype = (xDocumentType)new_child;
				} else {
					GXml.exception (DomException.HIERARCHY_REQUEST, "Document already has a doctype.  Could not add new doctype with name '%s'.".printf (((xDocumentType)new_child).name));
				}
			} else {
				return this.child_nodes.append_child (new_child);
			}

			return null;
		}

		/**
		 * {@inheritDoc}
		 */
		public override bool has_child_nodes () {
			return (xmldoc->children != null);
		}

		public unowned xNode copy_node (xNode foreign_node, bool deep = true) {
			Xml.Node *our_copy_xml = ((BackedNode)foreign_node).node->doc_copy (this.xmldoc, deep ? 1 : 0);
			// TODO: do we need to append this to this.new_nodes?  Do we need to append the result to this.nodes_to_free?  Test memory implications
			return this.lookup_node (our_copy_xml); // inducing a GXmlNode
		}
		// GXml.Document interface
		public bool indent { get; set; default = false; }
		public bool ns_top { get; set; default = false; }
		public bool prefix_default_ns { get; set; default = false; }
		public bool backup { get; set; default = true; }
		public GLib.File file { get; set; }
		public virtual GXml.Node root { owned get { return (GXml.Node) document_element.ref (); } }
		public GXml.Node create_text (string str) { return (GXml.Node) this.create_text_node (str); }
		public GXml.Node create_comment (string text) { return create_managed_comment (text); }
		public GXml.Node create_cdata (string text) { return create_cdata_section (text); }
		public GXml.Node create_pi (string target, string data) { return create_processing_instruction (target, data); }
		public bool save (GLib.Cancellable? cancellable = null)
			throws GLib.Error
			requires (file != null)
			requires (file.query_exists ())
		{
			return save_as (file, cancellable);
		}
		public bool save_as (GLib.File f, GLib.Cancellable? cancellable = null) throws GLib.Error
		{
			var ostream = f.replace (null, backup, GLib.FileCreateFlags.NONE, cancellable);
#if DEBUG
			GLib.message ("Saving...");
#endif
			save_to_stream (ostream);
			return true;
		}
	}
}
