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) {
    String string = new String(chars, start, length);
    return string.trim().length() == 0;
  }

  /**
   * 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();
      }
    };
  }
}
