/**
 * File:    AspectManagerImpl.java
 * Created: 10.07.2008
 *
 *
/*******************************************************************************
 * Copyright (c) 2008-2010 Open Wide SA <www.openwide.fr>
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *    Marc Dutoo, Open Wide SA 
 *    	- initial API and implementation
 *******************************************************************************/

package org.eclipse.jwt.we.conf.model.aspects.internal;

import java.util.ArrayList;
import java.util.List;

import org.eclipse.emf.common.util.TreeIterator;
import org.eclipse.emf.ecore.EClass;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.eclipse.jwt.we.conf.model.Aspect;
import org.eclipse.jwt.we.conf.model.AspectInstance;
import org.eclipse.jwt.we.conf.model.ConfModel;
import org.eclipse.jwt.we.conf.model.Profile;
import org.eclipse.jwt.we.conf.model.aspects.AspectManager;
import org.eclipse.jwt.we.conf.model.aspects.ConfRegistry;
import org.eclipse.jwt.we.conf.model.aspects.factory.AspectFactory;
import org.eclipse.jwt.we.conf.model.plugin.ConfPlugin;
import org.eclipse.jwt.we.conf.model.plugin.internal.EMFUtils;
import org.eclipse.jwt.we.conf.model.plugin.internal.Logger;
import org.eclipse.jwt.we.conf.model.resource.ConfModelResourceManager;
import org.eclipse.jwt.we.conf.model.resource.ConfModelStatus;

/**
 * This class does the aspects management.
 * 
 * It manages conf models. They can be provided in the following ways :
 * <ul>
 * <li>declared using extension point org.eclipse.jwt.we.conf (useful for
 * vendors)</li>
 * <li>discovered under directory conf (useful for power users)</li>
 * <li>embedded in a given workflow model (useful for export)</li>
 * </ul>
 * 
 * These makes it able to tell which aspects are relevant to a given model
 * element (see getAspects()).
 * 
 * From these, it enables the following features :
 * <ul>
 * <li>ModelElementItemProvider can display "new child" commands on a model
 * element for every relevant aspect (thanks to getAspects()) and then create
 * them (thanks to createAspectInstance())</li>
 * <li>TODO XXX can automatically add "default" aspects to a newly created model
 * element(thanks to onCreate())</li>
 * </ul>
 * 
 * Aspect instantiation is handled by its AspectFactory.
 * 
 * TODO : still itf, doc, polish maybe merge with Views maybe stil improve
 * graphical editor for jwt-we-conf model improve generic property property tab,
 * maybe still improve facilities for building aspect-specific property tabs
 * 
 * @version $Id: AspectManagerImpl.java,v 1.4 2010-06-28 16:29:16 chsaad Exp $
 * @author Marc Dutoo, Open Wide SA
 */
public class AspectManagerImpl implements AspectManager
{

	protected static Logger logger = Logger.getLogger(AspectManagerImpl.class);

	protected static final List<Profile> EMPTY_PROFILE_LIST = new ArrayList<Profile>(0);


	protected AspectManagerImpl()
	{

	}


	public static AspectManagerImpl createAspectManager()
	{
		AspectManagerImpl aspectManager = new AspectManagerImpl();
		return aspectManager;
	}


	/**
	 * Helper
	 * 
	 * @param element
	 *            of the conf model or of the decorated model
	 * @return its root element
	 */
	public final EObject getModelRoot(Object element)
	{
		EObject rootEObject = EMFUtils.getRootEObject(element);
		if (rootEObject instanceof ConfModel)
		{
			return ((ConfModel) rootEObject).getEnrichedModel();
		}
		return rootEObject;
	}


