/*******************************************************************************
 * 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 com.google.inject.Guice.createInjector;
import static org.eclipse.core.runtime.ContributorFactoryOSGi.resolve;
import static org.eclipse.core.runtime.RegistryFactory.getRegistry;
import static org.eclipse.sisu.peaberry.eclipse.EclipseRegistry.eclipseRegistry;
import static org.eclipse.sisu.peaberry.Peaberry.osgiModule;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IContributor;
import org.eclipse.core.runtime.IExecutableExtension;
import org.eclipse.core.runtime.IExecutableExtensionFactory;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.InvalidRegistryObjectException;
import org.eclipse.core.runtime.Status;

import com.google.inject.Injector;
import com.google.inject.Module;

/**
 * {@link IExecutableExtensionFactory} that creates an injected extension based
 * on the class named in the adapter data section. If the data section is empty
 * the class is assumed to be in the {@code id} attribute. The factory searches
 * the {@code org.eclipse.sisu.peaberry.eclipse.modules} extension for {@link Module}s
 * belonging to the same plug-in and uses them to create a new {@link Injector}.
 * This injector creates the injected extension and is then cached so it can be
 * re-used for other extensions in the same plug-in.
 * <p>
 * To use the factory put it in front of your class name in the extension XML.
 * Or replace your class with the factory and put your class in the {@code id}
 * attribute instead. Because the implementation will be injected based on the
 * bindings you could even replace your class name with one of its interfaces,
 * and that interface will then be used to lookup the correct implementation.
 * <p>
 * Here's a more detailed walkthrough, based on the example RCP Mail Template:
 * 
 * <pre> {@literal <}extension point="org.eclipse.ui.views"{@literal >}
 *   {@literal <}view name="Message"
 *         allowMultiple="true"
 *         icon="icons/sample2.gif"
 *         class="example.ViewImpl"
 *         id="example.view" /{@literal >}
 * {@literal <}/extension{@literal >}</pre>
 * becomes:
 * 
 * <pre> {@literal <}extension point="org.eclipse.ui.views"{@literal >}
 *   {@literal <}view name="Message"
 *         allowMultiple="true"
 *         icon="icons/sample2.gif"
 *         class="org.eclipse.sisu.peaberry.eclipse.GuiceExtensionFactory:example.ViewImpl"
 *         id="example.view" /{@literal >}
 * {@literal <}/extension{@literal >}
 * {@literal <}extension point="org.eclipse.sisu.peaberry.eclipse.modules"{@literal >}
 *   {@literal <}module class="example.ViewModule" /{@literal >}
 * {@literal <}/extension{@literal >}</pre>
 * Here's the same example with the class in the {@code id} attribute:
 * 
 * <pre> {@literal <}extension point="org.eclipse.ui.views"{@literal >}
 *   {@literal <}view name="Message"
 *         allowMultiple="true"
 *         icon="icons/sample2.gif"
 *         class="org.eclipse.sisu.peaberry.eclipse.GuiceExtensionFactory"
 *         id="example.ViewImpl" /{@literal >}
 * {@literal <}/extension{@literal >}
 * {@literal <}extension point="org.eclipse.sisu.peaberry.eclipse.modules"{@literal >}
 *   {@literal <}module class="example.ViewModule" /{@literal >}
 * {@literal <}/extension{@literal >}</pre>
 * and again, this time using an interface instead of the implementation:
 * 
 * <pre> {@literal <}extension point="org.eclipse.ui.views"{@literal >}
 *   {@literal <}view name="Message"
 *         allowMultiple="true"
 *         icon="icons/sample2.gif"
 *         class="org.eclipse.sisu.peaberry.eclipse.GuiceExtensionFactory:org.eclipse.ui.IViewPart"
 *         id="example.view" /{@literal >}
 * {@literal <}/extension{@literal >}
 * {@literal <}extension point="org.eclipse.sisu.peaberry.eclipse.modules"{@literal >}
 *   {@literal <}module class="example.ViewModule" /{@literal >}
 * {@literal <}/extension{@literal >}</pre>
 * 
 * @author mcculls@gmail.com (Stuart McCulloch)
 */
public final class GuiceExtensionFactory
    implements IExecutableExtension, IExecutableExtensionFactory {

  public static final String POINT_ID = "org.eclipse.sisu.peaberry.eclipse.modules";

  private static final Logger LOGGER = Logger.getLogger(GuiceExtensionFactory.class.getName());

  // re-use injectors for extensions with the same contributor
  private static final Map<IContributor, Injector> INJECTORS =
      new HashMap<IContributor, Injector>();

  private IConfigurationElement configuration;
  private IContributor contributor;
  private String clazzName;

  public void setInitializationData(final IConfigurationElement config, final String name,
      final Object data) {

    configuration = config;

    // find contributor while still valid
    contributor = config.getContributor();

    // if there's no (string-based) adapter data then class must be under "id"
    clazzName = data instanceof String ? (String) data : config.getAttribute("id");
  }

  public Object create()
      throws CoreException {

    if (null == clazzName) {
      throw newCoreException("Configuration is missing class information");
    }

    final Class<?> clazz;
    try {
      clazz = resolve(contributor).loadClass(clazzName);
    } catch (final InvalidRegistryObjectException e) {
      throw newCoreException(e);
    } catch (final ClassNotFoundException e) {
      throw newCoreException(e);
    }

    final Object o = getInjector().getInstance(clazz);
    if (o instanceof IExecutableExtension) {
      ((IExecutableExtension) o).setInitializationData(configuration, null, null);
    }
    return o;
  }

  /**
   * Remove any {@link Injector}s belonging to uninstalled bundles.
   */
  public static void cleanup() {
    synchronized (INJECTORS) { // LOCK
      for (final IContributor c : INJECTORS.keySet()) {
        if (null == resolve(c)) {
          INJECTORS.remove(c);
        }
      }
    } // UNLOCK
  }

  private Injector getInjector()
      throws CoreException {

    synchronized (INJECTORS) { // LOCK
      Injector injector = INJECTORS.get(contributor);
      if (null == injector) {

        final List<Module> modules = new ArrayList<Module>();

        // first add the OSGi service registry and Eclipse extension bindings
        modules.add(osgiModule(resolve(contributor).getBundleContext(), eclipseRegistry()));

        // now add any module extensions contributed by the current plug-in
        for (final IConfigurationElement e : getRegistry().getConfigurationElementsFor(POINT_ID)) {
          if (contributor.equals(e.getContributor())) {
            modules.add((Module) e.createExecutableExtension("class"));
          }
        }

        injector = createInjector(modules);
        INJECTORS.put(contributor, injector);
      }
      return injector;
    } // UNLOCK
  }

  private CoreException newCoreException(final Throwable e) {
    LOGGER.warning(e.getMessage());
    return new CoreException(new Status(IStatus.ERROR, contributor.getName(), e.getMessage(), e));
  }

  private CoreException newCoreException(final String message) {
    LOGGER.warning(message);
    return new CoreException(new Status(IStatus.ERROR, contributor.getName(), message));
  }
}
