/************************************************************************
 *
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER
 *
 * Copyright 2008, 2010 Oracle and/or its affiliates. All rights reserved.
 *
 * Use is subject to license terms.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy
 * of the License at http://www.apache.org/licenses/LICENSE-2.0. You can also
 * obtain a copy of the License at http://odftoolkit.org/docs/license.txt
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 ************************************************************************/
package org.odftoolkit.odfdom.pkg;

import java.util.ArrayList;
import org.apache.xerces.dom.ElementNSImpl;
import org.apache.xerces.dom.ParentNode;
import org.odftoolkit.odfdom.dom.OdfDocumentNamespace;
import org.w3c.dom.DOMException;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;

abstract public class OdfElement extends ElementNSImpl {

	private static final long serialVersionUID = -4939293285696678939L;

	/** Creates a new instance of OdfElement */
	public OdfElement(OdfFileDom ownerDocument, String namespaceURI,
			String qualifiedName) throws DOMException {
		super(ownerDocument, namespaceURI, qualifiedName);
	}

	/** Creates a new instance of OdfElement */
	public OdfElement(OdfFileDom ownerDocument, OdfName aName)
			throws DOMException {
		super(ownerDocument, aName.getUri(), aName.getQName());
	}

	abstract public OdfName getOdfName();

	protected <T extends OdfElement> T getParentAs(Class<T> clazz) {
		Node parent = getParentNode();
		if (parent != null && clazz.isInstance(parent)) {
			return clazz.cast(parent);
		} else {
			return null;
		}
	}

	protected <T extends OdfElement> T getAncestorAs(Class<T> clazz) {
		Node node = getParentNode();
		while (node != null) {
			if (clazz.isInstance(node)) {
				return clazz.cast(node);
			}
			node = node.getParentNode();
		}
		return null;
	}

	@Override
	public String toString() {
		return mapNode(this, new StringBuilder()).toString();
	}

	/** Only Siblings will be traversed by this method as Children */
	static private StringBuilder mapNodeTree(Node node, StringBuilder xml) {
		while (node != null) {
			// mapping node and this mapping include always all descendants
			xml = mapNode(node, xml);
			// next sibling will be mapped to XML
			node = node.getNextSibling();
		}
		return xml;
	}

	private static StringBuilder mapNode(Node node, StringBuilder xml) {
		if (node instanceof Element) {
			xml = mapElementNode(node, xml);
		} else if (node instanceof Text) {
			xml = mapTextNode(node, xml);
		}
		return xml;
	}

	private static StringBuilder mapTextNode(Node node, StringBuilder xml) {
		if (node != null) {
			xml = xml.append(node.getTextContent());
		}
		return xml;
	}

	private static StringBuilder mapElementNode(Node node, StringBuilder xml) {
		if (node != null) {
			xml = xml.append("<");
			xml = xml.append(node.getNodeName());
			xml = mapAttributeNode(node, xml);
			xml = xml.append(">");
			xml = mapNodeTree(node.getFirstChild(), xml);
			xml = xml.append("</");
			xml = xml.append(node.getNodeName());
			xml = xml.append(">");
		}
		return xml;
	}

	private static StringBuilder mapAttributeNode(Node node, StringBuilder xml) {
		NamedNodeMap attrs = null;
		int length;
		if ((attrs = node.getAttributes()) != null
				&& (length = attrs.getLength()) > 0) {
			for (int i = 0; length > i; i++) {
				xml = xml.append(" ");
				xml = xml.append(attrs.item(i).getNodeName());
				xml = xml.append("=\"");
				xml = xml.append(attrs.item(i).getNodeValue());
				xml = xml.append("\"");
			}
		}
		return xml;
	}

	/**
	 * Set the value of an ODF attribute by <code>OdfName</code>.
	 *
	 * @param name
	 *            The qualified name of the ODF attribute.
	 * @param value
	 *            The value to be set in <code>String</code> form
	 */
	public void setOdfAttributeValue(OdfName name, String value) {
		setAttributeNS(name.getUri(), name.getQName(), value);
	}

	/**
	 * Set an ODF attribute to this element
	 *
	 * @param attribute
	 *            the attribute to be set
	 */
	public void setOdfAttribute(OdfAttribute attribute) {
		setAttributeNodeNS(attribute);
	}

	/**
	 * Retrieves a value of an ODF attribute by <code>OdfName</code>.
	 *
	 * @param name
	 *            The qualified name of the ODF attribute.
	 * @return The value of the attribute as <code>String</code> or
	 *         <code>null</code> if the attribute does not exist.
	 */
	public String getOdfAttributeValue(OdfName name) {
		return getAttributeNS(name.getUri(), name.getLocalName());
	}

