/*******************************************************************************
 * Copyright (c) 1997, 2018 by ProSyst Software GmbH
 * http://www.prosyst.com
 *
 * 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:
 *    ProSyst Software GmbH - initial API and implementation
 *******************************************************************************/
package org.eclipse.equinox.internal.util.event;

import java.security.AccessController;
import java.security.PrivilegedAction;
import org.eclipse.equinox.internal.util.UtilActivator;

/**
 * Abstract class for asynchronous event dispatching
 *
 * @author Pavlin Dobrev
 * @version 1.0
 */

public abstract class EventThread implements Runnable {

	/**
	 * The last callbacked listener. If the events thread is not responding,
	 * subclasses can take the appropriate actions - remove the listener for
	 * example
	 */
	public Object bad;

	/**
	 * The event queue. This object must be used for synchronization with the
	 * events thread's state
	 */
	protected Queue queue;

	/**
	 * The state of the thread.
	 * <li> bit 0: 0 = started / 1 = stopped;
	 * <li> bit 1: 0 not waiting / 1 = waiting
	 */
	protected int state;

	/**
	 * The time spent in the current callback, or 0 if the thread is not in a
	 * callback
	 */
	protected long time = 0;
	/**
	 * Instancies counter. Subclasses must not modify it.
	 */
	protected int counter = 1;

	/**
	 * The event to be dispatched
	 */
	protected Object element;
	protected String baseName;
	protected String name;
	protected Thread thread;
	protected ThreadGroup group;
	private static PrivilegedActionImpl privilegedAction = null;

	/**
	 * Constructs the first instance of the EventThread
	 * 
	 * @param group
	 *            The ThreadGroup of the thread, or null for the current thread
	 *            group
	 * @param name
	 *            The base name of the thread. The <code> counter  </code> value
	 *            will be added at the end of the string to construct the full
	 *            name.
	 * @param size
	 *            The initial number of elements of the events queue
	 */
	public EventThread(ThreadGroup group, String name, int size) {
		makeThread(this.group = group, this.name = name + '0');
		baseName = name;
		queue = new Queue(size);
		int priority = getThreadPriority();
		if (priority != Thread.NORM_PRIORITY)
			thread.setPriority(priority);
	}

	/**
	 * Constructs the first instance of the EventThread
	 * 
	 * @param group
	 *            The ThreadGroup of the thread, or null for the current thread
	 *            group
	 * @param name
	 *            The base name of the thread. The <code> counter  </code> value
	 *            will be added at the end of the string to construct the full
	 *            name.
	 * @param queue
	 *            The events queue
	 */
	public EventThread(ThreadGroup group, String name, Queue queue) {
		makeThread(this.group = group, this.name = name + '0');
		baseName = name;
		this.queue = queue;
		int priority = getThreadPriority();
		if (priority != Thread.NORM_PRIORITY)
			thread.setPriority(priority);
	}

	/**
	 * Constructs a new EventThread, after the <code> old </code> event thread
	 * has stopped responding
	 * 
	 * @param old
	 *            The previous instance
	 */
	protected EventThread(EventThread old) {
		makeThread(group = old.thread.getThreadGroup(), name = old.baseName + old.counter++);
		baseName = old.baseName;
		counter = old.counter;
		queue = old.queue;
		int priority = getThreadPriority();
		if (priority != Thread.NORM_PRIORITY)
			thread.setPriority(priority);
	}

	public void start() {
		thread.start();
	}

	/**
	 * Adds an event in the event queue. The method must be synchronized
	 * outside, on the <code> queue </code> field.
	 * 
	 * @param event
	 *            The event to add
	 * @param check
	 *            If true, the method will check if the EventThread is still
	 *            responding
	 */
	public void addEvent(Object event, boolean check) {
		try {
			queue.put(event);
		} catch (Throwable t) {
			print(t);
			return;
		}
		if ((state & 2) != 0)
			queue.notify();
		else if (check && checkTime())
			try {
				state |= 1;
				newEventDispatcher(); // must call start
			} catch (Throwable t) {
				print(t);
				state &= 254;
			}
	}

