/**
 *                    WTFPL
 *                    Version 2, December 2004
 *
 * Copyright (C) sponge
 *   Planet Earth
 *
 * See...
 *
 *	http://sam.zoy.org/wtfpl/
 *	and
 *	http://en.wikipedia.org/wiki/WTFPL
 *
 * ...for any additional details and license questions.
 */
package net.i2p.BOB;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicBoolean;

import net.i2p.I2PException;
import net.i2p.client.I2PClient;
import net.i2p.client.streaming.I2PServerSocket;
import net.i2p.client.streaming.I2PSocketManager;
import net.i2p.client.streaming.I2PSocketManagerFactory;
import net.i2p.util.I2PAppThread;
import net.i2p.util.Log;

/**
 *
 * Multiplex listeners for TCP and I2P
 *
 * @author sponge
 */
public class MUXlisten implements Runnable {

	private final NamedDB database, info;
	private final Logger _log;
	private final I2PSocketManager socketManager;
	private final ByteArrayInputStream prikey;
	private ThreadGroup tg;
	private final String N;
	private ServerSocket listener;
	private final int backlog = 50; // should this be more? less?
	private final boolean go_out;
	private final boolean come_in;
	private final AtomicBoolean lock;
	private final AtomicBoolean lives;

	/**
	 * Constructor Will fail if INPORT is occupied.
	 *
	 * @param info DB entry for this tunnel
	 * @param database master database of tunnels
	 * @param _log
	 * @throws net.i2p.I2PException
	 * @throws java.io.IOException
	 */
	MUXlisten(AtomicBoolean lock, NamedDB database, NamedDB info, Logger _log) throws I2PException, IOException, RuntimeException {
		int port = 0;
		InetAddress host = null;
		this.lock = lock;
		this.tg = null;
		this.database = database;
		this.info = info;
		this._log = _log;
		lives = new AtomicBoolean(false);
		try {
			wlock();
			try {
				this.info.add("STARTING", Boolean.TRUE);
			} finally {
				wunlock();
			}
			Properties Q = new Properties();
			rlock();
			try {
				N = this.info.get("NICKNAME").toString();
				prikey = new ByteArrayInputStream((byte[]) info.get("KEYS"));
				// Make a new copy so that anything else won't muck with our database.
				Properties R = (Properties) info.get("PROPERTIES");
				Lifted.copyProperties(R, Q);

				this.go_out = info.exists("OUTPORT");
				this.come_in = info.exists("INPORT");
				if (this.come_in) {
					port = Integer.parseInt(info.get("INPORT").toString());
					host = InetAddress.getByName(info.get("INHOST").toString());
				}
			} finally {
				runlock();
			}

			String i2cpHost = Q.getProperty(I2PClient.PROP_TCP_HOST, "127.0.0.1");
			int i2cpPort = 7654;
			String i2cpPortStr = Q.getProperty(I2PClient.PROP_TCP_PORT, "7654");
			try {
				i2cpPort = Integer.parseInt(i2cpPortStr);
			} catch (NumberFormatException nfe) {
				throw new IllegalArgumentException("Invalid I2CP port specified [" + i2cpPortStr + "]");
			}

			if (this.come_in) {
				this.listener = new ServerSocket(port, backlog, host);
			}
			socketManager = I2PSocketManagerFactory.createManager(
					prikey, i2cpHost, i2cpPort, Q);
		} catch (IOException e) {
			// Something went bad.
			wlock();
			try {
				this.info.add("STARTING", Boolean.FALSE);
			} finally {
				wunlock();
			}
			throw e;
		} catch (RuntimeException e) {
			// Something went bad.
			wlock();
			try {
				this.info.add("STARTING", Boolean.FALSE);
			} finally {
				wunlock();
			}
			throw e;
		} catch (Exception e) {
			// Something else went bad.
			wlock();
			try {
				this.info.add("STARTING", Boolean.FALSE);
			} finally {
				wunlock();
			}
			e.printStackTrace();
			throw new RuntimeException(e);
		}
	}

	private void rlock() {
		database.getReadLock();
		info.getReadLock();
	}

	private void runlock() {
		info.releaseReadLock();
		database.releaseReadLock();
	}

	private void wlock() {
		database.getWriteLock();
		info.getWriteLock();
	}

	private void wunlock() {
		info.releaseWriteLock();
		database.releaseWriteLock();
	}

