/*
 *  eXist Open Source Native XML Database
 *  Copyright (C) 2001-06 Wolfgang M. Meier
 *  wolfgang@exist-db.org
 *  http://exist.sourceforge.net
 *  
 *  This program 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
 *  of the License, or (at your option) any later version.
 *  
 *  This program 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.
 *  
 *  You should have received a copy of the GNU Lesser General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *  
 *  $Id: DOMStreamer.java 3308 2006-04-26 12:46:59Z wolfgang_m $
 */
package org.exist.util.serializer;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Stack;

import org.exist.dom.QName;
import org.exist.memtree.ReferenceNode;
import org.w3c.dom.Attr;
import org.w3c.dom.CharacterData;
import org.w3c.dom.Comment;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.ProcessingInstruction;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;
import org.xml.sax.ext.LexicalHandler;
import org.xml.sax.helpers.AttributesImpl;
import org.xml.sax.helpers.NamespaceSupport;

/**
 * General purpose class to stream a DOM node to SAX.
 * 
 * @author Wolfgang Meier (wolfgang@exist-db.org)
 */
public class DOMStreamer {

	private ContentHandler contentHandler = null;
	private LexicalHandler lexicalHandler = null;
	private NamespaceSupport nsSupport = new NamespaceSupport();
	private HashMap namespaceDecls = new HashMap();
    private Stack stack = new Stack();
    
	public DOMStreamer() {
		super();
	}

	public DOMStreamer(ContentHandler contentHandler, LexicalHandler lexicalHandler) {
		this.contentHandler = contentHandler;
		this.lexicalHandler = lexicalHandler;
	}
	
	public void setContentHandler(ContentHandler handler) {
		contentHandler = handler;
	}

	public void setLexicalHandler(LexicalHandler handler) {
		lexicalHandler = handler;
	}

	/**
	 * Reset internal state for reuse. Registered handlers will be set
	 * to null.
	 *
	 */
	public void reset() {
		nsSupport.reset();
		namespaceDecls.clear();
		stack.clear();
		contentHandler = null;
		lexicalHandler = null;
	}
	
	/**
	 * Serialize the given node and all its descendants to SAX.
	 * 
	 * @param node
	 * @throws SAXException
	 */
    public void serialize(Node node) throws SAXException {
        serialize(node, false);
    }
    
    /**
     * Serialize the given node and all its descendants to SAX. If
     * callDocumentEvents is set to false, startDocument/endDocument
     * events will not be fired.
     * 
     * @param node
     * @param callDocumentEvents
     * @throws SAXException
     */
	public void serialize(Node node, boolean callDocumentEvents) throws SAXException {
        if(callDocumentEvents)
            contentHandler.startDocument();
		Node top = node;
		while (node != null) {
			startNode(node);
			Node nextNode = node.getFirstChild();
			//TODO : make it happy
			if (node instanceof ReferenceNode)
				nextNode = null;
			while (nextNode == null) {
				endNode(node);
				if (top != null && top.equals(node))
					break;
				nextNode = node.getNextSibling();
				if (nextNode == null) {
					node = node.getParentNode();
					if (node == null || (top != null && top.equals(node))) {
						endNode(node);
						nextNode = null;
						break;
					}
				}
			}			
			node = nextNode;
		}
        if(callDocumentEvents)
            contentHandler.endDocument();
	}

