/*
 * This file has been developed at the University of Munich, Chair for Programming & Software Engineering.
 * 
 * This file is licensed under the Eclipse Public License (EPL) 1.0
 * 
 */
package eu.mdd4soa.trans.smm2bpel.impl;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.wsdl.extensions.soap.SOAPAddress;
import javax.xml.namespace.QName;

import org.eclipse.bpel.model.messageproperties.MessagepropertiesFactory;
import org.eclipse.bpel.model.messageproperties.PropertyAlias;
import org.eclipse.bpel.model.messageproperties.Query;
import org.eclipse.bpel.model.partnerlinktype.PartnerLinkType;
import org.eclipse.bpel.model.partnerlinktype.PartnerlinktypeFactory;
import org.eclipse.bpel.model.partnerlinktype.Role;
import org.eclipse.bpel.model.partnerlinktype.util.PartnerlinktypeConstants;
import org.eclipse.emf.common.util.EList;
import org.eclipse.wst.wsdl.Binding;
import org.eclipse.wst.wsdl.BindingInput;
import org.eclipse.wst.wsdl.BindingOperation;
import org.eclipse.wst.wsdl.BindingOutput;
import org.eclipse.wst.wsdl.Definition;
import org.eclipse.wst.wsdl.Import;
import org.eclipse.wst.wsdl.Input;
import org.eclipse.wst.wsdl.Message;
import org.eclipse.wst.wsdl.Operation;
import org.eclipse.wst.wsdl.Output;
import org.eclipse.wst.wsdl.Part;
import org.eclipse.wst.wsdl.Port;
import org.eclipse.wst.wsdl.PortType;
import org.eclipse.wst.wsdl.Types;
import org.eclipse.wst.wsdl.WSDLFactory;
import org.eclipse.wst.wsdl.XSDSchemaExtensibilityElement;
import org.eclipse.wst.wsdl.binding.soap.SOAPBinding;
import org.eclipse.wst.wsdl.binding.soap.SOAPBody;
import org.eclipse.wst.wsdl.binding.soap.SOAPFactory;
import org.eclipse.wst.wsdl.binding.soap.SOAPOperation;
import org.eclipse.wst.wsdl.util.WSDLConstants;
import org.eclipse.xsd.XSDFactory;
import org.eclipse.xsd.XSDImport;
import org.eclipse.xsd.XSDSchema;
import org.eclipse.xsd.util.XSDConstants;

import eu.mdd4soa.smm.exception.TransformationException;
import eu.mdd4soa.smm.statik.InParameter;
import eu.mdd4soa.smm.statik.InterfaceOperation;
import eu.mdd4soa.smm.statik.InterfaceParameter;
import eu.mdd4soa.smm.statik.MessageProperty;
import eu.mdd4soa.smm.statik.OutParameter;
import eu.mdd4soa.smm.statik.Participant;
import eu.mdd4soa.smm.statik.RequiredService;
import eu.mdd4soa.smm.statik.Service;
import eu.mdd4soa.smm.statik.TypedMultiElement;
import eu.mdd4soa.trans.smm2bpel.impl.util.GPTracker;
import eu.mdd4soa.trans.smm2bpel.impl.util.WSDLMessageType;

/**
 * 
 * The static partner converter creates one WSDL definition for each partner
 * point -- both for service points and request points.
 * 
 * The types are each imported from the XSD file generated by the data
 * converter.
 * 
 * For the implemented operations of the partner, one port type is created with
 * the appropriate operations. For each operation, a maximum of two messages are
 * created -- one for the input parameters, and one for the output parameters.
 * The messages have the parameters as parts.
 * 
 * If there are used operations in the partner interface, this means that a
 * callback is necessary (either to the orchestration or to the partner). Thus,
 * another port type is created with these operations.
 * 
 * In the concrete part, bindings are generated for the port types generated
 * above. Furthermore, for the standard porttype, a service element is created.
 * For the callback porttype, no service element is created as the service URL
 * has to be provided by WS-Addressing anyway.
 * 
 * Note that there is no such thing as a "required wsdl file". All wsdl files
 * are provided in the sense that the operations are all implemented.
 * 
 * TODO write about rpc/literal style SOAP encoding.
 * 
 * TODO write about mapping of service/request points to roles in plts
 * 
 * TODO there are some major issues with addressing. There is a field
 * SOAPOperation:SOAPAction, which needs to map to the SOAPAction HTTP header
 * sent by a client and also to an (optional) WS-Addressing "Action" element in
 * a SOAP header. However, there seem to be different requirements for whether
 * this field is qualified or not. ODE seems to send out the field as-is,
 * without qualifying it. The WTP standard service implementation from the same
 * WSDL file seems to expect it to be qualified, although it also simply uses
 * the non-qualified version.
 * 
 * As a remedy, change the action mapping present in
 * WEB-INF/services/{servicename}/META-INF/services.xml to include the fully
 * qualified name of the operation (i.e. prefixed with the namespace of the
 * wsdl).
 * 
 * There is also the possibility of changing the WSDL to use WS-A, but this does
 * not seem to work either.
 * 
 * TODO some other gotchas include: Eclipse WTP does not support Axis 1.5, so
 * 1.4.1 is necessary. Additionally, it seems that running both ODE and Axis
 * inside the same tomcat is problematic (but need to check that).
 * 
 * 
 * @author Philip Mayer
 * 
 */
