/*******************************************************************************
 * Copyright (c) 2001, 2017 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *     Jens Lukowski/Innoopract - initial renaming/restructuring
 *     David Carver (STAR) - bug 296999 - Inefficient use of new String()
 *******************************************************************************/
package org.eclipse.wst.xml.core.internal.document;


import org.eclipse.osgi.util.NLS;
import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocumentRegion;
import org.eclipse.wst.xml.core.internal.provisional.document.IDOMNode;
import org.w3c.dom.DOMException;
import org.w3c.dom.Document;
import org.w3c.dom.DocumentFragment;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;


/**
 * NodeContainer class
 */
public abstract class NodeContainer extends NodeImpl implements Node, NodeList {

	/**
	 */
	private class ChildNodesCache implements NodeList {
		private Node curChild = null;
		private int curIndex = -1;
		private int length = 0;

		ChildNodesCache() {
			initializeCache();
		}

		public int getLength() {
			// atomic
			return this.length;
		}

		private void initializeCache() {
			// note we use the outter objects lockobject
			// (since we are using their "children".
			synchronized (lockObject) {
				for (Node child = firstChild; child != null; child = child.getNextSibling()) {
					this.length++;
				}
			}
		}

		public Node item(int index) {
			synchronized (lockObject) {
				if (this.length == 0)
					return null;
				if (index < 0)
					return null;
				if (index >= this.length)
					return null;

				if (this.curIndex < 0) { // first time
					if (index * 2 >= this.length) { // search from the last
						this.curIndex = this.length - 1;
						this.curChild = lastChild;
					} else { // search from the first
						this.curIndex = 0;
						this.curChild = firstChild;
					}
				}

				if (index == this.curIndex)
					return this.curChild;

				if (index > this.curIndex) {
					while (index > this.curIndex) {
						this.curIndex++;
						this.curChild = this.curChild.getNextSibling();
					}
				} else { // index < this.curIndex
					while (index < this.curIndex) {
						this.curIndex--;
						this.curChild = this.curChild.getPreviousSibling();
					}
				}

				return this.curChild;
			}
		}
	}

	private NodeList childNodesCache = null;

	private boolean fChildEditable = true;
	NodeImpl firstChild = null;
	NodeImpl lastChild = null;

	Object lockObject = new byte[0];

	/**
	 * NodeContainer constructor
	 */
	protected NodeContainer() {
		super();
	}

	/**
	 * NodeContainer constructor
	 * 
	 * @param that
	 *            NodeContainer
	 */
	protected NodeContainer(NodeContainer that) {
		super(that);
	}

	/**
	 * appendChild method
	 * 
	 * @return org.w3c.dom.Node
	 * @param newChild
	 *            org.w3c.dom.Node
	 */
	public Node appendChild(Node newChild) throws DOMException {
		return insertBefore(newChild, null);
	}

	/**
	 * cloneChildNodes method
	 * 
	 * @param container
	 *            org.w3c.dom.Node
	 * @param deep
	 *            boolean
	 */
	protected void cloneChildNodes(Node newParent, boolean deep) {
		if (newParent == null || newParent == this)
			return;
		if (!(newParent instanceof NodeContainer))
			return;

		NodeContainer container = (NodeContainer) newParent;
		container.removeChildNodes();

		for (Node child = getFirstChild(); child != null; child = child.getNextSibling()) {
			Node cloned = child.cloneNode(deep);
			if (cloned != null)
				container.appendChild(cloned);
		}
	}

	/**
	 * getChildNodes method
	 * 
	 * @return org.w3c.dom.NodeList
	 */
	public NodeList getChildNodes() {
		return this;
	}

	/**
	 * getFirstChild method
	 * 
	 * @return org.w3c.dom.Node
	 */
	public Node getFirstChild() {
		return this.firstChild;
	}

	/**
	 * getLastChild method
	 * 
	 * @return org.w3c.dom.Node
	 */
	public Node getLastChild() {
		return this.lastChild;
	}

	/**
	 * getLength method
	 * 
	 * @return int
	 */
	public int getLength() {
		if (this.firstChild == null)
			return 0;
		synchronized (lockObject) {
			if (this.childNodesCache == null)
				this.childNodesCache = new ChildNodesCache();
			return this.childNodesCache.getLength();
		}
	}

	/**
	 */
	public String getSource() {
		StringBuffer buffer = new StringBuffer();

		IStructuredDocumentRegion startStructuredDocumentRegion = getStartStructuredDocumentRegion();
		if (startStructuredDocumentRegion != null) {
			String source = startStructuredDocumentRegion.getText();
			if (source != null)
				buffer.append(source);
		}

		for (NodeImpl child = firstChild; child != null; child = (NodeImpl) child.getNextSibling()) {
			String source = child.getSource();
			if (source != null)
				buffer.append(source);
		}

		IStructuredDocumentRegion endStructuredDocumentRegion = getEndStructuredDocumentRegion();
		if (endStructuredDocumentRegion != null) {
			String source = endStructuredDocumentRegion.getText();
			if (source != null)
				buffer.append(source);
		}

		return buffer.toString();
	}