	protected void startNode(Node node) throws SAXException {
		String cdata;
		switch (node.getNodeType()) {
			case Node.DOCUMENT_NODE :
			case Node.DOCUMENT_FRAGMENT_NODE :
				break;
			case Node.ELEMENT_NODE :
				namespaceDecls.clear();
				nsSupport.pushContext();
				String uri = node.getNamespaceURI();
				String prefix = node.getPrefix();
				if (uri == null)
					uri = "";
				if (prefix == null)
					prefix = "";
				if (nsSupport.getURI(prefix) == null) {
					namespaceDecls.put(prefix, uri);
					nsSupport.declarePrefix(prefix, uri);
				}
				// check attributes for required namespace declarations
				NamedNodeMap attrs = node.getAttributes();
				Attr nextAttr;
				String attrName;
				for (int i = 0; i < attrs.getLength(); i++) {
					nextAttr = (Attr) attrs.item(i);
					attrName = nextAttr.getName();
					if (attrName.equals("xmlns")) {
						if (nsSupport.getURI("") == null) {
							uri = nextAttr.getValue();
							namespaceDecls.put("", uri);
							nsSupport.declarePrefix("", uri);
						}
					} else if (attrName.startsWith("xmlns:")) {
						prefix = attrName.substring(6);
						if (nsSupport.getURI(prefix) == null) {
							uri = nextAttr.getValue();
							namespaceDecls.put(prefix, uri);
							nsSupport.declarePrefix(prefix, uri);
						}
					} else if (attrName.indexOf(':') > 0) {
						prefix = nextAttr.getPrefix();
						uri = nextAttr.getNamespaceURI();
						if (nsSupport.getURI(prefix) == null) {
							namespaceDecls.put(prefix, uri);
							nsSupport.declarePrefix(prefix, uri);
						}
					}
				}
				ElementInfo info = new ElementInfo(node);
				String[] declaredPrefixes = null;
				if(namespaceDecls.size() > 0)
					declaredPrefixes = new String[namespaceDecls.size()];
				// output all namespace declarations
				Map.Entry nsEntry;
				int j = 0;
				for (Iterator i = namespaceDecls.entrySet().iterator(); i.hasNext(); j++) {
					nsEntry = (Map.Entry) i.next();
					declaredPrefixes[j] = (String) nsEntry.getKey();
					contentHandler.startPrefixMapping(
						declaredPrefixes[j],
						(String) nsEntry.getValue());
				}
				info.prefixes = declaredPrefixes;
                stack.push(info);
				// output attributes
				AttributesImpl saxAttrs = new AttributesImpl();
				String attrNS, attrLocalName;
				for (int i = 0; i < attrs.getLength(); i++) {
					nextAttr = (Attr) attrs.item(i);
					attrNS = nextAttr.getNamespaceURI();
					if(attrNS == null)
					    attrNS = "";
					attrLocalName = nextAttr.getLocalName();
					if(attrLocalName == null)
					    attrLocalName = QName.extractLocalName(nextAttr.getNodeName());
					saxAttrs.addAttribute(
						attrNS,
						attrLocalName,
						nextAttr.getNodeName(),
						"CDATA",
						nextAttr.getValue());
				}
				String localName = node.getLocalName();
				if(localName == null)
				    localName = QName.extractLocalName(node.getNodeName());
				contentHandler.startElement(node.getNamespaceURI(), localName, 
					node.getNodeName(), saxAttrs);
				break;
			case Node.TEXT_NODE :
			case Node.CDATA_SECTION_NODE :
				cdata = ((CharacterData) node).getData();
				if(lexicalHandler != null)
					lexicalHandler.startCDATA();
				contentHandler.characters(cdata.toCharArray(), 0, cdata.length());
				if(lexicalHandler != null)
					lexicalHandler.endCDATA();
				break;
			case Node.ATTRIBUTE_NODE :
				break;
			case Node.PROCESSING_INSTRUCTION_NODE :
				contentHandler.processingInstruction(
					((ProcessingInstruction) node).getTarget(),
					((ProcessingInstruction) node).getData());
				break;
			case Node.COMMENT_NODE :
				if(lexicalHandler != null) {
					cdata = ((Comment) node).getData();
					lexicalHandler.comment(cdata.toCharArray(), 0, cdata.length());
				}
				break;
			default :
                //TODO : what kind of default here ? -pb
				System.out.println("Found node: " + node.getNodeType());
				break;
		}
	}

	protected void endNode(Node node) throws SAXException {
		if (node == null)
			return;
		if (node.getNodeType() == Node.ELEMENT_NODE) {
            ElementInfo info = (ElementInfo)stack.pop();
			nsSupport.popContext();
			contentHandler.endElement(node.getNamespaceURI(), node.getLocalName(), node.getNodeName());
            if(info.prefixes != null) {
                for(int i = 0; i < info.prefixes.length; i++) {
                    contentHandler.endPrefixMapping(info.prefixes[i]);
                }
            }
		}
	}
    
    private class ElementInfo {
        Node element;
        String[] prefixes = null;
        
        public ElementInfo(Node element) {
            this.element = element;
        }
    }
}