/*******************************************************************************
 * Copyright (c) 2015 University of York.
 * 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:
 *    Antonio Garcia-Dominguez - initial API and implementation
 *******************************************************************************/
package org.hawk.emfresource.util;

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

import org.eclipse.emf.ecore.EClass;
import org.eclipse.emf.ecore.EFactory;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EPackage.Registry;
import org.eclipse.emf.ecore.EReference;
import org.eclipse.emf.ecore.EStructuralFeature;
import org.eclipse.emf.ecore.impl.DynamicEObjectImpl;

import net.sf.cglib.proxy.CallbackHelper;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.NoOp;

/**
 * Factory for instrumented {@link EObject}s with support for lazy loading.
 */
public class LazyEObjectFactory {

	private Registry packageRegistry;
	private MethodInterceptor methodInterceptor;

	/** Map from classes to factories of instances instrumented by CGLIB for lazy loading. */
	private Map<Class<?>, net.sf.cglib.proxy.Factory> factories = null;

	public LazyEObjectFactory(Registry registry, MethodInterceptor interceptor) {
		this.packageRegistry = registry;
		this.methodInterceptor = interceptor;
	}

	public EObject createInstance(final EClass eClass) {
		final EFactory factory = packageRegistry.getEFactory(eClass.getEPackage().getNsURI());
		final EObject obj = factory.create(eClass);
		return createLazyLoadingInstance(eClass, obj.getClass());
	}

	private EObject createLazyLoadingInstance(final EClass eClass, final Class<?> klass) {
		/*
		 * We need to create a proxy to intercept eGet calls for lazy loading,
		 * but we need to use a subclass of the *real* implementation, or we'll
		 * have all kinds of issues with static metamodels (e.g. not using
		 * DynamicEObjectImpl).
		 */
		if (factories == null) {
			factories = new HashMap<>();
		}

		final net.sf.cglib.proxy.Factory factory = factories.get(klass);
		EObject o;
		if (factory == null) {
			
			final Enhancer enh = new Enhancer();
			final CallbackHelper helper = new CallbackHelper(klass, new Class[0]) {
				@Override
				protected Object getCallback(final Method m) {
					if ("eGet".equals(m.getName())
							&& m.getParameterTypes().length > 0
							&& EStructuralFeature.class.isAssignableFrom(m.getParameterTypes()[0])) {
						return methodInterceptor;
					} else if ("eContents".equals(m.getName()) && m.getParameterTypes().length == 0) {
						return methodInterceptor;
					} else if ("eContainer".equals(m.getName()) && m.getParameterTypes().length == 0) {
						return methodInterceptor;
					} else if ("eContainmentFeature".equals(m.getName()) && m.getParameterTypes().length == 0) {
						return methodInterceptor;
					} else if ("eResource".equals(m.getName()) && m.getParameterTypes().length == 0) {
						return methodInterceptor;
					} else if ("eIsSet".equals(m.getName()) && m.getParameterTypes().length == 1 && EStructuralFeature.class.isAssignableFrom(m.getParameterTypes()[0])) {
						return methodInterceptor;
					} else if (m.getName().startsWith("get") && m.getParameterTypes().length == 0) {
						EReference eRef = guessEReferenceFromGetter(eClass, m.getName());
						if (eRef != null) {
							return methodInterceptor;
						} else {
							return NoOp.INSTANCE;
						}
					}
					else {
						return NoOp.INSTANCE;
					}
				}
			};
			enh.setSuperclass(klass);

			/*
			 * We need both classloaders: the classloader of the class to be
			 * enhanced, and the classloader of this plugin (which includes
			 * CGLIB). We want the CGLIB classes to always resolve to the same
			 * Class objects, so this plugin's classloader *has* to go first.
			 */
			enh.setClassLoader(new BridgeClassLoader(
				this.getClass().getClassLoader(),
				klass.getClassLoader()));

			/*
			 * The objects created by the Enhancer implicitly implement the
			 * CGLIB Factory interface as well. According to CGLIB, going
			 * through the Factory is faster than recreating or reusing the
			 * Enhancer.
			 */
			enh.setCallbackFilter(helper);
			enh.setCallbacks(helper.getCallbacks());
			o = (EObject)enh.create();
			factories.put(klass, (net.sf.cglib.proxy.Factory)o);
		} else {
			o = (EObject) factory.newInstance(factory.getCallbacks());
		}

		/*
		 * A newly created and instrumented DynamicEObjectImpl won't have the
		 * eClass set. We need to redo that here.
		 */
		if (o instanceof DynamicEObjectImpl) {
			((DynamicEObjectImpl)o).eSetClass(eClass);
		}
		return o;
	}

	public EReference guessEReferenceFromGetter(EClass eClass, String methodName) {
		assert methodName.startsWith("get") : "method name should start with get";

		String referenceName = methodName.substring("get".length());
		if (referenceName.length() == 0) {
			return null;
		}

		// For getFirstElement, try both "FirstElement" and "firstElement" 
		EStructuralFeature sf = eClass.getEStructuralFeature(referenceName);
		if (sf == null) {
			referenceName = Character.toLowerCase(referenceName.charAt(0)) + referenceName.substring(1);
			sf = eClass.getEStructuralFeature(referenceName);
		}
		if (sf instanceof EReference) {
			return (EReference)sf;
		} else {
			return null;
		}
	}
}

@@ -659,16 +659,58 @@
 il.Map;%0D
+%0Aimport java.util.NoSuchElementException;%0D
 %0A%0D%0Aimpor
@@ -1783,90 +1783,299 @@
 nal 
-EFactory factory = packageRegistry.getEFactory(eClass.getEPackage().getN
+String nsURI = eClass.getEPackage().getNsURI();%0D%0A%09%09final EFactory factory = packageRegistry.getEFactory(nsURI);%0D%0A%09%09if (factory == null) %7B%0D%0A%09%09%09throw new NoSuchElementException(String.format(%22Could not find the EFactory for nsURI '%25s' in the resource set's package registry%22, n
 sURI
-(
 ));%0D%0A
+%09%09%7D%0D%0A%0D%0A
 %09%09fi

improve error reporting when the EFactory for the EClass is not found