package com.jclark.xsl.sax;

import java.net.URL;
import java.net.MalformedURLException;
import java.io.IOException;
import java.util.Hashtable;
import org.xml.sax.*;
import com.jclark.xsl.om.*;
import com.jclark.xsl.tr.Result;
import com.jclark.xsl.tr.LoadContext;

public class XMLProcessorImpl implements XMLProcessorEx {

  static private abstract
  class NodeImpl implements Node {
    ContainerNodeImpl parent;
    RootNodeImpl root;
    int index;
    NodeImpl nextSibling;

    NodeImpl() {
      this.index = 0;
      this.parent = null;
      this.nextSibling = null;
    }
    NodeImpl(int index, ContainerNodeImpl parent) {
      this.index = index;
      this.parent = parent;
      this.root = parent.root;
      this.nextSibling = null;
      if (parent.lastChild == null)
	parent.firstChild = parent.lastChild = this;
      else {
	parent.lastChild.nextSibling = this;
	parent.lastChild = this;
      }
    }
    public Node getParent() {
      return parent;
    }
    public SafeNodeIterator getFollowingSiblings() {
      return new NodeIteratorImpl(nextSibling);
    }
    public URL getURL() {
      return parent.getURL();
    }
    public int getLineNumber() {
      return parent.getLineNumber();
    }
    boolean canStrip() {
      return false;
    }
    public Node getAttribute(Name name) {
      return null;
    }
    public String getAttributeValue(Name name) {
      return null;
    }
    public SafeNodeIterator getAttributes() {
      return new NodeIteratorImpl(null);
    }
    public Name getName() {
      return null;
    }
    public NamespacePrefixMap getNamespacePrefixMap() {
      return parent.nsMap;
    }
    public int compareTo(Node node) {
      NodeImpl ni = (NodeImpl)node;
      if (root == ni.root)
	return index - ((NodeImpl)node).index;
      return root.compareRootTo(ni.root);
    }
    public Node getElementWithId(String name) {
      return root.getElementWithId(name);
    }
    public String getUnparsedEntityURI(String name) {
      return root.getUnparsedEntityURI(name);
    }
    public boolean isId(String name) {
      return false;
    }
    public String getGeneratedId() {
      int d = root.getDocumentIndex();
      if (d == 0)
	return "N" + String.valueOf(index);
      else
	return "N" + String.valueOf(d) + "_" + String.valueOf(index);
    }
    public Node getRoot() {
      return root;
    }
  }

  static private
  class NodeIteratorImpl implements SafeNodeIterator {
    private NodeImpl nextNode;

    NodeIteratorImpl(NodeImpl nextNode) {
      this.nextNode = nextNode;
    }

    public Node next() {
      NodeImpl tem = nextNode;
      if (tem != null)
	nextNode = tem.nextSibling;
      return tem;
    }
  }

  static private abstract
  class ContainerNodeImpl extends NodeImpl {
    NodeImpl firstChild;
    NodeImpl lastChild;
    NamespacePrefixMap nsMap;

    ContainerNodeImpl(NamespacePrefixMap nsMap) {
      this.nsMap = nsMap;
    }

    ContainerNodeImpl(int index, ContainerNodeImpl parent) {
      super(index, parent);
      nsMap = parent.nsMap;
    }

    public SafeNodeIterator getChildren() {
      return new NodeIteratorImpl(firstChild);
    }

    public String getData() {
      return null;
    }

    boolean preserveSpace() {
      return false;
    }

    public NamespacePrefixMap getNamespacePrefixMap() {
      return nsMap;
    }

    void addId(String id, NodeImpl node) {
      parent.addId(id, node);
    }
  }

  static private
  class RootNodeImpl extends ContainerNodeImpl {
    private String systemId;
    private int documentIndex;
    private Hashtable idTable = new Hashtable();
    private Hashtable unparsedEntityURITable = new Hashtable();

    RootNodeImpl(String systemId, int documentIndex, NamespacePrefixMap nsMap) {
      super(nsMap);
      this.systemId = systemId;
      this.documentIndex = documentIndex;
      this.root = this;
    }
    public byte getType() {
      return Node.ROOT;
    }
    public URL getURL() {
      if (systemId != null) {
	try {
	  return new URL(systemId);
	}
	catch (MalformedURLException e) { }
      }
      return null;
    }
    public int getLineNumber() {
      return 1;
    }

    public Node getElementWithId(String name) {
      return (Node)idTable.get(name);
    }

