/*******************************************************************************
 * Copyright (c) 2007, 2013 David Green and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     David Green - initial API and implementation
 *******************************************************************************/
package org.eclipse.mylyn.wikitext.core.util;

import java.util.Stack;

/**
 * An {@link XmlStreamWriter} that creates formatted output by emitting whitespace into the document output stream.
 * 
 * @author David Green
 * @since 1.0
 */
public class FormattingXMLStreamWriter extends XmlStreamWriter {

	private final XmlStreamWriter delegate;

	private int indentLevel;

	private final Stack<Integer> childCounts = new Stack<Integer>();

	private int childCount;

	private final Stack<String> elements = new Stack<String>();

	private int lineOffset = 0;

	public FormattingXMLStreamWriter(XmlStreamWriter delegate) {
		this.delegate = delegate;
	}

	@Override
	public void close() {
		delegate.close();
	}

	@Override
	public void flush() {
		delegate.flush();
	}

	@Override
	public String getPrefix(String uri) {
		return delegate.getPrefix(uri);
	}

	@Override
	public void setDefaultNamespace(String uri) {
		delegate.setDefaultNamespace(uri);
	}

	@Override
	public void setPrefix(String prefix, String uri) {
		delegate.setPrefix(prefix, uri);
	}

	@Override
	public void writeAttribute(String prefix, String namespaceURI, String localName, String value) {
		if (value == null) {
			value = ""; //$NON-NLS-1$
		}
		delegate.writeAttribute(prefix, namespaceURI, localName, value);
	}

	@Override
	public void writeAttribute(String namespaceURI, String localName, String value) {
		if (value == null) {
			value = ""; //$NON-NLS-1$
		}
		delegate.writeAttribute(namespaceURI, localName, value);
	}

	@Override
	public void writeAttribute(String localName, String value) {
		if (value == null) {
			value = ""; //$NON-NLS-1$
		}
		delegate.writeAttribute(localName, value);
	}

	@Override
	public void writeCData(String data) {
		++lineOffset;
		delegate.writeCData(data);
	}

	@Override
	public void writeCharacters(char[] text, int start, int len) {
		int lineStart = start;
		int length = 0;
		for (int x = 0; x < len; ++x) {
			final int charOffset = start + x;
			++length;
			if (lineOffset == 0 && text[charOffset] != '\n') {
				maybeIndent(false, true);
			}
			++lineOffset;
			if (text[charOffset] == '\n') {
				delegate.writeCharacters(text, lineStart, length);
				length = 0;
				lineOffset = 0;
				lineStart = charOffset;
			}
		}
		if (length > 0) {
			delegate.writeCharacters(text, lineStart, length);
			lineOffset += length;
		}
	}

	@Override
	public void writeCharacters(String text) {
		if (text == null) {
			return;
		}
		char[] chars = text.toCharArray();
		writeCharacters(chars, 0, chars.length);
	}

	@Override
	public void writeLiteral(String literal) {
		++lineOffset;
		delegate.writeLiteral(literal);
	}

	@Override
	public void writeComment(String data) {
		if (data == null) {
			data = ""; //$NON-NLS-1$
		}
		++childCount;
		maybeIndent();
		++lineOffset;
		delegate.writeComment(data);
	}

	@Override
	public void writeDefaultNamespace(String namespaceURI) {
		delegate.writeDefaultNamespace(namespaceURI);
	}

	@Override
	public void writeDTD(String dtd) {
		++lineOffset;
		delegate.writeDTD(dtd);
	}

	@Override
	public void writeEmptyElement(String prefix, String localName, String namespaceURI) {
		++childCount;
		maybeIndent();
		++lineOffset;
		delegate.writeEmptyElement(prefix, localName, namespaceURI);
	}

	@Override
	public void writeEmptyElement(String namespaceURI, String localName) {
		++childCount;
		maybeIndent();
		++lineOffset;
		delegate.writeEmptyElement(namespaceURI, localName);
	}

	@Override
	public void writeEmptyElement(String localName) {
		++childCount;
		maybeIndent();
		++lineOffset;
		delegate.writeEmptyElement(localName);
	}

	@Override
	public void writeEndDocument() {
		delegate.writeEndDocument();
	}

	@Override
	public void writeEndElement() {
		--indentLevel;
		maybeIndent();
		elements.pop();
		delegate.writeEndElement();
		childCount = childCounts.pop();
	}

	@Override
	public void writeEntityRef(String name) {
		++lineOffset;
		delegate.writeEntityRef(name);
	}

	@Override
	public void writeNamespace(String prefix, String namespaceURI) {
		delegate.writeNamespace(prefix, namespaceURI);
	}

	@Override
	public void writeProcessingInstruction(String target, String data) {
		++childCount;
		++lineOffset;
		delegate.writeProcessingInstruction(target, data);
	}

	@Override
	public void writeProcessingInstruction(String target) {
		++childCount;
		++lineOffset;
		delegate.writeProcessingInstruction(target);
	}

	@Override
	public void writeStartDocument() {
		++childCount;
		++lineOffset;
		delegate.writeStartDocument();
	}

	@Override
	public void writeStartDocument(String encoding, String version) {
		++childCount;
		++lineOffset;
		delegate.writeStartDocument(encoding, version);
	}

	@Override
	public void writeStartDocument(String version) {
		++childCount;
		++lineOffset;
		delegate.writeStartDocument(version);
	}

	@Override
	public void writeStartElement(String prefix, String localName, String namespaceURI) {
		++childCount;
		maybeIndent();
		elements.push(localName);
		childCounts.push(childCount);
		childCount = 0;
		++indentLevel;
		++lineOffset;
		delegate.writeStartElement(prefix, localName, namespaceURI);
	}

	@Override
	public void writeStartElement(String namespaceURI, String localName) {
		++childCount;
		maybeIndent();
		elements.push(localName);
		childCounts.push(childCount);
		childCount = 0;
		++indentLevel;
		++lineOffset;
		delegate.writeStartElement(namespaceURI, localName);
	}

	@Override
	public void writeStartElement(String localName) {
		++childCount;
		maybeIndent();
		elements.push(localName);
		childCounts.push(childCount);
		childCount = 0;
		++indentLevel;
		++lineOffset;
		delegate.writeStartElement(localName);
	}

	private void maybeIndent() {
		maybeIndent(true, false);
	}

	private void maybeIndent(boolean withNewline, boolean force) {
		if ((childCount == 0 && !force) || preserveWhitespace()) {
			return;
		}
		StringBuilder buf = new StringBuilder();
		if (withNewline && (childCount > 1 || !childCounts.isEmpty())) {
			buf.append('\n');
			lineOffset = 0;
		}
		for (int x = 0; x < indentLevel; ++x) {
			buf.append('\t');
		}
		lineOffset += indentLevel;
		if (buf.length() > 0) {
			delegate.writeCharacters(buf.toString().toCharArray(), 0, buf.length());
		}
	}

	private boolean preserveWhitespace() {
		for (int x = elements.size() - 1; x >= 0; --x) {
			if (preserveWhitespace(elements.get(x))) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Override this method to indicate which elements must have whitespace preserved.
	 * 
	 * @param elementName
	 *            the local name of the element
	 */
	protected boolean preserveWhitespace(String elementName) {
		return false;
	}

}