package com.sun.msv.schematron.verifier;

import com.sun.msv.schematron.grammar.SAction;
import com.sun.msv.schematron.grammar.SActions;
import com.sun.msv.schematron.grammar.SElementExp;
import com.sun.msv.schematron.grammar.SRule;
import com.sun.msv.schematron.util.DOMBuilder;
import com.sun.msv.verifier.DocumentDeclaration;
import com.sun.msv.verifier.IVerifier;
import com.sun.msv.verifier.ValidityViolation;
import com.sun.msv.verifier.Verifier;
import com.sun.msv.verifier.VerifierFilter;
import org.apache.xml.utils.PrefixResolverDefault;
import org.apache.xpath.XPathContext;
import org.relaxng.datatype.Datatype;
import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.Attributes;
import org.xml.sax.ErrorHandler;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.LocatorImpl;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.TransformerException;
import java.util.HashMap;
import java.util.Map;
import java.util.Stack;

public class RelmesVerifier implements IVerifier {
	
	public RelmesVerifier( IVerifier core ) throws ParserConfigurationException {
		this.core = new VerifierFilter(core);
		this.core.setContentHandler(new SchematronVerifier());
	}
	public RelmesVerifier( DocumentDeclaration docDecl, ErrorHandler handler )
				throws ParserConfigurationException {
		this(new Verifier(docDecl,handler));
	}
	
	/**
	 * this object performs RELAX NG validation,
	 * then pass type-information to the schematron checker.
	 */
	private final VerifierFilter core;
	
	/**
	 * this flag is set to true when all schematron validations are successful.
	 */
	private boolean schematronValid;

	/** performs schematron validation. */
	class SchematronVerifier extends DOMBuilder {
		
        /**
         * ID -> value map. We can't trust DOM to do this for us.
         */
        private final Map idMap = new HashMap();
        
        XPathContext xctxt = new XPathContext();


		SchematronVerifier() throws ParserConfigurationException {}
		
		public void startElement( String ns, String local, String qname, Attributes atts ) throws SAXException {
			super.startElement(ns,local,qname,atts);
			locationMap.put( super.parent, new Loc(getLocator()) );
			
			Object o = getCurrentElementType();
			if( o instanceof SElementExp ) {
//				System.out.println("Schematron node found");
				// memorize this node so that we can check it later.
				checks.put( super.parent, o );
			}
            
            // build ID map
            for( int i=0; i<atts.getLength(); i++ ) {
                if( "ID".equals(atts.getType(i)) ) {
                    // since there's no way to achieve this via DOM,
                    // we need to directly talk to the implementation.
                    try {
                        // Xerces?
                        ((org.apache.xerces.dom.CoreDocumentImpl)super.dom).putIdentifier(
                            atts.getValue(i), (Element)super.parent );
                    } catch( Throwable t ) {
                        // ignore any error. it wasn't Xerces.
                    }
                    try {
                        // Crimson?
                        ((org.apache.crimson.tree.ElementNode2)super.parent)
                            .setIdAttributeName(atts.getQName(i));
                    } catch( Throwable t ) {
                        // ignore any error. it wasn't Crimson.
                    }
                }
            }
		}
		
		/**
		 * a map from Element to Loc object.
		 * This map will be used to report the source of error.
		 */
		private final Map locationMap = new HashMap();
		
		/**
		 * a map from Node to SElementExp.
		 * These are checked later.
		 */
		private final Map checks = new HashMap();
		
		public void startDocument() throws SAXException {
			super.startDocument();
			checks.clear();
			locationMap.clear();
		}
		
		public void endDocument() throws SAXException {
			super.endDocument();
			schematronValid = true;

			try {
				testNode(super.dom);
			} catch( TransformerException e ) {
				getErrorHandler().error( new ValidityViolation(
					null, "XPath error:"+e.getMessage(), null ) );
				schematronValid = false;
			}
		}
		
		/** SRule objects that are currently in effect. */
		private final Stack effectiveRules = new Stack();
		