public class StaticPartnerConverter {

	public static Definition convert(GPTracker tracker, Participant participant, Service partnerPoint) throws TransformationException {
		StaticPartnerConverter spc= new StaticPartnerConverter(tracker, participant, partnerPoint);
		spc.execute();
		return spc.getResult();
	}

	private static final WSDLFactory wf= WSDLFactory.eINSTANCE;

	private Participant fParticipant;

	private Definition fDefinition;

	private GPTracker fTracker;

	private Service fPartnerPoint;

	private PortType fImplPortType;

	private PortType fCallbackPortType;

	public StaticPartnerConverter(Participant participant, Service partnerPoint) {
		fParticipant= participant;
	}

	public StaticPartnerConverter(GPTracker tracker, Participant participant, Service partnerPoint) {
		fParticipant= participant;
		fTracker= tracker;
		fPartnerPoint= partnerPoint;
	}

	private Definition getResult() {
		return fDefinition;
	}

	private void execute() throws TransformationException {

		fDefinition= wf.createDefinition();

		String targetNS= fTracker.getPartnerNamespace(fPartnerPoint.getName());

		fDefinition.addNamespace("", WSDLConstants.WSDL_NAMESPACE_URI);
		fDefinition.addNamespace("this", targetNS);
		fDefinition.addNamespace("plnk", PartnerlinktypeConstants.NAMESPACE_2004);
		fDefinition.addNamespace("xs", WSDLConstants.SCHEMA_FOR_SCHEMA_URI_2001);
		fDefinition.addNamespace("corr", fTracker.getTypeNamespace() + "correlation/");

		fDefinition.addNamespace("soap", "http://schemas.xmlsoap.org/wsdl/soap/");

		fDefinition.addNamespace("types", fTracker.getTypeNamespace());

		fDefinition.setQName(new QName(targetNS, fPartnerPoint.getName()));
		fDefinition.setTargetNamespace(targetNS);

		// --------------- Abstract Part -----------------

		importTypeSchema();

		importCorrelationFile();

		EList<InterfaceOperation> implemented= fPartnerPoint.getInterface().getImplemented();
		EList<InterfaceOperation> used= fPartnerPoint.getInterface().getUsed();

		List<InterfaceOperation> all= new ArrayList<InterfaceOperation>();
		all.addAll(implemented);
		all.addAll(used);

		// Standard port type for the operations to be implemented by the
		// partner the wsdl file is for
		fImplPortType= wf.createPortType();
		fImplPortType.setQName(new QName(targetNS, fPartnerPoint.getName()));
		fDefinition.addPortType(fImplPortType);

		// Callback port type for operations to be implemented by the other side
		if (!used.isEmpty()) {
			fCallbackPortType= wf.createPortType();
			fCallbackPortType.setQName(new QName(targetNS, fPartnerPoint.getName() + "_callback"));
			fDefinition.addPortType(fCallbackPortType);
		}

		// Operations and messages.
		Map<InterfaceOperation, Operation> operationMap= new HashMap<InterfaceOperation, Operation>();

		for (InterfaceOperation op : all) {

			Operation wsdlOp= wf.createOperation();
			wsdlOp.setName(op.getName());

			fTracker.registerOperation(op, wsdlOp);

			List<InParameter> inputs= getParameterForType(op.getParameters(), InParameter.class);
			if (inputs.size() > 0) {
				Message msg= createMessage(inputs, op.getName() + "_inMessage");
				fDefinition.addMessage(msg);
				fTracker.registerMessage(op, msg, WSDLMessageType.IN);

				Input input= wf.createInput();
				input.setName(op.getName() + "_input");
				input.setMessage(msg);
				wsdlOp.setInput(input);
			}

			List<OutParameter> outputs= getParameterForType(op.getParameters(), OutParameter.class);
			if (outputs.size() > 0) {

				Message msg= createMessage(outputs, op.getName() + "_outMessage");
				fDefinition.addMessage(msg);
				fTracker.registerMessage(op, msg, WSDLMessageType.OUT);

				Output output= wf.createOutput();
				output.setName(op.getName() + "_output");
				output.setMessage(msg);
				wsdlOp.setOutput(output);
			}

			if (implemented.contains(op))
				fImplPortType.addOperation(wsdlOp);
			else
				fCallbackPortType.addOperation(wsdlOp);

			operationMap.put(op, wsdlOp);
		}

		// --------------- Concrete Part -----------------

		// Bindings

		Binding implBinding= createBinding(fPartnerPoint.getName() + "_binding", fImplPortType);

		Binding callbackBinding= null;
		if (!used.isEmpty())
			callbackBinding= createBinding(fPartnerPoint.getName() + "_callback_binding", fCallbackPortType);

		// Operations

		for (InterfaceOperation interfaceOperation : operationMap.keySet()) {

			Operation operation= operationMap.get(interfaceOperation);

			BindingOperation bindingOperation= wf.createBindingOperation();
			bindingOperation.setOperation(operation);
			bindingOperation.setName(operation.getName());

			SOAPOperation soapOperation= SOAPFactory.eINSTANCE.createSOAPOperation();

			// Set the unqualified name, such that ODE sends the correct string.
			soapOperation.setSoapActionURI(interfaceOperation.getName());
			bindingOperation.addExtensibilityElement(soapOperation);

			if (operation.getInput() != null) {
				BindingInput bindingInput= wf.createBindingInput();
				bindingInput.setName(operation.getInput().getName());
				SOAPBody soapBody= SOAPFactory.eINSTANCE.createSOAPBody();
				soapBody.setNamespaceURI(fTracker.getTypeNamespace());
				soapBody.setUse("literal");

				bindingInput.setInput(operation.getInput());
				bindingInput.addExtensibilityElement(soapBody);
				bindingOperation.setBindingInput(bindingInput);
			}

			if (operation.getOutput() != null) {
				BindingOutput bindingOutput= wf.createBindingOutput();
				bindingOutput.setName(operation.getOutput().getName());
				SOAPBody soapBody= SOAPFactory.eINSTANCE.createSOAPBody();
				soapBody.setNamespaceURI(fTracker.getTypeNamespace());
				soapBody.setUse("literal");

				bindingOutput.setOutput(operation.getOutput());
				bindingOutput.addExtensibilityElement(soapBody);
				bindingOperation.setBindingOutput(bindingOutput);
			}

			if (implemented.contains(interfaceOperation))
				implBinding.addBindingOperation(bindingOperation);
			else
				callbackBinding.addBindingOperation(bindingOperation);
		}

		// Service
		String serviceAndPortName= fPartnerPoint.getName();
		String assumedLocation= "";
		if (fPartnerPoint instanceof RequiredService) {
			// Use Eclipse WTP standard location of the partner point
			assumedLocation= "http://localhost:9080/" + fPartnerPoint.getName() + "/services/" + fPartnerPoint.getName() + "/";
		} else {
			// Use ODE standard location for the partner point (one location for
			// two provided services is not permissible.
			assumedLocation= "http://localhost:8080/ode/processes/" + fPartnerPoint.getName();
		}

		createService(implBinding, serviceAndPortName, assumedLocation);

		if (!used.isEmpty()) {
			// We include a service for the callback to be able to reference it
			// later on in deploy.xml. The address provided is the default one;
			// it can later be changed to the one provided dynamically.
			String serviceAndPortName_callback= fPartnerPoint.getName() + "_callback";
			String assumedLocation_callback= "http://localhost:9080/" + fPartnerPoint.getName() + "/services/" + fPartnerPoint.getName()
					+ "_callback/";
			createService(callbackBinding, serviceAndPortName_callback, assumedLocation_callback);
		}


		PartnerLinkType plt= PartnerlinktypeFactory.eINSTANCE.createPartnerLinkType();
		plt.setName(fPartnerPoint.getName());

		Role role= PartnerlinktypeFactory.eINSTANCE.createRole();
		role.setPortType(fImplPortType);
		role.setName(fPartnerPoint.getName());
		plt.getRole().add(role);

		if (!used.isEmpty()) {
			Role cbRole= PartnerlinktypeFactory.eINSTANCE.createRole();
			cbRole.setPortType(fCallbackPortType);
			cbRole.setName(fPartnerPoint.getName() + "_callback");
			plt.getRole().add(cbRole);
		}

		fTracker.setServicePLTForPartner(fPartnerPoint, plt);
		fDefinition.addExtensibilityElement(plt);
	}