    void addId(String id, NodeImpl node) {
      if (idTable.get(id) == null)
	idTable.put(id, node);
    }

    public String getUnparsedEntityURI(String name) {
      return (String)unparsedEntityURITable.get(name);
    }

    int compareRootTo(RootNodeImpl r) {
      if (systemId == null) {
	if (r.systemId == null)
	  return documentIndex - r.documentIndex;
	return -1;
      }
      else if (r.systemId == null)
	return 1;
      int n = systemId.compareTo(r.systemId);
      if (n != 0)
	return n;
      return documentIndex - r.documentIndex;
    }
    int getDocumentIndex() {
      return documentIndex;
    }

  }

  static private class NullLocator implements Locator {
    public String getPublicId() { return null; }
    public String getSystemId() { return null; }
    public int getLineNumber() { return -1; }
    public int getColumnNumber() { return -1; }
  }

  static private
  class ElementNodeImpl extends ContainerNodeImpl {
    private Name name;
    private Object[] atts;
    private int lineNumber;
    private String systemId;

    ElementNodeImpl(String name, AttributeList attList, Locator loc,
		    int index, ContainerNodeImpl parent) throws XSLException {
      super(index, parent);
      lineNumber = loc.getLineNumber();
      systemId = loc.getSystemId();
      int nAtts = attList.getLength();
      if (nAtts > 0) {
	int nNsAtts = 0;
	for (int i = 0; i < nAtts; i++) {
	  String tem = attList.getName(i);
	  if (tem.startsWith("xmlns")) {
	    nNsAtts++;
	    if (tem.length() == 5) {
	      String ns = attList.getValue(i);
	      if (ns.length() == 0)
		nsMap = nsMap.unbindDefault();
	      else
		nsMap = nsMap.bindDefault(ns);
	    }
	    else if (tem.charAt(5) == ':')
	      nsMap = nsMap.bind(tem.substring(6),
				 attList.getValue(i));
	  }
	}
	int n = nAtts - nNsAtts;
	if (n > 0) {
	  Object[] vec = new Object[n*2];
	  int j = 0;
	  for (int i = 0; i < nAtts; i++) {
	    String tem = attList.getName(i);
	    if (!tem.startsWith("xmlns")) {
	      vec[j++] = nsMap.expandAttributeName(tem, this);
	      // FIXME resolve relative URL
	      vec[j++] = attList.getValue(i);
	    }
	    if (attList.getType(i).length() == 2)
	      parent.addId(attList.getValue(i), this);
	  }
	  // Assign here to avoid inconsistent state if exception
	  // is thrown.
	  atts = vec;
	}
      }
      this.name = nsMap.expandElementTypeName(name, this);
    }

    public Name getName() {
      return name;
    }

    public byte getType() {
      return Node.ELEMENT;
    }

    public SafeNodeIterator getAttributes() {
      return new SafeNodeIterator() {
	private int i = 0;
	public Node next() {
	  if (atts == null)
	    return null;
	  int i2 = i*2;
	  if (i2 == atts.length)
	    return null;
	  return new AttributeNodeImpl(index + ++i,
				       ElementNodeImpl.this,
				       (Name)atts[i2],
				       (String)atts[i2 + 1]);
	}
      };
    }

    public Node getAttribute(Name name) {
      if (atts != null) {
	for (int i = 0; i < atts.length; i += 2)
	  if (atts[i].equals(name))
	    return new AttributeNodeImpl(this.index + (i >> 1) + 1,
					 this,
					 name,
					 (String)atts[i + 1]);
      }
      return null;
    }

    public String getAttributeValue(Name name) {
      if (atts != null) {
	for (int i = 0; i < atts.length; i += 2)
	  if (atts[i].equals(name))
	    return (String)atts[i + 1];
      }
      return null;
    }

    public int getLineNumber() {
      return lineNumber;
    }

    public URL getURL() {
      if (systemId != null) {
	try {
	  return new URL(systemId);
	}
	catch (MalformedURLException e) { }
      }
      return null;
    }

    public boolean isId(String name) {
      return this.equals(getElementWithId(name));
    }
  }

  static private
  class PreserveElementNodeImpl extends ElementNodeImpl {
    PreserveElementNodeImpl(String name, AttributeList atts, Locator loc,
			    int index, ContainerNodeImpl parent)  throws XSLException {
      super(name, atts, loc, index, parent);
    }
    boolean preserveSpace() {
      return true;
    }
  }

