/*******************************************************************************
 * Copyright (c) 2005 IBM 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:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.bpel.model.util;

import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

import javax.xml.namespace.QName;

import org.eclipse.bpel.model.messageproperties.MessagepropertiesPackage;
import org.eclipse.bpel.model.messageproperties.Property;
import org.eclipse.bpel.model.partnerlinktype.PartnerLinkType;
import org.eclipse.bpel.model.partnerlinktype.PartnerlinktypePackage;
import org.eclipse.bpel.model.partnerlinktype.Role;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.wst.wsdl.Definition;
import org.eclipse.wst.wsdl.Message;
import org.eclipse.wst.wsdl.Operation;
import org.eclipse.wst.wsdl.Part;
import org.eclipse.wst.wsdl.PortType;
import org.eclipse.wst.wsdl.Service;
import org.eclipse.wst.wsdl.Types;
import org.eclipse.wst.wsdl.WSDLPackage;
import org.eclipse.wst.wsdl.XSDSchemaExtensibilityElement;
import org.eclipse.wst.wsdl.internal.impl.ImportImpl;
import org.eclipse.xsd.XSDElementDeclaration;
import org.eclipse.xsd.XSDPackage;
import org.eclipse.xsd.XSDSchema;
import org.eclipse.xsd.XSDTypeDefinition;
import org.eclipse.xsd.impl.XSDSchemaImpl;
import org.eclipse.xsd.util.XSDConstants;



/**
 * WSDLUtil supplies resolution (lookup) of various things that are useful in the BPEL 
 * world. 
 *
 */

public class WSDLUtil
{	 
	/** The WSDL Message attribute/name, most likely "message" */
	
    public static final String WSDL_MESSAGE = WSDLPackage.eINSTANCE.getMessage().getName();
    
    /** The WSDL Port Type attribute/name, most likely "portType" */
    public static final String WSDL_PORT_TYPE = WSDLPackage.eINSTANCE.getPortType().getName();
    
    /** The WSDL Operation attribute/name, most likely "operation" */
    public static final String WSDL_OPERATION = WSDLPackage.eINSTANCE.getOperation().getName();
    
    /** The WSDL Part attribute/name, most likely "part" */
    public static final String WSDL_PART = WSDLPackage.eINSTANCE.getPart().getName();
    
    /** The WSDL Service attribute/name, most likely "service" */
    public static final String WSDL_SERVICE = WSDLPackage.eINSTANCE.getService().getName();
    
    /** The XSD Type Definition attribute/name, most likely "type" */
    public static final String XSD_TYPE_DEFINITION = XSDPackage.eINSTANCE.getXSDTypeDefinition().getName();
    
    /** The XSD Element Declaration attribute/name, most likely "element" */
    public static final String XSD_ELEMENT_DECLARATION = XSDPackage.eINSTANCE.getXSDElementDeclaration().getName();
    
    /** The BPEL Partner Link Type attribute/name, most likely "partnerLinkType" */
    public static final String BPEL_PARTNER_LINK_TYPE = PartnerlinktypePackage.eINSTANCE.getPartnerLinkType().getName();
    
    /** The BPEL Role attribute/name, most likely "role" */
    public static final String BPEL_ROLE = PartnerlinktypePackage.eINSTANCE.getRole().getName();

    /** The BPEL Property attribute/name, most likely "property" */
    public static final String BPEL_PROPERTY = MessagepropertiesPackage.eINSTANCE.getProperty().getName();

    /** This should be a preference that can be easily turned on/off */
	private static boolean RESOLVING_DEEPLY = true;  // TODO: Preference based ?
    
	
    /**
     * Answer true if the type name passed is a WSDL thing.
     * 
     * @param typeName
     * @return true if yes, false if no.
     */
	
