/*******************************************************************************
 * Copyright (c) 2014, 2018 BestSolution.at 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:
 *     Tom Schindl <tom.schindl@bestsolution.at> - initial API and implementation
 *     Dirk Fauth <dirk.fauth@googlemail.com> - Bug 513563
 *     Christoph Läubrich - Bug 572476
 *******************************************************************************/
package org.eclipse.e4.core.di.internal.extensions;

import java.lang.Thread.UncaughtExceptionHandler;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BooleanSupplier;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.e4.core.contexts.IEclipseContext;
import org.eclipse.e4.core.di.IInjector;
import org.eclipse.e4.core.di.extensions.Service;
import org.eclipse.e4.core.di.suppliers.ExtendedObjectSupplier;
import org.eclipse.e4.core.di.suppliers.IObjectDescriptor;
import org.eclipse.e4.core.di.suppliers.IRequestor;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleEvent;
import org.osgi.framework.Filter;
import org.osgi.framework.FrameworkUtil;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceReference;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.annotations.ReferenceCardinality;
import org.osgi.service.component.annotations.ReferencePolicy;
import org.osgi.service.event.Event;
import org.osgi.service.event.EventHandler;
import org.osgi.service.log.Logger;
import org.osgi.service.log.LoggerFactory;
import org.osgi.util.tracker.BundleTracker;
import org.osgi.util.tracker.BundleTrackerCustomizer;
import org.osgi.util.tracker.ServiceTracker;

/**
 * Supplier for {@link Service}
 */
@Component(service = { ExtendedObjectSupplier.class, EventHandler.class }, property = {
		"dependency.injection.annotation=org.eclipse.e4.core.di.extensions.Service",
		"event.topics=" + IEclipseContext.TOPIC_DISPOSE })
public class ServiceSupplier extends ExtendedObjectSupplier implements EventHandler {

	LoggerFactory factory;
	Logger logger;

	private volatile BundleTracker<ServiceSupplierContext> supplierContextTracker;

	@Activate
	void activate(BundleContext bundleContext) {
		this.supplierContextTracker = new BundleTracker<>(bundleContext,
				Bundle.RESOLVED | Bundle.STARTING | Bundle.ACTIVE | Bundle.STOPPING,
				new BundleTrackerCustomizer<ServiceSupplierContext>() {

					@Override
					public ServiceSupplierContext addingBundle(Bundle bundle, BundleEvent event) {
						return new ServiceSupplierContext(bundle, bundleContext,
								(t, e) -> logError("Injection failed", e));//$NON-NLS-1$
					}

					@Override
					public void modifiedBundle(Bundle bundle, BundleEvent event, ServiceSupplierContext object) {
						switch (event.getType()) {
						case BundleEvent.STOPPING:
							object.serviceTracker.values().forEach(ServiceTracker::close);
							break;
						case BundleEvent.STARTED:
						case BundleEvent.STOPPED:
							object.refreshServices();
							break;
						default:
							break;
						}
					}

					@Override
					public void removedBundle(Bundle bundle, BundleEvent event, ServiceSupplierContext object) {
						object.dispose();
					}
				});
		this.supplierContextTracker.open();
	}

	@Deactivate
	void deactivate(BundleContext bundleContext) {
		this.supplierContextTracker.close();
	}

	@Override
	public Object get(IObjectDescriptor descriptor, IRequestor requestor, boolean track, boolean group) {
		Type desiredType = descriptor.getDesiredType();
		Bundle bundle = FrameworkUtil.getBundle(requestor.getRequestingObjectClass());
		ServiceSupplierContext supplierContext;
		if (bundle == null || (supplierContext = supplierContextTracker.getObject(bundle)) == null
				|| supplierContext.disposed) {
			// bundle has gone...
			return IInjector.NOT_A_VALUE;
		}
		Service qualifier = descriptor.getQualifier(Service.class);
		if (desiredType instanceof ParameterizedType) {
			ParameterizedType t = (ParameterizedType) desiredType;
			if (t.getRawType() == Collections.class || t.getRawType() == List.class) {
				return handleCollection(supplierContext, t.getActualTypeArguments()[0], requestor,
						track && qualifier.dynamic(), qualifier);
			}
		}

		return handleSingle(supplierContext, desiredType, requestor, track && qualifier.dynamic(), qualifier);
	}

