package br.uem.din.architectureEvolution.helpers;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map.Entry;

import org.eclipse.emf.common.util.BasicEList;
import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.uml2.uml.AggregationKind;
import org.eclipse.uml2.uml.Comment;
import org.eclipse.uml2.uml.Model;
import org.eclipse.uml2.uml.Package;
import org.eclipse.uml2.uml.PrimitiveType;
import org.eclipse.uml2.uml.Stereotype;
import org.eclipse.uml2.uml.TemplateableElement;
import org.eclipse.uml2.uml.Type;
import org.eclipse.uml2.uml.UMLFactory;
import org.eclipse.uml2.uml.UMLPackage;
import org.eclipse.uml2.uml.resource.UMLResource;

import br.uem.din.architectureEvolution.representation.AbstractionInterElementRelationship;
import br.uem.din.architectureEvolution.representation.Architecture;
import br.uem.din.architectureEvolution.representation.AssociationEnd;
import br.uem.din.architectureEvolution.representation.AssociationInterClassRelationship;
import br.uem.din.architectureEvolution.representation.Attribute;
import br.uem.din.architectureEvolution.representation.Class;
import br.uem.din.architectureEvolution.representation.Component;
import br.uem.din.architectureEvolution.representation.Concern;
import br.uem.din.architectureEvolution.representation.DependencyComponentInterfaceRelationship;
import br.uem.din.architectureEvolution.representation.DependencyInterClassRelationship;
import br.uem.din.architectureEvolution.representation.Element;
import br.uem.din.architectureEvolution.representation.GeneralizationInterClassRelationship;
import br.uem.din.architectureEvolution.representation.InterClassRelationship;
import br.uem.din.architectureEvolution.representation.InterElementRelationship;
import br.uem.din.architectureEvolution.representation.Interface;
import br.uem.din.architectureEvolution.representation.Method;
import br.uem.din.architectureEvolution.representation.Operation;
import br.uem.din.architectureEvolution.representation.Variability;

public class SDMetricsToEMFParser {
	
	private final HashMap<Interface, org.eclipse.uml2.uml.Interface> interfaces = new HashMap<Interface, org.eclipse.uml2.uml.Interface>();
	private final HashMap<Component, org.eclipse.uml2.uml.Component> components = new HashMap<Component, org.eclipse.uml2.uml.Component>();
	private final HashMap<Class, org.eclipse.uml2.uml.Class> classes = new HashMap<Class, org.eclipse.uml2.uml.Class>();
	private final HashMap<Operation, org.eclipse.uml2.uml.Operation> operations = new HashMap<Operation, org.eclipse.uml2.uml.Operation>();;
	private final HashMap<Method, org.eclipse.uml2.uml.Operation> methods = new HashMap<Method, org.eclipse.uml2.uml.Operation>();;
	private final Architecture architecture;
	private Collection<EObject> appliedStereotypes = new ArrayList<EObject>();
	private org.eclipse.uml2.uml.Model emfModel;
	private org.eclipse.uml2.uml.Profile emfProfile;
	private Model umlMetamodel;
	private org.eclipse.uml2.uml.Package umlPrimitiveTypes;
	private String pathToSave;
	private int number;

	public SDMetricsToEMFParser(Architecture architecture) {
		this(architecture, "", -1);
	}
	
	public SDMetricsToEMFParser(Architecture architecture, String pathToSave, int number) {
		this.architecture = architecture;
		this.pathToSave = pathToSave;
		this.number = number;
	}
	
	public void parseAndSave() {
		parse();
		save();
	}

	private void save() {
		try {
			URI profileURI = URI.createFileURI(pathToSave + emfProfile.getName() + getNumberStr());
			XMIHelper.save(emfProfile, profileURI);
			
			ArrayList<EObject> contents = new ArrayList<EObject>();
			contents.add(emfModel);
			contents.addAll(appliedStereotypes);
			URI modelURI = URI.createFileURI(pathToSave + architecture.getXMIFileName() + getNumberStr());
			XMIHelper.save(contents, modelURI);
		} catch (IOException e) {
			System.out.println(e);
		}
	}

	private String getNumberStr() {
		if (number >= 0) return String.valueOf(number);
		return "";
	}

	public void parse() {
		initialize();
		createModel();
		createComponents();
		createInterfaces();
		createRelationships();
		createNotes();
		createAndApplyProfile();
	}

	private void createAndApplyProfile() {
		HashMap<Concern, Stereotype> emfStereotypes = createProfile();
		
		emfModel.applyProfile(emfProfile);
		
		applyStereotypesToElements(emfStereotypes, classes);
		applyStereotypesToElements(emfStereotypes, components);
		applyStereotypesToElements(emfStereotypes, interfaces);
		applyStereotypesToElements(emfStereotypes, operations);
		applyStereotypesToElements(emfStereotypes, methods);
	}

