/*
 * 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.uml2smm.transform;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.eclipse.emf.common.util.ECollections;
import org.eclipse.emf.common.util.EList;
import org.eclipse.uml2.uml.Class;
import org.eclipse.uml2.uml.Interface;
import org.eclipse.uml2.uml.Operation;
import org.eclipse.uml2.uml.Parameter;
import org.eclipse.uml2.uml.ParameterDirectionKind;
import org.eclipse.uml2.uml.Port;
import org.eclipse.uml2.uml.Type;

import eu.mdd4soa.smm.exception.TransformationException;
import eu.mdd4soa.smm.statik.ISMStatikFactory;
import eu.mdd4soa.smm.statik.InterfaceOperation;
import eu.mdd4soa.smm.statik.InterfaceParameter;
import eu.mdd4soa.smm.statik.InterfaceType;
import eu.mdd4soa.smm.statik.Participant;
import eu.mdd4soa.smm.statik.SMMType;
import eu.mdd4soa.smm.statik.Service;
import eu.mdd4soa.trans.uml2smm.SMMUtil;
import eu.mdd4soa.trans.uml2smm.StereotypeUtil;
import eu.mdd4soa.trans.uml2smm.StereotypeUtil.StereoType;


public class StaticConverter {

	private Class fOriginalClass;

	private Participant fParticipant;

	public static Participant convert(Class participant) throws TransformationException {
		StaticConverter c= new StaticConverter(participant);
		c.convert();
		return c.getResult();
	}

	public StaticConverter(Class participant) {
		fOriginalClass= participant;
	}

	public void convert() throws TransformationException {
		fParticipant= ISMStatikFactory.eINSTANCE.createParticipant();
		fParticipant.setName(fOriginalClass.getName());

		convertPorts();

		createOrderInChaos();
	}

	private void createOrderInChaos() {
		EList<SMMType> types= fParticipant.getTypes();
		for (SMMType ismType : types) {
			if (ismType instanceof InterfaceType) {
				ECollections.sort( ((InterfaceType) ismType).getUsed());
				ECollections.sort( ((InterfaceType) ismType).getImplemented());
			}
		}
	}

	private void convertPorts() throws TransformationException {
		EList<Port> ownedPorts= fOriginalClass.getOwnedPorts();
		for (Port port : ownedPorts) {
			if (StereotypeUtil.hasStereotype(port, StereoType.SERVICEPOINT))
				convertPartnerPoint(port, ISMStatikFactory.eINSTANCE.createProvidedService());
			else if (StereotypeUtil.hasStereotype(port, StereoType.REQUESTPOINT))
				convertPartnerPoint(port, ISMStatikFactory.eINSTANCE.createRequiredService());
			else
				throw new TransformationException("Participant " + fOriginalClass.getQualifiedName()
						+ " has a port which is not a servicepoint or requestpoint: " + port.getQualifiedName());
		}
	}

	private void convertPartnerPoint(Port port, Service partnerPoint) throws TransformationException {
		partnerPoint.setName(port.getName());
		fParticipant.getPartners().add(partnerPoint);

		Type portType= port.getType();
		if (portType == null)
			throw new TransformationException("Port " + port.getQualifiedName() + " of participant " + fParticipant.getName()
					+ " does not have a type.");

		if (!StereotypeUtil.hasStereotype(portType, StereoType.SERVICEINTERFACE))
			throw new TransformationException("Port Type " + portType.getQualifiedName() + "  of port " + port.getQualifiedName()
					+ " of participant " + fParticipant.getName() + " is not a service interface.");

		if (! (portType instanceof Class))
			throw new TransformationException("Port Type " + portType.getQualifiedName() + "  of port " + port.getQualifiedName()
					+ " of participant " + fParticipant.getName() + " is not a class.");


		handleInterface((Class) portType, partnerPoint);
	}

	/**
	 * Handles the interface of this port.
	 * 
	 * We use both the IMPLEMENTED operations of the port type and the USED
	 * operations of the port type -- the latter is used for callbacks.
	 * Depending on whether this is a service-or request point, the roles
	 * differ:
	 * 
	 * <ul>
	 * <li>ServicePoint: Operations declared in port type are received by the
	 * participant; Operations used in the port type are sent by the
	 * participant.</li>
	 * <li>RequestPoint: Operations declared in the port type are sent by the
	 * participant; Operation used in the port type are received by the
	 * participant.
	 * </ul>
	 * 
	 * @param portType
	 * @param partnerPoint
	 * @throws TransformationException
	 */
	private void handleInterface(Class portType, Service partnerPoint) throws TransformationException {

		// See if the interface is already there... (two ports with the same
		// interface)
		SMMType existingInterface= null;
		EList<SMMType> types= fParticipant.getTypes();
		for (SMMType type : types) {
			if (type.getName().equals(portType.getName())) {
				existingInterface= type;
				break;
			}
		}

		if (existingInterface != null) {
			if (! (existingInterface instanceof InterfaceType))
				throw new TransformationException("Found an existing type for port " + partnerPoint.getName() + " with name  " + portType.getName()
						+ " which is not a service interface.");
			partnerPoint.setInterface((InterfaceType) existingInterface);
			return;
		}

		InterfaceType serviceInterface= ISMStatikFactory.eINSTANCE.createInterfaceType();
		serviceInterface.setName(portType.getName());

		fParticipant.getTypes().add(serviceInterface);

		partnerPoint.setInterface(serviceInterface);

		Set<Operation> implementedOperations= new HashSet<Operation>();
		implementedOperations.addAll(portType.getOperations());
		EList<Interface> implementedInterfaces= portType.getImplementedInterfaces();
		for (Interface interface1 : implementedInterfaces) {
			implementedOperations.addAll(interface1.getAllOperations());
		}

		for (Operation operation : implementedOperations) {
			handleDeclaredOperation(serviceInterface, operation);
		}

		List<Operation> usedOperations= new ArrayList<Operation>();
		EList<Interface> usedInterfaces= portType.getUsedInterfaces();
		for (Interface interface1 : usedInterfaces) {
			usedOperations.addAll(interface1.getOperations());
		}

		for (Operation operation : usedOperations) {
			handleUsedOperation(serviceInterface, operation);
		}
	}

	private void handleDeclaredOperation(InterfaceType ismServiceInterface, Operation umlOperation) throws TransformationException {
		InterfaceOperation ismServiceOperation= ISMStatikFactory.eINSTANCE.createInterfaceOperation();
		ismServiceOperation.setName(umlOperation.getName());
		ismServiceInterface.getImplemented().add(ismServiceOperation);

		handleParametersAndReturns(ismServiceOperation, umlOperation);
	}


	private void handleUsedOperation(InterfaceType ismServiceInterface, Operation umlOperation) throws TransformationException {
		InterfaceOperation ismServiceOperation= ISMStatikFactory.eINSTANCE.createInterfaceOperation();
		ismServiceOperation.setName(umlOperation.getName());
		ismServiceInterface.getUsed().add(ismServiceOperation);

		handleParametersAndReturns(ismServiceOperation, umlOperation);
	}

	private void handleParametersAndReturns(InterfaceOperation ismOperation, Operation umlOperation) throws TransformationException {

		EList<Parameter> ownedParameters= umlOperation.getOwnedParameters();
		for (Parameter umlParam : ownedParameters) {

			InterfaceParameter ismParam;

			if (umlParam.getDirection().equals(ParameterDirectionKind.IN_LITERAL)) {
				ismParam= ISMStatikFactory.eINSTANCE.createInParameter();
			} else if (umlParam.getDirection().equals(ParameterDirectionKind.OUT_LITERAL)
					|| umlParam.getDirection().equals(ParameterDirectionKind.RETURN_LITERAL)) {
				ismParam= ISMStatikFactory.eINSTANCE.createOutParameter();
			} else {
				throw new TransformationException("Unsupported parameter type in operation  " + umlOperation + " :" + umlParam.getDirection());
			}

			// Name of the parameter...
			ismParam.setName(umlParam.getName());

			// And the type...
			SMMType ismType= SMMUtil.addType(fParticipant, umlParam.getType());
			ismParam.setType(ismType);

			// Multiplicity.
			ismParam.setMin(umlParam.getLower());
			ismParam.setMax(umlParam.getUpper());
			ismParam.setIsOrdered(umlParam.isOrdered());

			ismOperation.getParameters().add(ismParam);
		}
	}

	private Participant getResult() {
		return fParticipant;
	}

}