	/**
	 * Retrieves an ODF attribute by <code>OdfName</code>.
	 *
	 * @param name
	 *            The qualified name of the ODF attribute.
	 * @return The <code>OdfAttribute</code> or <code>null</code> if the
	 *         attribute does not exist.
	 */
	public OdfAttribute getOdfAttribute(OdfName name) {
		return (OdfAttribute) getAttributeNodeNS(name.getUri(), name.getLocalName());
	}

	/**
	 * Retrieves an ODF attribute by <code>NamespaceName</code>, and local name.
	 *
	 * @param namespace
	 *            The namespace of the ODF attribute.
	 * @param localname
	 *            The local name of the ODF attribute.
	 * @return The <code>OdfAttribute</code> or <code>null</code> if the
	 *         attribute does not exist.
	 */
	public OdfAttribute getOdfAttribute(NamespaceName namespace, String localname) {
		return (OdfAttribute) getAttributeNodeNS(namespace.getUri(),
				localname);
	}

	/**
	 * Determines if an ODF attribute exists.
	 *
	 * @param name
	 *            The qualified name of the ODF attribute.
	 * @return True if the attribute exists.
	 */
	public boolean hasOdfAttribute(OdfName name) {
		return hasAttributeNS(name.getUri(), name.getLocalName());
	}

	/**
	 * returns the first child node that implements the given class.
	 *
	 * @param <T>
	 *            The type of the ODF element to be found.
	 * @param clazz
	 *            is a class that extends OdfElement.
	 * @param parentNode
	 *            is the parent O of the children to be found.
	 * @return the first child node of the given parentNode that is a clazz or
	 *         null if none is found.
	 */
	@SuppressWarnings("unchecked")
	static public <T extends OdfElement> T findFirstChildNode(Class<T> clazz,
			Node parentNode) {
		if (parentNode != null && parentNode instanceof ParentNode) {
			Node node = ((ParentNode) parentNode).getFirstChild();
			while ((node != null) && !clazz.isInstance(node)) {
				node = node.getNextSibling();
			}

			if (node != null) {
				return (T) node;
			}
		}

		return null;
	}

	/**
	 * returns the first sibling after the given reference node that implements
	 * the given class.
	 *
	 * @param <T>
	 *            The type of the ODF element to be found.
	 * @param clazz
	 *            is a class that extends OdfElement.
	 * @param refNode
	 *            the reference node of the siblings to be found.
	 * @return the first sibbling of the given reference node that is a clazz or
	 *         null if none is found.
	 */
	@SuppressWarnings("unchecked")
	static public <T extends OdfElement> T findNextChildNode(Class<T> clazz,
			Node refNode) {
		if (refNode != null) {
			Node node = refNode.getNextSibling();
			while (node != null && !clazz.isInstance(node)) {
				node = node.getNextSibling();
			}

			if (node != null) {
				return (T) node;
			}
		}

		return null;
	}

	/**
	 * returns the first previous sibling before the given reference node that
	 * implements the given class.
	 *
	 * @param clazz
	 *            is a class that extends OdfElement.
	 * @param refNode
	 *            the reference node which siblings are to be searched.
	 * @return the first previous sibbling of the given reference node that is a
	 *         clazz or null if none is found.
	 */
	@SuppressWarnings("unchecked")
	static public <T extends OdfElement> T findPreviousChildNode(
			Class<T> clazz, Node refNode) {
		if (refNode != null) {
			Node node = refNode.getPreviousSibling();
			while (node != null && !clazz.isInstance(node)) {
				node = node.getPreviousSibling();
			}

			if (node != null) {
				return (T) node;
			}
		}

		return null;
	}

    protected OdfElement cloneOdfElement() {
		return ((OdfFileDom) this.ownerDocument).newOdfElement(this.getClass());
	}

	@Override
	public Node cloneNode(boolean deep) {
		OdfElement cloneElement = this.cloneOdfElement();

		if (attributes != null) {
			for (int i = 0; i < attributes.getLength(); i++) {
				Node item = attributes.item(i);
				String qname = null;
				String prefix = item.getPrefix();
				if (prefix == null) {
					qname = item.getLocalName();
                    cloneElement.setAttribute(qname, item.getNodeValue());
				} else {
					qname = prefix + ":" + item.getLocalName();
                    cloneElement.setAttributeNS(item.getNamespaceURI(), qname, item.getNodeValue());
				}
			}
		}

		if (deep) {
			Node childNode = getFirstChild();
			while (childNode != null) {
				cloneElement.appendChild(childNode.cloneNode(true));
				childNode = childNode.getNextSibling();
			}
		}

		return cloneElement;
	}

