/*******************************************************************************
 * 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.impl.tpt.timer;

import java.security.*;
import java.util.Hashtable;
import org.eclipse.equinox.internal.util.UtilActivator;
import org.eclipse.equinox.internal.util.impl.tpt.ServiceFactoryImpl;
import org.eclipse.equinox.internal.util.impl.tpt.threadpool.Executor;
import org.eclipse.equinox.internal.util.impl.tpt.threadpool.ThreadPoolFactoryImpl;
import org.eclipse.equinox.internal.util.pool.ObjectPool;
import org.eclipse.equinox.internal.util.ref.Log;
import org.eclipse.equinox.internal.util.timer.Timer;
import org.eclipse.equinox.internal.util.timer.TimerListener;

/**
 * @author Pavlin Dobrev
 * @version 1.0
 */

public class TimerImpl implements Runnable {

	static Hashtable<TimerQueueNode, TimerQueueNode> nodes;
	static ObjectPool nodePool;

	static ThreadPoolFactoryImpl threadPool;

	private TimerQueue queue;
	private boolean terminated = false;
	private Object sync = new Object();
	private Thread th;

	public TimerImpl(ThreadPoolFactoryImpl threadPool) {
		nodePool = new ObjectPool(new TimerQueueNode(), 2, 4, 2);
		TimerImpl.threadPool = threadPool;
		nodes = new Hashtable<>(10);
		queue = new TimerQueue();
		try {
			th = ((ServiceFactoryImpl.privileged()) ? getOne() : new Thread(this, "[Timer] - Main Queue Handler"));
			try {
				String str = UtilActivator.bc.getProperty("equinox.timer.priority");
				if (str != null)
					th.setPriority(Integer.parseInt(str));
			} catch (Throwable ignored) {
			}
			th.start();
		} catch (Exception e) {
			throw new RuntimeException("Can not start Timer thread!" + e);
		}
	}

	@Override
	public void run() {
		TimerQueueNode n = null;
		while (!terminated) {
			synchronized (sync) {
				if (n == null && queue.isEmpty()) {
					try {
						sync.wait();
					} catch (Exception e) {
					}
					// todo check if isEmpty is necessary
					if (queue.isEmpty() || terminated) {
						continue;
					}
				}
			}
			synchronized (queue) {
				n = queue.getMin();
				while (n != null && !n.enabled) {
					queue.removeMin();
					n.returnInPool();
					n = queue.getMin();
				}
				if (n == null)
					continue;
				long current = System.currentTimeMillis();
				if (n.runOn <= current) {
					switch (n.type) {
						case (Timer.ONE_SHOT_TIMER) : {
							threadPool.execute0(n, n.priority, n.getEName(), n.acc);
							queue.removeMin();
							nodes.remove(n);
							break;
						}
						case (Timer.ONE_SHOT_TIMER_NO_DELAY) : {
							Executor e = threadPool.getExecutor();
							if (e != null) {
								e.setPriorityI(n.priority);
								e.setRunnable(n, n.getEName(), threadPool, n.acc);
							} else {
								Thread th = new Thread(n, n.getEName());
								th.setPriority(n.priority);
								th.start();
							}
							queue.removeMin();
							nodes.remove(n);
							break;
						}
						case (Timer.PERIODICAL_TIMER) : {
							threadPool.execute0(n, n.priority, n.getEName(), n.acc);
							n.runOn += n.period;
							if (n.runOn < current) { // time changed
								n.runOn = current + n.period;
							}
							queue.rescheduleMin(n.runOn);
							break;
						}
						case (Timer.PERIODICAL_TIMER_NO_DELAY) : {
							Executor e = threadPool.getExecutor();
							if (e != null) {
								e.setPriorityI(n.priority);
								e.setRunnable(n, n.getEName(), threadPool, n.acc);
							} else {
								Thread th = new Thread(n, n.getEName());
								th.setPriority(n.priority);
								th.start();
							}
							if (n.runOn < current) { // time changed
								n.runOn = current + n.period;
							}
							queue.rescheduleMin(n.runOn);
						}
					}
					continue;
				}
			}
			synchronized (sync) {
				long tmpWait;
				if (n != null && (tmpWait = n.runOn - System.currentTimeMillis()) > 0) {
					try {
						sync.wait(tmpWait);
					} catch (Exception e) {
						e.printStackTrace();
					}
					TimerQueueNode tmp = null;
					synchronized (queue) {
						tmp = queue.getMin();
						if (tmp != n) {
							n = tmp;
						}
					}
					/* what's this for - we'll continue in any case */
					continue;
				}
			}
		} // while (!terminated)
		nodePool.clear();
		nodePool = null;
		nodes.clear();
		nodes = null;
		queue = null;
	}

