/*******************************************************************************
 * Copyright (c) 2003, 2004 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 * 
 * Contributors:
 * IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.wst.common.internal.emf.resource;


import java.util.List;
import java.util.Stack;

import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.ecore.EObject;
import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.AttributesImpl;

/**
 * The EMF2SAXWriter handles the serialization of EMF Resources using SAX events. SAX events are
 * triggered to the content handler as the tree is being parsed. These events can then be written
 * into any stream wrapped by the ContentHandler.
 * 
 * @author mdelder
 */
public class EMF2SAXWriter {

	public static final String NAMESPACE = "";//"http://java.sun.com/xml/ns/j2ee"; //$NON-NLS-1$

	/* Used in those cases where no Attributes are necessary */
	private static final Attributes EMPTY_ATTRIBUTES = new AttributesImpl();

	/**
	 * Serialize an EMF resource into an XML Stream using the given ContentHandler. Note that this
	 * method can also be used to copy a given EMF Resource if the EMF2SAXDocumentHandler is used as
	 * the given ContentHandler.
	 * 
	 * @param resource
	 * @param handler
	 */
	public void serialize(TranslatorResource resource, ContentHandler handler) throws SAXException {

		Translator rootTranslator = resource.getRootTranslator();
		EList contents = resource.getContents();

		if (contents.size() != 1) {
			throw new IllegalStateException("The contents of a resource may only contain one EMF Model Object."); //$NON-NLS-1$
		}
		handler.startDocument();
		EObject element = (EObject) contents.get(0);
		serialize(handler, element, rootTranslator, new WriterHints(resource));
		handler.endDocument();

	}

	private void serialize(ContentHandler handler, EObject target, Translator translator, WriterHints hints) throws SAXException {

		List mofChildren = null;
		Object rawValue = null;
		EObject newTarget = null;
		Translator currentChildTranslator = null;
		Translator nextTranslator = null;
		char[] characterData = null;
		String convertedValue = null;
		Attributes attributes = null;
		String childDomName = null;
		final int version = hints.getVersion();

		/*
		 * Processing hints are used to remember where are in the iteration of the translator's
		 * children. see the TranslatorFilter for more information on how this array is used.
		 */
		int[] processingHints = TranslatorFilter.createProcessingHints();

		String targetDomName = translator.getDOMName(target);

		attributes = getAttributes(translator, target, hints);

		handler.startElement(NAMESPACE, targetDomName, targetDomName, attributes);

		currentChildTranslator = TranslatorFilter.getNextObjectTranslator(translator, processingHints[TranslatorFilter.NEXT_START_HINT_INDX], processingHints, target, version);
		while (currentChildTranslator != null) {
			/* For each Child Translator of the Translator parameter passed into the method */

			/* Does the Translator have any MOF Children? */
			mofChildren = currentChildTranslator.getMOFChildren(target);
			openDomPathIfNecessary(handler, hints, currentChildTranslator, target, mofChildren);

			if (currentChildTranslator.isManagedByParent()) {
				/*
				 * Translators which are managed by their parents require less processing -- just
				 * convert their value to a string and write it out as the content of an XML element
				 */
				childDomName = currentChildTranslator.getDOMName(target);
				if (!currentChildTranslator.isEmptyTag()) {
					/* The Translator is not an Empty tag. Its text content is significant */

					if (mofChildren.size() > 0) {
						for (int j = 0; j < mofChildren.size(); j++) {

							/* Text only translators will not have open and close XML elements */
							if (!currentChildTranslator.isDOMTextValue())
								handler.startElement(NAMESPACE, childDomName, childDomName, EMPTY_ATTRIBUTES);

							rawValue = mofChildren.get(j);
							/* convertValueToString should always return a non-null String */
							convertedValue = currentChildTranslator.convertValueToString(rawValue, target);
							characterData = XMLEncoderDecoder.escape(convertedValue).toCharArray();
							handler.characters(characterData, 0, characterData.length);

							if (!currentChildTranslator.isDOMTextValue())
								handler.endElement(NAMESPACE, childDomName, childDomName);
						}
					}
				} else {
					/*
					 * The Translator is an Empty Element (its mere presence has significance) (e.g.
					 * <cascade-delete/>
					 */

					if (currentChildTranslator.isBooleanFeature()) {
						/* Boolean features may or may not be rendered */
						rawValue = mofChildren.get(0);
						if (rawValue != null && ((Boolean) rawValue).booleanValue()) {
							handler.startElement(NAMESPACE, childDomName, childDomName, EMPTY_ATTRIBUTES);
							handler.endElement(NAMESPACE, childDomName, childDomName);
						}

					} else {
						/* Always render any other Empty elements */
						handler.startElement(NAMESPACE, childDomName, childDomName, EMPTY_ATTRIBUTES);
						handler.endElement(NAMESPACE, childDomName, childDomName);
					}
				}
			} else {

				/* The Translator is a more complex feature, handle its processing recursively */
				for (int j = 0; j < mofChildren.size(); j++) {
					newTarget = (EObject) mofChildren.get(j);
					serialize(handler, newTarget, currentChildTranslator, hints);
				}
			}

			/* Fetch the next peer translator */
			nextTranslator = TranslatorFilter.getNextObjectTranslator(translator, processingHints[TranslatorFilter.NEXT_START_HINT_INDX], processingHints, target, version);

			closeDomPathIfNecessary(handler, hints, currentChildTranslator, nextTranslator, target, mofChildren);

			/*
			 * We needed to invoke closeDomPathIfNecessary() with the peer, now we move on to
			 * process that peer
			 */
			currentChildTranslator = nextTranslator;

		}
		handler.endElement(NAMESPACE, targetDomName, targetDomName);
	}