	/**
	 * indicates if some other object is equal to this one.
	 *
	 * @param obj
	 *            - the reference object with which to compare.
	 * @return true if this object is the same as the obj argument; false
	 *         otherwise.
	 */
	@Override
	public boolean equals(Object obj) {
		if (this == obj) {
			return true;
		}

		if ((obj == null) || !(obj instanceof OdfElement)) {
			return false;
		}

		OdfElement compare = (OdfElement) obj;

		// compare node name
		if (!localName.equals(compare.localName)) {
			return false;
		}

		if (!this.namespaceURI.equals(compare.namespaceURI)) {
			return false;
		}

		// compare node attributes
		if (attributes == compare.attributes) {
			return true;
		}

		if ((attributes == null) || (compare.attributes == null)) {
			return false;
		}

		int attr_count1 = attributes.getLength();
		int attr_count2 = compare.attributes.getLength();

		ArrayList<Node> attr1 = new ArrayList<Node>();
		for (int i = 0; i < attr_count1; i++) {
			Node node = attributes.item(i);
			if (node.getNodeValue().length() == 0) {
				continue;
			}
			attr1.add(node);
		}

		ArrayList<Node> attr2 = new ArrayList<Node>();
		for (int i = 0; i < attr_count2; i++) {
			Node node = compare.attributes.item(i);
			if (node.getNodeValue().length() == 0) {
				continue;
			}
			attr2.add(node);
		}

		if (attr1.size() != attr2.size()) {
			return false;
		}

		for (int i = 0; i < attr1.size(); i++) {
			Node n1 = attr1.get(i);
			if (n1.getLocalName().equals("name")
					&& n1.getNamespaceURI().equals(
					OdfDocumentNamespace.STYLE.getUri())) {
				continue; // do not compare style names
			}
			Node n2 = null;
			int j = 0;
			for (j = 0; j < attr2.size(); j++) {
				n2 = attr2.get(j);
				if (n1.getLocalName().equals(n2.getLocalName())
						&& n1.getNamespaceURI().equals(n2.getNamespaceURI())) {
					break;
				}
			}
			if (j == attr2.size()) {
				return false;
			}

			if (!n1.getTextContent().equals(n2.getTextContent())) {
				return false;
			}
		}

		// now compare child elements
		NodeList childs1 = this.getChildNodes();
		NodeList childs2 = compare.getChildNodes();

		int child_count1 = childs1.getLength();
		int child_count2 = childs2.getLength();
		if ((child_count1 == 0) && (child_count2 == 0)) {
			return true;
		}

		ArrayList<Node> nodes1 = new ArrayList<Node>();
		for (int i = 0; i < child_count1; i++) {
			Node node = childs1.item(i);
			if (node.getNodeType() == Node.TEXT_NODE) {
				if (node.getNodeValue().trim().length() == 0) {
					continue; // skip whitespace text nodes
				}
			}
			nodes1.add(node);
		}

		ArrayList<Node> nodes2 = new ArrayList<Node>();
		for (int i = 0; i < child_count2; i++) {
			Node node = childs2.item(i);
			if (node.getNodeType() == Node.TEXT_NODE) {
				if (node.getNodeValue().trim().length() == 0) {
					continue; // skip whitespace text nodes
				}
			}
			nodes2.add(node);
		}

		if (nodes1.size() != nodes2.size()) {
			return false;
		}

		for (int i = 0; i < nodes1.size(); i++) {
			Node n1 = nodes1.get(i);
			Node n2 = nodes2.get(i);
			if (!n1.equals(n2)) {
				return false;
			}
		}
		return true;
	}

	protected void onRemoveNode(Node node) {
		Node child = node.getFirstChild();
		while (child != null) {
			this.onRemoveNode(child);
			child = child.getNextSibling();
		}

		if (OdfElement.class.isInstance(node)) {
			((OdfElement) node).onRemoveNode();
		}
	}

	protected void onInsertNode(Node node) {
		Node child = node.getFirstChild();
		while (child != null) {
			this.onInsertNode(child);
			child = child.getNextSibling();
		}

		if (OdfElement.class.isInstance(node)) {
			((OdfElement) node).onInsertNode();
		}
	}

	protected void onRemoveNode() {
	}

	protected void onInsertNode() {
	}

	@Override
	public Node insertBefore(Node newChild, Node refChild) throws DOMException {
		onInsertNode(newChild);
		return super.insertBefore(newChild, refChild);
	}

	@Override
	public Node removeChild(Node oldChild) throws DOMException {
		onRemoveNode(oldChild);
		return super.removeChild(oldChild);
	}

	@Override
	public Node replaceChild(Node newChild, Node oldChild) throws DOMException {
		onInsertNode(newChild);
		onRemoveNode(oldChild);
		return super.replaceChild(newChild, oldChild);
	}

	/**
	 * Accept an visitor instance to allow the visitor to do some operations.
	 * Refer to visitor design pattern to get a better understanding.
	 * @param visitor	an instance of DefaultElementVisitor
	 */
	public void accept(ElementVisitor visitor) {
		visitor.visit(this);
	}
}
