/*******************************************************************************
 * 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.internal;

import static java.security.AccessController.doPrivileged;
import static jsr166y.ConcurrentReferenceHashMap.ReferenceType.WEAK;
import static org.eclipse.sisu.peaberry.internal.ComputedMapFactory.computedMap;
import static org.eclipse.sisu.peaberry.internal.ImportGlue.generateProxy;
import static org.eclipse.sisu.peaberry.internal.ImportGlue.getClazzName;
import static org.eclipse.sisu.peaberry.internal.ImportGlue.getProxyName;

import java.lang.reflect.Constructor;
import java.security.PrivilegedAction;
import java.util.concurrent.ConcurrentMap;

import org.eclipse.sisu.peaberry.Import;
import org.eclipse.sisu.peaberry.ServiceException;
import org.eclipse.sisu.peaberry.ServiceUnavailableException;
import org.eclipse.sisu.peaberry.internal.ComputedMapFactory.Function;

/**
 * Custom classloader that provides optimized proxies for imported services.
 * 
 * @author mcculls@gmail.com (Stuart McCulloch)
 */
final class ImportProxyClassLoader
    extends ClassLoader {

  private static final String UNAVAILABLE_CLAZZ_NAME = ServiceUnavailableException.class.getName();
  private static final String IMPORT_CLAZZ_NAME = Import.class.getName();

  private static final Object NULL_CLASS_LOADER_KEY = new Object();

  @SuppressWarnings("unchecked")
  static <T> Constructor<T> getProxyConstructor(final Class<T> clazz) {
    try {
      // use a different custom classloader for each class-space, to avoid leaks
      return (Constructor<T>) getProxyClass(clazz).getConstructor(Import.class);
    } catch (final LinkageError e) {
      throw new ServiceException(e);
    } catch (final NoSuchMethodException e) {
      // /CLOVER:OFF
      throw new ServiceException(e);
    } catch (final ClassNotFoundException e) {
      throw new ServiceException(e);
      // /CLOVER:ON
    }
  }

  /**
   * @return unique proxy class per given type
   */
  static Class<?> getProxyClass(final Class<?> clazz)
      throws ClassNotFoundException {

    final Object key = getKeyFromClassLoader(clazz.getClassLoader());
    final String name = getProxyName(clazz.getName());

    return LOADER_MAP.get(key).loadClass(name);
  }

  /**
   * @return non-null key for the given class loader
   */
  static Object getKeyFromClassLoader(final ClassLoader classLoader) {
    if (null != classLoader) {
      return classLoader;
    }

    try {
      return doPrivileged(new PrivilegedAction<ClassLoader>() {
        public ClassLoader run() {
          return getSystemClassLoader();
        }
      });
    } catch (final SecurityException e) {
      return NULL_CLASS_LOADER_KEY; // unable to canonicalise!
    }
  }

  /**
   * @return class loader related to the given key
   */
  static ClassLoader getClassLoaderFromKey(final Object key) {
    return NULL_CLASS_LOADER_KEY != key ? (ClassLoader) key : null; // NOPMD
  }

  // weak map of classloaders, to allow eager collection of proxied classes
  private static final ConcurrentMap<Object, ClassLoader> LOADER_MAP =
      computedMap(WEAK, WEAK, 32, new Function<Object, ClassLoader>() {
        public ClassLoader compute(final Object parentKey) {
          return doPrivileged(new PrivilegedAction<ClassLoader>() {
            public ClassLoader run() {
              return new ImportProxyClassLoader(getClassLoaderFromKey(parentKey));
            }
          });
        }
      });

  // delegate to the original type's classloader
  ImportProxyClassLoader(final ClassLoader parent) {
    super(parent);
  }

  @Override
  protected Class<?> loadClass(final String name, final boolean resolve)
      throws ClassNotFoundException {

    // short-circuit access to these classes
    if (IMPORT_CLAZZ_NAME.equals(name)) {
      return Import.class;
    }
    if (UNAVAILABLE_CLAZZ_NAME.equals(name)) {
      return ServiceUnavailableException.class;
    }

    return super.loadClass(name, resolve);
  }

  @Override
  protected Class<?> findClass(final String clazzOrProxyName)
      throws ClassNotFoundException {

    final String clazzName = getClazzName(clazzOrProxyName);

    // is this a new proxy class request?
    if (!clazzName.equals(clazzOrProxyName)) {
      final byte[] code = generateProxy(loadClass(clazzName));
      return defineClass(clazzOrProxyName, code, 0, code.length);
    }

    // ignore any non-proxy requests
    throw new ClassNotFoundException(clazzOrProxyName);
  }
}
