package org.teiid.query.xquery.saxon;

import java.util.LinkedList;
import java.util.List;

import net.sf.saxon.event.ProxyReceiver;
import net.sf.saxon.event.Receiver;
import net.sf.saxon.expr.parser.PathMap.PathMapArc;
import net.sf.saxon.expr.parser.PathMap.PathMapNode;
import net.sf.saxon.expr.parser.PathMap.PathMapRoot;
import net.sf.saxon.om.AxisInfo;
import net.sf.saxon.om.NamespaceBinding;
import net.sf.saxon.om.NodeName;
import net.sf.saxon.pattern.NodeTest;
import net.sf.saxon.trans.XPathException;
import net.sf.saxon.type.SchemaType;
import net.sf.saxon.type.SimpleType;
import net.sf.saxon.type.Type;

import org.teiid.logging.LogConstants;
import org.teiid.logging.LogManager;
import org.teiid.logging.MessageLevel;

/**
 * A filter that uses the PathMap to determine what should be included in the document
 * 
 * TODO: optimize filtering by not reconstructing the matchcontexts
 * TODO: we may still need to do xom/nux style handling of large results, but 
 *       that requires more analysis to determine subtree independence
 */
class PathMapFilter extends ProxyReceiver {
	
	static class MatchContext {
		List<PathMapArc> elementArcs;
		List<PathMapArc> attributeArcs;
		boolean matchedElement;
		boolean matchesText;
		boolean matchesComment;
		
		void bulidContext(PathMapNode node) {
			for (PathMapArc arc : node.getArcs()) {
    			processArc(arc);
			}
		}

		void processArc(PathMapArc arc) {
			NodeTest test = arc.getNodeTest();
			if (test == null) {
				addAnyNodeArc(arc);
			} else {
				switch (test.getPrimitiveType()) {
				case Type.TEXT:
					matchesText = true;
				case Type.NODE:
					addAnyNodeArc(arc);
					break;
				case Type.COMMENT:
					matchesComment = true;
					break;
				case Type.ELEMENT:
					addElementArc(arc);
					break;
				case Type.ATTRIBUTE:
					addAttributeArc(arc);
					break;
				}
			}
		}

		private void addAnyNodeArc(PathMapArc arc) {
			if (arc.getAxis() == AxisInfo.ATTRIBUTE) {
				addAttributeArc(arc);
				return;	
			}
			addElementArc(arc);
			addAttributeArc(arc);
			matchesText = true;
			matchesComment = true;
		}

		private void addAttributeArc(PathMapArc arc) {
			if (attributeArcs == null) {
				attributeArcs = new LinkedList<PathMapArc>();
			}
			attributeArcs.add(arc);
		}

		private void addElementArc(PathMapArc arc) {
			if (elementArcs == null) {
				elementArcs = new LinkedList<PathMapArc>();
			}
			elementArcs.add(arc);
		}
	}

	private boolean closed;
	private LinkedList<MatchContext> matchContext = new LinkedList<MatchContext>();
	private boolean logTrace = LogManager.isMessageToBeRecorded(LogConstants.CTX_RUNTIME, MessageLevel.TRACE);
	
	public PathMapFilter(PathMapRoot root, Receiver receiver) {
		super(receiver);
		MatchContext mc = new MatchContext();
		mc.bulidContext(root);
		matchContext.add(mc);
	}
	
	@Override
	public void startElement(NodeName elemName, SchemaType typeCode,
			int locationId, int properties) throws XPathException {
		MatchContext mc = matchContext.getLast();
		MatchContext newContext = new MatchContext();
		if (mc.elementArcs != null) {
			for (PathMapArc arc : mc.elementArcs) {
				NodeTest test = arc.getNodeTest();
				if (test == null || test.matches(Type.ELEMENT, elemName, typeCode.getFingerprint())) {
					newContext.bulidContext(arc.getTarget());
					newContext.matchedElement = true;
				} 
				if (arc.getAxis() == AxisInfo.DESCENDANT || arc.getAxis() == AxisInfo.DESCENDANT_OR_SELF) {
					newContext.processArc(arc);
				}
			}
		}
		matchContext.add(newContext);
		if (newContext.matchedElement) {
			super.startElement(elemName, typeCode, locationId, properties);
		} else if (logTrace) {
			LogManager.logTrace(LogConstants.CTX_RUNTIME, "Document projection did not match element", elemName.getURI(), ':', elemName.getLocalPart()); //$NON-NLS-1$
		}
	}
	
	@Override
	public void attribute(NodeName nameCode, SimpleType typeCode,
			CharSequence value, int locationId, int properties)
			throws XPathException {
		MatchContext mc = matchContext.getLast();
		if (!mc.matchedElement) {
			return;
		}
		if (nameCode.isInNamespace(javax.xml.XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI)) {
			super.attribute(nameCode, typeCode, value, locationId, properties);
			return;
		}
		if (mc.attributeArcs != null) {
			for (PathMapArc arc : mc.attributeArcs) {
				NodeTest test = arc.getNodeTest();
				if (test == null || test.matches(Type.ATTRIBUTE, nameCode, typeCode.getFingerprint())) {
					super.attribute(nameCode, typeCode, value, locationId, properties);
					return;
				} 
			}
		}
	}

	@Override
	public void characters(CharSequence chars, int locationId,
			int properties) throws XPathException {
		MatchContext context = matchContext.getLast();
		if (context.matchedElement && context.matchesText) {
			super.characters(chars, locationId, properties);
		}
	}

	@Override
	public void comment(CharSequence chars, int locationId,
			int properties) throws XPathException {
		MatchContext context = matchContext.getLast();
		if (context.matchedElement && context.matchesComment) {
			super.comment(chars, locationId, properties);
		}
	}

	@Override
	public void processingInstruction(String target,
			CharSequence data, int locationId, int properties)
			throws XPathException {
		MatchContext context = matchContext.getLast();
		if (context.matchedElement) {
			super.processingInstruction(target, data, locationId, properties);
		}
	}

	@Override
	public void namespace(NamespaceBinding namespaceBinding, int properties)
			throws XPathException {
		MatchContext context = matchContext.getLast();
		if (context.matchedElement) {
			super.namespace(namespaceBinding, properties);
		}
	}

	@Override
	public void endElement() throws XPathException {
		MatchContext context = matchContext.removeLast();
		if (context.matchedElement) {
			super.endElement();
		}
	}
	
	@Override
	public void startContent() throws XPathException {
		MatchContext context = matchContext.getLast();
		if (context.matchedElement) {
			super.startContent();
		}
	}
	
	@Override
	public void close() throws XPathException {
		if (!closed) {
			super.close();
			closed = true;
		}
	}

}