  static private
  class AttributeNodeImpl extends NodeImpl {
    private Name name;
    private String value;
    AttributeNodeImpl(int index, ContainerNodeImpl parent, Name name, String value) {
      // Don't use super(parent) because it add's this node to the children.
      this.index = index;
      this.parent = parent;
      this.root = parent.root;
      this.name = name;
      this.value = value;
    }
    public byte getType() {
      return Node.ATTRIBUTE;
    }
    public String getData() {
      return value;
    }
    public Name getName() {
      return this.name;
    }
    public SafeNodeIterator getChildren() {
      return new NodeIteratorImpl(null);
    }
    public int hashCode() {
      return index;
    }
    public boolean equals(Object obj) {
      return (obj != null
	      && obj instanceof AttributeNodeImpl
	      && ((AttributeNodeImpl)obj).index == index);
    }
  }

  static private
  class TextNodeImpl extends NodeImpl {
    private String data;
     
    public TextNodeImpl(char[] buf, int off, int len, int index, ContainerNodeImpl parent) {
      super(index, parent);
      data = new String(buf, off, len);
    }
    public byte getType() {
      return Node.TEXT;
    }
    public String getData() {
      return data;
    }
    public SafeNodeIterator getChildren() {
      return new NodeIteratorImpl(null);
    }

  }

  static private
  class StripTextNodeImpl extends TextNodeImpl {
    public StripTextNodeImpl(char[] buf, int off, int len, int index, ContainerNodeImpl parent) {
      super(buf, off, len, index, parent);
    }
    boolean canStrip() {
      return true;
    }
  }

  static private
  class CommentNodeImpl extends NodeImpl {
    private String data;
     
    public CommentNodeImpl(String data, int index, ContainerNodeImpl parent) {
      super(index, parent);
      this.data = data;
    }
    public byte getType() {
      return Node.COMMENT;
    }
    public String getData() {
      return data;
    }
    public SafeNodeIterator getChildren() {
      return new NodeIteratorImpl(null);
    }

  }

  static private
  class ProcessingInstructionNodeImpl extends NodeImpl {
    private Name name;
    private String data;
    private String systemId;
     
    // FIXME should include location

    public ProcessingInstructionNodeImpl(String name, String data, Locator loc, int index, ContainerNodeImpl parent) {
      super(index, parent);
      systemId = loc.getSystemId();
      this.name = parent.getNamespacePrefixMap().getNameTable().createName(name);
      this.data = data;
    }

    public Name getName() {
      return name;
    }

    public byte getType() {
      return Node.PROCESSING_INSTRUCTION;
    }

    public String getData() {
      return data;
    }

    public SafeNodeIterator getChildren() {
      return new NodeIteratorImpl(null);
    }
    public URL getURL() {
      if (systemId != null) {
	try {
	  return new URL(systemId);
	}
	catch (MalformedURLException e) { }
      }
      return null;
    }

  }


  static public
  interface Builder extends DocumentHandler, CommentHandler, DTDHandler {
    public Node getRootNode();
  }

  static private
  class BuilderImpl implements Builder {
    char[] dataBuf = new char[1024];
    int dataBufUsed = 0;
    RootNodeImpl rootNode;
    ContainerNodeImpl currentNode;
    int currentIndex = 1;
    boolean includeProcessingInstructions;
    boolean includeComments;
    LoadContext context;
    Locator locator = new NullLocator();

    BuilderImpl(LoadContext context, String systemId, int documentIndex,  NamespacePrefixMap nsMap) {
      this.context = context;
      includeProcessingInstructions = context.getIncludeProcessingInstructions();
      includeComments = context.getIncludeComments();
      currentNode = rootNode = new RootNodeImpl(systemId,
						documentIndex,
						nsMap);
    }

    public void startDocument() { }

    public void endDocument() { }

    public void setDocumentLocator(Locator locator) {
      this.locator = locator;
    }

    public void startElement(String name, AttributeList atts) throws SAXException {
      flushData();
      ElementNodeImpl element;
      boolean preserve;
      String space = atts.getValue("xml:space");
      if (space == null)
	preserve = currentNode.preserveSpace();
      else if (space.equals("default"))
	preserve = false;
      else if (space.equals("preserve"))
	preserve = true;
      else
	preserve = currentNode.preserveSpace();
      try {
	if (preserve)
	  element = new PreserveElementNodeImpl(name, atts, locator,
						currentIndex++, currentNode);
	else
	  element = new ElementNodeImpl(name, atts, locator,
					currentIndex++, currentNode);
      }
      catch (XSLException e) {
	throw new SAXException(e);
      }
      currentIndex += atts.getLength();
      currentNode = element;
    }