	/**
	 * Determines whether or not a DOM Path should be rendered. This method is particularly useful
	 * for determining whether Empty XML elements are relevant and should be written to the XML
	 * stream.
	 * 
	 * @param target
	 *            The EMF Target of the Translation
	 * @param currentChildTranslator
	 *            The current Translator
	 * @param mofChildren
	 *            The mofChildren that were found for the Translator on the Target
	 * @return
	 */
	private boolean shouldRenderDomPath(EObject target, Translator currentChildTranslator, List mofChildren) {
		return  (currentChildTranslator.shouldRenderEmptyDOMPath(target) || mofChildren.size() > 0);
	}

	/**
	 * openDomPathIfNecessary will write the current DOM Path to the serialization stream if it has
	 * not been written by a previous peer translator. The processing results in the collapse of
	 * Peer Translators with matching DOM Paths into a single XML parent element.
	 * 
	 * @param handler
	 *            The ContentHandler which is writing the XML result
	 * @param hints
	 *            A Global container for information specific to a single XML document
	 * @param currentChildTranslator
	 *            The active Translator being processed
	 * @param target
	 *            The EMF Target of the Translation
	 * @throws SAXException
	 */
	private void openDomPathIfNecessary(ContentHandler handler, WriterHints hints, Translator currentChildTranslator, EObject target, List mofChildren) throws SAXException {

		/* If the translator does not have a DOM Path, then we do nothing */
		if (currentChildTranslator.hasDOMPath() && shouldRenderDomPath(target, currentChildTranslator, mofChildren)) {

			String childDomPath = currentChildTranslator.getDOMPath();

			/*
			 * IsDomPathActive() will verify whether this DOM Path has already been written to the
			 * XML stream
			 */
			if (!hints.isDomPathActive(childDomPath)) {

				/*
				 * Write an open element for the DOM Path and "remember" that we have written it
				 */
				handler.startElement(NAMESPACE, childDomPath, childDomPath, EMPTY_ATTRIBUTES);
				hints.pushDomPath(childDomPath);
			}

		}
	}

	/**
	 * closeDomPathIfNecessary will determine whether the next peer Translator shares the active DOM
	 * Path of the current Translator. If the next peer Translator has the same DOM Path, no action
	 * will be taken (hence condensing the elements into a single XML parent). However, if the DOM
	 * Path differs (including the Next Peer Translator has no DOM Path) then the current DOM Path
	 * will be closed (a close XML element is generated.
	 * 
	 * @param handler
	 *            The ContentHandler which is writing the XML result
	 * @param hints
	 *            A Global container for information specific to a single XML document
	 * @param currentChildTranslator
	 *            The last Translator to have completed processing
	 * @param nextTranslator
	 *            The next peer Translator that will become active
	 * @param target
	 *            The EMF Target of the Translation
	 * @throws SAXException
	 */
	private void closeDomPathIfNecessary(ContentHandler handler, WriterHints hints, Translator currentChildTranslator, Translator nextTranslator, EObject target, List mofChildren) throws SAXException {

		if (currentChildTranslator.hasDOMPath() && shouldRenderDomPath(target, currentChildTranslator, mofChildren)) {
			String childDomPath = currentChildTranslator.getDOMPath();
			if (nextTranslator != null) { /*
										   * There are more peers after this element, we can peek
										   * ahead
										   */
				String nextPeerDomPath = nextTranslator.getDOMPath();
				if (nextPeerDomPath == null || !nextPeerDomPath.equals(childDomPath)) {
					handler.endElement(NAMESPACE, childDomPath, childDomPath);
					hints.popDomPath();
				}

			} else { /* This was the last child element, we must close the dompath */
				handler.endElement(NAMESPACE, childDomPath, childDomPath);
				hints.popDomPath();
			}
		}
	}

