/*******************************************************************************
 * Copyright (c) 2006 Oracle Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     Oracle Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.bpel.validator.xpath;

/**
 * Java JDK dependencies ...
 */
import java.util.Collections;
import java.util.List;
import java.util.Stack;

import javax.xml.namespace.QName;

/**
 * Dependency on the validation model 
 */
import org.eclipse.bpel.validator.model.IConstants;
import org.eclipse.bpel.validator.model.IModelQuery;
import org.eclipse.bpel.validator.model.IModelQueryLookups;
import org.eclipse.bpel.validator.model.INode;
import org.eclipse.bpel.validator.model.IProblem;



/**
 * Dependency on XPath 1.0 semantics tree model 
 */

import org.eclipse.bpel.xpath10.*;

/**
 * Dependency on the ParserTool
 */



/**
 * This XPathExprValidationVisitor performs minimal statical analysis on
 * the XPath expression that is given to it.
 * <p>
 * Java types are used and mapped onto XPath types in the following way:
 * <ol>
 * <li> String.class   is XPath literal
 * <li> Number.class   is XPath number
 * <li> Boolean.class  is XPath boolean
 * <li> List.class     is a NodeSet
 * <li> Object.class   is a Node
 * </ol> 
 * 
 * @author Michal Chmielewski (michal.chmielewski@oracle.com)
 * @author Alex Yiu (alex.yiu@oracle.com)
 * 
 * @date Sep 29, 2006
 *
 */

@SuppressWarnings({"nls","boxing"})

public class XPathVisitor  {
	
	// NamespaceContext mNamespaceContext;

	/** The XPathExpression Validator that is running this */
	XPathValidator mValidator ;
	
	/** The IModelQuery to ask certain things of the model */
	IModelQuery mModelQuery;
	
	/** The context node on which the expression is attached to */
	INode mContextNode ;
	
	
	// The four types that XPath evaluates into.
	/** An object representing an XPath boolean */ 
	// static final public Object BOOLEAN = new String("#boolean");
	// static final public Object NUMBER  = new String("#number");
	// static final public Object TEXT = new String("#literal");
	// static final public Object Collections.EMPTY_LIST = new String("#node-set");	
	 
	/** For static checking */
	Stack<Object> mContext = new Stack<Object>();
	
	/**
	 * Create a brand new shiny XPathExpression visitor. It is used to visit 
	 * the parsed XPath expression nodes and perform some static analysis on the 
	 * expression.
	 * 
	 * @param validator the XPath expression validator.
	 */
	
	
	public XPathVisitor ( XPathValidator validator )
    {	
		/** The XPath Expression validator */
		mValidator = validator;
		
		/** The access to the model data */
		mModelQuery = validator.getModelQuery();
	
		/** The context INode */
		mContextNode = mValidator.getNode();
			
   	}


	
	protected void visit (PathExpr expr) {
		
		mValidator.runRules("pathexpr",expr);
		
		visit ( expr.getFilterExpr() );
		
		Object last = contextPeek();
		
		if (isSimpleType(last)) {
			
		}
		visit( expr.getLocationPath() );
	}
		
	
	protected void visit (LocationPath expr ) {
		mValidator.runRules("location",expr);
		visitList( expr.getSteps() );
	}


		
	protected void visit (FilterExpr expr) {		
		visit(expr.getExpr());		
		visitList( expr.getPredicates() );				
	}

	protected void visit (BinaryExpr expr) {
		
		visit(expr.getLHS());			
		Object lhsType = contextPop();
		
		visit(expr.getRHS());		
		Object rhsType = contextPop();
		
		if (expr instanceof LogicalExpr || expr instanceof RelationalExpr || expr instanceof EqualityExpr) {
			mContext.push( true );			
		} else {
			mContext.push( 0.0 );
		}
	}
	
	
	protected void visit(UnaryExpr expr) {
		visit(expr.getExpr());
		Object obj = contextPop();
		if (obj instanceof Number) {
			mContext.push(  ((Number)obj).doubleValue() * (-1) );
		} else {
			mContext.push(0.0);
		}
	}

	
	protected void visit(UnionExpr expr) {		
		
		visit(expr.getLHS());			
		Object lhs = contextPop();
		
		visit(expr.getRHS());		
		Object rhs = contextPop();
		
		//	the union expression must be between Collections.EMPTY_LISTS.
		IProblem problem ;		
		
		if ( isSimpleType(rhs) ) {
			problem = mValidator.createError();
			problem.fill("XPATH_NOT_A_Collections.EMPTY_LIST",  //$NON-NLS-1$
					0, expr.getLHS().getText() );
		}
			
		if (isSimpleType(lhs)) {
			problem = mValidator.createError();
			problem.fill("XPATH_NOT_A_Collections.EMPTY_LIST",  //$NON-NLS-1$
					1,expr.getLHS().getText() );			
		}
		
		mContext.push( Collections.EMPTY_LIST );
	}
	