	private Object handleSingle(ServiceSupplierContext supplierContext, Type t, IRequestor requestor, boolean track,
			Service qualifier) {
		Class<?> cls = t instanceof ParameterizedType ? (Class<?>) ((ParameterizedType) t).getRawType() : (Class<?>) t;
		try {
			Filter filter = supplierContext.getFilter(qualifier.filterExpression());
			ServiceSupplierTracker<?> tracker = supplierContext.getTracker(cls);
			return tracker.getAndTrack(filter, track ? requestor : null, s -> {
				Optional<?> service = s.findFirst();
				if (service.isPresent()) {
					return service.get();
				}
				return IInjector.NOT_A_VALUE;
			});
		} catch (InvalidSyntaxException e) {
			logError("Invalid filter expression", e); //$NON-NLS-1$
			return IInjector.NOT_A_VALUE;
		}
	}

	private Object handleCollection(ServiceSupplierContext supplierContext, Type t, IRequestor requestor, boolean track,
			Service qualifier) {
		Class<?> cls = t instanceof ParameterizedType ? (Class<?>) ((ParameterizedType) t).getRawType() : (Class<?>) t;
		try {
			Filter filter = supplierContext.getFilter(qualifier.filterExpression());
			ServiceSupplierTracker<?> tracker = supplierContext.getTracker(cls);
			return tracker.getAndTrack(filter, track ? requestor : null, s -> s.collect(Collectors.toList()));
		} catch (InvalidSyntaxException e) {
			logError("Invalid filter expression", e); //$NON-NLS-1$
			return IInjector.NOT_A_VALUE;
		}
	}

	/**
	 * Method to log an exception.
	 *
	 * @param message The log message.
	 * @param e       The exception that should be logged.
	 */
	void logError(String message, Throwable e) {
		Logger log = this.logger;
		if (log != null) {
			log.error(message, e);
		} else {
			// fallback if no LogService is available
			e.printStackTrace();
		}
	}

	@Override
	public void handleEvent(Event event) {
		supplierContextTracker.getTracked().values().stream().flatMap(ctx -> ctx.serviceTracker.values().stream())
				.forEach(ServiceSupplierTracker::cleanup);
	}

	@Reference(cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC)
	void setLogger(LoggerFactory factory) {
		this.factory = factory;
		this.logger = factory.getLogger(getClass());
	}

	void unsetLogger(LoggerFactory loggerFactory) {
		if (this.factory == loggerFactory) {
			this.factory = null;
			this.logger = null;
		}
	}

	private static final class ServiceSupplierContext {

		private final Bundle bundle;
		private final BundleContext serviceBundleContext;
		final Map<Class<?>, ServiceSupplierTracker<?>> serviceTracker = new ConcurrentHashMap<>();
		volatile boolean disposed;
		private final UncaughtExceptionHandler exceptionHandler;

		ServiceSupplierContext(Bundle bundle, BundleContext serviceBundleContext,
				UncaughtExceptionHandler exceptionHandler) {
			this.bundle = bundle;
			this.serviceBundleContext = serviceBundleContext;
			this.exceptionHandler = exceptionHandler;
		}

		public Filter getFilter(String filterExpression) throws InvalidSyntaxException {
			if (filterExpression == null || filterExpression.isEmpty() || disposed) {
				return null;
			}
			return serviceBundleContext.createFilter(filterExpression);
		}

