/**
 *                    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 static net.i2p.app.ClientAppState.*;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.ConnectException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.util.Arrays;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.jar.Attributes;
import java.util.jar.Manifest;

import net.i2p.I2PAppContext;
import net.i2p.app.*;
import net.i2p.client.I2PClient;
import net.i2p.util.I2PAppThread;
import net.i2p.util.SimpleScheduler;
import net.i2p.util.SimpleTimer2;

/**
 * <span style="font-size:8px;font-family:courier;color:#EEEEEE;background-color:#000000">
 * ################################################################################<br>
 * ############################.#..........#..#..........##########################<br>
 * #######################......................................###################<br>
 * ####################...........................#.......#........################<br>
 * #################..................##...................#.........##############<br>
 * ###############................###...####.....#..###.....#.........#############<br>
 * #############...........###..#..###...#####...###.##........#.......############<br>
 * ###########................#......##...#####...##..##.......#..#........########<br>
 * ##########.........................#....##.##..#...##.....................######<br>
 * #########...................................#....#.........................#####<br>
 * ########.........................................#...............#..........####<br>
 * ########.........................................#..........#######..........###<br>
 * #######.................................................############..........##<br>
 * #######..........................................####################.........##<br>
 * #######............####################......########################.........##<br>
 * ######.............###############################################.##.........##<br>
 * ######............################################################..##........##<br>
 * ######............################################################..##........##<br>
 * ######.............##############################################..##.........##<br>
 * ######............##############################################...##..........#<br>
 * ######............#..###########################################...##..........#<br>
 * ######.............#############################################....#..........#<br>
 * #######...........###############################################..##.........##<br>
 * #######...........#####.#.#.#.########################.....#.####...##........##<br>
 * ######............#..............##################.................##.........#<br>
 * ######................####.........###############........#####......##........#<br>
 * ######..............####..#.........############.......##.#.######...##.......##<br>
 * ######.................#.####.........########...........##....###...##.......##<br>
 * #######....#....###...................#######...............#...###..##.......##<br>
 * #######.........###..###.....###.......######.##.#####.........####..##.......##<br>
 * #######.....#...##############.........############......###########.###......##<br>
 * #######....##...##########.......##...##############......#.############.....###<br>
 * ########....#..########......######...##################################....####<br>
 * ########....##.####################...##################################....####<br>
 * ########..#.##..###################..##################################..#..####<br>
 * ##########..###..#################...##################################...#.####<br>
 * #########....##...##############....########..#####.################.##..#.#####<br>
 * ############.##....##########.......#########.###.......###########..#.#########<br>
 * ###############.....#######...#.......########.....##.....######.....###########<br>
 * ###############......###....##..........##.......######....#.........#.#########<br>
 * ##############............##..................##########..............##########<br>
 * ##############..............................##########..#.............##########<br>
 * ###############.......##..................#####..............####....###########<br>
 * ###############.......#####.......#.............####.....#######.....###########<br>
 * ################...#...####......##################.....########....############<br>
 * ################...##..#####.........####.##.....#....##########....############<br>
 * ##################..##..####...........#####.#....############.....#############<br>
 * ##################......#####.................################....##############<br>
 * ###################.....####..........##########..###########....###############<br>
 * ####################..#..#..........................########.....###############<br>
 * #####################.##.......###.................########....#################<br>
 * ######################.........#.......#.##.###############....#################<br>
 * #############.#######...............#####################....###################<br>
 * ###..#.....##...####..........#.....####################....####################<br>
 * ####......##........................##################....######################<br>
 * #.##...###..............###.........###############......#######################<br>
 * #...###..##............######...........................########################<br>
 * ##.......###..........##########....#...#...........############################<br>
 * ##.........##.......############################################################<br>
 * ###........##.....##############################################################<br>
 * ####.............###############################################################<br>
 * ######.........#################################################################<br>
 * #########....###################################################################<br>
 * ################################################################################<br>
 * </span>
 * BOB, main command socket listener, launches the command parser engine.
 *
 * @author sponge
 */