    public void characters(char ch[], int start, int length) {
      int need = length + dataBufUsed;
      if (need > dataBuf.length) {
	int newLength = dataBuf.length << 1;
	while (need > newLength)
	  newLength <<= 1;
	char[] tem = dataBuf;
	dataBuf = new char[newLength];
	if (dataBufUsed > 0)
	  System.arraycopy(tem, 0, dataBuf, 0, dataBufUsed);
      }
      for (; length > 0; length--)
	dataBuf[dataBufUsed++] = ch[start++];
    }

    public void ignorableWhitespace(char ch[], int start, int length) {
      if (dataBufUsed > 0
	  || currentNode.preserveSpace()
	  || !context.getStripSource(currentNode.getName()))
	characters(ch, start, length);
    }

    public void endElement(String name) {
      flushData();
      currentNode = currentNode.parent;
    }

    public void processingInstruction(String target, String data) {
      if (target == null)
	comment(data);
      else {
	if (includeProcessingInstructions) {
	  flushData();
	  new ProcessingInstructionNodeImpl(target, data, locator, currentIndex++, currentNode);
	}
      }
    }

    public void comment(String contents) {
      if (includeComments) {
	flushData();
	new CommentNodeImpl(contents, currentIndex++, currentNode);
      }
    }

    public void unparsedEntityDecl(String name,
				   String publicId,
				   String systemId,
				   String notationName) {
      rootNode.unparsedEntityURITable.put(name, systemId);
    }

    public void notationDecl(String name,
			     String publicId,
			     String systemId) {
    }

    public Node getRootNode() {
      return rootNode;
    }

    private static boolean isWhitespace(char[] buf, int len) {
      for (int i = 0; i < len; i++) {
	switch (buf[i]) {
	case ' ':
	case '\n':
	case '\r':
	case '\t':
	  break;
	default:
	  return false;
	}
      }
      return true;
    }

    private final void flushData() {
      if (dataBufUsed > 0) {
	if (!isWhitespace(dataBuf, dataBufUsed)
	    || currentNode.preserveSpace()
	    || !context.getStripSource(currentNode.getName()))
	  new TextNodeImpl(dataBuf, 0, dataBufUsed, currentIndex++, currentNode);
	dataBufUsed = 0;
      }
    }
  }

  public Node load(InputSource source,
		   int documentIndex,
		   LoadContext context,
		   NameTable nameTable)
    throws IOException, XSLException {
    try {
      Builder builder = new BuilderImpl(context,
					source.getSystemId(),
					documentIndex,
					nameTable.getEmptyNamespacePrefixMap());
      parser.setDocumentHandler(builder);
      parser.setDTDHandler(builder);
      parser.parse(source);
      return builder.getRootNode();
    }
    catch (SAXParseException e) {
      throw new XSLException(e);
    }
    catch (SAXException e) {
      Exception wrapped = e.getException();
      if (wrapped == null)
	throw new XSLException(e.getMessage());
      if (wrapped instanceof XSLException)
	throw (XSLException)e.getException();
      throw new XSLException(wrapped);
    }
  }

  public Node load(URL url, int documentIndex, LoadContext context, NameTable nameTable) throws IOException, XSLException {
    return load(new InputSource(url.toString()),
		documentIndex,
	        context,
		nameTable);
  }

  static
  public Builder createBuilder(String systemId, int documentIndex, LoadContext context, NameTable nameTable) {
    return new BuilderImpl(context,
			   systemId,
			   documentIndex,
			   nameTable.getEmptyNamespacePrefixMap());
  }

  public Result createResult(Node baseNode,
			     int documentIndex,
			     LoadContext loadContext,
			     Node[] rootNode) throws XSLException {
    URL baseURL = null;
    if (baseNode != null)
      baseURL = baseNode.getURL();
    String base;
    if (baseURL == null)
      base = null;
    else
      base = baseURL.toString();
    XMLProcessorImpl.Builder builder
      = XMLProcessorImpl.createBuilder(base,
				       documentIndex,
				       loadContext,
				       baseNode.getNamespacePrefixMap().getNameTable());
    rootNode[0] = builder.getRootNode();
    return new MultiNamespaceResult(builder, errorHandler);
  }

  private Parser parser;
  private ErrorHandler errorHandler;

  public XMLProcessorImpl(Parser parser) {
    this.parser = parser;
  }
  
  public void setErrorHandler(ErrorHandler errorHandler) {
    this.errorHandler = errorHandler;
  }
}
