/* 
 * E-XML Library:  For XML, XML-RPC, HTTP, and related.
 * Copyright (C) 2002-2008  Elias Ross
 * 
 * genman@noderunner.net
 * http://noderunner.net/~genman
 * 
 * 1025 NE 73RD ST
 * SEATTLE WA 98115
 * USA
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * $Id$
 */

package net.noderunner.exml;

import java.io.IOException;
import java.io.Writer;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;

/**
 * Writes and serializes XML data to a character <code>Writer</code>.
 * It is a good idea to call <code>flush</code> or <code>close</code> when
 * finished, otherwise, any buffered data may not be sent.
 * Buffering the underlying output stream is recommended.
 */
public class XmlWriter 
{
	/**
	 * This stack should store only <code>String</code> instances.
	 */
	ArrayStack<String> writeStack;

	/**
	 * Output writer.
	 */
	Writer writer;

	/**
	 * Constructs a new XmlWriter around a <code>Writer</code> instance.
	 */
	public XmlWriter(Writer writer) 
	{
		this.writer = writer;
		this.writeStack = ArrayStack.create();
	}

	/**
	 * Sets the writer to output with.
	 * @param writer writer to wrap
	 */
	public void setWriter(Writer writer) {
		this.writer = writer;
		writeStack.clear();
	}

	/**
	 * Writes the <code>&lt;?xml ?&gt;</code> header.
	 * @param encoding for outputting the corrent Xml header. 
	 */
	public void writeHeader(String encoding) 
		throws IOException
	{
		writer.write("<?xml version=\"1.0\" encoding=\""+encoding+"\"?>\n");
	}
	
	/**
	 * Writes a String as a start tag, like this:
	 * <code>&lt;element&gt;</code>.
	 * @param elem name of element
	 */
	public void startElement(String elem) 
		throws IOException
	{
		writer.write('<');
		writer.write(elem);
		writeStack.add(elem);
		writer.write('>');
	}

	/**
	 * Writes a String as a start tag, and writes a
	 * series of attribute names and values.
	 * Example usage:
	 * <pre>
	 * w.startElement("elem", new String[] { "a", "b", "c", "d"});
	 * </pre>
	 * Output result:
	 * <pre>
	 * &lt;elem a="b" c="d"&gt;
	 * </code>
	 * @param elem name of element
	 * @param attrs list of attribute name-value pairs,
	 * which must be even in length
	 * @throws ArrayIndexOutOfBoundsException if <code>attr</code> array
	 * length was not even
	 */
	public void startElement(String elem, String attrs[]) 
		throws IOException
	{
		startElement(elem, attrs, true);
	}

	private void startElement(String elem, String attrs[], boolean open) 
		throws IOException
	{
		writer.write('<');
		writer.write(elem);
		for (int i = 0; i < attrs.length; i += 2) {
			writer.write(' ');
			writer.write(attrs[i]);
			writer.write("=\"");
			escape(attrs[i + 1], writer);
			writer.write('"');
		}
		if (open) {
			writeStack.add(elem);
			writer.write('>');
		} else {
			writer.write("/>");
		}
	}

	/**
	 * Writes an Element object as a start tag, including its attributes.
	 * This works for empty elements as well.  Once written, it can be
	 * closed with
	 * <code>endElement</code> or <code>endAll</code>.  Do not call
	 * try to call <code>endElement</code> for closed elements.
	 * Output appears like this: <code>&lt;element
	 * attribute1='value'&gt;</code>.
	 *
	 * @see #endElement
	 */
	public void startElement(Element elem) 
		throws IOException
	{
		writer.write('<');
		writer.write(elem.getName());
		List<Attribute> l = elem.getAttributes();
		if (l != null) {
			Iterator<Attribute> i = l.iterator();
			while (i.hasNext()) {
				writer.write(' ');
				writer.write(i.next().toString());
			}
		}
		if (elem.isOpen()) {
			writer.write('>');
			writeStack.add(elem.getName());
		}
		else {
			writer.write("/>");
		}
	}

	/**
	 * Writes an Element object completely.
	 * Example: <code>&lt;element x='value'&gt; stuff
	 * &lt;/element&gt;</code>.
	 */
	public void element(Element e) 
		throws IOException
	{
		startElement(e);
		List<Node> l = e.getChildNodes();
		if (l != null) {
			int size = l.size();
			for (int i = 0; i < size; i++) {
				Node n = l.get(i);
				switch (n.getNodeType()) {
					case Node.ELEMENT_NODE:
						element((Element)n);
					break;
					case Node.TEXT_NODE: 
						writeCData((CharacterData)n);
					break;
					default: 
						write(n.toString());
					break;

				}
			}
		}
		if (e.isOpen())
			endElement();
	}