	/**
	 * Aggregate the Attribute translator children from a given translator. This method will request
	 * the AttributesImpl object from the WriterHints object. The WriterHints maintains this
	 * reusable collection to limit the requirement for new object creation.
	 * 
	 * @param translator
	 * @param target
	 * @param hints
	 * @return an initialized set of Attributes for the given Translator and EMF Target
	 */
	private Attributes getAttributes(Translator translator, EObject target, WriterHints hints) {

		AttributesImpl attributes = hints.getAttributeHolder();
		int version = hints.getVersion();
		Object rawValue = null;
		String convertedValue = null;
		String childDomName = null;
		Translator attributeTranslator = null;
		int[] processingHints = TranslatorFilter.createProcessingHints();

		while ((attributeTranslator = TranslatorFilter.getNextAttributeTranslator(translator, processingHints[TranslatorFilter.NEXT_START_HINT_INDX], processingHints, target, version)) != null) {

			List mofChildren = attributeTranslator.getMOFChildren(target);
			if (mofChildren.size() > 0) {
				for (int j = 0; j < mofChildren.size(); j++) {

					childDomName = attributeTranslator.getDOMName(target);
					rawValue = mofChildren.get(j);
					convertedValue = attributeTranslator.convertValueToString(rawValue, target);
					convertedValue = XMLEncoderDecoder.escape(convertedValue);
					attributes.addAttribute(NAMESPACE, childDomName, childDomName, "String", convertedValue); //$NON-NLS-1$
				}

			} else {
				childDomName = attributeTranslator.getDOMName(target);
				convertedValue = (String) attributeTranslator.getMOFValue(target);
				if (convertedValue != null)
					attributes.addAttribute(NAMESPACE, childDomName, childDomName, "String", convertedValue); //$NON-NLS-1$
			}
		}
		return attributes;
	}

	/**
	 * WriterHints is used to "remember" certain pieces of information while the writer is
	 * processing. Of particular interest are the version and the state of the DOM Path output.
	 * Consecutive elements with consistent (identical) DOM Paths are collapsed under a single XML
	 * element.
	 * 
	 * The WriterHints provides global state between recursive invocations of serialize(). It should
	 * be not be used to store local data (e.g. data that is only relevant to a single Translator in
	 * a given context).
	 * 
	 * The WriterHints also stores an AttributesImpl object that is re-used to store attributes. The
	 * getAttributes() method will request the Attributes Holder.
	 * 
	 * @author mdelder
	 */
	public final class WriterHints {
		private final TranslatorResource resource;
		private final Stack domStack = new Stack();
		private final AttributesImpl attributesImpl = new AttributesImpl();

		public WriterHints(TranslatorResource res) {
			this.resource = res;
		}

		/**
		 * Push a new domPath onto the stack
		 * 
		 * @param domPath
		 *            a DOMPath which has been written to the XML stream
		 */
		public void pushDomPath(String domPath) {

			if (domPath != null && domPath.length() > 0)
				domStack.push(domPath);
		}

		/**
		 * Pop the current domPath from the Array
		 */
		public void popDomPath() {

			if (!domStack.isEmpty())
				domStack.pop();
		}

		/**
		 * Determines if the given DOMPath has already been written to the XML stream
		 * 
		 * @param domPath
		 * @return true if the given DOMPath has already been written to the XML stream
		 */
		public boolean isDomPathActive(String domPath) {
			boolean result = false;
			if (!domStack.isEmpty()) {

				String currentDomPath = (String) domStack.peek();
				if (currentDomPath != null && domPath != null)
					result = currentDomPath.equals(domPath);
				else if (!(currentDomPath == null ^ domPath == null))
					result = true;
			}

			return result;
		}

		/**
		 * @return the version of the EMF Resource
		 */
		public int getVersion() {
			return this.resource.getVersionID();
		}

		/**
		 * Returns an empty AttributesImpl object to store attributes. Within the context of a given
		 * WriterHints object (and hence single XML document), the object returned is a singleton.
		 * The same AttributesImpl object is cleared and reused for each invocation.
		 * 
		 * @return an empty AttributesImpl object to store attributes
		 */
		public AttributesImpl getAttributeHolder() {
			this.attributesImpl.clear();
			return this.attributesImpl;
		}

	}
}