/*******************************************************************************
 * 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
 *    
 *     Valentin Baciu - https://bugs.eclipse.org/bugs/show_bug.cgi?id=139552
 *     
 *     Balazs Banfai: Bug 154737 getUserData/setUserData support for Node
 *     https://bugs.eclipse.org/bugs/show_bug.cgi?id=154737
 *     David Carver (STAR) - bug 296999 - Inefficient use of new String()
 *******************************************************************************/
package org.eclipse.wst.xml.core.internal.document;



import java.util.Iterator;
import java.util.Stack;

import org.eclipse.osgi.util.NLS;
import org.eclipse.wst.sse.core.internal.ltk.parser.RegionParser;
import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocumentRegion;
import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegion;
import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegionList;
import org.eclipse.wst.xml.core.internal.commentelement.CommentElementAdapter;
import org.eclipse.wst.xml.core.internal.contentmodel.CMAttributeDeclaration;
import org.eclipse.wst.xml.core.internal.contentmodel.CMElementDeclaration;
import org.eclipse.wst.xml.core.internal.contentmodel.CMNamedNodeMap;
import org.eclipse.wst.xml.core.internal.contentmodel.CMNode;
import org.eclipse.wst.xml.core.internal.contentmodel.modelquery.ModelQuery;
import org.eclipse.wst.xml.core.internal.modelquery.ModelQueryUtil;
import org.eclipse.wst.xml.core.internal.parser.XMLSourceParser;
import org.eclipse.wst.xml.core.internal.provisional.IXMLNamespace;
import org.eclipse.wst.xml.core.internal.provisional.document.IDOMElement;
import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel;
import org.eclipse.wst.xml.core.internal.provisional.document.IDOMNode;
import org.eclipse.wst.xml.core.internal.regions.DOMRegionContext;
import org.w3c.dom.Attr;
import org.w3c.dom.DOMException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.UserDataHandler;
import org.w3c.dom.traversal.NodeFilter;
import org.w3c.dom.traversal.NodeIterator;


/**
 * ElementImpl class
 */
public class ElementImpl extends NodeContainer implements IDOMElement {

	private class Attributes implements NamedNodeMap {
		Attributes() {
			super();
		}

		public int getLength() {
			if (attrNodes == null)
				return 0;
			return attrNodes.getLength();
		}

		public Node getNamedItem(String name) {
			return getAttributeNode(name);
		}

		public Node getNamedItemNS(String uri, String name) {
			return getAttributeNodeNS(uri, name);
		}

		public Node item(int index) {
			if (attrNodes == null)
				return null;
			if (index >= attrNodes.getLength())
				return null;
			return attrNodes.item(index);
		}

		public Node removeNamedItem(String name) throws DOMException {
			return removeAttributeNode(name);
		}

		public Node removeNamedItemNS(String uri, String name) throws DOMException {
			return removeAttributeNodeNS(uri, name);
		}

		public Node setNamedItem(Node arg) throws DOMException {
			return setAttributeNode((AttrImpl) arg);
		}

		public Node setNamedItemNS(Node arg) throws DOMException {
			return setAttributeNodeNS((AttrImpl) arg);
		}
	}

//	private static final char[] XMLNS_PREFIX = IXMLNamespace.XMLNS_PREFIX.toCharArray();
	private static final byte FLAG_COMMENT = 0x1;
	private static final byte FLAG_EMPTY = 0x2;
	private static final byte FLAG_JSP = 0x4;
	
	private byte fTagFlags = 0;
	
	NodeListImpl attrNodes = null;
	private IStructuredDocumentRegion endStructuredDocumentRegion = null;
	
	private char[] fTagName = null;

	private char[] fNamespaceURI = null;
	
	private Stack fDefaultValueLookups = null;

	/** Lock for controlling access to the default value stack */
	private Object STACK_LOCK = new byte[0];

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

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

