package transformation.us_transformationplugin;

import transformation.associationtags.AssociationTag;

import com.nomagic.magicdraw.core.Application;
import com.nomagic.magicdraw.core.Project;
import com.nomagic.magicdraw.openapi.uml.ModelElementsManager;
import com.nomagic.magicdraw.openapi.uml.PresentationElementsManager;
import com.nomagic.magicdraw.openapi.uml.ReadOnlyElementException;
import com.nomagic.magicdraw.openapi.uml.SessionManager;
import com.nomagic.magicdraw.ui.actions.DefaultDiagramAction;
import com.nomagic.magicdraw.ui.dialogs.MDDialogParentProvider;
import com.nomagic.magicdraw.uml.DiagramTypeConstants;
import com.nomagic.magicdraw.uml.symbols.DiagramPresentationElement;
import com.nomagic.magicdraw.uml.symbols.shapes.ShapeElement;
import com.nomagic.uml2.ext.jmi.helpers.ModelHelper;
import com.nomagic.uml2.ext.jmi.helpers.StereotypesHelper;
import com.nomagic.uml2.ext.magicdraw.actions.mdbasicactions.CallBehaviorAction;
import com.nomagic.uml2.ext.magicdraw.activities.mdbasicactivities.ActivityEdge;
import com.nomagic.uml2.ext.magicdraw.activities.mdintermediateactivities.ActivityPartition;
import com.nomagic.uml2.ext.magicdraw.classes.mdassociationclasses.AssociationClass;
import com.nomagic.uml2.ext.magicdraw.classes.mdassociationclasses.impl.AssociationClassImpl;
import com.nomagic.uml2.ext.magicdraw.classes.mddependencies.Dependency;
import com.nomagic.uml2.ext.magicdraw.classes.mdkernel.Comment;
import com.nomagic.uml2.ext.magicdraw.classes.mdkernel.Diagram;
import com.nomagic.uml2.ext.magicdraw.classes.mdkernel.Element;
import com.nomagic.uml2.ext.magicdraw.classes.mdkernel.Namespace;
import com.nomagic.uml2.ext.magicdraw.classes.mdkernel.Operation;
import com.nomagic.uml2.ext.magicdraw.classes.mdkernel.Property;
import com.nomagic.uml2.ext.magicdraw.classes.mdkernel.Relationship;
import com.nomagic.uml2.ext.magicdraw.mdprofiles.Profile;
import com.nomagic.uml2.impl.ElementsFactory;

import transformation.wizards.SecureUML_Wizard;

import java.awt.event.ActionEvent;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.swing.JOptionPane;

public class US_Transformation extends DefaultDiagramAction {


	/**
	* Creates action with name "UMLsec -> SecureUML Transformation" in the diagram context menu
	*/
	public US_Transformation()	{
	super("", "UMLsec -> SecureUML Transformation", null, null);
	}
	
	/**
	 * Describes the actions that follow clicking the menu item in the diagram context menu. 
	 */
	public void actionPerformed(ActionEvent e)	{
	
	Project project = Application.getInstance().getProject();

	Namespace parent = project.getModel();
	Diagram activityDiagram = project.getActiveDiagram().getDiagram();
	
	// Consistency check: US transformation can only be performed on an activity diagram with the stereotype <<rbac>>
	if (project.getActiveDiagram().getDiagramType().getType().equals("Activity Diagram") == false) {
		javax.swing.JOptionPane.showMessageDialog( null, "Diagram must be an Activity Diagram");
	}
	
	else if (StereotypesHelper.getAppliedStereotypeByString(activityDiagram, "rbac") == null) {
		javax.swing.JOptionPane.showMessageDialog( null, "Diagram must be of sterotype <<rbac>>");
	}
	
	else {
		try {
			transform(project, parent, activityDiagram);
		}
		catch(ReadOnlyElementException e1) {
			javax.swing.JOptionPane.showMessageDialog( null, "Element on diagram must be of class Element.");
		}
	}
	
	
}