public class BOB implements Runnable, ClientApp {

	public final static String PROP_CONFIG_LOCATION = "BOB.config";
	public final static String PROP_BOB_PORT = "BOB.port";
	public final static String PROP_BOB_HOST = "BOB.host";
	public final static String PROP_CFG_VER = "BOB.CFG.VER";

	private static BOB _bob;

	private NamedDB database;
	private Properties props = new Properties();
	private AtomicBoolean spin = new AtomicBoolean(true);
	private static final String P_RUNNING = "RUNNING";
	private static final String P_STARTING = "STARTING";
	private static final String P_STOPPING = "STOPPING";
	private AtomicBoolean lock = new AtomicBoolean(false);
	// no longer used.
	// private static int maxConnections = 0;

	private final Logger _log;
	private final ClientAppManager _mgr;
	private final String[] _args;
	private volatile ClientAppState _state = UNINITIALIZED;

	private volatile ServerSocket listener;
	private volatile Thread _runner;

	/**
	 * Stop BOB gracefully
	 * @deprecated unused
	 */
	public static void stop() {
		_bob.shutdown(null);
	}

	/**
	 *  For ClientApp interface.
	 *  Does NOT open the listener socket or start threads; caller must call startup()
	 *
	 *  @param mgr may be null
	 *  @param args non-null
	 *  @throws Exception on bad args
	 *  @since 0.9.10
	 */
	public BOB(I2PAppContext context, ClientAppManager mgr, String[] args) {
		// If we were run from command line, log to stdout
		boolean logToStdout = false;
		URL classResource = BOB.class.getResource("BOB.class");
		if (classResource != null) {
		    String classPath = classResource.toString();
		    if (classPath.startsWith("jar")) {
		        String manifestPath = classPath.substring(0, classPath.lastIndexOf("!") + 1) +
		                "/META-INF/MANIFEST.MF";
		        try {
		            Manifest manifest = new Manifest(new URL(manifestPath).openStream());
		            Attributes attrs = manifest.getMainAttributes();
		            String mainClass = attrs.getValue("Main-Class");
		            if ("net.i2p.BOB.Main".equals(mainClass))
		                logToStdout = true;
		        } catch (IOException ioe) {}
		    }
		}

		_log = new Logger(context.logManager().getLog(BOB.class), logToStdout);

		_mgr = mgr;
		_args = args;
		_state = INITIALIZED;
		database = new NamedDB();
		loadConfig();
	}

	/**
	 * Listen for incoming connections and handle them
	 *
	 * @param args
	 */
	public static void main(String[] args) {
		try {
			_bob = new BOB(I2PAppContext.getGlobalContext(), null, args);
			_bob.startup();
		} catch (RuntimeException e) {
			e.printStackTrace();
			throw e;
		} catch (Exception e) {
			e.printStackTrace();
			throw new RuntimeException(e);
		}
	}