	private HashMap<Concern, Stereotype> createProfile() {
		emfProfile = UMLFactory.eINSTANCE.createProfile();
		emfProfile.setName("Profile_" + architecture.getXMIFileName());
		
		org.eclipse.uml2.uml.Class componentMetaClass = referenceMetaclass("Component");
		org.eclipse.uml2.uml.Class classMetaClass = referenceMetaclass("Class");
		org.eclipse.uml2.uml.Class interfaceMetaClass = referenceMetaclass("Interface");
		org.eclipse.uml2.uml.Class methodMetaClass = referenceMetaclass("Operation");
		
		PrimitiveType stereotypeType = getPrimitiveType("Boolean");
		
		HashMap<Concern, Stereotype> emfStereotypes = new HashMap<Concern, Stereotype>(); 
		for (Concern concern : architecture.getConcerns()) {
			Stereotype stereotype = emfProfile.createOwnedStereotype(concern.getName(), false);
			stereotype.createExtension(componentMetaClass, false);
			stereotype.createExtension(classMetaClass, false);
			stereotype.createExtension(interfaceMetaClass, false);
			stereotype.createExtension(methodMetaClass, false);
			stereotype.setIsActive(true);
			stereotype.createOwnedAttribute("concern", stereotypeType);
			emfStereotypes.put(concern, stereotype);
		}
		
		emfProfile.define();
		return emfStereotypes;
	}

	private <T extends Element, S extends TemplateableElement> void applyStereotypesToElements(HashMap<Concern, Stereotype> emfStereotypes, HashMap<T, S> elements) {
		for (T element : elements.keySet()) {
			for (Concern concern : element.getConcerns()) {
				EObject stereotypeApplication = elements.get(element).applyStereotype(emfStereotypes.get(concern));
				appliedStereotypes.add(stereotypeApplication);
			}
		}
	}
	
	private org.eclipse.uml2.uml.Class referenceMetaclass(String name) {		
		org.eclipse.uml2.uml.Class metaclass = (org.eclipse.uml2.uml.Class) getUmlMetamodel().getOwnedType(name);
		emfProfile.createMetaclassReference(metaclass);
		return metaclass;
	}

	private Model getUmlMetamodel() {
		if (umlMetamodel == null){
			umlMetamodel = (Model) XMIHelper.load(URI.createURI(UMLResource.UML_METAMODEL_URI));
		}
		return umlMetamodel;
	}
	
	private Package getUmlPrimitiveTypes() {
		if (umlPrimitiveTypes == null){
			umlPrimitiveTypes = XMIHelper.load(URI.createURI(UMLResource.UML_PRIMITIVE_TYPES_LIBRARY_URI));
		}
		return umlPrimitiveTypes;
	}

	private void createNotes() {
		for (Variability variability : architecture.getVariability().values()) {
			Comment comment = emfModel.createOwnedComment();			
			comment.setBody(variability.toString());
			for (Element variationPoint : variability.getVariationPoints()) {
				org.eclipse.uml2.uml.Element emfElement = findEmfElement(variationPoint);
				comment.getAnnotatedElements().add(emfElement);
			}
		}
	}

	private org.eclipse.uml2.uml.Element findEmfElement(Element variationPoint) {
		if (variationPoint instanceof Component)
			return components.get(variationPoint);
		else if (variationPoint instanceof Class)
			return classes.get(variationPoint);
		else if (variationPoint instanceof Interface)
			return interfaces.get(variationPoint);
		
		return null;
	}

	private void initialize() {
		XMIHelper.init();
		components.clear();
		interfaces.clear();
		classes.clear();
		operations.clear();
		methods.clear();
		appliedStereotypes.clear();
	}
	
	private void createModel() {
		emfModel = UMLFactory.eINSTANCE.createModel();
		emfModel.setName(architecture.getXMIFileName());
	}
	
	private void createComponents() {
		for (Component component : architecture.getComponents().values()) {
			org.eclipse.uml2.uml.Component emfComponent = (org.eclipse.uml2.uml.Component) emfModel.createOwnedType(component.getName(), UMLPackage.Literals.COMPONENT);
			components.put(component, emfComponent);
			createClasses(component, emfComponent);
		}
	}