	private void transform(Project project, Namespace parent, Diagram activityDiagram) throws ReadOnlyElementException {
		
		// In order to create new elements in a model, a session needs to be started
		SessionManager.getInstance().createSession("Create element");
		
		// Elements are created with the help of an elements factory
		ElementsFactory ef = Application.getInstance().getProject().getElementsFactory();
			
		// A new profile is created
		Profile profile = ef.createProfileInstance();
		profile.setName("SecureUML model");
		
		// The profile is added to the model
		ModelElementsManager.getInstance().addElement(profile, project.getModel());
		
		PresentationElementsManager PEMInstance = PresentationElementsManager.getInstance();
		
		// A class diagram is created
		Diagram classDiagram = ModelElementsManager.getInstance().
			createDiagram(DiagramTypeConstants.UML_CLASS_DIAGRAM, parent);
		
		
		classDiagram.setName("SecureUML diagram");
		classDiagram.setOwner(profile);
	
		// Open class diagram for editing
		DiagramPresentationElement classDiagramPE = project.getDiagram(classDiagram);
		classDiagramPE.open();
		
		// Elements on the UMLsec diagram
		Collection<Element> diagramOwnedElements = activityDiagram.getOwnedElement();
		
		// List of association tags in the UMLsec model 
		List<AssociationTag> tags = extractTags(diagramOwnedElements);
		
		// Lists of resource, user and role names
		List<String> resourcesStrList = new ArrayList<String>();
		List<String> usersStrList = new ArrayList<String>();
		List<String> rolesStrList = new ArrayList<String>();
		
		// List with references to protected resource classes on the SecureUML diagram
		List<com.nomagic.uml2.ext.magicdraw.classes.mdkernel.Class> resourceClasses = 
			new ArrayList<com.nomagic.uml2.ext.magicdraw.classes.mdkernel.Class>();
		
		if (tags == null) {
			tags = new ArrayList<AssociationTag>();
		}
		
		// Mappings of user/role/resource names to references of corresponding class shapes on the SecureUML diagram
		Map<String, ShapeElement> userShapes = new HashMap<String, ShapeElement>();
		Map<String, ShapeElement> roleShapes =	new HashMap<String, ShapeElement>();
		Map<String, ShapeElement> resourceShapes =	new HashMap<String, ShapeElement>();
		
		// Mappings of user/role names to references of corresponding classes in the SecureUML model
		Map<String, com.nomagic.uml2.ext.magicdraw.classes.mdkernel.Class> userClasses = 
			new HashMap<String, com.nomagic.uml2.ext.magicdraw.classes.mdkernel.Class>();
		Map<String, com.nomagic.uml2.ext.magicdraw.classes.mdkernel.Class> roleClasses = 
			new HashMap<String, com.nomagic.uml2.ext.magicdraw.classes.mdkernel.Class>();
		Map<String, com.nomagic.uml2.ext.magicdraw.classes.mdkernel.Class> actionOperationClasses =
			new HashMap<String, com.nomagic.uml2.ext.magicdraw.classes.mdkernel.Class>();
		
		// Containers for information needed in the transformation 
		Map<String, List<String>> resourceOperations = new HashMap<String, List<String>>();
		Map<String, List<String>> roleResourceCombos = new HashMap<String, List<String>>();
		Map<String, List<String>> actionPreviousActions = new HashMap<String, List<String>>();
		
		// Iterate through the association tags of the UMLsec model
		for (AssociationTag tag : tags) {
			
			// Find the resource, which the action belongs to
			String resourceStr = findActionResource(tag, activityDiagram, actionPreviousActions);
			
			addResourceClasses(project, ef, profile, PEMInstance, classDiagramPE,
					resourcesStrList, resourceClasses, resourceShapes,
					actionOperationClasses, resourceOperations, tag,
					resourceStr);
			
			addUserClasses(project, ef, profile, PEMInstance, classDiagramPE,
					usersStrList, userShapes, userClasses, tag);
			
			
		}
		
		// Iterate through all the elements of the activity diagram in order to identify roles
		for (Element element : activityDiagram.getOwner().getOwnedElement()) {
			
			addRoleClasses(project, ef, profile, PEMInstance, classDiagramPE,
					resourcesStrList, rolesStrList, roleShapes, roleClasses,
					element);
		}
		
		// Iterate through association tags again
		for (AssociationTag tag : tags) {
			try {
				
				ShapeElement roleShape = addDependencies(project, ef, profile,
						PEMInstance, userShapes, roleShapes, userClasses,
						roleClasses, tag);
			
				addAssociationClasses(project, ef, profile, PEMInstance,
						resourceShapes, actionOperationClasses,
						roleResourceCombos, actionPreviousActions, tag,
						roleShape);
			}
			catch (IllegalArgumentException e) {
				JOptionPane.showMessageDialog(MDDialogParentProvider.getProvider().getDialogParent(), 
						"The association tags contain some mismatches considering the following tag:\n\n" +
						tag.toString() + 
						"\n\nAre you sure the protected action doesn't belong to a role instead of a resource?");
				continue;
			}
		}
		
		// Session needs to be closed after we're done creating new elements
		SessionManager.getInstance().closeSession();
		
		// Run consistency check and add notes on the diagram
		new SecureUML_Wizard();
		
		// Set the project editable
		project.setDirty(true);
	}

