/*
 * 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.smm2java.impl;

import java.util.HashMap;
import java.util.Map;

import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.common.util.TreeIterator;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.eclipse.gmt.modisco.java.AbstractTypeDeclaration;
import org.eclipse.gmt.modisco.java.ClassDeclaration;
import org.eclipse.gmt.modisco.java.CompilationUnit;
import org.eclipse.gmt.modisco.java.InterfaceDeclaration;
import org.eclipse.gmt.modisco.java.MethodDeclaration;
import org.eclipse.gmt.modisco.java.Model;
import org.eclipse.gmt.modisco.java.Package;
import org.eclipse.gmt.modisco.java.ParameterizedType;
import org.eclipse.gmt.modisco.java.PrimitiveType;
import org.eclipse.gmt.modisco.java.Type;
import org.eclipse.gmt.modisco.java.TypeAccess;
import org.eclipse.gmt.modisco.java.TypeDeclaration;
import org.eclipse.gmt.modisco.java.emf.JavaFactory;

import eu.mdd4soa.smm.behaviour.ServiceActivity;
import eu.mdd4soa.smm.statik.BooleanType;
import eu.mdd4soa.smm.statik.ExceptionType;
import eu.mdd4soa.smm.statik.InParameter;
import eu.mdd4soa.smm.statik.IntegerType;
import eu.mdd4soa.smm.statik.InterfaceOperation;
import eu.mdd4soa.smm.statik.InterfaceParameter;
import eu.mdd4soa.smm.statik.MessageType;
import eu.mdd4soa.smm.statik.RealType;
import eu.mdd4soa.smm.statik.SMMType;
import eu.mdd4soa.smm.statik.StringType;
import eu.mdd4soa.smm.statik.TypedMultiElement;
import eu.mdd4soa.trans.smm2java.JMHelper;

public class TypeTracker {

	private Model fModel;

	private Package fRootPackage;

	private static String[] fPrimitives= { "void", "boolean", "int", "double" };

	private static final JavaFactory jf= JavaFactory.eINSTANCE;

	Map<ServiceActivity, TypeDeclaration> fBehaviourClasses= new HashMap<ServiceActivity, TypeDeclaration>();

	public TypeTracker(Model javaModel, Package rootPackage) {
		fModel= javaModel;
		fRootPackage= rootPackage;
	}

	/**
	 * Creates a class with the qualified name, creating packages if necessary.
	 * If the class does not exist, a compilation unit is created for it.
	 */
	public TypeDeclaration createClass(String qualifiedName) {
		return createType(qualifiedName, false);
	}

	/**
	 * Creates an interface with the qualified name, creating packages if
	 * necessary. If the interface does not exist, a compilation unit is created
	 * for it.
	 */
	public InterfaceDeclaration createInterface(String qualifiedName) {
		return (InterfaceDeclaration) createType(qualifiedName, true);
	}

	/**
	 * Creates a parameterized version of the given type with the given
	 * parameter types
	 * 
	 */
	public ParameterizedType createParameterizedType(Type baseType, Type... parameter) {

		if (baseType == null)
			throw new NullPointerException("getParameterizedType() called with null baseType");

		ParameterizedType type= jf.createParameterizedType();

		TypeAccess t1= jf.createTypeAccess();
		t1.setType(baseType);

		type.setType(t1);

		for (Type type2 : parameter) {
			TypeAccess t2= jf.createTypeAccess();
			t2.setType(type2);
			type.getTypeArguments().add(t2);
		}

		fModel.getOrphanTypes().add(type);
		return type;
	}

	private TypeDeclaration createType(String fqName, boolean isInterface) {

		if (!fqName.contains("."))
			throw new RuntimeException("Assertion failed, qualified name of handleJavaType has no segments.");

		if (fqName.contains("<"))
			throw new RuntimeException("Assertion failed, no generics allowed.");

		String pckg= fqName.substring(0, fqName.lastIndexOf("."));
		String typeName= fqName.substring(fqName.lastIndexOf(".") + 1);

		Package thePackage= ensurePackageInFromModel(pckg);

		EList<AbstractTypeDeclaration> ownedElements= thePackage.getOwnedElements();
		for (AbstractTypeDeclaration abstractTypeDeclaration : ownedElements) {
			if (abstractTypeDeclaration.getName().equals(typeName))
				return (ClassDeclaration) abstractTypeDeclaration;
		}

		TypeDeclaration d= (isInterface) ? jf.createInterfaceDeclaration() : jf.createClassDeclaration();
		d.setName(typeName);

		thePackage.getOwnedElements().add(d);

		CompilationUnit cu= jf.createCompilationUnit();
		cu.setName(typeName + ".java");
		fModel.getCompilationUnits().add(cu);
		cu.setPackage(thePackage);
		d.setOriginalCompilationUnit(cu);
		cu.getTypes().add(d);

		return d;
	}

	public Type getType(String fullyQualifiedName) {

		if (isPrimitive(fullyQualifiedName)) {
			return getPrimitiveType(fModel, fullyQualifiedName);
		}

		String simpleName= fullyQualifiedName;
		String packageName= null;
		if (fullyQualifiedName.contains(".")) {
			simpleName= fullyQualifiedName.substring(fullyQualifiedName.lastIndexOf(".") + 1);
			packageName= fullyQualifiedName.substring(0, fullyQualifiedName.lastIndexOf("."));
		}

		TreeIterator<Object> contents= EcoreUtil.getAllContents(fModel, false);
		while (contents.hasNext()) {
			Object next= contents.next();
			if (next instanceof TypeDeclaration) {
				TypeDeclaration theType= (TypeDeclaration) next;
				if (theType.getName().equals(simpleName)) {
					if (matchesPackage(theType.getPackage(), packageName))
						return theType;
				}
			}
		}

		return null;

	}

	private Type getPrimitiveType(Model model, String name) {

		EList<Type> orphanTypes= model.getOrphanTypes();
		for (Type type : orphanTypes) {
			if (type instanceof PrimitiveType) {
				if (type.getName().equals(name))
					return type;
			}
		}

		return null;
	}

	private boolean isPrimitive(String name) {
		for (String typeString : fPrimitives) {
			if (typeString.equals(name))
				return true;
		}
		return false;
	}

	private boolean matchesPackage(Package package1, String packageName) {
		if (packageName == null && package1 == null)
			return true;

		if (packageName.contains(".")) {
			String lastPackage= packageName.substring(packageName.lastIndexOf(".") + 1);
			if (package1.getName().equals(lastPackage)) {
				String prevPackage= packageName.substring(0, packageName.lastIndexOf("."));
				return matchesPackage(package1.getPackage(), prevPackage);

			} else
				return false;

		} else {
			return package1.getPackage() == null && package1.getName().equals(packageName);
		}
	}

	public Type resolveTypeWithMultiplicityAndImports(CompilationUnit unit, TypedMultiElement me) {

		Type rtype= null;

		if (me.getMax() == 1) {
			rtype= getJavaTypeForSMMType(me.getType());
		} else {
			rtype= getMatchingJavaCollectionType(me);
		}

		// Imports?
		if (!isPrimitive(rtype.getName())) {

			// Add the set/list
			if (rtype instanceof org.eclipse.gmt.modisco.java.ParameterizedType) {
				JMHelper.ensureImport(unit, ((org.eclipse.gmt.modisco.java.ParameterizedType) rtype).getType().getType());

				// Add the type parameter import.
				Type baseType= getJavaTypeForSMMType(me.getType());
				JMHelper.ensureImport(unit, baseType);

			} else
				JMHelper.ensureImport(unit, rtype);
		}

		return rtype;
	}

	private org.eclipse.gmt.modisco.java.ParameterizedType getMatchingJavaCollectionType(TypedMultiElement el) {
		Type rawParamType= null;

		if (el.isIsOrdered()) {
			rawParamType= getType("java.util.ArrayList");
		} else {
			rawParamType= getType("java.util.HashSet");
		}

		return createParameterizedType(rawParamType, getJavaTypeForSMMType(el.getType()));
	}

	public Type getJavaTypeForSMMType(SMMType type) {

		String name= null;

		if (type instanceof StringType)
			name= "java.lang.String";
		if (type instanceof RealType)
			name= "double";
		if (type instanceof IntegerType)
			name= "int";
		if (type instanceof BooleanType)
			name= "boolean";

		if (type instanceof MessageType)
			name= fRootPackage.getName() + ".data." + type.getName();

		if (type instanceof ExceptionType)
			name= fRootPackage.getName() + ".data." + type.getName();

		return getType(name);
	}

	public MethodDeclaration createMethodForInterfaceOperation(CompilationUnit cUnit, InterfaceOperation interfaceOperation) {

		// GOTCHA Create a method WITHOUT a body!
		MethodDeclaration d= JavaFactory.eINSTANCE.createMethodDeclaration();
		d.setName(interfaceOperation.getName());
		d.setModifier(JMHelper.createModifier("public"));

		d.getThrownExceptions().add(JMHelper.createTypeAccessTo(getType("util.ServiceException")));

		EList<InterfaceParameter> parameters= interfaceOperation.getParameters();
		for (InterfaceParameter interfaceParameter : parameters) {

			Type parameterType= resolveTypeWithMultiplicityAndImports(cUnit, interfaceParameter);

			if (interfaceParameter instanceof InParameter) {
				// Input
				d.getParameters().add(JMHelper.createLocalVariable(parameterType, interfaceParameter.getName()));
			} else {
				// Output
				d.setReturnType(JMHelper.createTypeAccessTo(parameterType));
			}
		}

		return d;
	}

	private Package ensurePackageInFromModel(String qualifiedPackageName) {

		String[] split= qualifiedPackageName.split("\\.");
		Package current= null;
		for (String string : split) {
			Package next= getPackageFromModel(current, string);
			if (next == null)
				next= addPackage(current, string);
			current= next;
		}

		return current;
	}

	private Package addPackage(Package current, String string) {

		Package pck= jf.createPackage();
		pck.setName(string);
		if (current == null)
			fModel.getOwnedElements().add(pck);
		else
			current.getOwnedPackages().add(pck);

		return pck;
	}

	private Package getPackageFromModel(Package parent, String string) {

		EList<Package> tosearch= null;
		if (parent != null)
			tosearch= parent.getOwnedPackages();
		else
			tosearch= fModel.getOwnedElements();

		for (Package package1 : tosearch) {
			if (package1.getName().equals(string))
				return package1;
		}
		return null;
	}

	public void registerMainClass(ServiceActivity behaviour, TypeDeclaration bClass) {
		fBehaviourClasses.put(behaviour, bClass);
	}

	public TypeDeclaration getMainClass(ServiceActivity behaviour) {
		return fBehaviourClasses.get(behaviour);
	}

}