		@SuppressWarnings("unchecked")
		public <T> ServiceSupplierTracker<T> getTracker(Class<T> serviceClass) {
			return (ServiceSupplierTracker<T>) serviceTracker.computeIfAbsent(serviceClass, cls -> {
				BundleContext bundleContext = bundle.getBundleContext();
				if (bundleContext == null || bundle.getState() == Bundle.STOPPING) {
					bundleContext = serviceBundleContext;
				}
				ServiceSupplierTracker<T> tracker = new ServiceSupplierTracker<>(bundleContext, serviceClass,
						exceptionHandler);
				tracker.open();
				return tracker;
			});

		}

		void refreshServices() {
			for (Iterator<Entry<Class<?>, ServiceSupplierTracker<?>>> iter = serviceTracker.entrySet().iterator(); iter
					.hasNext();) {
				Entry<Class<?>, ServiceSupplierTracker<?>> entry = iter.next();
				serviceTracker.compute(entry.getKey(), (k, v) -> {
					if (v != null) {
						v.close();
						v.update(null);
					}
					return null;
				});
			}
		}

		void dispose() {
			disposed = true;
			refreshServices();
		}

	}

	private static final class ServiceSupplierTracker<T> extends ServiceTracker<T, T> {

		Set<IRequestor> trackedRequestors = ConcurrentHashMap.newKeySet();
		private final UncaughtExceptionHandler exceptionHandler;
		private final AtomicReference<CompletableFuture<?>> pending = new AtomicReference<>();

		public ServiceSupplierTracker(BundleContext context, Class<T> clazz,
				UncaughtExceptionHandler exceptionHandler) {
			super(context, clazz, null);
			this.exceptionHandler = exceptionHandler;
		}

		public synchronized <R> R getAndTrack(Filter f, IRequestor requestor, Function<Stream<T>, R> extractor) {
			cleanup();
			Stream<T> stream = getTracked().entrySet().stream().filter(entry -> f == null || f.match(entry.getKey()))
					.map(Entry::getValue);
			R value = extractor.apply(stream);
			if (requestor != null) {
				trackedRequestors.add(requestor);
			}
			return value;
		}

		void cleanup() {
			for (Iterator<IRequestor> iterator = trackedRequestors.iterator(); iterator.hasNext();) {
				IRequestor requestor = iterator.next();
				if (!requestor.isValid()) {
					iterator.remove();
				}
			}
		}

		@Override
		public T addingService(ServiceReference<T> reference) {
			T service = super.addingService(reference);
			if (service != null) {
				update(() -> getService(reference) != null);
			}
			return service;
		}

		@Override
		public void removedService(ServiceReference<T> reference, T service) {
			super.removedService(reference, service);
			update(() -> getService(reference) == null);
		}

		@Override
		public void modifiedService(ServiceReference<T> reference, T service) {
			super.modifiedService(reference, service);
			update(null);
		}

		private void update(BooleanSupplier check) {
			IRequestor[] requestors = trackedRequestors.toArray(IRequestor[]::new);
			if (requestors.length == 0) {
				return;
			}
			CompletableFuture<Void> execution = new CompletableFuture<>();
			CompletableFuture<?> pendingExecution = pending.getAndSet(execution);
			if (pendingExecution != null) {
				pendingExecution.cancel(true);
			}
			execution.completeAsync(() -> {
				if (check != null) {
					do {
						Thread.yield();
					} while (!check.getAsBoolean() && !execution.isCancelled());
				}
				if (!execution.isCancelled()) {
					refreshRequestors(requestors);
				}
				return null;
			}).handle((v, e) -> {
				if (e instanceof CancellationException) {
					// request was canceled
					return null;
				}
				if (e != null) {
					exceptionHandler.uncaughtException(Thread.currentThread(), e);
				}
				pending.compareAndSet(execution, null);
				return null;
			});
		}

		private synchronized void refreshRequestors(IRequestor[] requestors) {
			for (IRequestor requestor : requestors) {
				if (requestor.isValid()) {
					try {
						requestor.resolveArguments(false);
						requestor.execute();
					} catch (RuntimeException e) {
						exceptionHandler.uncaughtException(Thread.currentThread(), e);
					}
				} else {
					trackedRequestors.remove(requestor);
				}

			}
		}

	}

}