	private void addAssociationClasses(
			Project project,
			ElementsFactory ef,
			Profile profile,
			PresentationElementsManager PEMInstance,
			Map<String, ShapeElement> resourceShapes,
			Map<String, com.nomagic.uml2.ext.magicdraw.classes.mdkernel.Class> actionOperationClasses,
			Map<String, List<String>> roleResourceCombos,
			Map<String, List<String>> actionPreviousActions,
			AssociationTag tag, ShapeElement roleShape)
			throws ReadOnlyElementException {
		ShapeElement resourceShape = null;
		AssociationClass associationClass = null;
		
		if (mapContainsKeyStr(actionOperationClasses, tag.getAction())) {
			// Select the name of the resource, which the action of the tag belongs to
			String resourceName = actionOperationClasses.get(tag.getAction()).getName();
			if (mapContainsKeyStr(roleResourceCombos, tag.getRole()) == false) {
				roleResourceCombos.put(tag.getRole(), new ArrayList<String>());
			}
			resourceShape = resourceShapes.get(resourceName);
			if (listContainsString(roleResourceCombos.get(tag.getRole()), resourceName) == false) {
				// Associate the role with the resource of the action in the tag
				roleResourceCombos.get(tag.getRole()).add(resourceName);
			}
			
			for (Relationship relationship : resourceShape.getActualElement().get_relationshipOfRelatedElement()) {
				if (relationship instanceof AssociationClassImpl) {
					if (relationship.getRelatedElement().contains(roleShape.getActualElement())) {
						associationClass = (AssociationClass) relationship;
					}
				}
			}
		}
		
		if (roleShape != null && resourceShape != null && associationClass == null) {
			
			// Create an association class between the role and the resource
			associationClass = ef.createAssociationClassInstance();
			associationClass.setOwner(profile);
			addStereotype(associationClass, "secuml.permission", project, profile);
			ModelHelper.setSupplierElement(associationClass, resourceShape.getActualElement());
			ModelHelper.setClientElement(associationClass, roleShape.getActualElement());
			PEMInstance.createPathElement(associationClass, roleShape, resourceShape);
			
		}
		
		// Operations are added to the association class:
		// actions that belong to the role's activity partition 
		// and are performed immediately before the actions of the resource's activity partition
		if (mapContainsKeyStr(actionPreviousActions, tag.getAction())) {
			for (String action : actionPreviousActions.get(tag.getAction())) {
				boolean actionExists = false;
				for (Property existingAttribute : associationClass.getAttribute()) {
					if (existingAttribute.getName().equals(action)) {
						actionExists = true;
					}
				}
				if (!actionExists) { 
					Property pr = ef.createPropertyInstance();
					pr.setName(action);
					associationClass.getOwnedAttribute().add(pr);
				}
			}
		}
		
		for (Property memberEnd : associationClass.getMemberEnd()) {
			ModelHelper.setNavigable(memberEnd, true);
		}
	}

	private ShapeElement addDependencies(
			Project project,
			ElementsFactory ef,
			Profile profile,
			PresentationElementsManager PEMInstance,
			Map<String, ShapeElement> userShapes,
			Map<String, ShapeElement> roleShapes,
			Map<String, com.nomagic.uml2.ext.magicdraw.classes.mdkernel.Class> userClasses,
			Map<String, com.nomagic.uml2.ext.magicdraw.classes.mdkernel.Class> roleClasses,
			AssociationTag tag) throws ReadOnlyElementException {
		// Create a dependency link with the stereotype <<assignment>> between the user and the role
		Dependency dependencyLink = ef.createDependencyInstance();
		dependencyLink.setOwner(profile);
		dependencyLink.getClient().add(userClasses.get(tag.getUser()));
		dependencyLink.getSupplier().add(roleClasses.get(tag.getRole()));
		addStereotype(dependencyLink, "assignment", project, profile);
		
		ShapeElement userShape = userShapes.get(tag.getUser());
		ShapeElement roleShape = roleShapes.get(tag.getRole());
		if (userShape != null && roleShape != null) {
			PEMInstance.createPathElement(dependencyLink,	userShape, roleShape);
		}
		return roleShape;
	}

