package code;

import java.io.*;
import java.util.*;
import java.util.Map.Entry;

import org.eclipse.emf.common.util.*;
import org.eclipse.emf.ecore.*;
import org.eclipse.emf.ecore.resource.*;
import org.eclipse.emf.ecore.resource.impl.ResourceSetImpl;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.eclipse.emf.ecore.xmi.impl.XMIResourceFactoryImpl;

/**
 * Utilitary class for EMF models.
 * This class gathers usefull functions to load, save and modify easily
 * EMF models and metamodels.
 * @author M. Kerboeuf
 */
public class LoadSaveModel {

	// PRIVATE **********************************************************************************

	/**
	 * Private class dedicated to the utilitary function changeMetamodel.
	 * This class maps two sets of instances. The first one corresponds to the instances
	 * of a source model. The second one correponds to the instances of a copy of
	 * the source model conforming to another metamodel.
	 * @author M. Kerboeuf
	 */
	private static class EObjectMap {
		private Map<EObject, EObject> map;
		private Map<EObject, EObject> initialmap;
		private EObject  modelRootObject;
		private EPackage oldMetamodelRootPackage;
		private EPackage newMetamodelRootPackage;

		/**
		 * Provides the package matching with a package path corresponding to a given package list.
		 * @param path The package path.
		 * @param ePackage The parent package from which the search starts.
		 * @return The package at the end of the path if it exists or null.
		 */
		private static EPackage getEndOfEPackagePath(List<EPackage> path, EPackage ePackage) {
			for (int i=1; i<path.size(); i++) {
				ePackage = getSubEPackage(ePackage, path.get(i).getName());
				if (ePackage==null) return null;
			}
			return ePackage;
		}

		/**
		 * Creates the list of the nested packages containing a given package.
		 * If this list contains a given first package, then this
		 * package is the first of the resulting list.
		 * @param p A package to scan.
		 * @param startPackage The start package from which the list is supposed to begin.
		 * @return The list of nested packages including p.
		 */
		private static List<EPackage> getEPackagePath(EPackage p, EPackage startPackage) {
			List<EPackage> path = new ArrayList<EPackage>();
			while (p!=null) {
				path.add(0, p);
				if (p.equals(startPackage)) break;
				p = p.getESuperPackage();
			}
			return path;
		}

		/**
		 * Provides a subpackage identified by a given name.
		 * @param p The parent package.
		 * @param ePackageName The name of the subpackage.
		 * @return The subpackage of p with the given name if it exists or null.
		 */
		private static EPackage getSubEPackage(EPackage p, String ePackageName) {
			for(EPackage subPackage : p.getESubpackages())
				if (subPackage.getName().equals(ePackageName)) return subPackage;
			return null;
		}

		/**
		 * Creates a map between the provided source instances and copies of
		 * this instances in the new metamodel.
		 * @param modelRootObject root instance of the source model.
		 * @param newMetamodelRootPackage root package of the new metamodel.
		 */
		public EObjectMap(EObject modelRootObject, EPackage newMetamodelRootPackage) {
			this.modelRootObject = modelRootObject;
			this.oldMetamodelRootPackage = modelRootObject.eClass().getEPackage();
			this.newMetamodelRootPackage = newMetamodelRootPackage;
			this.map = new HashMap<EObject, EObject>();
			for (EObject eo : getAllInstances(modelRootObject)){
				this.map.put(eo, this.createObjectCopy(eo));
			}
		}

		public EObjectMap(EObject modelRootObject, EPackage newMetamodelRootPackage, EObject modelRootObjectCopy) {
			this.modelRootObject = modelRootObject;
			this.oldMetamodelRootPackage = modelRootObject.eClass().getEPackage();
			this.newMetamodelRootPackage = newMetamodelRootPackage;
			this.map = new HashMap<EObject, EObject>();
			this.initialmap = new HashMap<EObject, EObject>();
			for (EObject eo : getAllInstances(modelRootObject)){
				this.map.put(eo, this.createObjectCopy(eo));
			}
			for (EObject eo : getAllInstances(modelRootObjectCopy)){
				this.initialmap.put(eo, this.createObjectCopy(eo));
			}
		}

