/*******************************************************************************
 * Copyright (c) 2005, 2018 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.equinox.internal.event;

import java.security.Permission;
import java.util.Map;
import java.util.Set;
import org.eclipse.osgi.framework.eventmgr.*;
import org.eclipse.osgi.util.NLS;
import org.osgi.framework.BundleContext;
import org.osgi.service.event.*;
import org.osgi.service.log.LogService;

/**
 * Implementation of org.osgi.service.event.EventAdmin. EventAdminImpl uses
 * org.eclipse.osgi.framework.eventmgr.EventManager. It is assumed
 * org.eclipse.osgi.framework.eventmgr package is exported by some other bundle.
 */
public class EventAdminImpl implements EventAdmin {
	private final LogTracker log;
	private final EventHandlerTracker handlers;
	private volatile EventManager eventManager;

	/**
	 * Constructor for EventAdminImpl.
	 * 
	 * @param context BundleContext
	 */
	EventAdminImpl(BundleContext context) {
		super();
		log = new LogTracker(context, System.out);
		handlers = new EventHandlerTracker(context, log);
	}

	/**
	 * This method should be called before registering EventAdmin service
	 */
	void start() {
		log.open();
		ThreadGroup eventGroup = new ThreadGroup("Equinox Event Admin"); //$NON-NLS-1$
		eventGroup.setDaemon(true);
		eventManager = new EventManager(EventAdminMsg.EVENT_ASYNC_THREAD_NAME, eventGroup);
		handlers.open();
	}

	/**
	 * This method should be called after unregistering EventAdmin service
	 */
	void stop() {
		handlers.close();
		eventManager.close();
		eventManager = null; // signify we have stopped
		log.close();
	}

	@Override
	public void postEvent(Event event) {
		dispatchEvent(event, true);
	}

	@Override
	public void sendEvent(Event event) {
		dispatchEvent(event, false);
	}

	/**
	 * Internal main method for sendEvent() and postEvent(). Dispatching an
	 * event to EventHandler. All exceptions are logged except when dealing with
	 * LogEntry.
	 * 
	 * @param event to be delivered
	 * @param isAsync must be set to true for synchronous event delivery, false
	 *        for asynchronous delivery.
	 */
	private void dispatchEvent(Event event, boolean isAsync) {
		// keep a local copy in case we are stopped in the middle of dispatching
		EventManager currentManager = eventManager;
		if (currentManager == null) {
			// EventAdmin is stopped
			return;
		}
		if (event == null) {
			log.log(LogService.LOG_ERROR, EventAdminMsg.EVENT_NULL_EVENT);
			// continue from here will result in an NPE below; the spec for EventAdmin does not allow for null here
			throw new NullPointerException(EventAdminMsg.EVENT_NULL_EVENT);
		}

		String topic = event.getTopic();

		try {
			checkTopicPermissionPublish(topic);
		} catch (SecurityException e) {
			String msg = NLS.bind(EventAdminMsg.EVENT_NO_TOPICPERMISSION_PUBLISH, event.getTopic());
			log.log(LogService.LOG_ERROR, msg);
			// must throw a security exception here according to the EventAdmin spec
			throw e;
		}

		Set<EventHandlerWrapper> eventHandlers = handlers.getHandlers(topic);
		// If there are no handlers, then we are done
		if (eventHandlers.isEmpty()) {
			return;
		}

		SecurityManager sm = System.getSecurityManager();
		Permission perm = (sm == null) ? null : new TopicPermission(topic, TopicPermission.SUBSCRIBE);

		Map<EventHandlerWrapper, Permission> listeners = new CopyOnWriteIdentityMap<>();
		for (EventHandlerWrapper wrapper : eventHandlers)
			listeners.put(wrapper, perm);

		// Create the listener queue for this event delivery
		ListenerQueue<EventHandlerWrapper, Permission, Event> listenerQueue = new ListenerQueue<>(currentManager);
		// Add the listeners to the queue and associate them with the event
		// dispatcher
		listenerQueue.queueListeners(listeners.entrySet(), handlers);
		// Deliver the event to the listeners.
		if (isAsync) {
			listenerQueue.dispatchEventAsynchronous(0, event);
		} else {
			listenerQueue.dispatchEventSynchronous(0, event);
		}
	}

	/**
	 * Checks if the caller bundle has right PUBLISH TopicPermision.
	 * 
	 * @param topic
	 * @throws SecurityException if the caller does not have the right to PUBLISH TopicPermission
	 */
	private void checkTopicPermissionPublish(String topic) throws SecurityException {
		SecurityManager sm = System.getSecurityManager();
		if (sm == null)
			return;
		sm.checkPermission(new TopicPermission(topic, TopicPermission.PUBLISH));
	}

}
