package org.exolab.castor.xml;

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

import javax.xml.namespace.QName;
import javax.xml.stream.Location;
import javax.xml.stream.XMLEventReader;
import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamReader;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.ErrorHandler;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;

/**
 * This provides shared code for {@link Sax2EventFromStaxEventProducer} and
 * {@link Sax2EventFromStaxStreamProducer}. It consumes StAX events and 
 * produces SAX2 events.
 * 
 * @author <a href="mailto:philipp DOT erlacher AT gmail DOT com">Philipp
 *         Erlacher</a>
 * 
 */
public abstract class BaseSax2EventFromStaxProducer implements
        SAX2EventAndErrorProducer {

    /**
     * Logger from commons-logging.
     */
    private static final Log LOG = LogFactory
            .getLog(BaseSax2EventFromStaxProducer.class);

    /**
     * A stack to keep track when it's time to invoke endPrefixMapping
     */
    private Stack<List<String>> prefixes = new Stack<List<String>>();


    /**
     * On this interface the SAX methods get invoked
     */
    private ContentHandler contentHandler;

    /**
     * Callback Interface to handle errors
     */
    private ErrorHandler errorHandler;

    public static SAX2EventAndErrorProducer createSax2EventFromStax(
            XMLStreamReader streamReader) {
        return new Sax2EventFromStaxStreamProducer(streamReader);
    }

    public static SAX2EventAndErrorProducer createSax2EventFromStax(
            XMLEventReader eventReader) {
        return new Sax2EventFromStaxEventProducer(eventReader);
    }

    public void setContentHandler(ContentHandler contentHandler) {
        this.contentHandler = contentHandler;
    }

    public void setErrorHandler(ErrorHandler errorHandler) {
        this.errorHandler = errorHandler;
    }

    public Stack<List<String>> getPrefixes() {
        return prefixes;
    }

    public ContentHandler getContentHandler() {
        return contentHandler;
    }

    public ErrorHandler getErrorHandler() {
        return errorHandler;
    }

    /**
     * This method takes an eventType and invokes a method to handle that event.
     * 
     * <p>
     * It also takes information about the depth of the read element. Maybe
     * depth changes due to handling that event.
     * </p>
     * 
     * @param eventType
     *            The event type
     * @param depth
     *            The current depth of the element
     * @return depth The updated depth
     * @throws SAXException
     */
    int handleEventType(int eventType, int depth) throws SAXException {
        switch (eventType) {
        case XMLStreamConstants.START_ELEMENT:
            handleStartElement();
            return ++depth;
        case XMLStreamConstants.END_ELEMENT:
            handleEndElement();
            return --depth;
        case XMLStreamConstants.START_DOCUMENT:
            handleStartDocument();
            return ++depth;
        case XMLStreamConstants.END_DOCUMENT:
            handleEndDocument();
            return --depth;
        case XMLStreamConstants.CHARACTERS:
            handleCharacters();
            return depth;
        case XMLStreamConstants.SPACE:
            handleSpace();
            return depth;
        default:
            return depth;
        }
    }

    /**
     * Invoke {@link #handleDocumentLocator()} and {@link
     * getContentHandler().startDocument()};
     * 
     * @throws SAXException
     */
    void handleStartDocument() throws SAXException {
        LOG.info("< handleStartDocument >");

        handleDocumentLocator();

        contentHandler.startDocument();
    }

    /**
     * Handles a end document event.
     * <p>
     * Invoke {@link getContentHandler().endDocument()};
     * </p>
     * 
     * @throws SAXException
     */
    void handleEndDocument() throws SAXException {
        LOG.info("< handleEndDocument >");
        contentHandler.endDocument();

    }

    /**
     * Handles a start element event.
     * <p>
     * Invoke {@link #doStartPrefixMapping()} and {@link
     * getContentHandler().startElement()};
     * </p>
     * 
     * @throws SAXException
     */
    void handleStartElement() throws SAXException {
        LOG.info("< handleStartElement >");

        QName qName = getQName();
        String localName = qName.getLocalPart();
        String uri = qName.getNamespaceURI();
        String prefix = qName.getPrefix();
        String qNameString = getQName(prefix, localName);

        Attributes atts = getAttributes();

        doStartPrefixMapping();

        contentHandler.startElement(uri, localName, qNameString, atts);

    }

    /**
     * Handles an end element event.
     * <p>
     * Invoke {@link getContentHandler().endElement()} and
     * {@link #doEndPrefixMapping()};
     * </p>
     * 
     * @throws SAXException
     */
    void handleEndElement() throws SAXException {
        LOG.info("< handleEndElement >");

        QName qName = getQName();
        String localName = qName.getLocalPart();
        String uri = qName.getNamespaceURI();
        String prefix = qName.getPrefix();
        String qNameString = getQName(prefix, localName);

        contentHandler.endElement(uri, localName, qNameString);

        doEndPrefixMapping();
    }

    /**
     * Handles a space event.
     * 
     * @throws SAXException
     */
    void handleSpace() throws SAXException {

    }

    /**
     * Handles a character event.
     * <p>
     * If chars is ignorable whitespace {@link
     * getContentHandler().ignorableWhitespace will be called. Otherwise {@link
     * getContentHandler().characters()} will be called with characters(char[],
     * 0, length)
     * 
     * </p>
     * 
     * @throws SAXException
     */
    void handleCharacters() throws SAXException {
        LOG.info("< handleCharacters >");
        char[] chars;
        chars = getCharacters();

        if (isIgnorableWhitespace(chars, 0, chars.length))
            contentHandler.ignorableWhitespace(chars, 0, chars.length);
        else
            contentHandler.characters(chars, 0, chars.length);

    }

    /**
     * @param prefix
     * @param localPart
     * @return qName. If prefix length >=1 then it's like prefix:localPart,
     *         otherwise it's just the localPart
     */
    String getQName(String prefix, String localPart) {
        return getNonEmpty(prefix).length() >= 1 ? prefix + ":" + localPart
                : localPart;
    }

    /**
     * If a chars without leading and trailing whitespaces would be empty, this
     * method returns true, otherwise false,
     * 
     * @param chars
     * @param start
     *            the offset
     * @param length
     * @return
     */
    boolean isIgnorableWhitespace(char[] chars, int start, int length) {
        StringBuffer buffer = new StringBuffer();
        buffer.append(chars, start, length);
        String string = buffer.toString();
        if (string.trim().length() == 0)
            return true;
        return false;
    }

    /**
     * If string equals null this returns an empty string, otherwise it returns
     * the string
     * 
     * @param string
     *            the string to check
     * @return a string. If string equals null this returns an empty string,
     *         otherwise it returns the string
     */
    String getNonEmpty(String string) {
        return string == null ? "" : string;
    }

    /**
     * @return a Location
     */
    abstract Location getLocation();

    /**
     * @return characters of the current event.
     */
    abstract char[] getCharacters();

    /**
     * For every declared namespace in the current event {@link
     * getContentHandler().startPrefixMapping()} gets invoked.
     * 
     * @throws SAXException
     */
    abstract void doStartPrefixMapping() throws SAXException;

    /**
     * 
     * @throws SAXException
     */
    abstract void doEndPrefixMapping() throws SAXException;

    /**
     * @return attributes of the current event
     */
    abstract Attributes getAttributes();

    /**
     * @return QName of the current event
     */
    abstract QName getQName();

    /**
     * If {@link #getLocation()} gets a location {@link
     * getContentHandler().setDocumentLocator()} will be called, otherwise not.
     */
    private void handleDocumentLocator() {
        Locator locator = getSAXLocator(getLocation());
        contentHandler.setDocumentLocator(locator);
    }

    /**
     * Gets a {@link org.xml.sax.Locator} to a given {@link Location}.
     * 
     * @param location
     *            A {@link Location}
     * @return A {@link Locator}
     */
    protected Locator getSAXLocator(Location location) {
        return new Locator() {
            public String getSystemId() {
                return getLocation().getSystemId();
            }

            public String getPublicId() {
                return getLocation().getPublicId();
            }

            public int getLineNumber() {
                return getLocation().getLineNumber();
            }

            public int getColumnNumber() {
                return getLocation().getColumnNumber();
            }
        };
    }
}