	public void terminate() {
		terminated = true;
		synchronized (sync) {
			sync.notify();
		}
		try {
			th.join();
		} catch (InterruptedException ie) {
		}
	}

	private void put(TimerListener listener, int priority, int timerType, long periodMilis, int event, String name, AccessControlContext acc) {
		if (terminated || nodePool == null) {
			throw new RuntimeException("This Instance is a ZOMBIE!!!" + terminated + " " + nodePool);
		}

		TimerQueueNode n = (TimerQueueNode) nodePool.getObject();
		n.setEvent(listener, priority, timerType, System.currentTimeMillis() + periodMilis, periodMilis, event, name, acc);
		TimerQueueNode tmp = nodes.remove(n);
		if (tmp != null) {
			synchronized (queue) {
				queue.removeTimerNode(tmp);
			}
			tmp.returnInPool();
		}
		nodes.put(n, n);
		TimerQueueNode nx;
		synchronized (queue) {
			queue.add(n);
			nx = queue.getMin();
		}
		if (nx == n) {
			synchronized (sync) {
				sync.notifyAll();
			}
		}
	}

	void addNotifyListener(TimerListener listener, int priority, int timerType, long periodMilis, int event, String name) {
		if (timerType < Timer.ONE_SHOT_TIMER || timerType > Timer.PERIODICAL_TIMER_NO_DELAY) {
			throw new IllegalArgumentException("Invalid Timer Type");
		}
		if (listener != null) {
			if (priority >= Thread.MIN_PRIORITY && priority <= Thread.MAX_PRIORITY) {
				if (periodMilis > 0) {
					AccessControlContext acc = Log.security() ? AccessController.getContext() : null;
					put(listener, priority, timerType, periodMilis, event, name, acc);
				} else {
					throw new IllegalArgumentException("Time period must be positive!");
				}
			} else {
				throw new IllegalArgumentException("Priority must be between Thread.MIN_PRIORITY and Thread.MAX_PRIORITY!");
			}
		} else {
			throw new IllegalArgumentException("The timer listener is null");
		}
	}

	public void removeListener(TimerListener listener, int event) {
		TimerQueueNode rmTmp = (TimerQueueNode) nodePool.getObject();
		rmTmp.setEvent(listener, 0, 0, 0, 0, event, null, null);
		TimerQueueNode old = nodes.remove(rmTmp);
		if (old != null) {
			synchronized (queue) {
				queue.removeTimerNode(old);
			}
			old.returnInPool();
		}
		rmTmp.returnInPool();
	}

	private class PrivilegedActionImpl implements PrivilegedAction<Thread> {
		private Runnable runnable = null;
		private boolean locked = false;
		private boolean waiting = false;

		PrivilegedActionImpl() {
		}

		public synchronized void set(Runnable runnable) {
			while (locked) {
				waiting = true;
				try {
					wait();
				} catch (Exception _) {
				}
				waiting = false;
			}
			locked = true;
			this.runnable = runnable;
		}

		@Override
		public Thread run() {
			Runnable runnableLocal = null;
			synchronized (this) {
				runnableLocal = this.runnable;
				this.runnable = null;
				locked = false;
				if (waiting)
					notifyAll();
			}
			return new Thread(runnableLocal, "[Timer] - Main Queue Handler");
		}
	}

	public Thread getOne() throws Exception {
		if (action == null)
			action = new PrivilegedActionImpl();
		action.set(this);
		return AccessController.doPrivileged(action);
	}

	PrivilegedActionImpl action = null;
}