	/**
	 * Processes the event queue. Sets the event to be dispathed in the
	 * <code> element </code> field and calls <cope> processEvent </code>
	 */
	@Override
	public void run() {
		synchronized (queue) {
			queue.notifyAll();
		}
		while (true) {
			try {
				synchronized (queue) {
					if ((state & 1) != 0)
						return; // closed
					while ((element = queue.get()) == null)
						try {
							state |= 2; // waiting
							queue.wait();
							if ((state & 1) != 0)
								return; // closed
							state &= 253; // not waiting
						} catch (InterruptedException ie) {
						}
				}
				processEvent();

			} catch (Throwable t) {
				print(t);
				try { // fix memory leak
					throw new Exception();
				} catch (Exception _) {
				}
			}
		}
	}

	private void makeThread(ThreadGroup threadGroup, String threadName) {
		try {
			if (privilegedAction == null) {
				privilegedAction = new PrivilegedActionImpl();
			}
			privilegedAction.set(threadGroup, this, threadName);
			thread = AccessController.doPrivileged(privilegedAction);
			// thread = new Thread(group, this, name);
			// thread.setDaemon(false);
			// if (!disableContextClassLoader)
			// thread.setContextClassLoader(null);
		} catch (RuntimeException re) {
			throw re;
		} catch (Exception exc) {
			throw new RuntimeException(exc.toString());
		}
	}

	public Thread getThread() {
		return thread;
	}

	public String getName() {
		return name;
	}

	/**
	 * Returns the desired thread priority. Called in the constructors of the
	 * class in order to set the returned value to the thread.
	 * 
	 * @return priority of the thread
	 */
	public abstract int getThreadPriority();

	/**
	 * Performs the actual event delivery. The event is stored in the
	 * <code> element </code> field. The method is supposed to perform the
	 * following for every listener:
	 * <li> synchronized on <code> queue </code> check the state of the thread -
	 * if it si closed - return
	 * <li> set the fields <code> bad and time <code> 
	 * <li> callback 
	 * <li> set bad to null and time to 0
	 */
	public abstract void processEvent();

	/**
	 * Checks if the thread is still active. The fields <code> time </code> and
	 * <code> bad </code> must be used. The method is called from the addEvent
	 * method - thus should be synchronizes on the <code> queue </code> field
	 * outside and additional synchronization is not needed.
	 */
	public abstract boolean checkTime();

	/**
	 * The method must create a new EventThread instance, using
	 * <code> super.EventThread(this) </code> and start it.
	 */
	public abstract void newEventDispatcher();

	/**
	 * Logs the error.
	 * 
	 * @param t
	 */
	public abstract void print(Throwable t);
}

class PrivilegedActionImpl implements PrivilegedAction<Thread> {
	private ThreadGroup group;
	private Runnable runnable;
	private String name;

	private boolean locked = false;
	private int waiting = 0;

	void set(ThreadGroup group, Runnable runnable, String name) {
		lock();
		this.group = group;
		this.runnable = runnable;
		this.name = name;
	}

	@Override
	public Thread run() {
		ThreadGroup group1 = this.group;
		Runnable runnable1 = this.runnable;
		String name1 = this.name;
		unlock();
		Thread th = new Thread(group1, runnable1, name1);
		if (!UtilActivator.getBoolean("equinox.disableContextClassLoader")) //$NON-NLS-1$
			th.setContextClassLoader(null);
		th.setDaemon(false);
		return th;
	}

	private synchronized void lock() {
		while (locked)
			try {
				waiting++;
				wait();
				waiting--;
			} catch (Exception exc) {
			}
		locked = true;
	}

	private synchronized void unlock() {
		locked = false;
		group = null;
		runnable = null;
		name = null;
		if (waiting > 0)
			notifyAll();
	}
}