	private void createService(Binding implBinding, String serviceAndPortName, String assumedLocation) {
		javax.wsdl.Service service= wf.createService();
		service.setQName(new QName(serviceAndPortName));
		fDefinition.addService(service);

		Port port= wf.createPort();
		port.setName(serviceAndPortName);
		port.setBinding(implBinding);
		service.addPort(port);

		SOAPAddress soapAddress= SOAPFactory.eINSTANCE.createSOAPAddress();
		soapAddress.setLocationURI(assumedLocation);
		port.addExtensibilityElement(soapAddress);
	}


	private Binding createBinding(String bindingName, PortType portType) {
		Binding fImplBinding= wf.createBinding();
		fDefinition.addBinding(fImplBinding);

		fImplBinding.setQName(new QName(fTracker.getPartnerNamespace(fPartnerPoint.getName()), bindingName));
		fImplBinding.setPortType(portType);

		SOAPBinding soapBinding= SOAPFactory.eINSTANCE.createSOAPBinding();
		soapBinding.setStyle("rpc");
		soapBinding.setTransportURI("http://schemas.xmlsoap.org/soap/http");
		fImplBinding.addExtensibilityElement(soapBinding);
		return fImplBinding;
	}

	private Message createMessage(List<? extends InterfaceParameter> inputs, String msgName) throws TransformationException {

		Message msg= wf.createMessage();
		msg.setQName(new QName(fTracker.getPartnerNamespace(fPartnerPoint.getName()), msgName));

		for (InterfaceParameter interfaceParameter : inputs) {
			Part p= wf.createPart();
			p.setName(interfaceParameter.getName());

			p.setTypeName(fTracker.getMatchingXSDTypeQName(interfaceParameter.getType()));
			msg.addPart(p);

			// Correlation alias?
			handleCorrelationAlias(interfaceParameter, msg, p);
		}


		return msg;
	}