	protected void visit (NumberExpr expr) {
		mContext.push( expr.getNumber() );
	}
	
	
	protected void visit (LiteralExpr expr) {
		mContext.push( expr.getLiteral() );
	}

	
	@SuppressWarnings("nls")
	protected void visit (VariableReferenceExpr expr) {
		mValidator.runRules("variables",expr);
	}

	
	protected void visit (FunctionCallExpr expr) {
				
		mValidator.runRules("functions",expr);	
		

		// TODO: Try to figure out what functions return. Most likely these would be
		// node-sets, but the meta data could be more specific in telling us what type
		// does a function return.
		
		mContext.push( Collections.EMPTY_LIST );
		
		// Now process the arguments
		visitList( expr.getParameters() );
	}
	

	/**
	 * Steps generally involve "stepping" over the elements in the query.
	 * 
	 * For this we use the value that was last on the stack as a reference point
	 * to evaluate the "next step". We query the model to make sure that the "next 
	 * step" makes sense until such point that it either does and we are done
	 * or it stops making sense at which point we abandon the effort.
	 */
	
		
	protected void visit (NameStep step) {
		
		String prefix = step.getPrefix();
		IProblem problem;
		
		Object last = contextPop();
		
		// We throw our hands in the air when the prefix does not resolve
		// and we are being asked to do a name step on an axis that we don't support
		// in static checking.
		
		if ( mValidator.checkPrefix ( prefix , step.getLocalName() ) == false) {
			mContext.push(Collections.EMPTY_LIST);
			return ;
		}

				
		if ( last instanceof List ) {
			mContext.push( Collections.EMPTY_LIST );
		} else if (last instanceof INode) {
			
			// walk the step
			INode context = (INode) last;
			int axis = step.getAxis();

			String nsURI = mValidator.lookupNamespace(prefix);
			QName qname = new QName(nsURI,step.getLocalName(),prefix != null ? prefix : "");
			boolean notChecked = false;
			
			INode result = null;
			switch (axis) {
				case Axis.CHILD :
					result = mModelQuery.lookup (context,IModelQueryLookups.LOOKUP_NODE_NAME_STEP , qname );
					break;
				case Axis.ATTRIBUTE :
					result = mModelQuery.lookup (context,IModelQueryLookups.LOOKUP_NODE_NAME_STEP_ATTRIBUTE, qname);
					break;				
				case Axis.PARENT :
					result = mModelQuery.lookup(context, IModelQueryLookups.LOOKUP_NODE_NAME_STEP_PARENT, qname);
					break;
				case Axis.DESCENDANT :
					result = mModelQuery.lookup(context, IModelQueryLookups.LOOKUP_NODE_NAME_STEP_DESCENDANT, qname);
					break;
				case Axis.DESCENDANT_OR_SELF :
					result = mModelQuery.lookup(context, IModelQueryLookups.LOOKUP_NODE_NAME_STEP_DESCENDANT_OR_SELF, qname);
					break;
					
				/** These are un-implemented */
					
				case Axis.ANCESTOR :
				case Axis.ANCESTOR_OR_SELF :
				case Axis.FOLLOWING :
				case Axis.FOLLOWING_SIBLING :
				case Axis.PRECEDING :
				case Axis.PRECEDING_SIBLING :
				case Axis.SELF :
				case Axis.NAMESPACE :
				case Axis.INVALID_AXIS :
					notChecked = true;
					break;				
			}
			
				
			if (result != null && result.isResolved()) {
				
				mContext.push(result);
				
			} else {
				
				mContext.push(Collections.EMPTY_LIST);
				if (notChecked) {
					// information and ignore
					problem = mValidator.createWarning();
					problem.fill("XPATH_AXIS_NOT_CHECKED", //$NON-NLS-1$
							step.getText()						
					);
				} else {
					mContext.push(Collections.EMPTY_LIST);
					// information and ignore
					problem = mValidator.createError();
					problem.fill("XPATH_NAME_STEP", //$NON-NLS-1$							
							step.getText()
						);
				}
			}
			
		} else {
			
			problem = mValidator.createError();
			problem.fill("XPATH_NAME_STEP",
					step.getText() );
			
			mContext.push(Collections.EMPTY_LIST);
		}
		
		
		// predicates work on the currently selected node as the context node.
		if (mContext.size() > 0) {
			mContext.push(mContext.peek());
		}
		
		visitList(step.getPredicates());
		contextPop();
		
	}