	/**
	 * MUX sockets, fire off a thread to connect, get destination info, and do I/O
	 *
	 */
	public void run() {
		I2PServerSocket SS = null;
		Thread t = null;
		Thread q = null;
		try {
			wlock();
			try {
				try {
					info.add("RUNNING", Boolean.TRUE);
				} catch (Exception e) {
					lock.set(false);
					return;
				}
			} catch (Exception e) {
				lock.set(false);
				return;
			} finally {
				wunlock();
			}
			lives.set(true);
			lock.set(false);
			quit:
			{
				try {
					tg = new ThreadGroup(N);
					{
						// toss the connections to a new threads.
						// will wrap with TCP and UDP when UDP works

						if (go_out) {
							// I2P -> TCP
							SS = socketManager.getServerSocket();
							I2Plistener conn = new I2Plistener(SS, socketManager, info, database, _log, lives);
							t = new I2PAppThread(tg, conn, "BOBI2Plistener " + N);
							t.start();
						}

						if (come_in) {
							// TCP -> I2P
							TCPlistener conn = new TCPlistener(listener, socketManager, info, database, _log, lives);
							q = new I2PAppThread(tg, conn, "BOBTCPlistener " + N);
							q.start();
						}

						wlock();
						try {
							try {
								info.add("STARTING", Boolean.FALSE);
							} catch (Exception e) {
								break quit;
							}
						} catch (Exception e) {
							break quit;
						} finally {
							wunlock();
						}
						boolean spin = true;
						while (spin && lives.get()) {
							try {
								Thread.sleep(1000); //sleep for 1 second
							} catch (InterruptedException e) {
								break quit;
							}
							rlock();
							try {
								try {
									spin = info.get("STOPPING").equals(Boolean.FALSE);
								} catch (Exception e) {
									break quit;
								}
							} catch (Exception e) {
								break quit;
							} finally {
								runlock();
							}
						}
					} // die

				} catch (Exception e) {
					// System.out.println("MUXlisten: Caught an exception" + e);
					break quit;
				}
			} // quit
		} finally {
			lives.set(false);
			// Some grace time.
			try {
				Thread.sleep(100);
			} catch (InterruptedException ex) {
			}
			try {
				wlock();
				try {
					info.add("STARTING", Boolean.FALSE);
					info.add("STOPPING", Boolean.TRUE);
					info.add("RUNNING", Boolean.FALSE);
				} catch (Exception e) {
					lock.set(false);
					return;
				}
			} catch (Exception e) {
			} finally {
				wunlock();
			}
			// Start cleanup.
			while (!lock.compareAndSet(false, true)) {
				// wait
			}
			if (SS != null) {
				try {
					SS.close();
				} catch (I2PException ex) {
				}
			}
			if (listener != null) {
				try {
					listener.close();
				} catch (IOException e) {
				}
			}

			// Some grace time.
			try {
				Thread.sleep(100);
			} catch (InterruptedException ex) {
			}

			// Hopefully nuke stuff here...
			{
				String groupName = tg.getName();
				try {
					_log.warn("destroySocketManager " + groupName);
					socketManager.destroySocketManager();
					_log.warn("destroySocketManager Successful" + groupName);
				} catch (Exception e) {
					// nop
					_log.warn("destroySocketManager Failed" + groupName);
					_log.warn(e.toString());
				}
			}
			// zero out everything.
			try {
				wlock();
				try {
					info.add("STARTING", Boolean.FALSE);
					info.add("STOPPING", Boolean.FALSE);
					info.add("RUNNING", Boolean.FALSE);
				} catch (Exception e) {
					lock.set(false);
					return;
				} finally {
					wunlock();
				}	
			} catch (Exception e) {
			}

			lock.set(false); // Should we force waiting for all threads??

			// Wait around till all threads are collected.
			if (tg != null) {
				String groupName = tg.getName();
				// System.out.println("BOB: MUXlisten: Starting thread collection for: " + groupName);
				_log.warn("BOB: MUXlisten: Starting thread collection for: " + groupName);
				if (tg.activeCount() + tg.activeGroupCount() != 0) {
					// visit(tg, 0, groupName);
					int foo = tg.activeCount() + tg.activeGroupCount();
					// hopefully no longer needed!
					// int bar = lives;
					// System.out.println("BOB: MUXlisten: Waiting on threads for " + groupName);
					// System.out.println("\nBOB: MUXlisten: ThreadGroup dump BEGIN " + groupName);
					// visit(tg, 0, groupName);
					// System.out.println("BOB: MUXlisten: ThreadGroup dump END " + groupName + "\n");
					// Happily spin forever :-(
					while (foo != 0) {
						foo = tg.activeCount() + tg.activeGroupCount();
						// if (lives != bar && lives != 0) {
						// 	System.out.println("\nBOB: MUXlisten: ThreadGroup dump BEGIN " + groupName);
						// 	visit(tg, 0, groupName);
						// 	System.out.println("BOB: MUXlisten: ThreadGroup dump END " + groupName + "\n");
						// }
						// bar = lives;
						try {
							Thread.sleep(100); //sleep for 100 ms (One tenth second)
						} catch (InterruptedException ex) {
							// nop
						}
					}
				}
				// System.out.println("BOB: MUXlisten: Threads went away. Success: " + groupName);
				_log.warn("BOB: MUXlisten: Threads went away. Success: " + groupName);
				tg.destroy();
				// Zap reference to the ThreadGroup so the JVM can GC it.
				tg = null;
			}
			try {
				socketManager.destroySocketManager();
			} catch (Exception e) {
				// nop
			}

		}
	}


	// Debugging... None of this is normally used.
	/**
	 *	Find the root thread group and print them all.
	 *
	 */
	private void visitAllThreads() {
		ThreadGroup root = Thread.currentThread().getThreadGroup().getParent();
		while (root.getParent() != null) {
			root = root.getParent();
		}

		// Visit each thread group
		visit(root, 0, root.getName());
	}

	/**
	 * Recursively visits all thread groups under `group' and dumps them.
	 * @param group ThreadGroup to visit
	 * @param level Current level
	 */
	private static void visit(ThreadGroup group, int level, String tn) {
		// Get threads in `group'
		int numThreads = group.activeCount();
		Thread[] threads = new Thread[numThreads * 2];
		numThreads = group.enumerate(threads, false);
		String indent = "------------------------------------".substring(0, level) + "-> ";
		// Enumerate each thread in `group' and print it.
		for (int i = 0; i < numThreads; i++) {
			// Get thread
			Thread thread = threads[i];
			System.out.println("BOB: MUXlisten: " + tn + ": " + indent + thread.toString());
		}

		// Get thread subgroups of `group'
		int numGroups = group.activeGroupCount();
		ThreadGroup[] groups = new ThreadGroup[numGroups * 2];
		numGroups = group.enumerate(groups, false);

		// Recursively visit each subgroup
		for (int i = 0; i < numGroups; i++) {
			visit(groups[i], level + 1, groups[i].getName());
		}
	}
}