    public static boolean isWSDLType(String typeName)
    {
        return typeName == null ? false :
            WSDL_MESSAGE.equals(typeName)
            || WSDL_PORT_TYPE.equals(typeName)
            || WSDL_OPERATION.equals(typeName)
            || WSDL_PART.equals(typeName)
            || WSDL_SERVICE.equals(typeName)
            || XSD_TYPE_DEFINITION.equals(typeName)
            || XSD_ELEMENT_DECLARATION.equals(typeName)
            || BPEL_PARTNER_LINK_TYPE.equals(typeName)
            || BPEL_ROLE.equals(typeName)
            || BPEL_PROPERTY.equals(typeName);
    }

    
    /**
     * Set to resolve deeply.
     * @param resolveDeeply
     */
    
    public static void setResolveDeeply ( boolean resolveDeeply ) {
    	RESOLVING_DEEPLY = resolveDeeply;
    }
    
    /**
     * Answer if resolving deeply.
     * @return answer true if resolving deeply, false otherwise.
     */
    
    public static boolean isResolvingDeeply () {
    	return RESOLVING_DEEPLY;
    }
    
    /**
     * Resolve with the given definition the QName given.
     * @param definition the definition to use.
     * @param qname the QName to resolve.
     * @param name 
     * @param refType what to resolve (that things from WSDL that we can resolve).
     * @return  the EMF object that the QName resolves to.
     */
    
    public static EObject resolveWSDLReference(Definition definition, QName qname, String name, String refType)
    {
	    EObject resolvedObject = null;
	    
	    if (WSDL_PORT_TYPE.equals(refType))  {
	        resolvedObject = resolvePortType(definition, qname);
	    } else if (WSDL_MESSAGE.equals(refType)) {
	        resolvedObject = resolveMessage(definition, qname);
	    } else if (WSDL_OPERATION.equals(refType))  {
	        resolvedObject = resolveOperation(definition, qname, name);
	    } else if (WSDL_PART.equals(refType)) {
	        resolvedObject = resolvePart(definition, qname, name);
	    } else if (WSDL_SERVICE.equals(refType)) {
            resolvedObject = resolveService(definition, qname);
        } else if (XSD_TYPE_DEFINITION.equals(refType)) {
	        resolvedObject = resolveXSDTypeDefinition(definition, qname);
	    } else if (XSD_ELEMENT_DECLARATION.equals(refType)) {
	        resolvedObject = resolveXSDElementDeclaration(definition, qname);
	    } else if (BPEL_PARTNER_LINK_TYPE.equals(refType)) {
	        resolvedObject = resolveBPELPartnerLinkType(definition, qname);
	    } else if (BPEL_ROLE.equals(refType)) {
            resolvedObject = resolveBPELRole(definition, qname, name);
        } else if (BPEL_PROPERTY.equals(refType)) {
	        resolvedObject = resolveBPELProperty(definition, qname);
	    } else {
	        System.err.println(WSDLUtil.class.getName() + ": unrecognized refType: " + refType); //$NON-NLS-1$
	    }
	    
	    return resolvedObject;
    }
    
    
     
    /** 
     * Use Java generics to write the "finders" of various elements, so that the general code
     * which does the lookups and follows imports does not need to be repeated.
     * 
     * @author Michal Chmielewski (michal.chmielewski@oracle.com)
     * @date Feb 27, 2007
     *
     * @param <T> the type that we are looking up (PortType, Message, PartnerLinkType)
     */
    
    interface Finder<S,T> {
    	/**
    	 * Find the thing in the container
    	 * 
    	 * @param source the source where to look (definition, schema)
    	 * @param qname the QName to lookup
    	 * @return the resolved entity, or null
    	 */
    	T find (S source, QName qname);
    }
     
    
    /** Generic way of looking up port types */
	static final Finder<Definition,PortType> PORT_TYPE_FINDER = new Finder<Definition, PortType>() {
		public PortType find (Definition defn, QName qname) {
			return (PortType) defn.getPortType(qname) ;
		}    	
    };
    
    /** Generic way of looking up messages */
    static final Finder<Definition,Message> MESSAGE_FINDER = new Finder<Definition,Message> () {
    	public Message find (Definition defn, QName qname) {
    		return (Message) defn.getMessage(qname);
    	}
    };
    