	/**
	 * @since 0.9.10
	 */
	private void loadConfig() {
		int i = 0;
		boolean save = false;
		// Set up all defaults to be passed forward to other threads.
		// Re-reading the config file in each thread is pretty damn stupid.
		String configLocation = System.getProperty(PROP_CONFIG_LOCATION, "bob.config");
		// This is here just to ensure there is no interference with our threadgroups.
		SimpleScheduler Y1 = SimpleScheduler.getInstance();
		SimpleTimer2 Y2 = SimpleTimer2.getInstance();
		i = Y1.hashCode();
		i = Y2.hashCode();
		{
			File cfg = new File(configLocation);
			if (!cfg.isAbsolute()) {
				cfg = new File(I2PAppContext.getGlobalContext().getConfigDir(), configLocation);
			}
			FileInputStream fi = null;
			try {
				fi = new FileInputStream(cfg);
				props.load(fi);
			} catch (FileNotFoundException fnfe) {
				_log.warn("Unable to load up the BOB config file " + cfg.getAbsolutePath() + ", Using defaults.", fnfe);
				save = true;
			} catch (IOException ioe) {
				_log.warn("IOException on BOB config file " + cfg.getAbsolutePath() + ", using defaults.", ioe);
			} finally {
				if (fi != null) try { fi.close(); } catch (IOException ioe) {}
			}
		}
		// Global router and client API configurations that are missing are set to defaults here.
		if (!props.containsKey(I2PClient.PROP_TCP_HOST)) {
			props.setProperty(I2PClient.PROP_TCP_HOST, "localhost");
			save = true;
		}
		if (!props.containsKey(I2PClient.PROP_TCP_PORT)) {
			props.setProperty(I2PClient.PROP_TCP_PORT, "7654");
			save = true;
		}
		if (!props.containsKey(PROP_BOB_PORT)) {
			props.setProperty(PROP_BOB_PORT, "2827"); // 0xB0B
			save = true;
		}
		if (!props.containsKey("inbound.length")) {
			props.setProperty("inbound.length", "1");
			save = true;
		}
		if (!props.containsKey("outbound.length")) {
			props.setProperty("outbound.length", "1");
			save = true;
		}
		if (!props.containsKey("inbound.lengthVariance")) {
			props.setProperty("inbound.lengthVariance", "0");
			save = true;
		}
		if (!props.containsKey("outbound.lengthVariance")) {
			props.setProperty("outbound.lengthVariance", "0");
			save = true;
		}
		if (!props.containsKey(PROP_BOB_HOST)) {
			props.setProperty(PROP_BOB_HOST, "localhost");
			save = true;
		}
		// PROP_RELIABILITY_NONE, PROP_RELIABILITY_BEST_EFFORT, PROP_RELIABILITY_GUARANTEED
		if (!props.containsKey(PROP_CFG_VER)) {
			props.setProperty(I2PClient.PROP_RELIABILITY, I2PClient.PROP_RELIABILITY_NONE);
			props.setProperty(PROP_CFG_VER,"1");
			save = true;
		}
		if (save) {
			File cfg = new File(configLocation);
			if (!cfg.isAbsolute()) {
				cfg = new File(I2PAppContext.getGlobalContext().getConfigDir(), configLocation);
			}
			FileOutputStream fo = null;
			try {
				_log.warn("Writing new defaults file " + cfg.getAbsolutePath());
				fo = new FileOutputStream(cfg);
				props.store(fo, cfg.getAbsolutePath());
			} catch (IOException ioe) {
				_log.error("IOException on BOB config file " + cfg.getAbsolutePath(), ioe);
			} finally {
				if (fo != null) try { fo.close(); } catch (IOException ioe) {}
			}
		}
	}

	/**
	 * @since 0.9.10
	 */
	private void startListener() throws IOException {
		listener = new ServerSocket(Integer.parseInt(props.getProperty(PROP_BOB_PORT)), 10, InetAddress.getByName(props.getProperty(PROP_BOB_HOST)));
		listener.setSoTimeout(500); // .5 sec
	}

	/**
	 * @since 0.9.10
	 */
	private void startThread() {
		I2PAppThread t = new I2PAppThread(this, "BOBListener");
		t.start();
		_runner = t;
	}