		/**
		 * Creates a copy in the new metamodel of an instance conforming to the class
		 * of the source metamodel.
		 * @param source Object to copy.
		 * @return copy of the source object.
		 */
		private EObject createObjectCopy(EObject source) {
			EClass sourceClassType = source.eClass();
			List<EPackage> sourceClassTypePackagePath = getEPackagePath(sourceClassType.getEPackage(), this.oldMetamodelRootPackage);
			if (!sourceClassTypePackagePath.get(0).equals(this.oldMetamodelRootPackage))
				// The type of the object is not defined within the new package, so we try to create a new instance from this object's class:
				return EcoreUtil.create(sourceClassType);
			EPackage targetPackage = getEndOfEPackagePath(sourceClassTypePackagePath, this.newMetamodelRootPackage);
			if (targetPackage==null) throw new IllegalArgumentException("The target EPackage matching with the source EPackage "+sourceClassType.getEPackage()+" is not found.");
			EClassifier copyClass = targetPackage.getEClassifier(sourceClassType.getName());
			if (copyClass==null){	
				throw new IllegalArgumentException("The target EPackage "+targetPackage.getName()+" does not contain the required class "+sourceClassType.getName());
			}
			if (!(copyClass instanceof EClass)) throw new IllegalArgumentException("The target EPackage "+targetPackage.getName()+" contains a classifier "+sourceClassType.getName()+" which is not an EClass (and which sould be)");
			return EcoreUtil.create((EClass)copyClass);
		}

		public EPackage getContainer(EPackage targetPackage, EClass sourceClassType){
			EPackage subpack = null;
			for( EPackage subp : targetPackage.getESubpackages()){
				EClassifier copyClass2 = subp.getEClassifier(sourceClassType.getName());
				if(copyClass2!=null)
					subpack = subp;
				break;
			}

			return subpack;
		}

		/**
		 *  Creates a copy in the new metamodel of an instance conforming to the class
		 * of the source metamodel. 
		 * 
		 * @param source
		 * @param renameMap
		 * @return
		 */


		// Migration
		private EObject createObjectCopy(EObject source, Map<String, Map<String,Map<String,String>>> renameMap) {
			EClass sourceClassType = source.eClass();
			Map<String, Map<String,String>> value = renameMap.get(sourceClassType.getName());
			String key = null ;
			if(value != null){
				for (Entry<String, Map<String,String>> e: value.entrySet()) {
					key = e.getKey();
				}
			}
			List<EPackage> sourceClassTypePackagePath = getEPackagePath(sourceClassType.getEPackage(), this.oldMetamodelRootPackage);
			if (!sourceClassTypePackagePath.get(0).equals(this.oldMetamodelRootPackage))
				// The type of the object is not defined within the new package, so we try to create a new instance from this object's class:
				return EcoreUtil.create(sourceClassType);
			EPackage targetPackage = getEndOfEPackagePath(sourceClassTypePackagePath, this.newMetamodelRootPackage);
			if (targetPackage==null) throw new IllegalArgumentException("The target EPackage matching with the source EPackage "+sourceClassType.getEPackage()+" is not found.");
			EClassifier searched = null;
			if(key != null){
				for(EClassifier tp : targetPackage.getEClassifiers()){
					if(tp.getName().equals(key)){
						searched = tp;
						break;
					}	
				}
			}
			EClassifier copyClass = searched;
			if (copyClass==null) throw new IllegalArgumentException("The target EPackage "+targetPackage.getName()+" does not contain the required class "+sourceClassType.getName());
			if (!(copyClass instanceof EClass)) throw new IllegalArgumentException("The target EPackage "+targetPackage.getName()+" contains a classifier "+sourceClassType.getName()+" which is not an EClass (and which sould be)");
			return EcoreUtil.create((EClass)copyClass);
		}


		/**
		 * Provides the source model copy.
		 * @return the copy of the root instance of the source model.
		 */
		public EObject getRootCopy() {
			return this.map.get(this.modelRootObject);
		}

	}

	// PUBLIC ***********************************************************************************

	/**
	 * Creation of a default resource set associated to an EPackage NS_URI
	 * and an instance of this EPackage.
	 * @param nsuri NS_URI of the EPackage.
	 * @param metamodelRootPackage Instance of the EPackage.
	 * @return New resource set which package registry maps nsuri to metamodelRootPackage.
	 */
	public static ResourceSet createResourceSet (String nsuri, Object metamodelRootPackage) {
		ResourceSet resourceSet = new ResourceSetImpl();
		// Default XMI extension:
		resourceSet.getResourceFactoryRegistry().getExtensionToFactoryMap()
		.put(Resource.Factory.Registry.DEFAULT_EXTENSION, new XMIResourceFactoryImpl());
		// Register:
		resourceSet.getPackageRegistry().put(nsuri, metamodelRootPackage);
		return resourceSet;
	}

