/*******************************************************************************
 * Copyright (c) 2008, 2014 Stuart McCulloch
 * 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:
 *    Stuart McCulloch - initial API and implementation
 *******************************************************************************/

package org.eclipse.sisu.peaberry.eclipse;

import static java.lang.reflect.Proxy.newProxyInstance;

import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method;

import org.eclipse.core.runtime.ContributorFactoryOSGi;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.sisu.peaberry.ServiceException;
import org.osgi.framework.Bundle;

/**
 * A factory that attempts to create bean instances from Eclipse Extensions.
 * 
 * @author mcculls@gmail.com (Stuart McCulloch)
 */
final class ExtensionBeanFactory {

  private static final String CONTENT_KEY = "text()";

  // instances not allowed
  private ExtensionBeanFactory() {}

  static Object newInstance(final Class<?> clazz, final IConfigurationElement config) {

    // client just wants the actual configuration object
    if (Object.class == clazz || IConfigurationElement.class == clazz) {
      return config;
    }

    try {
      // first try to instantiate direct class
      return newExtensionImpl(clazz, config);
    } catch (final RuntimeException re) {/* fall back to proxy */} // NOPMD

    // create proxy based on the supplied interface
    final ClassLoader loader = clazz.getClassLoader();
    final Class<?>[] api = new Class[]{clazz};

    return newProxyInstance(loader, api, new ExtensionBeanHandler(config));
  }

  static Object newExtensionImpl(final Class<?> clazz, final IConfigurationElement config) {

    // assume name is kept under "class" (unless mapped)
    final String clazzKey = mapName(clazz, "class");
    final String clazzName = mapContent(config, clazzKey);

    // make sure implementation is compatible with the required interface
    if (!clazz.isAssignableFrom(loadExtensionClass(config, clazzName))) {
      throw new ClassCastException(clazz + " is not assignable from: " + clazzName);
    }

    try {
      return config.createExecutableExtension(clazzKey);
    } catch (final CoreException e) {
      throw new ServiceException(e);
    }
  }

  static String mapName(final AnnotatedElement type, final String name) {
    if (null != findAnnotation(type, "MapContent")) {
      return CONTENT_KEY;
    }
    final String mapName = getAnnotationValue(findAnnotation(type, "MapName"), String.class);
    return null == mapName || mapName.length() == 0 ? name : mapName;
  }

  static String mapContent(final IConfigurationElement config, final String elementKey) {
    return CONTENT_KEY.equals(elementKey) ? config.getValue() : config.getAttribute(elementKey);
  }

  static Annotation findAnnotation(final AnnotatedElement element, final String simpleName) {
    for (final Annotation a : element.getAnnotations()) {
      if (simpleName.equals(a.annotationType().getSimpleName())) {
        return a;
      }
    }
    return null;
  }

  @SuppressWarnings("unchecked")
  static <T> T getAnnotationValue(final Annotation element, final Class<? extends T> clazz) {

    try {
      final Method m = element.annotationType().getDeclaredMethod("value");
      if (clazz.isAssignableFrom(m.getReturnType())) {
        return (T) m.invoke(element);
      }
    } catch (final Exception e) {/* no such setting */} // NOPMD

    return null;
  }

  static Class<?> loadExtensionClass(final IConfigurationElement config, final String clazzName) {
    final Bundle bundle = ContributorFactoryOSGi.resolve(config.getContributor());

    String value = clazzName;
    int n = value.indexOf(':');

    // unravel the factory indirection to get the real class
    if (value.startsWith(GuiceExtensionFactory.class.getName())) {
      value = n < 0 ? config.getAttribute("id") : value.substring(n + 1);
      n = value.indexOf(':');
    }

    try {
      return bundle.loadClass(n < 0 ? value : value.substring(0, n));
    } catch (final ClassNotFoundException e) {
      throw new ServiceException(e);
    }
  }
}