	protected void visit(ProcessingInstructionNodeStep step) {
		visitList(step.getPredicates());
		
	}

	protected void visit(AllNodeStep step) {
		
		if (mContext.size() > 0) {
			mContext.pop();
		}
		mContext.push(Collections.EMPTY_LIST);
		
		visitList(step.getPredicates());
	}

	
	protected void visit (TextNodeStep step) {
		visitList(step.getPredicates());
		contextPop();
		mContext.push ( new String("text()") );		
	}

	protected void visit (CommentNodeStep step) {
		visitList(step.getPredicates());
		contextPop();
		mContext.push (new String("comment()")) ;
		
	}

	protected void visit (Predicate predicate) {
		
		visit(predicate.getExpr());		
		
		Object result = contextPop();
		
		// Predicates expressions are either boolean or numeric types.
		if (result instanceof Boolean || result instanceof Number ) {
			// warn about predicates.
		}
	}
	

	void visitList ( List<?> list) {
		for(Object next: list) {
			visit(next);
		}
	}

	
	/**
	 * @param obj
	 */
	public void visit ( Object obj ) {
		
		if (obj instanceof PathExpr) {
			visit((PathExpr) obj);
		} else if (obj instanceof LocationPath) {
			visit((LocationPath) obj);
		} else if (obj instanceof BinaryExpr) {
			visit((BinaryExpr) obj);
		} else if (obj instanceof FilterExpr) {
			visit((FilterExpr)obj);			
		} else if (obj instanceof UnaryExpr) {
			visit((UnaryExpr)obj);			
		} else if (obj instanceof NumberExpr) {
			visit((NumberExpr)obj);
		} else if (obj instanceof LiteralExpr) {
			visit((LiteralExpr)obj);			
		} else if (obj instanceof VariableReferenceExpr) {
			visit((VariableReferenceExpr)obj);
		} else if (obj instanceof FunctionCallExpr) {
			visit((FunctionCallExpr) obj);			
		} else if (obj instanceof List) { 
			visitList((List<?>) obj);
		} else if (obj instanceof NameStep) {
			visit((NameStep)obj);
		} else if (obj instanceof ProcessingInstructionNodeStep) {
			visit((ProcessingInstructionNodeStep) obj);
		} else if (obj instanceof AllNodeStep) {
			visit((AllNodeStep)obj);			
		} else if (obj instanceof TextNodeStep) {
			visit((TextNodeStep)obj);
		} else if (obj instanceof CommentNodeStep) {
			visit((CommentNodeStep)obj);
		} else if (obj instanceof Predicate) {
			visit((Predicate)obj);
		} else {
			// ignore or throw error ... ?
		}
	}
	/**
	 * Private methods follow
	 */
	
			
	protected Object contextPush ( Object obj ) {
		return mContext.push( obj );
	}
	
	protected Object contextPeek () {
		return mContext.size() > 0 ? mContext.peek() : null;
	}

	protected Object contextPop () {
		return mContext.size() > 0 ? mContext.pop() : null;
	}

	static boolean isSimpleType ( Object obj ) {
		if (obj == null) {
			return false;
		}
		
		return (obj instanceof Number  || obj instanceof String || 
				obj instanceof Boolean );
	}
	
	static boolean isBPELNS ( String namespaceUri ) {
		
		return IConstants.XMLNS_BPEL.equals(namespaceUri) || 
		IConstants.XMLNS_BPEL20_OLD.equals(namespaceUri);
	}
	
	
    /**
     * Returns true if the string is either null or contains just whitespace.
     */
	
    static boolean isEmptyOrWhitespace( String value )
    {
        if( value == null || value.length() == 0) {
            return true;
        }               
        for( int i = 0, j = value.length(); i < j; i++ )
        {
            if( ! Character.isWhitespace( value.charAt(i) ) ) {
                return false;
            }
        }
        return true;
    }
    
    /**
     * Test to see if a string is empty or has a value that is empty.
     * 
     * @param value
     * @return true if empty or null, false otherwise.
     */
    
    static boolean isEmpty ( String value ) {
    	return value == null || value.length() == 0;
    }
}