	/**
	 * Provides the set of external packages definig feature types.
	 * @param root The root package to scan.
	 * @return The set of packages that are not defined within or equal to root, and that
	 * define classifiers used as type of features.
	 */
	public static Set<EPackage> getAllExternalPackages(EPackage root) {
		Set<EPackage> allExternal = new HashSet<EPackage>(); // result: new empty set recursively extended by the subpackages:
		for (EPackage subPackage : root.getESubpackages()) allExternal.addAll(getAllExternalPackages(subPackage));
		// We scan now the direct content of the package:
		for (EClassifier ec : root.getEClassifiers())
			if (ec instanceof EClass)
				for (EStructuralFeature er : ((EClass)ec).getEAllStructuralFeatures()) {
					EPackage refPackage = er.getEType().getEPackage(); // package to check
					boolean inside = false;
					while  (!inside && refPackage!=null) {
						if (refPackage.equals(root)) inside=true;
						refPackage = refPackage.getESuperPackage();
					}
					if (!inside) allExternal.add(er.getEType().getEPackage());
				}		
		return allExternal;
	}

	/**
	 * Creation of a set of instances contained into a root instance, including this root instance.
	 * @param root Root instance.
	 * @return A new set containing all the content of root, including root.
	 */
	public static Set<EObject> getAllInstances(EObject root) {
		Set<EObject> allContent = new HashSet<EObject>();
		allContent.add(root);
		for(TreeIterator<EObject> it = root.eAllContents(); it.hasNext();) allContent.add(it.next());
		return allContent;
	}

	/**
	 * Metamodel (Ecore model) loading.
	 * @param metamodelFilePath File path of the metamodel to load.
	 * @return Root package of the loaded metamodel.
	 */
	public static EPackage loadMetamodel (String metamodelFilePath) {
		// Metamodel loading:				
		return loadMetamodel(URI.createFileURI(new File(metamodelFilePath).getAbsolutePath()));
	}

	/**
	 * Metamodel (Ecore model) loading.
	 * @param metamodelURI URI of the metamodel to load.
	 * @return Root package of the loaded metamodel.
	 */
	public static EPackage loadMetamodel (URI metamodelURI) {
		// Model loading:		
		return (EPackage)loadModel(metamodelURI, EcorePackage.eINSTANCE);
	}

	/**
	 * Model loading.
	 * @param inputFilePath File name of the model to load.
	 * @param metamodelRootPackage EPackage expected to be
	 * the container of the metaclass of the model root object.
	 * @return The EObject corresponding to the loaded model root object.
	 */
	public static EObject loadModel(String inputFilePath, EPackage metamodelRootPackage) {
		return loadModel(URI.createFileURI(new File(inputFilePath).getAbsolutePath()), metamodelRootPackage);
	}

	/**
	 * Model loading.
	 * @param modelInputFilePath File name of the model to load.
	 * @param metamodelInputFilePath File name of the corresponding metamodel.
	 * @return The EObject corresponding to the loaded model root object.
	 */
	public static EObject loadModel(String modelInputFilePath, String metamodelInputFilePath) {
		return loadModel(URI.createFileURI(new File(modelInputFilePath).getAbsolutePath()), metamodelInputFilePath);
	}

	/**
	 * Model loading.
	 * @param inputFilePath File name of the model to load.
	 * @param metamodelURI URI of the corresponding metamodel.
	 * @return The EObject corresponding to the loaded model root object.
	 */
	public static EObject loadModel(String inputFilePath, URI metamodelURI) {
		return loadModel(URI.createFileURI(new File(inputFilePath).getAbsolutePath()), metamodelURI);
	}

	/**
	 * Model loading.
	 * @param modeluri URI of the model to load.
	 * @param metamodelRootPackage EPackage expected to be
	 * the container of the metaclass of the model root object.
	 * @return The EObject corresponding to the loaded model root object.
	 */
	public static EObject loadModel (URI modeluri, EPackage metamodelRootPackage) {
		// Creation of a ResourceSet mapping the package ns uri to the package:
		ResourceSet resourceSet = createResourceSet(metamodelRootPackage.getNsURI(), metamodelRootPackage);
		// Creation of a resource linked to the model uri:
		Resource resource = resourceSet.getResource(modeluri, true);
		// First element of the model (root object):
		return resource.getContents().get(0);
	}