	/**
	 * TODO impl, cache loaded, manage rename getEmbeddedConfModel ?
	 * getModelConfModel ?
	 * 
	 * @param modelRoot
	 * @return
	 */
	public ConfModel getConfModel(EObject modelElement)
	{
		if (modelElement instanceof ConfModel) {
			return (ConfModel) modelElement;
		}
		ConfModelStatus cmStatus = new ConfModelStatus();
		ConfModel confModel = getConfModel(modelElement, cmStatus);
		// NB. errors already logged within above
		return confModel;
	}


	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.eclipse.jwt.we.conf.aspects.AspectManager#getConfModel(org.eclipse
	 * .emf.ecore.EObject, org.eclipse.jwt.we.conf.resource.ConfModelStatus)
	 */
	public ConfModel getConfModel(EObject modelElement, ConfModelStatus cmStatus)
	{
		// allow conf model only for workflow files
		if (!modelElement.eResource().getURI().lastSegment().endsWith(".workflow"))
			return null;

		if (modelElement == null)
		{
			// guard
			return null;
		}

		EObject rootEObject = EMFUtils.getRootEObject(modelElement);
		if (rootEObject instanceof ConfModel)
		{
			return (ConfModel) rootEObject;
		}
		// loading without creating it if it doesn't exist
		cmStatus.setCreateIfDoesntExist(true);
		return ConfModelResourceManager.INSTANCE.loadConfModelOfModel(rootEObject,
				cmStatus);
	}


	/**
	 * 
	 * @param modelElement
	 * @return true if it is a ConfModel element (ConfModel, Profile, Aspect...)
	 *         and not an element of the aspected model.
	 */
	public boolean isConfModelElement(EObject modelElement)
	{
		EObject rootEObject = EMFUtils.getRootEObject(modelElement);
		return (rootEObject instanceof ConfModel);
	}


	/**
	 * Returns all aspects configured for the given model element, according to
	 * the merged conf model. Cached impl.
	 * 
	 * @param modelElement
	 * @return
	 */
	public List<Aspect> getAspects(EObject modelElement)
	{
		if (modelElement == null)
		{
			return null;
		}
		return getAspectsInternal(modelElement);

		// TODO activate once obsolescence notification
		/*
		 * synchronized(eclassToAspectsMapCache) { List<Aspect> cachedAspects =
		 * eclassToAspectsMapCache.get(modelElement.eClass()); if (cachedAspects
		 * == null) { cachedAspects = getAspectsInternal(modelElement);
		 * eclassToAspectsMapCache.put(modelElement.eClass(), cachedAspects); }
		 * return cachedAspects; }
		 */
	}


	/**
	 * Internal, cached impl TODO. Finds and returns all aspects in the merged
	 * conf model that target the given model element.
	 * 
	 * @param modelElement
	 *            not null
	 * @return a List of Aspect that are relevent for modelElement
	 */
	protected List<Aspect> getAspectsInternal(EObject modelElement)
	{
		// TODO advanced optimization : compute / cache transient EClass to
		// Aspects map
		List<Profile> activatedProfiles = getActivatedProfiles(modelElement);
		return getAspectsInternal(modelElement, activatedProfiles);
	}


	/**
	 * Internal, cached impl TODO. Finds and returns all aspects in the merged
	 * conf model that target the given model element.
	 * 
	 * @param modelElement
	 *            not null
	 * @return a List of Aspect that are relevent for modelElement
	 */
	protected List<Aspect> getAspectsInternal(EObject modelElement, List<Profile> profiles)
	{
		// TODO advanced optimization : compute / cache transient EClass to
		// Aspects map
		EClass eclass = modelElement.eClass();
		List<Aspect> foundAspects = null;
		for (Profile profile : profiles)
		{
			for (Aspect aspect : profile.getAspects())
			{
				for (EClass targetModelElementEClass : aspect.getTargetModelElements())
				{
					if (eclass.isSuperTypeOf(targetModelElementEClass))
					{
						if (foundAspects == null)
						{
							foundAspects = new ArrayList<Aspect>(2);
						}
						foundAspects.add(aspect);
					}
				}
			}
		}
		if (foundAspects == null)
		{
			foundAspects = new ArrayList<Aspect>(0);
		}
		return foundAspects;
	}


	/**
	 * Returns the profiles that are activated for the model of the given
	 * element.
	 * 
	 * @param model
	 *            if null, returns all installed profiles
	 * @return installed profiles referenced by the modelElement's model's
	 *         embedded conf's profiles
	 */
	public List<Profile> getActivatedProfiles(EObject modelElement)
	{
		if (modelElement == null)
		{
			return null;
		}
		return this.getActivatedProfilesInternal(modelElement);

		// TODO activate once obsolescence notification, use soft reference to
		// model to let it gc
		/*
		 * synchronized(modelToActivatedProfilesCache) { List<Profile>
		 * cachedProfiles = modelToActivatedProfilesCache.get(model); if
		 * (cachedProfiles == null) { cachedProfiles =
		 * getActivatedProfilesInternal(model);
		 * modelToActivatedProfilesCache.put(model, cachedProfiles); } return
		 * cachedProfiles; }
		 */
	}


	/**
	 * Returns the profiles that are activated for the given model. Actually
	 * returns the installed confs' profiles corresponding to the profiles
	 * listed in the given model's embedded conf, except if
	 * useEmbeddedRatherThanInstalledConf is configured.
	 * 
	 * @param modelElement
	 *            not null
	 * @return installed profiles referenced by the modelElement's model's
	 *         embedded conf's profiles ; not to be modified
	 */
	private List<Profile> getActivatedProfilesInternal(EObject modelElement)
	{
		ConfModel modelConfModel = this.getConfModel(modelElement);
		if (modelConfModel == null)
		{
			// this is normal in the case of an old model, which never had
			// profiles.
			logger.debug("No embedded conf model found for model element " //$NON-NLS-1$
					+ modelElement + " of model " + getModelRoot(modelElement)); //$NON-NLS-1$
			// default behaviour for old models is no profile activated
			// TODO could be made configurable in a specific file
			return EMPTY_PROFILE_LIST;
			// NB. alternative behaviour of assuming that all installed profiles
			// are activated
			// would be asimple good behaviour in the case of old workflow
			// models to be migrated
			// but here we're not concerned with migration.
			// return this.getInstalledProfiles();
		}

		if (modelConfModel.isCachedNoMissingProfile() // computation
														// optimization if all
														// profiles are
														// activated
				|| modelConfModel.getUseEmbeddedConf())
		{ // use of embedded ones is enabled
			// ... then return embedded ones
			return modelConfModel.getProfiles();
		}

		// TODO optimization : compute / cache transient activatedProfiles
		ArrayList<Profile> activatedProfiles = new ArrayList<Profile>();
		for (Profile profileRef : modelConfModel.getProfiles())
		{
			String profileId = profileRef.getName();
			Profile profile = ConfRegistry.INSTANCE.getInstalledProfile(profileId);
			if (profile == null)
			{
				// if no profile found, use embedded one TODO make it confble,
				// importable
				logger.warning("In model " + getModelRoot(modelElement) + " of model element " //$NON-NLS-1$  //$NON-NLS-2$
						+ modelElement
						+ ", no installed profile found for embedded profile reference with id " //$NON-NLS-1$
						+ profileId
						+ ", using definition emebdded within workflow model."); //$NON-NLS-1$
				// profile = profileRef;
				continue; // TODO add useEmbeddedIfNotInstalled to ConfModel
			}
			activatedProfiles.add(profile);
		}

		if (activatedProfiles.size() == modelConfModel.getProfiles().size())
		{
			// activating computation optimization
			modelConfModel.setCachedNoMissingProfile(true);
		}

		return activatedProfiles;
	}


	/**
	 * Activates the given installed profile on the given model, including
	 * updating the model according to aspect behaviour. If the given model has
	 * a conf without the given profile, copies and adds the given profile to
	 * the given model's conf, then triggers "onCreated" aspect behaviour on all
	 * impacted elements. of the model.
	 * 
	 * @param modelRoot
	 * @param profile
	 */
	public void activateProfile(EObject modelElement, Profile installedProfile)
	{
		activateProfile(modelElement, installedProfile, true);
	}


	/**
	 * Activates the given installed profile on the given model, including
	 * updating the model according to aspect behaviour. If the given model has
	 * a conf without the given profile, copies and adds the given profile to
	 * the given model's conf, then if executeBehaviours is true, triggers
	 * "onCreated" aspect behaviour on all impacted elements of the model.
	 * 
	 * @param modelRoot
	 * @param profile
	 * @param executeBehaviours
	 */
	public void activateProfile(EObject modelElement, Profile profile,
			boolean executeBehaviours)
	{
		ConfModel modelConfModel = getConfModel(modelElement);
		if (modelConfModel == null)
		{
			return;
		}

		// check it is not already activated TODO better in Aspects or EMF code
		if (getProfile(modelConfModel, profile.getName()) != null)
		{
			return;
		}

		// adding a copy to embedded profiles
		Profile importedProfile = (Profile) EcoreUtil.copy(profile);
		modelConfModel.getProfiles().add(importedProfile);

		if (!executeBehaviours)
		{
			return;
		}

		// add behaviour impact : play onCreated events
		List<Profile> importedProfiles = new ArrayList<Profile>(1);
		importedProfiles.add(importedProfile);
		EObject modelRoot = modelConfModel.getEnrichedModel();
		for (TreeIterator<EObject> eObjectIt = modelRoot.eAllContents(); eObjectIt
				.hasNext();)
		{
			EObject currentModelElement = eObjectIt.next();

			List<Aspect> currentModelElementAspects = this.getAspectsInternal(
					currentModelElement, importedProfiles);

			ConfPlugin.getDefaultAspectEventManager().onCreatedInternal(
					currentModelElement, currentModelElementAspects);
		}
	}


	/**
	 * Removes the given profile from the given model's conf if any, and removes
	 * the corresponding aspect instances from the model
	 * 
	 * @param modelElement
	 * @param installedProfile
	 */
	public void disableProfile(EObject modelElement, Profile installedProfile)
	{
		disableProfile(modelElement, installedProfile, true);
	}


	/**
	 * Removes the given profile from the given model's conf if any, and if
	 * removeAspectInstances is true, removes the corresponding aspect instances
	 * from the model
	 * 
	 * @param modelElement
	 * @param installedProfile
	 * @param removeAspectInstances
	 */
	public void disableProfile(EObject modelElement, Profile profile,
			boolean removeAspectInstances)
	{
		ConfModel modelConfModel = getConfModel(modelElement);
		if (modelConfModel == null)
		{
			return;
		}

		// check it is not already disabled TODO better in Aspects or EMF code
		Profile existingProfile = getProfile(modelConfModel, profile.getName());
		if (existingProfile == null)
		{
			return;
		}

		if (removeAspectInstances)
		{
			// removing corresponding aspect instances from the model, by
			// iterating on all aspect instances
			String disabledProfileName = existingProfile.getName();
			List<AspectInstance> toBeRemovedAspectInstances = new ArrayList<AspectInstance>();
			for (AspectInstance currentAspectInstance : modelConfModel
					.getAspectInstances())
			{
				Aspect currentAspect = this.getAspect(currentAspectInstance);

				Profile currentProfile = (Profile) currentAspect.eContainer();
				if (currentProfile != null
						&& currentProfile.getName().equals(disabledProfileName))
				{
					// this is an aspect instance of the disabled profile, let's
					// schedule it for removal
					toBeRemovedAspectInstances.add(currentAspectInstance);
				}
			}
			// let's do the actual remove :
			for (AspectInstance toBeRemovedAspectInstance : toBeRemovedAspectInstances)
			{
				modelConfModel.getAspectInstances().remove(toBeRemovedAspectInstance);
			}
		}

		// removing disabled profile from embedded profiles
		modelConfModel.getProfiles().remove(existingProfile);
	}


	/**
	 * Returns whether the given profile is already in the conf
	 * 
	 * @param modelConfModel
	 * @param profileName
	 * @return
	 */
	public Profile getProfile(ConfModel modelConfModel, String profileName)
	{
		for (Profile embeddedProfile : modelConfModel.getProfiles())
		{
			if (profileName.equals(embeddedProfile.getName()))
			{
				return embeddedProfile;
			}
		}
		return null;
	}


	/**
	 * Returns whether this model's conf & Aspects features should be based on
	 * his embedded conf (useful when designing aspects) rather than on JWT's
	 * installed conf (normal behaviour). TODO make it confble in model and why
	 * not globally
	 * 
	 * @param model
	 * @return
	 */
	public boolean useEmbeddedConf(EObject modelElement)
	{ // useEmbeddedRatherThanInstalledConf
		ConfModel modelConfModel = getConfModel(modelElement);
		if (modelConfModel == null)
		{
			return false;
		}
		return modelConfModel.getUseEmbeddedConf();
	}


	/**
	 * Finds and returns the corresponding Aspect
	 * 
	 * @param aspectInstance
	 *            used for knowing activated conf and aspectId
	 * @return the Aspect definition that has the given id
	 */
	public Aspect getAspect(AspectInstance aspectInstance)
	{
		EObject aspectInstanceContainer = aspectInstance.eContainer();
		if (aspectInstanceContainer == null)
		{
			// happens ex. when aspectInstance has just been removed (delete
			// command)
			return null;
		}
		return this.getAspect(aspectInstanceContainer, aspectInstance.getId());
	}


	/**
	 * Finds and returns the corresponding Aspect TODO still used ??
	 * 
	 * @param modelElement
	 *            required in order to know the activated profiles
	 * @param aspectId
	 * @return the Aspect definition that has the given id
	 * @obsolete
	 */
	public Aspect getAspect(EObject modelElement, String aspectId)
	{
		ConfModel modelConfModel = getConfModel(modelElement);
		if (modelConfModel.isCachedNoMissingProfile() // computation
														// optimization if all
														// profiles are
														// activated
				|| !modelConfModel.getUseEmbeddedConf())
		{ // use of embedded ones is disabled
			// ... then installed aspects cache provides the right answer
			return ConfRegistry.INSTANCE.getInstalledAspect(aspectId);
		}

		// else look for the aspect in this model's activated profiles
		List<Profile> activatedProfiles = getActivatedProfiles(modelElement);
		if (activatedProfiles == null)
		{
			logger.debug("No model found for " + modelElement); // TODO better //$NON-NLS-1$
			return null;
		}

		// TODO optimization : get aspect from transient activatedAspects by key
		// if possible
		for (Profile profile : activatedProfiles)
		{
			for (Aspect aspect : profile.getAspects())
			{
				if (aspect.getId().equals(aspectId))
				{
					return aspect;
				}
			}
		}
		return null;
	}


	/**
	 * ? copy required when long winded manipulations since comes from a loaded
	 * model TODO versions
	 * 
	 * @param model
	 * @return
	 */
	public List<Profile> getMissingInstalledProfiles(EObject modelElement)
	{
		ConfModel modelConfModel = getConfModel(modelElement);
		if (modelConfModel == null)
		{
			// this is the case of a model without extension, i.e. which
			// requires no profile.
			logger.debug("No embedded conf model found for model " + getModelRoot(modelElement) //$NON-NLS-1$
					+ " of model element " + modelElement); //$NON-NLS-1$
			return EMPTY_PROFILE_LIST;
			// NB. in order to ease migration, we could also assume all
			// installed profiles
			// (simple good behaviour in the case of old workflow models to be
			// migrated)
			// TODO in migration feature :
			// return this.getInstalledProfiles();
		}

		// NB. optimization : could be compute / cache transient missing
		// profiles, but not much useful
		ArrayList<Profile> missingProfiles = new ArrayList<Profile>();
		for (Profile profileRef : modelConfModel.getProfiles())
		{
			String profileId = profileRef.getName();
			Profile profile = ConfRegistry.INSTANCE.getInstalledProfile(profileId);
			if (profile == null)
			{
				missingProfiles.add(profileRef);
			}
		}
		return missingProfiles;
	}


	/**
	 * Finds and returns the corresponding Aspect
	 * 
	 * @param aspectInstance
	 *            used for knowing activated conf and aspectId
	 * @return the Aspect definition that has the given id
	 */
	public List<AspectInstance> getAspectInstances(EObject modelElement)
	{
		// TODO optimization : get aspectInstance by modelElement key if
		// possible
		ConfModel modelConfModel = getConfModel(modelElement);
		if (modelConfModel == null)
		{
			// this is the case of a model without extension, i.e. which
			// requires no profile.
			logger.debug("No embedded conf model found for model " + getModelRoot(modelElement) //$NON-NLS-1$
					+ " of model element " + modelElement); //$NON-NLS-1$
			return new ArrayList<AspectInstance>(0); // TODO
														// EMPTY_ASPECT_INSTANCE_LIST
			// NB. in order to ease migration, we could also assume all
			// installed profiles
			// (simple good behaviour in the case of old workflow models to be
			// migrated)
			// TODO in migration feature :
			// return this.getInstalledProfiles();
		}
		// TODO get rather only from activated profiles ?
		ArrayList<AspectInstance> aspectInstances = new ArrayList<AspectInstance>();
		for (AspectInstance aspectInstance : modelConfModel.getAspectInstances())
		{
			if (modelElement.equals(aspectInstance.getTargetModelElement()))
			{
				aspectInstances.add(aspectInstance);
			}
		}
		return aspectInstances;
	}


	/**
	 * Returns the corresponding aspect instance of the given model element if
	 * any
	 * 
	 * @param modelElement
	 * @param aspect
	 */
	public AspectInstance getAspectInstance(EObject modelElement, Aspect aspect)
	{
		// TODO optimization : get aspectInstance by modelElement key if
		// possible, else by aspect
		ConfModel modelConfModel = getConfModel(modelElement);
		// TODO get rather only from activated profiles ?
		for (AspectInstance aspectInstance : modelConfModel.getAspectInstances())
		{
			if (modelElement.equals(aspectInstance.getTargetModelElement())
					&& aspectInstance.getId().equals(aspect.getId()))
			{
				return aspectInstance;
			}
		}
		return null;
	}


	/**
	 * Creates a new instance of the given aspect on the given model element if
	 * any
	 * 
	 * @param modelElement
	 * @param aspect
	 */
	public AspectInstance createAndAddAspectInstance(Aspect aspect, EObject modelElement)
	{
		AspectInstance newAspectInstance = AspectFactory.INSTANCE.createAspectInstance(
				aspect, modelElement);
		if (newAspectInstance == null)
		{
			logger.warning("Error creating aspects of element " + modelElement //$NON-NLS-1$
					+ ": error creating aspect " + aspect.getId()); //$NON-NLS-1$
			return null;
		}

		this.addAspectInstance(newAspectInstance, modelElement);
		return newAspectInstance;
	}


	/**
	 * Adds the given new aspect instance to the given model element. Used by
	 * createAndAdd.
	 * 
	 * @param newAspectInstance
	 * @param modelElement
	 */
	protected void addAspectInstance(AspectInstance newAspectInstance,
			EObject modelElement)
	{
		this.getConfModel(modelElement).getAspectInstances().add(newAspectInstance);
	}

}