	/**
	 * hasChildNodes method
	 * 
	 * @return boolean
	 */
	public boolean hasChildNodes() {
		return (this.firstChild != null);
	}

	/**
	 * insertBefore method
	 * 
	 * @return org.w3c.dom.Node
	 * @param newChild
	 *            org.w3c.dom.Node
	 * @param refChild
	 *            org.w3c.dom.Node
	 */
	public Node insertBefore(Node newChild, Node refChild) throws DOMException {
		if (newChild == null)
			return null; // nothing to do
		if (refChild != null && refChild.getParentNode() != this) {
			throw new DOMException(DOMException.NOT_FOUND_ERR, DOMMessages.NOT_FOUND_ERR);
		}
		if (!isChildEditable()) {
			throw new DOMException(DOMException.NO_MODIFICATION_ALLOWED_ERR, DOMMessages.NO_MODIFICATION_ALLOWED_ERR);
		}
		if (newChild == refChild)
			return newChild; // nothing to do
		//new child can not be a parent of this, would cause cycle
		if(isParent(newChild)) {
			throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR, NLS.bind(DOMMessages.HIERARCHY_REQUEST_ERR_WITH_DETAILS, new String[] {newChild.getNodeName(), (refChild != null ? refChild.getNodeName() : "<null>") })); //$NON-NLS-1$
		}

		if (newChild.getNodeType() == DOCUMENT_FRAGMENT_NODE) {
			// insert child nodes instead
			for (Node child = newChild.getFirstChild(); child != null; child = newChild.getFirstChild()) {
				newChild.removeChild(child);
				insertBefore(child, refChild);
			}
			return newChild;
		}
		// synchronized in case another thread is getting item, or length
		synchronized (lockObject) {
			this.childNodesCache = null; // invalidate child nodes cache
		}

		NodeImpl child = (NodeImpl) newChild;
		NodeImpl next = (NodeImpl) refChild;
		NodeImpl prev = null;
		Node oldParent = child.getParentNode();
		if (oldParent != null)
			oldParent.removeChild(child);
		if (next == null) {
			prev = this.lastChild;
			this.lastChild = child;
		} else {
			prev = (NodeImpl) next.getPreviousSibling();
			next.setPreviousSibling(child);
		}
		if (prev == null)
			this.firstChild = child;
		else
			prev.setNextSibling(child);
		child.setPreviousSibling(prev);
		child.setNextSibling(next);
		child.setParentNode(this);
		// make sure having the same owner document
		if (child.getOwnerDocument() == null) {
			if (getNodeType() == DOCUMENT_NODE) {
				child.setOwnerDocument((Document) this);
			} else {
				child.setOwnerDocument(getOwnerDocument());
			}
		}

		notifyChildReplaced(child, null);

