/*
 * 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;

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

import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.common.util.TreeIterator;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.eclipse.uml2.uml.ActivityEdge;
import org.eclipse.uml2.uml.ActivityNode;
import org.eclipse.uml2.uml.Association;
import org.eclipse.uml2.uml.Element;
import org.eclipse.uml2.uml.Property;
import org.eclipse.uml2.uml.Type;

import eu.mdd4soa.smm.behaviour.CompositeElement;
import eu.mdd4soa.smm.behaviour.ServiceActivity;
import eu.mdd4soa.smm.exception.TransformationException;
import eu.mdd4soa.smm.statik.ExceptionType;
import eu.mdd4soa.smm.statik.ISMStatikFactory;
import eu.mdd4soa.smm.statik.MessageProperty;
import eu.mdd4soa.smm.statik.MessageType;
import eu.mdd4soa.smm.statik.NullType;
import eu.mdd4soa.smm.statik.Participant;
import eu.mdd4soa.smm.statik.SMMType;
import eu.mdd4soa.trans.uml2smm.StereotypeUtil.StereoType;

public class SMMUtil {

	public static enum SupportedPrimitiveTypes {
		String, Boolean, Int, Date, Double
	}

	public static final String LISTNAME= "List";

	public static final String SETNAME= "Set";

	public static Participant getParticipant(ServiceActivity someActivity) {

		CompositeElement topLevel= someActivity;
		while (topLevel != null) {
			if (topLevel instanceof ServiceActivity && ((ServiceActivity) topLevel).getParticipant() != null)
				break;
			topLevel= topLevel.getParent();
		}

		if (topLevel instanceof ServiceActivity && ((ServiceActivity) topLevel).getParticipant() != null)
			return ((ServiceActivity) topLevel).getParticipant();

		throw new NullPointerException("Could not find the participant of service activity " + someActivity);
	}

	public static ServiceActivity resolveServiceActivity(Participant participant, String name) {

		if (participant == null)
			throw new NullPointerException("resolveServiceActivity() called with a null argument.");

		for (TreeIterator<Object> contents= EcoreUtil.getAllContents(participant, false); contents.hasNext();) {
			Object next= contents.next();
			if (next instanceof ServiceActivity && ((ServiceActivity) next).getName() != null && ((ServiceActivity) next).getName().equals(name))
				return (ServiceActivity) next;
		}

		return null;
	}

	public static boolean isStandardType(String typeName) {

		SupportedPrimitiveTypes type= parseStandardType(typeName);
		return type != null;
	}

	private static SupportedPrimitiveTypes parseStandardType(String typeName) {

		// UML types:
		if (typeName.equalsIgnoreCase("String"))
			return SupportedPrimitiveTypes.String;
		if (typeName.equalsIgnoreCase("Integer"))
			return SupportedPrimitiveTypes.Int;
		if (typeName.equalsIgnoreCase("Int"))
			return SupportedPrimitiveTypes.Int;
		if (typeName.contains("Boolean"))
			return SupportedPrimitiveTypes.Boolean;

		// MagicDraw types
		if (typeName.equals("JavaPrimitiveTypes::int"))
			return SupportedPrimitiveTypes.Int;
		if (typeName.equals("JavaPrimitiveTypes::double"))
			return SupportedPrimitiveTypes.Double;
		if (typeName.equals("UMLPrimitiveTypes::String"))
			return SupportedPrimitiveTypes.String;

		return null;
	}

	public static boolean isMessageType(Type type) {
		return StereotypeUtil.hasStereotype(type, StereoType.MESSAGETYPE);
	}

	public static SMMType addType(Participant participant, Type type) throws TransformationException {
		if (SMMUtil.isStandardType(type.getQualifiedName())) {
			return SMMUtil.addStandardType(participant, type.getQualifiedName());
		} else if (SMMUtil.isMessageType(type)) {
			return SMMUtil.addMessageType(participant, type);
		} else {
			throw new TransformationException("Unknown type: " + type.getQualifiedName());
		}
	}

	public static SMMType addNullType(Participant participant) {

		EList<SMMType> types= participant.getTypes();
		for (SMMType ismType : types) {
			if (ismType instanceof NullType)
				return ismType;
		}
		NullType nullType= ISMStatikFactory.eINSTANCE.createNullType();
		nullType.setName("null");
		participant.getTypes().add(nullType);

		return nullType;
	}

	public static SMMType addStandardType(Participant participant, String typeName) throws TransformationException {

		// Resolve type


		SupportedPrimitiveTypes standardType= parseStandardType(typeName);
		if (standardType == null)
			throw new TransformationException("The given type " + typeName + " is not a UML standard type.");

		return addStandardType(participant, standardType);
	}

	public static SMMType addStandardType(Participant participant, SupportedPrimitiveTypes standardType) {

		// Check for the type...
		SMMType typeExists= checkIfTypeExists(participant, standardType.name());
		if (typeExists != null)
			return typeExists;

		// Add it.
		SMMType ismType= null;
		switch (standardType) {
			case String:
				ismType= ISMStatikFactory.eINSTANCE.createStringType();
				break;
			case Date:
				ismType= ISMStatikFactory.eINSTANCE.createDateType();
				break;
			case Boolean:
				ismType= ISMStatikFactory.eINSTANCE.createBooleanType();
				break;
			case Int:
				ismType= ISMStatikFactory.eINSTANCE.createIntegerType();
				break;
			case Double:
				ismType= ISMStatikFactory.eINSTANCE.createRealType();
				break;
		}

		ismType.setName(standardType.name());
		participant.getTypes().add(ismType);
		return ismType;
	}

	public static SMMType addExceptionType(Participant participant, Type exceptionType) throws TransformationException {

		String name= exceptionType.getName();

		SMMType typeExists= checkIfTypeExists(participant, name);

		if (typeExists != null && (! (typeExists instanceof ExceptionType)))
			throw new TransformationException(
					"Trying to add an exception type, but a type with the same name already exists (and is not an exception type): " + name);

		if (typeExists != null)
			return typeExists;

		ExceptionType excType= ISMStatikFactory.eINSTANCE.createExceptionType();
		excType.setName(name);
		participant.getTypes().add(excType);

		return excType;
	}


	public static SMMType addMessageType(Participant participant, Type type) throws TransformationException {

		if (!SMMUtil.isMessageType(type))
			throw new TransformationException("Trying to add type " + type + " to ISM model failed as it is not a message type.");

		String name= type.getName();

		SMMType typeExists= checkIfTypeExists(participant, name);
		if (typeExists != null)
			return typeExists;

		// Add the message type, which includes references...
		MessageType msgType= ISMStatikFactory.eINSTANCE.createMessageType();
		msgType.setName(name);
		participant.getTypes().add(msgType);

		// Get properties.
		Map<String, Property> availableFields= getAvailableProperties(type);

		for (String fieldName : availableFields.keySet()) {
			Property prop= availableFields.get(fieldName);

			SMMType fieldType= addType(participant, prop.getType());

			MessageProperty property= ISMStatikFactory.eINSTANCE.createMessageProperty();
			property.setIdentifier(fieldName);
			property.setType(fieldType);

			property.setMin(prop.getLower());
			property.setMax(prop.getUpper());
			if (prop.isOrdered())
				property.setIsOrdered(true);

			msgType.getProperties().add(property);
		}

		return msgType;
	}

	/**
	 * Attempts to resolve the type with the given simple name.
	 * 
	 * Called from the parser.
	 * 
	 * @param typeName
	 * @return
	 */
	public static SMMType resolveType(Participant p, String typeName) {

		EList<SMMType> types= p.getTypes();
		for (SMMType ismType : types) {
			if (ismType.getName().equalsIgnoreCase(typeName))
				return ismType;
		}

		return null;
	}

	// ***************** Helpers


	/**
	 * Returns a map of available fields of a UML class.
	 * 
	 * This is done by getting the directly owned elements and also scanning
	 * each association of the class. After that the inherited fields will be
	 * added recursively, if not present in a lower level already (overwriting).
	 * 
	 * @param type the UML {@link Type} to extract all own fields/associations
	 *        and inherited fields/associations
	 * @return a map of the name of the owned field and the corresponding UML
	 *         {@link Property}. The Property contains additional information
	 *         like the lower and upper boundaries of a field.
	 */
	public static Map<String, Property> getAvailableProperties(Type type) {
		Map<String, Property> availableFields= new HashMap<String, Property>();

		// check directly owned Elements
		for (Element element : type.getOwnedElements()) {
			if (element instanceof Property) {
				Property p= (Property) element;
				availableFields.put(getDefaultNameForProperty(p), p);
			}
		}
		// check in associations:
		for (Association assoc : type.getAssociations()) {

			// collect the used Types in this association
			// if it is only one, it means the association references the same
			// type
			// like a list data type pointing to the next / previous element
			Set<Type> associatedTypes= new HashSet<Type>();
			for (Property p : assoc.getOwnedEnds()) {
				associatedTypes.add(p.getType());
			}

			// NOTE: This is only correct for associations between one or two
			// classes only.
			// If someone needs associations between more types this logic won't
			// produce the correct result.

			for (Property p : assoc.getNavigableOwnedEnds()) {
				boolean navigableEndOwnedByType= true;
				if (p.getType().equals(type)) {
					// only add, if other end of this assoc is the current Type.
					//
					// either, this is the end of an association which comes
					// from another type, or it is a self-reference
					// if the other end is from another class, it is not owned
					// by this type
					if (associatedTypes.size() > 1) {
						// the association involves another type
						// we skip this navigable end.
						navigableEndOwnedByType= false;
					} else {
						// it is a self reference
					}
				}

				String name= getDefaultNameForProperty(p);
				if (navigableEndOwnedByType && !availableFields.containsKey(p.getName())) {
					availableFields.put(name, p);
				}
			}
		}
		return availableFields;
	}

	/**
	 * Gets the name of a property.
	 * 
	 * UML Default Name for unnamed properties / associations is typename
	 * 
	 * @param p Property
	 * @return String the default name
	 */
	private static String getDefaultNameForProperty(Property p) {
		if (p.getName().trim().length() > 0) {
			return p.getName();
		}

		String defaultName= p.getType().getName();
		defaultName= firstDown(defaultName);
		if (p.getUpper() > 1 || p.getUpper() < 0) {
			// plural name
			if (defaultName.endsWith("s")) {
				defaultName+= "es";
			} else {
				defaultName+= "s";
			}
		}
		return defaultName;
	}

	private static String firstDown(String string) {
		return string.substring(0, 1).toLowerCase() + string.substring(1);
	}


	private static SMMType checkIfTypeExists(Participant participant, String typeName) {
		EList<SMMType> types= participant.getTypes();
		for (SMMType ismType : types) {
			if (ismType.getName().equalsIgnoreCase(typeName))
				return ismType;
		}
		return null;
	}

	public static List<ActivityNode> filterEvents(EList<ActivityEdge> outgoings) {
		List<ActivityNode> filtered= new ArrayList<ActivityNode>();
		for (ActivityEdge activityEdge : outgoings) {
			if (StereotypeUtil.hasStereotype(activityEdge, StereoType.EVENTFLOW))
				filtered.add(activityEdge.getTarget());
		}
		return filtered;
	}

	public static List<ActivityNode> filterHandlerEdges(EList<ActivityEdge> outgoings) {
		List<ActivityNode> filtered= new ArrayList<ActivityNode>();
		for (ActivityEdge activityEdge : outgoings) {
			if (StereotypeUtil.hasStereotype(activityEdge, StereoType.EVENTFLOW, StereoType.COMPENSATIONFLOW))
				filtered.add(activityEdge.getTarget());
		}
		return filtered;
	}


}