		if (that != null) {
			this.fTagName = that.fTagName;
			this.fTagFlags = that.fTagFlags;

			// clone attributes
			that.cloneAttributes(this);
		}
	}

	/**
	 * addEndTag method
	 * 
	 * @param end
	 *            org.w3c.dom.Element
	 */
	protected void addEndTag(Element endTag) {
		if (endTag == null)
			return;
		if (hasEndTag())
			return;
		ElementImpl end = (ElementImpl) endTag;

		// move the end flat node from the end tag
		IStructuredDocumentRegion flatNode = end.getEndStructuredDocumentRegion();
		if (flatNode == null)
			return;
		end.setEndStructuredDocumentRegion(null);
		setEndStructuredDocumentRegion(flatNode);
	}

	/**
	 * appendAttibuteNode method
	 * 
	 * @return org.w3c.dom.Attr
	 * @param newAttr
	 *            org.w3c.dom.Attr
	 */
	public Attr appendAttributeNode(Attr newAttr) {
		if (newAttr == null)
			return null;
		AttrImpl attr = (AttrImpl) newAttr;
		if (attr.getOwnerElement() != null)
			return null;

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

		if (this.attrNodes == null)
			this.attrNodes = new NodeListImpl();
		this.attrNodes.appendNode(attr);
		attr.setOwnerElement(this);

		notifyAttrReplaced(attr, null);
		return attr;
	}

	/**
	 * cloneAttributes method
	 * 
	 * @param newOwner
	 *            org.w3c.dom.Element
	 */
	protected void cloneAttributes(Element newOwner) {
		if (newOwner == null || newOwner == this)
			return;

		ElementImpl element = (ElementImpl) newOwner;
		element.removeAttributes();

		if (this.attrNodes == null)
			return;

		int length = this.attrNodes.getLength();
		for (int i = 0; i < length; i++) {
			Node node = this.attrNodes.item(i);
			if (node == null)
				continue;
			Attr cloned = (Attr) node.cloneNode(false);
			if (cloned != null)
				element.appendAttributeNode(cloned);
		}
	}

	/**
	 * cloneNode method
	 * 
	 * @return org.w3c.dom.Node
	 * @param deep
	 *            boolean
	 */
	public Node cloneNode(boolean deep) {
		ElementImpl cloned = newInstance();
		if (deep)
			cloneChildNodes(cloned, deep);
		
		notifyUserDataHandlers(UserDataHandler.NODE_CLONED, cloned);
		return cloned;
	}

	protected ElementImpl newInstance() {
		return new ElementImpl(this);
	}

	/**
	 * getAttribute method
	 * 
	 * @return java.lang.String
	 * @param name
	 *            java.lang.String
	 */
	public String getAttribute(String name) {
		Attr attr = getAttributeNode(name);
		// In the absence of the attribute, get the default value
		if (attr == null) {
			final String defaultValue = getDefaultValue(name, NodeImpl.EMPTY_STRING);
			return defaultValue != null ? defaultValue : NodeImpl.EMPTY_STRING ;
		}
		return attr.getValue();
	}

	/* (non-Javadoc)
	 * @see org.w3c.dom.Element#getAttributeNode(java.lang.String)
	 */
	public Attr getAttributeNode(String name) {
		if (name == null)
			return null; // invalid parameter
		if (this.attrNodes == null)
			return null; // no attribute

		return findAttributeNode(name);
	}

	/**
	 * Finds an attribute node in this element's attribute nodelist
	 * @param name the name of the attribute to find
	 * @return The {@link Attr} whose name matches <code>name</code>. Returns null if an attribute by <code>name</code>
	 * could not be found.
	 */
	private Attr findAttributeNode(String name) {
		if (attrNodes == null)
			return null; // no attribute

		int length = attrNodes.getLength();
		char[] nameChars = name.toCharArray();
		for (int i = 0; i < length; i++) {
			AttrImpl attr = (AttrImpl) attrNodes.item(i);
			if (attr == null)
				continue;
			if (attr.matchName(nameChars))
				return attr; // found
		}
		return null;
	}

	/* (non-Javadoc)
	 * @see org.w3c.dom.Element#getAttributeNodeNS(java.lang.String, java.lang.String)
	 */
	public Attr getAttributeNodeNS(String uri, String name) {
		if (name == null)
			return null; // invalid parameter
		if (this.attrNodes == null)
			return null; // no attribute

		int length = this.attrNodes.getLength();
		for (int i = 0; i < length; i++) {
			AttrImpl attr = (AttrImpl) this.attrNodes.item(i);
			if (attr == null)
				continue;
			String localName = attr.getLocalName();
			if (localName == null || !localName.equals(name))
				continue;
			String nsURI = attr.getNamespaceURI();
			if (uri == null) {
				if (nsURI != null)
					continue;
			}
			else {
				if (nsURI == null || !nsURI.equals(uri))
					continue;
			}

			// found
			return attr;
		}

		return null; // not found
	}

	/* (non-Javadoc)
	 * @see org.w3c.dom.Element#getAttributeNS(java.lang.String, java.lang.String)
	 */
	public String getAttributeNS(String uri, String name) {
		Attr attr = getAttributeNodeNS(uri, name);
		// In the absence of the attribute, get the default value
		if (attr == null) {
			final String defaultValue = getDefaultValue(name, NodeImpl.EMPTY_STRING);
			return defaultValue != null ? defaultValue : NodeImpl.EMPTY_STRING;
		}
		return attr.getValue();
	}

	/**
	 * getAttributes method
	 * 
	 * @return org.w3c.dom.NamedNodeMap
	 */
	public NamedNodeMap getAttributes() {
		return new Attributes();
	}

	/**
	 */
	protected CMElementDeclaration getDeclaration() {
		Document document = getOwnerDocument();
		if (document == null)
			return null;
		ModelQuery modelQuery = ModelQueryUtil.getModelQuery(document);
		if (modelQuery == null)
			return null;
		return modelQuery.getCMElementDeclaration(this);
	}

	/**
	 * Get the implied default value for attribute <code>name</code>
	 * 
	 * @param name
	 * @return returns the default value for attribute <code>name</code> if it
	 *         is found in the content model, <code>unknownDefault</code> otherwise
	 */
	private String getDefaultValue(String name, String unknownDefault) {
		synchronized (STACK_LOCK) {
			if (fDefaultValueLookups != null && fDefaultValueLookups.contains(name))
				return null;
			try {
				if (fDefaultValueLookups == null)
					fDefaultValueLookups = new Stack();
				fDefaultValueLookups.push(name);
				CMNamedNodeMap map = ((DocumentImpl) getOwnerDocument()).getCMAttributes(this);
				if (map != null) {
					CMNode attribute = map.getNamedItem(name);
					if (attribute instanceof CMAttributeDeclaration)
						return ((CMAttributeDeclaration) attribute).getAttrType().getImpliedValue();
				}
				return unknownDefault;
			}
			finally {
				fDefaultValueLookups.pop();
			}
		}
	}

	/**
	 * getElementsByTagName method
	 * 
	 * @return org.w3c.dom.NodeList
	 * @param tagName
	 *            java.lang.String
	 */
	public NodeList getElementsByTagName(String tagName) {
		if (tagName == null)
			return new NodeListImpl();

		DocumentImpl document = (DocumentImpl) getOwnerDocument();
		if (document == null)
			return new NodeListImpl();
		NodeIterator it = document.createNodeIterator(this, NodeFilter.SHOW_ALL, null, false);
		if (it == null)
			return new NodeListImpl();
		NodeListImpl elements = new NodeListImpl();

		if (tagName.length() == 1 && tagName.charAt(0) == '*') {
			tagName = null; // do not care
		}

		it.nextNode(); // skip the first node since it is the root from createNodeIterator
		for (Node node = it.nextNode(); node != null; node = it.nextNode()) {
			if (node.getNodeType() != ELEMENT_NODE)
				continue;
			if (tagName != null) {
				ElementImpl element = (ElementImpl) node;
				if (!element.matchTagName(tagName))
					continue;
			}
			elements.appendNode(node);
		}

		return elements;
	}

	/**
	 */
	public NodeList getElementsByTagNameNS(String uri, String tagName) {
		if (tagName == null)
			return new NodeListImpl();

		DocumentImpl document = (DocumentImpl) getOwnerDocument();
		if (document == null)
			return new NodeListImpl();
		NodeIterator it = document.createNodeIterator(this, NodeFilter.SHOW_ALL, null, false);
		if (it == null)
			return new NodeListImpl();
		NodeListImpl elements = new NodeListImpl();

		if (uri != null && uri.length() == 1 && uri.charAt(0) == '*') {
			uri = null; // do not care
		}
		if (tagName.length() == 1 && tagName.charAt(0) == '*') {
			tagName = null; // do not care
		}

		it.nextNode(); // skip the first node since it is the root from createNodeIterator
		for (Node node = it.nextNode(); node != null; node = it.nextNode()) {
			if (node.getNodeType() != ELEMENT_NODE)
				continue;
			ElementImpl element = (ElementImpl) node;
			if (tagName != null) {
				String localName = element.getLocalName();
				if (localName == null || !localName.equals(tagName))
					continue;
			}
			if (uri != null) {
				String nsURI = element.getNamespaceURI();
				if (nsURI == null || !nsURI.equals(uri))
					continue;
			}
			elements.appendNode(element);
		}

		return elements;
	}

	/**
	 * getEndOffset method
	 * 
	 * @return int
	 */
	public int getEndOffset() {
		if (this.endStructuredDocumentRegion != null)
			return this.endStructuredDocumentRegion.getEnd();
		return super.getEndOffset();
	}

	/**
	 * getEndStartOffset method
	 * 
	 * @return int
	 */
	public int getEndStartOffset() {
		if (this.endStructuredDocumentRegion != null)
			return this.endStructuredDocumentRegion.getStart();
		return super.getEndOffset();
	}

	/**
	 * getEndStructuredDocumentRegion method
	 * 
	 */
	public IStructuredDocumentRegion getEndStructuredDocumentRegion() {
		return this.endStructuredDocumentRegion;
	}

	public String getEndTagName() {
		if (this.endStructuredDocumentRegion == null)
			return null;

		ITextRegionList regions = this.endStructuredDocumentRegion.getRegions();
		if (regions == null)
			return null;
		Iterator e = regions.iterator();
		while (e.hasNext()) {
			ITextRegion region = (ITextRegion) e.next();
			String regionType = region.getType();
			if (regionType == DOMRegionContext.XML_TAG_NAME || isNestedEndTag(regionType)) {
				return this.endStructuredDocumentRegion.getText(region);
			}
		}

		return null;
	}

	protected boolean isNestedEndTag(String regionType) {
		boolean result = false;
		return result;
	}

	/**
	 * getFirstStructuredDocumentRegion method
	 * 
	 */
	public IStructuredDocumentRegion getFirstStructuredDocumentRegion() {
		IStructuredDocumentRegion flatNode = getStructuredDocumentRegion();
		if (flatNode != null)
			return StructuredDocumentRegionUtil.getStructuredDocumentRegion(flatNode);
		return StructuredDocumentRegionUtil.getStructuredDocumentRegion(this.endStructuredDocumentRegion);
	}

	/**
	 * getLastStructuredDocumentRegion method
	 * 
	 */
	public IStructuredDocumentRegion getLastStructuredDocumentRegion() {
		if (this.endStructuredDocumentRegion != null)
			return StructuredDocumentRegionUtil.getStructuredDocumentRegion(this.endStructuredDocumentRegion);
		return StructuredDocumentRegionUtil.getStructuredDocumentRegion(getStructuredDocumentRegion());
	}

	/**
	 */
	public String getLocalName() {
		if (this.fTagName == null)
			return null;
		int index = CharOperation.indexOf(this.fTagName, ':');
		if (index < 0)
			return new String(this.fTagName);
		return new String(this.fTagName, index + 1, this.fTagName.length - index - 1);
	}

	public String getNamespaceURI() {
		if (this.fNamespaceURI != null)
			return new String(this.fNamespaceURI);

		String nsAttrName = null;
		String prefix = getPrefix();
		if (prefix != null && prefix.length() > 0) {
			nsAttrName = IXMLNamespace.XMLNS_PREFIX + prefix;
		}
		else {
			nsAttrName = IXMLNamespace.XMLNS;
		}

		for (Node node = this; node != null; node = node.getParentNode()) {
			if (node.getNodeType() != ELEMENT_NODE)
				break;
			Element element = (Element) node;
			Attr attr = element.getAttributeNode(nsAttrName);
			if (attr != null)
				return attr.getValue();
		}

		return null;
	}

	/**
	 * getNodeName method
	 * 
	 * @return java.lang.String
	 */
	public String getNodeName() {
		return getTagName();
	}

	/**
	 * getNodeType method
	 * 
	 * @return short
	 */
	public short getNodeType() {
		return ELEMENT_NODE;
	}

	/**
	 */
	public String getPrefix() {
		if (this.fTagName == null)
			return null;
		int index = CharOperation.indexOf(this.fTagName, ':');
		if (index <= 0)
			return null;
		// exclude JSP tag in name
		if (this.fTagName[0] == '<')
			return null;
		return new String(this.fTagName, 0, index);
	}

	/**
	 * getStartEndOffset method
	 * 
	 * @return int
	 */
	public int getStartEndOffset() {
		IStructuredDocumentRegion flatNode = getStructuredDocumentRegion();
		if (flatNode != null)
			return flatNode.getEnd();
		return super.getStartOffset();
	}

	/**
	 * getStartOffset method
	 * 
	 * @return int
	 */
	public int getStartOffset() {
		if (getStartStructuredDocumentRegion() == null && this.endStructuredDocumentRegion != null && !hasChildNodes()) {
			return this.endStructuredDocumentRegion.getStart();
		}
		return super.getStartOffset();
	}

	/**
	 * getStartStructuredDocumentRegion method
	 * 
	 */
	public IStructuredDocumentRegion getStartStructuredDocumentRegion() {
		return getStructuredDocumentRegion();
	}

	/**
	 * getTagName method
	 * 
	 * @return java.lang.String
	 */
	public String getTagName() {
		if (this.fTagName == null)
			return NodeImpl.EMPTY_STRING;
		return new String(fTagName);
	}

	/**
	 */
	public boolean hasAttribute(String name) {
		return (getAttributeNode(name) != null);
	}

	/**
	 */
	public boolean hasAttributeNS(String uri, String name) {
		return (getAttributeNodeNS(uri, name) != null);
	}

	/**
	 */
	public boolean hasAttributes() {
		return (this.attrNodes != null && this.attrNodes.getLength() > 0);
	}

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

	/**
	 */
	protected final boolean hasPrefix() {
		if (this.fTagName == null || this.fTagName.length == 0)
			return false;
		return CharOperation.indexOf(this.fTagName, ':') > 0 && this.fTagName[0] != '<';
	}

	/**
	 * hasStartTag method
	 * 
	 * @return boolean
	 */
	public boolean hasStartTag() {
		return (getStructuredDocumentRegion() != null);
	}

	/**
	 */
	protected final boolean ignoreCase() {
		DocumentImpl document = (DocumentImpl) getOwnerDocument();
		if (document != null && document.ignoreCase()) {
			// even in case insensitive document, if having prefix, it's case
			// sensitive tag
			return !hasPrefix();
		}
		return false;
	}

	/**
	 */
	protected Attr insertAttributeNode(Attr newAttr, int index) {
		if (newAttr == null)
			return null;
		AttrImpl attr = (AttrImpl) newAttr;
		if (attr.getOwnerElement() != null)
			return null;

		if (this.attrNodes == null)
			this.attrNodes = new NodeListImpl();
		this.attrNodes.insertNode(attr, index);
		attr.setOwnerElement(this);

		notifyAttrReplaced(attr, null);
		return attr;
	}

	/**
	 * 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 {
		// should throw DOMException instead of return null?
		if (newChild == null)
			return null;
		if (!isContainer()) { // never be container
			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() != TEXT_NODE && newChild.getNodeType() != CDATA_SECTION_NODE) {
			if (isJSPContainer() || isCDATAContainer()) { // accepts only
				// Text
				// child
				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$
			}
		}
		return super.insertBefore(newChild, refChild);
	}

	/**
	 */
	protected boolean isCDATAContainer() {
		// use BlockMaker instead of CMElementDeclaration
		// because <style> and <script> in XHTML is not CDATA content type
		IDOMModel model = getModel();
		if (model == null)
			return false; // error
		IStructuredDocument structuredDocument = model.getStructuredDocument();
		if (structuredDocument == null || fTagName == null)
			return false; // eror
		RegionParser parser = structuredDocument.getParser();
		if (parser == null || !(parser instanceof XMLSourceParser))
			return false;
		return (((XMLSourceParser) parser).getBlockMarker(new String(this.fTagName)) != null);
		/*
		 * CMElementDeclaration decl = getDeclaration(); if (decl == null)
		 * return false; if (decl instanceof CMNodeWrapper) { decl =
		 * (CMElementDeclaration)((CMNodeWrapper)decl).getOriginNode(); if
		 * (decl == null) return false; } if (decl instanceof
		 * TLDElementDeclaration) { String content =
		 * ((TLDElementDeclaration)decl).getBodycontent(); if (content ==
		 * null) return false; return
		 * content.equals(JSP11TLDNames.CONTENT_TAGDEPENDENT); } if
		 * (!isGlobalTag()) return false; return (decl.getContentType() ==
		 * CMElementDeclaration.CDATA);
		 */
	}

	/**
	 */
	public boolean isClosed() {
		IStructuredDocumentRegion flatNode = null;
		if (isEmptyTag() || !isContainer()) {
			flatNode = getStructuredDocumentRegion();
			if (flatNode == null)
				return true; // will be generated
		}
		else {
			flatNode = getEndStructuredDocumentRegion();
			if (flatNode == null)
				return false; // must be generated
		}
		String regionType = StructuredDocumentRegionUtil.getLastRegionType(flatNode);
		if (isCommentTag()) {
			return (isNestedClosedComment(regionType) || regionType == DOMRegionContext.XML_COMMENT_CLOSE);
		}
		if (isJSPTag()) {
			return isNestedClosed(regionType);
		}
		return (regionType == DOMRegionContext.XML_TAG_CLOSE || regionType == DOMRegionContext.XML_EMPTY_TAG_CLOSE || regionType == DOMRegionContext.XML_DECLARATION_CLOSE);
	}

	protected boolean isNestedClosed(String regionType) {
		boolean result = false;
		return result;
	}

	protected boolean isNestedClosedComment(String regionType) {
		boolean result = false;
		return result;
	}

	/**
	 */
	public final boolean isCommentTag() {
		return (fTagFlags & FLAG_COMMENT) != 0;
	}

	/**
	 * isContainer method
	 * 
	 * @return boolean
	 */
	public boolean isContainer() {
		if (isCommentTag()) {
			CommentElementAdapter adapter = (CommentElementAdapter) getAdapterFor(CommentElementAdapter.class);
			if (adapter != null) {
				return (adapter.isContainer());
			}
			return (getDeclaration() == null);
		}
		if (isJSPTag()) {
			// exclude JSP directive
			return (matchTagName(JSPTag.JSP_SCRIPTLET) || matchTagName(JSPTag.JSP_DECLARATION) || matchTagName(JSPTag.JSP_EXPRESSION));
		}
		if (!isXMLTag()) { // non-XML tag
			CMElementDeclaration decl = getDeclaration();
			if (decl == null)
				return true; // undefined tag
			return (decl.getContentType() != CMElementDeclaration.EMPTY);
		}
		return true;
	}

	/**
	 * isEmptyTag method
	 * 
	 * @return boolean
	 */
	public boolean isEmptyTag() {
		if (isJSPTag())
			return false;
		if (isCommentTag())
			return false;
		if (!isXMLTag())
			return false;
		return (fTagFlags & FLAG_EMPTY) != 0;
	}

	/**
	 */
	public boolean isEndTag() {
		return (hasEndTag() && !hasStartTag() && !hasChildNodes());
	}

	/**
	 */
	public boolean isGlobalTag() {
		return !hasPrefix();
	}

	/**
	 */
	public boolean isImplicitTag() {
		if (hasStartTag() || hasEndTag())
			return false;
		// make sure this is in the document tree
		// because if not in the document tree, no tags are generated yet
		return (getContainerDocument() != null);
	}

	/**
	 */
	public boolean isJSPContainer() {
		return (isJSPTag() && !isCommentTag() && isContainer());
	}

	/**
	 * isJSPTag method
	 * 
	 * @return boolean
	 */
	public final boolean isJSPTag() {
		return (fTagFlags & FLAG_JSP) != 0;
	}

	/**
	 */
	public boolean isStartTagClosed() {
		IStructuredDocumentRegion flatNode = getStructuredDocumentRegion();
		if (flatNode == null)
			return true; // will be generated
		String regionType = StructuredDocumentRegionUtil.getLastRegionType(flatNode);
		if (isCommentTag()) {
			return (isNestedClosedComment(regionType) || regionType == DOMRegionContext.XML_COMMENT_CLOSE);
		}
		if (isJSPTag()) {
			if (isContainer())
				return true; // start tag always has a single region
			return isClosedNestedDirective(regionType);
		}
		return (regionType == DOMRegionContext.XML_TAG_CLOSE || regionType == DOMRegionContext.XML_EMPTY_TAG_CLOSE || regionType == DOMRegionContext.XML_DECLARATION_CLOSE);
	}

	protected boolean isClosedNestedDirective(String regionType) {
		boolean result = false;
		return result;
	}

	/**
	 */
	public final boolean isXMLTag() {
		if (isJSPTag())
			return false;
		if (isCommentTag())
			return false;
		DocumentImpl document = (DocumentImpl) getOwnerDocument();
		if (document != null && !document.isXMLType()) {
			// even in non-XML document, if having prefix, it's XML tag
			return hasPrefix();
		}
		return true;
	}

	/**
	 */
	protected boolean matchEndTag(Element element) {
		if (element == null)
			return false;
		ElementImpl impl = (ElementImpl) element;
		if (isJSPTag() && !isCommentTag()) {
			return (impl.isJSPTag() && !impl.isCommentTag());
		}
		return matchTagName(element.getTagName());
	}

	/**
	 * matchTagName method
	 * 
	 * @return boolean
	 * @param tagName
	 *            java.lang.String
	 */
	public boolean matchTagName(String tagName) {
		if (tagName == null)
			return (this.fTagName == null);
		if (this.fTagName == null)
			return false;
		if (this.fTagName.length != tagName.length())
			return false;
		String stringName = new String(this.fTagName);
		if (!ignoreCase())
			return stringName.equals(tagName);
		return stringName.equalsIgnoreCase(tagName);
	}

	/**
	 * notifyAttrReplaced method
	 * 
	 * @param newAttr
	 *            org.w3c.dom.Attr
	 * @param oldAttr
	 *            org.w3c.dom.Attr
	 */
	protected void notifyAttrReplaced(Attr newAttr, Attr oldAttr) {
		DocumentImpl document = (DocumentImpl) getContainerDocument();
		if (document == null)
			return;
		DOMModelImpl model = (DOMModelImpl) document.getModel();
		if (model == null)
			return;
		model.attrReplaced(this, newAttr, oldAttr);
	}

	/**
	 * notifyValueChanged method
	 */
	public void notifyEndTagChanged() {
		DocumentImpl document = (DocumentImpl) getContainerDocument();
		if (document == null)
			return;
		DOMModelImpl model = (DOMModelImpl) document.getModel();
		if (model == null)
			return;
		model.endTagChanged(this);
	}

	/**
	 */
	public void notifyStartTagChanged() {
		DocumentImpl document = (DocumentImpl) getContainerDocument();
		if (document == null)
			return;
		DOMModelImpl model = (DOMModelImpl) document.getModel();
		if (model == null)
			return;
		model.startTagChanged(this);
	}

	/**
	 */
	public boolean preferEmptyTag() {
		if (hasChildNodes())
			return false;
		if (isJSPTag())
			return false;
		if (isCommentTag())
			return false;
		if (!isXMLTag())
			return false;
		CMElementDeclaration decl = getDeclaration();
		if (decl == null)
			return false;
		return (decl.getContentType() == CMElementDeclaration.EMPTY);
	}

	/* (non-Javadoc)
	 * @see org.w3c.dom.Element#removeAttribute(java.lang.String)
	 */
	public void removeAttribute(String name) throws DOMException {
		removeAttributeNode(name, false);
	}

	/**
	 * removeAttributeNode method
	 * 
	 * @return org.w3c.dom.Attr
	 * @param oldAttr
	 *            org.w3c.dom.Attr
	 */
	public Attr removeAttributeNode(Attr oldAttr) throws DOMException {
		if (oldAttr == null)
			return null; // invalid parameter

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

		if (this.attrNodes == null) { // no attribute
			throw new DOMException(DOMException.NOT_FOUND_ERR, DOMMessages.NOT_FOUND_ERR);
		}

		int length = this.attrNodes.getLength();
		for (int i = 0; i < length; i++) {
			AttrImpl attr = (AttrImpl) this.attrNodes.item(i);
			if (attr != oldAttr)
				continue;

			// found
			this.attrNodes.removeNode(i);
			attr.setOwnerElement(null);

			notifyAttrReplaced(null, attr);
			return attr;
		}

		// not found
		throw new DOMException(DOMException.NOT_FOUND_ERR, DOMMessages.NOT_FOUND_ERR);
	}

	/**
	 * @param name
	 * @return
	 */
	public Attr removeAttributeNode(String name) {
		return removeAttributeNode(name, true);
	}
	
	private  Attr removeAttributeNode(String name, boolean exceptionOnNotFound) {
		if (name == null)
			return null; // invalid parameter
		if (this.attrNodes == null)
			return null; // no attribute

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

		int length = this.attrNodes.getLength();
		for (int i = 0; i < length; i++) {
			AttrImpl attr = (AttrImpl) this.attrNodes.item(i);
			if (attr == null)
				continue;
			if (!attr.matchName(name))
				continue;

			// found
			this.attrNodes.removeNode(i);
			attr.setOwnerElement(null);

			notifyAttrReplaced(null, attr);
			return attr;
		}

		if (exceptionOnNotFound)
			throw new DOMException(DOMException.NOT_FOUND_ERR, DOMMessages.NOT_FOUND_ERR);
		return null; // not found
	}

	/**
	 */
	public Attr removeAttributeNodeNS(String uri, String name) {
		if (name == null)
			return null; // invalid parameter
		if (this.attrNodes == null)
			return null; // no attribute

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

		int length = this.attrNodes.getLength();
		for (int i = 0; i < length; i++) {
			AttrImpl attr = (AttrImpl) this.attrNodes.item(i);
			if (attr == null)
				continue;
			String localName = attr.getLocalName();
			if (localName == null || !localName.equals(name))
				continue;
			String nsURI = attr.getNamespaceURI();
			if (uri == null) {
				if (nsURI != null)
					continue;
			}
			else {
				if (nsURI == null || !nsURI.equals(uri))
					continue;
			}

			// found
			this.attrNodes.removeNode(i);
			attr.setOwnerElement(null);

			notifyAttrReplaced(null, attr);
			return attr;
		}

		return null; // not found
	}

	/**
	 */
	public void removeAttributeNS(String uri, String name) throws DOMException {
		removeAttributeNodeNS(uri, name);
	}

	/**
	 * removeAttributes method
	 */
	public void removeAttributes() {
		if (this.attrNodes == null)
			return;

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

		int length = this.attrNodes.getLength();
		for (int i = 0; i < length; i++) {
			AttrImpl attr = (AttrImpl) this.attrNodes.item(i);
			if (attr != null) {
				attr.setOwnerElement(null);
				notifyAttrReplaced(null, attr);
			}
		}

		this.attrNodes = null;
	}

	/**
	 * removeEndTag method
	 * 
	 * @return org.w3c.dom.Element
	 */
	protected Element removeEndTag() {
		if (!hasEndTag())
			return null;
		NodeListImpl attrNodes = this.attrNodes;
		this.attrNodes = null; // not to copy attributes
		ElementImpl end = (ElementImpl) cloneNode(false);
		this.attrNodes = attrNodes;
		if (end == null)
			return null;

		// move the end flat node to the end tag
		IStructuredDocumentRegion flatNode = getEndStructuredDocumentRegion();
		if (flatNode == null)
			return null;
		setEndStructuredDocumentRegion(null);
		end.setEndStructuredDocumentRegion(flatNode);
		return end;
	}

	/**
	 */
	protected void removeStartTag() {
		removeAttributes();
	}

	/**
	 * Resets attribute values from IStructuredDocumentRegion.
	 */
	void resetStructuredDocumentRegions() {
		if (this.attrNodes != null) {
			int length = this.attrNodes.getLength();
			for (int i = 0; i < length; i++) {
				AttrImpl attr = (AttrImpl) this.attrNodes.item(i);
				if (attr == null)
					continue;
				attr.resetRegions();
			}
		}

		super.resetStructuredDocumentRegions(); // for children

		this.endStructuredDocumentRegion = null;
	}

	/**
	 * setAttribute method
	 * 
	 * @param name
	 *            java.lang.String
	 * @param value
	 *            java.lang.String
	 */
	public void setAttribute(String name, String value) throws DOMException {
		if (name == null)
			return;

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

		Attr attr = findAttributeNode(name);
		if (attr != null) {
			attr.setValue(value); // change value
			return;
		}

		// new attribute
		Document doc = getOwnerDocument();
		if (doc == null)
			return;
		attr = doc.createAttribute(name);
		if (attr == null)
			return;
		attr.setValue(value);
		appendAttributeNode(attr);
	}

	/* (non-Javadoc)
	 * @see org.w3c.dom.Element#setAttributeNode(org.w3c.dom.Attr)
	 */
	public Attr setAttributeNode(Attr newAttr) throws DOMException {
		if (newAttr == null)
			return null; // nothing to do

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

		AttrImpl attr = (AttrImpl) newAttr;
		Element owner = attr.getOwnerElement();
		if (owner != null) {
			if (owner == this)
				return null; // nothing to do
			throw new DOMException(DOMException.INUSE_ATTRIBUTE_ERR, DOMMessages.INUSE_ATTRIBUTE_ERR);
		}

		Attr oldAttr = removeAttributeNode(newAttr.getName(), false);
		appendAttributeNode(attr);
		return oldAttr;
	}

	/**
	 */
	public Attr setAttributeNodeNS(Attr newAttr) throws DOMException {
		if (newAttr == null)
			return null; // nothing to do

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

		AttrImpl attr = (AttrImpl) newAttr;
		Element owner = attr.getOwnerElement();
		if (owner != null) {
			if (owner == this)
				return null; // nothing to do
			throw new DOMException(DOMException.INUSE_ATTRIBUTE_ERR, DOMMessages.INUSE_ATTRIBUTE_ERR);
		}

		String name = newAttr.getLocalName();
		String uri = newAttr.getNamespaceURI();
		Attr oldAttr = removeAttributeNodeNS(uri, name);
		appendAttributeNode(attr);
		return oldAttr;
	}

	/**
	 * ISSUE: we should check for and throw NAMESPACE_ERR, according to spec. 
	 */
	public void setAttributeNS(String uri, String qualifiedName, String value) throws DOMException {
		if (qualifiedName == null)
			return;

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

		// see https://bugs.eclipse.org/bugs/show_bug.cgi?id=139552
		// fix provided by Valentin Baciu
		int index = qualifiedName.indexOf(':');
		String localName = index != -1 ? qualifiedName.substring(index + 1) : qualifiedName;

		Attr attr = getAttributeNodeNS(uri, localName);
		if (attr != null) {
			attr.setValue(value); // change value
		}
		else {

			// new attribute
			Document doc = getOwnerDocument();
			if (doc != null) {
				attr = doc.createAttributeNS(uri, qualifiedName);
				if (attr != null) {
					attr.setValue(value);
					appendAttributeNode(attr);
				}
			}
		}
	}

	/**
	 */
	public void setCommentTag(boolean isCommentTag) {
		IDOMNode parent = (IDOMNode) getParentNode();
		if (parent != null && !parent.isChildEditable()) {
			throw new DOMException(DOMException.NO_MODIFICATION_ALLOWED_ERR, DOMMessages.NO_MODIFICATION_ALLOWED_ERR);
		}

		if (isCommentTag)
			fTagFlags |= FLAG_COMMENT;
		else
			fTagFlags &= ~FLAG_COMMENT;
	}

	/**
	 * setEmptyTag method
	 * 
	 * @param isEmptyTag
	 *            boolean
	 */
	public void setEmptyTag(boolean isEmptyTag) {
		IDOMNode parent = (IDOMNode) getParentNode();
		if (parent != null && !parent.isChildEditable()) {
			throw new DOMException(DOMException.NO_MODIFICATION_ALLOWED_ERR, DOMMessages.NO_MODIFICATION_ALLOWED_ERR);
		}

		if (isEmptyTag)
			fTagFlags |= FLAG_EMPTY;
		else
			fTagFlags &= ~FLAG_EMPTY;
	}

	/**
	 * setEndStructuredDocumentRegion method
	 * 
	 * @param flatNode
	 */
	void setEndStructuredDocumentRegion(IStructuredDocumentRegion flatNode) {
		this.endStructuredDocumentRegion = flatNode;

		NodeContainer parent = (NodeContainer) getParentNode();
		if (parent != null) {
			parent.syncChildEditableState(this);
		}
	}

	/**
	 * setJSPTag method
	 * 
	 * @param isJSPTag
	 *            boolean
	 */
	public void setJSPTag(boolean isJSPTag) {
		IDOMNode parent = (IDOMNode) getParentNode();
		if (parent != null && !parent.isChildEditable()) {
			throw new DOMException(DOMException.NO_MODIFICATION_ALLOWED_ERR, NodeImpl.EMPTY_STRING);
		}

		if (isJSPTag)
			fTagFlags |= FLAG_JSP;
		else
			fTagFlags &= ~FLAG_JSP;
	}

	protected void setNamespaceURI(String namespaceURI) {
		if (namespaceURI == null)
			this.fNamespaceURI = null;
		else
			this.fNamespaceURI = namespaceURI.toCharArray();
	}

	/**
	 */
	protected void setOwnerDocument(Document ownerDocument, boolean deep) {
		super.setOwnerDocument(ownerDocument, deep);

		if (this.attrNodes == null)
			return;

		int length = this.attrNodes.getLength();
		for (int i = 0; i < length; i++) {
			AttrImpl attr = (AttrImpl) this.attrNodes.item(i);
			if (attr == null)
				continue;
			attr.setOwnerDocument(ownerDocument);
		}
	}

	/**
	 */
	public void setPrefix(String prefix) throws DOMException {
		IDOMNode parent = (IDOMNode) getParentNode();
		if (parent != null && !parent.isChildEditable()) {
			throw new DOMException(DOMException.NO_MODIFICATION_ALLOWED_ERR, NodeImpl.EMPTY_STRING);
		}

		int prefixLength = (prefix != null ? prefix.length() : 0);
		String localName = getLocalName();
		if (prefixLength == 0) {
			if (localName == null || localName.length() == 0) {
				// invalid local name
				return;
			}
			setTagName(localName);
		}
		else {
			int localLength = (localName != null ? localName.length() : 0);
			StringBuffer buffer = new StringBuffer(prefixLength + 1 + localLength);
			buffer.append(prefix);
			buffer.append(':');
			if (localName != null)
				buffer.append(localName);
			setTagName(buffer.toString());
		}

		boolean changeEndTag = hasEndTag();
		notifyStartTagChanged();
		if (changeEndTag)
			notifyEndTagChanged();
	}

	/**
	 * setStartStructuredDocumentRegion method
	 * 
	 * @param flatNode
	 */
	void setStartStructuredDocumentRegion(IStructuredDocumentRegion flatNode) {
		setStructuredDocumentRegion(flatNode);
	}

	/**
	 * setTagName method
	 * 
	 * @param tagName
	 *            java.lang.String
	 */
	protected void setTagName(String tagName) {
		this.fTagName = tagName != null ? CharacterStringPool.getCharString(tagName) : null;
	}

	/**
	 * toString method
	 * 
	 * @return java.lang.String
	 */
	public String toString() {
		StringBuffer buffer = new StringBuffer();
		String tagName = getTagName();
		if (hasStartTag())
			buffer.append(tagName);
		if (isEmptyTag())
			buffer.append('/');
		if (hasEndTag()) {
			buffer.append('/');
			buffer.append(tagName);
		}
		if (buffer.length() == 0)
			buffer.append(tagName);

		IStructuredDocumentRegion startStructuredDocumentRegion = getStartStructuredDocumentRegion();
		if (startStructuredDocumentRegion != null) {
			buffer.append('@');
			buffer.append(startStructuredDocumentRegion.toString());
		}
		IStructuredDocumentRegion endStructuredDocumentRegion = getEndStructuredDocumentRegion();
		if (endStructuredDocumentRegion != null) {
			buffer.append('@');
			buffer.append(endStructuredDocumentRegion.toString());
		}
		return buffer.toString();
	}

	/**
	 * NOT IMPLEMENTED. Is defined here in preparation for DOM 3.
	 */
	public void setIdAttribute(String name, boolean isId) throws DOMException {
		throw new DOMException(DOMException.NOT_SUPPORTED_ERR, "Not supported in this version"); //$NON-NLS-1$
	}

	/**
	 * NOT IMPLEMENTED. Is defined here in preparation for DOM 3.
	 */
	public void setIdAttributeNS(String namespaceURI, String localName, boolean isId) throws DOMException {
		throw new DOMException(DOMException.NOT_SUPPORTED_ERR, "Not supported in this version"); //$NON-NLS-1$
	}

	/**
	 * NOT IMPLEMENTED. Is defined here in preparation for DOM 3.
	 */
	public void setIdAttributeNode(Attr idAttr, boolean isId) throws DOMException {
		throw new DOMException(DOMException.NOT_SUPPORTED_ERR, "Not supported in this version"); //$NON-NLS-1$
	}
}