	/**
	 * Model loading.
	 * @param modeluri URI of the model to load.
	 * @param metamodelFilePath File name of the corresponding metamodel.
	 * @return The EObject corresponding to the loaded model root object.
	 */
	public static EObject loadModel (URI modeluri, String metamodelFilePath) {
		// Model loading:		
		return loadModel(modeluri, URI.createFileURI(new File(metamodelFilePath).getAbsolutePath()));
	}

	/**
	 * Model loading.
	 * @param modeluri URI of the model to load.
	 * @param metamodelURI URI of the corresponding metamodel.
	 * @return The EObject corresponding to the loaded model root object.
	 */
	public static EObject loadModel (URI modeluri, URI metamodelURI) {
		// Metamodel loading:
		EPackage metamodelRootPackage = loadMetamodel(metamodelURI);
		// Model loading:				
		return loadModel(modeluri, metamodelRootPackage);
	}

	/**
	 * Metamodel saving.
	 * The ressource and NsURI of the package are updated.
	 * The packages corresponding to external dependencies 
	 * are serialised locally together with the metamodel itself.
	 * @param metaModelRootObject Root object of the metamodel to be serialized.
	 * @param outputFilePath Name of the file to be created or updated.
	 * @throws IOException Thrown if the output file cannot be created.
	 * @return The updated root package.
	 */
	public static EPackage saveMetamodel(EPackage metaModelRootObject, String outputFilePath) throws IOException {
		return saveMetamodel(metaModelRootObject, outputFilePath, true);
	}

	/**
	 * Metamodel saving.
	 * The ressource and NsURI of the package are updated.
	 * The packages corresponding to external dependencies 
	 * can be serialised locally together with the metamodel itself.
	 * @param metaModelRootObject Root object of the metamodel to be serialized.
	 * @param outputFilePath Name of the file to be created or updated.
	 * @param withExternalPackageDependencies States if the external dependencies must but serialized or not.
	 * @throws IOException Thrown if the output file cannot be created.
	 * @return The updated root package.
	 */
	public static EPackage saveMetamodel(EPackage metaModelRootObject, String outputFilePath, boolean withExternalPackageDependencies) throws IOException {
		File metamodelFile = new File(outputFilePath);
		String metamodelFilename  = metamodelFile.getAbsolutePath();
		// New NsURI matching with the new file path:
		metaModelRootObject.setNsURI(URI.createFileURI(metamodelFilename).toString());
		// Request for the serialization of external dependencies:
		if (withExternalPackageDependencies) {
			Map<EPackage, Resource> resourcesBackup = new HashMap<EPackage, Resource>();
			for (EPackage ep : getAllExternalPackages(metaModelRootObject)) {
				String pname = ep.getName()+".ecore";
				pname =  Character.toUpperCase(pname.charAt(0)) + pname.substring(1);
				// backup of current resource before saving (and so update):
				Resource currentResource = ep.eResource();
				if (currentResource!=null) resourcesBackup.put(ep, currentResource);
				saveModel(ep, metamodelFile.getParentFile().getAbsolutePath()+"/"+pname);
			}
			// Metamodel serialization:
			saveModel(metaModelRootObject, metamodelFilename);
			// Restoring the initial resources:
			for (EPackage ep : resourcesBackup.keySet()) resourcesBackup.get(ep).getContents().add(ep);
		}
		else saveModel(metaModelRootObject, metamodelFilename);
		return metaModelRootObject;
	}

	/**
	 * Model saving.
	 * @param modelRootObject Root object to be serialized.
	 * @param outputFilePath Name of the file to be created or updated.
	 * @throws IOException Thrown if the output file cannot be created.
	 * @return The updated root object.
	 */
	public static EObject saveModel(EObject modelRootObject, String outputFilePath) throws IOException {
		// Creating the resource set from the metadata of modelRootObject:
		ResourceSet resourceSet = createResourceSet(modelRootObject.eClass().getEPackage().getNsURI(), modelRootObject.eClass().getEPackage());
		// Creating a new resource in this resource set:
		Resource resource = resourceSet.createResource(URI.createFileURI(new File(outputFilePath).getAbsolutePath()));
		// Adding the object in the new resource and saving:
		resource.getContents().add(modelRootObject);
		resource.save(Collections.EMPTY_MAP);
		return modelRootObject;
	}

}