    /** Generic way of looking up partner link types */
    static final Finder<Definition,PartnerLinkType> PARTNER_LINK_TYPE_FINDER = new Finder<Definition,PartnerLinkType>() {
    	
    	public PartnerLinkType find (Definition defn, QName qname) {
    		
    		if (defn.getTargetNamespace() == null)
    			return null;
	       
	        if (defn.getTargetNamespace().equals(qname.getNamespaceURI()) == false) {
	        	return null;
	        }	       
	        Iterator<?> it = defn.getExtensibilityElements().iterator();
	        while (it.hasNext()) {
	        	Object e = it.next();
	        	if (e instanceof PartnerLinkType) {
	        		PartnerLinkType plt = (PartnerLinkType) e;
	                if (plt.getName().equals(qname.getLocalPart())) {
	                    return plt;
	                }
	        	}
	        }
	        return null;
    	}
    };
    
    /** Generic way of lookup up BPEL properties */
    static final Finder<Definition,Property> PROPERTY_FINDER = new Finder<Definition,Property>() {

		public Property find(Definition defn, QName qname) {				      
	        if (defn.getTargetNamespace().equals(qname.getNamespaceURI()) == false) {
	        	return null;
	        }
	        Iterator<?> it = defn.getExtensibilityElements().iterator();
		    while (it.hasNext()) {
	            Object e = it.next();
	            if (e instanceof Property)  {
	                Property property = (Property) e;
	                if (property.getName().equals(qname.getLocalPart())) {
	                    return property;
	                }
	            }
	        }	
		    return null;
		}    	
    };
    
    /** Type definition finder */    
    static private final Finder<XSDSchema,XSDTypeDefinition> TYPE_DEFINITION_FINDER = new Finder<XSDSchema,XSDTypeDefinition>() {

		public XSDTypeDefinition find(XSDSchema schema, QName typeName) {
	    	// Perhaps this is what we want ...
	    	//    http://www.eclipse.org/modeling/emf/faq/#dev20040602-1383194195
	    	// 
	    	XSDTypeDefinition typeDef = schema.resolveTypeDefinition(typeName.getNamespaceURI(), typeName.getLocalPart());    	    	
	    	if (typeDef.getContainer() == null) {
	    		return null;
	    	}
	    	return typeDef;
		}
    };
    
    /** Element declaration finder */
    static private final Finder<XSDSchema,XSDElementDeclaration> ELEMENT_DECLARATION_FINDER = new Finder<XSDSchema,XSDElementDeclaration> () {

		public XSDElementDeclaration find(XSDSchema schema, QName qn) {
	    	XSDElementDeclaration decl = schema.resolveElementDeclaration( qn.getNamespaceURI(), qn.getLocalPart());
	    	
	    	// TODO: (Hack) 
	    	// Why has this started returning instances of XSDElementDeclrarion that are not proxies or null
	    	// if the element declaration is missing in the schema ?
	    	// Perhaps this is what we want: http://www.eclipse.org/modeling/emf/faq/#dev20040602-1383194195
	    	
	    	if (decl.getContainer() == null) {
	    		return null;
	    	}
	    	return decl;
		}
    };
    
    /**
     * Resolve the port type in the definition given.
     * 
     * @param definition the WSDL definition
     * @param qname the QName of the portType
     * @return the resolved portType, or null
     */
    
    public static PortType resolvePortType (Definition definition, QName qname)
    {    	    	    	
    	return  resolveUsingFinder ( definition, qname, PORT_TYPE_FINDER, new HashSet<Definition>() );
    }
    
    
    /** 
     * Resolve using finder resolves the QName in the definitions passed. The return object
     * extends from EObject (so PortType, Message, PartnerLinkType, etc). The finder has the actual lookup
     * code while the general code flow that deals with following the imports is shared among the lookup
     * methods in this class.
     * 
     * @param <T> the type 
     * @param defn the definition to use as the lookup
     * @param qname the QName to lookup
     * @param finder the finder to use.
     * @param seen the seen set
     * @return the object to be resolved.
     */
    