	private void addRoleClasses(
			Project project,
			ElementsFactory ef,
			Profile profile,
			PresentationElementsManager PEMInstance,
			DiagramPresentationElement classDiagramPE,
			List<String> resourcesStrList,
			List<String> rolesStrList,
			Map<String, ShapeElement> roleShapes,
			Map<String, com.nomagic.uml2.ext.magicdraw.classes.mdkernel.Class> roleClasses,
			Element element) throws ReadOnlyElementException {
		if (element.getHumanType().equals("Activity Partition")) {
			String name = ((ActivityPartition)element).getName();
			
			// If an activity partition has not already been translated into a resource class,
			// create a new class in the SecureUML model with its name and the stereotype <<secuml.role>>
			if (listContainsString(resourcesStrList, name) == false &&
					listContainsString(rolesStrList, name) == false) {
				rolesStrList.add(name);
				com.nomagic.uml2.ext.magicdraw.classes.mdkernel.Class roleClass = ef.createClassInstance();
				roleClass.setName(name);
				roleClass.setOwner(profile);
				addStereotype(roleClass, "secuml.role", project, profile);
				
				// Create a visible element on the diagram
				ShapeElement roleShape = 
					PEMInstance.createShapeElement(roleClass, classDiagramPE);
				roleShape.setLocation(250, 50 + rolesStrList.size()*100);
				roleShapes.put(name, roleShape);
				roleClasses.put(name, roleClass);
			}
		}
	}

	private void addUserClasses(
			Project project,
			ElementsFactory ef,
			Profile profile,
			PresentationElementsManager PEMInstance,
			DiagramPresentationElement classDiagramPE,
			List<String> usersStrList,
			Map<String, ShapeElement> userShapes,
			Map<String, com.nomagic.uml2.ext.magicdraw.classes.mdkernel.Class> userClasses,
			AssociationTag tag) throws ReadOnlyElementException {
		String userStr = tag.getUser();
		
		// If a class with the name of this user has not been created in the SecureUML model,
		// create a new class with the stereotype <<secuml.user>>
		if (listContainsString(usersStrList, userStr) == false) {
			usersStrList.add(userStr);
			com.nomagic.uml2.ext.magicdraw.classes.mdkernel.Class userClass = ef.createClassInstance();
			userClass.setName(userStr);
			userClass.setOwner(profile);
			addStereotype(userClass, "secuml.user", project, profile);
			
			// Create a visible diagram element
			ShapeElement userShape = 
				PEMInstance.createShapeElement(userClass, classDiagramPE);
			userShape.setLocation(50, 50 + usersStrList.size()*100);
			userShapes.put(userStr, userShape);
			userClasses.put(userStr, userClass);
		}
	}

	private void addResourceClasses(
			Project project,
			ElementsFactory ef,
			Profile profile,
			PresentationElementsManager PEMInstance,
			DiagramPresentationElement classDiagramPE,
			List<String> resourcesStrList,
			List<com.nomagic.uml2.ext.magicdraw.classes.mdkernel.Class> resourceClasses,
			Map<String, ShapeElement> resourceShapes,
			Map<String, com.nomagic.uml2.ext.magicdraw.classes.mdkernel.Class> actionOperationClasses,
			Map<String, List<String>> resourceOperations, AssociationTag tag,
			String resourceStr) throws ReadOnlyElementException {
		
		com.nomagic.uml2.ext.magicdraw.classes.mdkernel.Class resourceClass;
		// If a class with the name of this resource has not already been created,
		// create a class with the stereotype <<secuml.resource>> in the SecureUML model 
		if (listContainsString(resourcesStrList, resourceStr) == false) {
			
			resourceClass = ef.createClassInstance();
			resourceClass.setName(resourceStr);
			resourceClass.setOwner(profile);
			addStereotype(resourceClass, "secuml.resource", project, profile);
			
			// Create a visible element on the diagram
			ShapeElement resourceShape = PEMInstance.createShapeElement(resourceClass, classDiagramPE);
			resourcesStrList.add(resourceStr);
			resourceShape.setLocation(650, 50 + resourcesStrList.size()*100);
			resourceOperations.put(resourceStr, new ArrayList<String>());
			resourceClasses.add(resourceClass);
			resourceShapes.put(resourceClass.getName(), resourceShape);
		}
		else {
			resourceClass = resourceClasses.get(resourcesStrList.indexOf(resourceStr));
		}
		
		// If the resource class does not already have an operation with the name of the tag's action,
		// add that operation to the previously created/selected class in the SecureUML model
		if (listContainsString(resourceOperations.get(resourceStr), tag.getAction()) == false) {
			Operation protectedAction = ef.createOperationInstance();
			protectedAction.setName(tag.getAction());
			resourceClass.getOwnedOperation().add(protectedAction);
			
			// Map the action's name to a reference to the resource class
			if (mapContainsKeyStr(actionOperationClasses, tag.getAction()) == false) {
				actionOperationClasses.put(tag.getAction(), resourceClass);
			}
			
			// Make a note that this operation has been added to this resource
			resourceOperations.get(resourceStr).add(tag.getAction());
		}
	}


	
	/**
	 * This method checks if a given stereotype already exists in the project and if not, creates it.
	 * Then the stereotype is applied to the given element.
	 * 
	 * @param element 			The element to which the stereotype is added
	 * @param stereotypeName 	Name of the stereotype
	 * @param project 			Name of the project
	 * @param profile			Name of the profile, which becomes the owner of the stereotype
	 * 
	 */
	private void addStereotype(Element element, String stereotypeName, Project project, Profile profile) {
		if (StereotypesHelper.getStereotype(project, stereotypeName) == null) {
			com.nomagic.uml2.ext.magicdraw.classes.mdkernel.Class metaClass =
				StereotypesHelper.getMetaClassByName(project, "Class");

			StereotypesHelper.createStereotype(profile, stereotypeName, Arrays.asList(metaClass));
		}
		StereotypesHelper.addStereotypeByString(element, stereotypeName);
	}

