/*******************************************************************************
 * Copyright (c) 2006, 2013 IBM Corporation and others.
 * 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:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/

package org.eclipse.core.runtime.internal.adaptor;

import java.io.IOException;
import java.net.URL;
import java.net.URLConnection;
import java.util.*;
import org.eclipse.osgi.baseadaptor.*;
import org.eclipse.osgi.baseadaptor.bundlefile.BundleEntry;
import org.eclipse.osgi.baseadaptor.hooks.AdaptorHook;
import org.eclipse.osgi.baseadaptor.hooks.ClassLoadingStatsHook;
import org.eclipse.osgi.baseadaptor.loader.ClasspathEntry;
import org.eclipse.osgi.baseadaptor.loader.ClasspathManager;
import org.eclipse.osgi.framework.adaptor.FrameworkAdaptor;
import org.eclipse.osgi.framework.adaptor.StatusException;
import org.eclipse.osgi.framework.debug.FrameworkDebugOptions;
import org.eclipse.osgi.framework.internal.core.*;
import org.eclipse.osgi.framework.internal.core.Constants;
import org.eclipse.osgi.framework.log.FrameworkLog;
import org.eclipse.osgi.framework.log.FrameworkLogEntry;
import org.eclipse.osgi.service.resolver.BundleDescription;
import org.eclipse.osgi.service.resolver.StateHelper;
import org.eclipse.osgi.util.NLS;
import org.osgi.framework.*;

public class EclipseLazyStarter implements ClassLoadingStatsHook, AdaptorHook, HookConfigurator {
	private static final boolean throwErrorOnFailedStart = "true".equals(FrameworkProperties.getProperty("osgi.compatibility.errorOnFailedStart", "true")); //$NON-NLS-1$//$NON-NLS-2$ //$NON-NLS-3$
	private static final String OPTION_DEBUG_CYCLES = FrameworkAdaptor.FRAMEWORK_SYMBOLICNAME + "/resolver/cycles"; //$NON-NLS-1$
	private static boolean DEBUG_CYCLES = false;
	private BaseAdaptor adaptor;
	// holds the current activation trigger class and the ClasspathManagers that need to be activated
	private ThreadLocal<List<Object>> activationStack = new ThreadLocal<List<Object>>();
	// used to store exceptions that occurred while activating a bundle
	// keyed by ClasspathManager->Exception
	// WeakHashMap is used to prevent pinning the ClasspathManager objects.
	private final Map<ClasspathManager, TerminatingClassNotFoundException> errors = Collections.synchronizedMap(new WeakHashMap<ClasspathManager, TerminatingClassNotFoundException>());

	public void preFindLocalClass(String name, ClasspathManager manager) throws ClassNotFoundException {
		AbstractBundle bundle = (AbstractBundle) manager.getBaseData().getBundle();
		// If the bundle is active, uninstalled or stopping then the bundle has already
		// been initialized (though it may have been destroyed) so just return the class.
		if ((bundle.getState() & (Bundle.ACTIVE | Bundle.UNINSTALLED | Bundle.STOPPING)) != 0)
			return;
		EclipseStorageHook storageHook = (EclipseStorageHook) manager.getBaseData().getStorageHook(EclipseStorageHook.KEY);
		// The bundle is not active and does not require activation, just return the class
		if (!shouldActivateFor(name, manager.getBaseData(), storageHook, manager))
			return;
		List<Object> stack = activationStack.get();
		if (stack == null) {
			stack = new ArrayList<Object>(6);
			activationStack.set(stack);
		}
		// the first element in the stack is the name of the trigger class, 
		// each element after the trigger class is a classpath manager 
		// that must be activated after the trigger class has been defined (see postFindLocalClass)
		int size = stack.size();
		if (size > 1) {
			for (int i = size - 1; i >= 1; i--)
				if (manager == stack.get(i))
					// the manager is already on the stack in which case we are already in the process of loading the trigger class
					return;
		}
		Thread threadChangingState = bundle.getStateChanging();
		if (bundle.getState() == Bundle.STARTING && threadChangingState == Thread.currentThread())
			return; // this thread is starting the bundle already
		if (size == 0)
			stack.add(name);
		stack.add(manager);
	}

	public void postFindLocalClass(String name, Class<?> clazz, ClasspathManager manager) throws ClassNotFoundException {
		List<Object> stack = activationStack.get();
		if (stack == null)
			return;
		int size = stack.size();
		if (size <= 1 || stack.get(0) != name)
			return;
		// if we have a stack we must clear it even if (clazz == null)
		ClasspathManager[] managers = null;
		managers = new ClasspathManager[size - 1];
		for (int i = 1; i < size; i++)
			managers[i - 1] = (ClasspathManager) stack.get(i);
		stack.clear();
		if (clazz == null)
			return;
		for (int i = managers.length - 1; i >= 0; i--) {
			if (errors.get(managers[i]) != null) {
				if (throwErrorOnFailedStart)
					throw errors.get(managers[i]);
				continue;
			}

			// The bundle must be started.
			// Note that another thread may already be starting this bundle;
			// In this case we will timeout after a default of 5 seconds and record the BundleException
			long startTime = System.currentTimeMillis();
			try {
				// do not persist the start of this bundle
				managers[i].getBaseClassLoader().getDelegate().setLazyTrigger();
			} catch (BundleException e) {
				Bundle bundle = managers[i].getBaseData().getBundle();
				Throwable cause = e.getCause();
				if (cause != null && cause instanceof StatusException) {
					StatusException status = (StatusException) cause;
					if ((status.getStatusCode() & StatusException.CODE_ERROR) == 0) {
						if (status.getStatus() instanceof Thread) {
							String message = NLS.bind(EclipseAdaptorMsg.ECLIPSE_CLASSLOADER_CONCURRENT_STARTUP, new Object[] {Thread.currentThread(), name, status.getStatus(), bundle, new Long(System.currentTimeMillis() - startTime)});
							adaptor.getFrameworkLog().log(new FrameworkLogEntry(FrameworkAdaptor.FRAMEWORK_SYMBOLICNAME, FrameworkLogEntry.WARNING, 0, message, 0, e, null));
						}
						continue;
					}
				}
				String message = NLS.bind(EclipseAdaptorMsg.ECLIPSE_CLASSLOADER_ACTIVATION, bundle.getSymbolicName(), Long.toString(bundle.getBundleId()));
				TerminatingClassNotFoundException error = new TerminatingClassNotFoundException(message, e);
				errors.put(managers[i], error);
				if (throwErrorOnFailedStart) {
					adaptor.getFrameworkLog().log(new FrameworkLogEntry(FrameworkAdaptor.FRAMEWORK_SYMBOLICNAME, FrameworkLogEntry.ERROR, 0, message, 0, e, null));
					throw error;
				}
				adaptor.getEventPublisher().publishFrameworkEvent(FrameworkEvent.ERROR, bundle, new BundleException(message, e));
			}
		}
	}

	public void preFindLocalResource(String name, ClasspathManager manager) {
		// do nothing
	}

	public void postFindLocalResource(String name, URL resource, ClasspathManager manager) {
		// do nothing
	}

	public void recordClassDefine(String name, Class<?> clazz, byte[] classbytes, ClasspathEntry classpathEntry, BundleEntry entry, ClasspathManager manager) {
		// do nothing
	}

	private boolean shouldActivateFor(String className, BaseData bundledata, EclipseStorageHook storageHook, ClasspathManager manager) throws ClassNotFoundException {
		if (!isLazyStartable(className, bundledata, storageHook))
			return false;
		//if (manager.getBaseClassLoader().getDelegate().isLazyTriggerSet())
		//	return false;
		// Don't activate non-starting bundles
		if (bundledata.getBundle().getState() == Bundle.RESOLVED) {
			if (throwErrorOnFailedStart) {
				TerminatingClassNotFoundException error = errors.get(manager);
				if (error != null)
					throw error;
			}
			return (bundledata.getStatus() & Constants.BUNDLE_STARTED) != 0;
		}
		return true;
	}

	private boolean isLazyStartable(String className, BaseData bundledata, EclipseStorageHook storageHook) {
		if (storageHook == null)
			return false;
		boolean lazyStart = storageHook.isLazyStart();
		String[] excludes = storageHook.getLazyStartExcludes();
		String[] includes = storageHook.getLazyStartIncludes();
		// no exceptions, it is easy to figure it out
		if (excludes == null && includes == null)
			return lazyStart;
		// otherwise, we need to check if the package is in the exceptions list
		int dotPosition = className.lastIndexOf('.');
		// the class has no package name... no exceptions apply
		if (dotPosition == -1)
			return lazyStart;
		String packageName = className.substring(0, dotPosition);
		if (lazyStart)
			return ((includes == null || contains(includes, packageName)) && (excludes == null || !contains(excludes, packageName)));
		return (excludes != null && contains(excludes, packageName));
	}

	private boolean contains(String[] array, String element) {
		for (int i = 0; i < array.length; i++)
			if (array[i].equals(element))
				return true;
		return false;
	}

	public void addHooks(HookRegistry hookRegistry) {
		hookRegistry.addClassLoadingStatsHook(this);
		hookRegistry.addAdaptorHook(this);
	}

	public void addProperties(Properties properties) {
		// do nothing
	}

	public FrameworkLog createFrameworkLog() {
		// do nothing
		return null;
	}

	/**
	 * @throws BundleException  
	 */
	public void frameworkStart(BundleContext context) throws BundleException {
		// nothing
	}

	/**
	 * @throws BundleException  
	 */
	public void frameworkStop(BundleContext context) throws BundleException {
		// nothing
	}

	public void frameworkStopping(BundleContext context) {
		if (!DEBUG_CYCLES)
			return;

		BundleDescription[] allBundles = adaptor.getState().getResolvedBundles();
		StateHelper stateHelper = adaptor.getPlatformAdmin().getStateHelper();
		Object[][] cycles = stateHelper.sortBundles(allBundles);
		logCycles(cycles);
	}

	public void handleRuntimeError(Throwable error) {
		// do nothing

	}

	public void initialize(BaseAdaptor baseAdaptor) {
		this.adaptor = baseAdaptor;
		setDebugOptions();
	}

	/**
	 * @throws IOException  
	 */
	public URLConnection mapLocationToURLConnection(String location) throws IOException {
		// do nothing
		return null;
	}

	private void logCycles(Object[][] cycles) {
		// log cycles
		if (cycles.length > 0) {
			StringBuffer cycleText = new StringBuffer("["); //$NON-NLS-1$			
			for (int i = 0; i < cycles.length; i++) {
				cycleText.append('[');
				for (int j = 0; j < cycles[i].length; j++) {
					cycleText.append(((BundleDescription) cycles[i][j]).getSymbolicName());
					cycleText.append(',');
				}
				cycleText.insert(cycleText.length() - 1, ']');
			}
			cycleText.setCharAt(cycleText.length() - 1, ']');
			String message = NLS.bind(EclipseAdaptorMsg.ECLIPSE_BUNDLESTOPPER_CYCLES_FOUND, cycleText);
			FrameworkLogEntry entry = new FrameworkLogEntry(FrameworkAdaptor.FRAMEWORK_SYMBOLICNAME, FrameworkLogEntry.INFO, 0, message, 0, null, null);
			adaptor.getFrameworkLog().log(entry);
		}
	}

	private void setDebugOptions() {
		FrameworkDebugOptions options = FrameworkDebugOptions.getDefault();
		// may be null if debugging is not enabled
		if (options == null)
			return;
		DEBUG_CYCLES = options.getBooleanOption(OPTION_DEBUG_CYCLES, false);
	}

	private static class TerminatingClassNotFoundException extends ClassNotFoundException implements StatusException {
		private static final long serialVersionUID = -6730732895632169173L;
		private Throwable cause;

		public TerminatingClassNotFoundException(String message, Throwable cause) {
			super(message, cause);
			this.cause = cause;
		}

		public Object getStatus() {
			return cause;
		}

		public int getStatusCode() {
			return StatusException.CODE_ERROR;
		}
	}
}