    @SuppressWarnings("restriction")
	static <T extends EObject> T resolveUsingFinder ( Definition defn, QName qname, Finder<Definition,T> finder , Set<Definition> seen) {
    	
    	if (seen.contains(defn)) {
    		return null;
    	}
    	seen.add(defn);    	
    	
    	T result = finder.find ( defn, qname );
    	
    	if (result != null || RESOLVING_DEEPLY == false) {
    		return result;
    	}    	
    	
    	Iterator<?> it = defn.getImports(qname.getNamespaceURI()).iterator();
    	while (it.hasNext() && result == null) {
             ImportImpl imp = (ImportImpl) it.next();
             imp.importDefinitionOrSchema();               
             Definition importedDefinition = (Definition) imp.getDefinition();
             if (importedDefinition != null) {
                 result = resolveUsingFinder (importedDefinition, qname, finder, seen );                          
             }
        }
    	return result;
    }
    

    
    
    /**
     * Resolve the message name in the definitions passed.
     * @param definition the definitions
     * @param qname the message name
     * @return the message name or null
     */
    
    public static Message resolveMessage(Definition definition, QName qname)
    {    	
    	return resolveUsingFinder(definition, qname, MESSAGE_FINDER, new HashSet<Definition>());
    }
        
    
    /**
     * Resolve operation within a portType. First lookup portType, then find the operation by name given.
     * @param definition the definition to search
     * @param portTypeQName QName of the portType to lookup operation on
     * @param operationName the operation name.
     * @return the operation or null 
     */
    
    public static Operation resolveOperation(Definition definition, QName portTypeQName, String operationName)
    {
        PortType portType = resolvePortType(definition, portTypeQName);
        return findOperation(portType, operationName);
    }

    /**
     * Resolve the part within a message.
     * @param definition the definition to use
     * @param qname QName of the message
     * @param name the name of the message part.
     * @return the part, or null
     */
    
    public static Part resolvePart(Definition definition, QName qname, String name)
    {        
        return findPart( resolveMessage(definition, qname) , name );     
    }

    

    /**
     * Resolve (lookup) a partner link type in the definitions given. 
     * @param defn the definitions to use
     * @param qname the QName to lookup
     * @return the partner link type or null
     */
    
    public static PartnerLinkType resolveBPELPartnerLinkType(Definition defn, QName qname)
    {
    	return resolveUsingFinder(defn, qname, PARTNER_LINK_TYPE_FINDER, new HashSet<Definition>() );
    }
        
    /**
     * Resolve BPEL Role on the partner link specified.
     * 
     * @param definition the definition to use
     * @param qname QNAme of the partner link type
     * @param name the name of the role on that partner link type
     * @return the BPEL role, or null
     */
    
    public static EObject resolveBPELRole(Definition definition, QName qname, String name)
    {
        return findRole (  resolveBPELPartnerLinkType(definition, qname) , name );
    }
    
    
    /**
     * Lookup the BPEL property in the definition given.
     * 
     * @param defn the definition
     * @param qname the property QName to lookup
     * @return the property or null
     */
    
    public static Property resolveBPELProperty(Definition defn, QName qname)
    {
    	return resolveUsingFinder(defn, qname, PROPERTY_FINDER, new HashSet<Definition>());
    }
    
    
    
    private static Service resolveService(Definition definition, QName qname) {
        return (Service) definition.getService(qname);
    }
       
    
    