	private void handleCorrelationAlias(InterfaceParameter interfaceParameter, Message msg, Part p) {

		TypedMultiElement correlationElement= fTracker.getCorrelationPropertyAlias(interfaceParameter);
		if (correlationElement == null)
			return;

		String name= (correlationElement instanceof InterfaceParameter)
				? ((InterfaceParameter) correlationElement).getName()
				: ((MessageProperty) correlationElement).getIdentifier();

		PropertyAlias alias= MessagepropertiesFactory.eINSTANCE.createPropertyAlias();
		alias.setPropertyName(fTracker.getCorrelationProperty(name));
		alias.setMessageType(msg);
		alias.setPart(p.getName());
		fTracker.registerCorrelationMessage(msg, name);

		if (correlationElement instanceof MessageProperty) {
			MessageProperty prop= (MessageProperty) correlationElement;

			Query query= MessagepropertiesFactory.eINSTANCE.createQuery();

			// The message and part have already been set, so just select the
			// element.
			query.setValue("types:" + prop.getIdentifier());

			alias.setQuery(query);
		}

		fDefinition.addExtensibilityElement(alias);

	}

	@SuppressWarnings("unchecked")
	private <E extends InterfaceParameter> List<E> getParameterForType(EList<InterfaceParameter> parameters, Class<E> clazz) {

		List<E> params= new ArrayList<E>();

		for (InterfaceParameter interfaceParameter : parameters) {
			if (clazz.isInstance(interfaceParameter))
				params.add((E) interfaceParameter);
		}

		return params;
	}

	private void importTypeSchema() {

		XSDSchemaExtensibilityElement schemaEE= WSDLFactory.eINSTANCE.createXSDSchemaExtensibilityElement();

		XSDSchema importSchema= XSDFactory.eINSTANCE.createXSDSchema();
		importSchema.setTargetNamespace(fDefinition.getTargetNamespace());
		importSchema.setSchemaForSchemaQNamePrefix("xs");
		importSchema.getQNamePrefixToNamespaceMap().put("xs", XSDConstants.SCHEMA_FOR_SCHEMA_URI_2001);

		XSDImport imp= XSDFactory.eINSTANCE.createXSDImport();
		imp.setSchemaLocation(fParticipant.getName() + ".xsd");
		imp.setNamespace(fTracker.getTypeNamespace());
		importSchema.getContents().add(imp);

		schemaEE.setSchema(importSchema);

		Types types= WSDLFactory.eINSTANCE.createTypes();
		types.addExtensibilityElement(schemaEE);
		fDefinition.setTypes(types);
	}

	private void importCorrelationFile() {

		XSDSchema importSchema= XSDFactory.eINSTANCE.createXSDSchema();
		importSchema.setTargetNamespace(fDefinition.getTargetNamespace());
		importSchema.setSchemaForSchemaQNamePrefix("corr");
		importSchema.getQNamePrefixToNamespaceMap().put("corr", fTracker.getTypeNamespace() + "correlation/");

		Import imp= wf.createImport();
		imp.setNamespaceURI(fTracker.getTypeNamespace() + "correlation/");
		imp.setLocationURI("correlation.wsdl");

		fDefinition.addImport(imp);
	}


}