		private void testNode( Node node ) throws SAXException, TransformerException {
			
			// if this node has the corresponding rule to be checked,
			// push it to the stack.
			// if we already have the same rule object, there is no need to
			// add it twice.
			SElementExp exp = (SElementExp)checks.get(node);
//			System.out.println("node tested");
			int numRulesAdded = 0;
			if(exp!=null ) {
//				System.out.println("rule added");
				for( int i=0; i<exp.rules.length; i++ ) {
					if(!effectiveRules.contains(exp.rules)) {
						effectiveRules.push(exp.rules[i]);
						numRulesAdded++;
					}
				}
                // if the element decl has any direction actions, run them now.
                testActions( exp.actions, node );
			}
			
			// test effective rules against this node
			int len = effectiveRules.size();
			for( int i=0; i<len; i++ )
				testRule( (SRule)effectiveRules.get(i), node );
			
			// recursively process children
			if( node.getNodeType()==Node.ELEMENT_NODE ) {
				Element e = (Element)node;
				NamedNodeMap atts = e.getAttributes();
				len = atts.getLength();
				for( int i=0; i<len; i++ )
					testNode( atts.item(i) );
			}
			
			NodeList children = node.getChildNodes();
			len = children.getLength();
			for( int i=0; i<len; i++ )
				testNode( children.item(i) );
			
			// a rule is in effect only in itself or descendants.
			for( ; numRulesAdded>0; numRulesAdded-- )
				effectiveRules.pop();
		}
		
		/**
		 * tests the specified rule against the node.
		 */
		private void testRule( SRule rule, Node node )
					throws SAXException, TransformerException {
			
			if( !rule.matches(xctxt,node) )	return;
            
            testActions(rule,node);
        }
                    
                    
		private void testActions( SActions actions, Node node )	
                throws SAXException, TransformerException {
//			System.out.println("rule tested");
			
			PrefixResolverDefault resolver = new PrefixResolverDefault(node);

            synchronized(actions) {
				// I'm not sure whether XPath object is thread-safe.
				// so for precaution, synchronize it.
                
                // I'm not sure if XPathContextImpl is reusable,
                // so again just for precaution, we will create new ones each time
				
				for( int i=0; i<actions.asserts.length; i++ )
                    if (!actions.asserts[i].xpath.execute(
                        xctxt, node, resolver).bool())
                        reportError(node, actions.asserts[i]);
							
				for( int i=0; i<actions.reports.length; i++ )
					if(actions.reports[i].xpath.execute(
						xctxt, node, resolver ).bool() )
						reportError( node, actions.reports[i] );
			}					
		}

		private void reportError( Node node, SAction action ) throws SAXException {
			Loc loc = (Loc)locationMap.get(node);
            if( loc==null ) {
                if( node instanceof Attr ) {
                    reportError( ((Attr)node).getOwnerElement(), action );
                    return;
                }
                if( node.getParentNode()!=null ) {
				    reportError( node.getParentNode(), action );
				    return;
                }
			}
			
			LocatorImpl src = new LocatorImpl();
            if(loc!=null) {
			    src.setLineNumber(loc.line);
			    src.setColumnNumber(loc.col);
            }
			src.setSystemId(getLocator().getSystemId());
			src.setPublicId(getLocator().getPublicId());
			
			schematronValid = false;
			getErrorHandler().error( new ValidityViolation(
				src, action.document, null ));
		}
	}
	
	
	
	
	public void setPanicMode(boolean v) {
		core.setPanicMode(v);
	}
	public boolean isValid() {
		return schematronValid && core.isValid();
	}
	public Object getCurrentElementType() {
		return core.getCurrentElementType();
	}
	public Datatype[] getLastCharacterType() {
		return core.getLastCharacterType();
	}
	public final Locator getLocator() {
		return core.getLocator();
	}
	public final ErrorHandler getErrorHandler() {
		return core.getErrorHandler();
	}
	public final void setErrorHandler( ErrorHandler handler ) {
		core.setErrorHandler(handler);
	}

	
	
	
	
	
	
    public void setDocumentLocator(Locator locator) {
		core.setDocumentLocator(locator);
    }

    public void startDocument() throws SAXException {
		core.startDocument();
    }

    public void endDocument() throws SAXException {
		core.endDocument();
    }

    public void startPrefixMapping( String prefix, String uri ) throws SAXException {
		core.startPrefixMapping(prefix, uri);
    }

    public void endPrefixMapping(String prefix) throws SAXException {
		core.endPrefixMapping(prefix);
    }

    public void startElement( String namespaceURI, String localName, String qName, Attributes atts ) throws SAXException {
		core.startElement(namespaceURI, localName, qName, atts);
    }

    public void endElement(	String namespaceURI, String localName, String qName ) throws SAXException {
		core.endElement(namespaceURI, localName, qName);
    }

    public void characters( char ch[], int start, int length ) throws SAXException {
		core.characters(ch, start, length);
    }

    public void ignorableWhitespace( char ch[], int start, int length ) throws SAXException {
		core.ignorableWhitespace(ch, start, length);
    }

    public void processingInstruction(String target, String data) throws SAXException {
		core.processingInstruction(target, data);
    }

    public void skippedEntity(String name) throws SAXException {
		core.skippedEntity(name);
    }
}