    /**
     * Common code for resolving XSDTypeDefinitions and XSDElementDeclarations
     */
    @SuppressWarnings("restriction")
	static <T> T resolveXSD (Definition definition, QName qname, Finder<XSDSchema,T> finder, Set<Definition> seen )
    {
        T result = null;
        
        if (seen.contains(definition)) {
        	return result;
        }
        seen.add(definition);
        
        // Check for built-in types
        // TODO Slightly inefficient to evaluate this when recursing

        XSDSchema schema = null;
        String namespace = qname.getNamespaceURI();
        
        if ("".equals(namespace)) { //$NON-NLS-1$
            namespace = null;
        }
        
        if (XSDConstants.isSchemaForSchemaNamespace(namespace)) {
            schema = XSDSchemaImpl.getSchemaForSchema(namespace);
        } else if (XSDConstants.isSchemaInstanceNamespace(namespace)) {
            schema = XSDSchemaImpl.getSchemaInstance(namespace);
        }
        if (schema != null) {
            result = finder.find(schema, qname);
            if (result != null) {
                return result;
            }
        }

        // Check in-line schema
        Types types = definition.getETypes();
        if (types != null) {
        	Iterator<?> it = types.getExtensibilityElements().iterator();
        	while (it.hasNext()) {
                Object e = it.next();
                if (e instanceof XSDSchemaExtensibilityElement == false) {
                	continue;
                }
                XSDSchemaExtensibilityElement schemaEE = (XSDSchemaExtensibilityElement) e;
                schema = schemaEE.getSchema();
                if (schema != null) {
                    result = finder.find(schema, qname);
                    if (result != null)
                        return result;
                }
            }
        }

        /**
          * Check imported schemas and definitions.
          * If we are here, then result is still null
          */
        
        // TODO: I think I need to check all imports, not just those
        // matching the same namespace...
        
        Iterator<?> it = definition.getImports(qname.getNamespaceURI()).iterator();
        while (it.hasNext()) {
            ImportImpl imp = (ImportImpl) it.next();
            imp.importDefinitionOrSchema();
            schema = imp.getESchema();
            if (schema != null) {
                result = finder.find(schema, qname);
                if (result != null)
                    return result;
            }
            Definition importedDefinition = imp.getEDefinition();
            if (importedDefinition != null) {
                result = resolveXSD(importedDefinition, qname, finder, seen );
                if (result != null) {
                    return result;
                }                
            }
        }
        return result;        
    }
    
    
    /**
     * Resolve type definition. Basically lookup type by QName.
     * 
     * @param definition
     * @param qname
     * @return the XSDTypeDefinition found or null if it does not exist.
     */
    
    public static XSDTypeDefinition resolveXSDTypeDefinition(Definition definition, QName qname)
    {      
        return resolveXSD(definition, qname ,TYPE_DEFINITION_FINDER, new HashSet<Definition>() );
    }
    
    /**
     * Resolve the XSDElement declaration in this definition. Basically, lookup element declaration using the
     * QName specified in the definition indicated. 
     * 
     * @param definition
     * @param qname
     * @return the XSDElement declaration if found. null otherwise. 
     */
    
    public static XSDElementDeclaration resolveXSDElementDeclaration(Definition definition, QName qname)
    {        
        return resolveXSD(definition, qname, ELEMENT_DECLARATION_FINDER, new HashSet<Definition>() );
    }

    
    
    
    /**
     * Find a role in the partner link type.
     * @param plt the partner link type
     * @param name the role name to find.
     * @return the found role or null if it does not exist.
     */
    
    @SuppressWarnings("unchecked")
	public static Role findRole ( PartnerLinkType plt, String name ) {
    	
    	if (plt != null) {
    		Iterator<Role> it = plt.getRole().iterator();
    		while (it.hasNext()) {
    			Role role = it.next();
    			if (name.equals(role.getName())) {
	                return role;
	            }
    		}
    	}
    	return null;
    }
    
    
    /**
     * Find the operation in the port type.
     * @param portType the port type
     * @param operationName the operation name
     * @return return the operation in the port type or null
     */
    
    @SuppressWarnings("unchecked")
	public static Operation findOperation (PortType portType, String operationName) {               
        if (portType != null) {
        	Iterator<Operation> it = portType.getOperations().iterator();
        	while (it.hasNext()) {
                Operation operation = it.next();
                if (operation.getName().equals(operationName)) {
                    return operation;
                }
            }
        }
        return null;
    }

	/**
	 * Find the message part in the message structure.
	 * @param message
	 * @param name
	 * @return the part or null
	 */
	public static Part findPart(Message message, String name) {
		Part result = null;
        if (message != null) {
            result = (Part) message.getPart(name);
        }
	    return result;
	}
}
