/*******************************************************************************
 * Copyright (c) 2011, 2017 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/

package org.eclipse.e4.core.internal.contexts.osgi;

import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.e4.core.contexts.IContextFunction;
import org.eclipse.e4.core.internal.contexts.EclipseContext;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleEvent;
import org.osgi.framework.Constants;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceEvent;
import org.osgi.framework.ServiceListener;
import org.osgi.framework.ServiceReference;
import org.osgi.framework.SynchronousBundleListener;

public class EclipseContextOSGi extends EclipseContext implements ServiceListener, SynchronousBundleListener {

	final private BundleContext bundleContext;

	private Map<String, ServiceReference<?>> refs = Collections.synchronizedMap(new HashMap<>());

	public EclipseContextOSGi(BundleContext bundleContext) {
		super(null);
		this.bundleContext = bundleContext;
		try {
			// process all IContextFunction services already registered
			ServiceReference<?>[] existing = bundleContext.getServiceReferences(IContextFunction.SERVICE_NAME, null);
			if (existing != null) {
				// Reorder so that lowest is first
				Arrays.sort(existing);
				for (ServiceReference<?> element : existing) {
					String name = (String) element.getProperty(IContextFunction.SERVICE_CONTEXT_KEY);
					refs.put(name, element);
					localValues.put(name, bundleContext.getService(element));
				}
			}
		} catch (InvalidSyntaxException e) {
			// should never happen
		}
		this.bundleContext.addServiceListener(this);
		this.bundleContext.addBundleListener(this);
	}

	@Override
	public boolean containsKey(String name, boolean localOnly) {
		if (super.containsKey(name, localOnly))
			return true;
		Object result = lookup(name, this);
		return (result != null);
	}

	@Override
	public Object lookup(String name, EclipseContext originatingContext) {
		if (name == null)
			return null;
		if (refs.containsKey(name)) { // retrieve service again
			// This could be reached, for instance, if previously stored service value is overridden or removed from the context.
			ServiceReference<?> ref = refs.get(name);
			if (ref == null)
				return null;
			Object service = bundleContext.getService(ref);
			bundleContext.ungetService(ref);
			localValues.put(name, service);
			return service;
		}
		ServiceReference<?> ref = bundleContext.getServiceReference(name);
		if (ref == null) {
			refs.put(name, null);
			return null;
		}
		refs.put(name, ref);
		Object service = bundleContext.getService(ref);
		localValues.put(name, service);
		return service;
	}

	@Override
	public void dispose() {
		for (ServiceReference<?> ref : refs.values()) {
			if (ref != null)
				bundleContext.ungetService(ref);
		}
		refs.clear();
		bundleContext.removeServiceListener(this);
		bundleContext.removeBundleListener(this);
		super.dispose();
	}

	@Override
	public void serviceChanged(ServiceEvent event) {
		ServiceReference<?> ref = event.getServiceReference();
		String[] names = ((String[]) ref.getProperty(Constants.OBJECTCLASS));
		for (String name : names) {
			if (IContextFunction.SERVICE_NAME.equals(name)) {
				name = (String) ref.getProperty(IContextFunction.SERVICE_CONTEXT_KEY);
			}

			if (refs.containsKey(name)) {
				// retrieve the highest ranked service of the same type
				ref = bundleContext.getServiceReference(name);

				ServiceReference<?> oldRef = refs.get(name);
				if (oldRef != null && oldRef != ref) {
					bundleContext.ungetService(oldRef);
				}

				if (ref != null) {
					Object service = bundleContext.getService(ref);
					refs.put(name, ref);
					set(name, service);
				} else {
					refs.put(name, null);
					remove(name);
				}
			}
		}
	}

	@Override
	public void bundleChanged(BundleEvent event) {
		// In case OSGi context has not being properly disposed by the application,
		// OSGi framework shutdown will triggered uninjection of all consumed
		// OSGi service. To avoid this, we detect framework shutdown and release
		// services.
		if (event.getType() != BundleEvent.STOPPING)
			return;
		if (event.getBundle().getBundleId() == 0)
			dispose();
	}
}