	private void createClasses(Component component, org.eclipse.uml2.uml.Component emfComponent) {
		for (Class class_ : component.getClasses().values()) {
			org.eclipse.uml2.uml.Class emfClass = emfComponent.createOwnedClass(class_.getName(), false);
			
			for (Attribute attribute : class_.getAttributes().values()) {
				PrimitiveType primitiveType = getPrimitiveType(attribute.getType());
				emfClass.createOwnedAttribute(attribute.getName(), primitiveType);
			}
			
			for (Method method : class_.getMethods().values()) {
				PrimitiveType returnType = getPrimitiveType(method.getReturnType());
				EList<String> parametersNames = new BasicEList<String>();
				EList<Type> parametersTypes = new BasicEList<Type>();
				for (Entry<String, String> parameter : method.getParameters().entrySet()) {
					parametersNames.add(parameter.getKey());
					parametersTypes.add(getPrimitiveType(parameter.getValue()));
				}
				org.eclipse.uml2.uml.Operation emfMethod = emfClass.createOwnedOperation(method.getName(), parametersNames, parametersTypes, returnType);
				methods.put(method, emfMethod);
			}
			
			classes .put(class_, emfClass);
		}
	}

	private PrimitiveType getPrimitiveType(String typeName) {
		return (PrimitiveType) getUmlPrimitiveTypes().getOwnedType(typeName);
	}
	
	private void createInterfaces() {
		for (Interface interface_ : architecture.getInterfaces().values()) {
			org.eclipse.uml2.uml.Interface emfInterface = emfModel.createOwnedInterface(interface_.getName());
			interfaces.put(interface_, emfInterface);

			for (Operation operation : interface_.getOperations()) {
				PrimitiveType returnType = getPrimitiveType(operation.getReturnType());
				EList<String> parametersNames = new BasicEList<String>();
				EList<Type> parametersTypes = new BasicEList<Type>();
				for (Entry<String, String> parameter : operation.getParameters().entrySet()) {
					parametersNames.add(parameter.getKey());
					parametersTypes.add(getPrimitiveType(parameter.getValue()));
				}
				org.eclipse.uml2.uml.Operation emfOperation = emfInterface.createOwnedOperation(operation.getName(), parametersNames, parametersTypes, returnType);
				operations.put(operation, emfOperation);
			}
		}
	}

	private void createRelationships() {
		createInterElementRelationships();
		createInterClassRelationships();
	}

	private void createInterElementRelationships() {
		for (InterElementRelationship relationship : architecture.getInterElementRelationships()) {
			if (relationship instanceof DependencyComponentInterfaceRelationship) {
				DependencyComponentInterfaceRelationship dependency = (DependencyComponentInterfaceRelationship)relationship;
				org.eclipse.uml2.uml.Component emfComponent = components.get(dependency.getComponent());
				emfComponent.createDependency(interfaces.get(dependency.getInterface()));
			} else if (relationship instanceof AbstractionInterElementRelationship) {
				AbstractionInterElementRelationship abstraction = (AbstractionInterElementRelationship)relationship;
				org.eclipse.uml2.uml.Component emfComponent = components.get(abstraction.getChild());
				emfComponent.createInterfaceRealization(null, interfaces.get(abstraction.getParent()));
			}
		}
	}
	
	private void createInterClassRelationships() {
		for (InterClassRelationship relationship : architecture.getInterClassRelationships()) {
			if(relationship instanceof AssociationInterClassRelationship) {
				AssociationInterClassRelationship association = (AssociationInterClassRelationship)relationship;
				AssociationEnd[] associationEnds = association.getParticipants().toArray(new AssociationEnd[0]);
				
				org.eclipse.uml2.uml.Class emfClass1 = classes.get(associationEnds[0].getCLSClass());
				org.eclipse.uml2.uml.Class emfClass2 = classes.get(associationEnds[1].getCLSClass());
				
				//TODO 
				emfClass1.createAssociation(associationEnds[0].isNavigable(), AggregationKind.NONE_LITERAL, emfClass1.getName(), 1, 1, emfClass2, associationEnds[1].isNavigable(), AggregationKind.NONE_LITERAL, emfClass2.getName(), 1, 1);
				
			} else if (relationship instanceof DependencyInterClassRelationship) {
				DependencyInterClassRelationship dependency = (DependencyInterClassRelationship)relationship;
				org.eclipse.uml2.uml.Class emfClassClient = classes.get(dependency.getClient());
				emfClassClient.createDependency(classes.get(dependency.getSupplier()));
			} else if (relationship instanceof GeneralizationInterClassRelationship) {
				GeneralizationInterClassRelationship generalization = (GeneralizationInterClassRelationship)relationship;
				org.eclipse.uml2.uml.Class emfClassChild = classes.get(generalization.getChild());
				org.eclipse.uml2.uml.Class emfClassParent = classes.get(generalization.getParent());
				emfClassChild.createGeneralization(emfClassParent);
			}
		}
	}
}