	/**
	 * This method finds the name of the resource,
	 * which the action of the given association tag belongs to.
	 * 
	 * @param tag					The association tag containing the name of the action
	 * @param activityDiagram		The activity diagram
	 * @param actionPreviousActions Map that links the name of an action to a list of actions that are performed immediately before it on the activity diagram
	 * @return						The name of the resource, to which the action belongs to on the activity diagram
	 */
	private String findActionResource(AssociationTag tag,
			Diagram activityDiagram, Map<String, List<String>> actionPreviousActions) {
		
		String resourceStr = "";
		
		// Iterate through all the elements of the activity diagram
		for (Element element : activityDiagram.getOwner().getOwnedElement()) {
			
			// Find the action with the given name
			if (element.getHumanType().equals("Call Behavior Action") && 
					element.getHumanName().equals("Call Behavior Action " + tag.getAction())) {
				
				// Select the name of the activity partition it belongs to
				resourceStr= ((CallBehaviorAction)element).getInPartition().iterator().next().getName();
				
				// For later operations, store the names of the actions that are performed immediately before the given action
				if (mapContainsKeyStr(actionPreviousActions, tag.getAction()) == false) {
					actionPreviousActions.put(tag.getAction(), new ArrayList<String>());
				}
				
				for (ActivityEdge edge : ((CallBehaviorAction)element).getIncoming()) {
					if (listContainsString(actionPreviousActions.get(tag.getAction()), edge.getSource().getName())
							== false) {
						actionPreviousActions.get(tag.getAction()).add(edge.getSource().getName());						
					}
				}
			}
		}
		
		return resourceStr;
	}

	/**
	 * This method finds the comment, where the information about association tags is stored
	 * and, based on that, creates new elements that can be used, while the plug-in is running
	 * 
	 * @param elements 	List of elements, one of which is the named comment
	 * @return			List of association tags extracted from the comment
	 */
	private List<AssociationTag> extractTags(Collection<Element> elements) {
		for (Element element : elements) {
			if (element.getHumanType().equals("Comment")) {
				
				List<AssociationTag> tags = new ArrayList<AssociationTag>();
				
				// Each line of the comment contains information about one association tag
				for (String tagStr : ((Comment) element).getBody().split("\n")) {
					AssociationTag tag = new AssociationTag();
					if (tag.extractSelfFromStr(tagStr) != null) {
						tags.add(tag);
					}
				}
				return tags;
			}
		}
		return null;
	}
	
	/**
	 * A simple method for checking whether a List of Strings contains a given String
	 * 
	 * @param list	List of Strings
	 * @param str	The String to be searched for
	 * @return		True if the List contains the given String, false otherwise
	 */
	private boolean listContainsString(List<String> list, String str) {
		
		for (String element : list) {
			if (element.equals(str)) {
				return true;
			}
		}
		return false;
	}
	
	/**
	 * A simple method for checking whether a Map contains a key with the value of a given String
	 * 
	 * @param map	A Map
	 * @param str	The String to be searched for
	 * @return		True, if the Map contains a key with the value of the given String, false otherwise
	 */
	private boolean mapContainsKeyStr(Map<String, ?> map, String str) {
		
		for (String key : map.keySet()) {
			if (key.equals(str)) {
				return true;
			}
		}
		return false;
	}
}