	/**
	 * @since 0.9.10
	 */
	public void run() {
		if (listener == null) return;
		changeState(RUNNING);
		_log.info("BOB is now running.");
		if (_mgr != null)
			_mgr.register(this);

		int i = 0;
		boolean g = false;
		spin.set(true);
		try {
			Socket server = null;

			while (spin.get()) {
				//DoCMDS connection;

				try {
					server = listener.accept();
					server.setKeepAlive(true);
					g = true;
				} catch (ConnectException ce) {
					g = false;
				} catch (SocketTimeoutException ste) {
					g = false;
				}

				if (g) {
					DoCMDS conn_c = new DoCMDS(spin, lock, server, props, database, _log);
					Thread t = new Thread(conn_c);
					t.setName("BOB.DoCMDS " + i);
					t.start();
					i++;
				}
			}
			changeState(STOPPING);
		} catch (Exception e) {
			if (spin.get())
				_log.error("Unexpected error while listening for connections", e);
			else
				e = null;
			changeState(STOPPING, e);
		} finally {
			_log.info("BOB is now shutting down...");
			// Clean up everything.
			try {
				listener.close();
			} catch (Exception ex) {
				// nop
			}
			// Find all our "BOB.DoCMDS" threads, wait for them to be finished.
			// We could order them to stop, but that could cause nasty issues in the locks.
			visitAllThreads();
			database.getReadLock();
			int all = database.getcount();
			database.releaseReadLock();
			NamedDB nickinfo;
			for (i = 0; i < all; i++) {
				database.getReadLock();
				nickinfo = (NamedDB) database.getnext(i);
				nickinfo.getReadLock();
				if (nickinfo.get(P_RUNNING).equals(Boolean.TRUE) && nickinfo.get(P_STOPPING).equals(Boolean.FALSE) && nickinfo.get(P_STARTING).equals(Boolean.FALSE)) {
					nickinfo.releaseReadLock();
					database.releaseReadLock();
					database.getWriteLock();
					nickinfo.getWriteLock();
					nickinfo.add(P_STOPPING, Boolean.valueOf(true));
					nickinfo.releaseWriteLock();
					database.releaseWriteLock();
				} else {
					nickinfo.releaseReadLock();
					database.releaseReadLock();
				}
			}
			changeState(STOPPED);
			_log.info("BOB is now stopped.");
		}
	}

	/**
	 *	Find the root thread group,
	 *	then find all theads with certain names and wait for them all to be dead.
	 *
	 */
	private static void visitAllThreads() {
		ThreadGroup root = Thread.currentThread().getThreadGroup().getParent();
		while (root.getParent() != null) {
			root = root.getParent();
		}

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

	private static void waitjoin(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);
		// Enumerate each thread in `group' and wait for it to stop if it is one of ours.
		for (int i = 0; i < numThreads; i++) {
			// Get thread
			Thread thread = threads[i];
			if (thread.getName().startsWith("BOB.DoCMDS ")) {
				try {
					if (thread.isAlive()) {
						try {
							thread.join();
						} catch (InterruptedException ex) {
						}
					}
				} catch (SecurityException se) {
					//nop
				}
			}
		}

		// 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++) {
			waitjoin(groups[i], level + 1, groups[i].getName());
		}
	}

	////// begin ClientApp interface

	/**
	 * @since 0.9.10
	 */
	@Override
	public void startup() throws IOException {
		if (_state != INITIALIZED)
			return;
		changeState(STARTING);
		try {
			startListener();
		} catch (IOException e) {
			_log.error("Error starting BOB on"
					+ props.getProperty(PROP_BOB_HOST)
					+ ":" + props.getProperty(PROP_BOB_PORT), e);
			changeState(START_FAILED, e);
			throw e;
		}
		startThread();
	}

	/**
	 * @since 0.9.10
	 */
	@Override
	public void shutdown(String[] args) {
		if (_state != RUNNING)
			return;
		changeState(STOPPING);
		spin.set(false);
		if (_runner != null)
			_runner.interrupt();
		else
			changeState(STOPPED);
	}

	/**
	 * @since 0.9.10
	 */
	@Override
	public ClientAppState getState() {
		return _state;
	}

	/**
	 * @since 0.9.10
	 */
	@Override
	public String getName() {
		return "BOB";
	}

	/**
	 * @since 0.9.10
	 */
	@Override
	public String getDisplayName() {
		return "BOB " + Arrays.toString(_args);
	}

	////// end ClientApp interface
	////// begin ClientApp helpers

	/**
	 *  @since 0.9.10
	 */
	private void changeState(ClientAppState state) {
		changeState(state, null);
	}

	/**
	 *  @since 0.9.10
	 */
	private synchronized void changeState(ClientAppState state, Exception e) {
		_state = state;
		if (_mgr != null)
			_mgr.notify(this, state, null, e);
	}

	////// end ClientApp helpers
}
