package org.exolab.castor.xml;

import java.util.LinkedList;
import java.util.Queue;

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.Locator;
import org.xml.sax.SAXException;

/**
 * This class is used to record the locator values in every SAX event. The recorded locators then
 * get compared to the locator values from the SAXEventFromStaxProducer.
 * 
 * <p>
 * If they are different then there an assertion is going fail
 * </p>
 * 
 * @implements ContentHandler
 * 
 * @author <a href="mailto:philipp DOT erlacher AT gmail DOT com">Philipp Erlacher</a>
 * 
 */
public class DocumentLocatorRecorder implements ContentHandler {

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

  /**
   * a fifo to record locations
   */
  private Queue<LocationElement> locations = null;

  /**
   * indicatios if we are in recordMode
   */
  private Boolean recordMode;

  public void setReplayMode() {
    recordMode = false;
  }

  /**
   * the SAX locator object
   */
  private Locator locator;

  public DocumentLocatorRecorder() {
    super();
    recordMode = true;
    locations = new LinkedList<LocationElement>();
  }

  /**
   * calls {@link #handleEvent()}
   */
  public void characters(char[] ch, int start, int length) throws SAXException {
    LOG.debug("endDocument");
    handleEvent();
  }

  /**
   * calls {@link #handleEvent()}
   */
  public void endDocument() throws SAXException {
    LOG.debug("endDocument");
    handleEvent();
  }

  /**
   * calls {@link #handleEvent()}
   */
  public void endElement(String uri, String localName, String qName) throws SAXException {
    LOG.debug("endElement");
    handleEvent();
  }

  /**
   * calls {@link #handleEvent()}
   */
  public void endPrefixMapping(String prefix) throws SAXException {
    LOG.debug("endPrefixMapping");
    handleEvent();
  }

  /**
   * calls {@link #handleEvent()}
   */
  public void ignorableWhitespace(char[] ch, int start, int length) throws SAXException {
    LOG.debug("ignorableWhitespace");
    handleEvent();
  }

  /**
   * calls {@link #handleEvent()}
   */
  public void processingInstruction(String target, String data) throws SAXException {
    LOG.debug("processingInstruction");
    handleEvent();
  }

  /**
   * don't handle this event. Check {@link http ://jira.codehaus.org/browse/CASTOR-2962} for more
   * information
   */
  public void setDocumentLocator(Locator locator) {
    LOG.debug("setDocumentLocator");
    this.locator = locator;
  }

  /**
   * calls {@link #handleEvent()}
   */
  public void skippedEntity(String name) throws SAXException {
    LOG.debug("skippedEntity");
    handleEvent();
  }

  /**
   * don't handle this event. Check {@link http ://jira.codehaus.org/browse/CASTOR-2962} for more
   * information
   */
  public void startDocument() throws SAXException {
    LOG.debug("startDocument");
  }

  /**
   * calls {@link #handleEvent()}
   */
  public void startElement(String uri, String localName, String qName, Attributes atts)
      throws SAXException {
    LOG.debug("startElement");
    handleEvent();
  }

  /**
   * calls {@link #handleEvent()}
   */
  public void startPrefixMapping(String prefix, String uri) throws SAXException {
    LOG.debug("startPrefixMapping");
    handleEvent();
  }

  /**
   * if in recordMode then record current location otherwise compare current location to expected
   * location
   */
  private void handleEvent() {
    if (inRecordMode())
      recordLocation(getCurrentLocationElement());
    else
      compareWithLocationElement(getCurrentLocationElement());
  }

  /**
   * are we in record mode ?
   * 
   * @return true if we are, false otherwise
   */
  private boolean inRecordMode() {
    return recordMode;
  }

  /**
   * record a location
   * 
   * @param locationElement location to record
   */
  private void recordLocation(LocationElement locationElement) {
    LOG.debug(
        "record " + locationElement.getColumnNumber() + ":" + locationElement.getLineNumber());
    locations.add(locationElement);

  }

  /**
   * @return Locator the current location as a new LocationElement
   */
  private LocationElement getCurrentLocationElement() {
    return new LocationElement(locator);
  }

  /**
   * compares the expected location with the current location. If they are not equal then an
   * Assertion fails
   * 
   * @param currentLocator the current location
   */
  private void compareWithLocationElement(LocationElement currentLocation) throws AssertionError {
    LOG.debug("comparison: ");
    if (locations.poll().equals(currentLocation))
      return;
    throw new AssertionError("Error !");
  }

  /**
   * A Wrapper for a locator element.
   * 
   * @author <a href="mailto:philipp DOT erlacher AT gmail DOT com">Philipp Erlacher</a>
   * 
   */
  private class LocationElement implements Locator {

    private String systemId;
    private String publicId;
    private int lineNumber;
    private int columnNumber;

    public LocationElement(Locator locator) {
      systemId = locator.getSystemId();
      publicId = locator.getPublicId();
      lineNumber = locator.getLineNumber();
      columnNumber = locator.getColumnNumber();
    }

    public String getSystemId() {
      return systemId;
    }

    public String getPublicId() {
      return publicId;
    }

    public int getLineNumber() {
      return lineNumber;
    }

    public int getColumnNumber() {
      return columnNumber;
    }

    @Override
    public boolean equals(Object that) {
      // check for self-comparison
      if (this == that)
        return true;

      if (!(that instanceof LocationElement))
        return false;

      LocationElement locator = (LocationElement) that;

      // it seems that there is a implementation difference of columnNumber;
      // in the previous version I used +1 wasn't necessary but here it is
      // [PE]
      if (!((columnNumber == locator.getColumnNumber() + 1)
          || (columnNumber == locator.getColumnNumber())))
        return false;
      if (lineNumber != locator.getLineNumber())
        return false;
      if (!nonEmpty(publicId).equals(nonEmpty(locator.getPublicId())))
        return false;
      if (!nonEmpty(systemId).equals(nonEmpty(locator.getSystemId())))
        return false;

      return true;
    }

    private String nonEmpty(String string) {
      return string == null ? "" : string;
    }
  };

}