	/**
	 * Writes an end tag, matching the tag written at this tree depth.
	 * @throws NoSuchElementException if the written tree depth is already at zero
	 */
	public void endElement() 
		throws IOException
	{
		writer.write("</");
		if (writeStack.isEmpty())
			throw new NoSuchElementException("No more matching elements");
		writer.write(writeStack.pop());
		writer.write('>');
		// writer.flush();
	}

	/**
	 * Closes all remaining open tags beyond a certain depth.  Does nothing if
	 * depth of the tree is lower than <code>depth</code>.  A value of
	 * <code>0</code> closes the entire document.
	 *
	 * @throws IllegalArgumentException if negative depth is given
	 * @param depth positive tree depth
	 * @see XmlParser#getDepth()
	 */
	public void up(int depth)
		throws IOException
	{
		if (depth < 0)
			throw new IllegalArgumentException("Depth must be positive: " + depth);
		while (writeStack.size() > depth)
			endElement();
	}

	/**
	 * Returns the depth of the tree currently written.  Returns zero if no
	 * open elements have been written.  This can be used in conjunction with
	 * the <code>up</code> function.
	 * @see #up
	 */
	public int getDepth()
	{
		return writeStack.size();
	}

	/**
	 * Writes a string as an empty element tag.
	 * Example: <code>&lt;element/&gt; </code>.
	 */
	public void emptyElement(String elem) 
		throws IOException
	{
		writer.write("<");
		writer.write(elem);
		writer.write("/>");
	}

	/**
	 * Writes a string as an empty element tag.
	 * Example: <code>&lt;element/&gt; </code>.
	 *
	 * @see #startElement
	 */
	public void emptyElement(String elem, String attrs[]) 
		throws IOException
	{
		startElement(elem, attrs, false);
	}

	/**
	 * Writes this element as an empty tag, without its
	 * children or contents.
	 * Calls {@link startElement(Element)}, since it also writes empty elements.
	 * If this element is considered open, a matching close tag is appended
	 * automatically, so the depth of the XML tree is not affected.
	 * @see #startElement
	 * @see Element#isOpen
	 */
	public void emptyElement(Element elem) 
		throws IOException
	{
		startElement(elem);
		if (elem.isOpen())
			endElement();
	}

	/**
	 * Writes a CDSection.  Do not pass in a string with the
	 * text ']]', since it is not allowed, this function does no
	 * checking for that.
	 */
	public void writeCDSection(String text) 
		throws IOException
	{
		writer.write(XmlTags.CDATA_BEGIN);
		writer.write(text);
		writer.write(XmlTags.CDATA_END);
	}

	/**
	 * Escapes a string, appends the results to <code>out</code>.
	 */
	static void escape(String in, Writer out)
		throws IOException
	{
		int l = in.length();
		for (int i = 0; i < l; i++) {
			char c = in.charAt(i);
			switch (c) {
			case '\'' :
				out.write("&apos;");
				break;
			case '"' :
				out.write("&quot;");
				break;
			case '<' :
				out.write("&lt;");
				break;
			case '>' :
				out.write("&gt;");
				break;
			case '&' :
				out.write("&amp;");
				break;
			default:
				out.write(c);
			}
		}
	}

	/**
	 * Escapes a string, appends the results to <code>out</code>.
	 */
	static void escape(StringBuffer in, Writer out)
		throws IOException
	{
		int l = in.length();
		for (int i = 0; i < l; i++) {
			char c = in.charAt(i);
			switch (c) {
			case '\'' :
				out.write("&apos;");
				break;
			case '"' :
				out.write("&quot;");
				break;
			case '<' :
				out.write("&lt;");
				break;
			case '>' :
				out.write("&gt;");
				break;
			case '&' :
				out.write("&amp;");
				break;
			default:
				out.write(c);
			}
		}
	}

	/**
	 * Writes character data, transforms &lt; &gt; &amp; &quot; &apos; symbols.
	 */
	public void writeCData(CharacterData cd) 
		throws IOException
	{
		cd.getWriter().writeEscapedTo(writer);
	}

	/**
	 * Writes character data, transforms &lt; &gt; &amp; &quot; &apos; symbols.
	 */
	public void writeCData(String text) 
		throws IOException
	{
		escape(text, writer);
	}
	
	/**
	 * Writes character data, does no entity transforms.
	 */
	public void write(char[] text)
		throws IOException
	{
		writer.write(text);
	}

	/**
	 * Writes character data, does no entity transforms.
	 */
	public void write(char[] text, int off, int len) 
		throws IOException
	{
		writer.write(text, off, len);
	}

	/**
	 * Writes character data, does no entity transforms.
	 */
	public void write(String text) 
		throws IOException
	{
		writer.write(text);
	}

	/**
	 * Flushes the underlying output stream.
	 */
	public void flush() 
		throws IOException
	{
		writer.flush();
	}

	/**
	 * Closes the underlying output stream.
	 */
	public void close() 
		throws IOException
	{
		writer.close();
	}
}