		return child;
	}

	public boolean isChildEditable() {
		if (!fChildEditable) {
			DOMModelImpl model = (DOMModelImpl) getModel();
			if (model != null && model.isReparsing()) {
				return true;
			}
		}
		return fChildEditable;
	}

	/**
	 * isContainer method
	 * 
	 * @return boolean
	 */
	public boolean isContainer() {
		return true;
	}

	/**
	 * item method
	 * 
	 * @return org.w3c.dom.Node
	 * @param index
	 *            int
	 */
	public Node item(int index) {
		if (this.firstChild == null)
			return null;
		synchronized (lockObject) {
			if (this.childNodesCache == null)
				this.childNodesCache = new ChildNodesCache();
			return this.childNodesCache.item(index);
		}
	}

	/**
	 * notifyChildReplaced method
	 * 
	 * @param newChild
	 *            org.w3c.dom.Node
	 * @param oldChild
	 *            org.w3c.dom.Node
	 */
	protected void notifyChildReplaced(Node newChild, Node oldChild) {
		DocumentImpl document = (DocumentImpl) getContainerDocument();
		if (document == null)
			return;

		syncChildEditableState(newChild);

		DOMModelImpl model = (DOMModelImpl) document.getModel();
		if (model == null)
			return;
		model.childReplaced(this, newChild, oldChild);
	}

	/**
	 * removeChild method
	 * 
	 * @return org.w3c.dom.Node
	 * @param oldChild
	 *            org.w3c.dom.Node
	 */
	public Node removeChild(Node oldChild) throws DOMException {
		if (oldChild == null)
			return null;
		if (oldChild.getParentNode() != this) {
			throw new DOMException(DOMException.NOT_FOUND_ERR, DOMMessages.NOT_FOUND_ERR);
		}

		if (!isChildEditable()) {
			throw new DOMException(DOMException.NO_MODIFICATION_ALLOWED_ERR, DOMMessages.NO_MODIFICATION_ALLOWED_ERR);
		}

		// synchronized in case another thread is getting item, or length
		synchronized (lockObject) {
			this.childNodesCache = null; // invalidate child nodes cache
		}

		NodeImpl child = (NodeImpl) oldChild;
		NodeImpl prev = (NodeImpl) child.getPreviousSibling();
		NodeImpl next = (NodeImpl) child.getNextSibling();

		child.setEditable(true, true); // clear ReadOnly flags

		if (prev == null)
			this.firstChild = next;
		else
			prev.setNextSibling(next);
		if (next == null)
			this.lastChild = prev;
		else
			next.setPreviousSibling(prev);
		child.setPreviousSibling(null);
		child.setNextSibling(null);
		child.setParentNode(null);

		notifyChildReplaced(null, child);

		return child;
	}

	/**
	 * removeChildNodes method
	 */
	public void removeChildNodes() {
		if (!isChildEditable()) {
			throw new DOMException(DOMException.NO_MODIFICATION_ALLOWED_ERR, DOMMessages.NO_MODIFICATION_ALLOWED_ERR);
		}

		Node nextChild = null;
		for (Node child = getFirstChild(); child != null; child = nextChild) {
			nextChild = child.getNextSibling();
			removeChild(child);
		}
	}

	/**
	 * removeChildNodes method
	 * 
	 * @return org.w3c.dom.DocumentFragment
	 * @param firstChild
	 *            org.w3c.dom.Node
	 * @param lastChild
	 *            org.w3c.dom.Node
	 */
	public DocumentFragment removeChildNodes(Node firstChild, Node lastChild) {
		if (!hasChildNodes())
			return null;
		if (!isChildEditable()) {
			throw new DOMException(DOMException.NO_MODIFICATION_ALLOWED_ERR, DOMMessages.NO_MODIFICATION_ALLOWED_ERR);
		}

		Document document = null;
		if (getNodeType() == DOCUMENT_NODE)
			document = (Document) this;
		else
			document = getOwnerDocument();
		if (document == null)
			return null;
		DocumentFragment fragment = document.createDocumentFragment();
		if (fragment == null)
			return null;

		if (firstChild == null)
			firstChild = getFirstChild();
		if (lastChild == null)
			lastChild = getLastChild();
		Node nextChild = null;
		for (Node child = firstChild; child != null; child = nextChild) {
			nextChild = child.getNextSibling();
			removeChild(child);
			fragment.appendChild(child);
			if (child == lastChild)
				break;
		}

		return fragment;
	}

	/**
	 * replaceChild method
	 * 
	 * @return org.w3c.dom.Node
	 * @param newChild
	 *            org.w3c.dom.Node
	 * @param oldChild
	 *            org.w3c.dom.Node
	 */
	public Node replaceChild(Node newChild, Node oldChild) throws DOMException {
		if (!isChildEditable()) {
			throw new DOMException(DOMException.NO_MODIFICATION_ALLOWED_ERR, DOMMessages.NO_MODIFICATION_ALLOWED_ERR);
		}

		if (oldChild == null || oldChild == newChild)
			return newChild;
		if (newChild != null)
			insertBefore(newChild, oldChild);
		return removeChild(oldChild);
	}

	public void setChildEditable(boolean editable) {
		if (fChildEditable == editable) {
			return;
		}

		ReadOnlyController roc = ReadOnlyController.getInstance();
		Node node;
		if (editable) {
			for (node = getFirstChild(); node != null; node = node.getNextSibling()) {
				roc.unlockNode((IDOMNode) node);
			}
		} else {
			for (node = getFirstChild(); node != null; node = node.getNextSibling()) {
				roc.lockNode((IDOMNode) node);
			}
		}

		fChildEditable = editable;
		notifyEditableChanged();
	}

	protected void syncChildEditableState(Node child) {
		ReadOnlyController roc = ReadOnlyController.getInstance();
		if (fChildEditable) {
			roc.unlockNode((NodeImpl) child);
		} else {
			roc.lockNode((NodeImpl) child);
		}
	}

	/**
	 * <p>Checks to see if the given <code>Node</code> is a parent of <code>this</code> node</p>
	 * 
	 * @param possibleParent the possible parent <code>Node</code> of <code>this</code> node
	 * @return <code>true</code> if <code>possibleParent</code> is the parent of <code>this</code>,
	 * <code>false</code> otherwise.
	 */
	private boolean isParent(Node possibleParent) {
		Node parent = this.getParentNode();
		while(parent != null && parent != possibleParent) {
			parent = parent.getParentNode();
		}

		return parent == possibleParent;
	}
}
