/***************************************************************************
 * Copyright 2014 Kieker Project (http://kieker-monitoring.net)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 ***************************************************************************/

package kieker.tools.bridge.cli;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.reflect.InvocationTargetException;
import java.net.URL;
import java.net.URLClassLoader;
import java.security.AccessController;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;

import org.apache.commons.cli.BasicParser;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.reflections.Reflections;

import kieker.common.configuration.Configuration;
import kieker.common.logging.Log;
import kieker.common.logging.LogFactory;
import kieker.common.record.IMonitoringRecord;
import kieker.monitoring.core.configuration.ConfigurationFactory;
import kieker.tools.bridge.IServiceListener;
import kieker.tools.bridge.LookupEntity;
import kieker.tools.bridge.ServiceContainer;
import kieker.tools.bridge.connector.ConnectorDataTransmissionException;
import kieker.tools.bridge.connector.IServiceConnector;
import kieker.tools.bridge.connector.ServiceConnectorFactory;
import kieker.tools.bridge.connector.jms.JMSClientConnector;
import kieker.tools.bridge.connector.jms.JMSEmbeddedConnector;
import kieker.tools.bridge.connector.tcp.TCPClientConnector;
import kieker.tools.bridge.connector.tcp.TCPMultiServerConnector;
import kieker.tools.bridge.connector.tcp.TCPSingleServerConnector;
import kieker.tools.util.CLIHelpFormatter;

/**
 * The command line server of the KDB.
 * 
 * @author Reiner Jung
 * @since 1.8
 */
public final class CLIServerMain {

	/**
	 * The logger used in the server.
	 */
	private static final Log LOG = LogFactory.getLog(CLIServerMain.class);

	private static final String CMD_TYPE = "t";
	private static final String CMD_TYPE_LONG = "type";

	private static final String CMD_HOST = "h";
	private static final String CMD_HOST_LONG = "host";

	private static final String CMD_PORT = "p";
	private static final String CMD_PORT_LONG = "port";

	private static final String CMD_USER = "u";
	private static final String CMD_USER_LONG = "user";

	private static final String CMD_PASSWORD = "w";
	private static final String CMD_PASSWORD_LONG = "password";

	private static final String CMD_URL = "l";
	private static final String CMD_URL_LONG = "url";

	private static final String CMD_KIEKER_CONFIGURATION = "c";
	private static final String CMD_KIEKER_CONFIGURATION_LONG = "configuration";

	private static final String CMD_LIBRARIES = "L";
	private static final String CMD_LIBRARIES_LONG = "libraries";

	private static final String CMD_DAEMON = "d";
	private static final String CMD_DAEMON_LONG = "daemon";

	private static final String CMD_STATS = "s";
	private static final String CMD_STATS_LONG = "stats";

	private static final String CMD_VERBOSE = "v";
	private static final String CMD_VERBOSE_LONG = "verbose";

	private static final String CMD_MAP_FILE = "m";
	private static final String CMD_MAP_FILE_LONG = "map";

	private static final String DAEMON_FILE = "daemon.pidfile";

	private static final String JAVA_TMP_DIR = "java.io.tmpdir";

	private static final String CLI_CONNECTOR = "kieker.tools.bridge.connector";

	private static boolean verbose;
	private static boolean stats;
	private static long startTime;
	private static long deltaTime;

	private static Options options;
	private static CommandLine commandLine;

	private static URLClassLoader classLoader;
	private static ServiceContainer container;

	private CLIServerMain() {
		// private default constructor
	}

	/**
	 * CLI server main.
	 * 
	 * @param args
	 *            command line arguments
	 */
	public static void main(final String[] args) {
		Configuration configuration;

		int exitCode = 0;
		CLIServerMain.declareOptions();
		try {
			// parse the command line arguments
			commandLine = new BasicParser().parse(options, args);

			// verbosity setup
			verbose = commandLine.hasOption(CMD_VERBOSE);

			// statistics
			stats = commandLine.hasOption(CMD_STATS);

			// daemon mode
			if (commandLine.hasOption(CMD_DAEMON)) {
				System.out.close();
				System.err.close();
				CLIServerMain.getPidFile().deleteOnExit();
			}

			// Find libraries and setup mapping
			final ConcurrentMap<Integer, LookupEntity> lookupEntityMap = ServiceConnectorFactory.createLookupEntityMap(CLIServerMain.createRecordMap());

			// Kieker setup
			if (commandLine.hasOption(CMD_KIEKER_CONFIGURATION)) {
				configuration = ConfigurationFactory.createConfigurationFromFile(commandLine.getOptionValue("c"));
			} else {
				configuration = ConfigurationFactory.createSingletonConfiguration();
			}

			// reconfigure kieker configuration
			if (commandLine.hasOption(CMD_PORT)) {
				configuration.setProperty(JMSEmbeddedConnector.PORT, commandLine.getOptionValue(CMD_PORT));
				configuration.setProperty(TCPSingleServerConnector.PORT, commandLine.getOptionValue(CMD_PORT));
				configuration.setProperty(TCPMultiServerConnector.PORT, commandLine.getOptionValue(CMD_PORT));
				configuration.setProperty(TCPClientConnector.PORT, commandLine.getOptionValue(CMD_PORT));
			}
			if (commandLine.hasOption(CMD_HOST)) {
				configuration.setProperty(TCPClientConnector.HOSTNAME, commandLine.getOptionValue(CMD_HOST));
			}
			if (commandLine.hasOption(CMD_USER)) {
				configuration.setProperty(JMSClientConnector.USERNAME, commandLine.getOptionValue(CMD_USER));
			}
			if (commandLine.hasOption(CMD_PASSWORD)) {
				configuration.setProperty(JMSClientConnector.PASSWORD, commandLine.getOptionValue(CMD_PASSWORD));
			}
			if (commandLine.hasOption(CMD_URL)) {
				configuration.setProperty(JMSClientConnector.URI, commandLine.getOptionValue(CMD_URL));
			}
			if (commandLine.hasOption(CMD_TYPE)) {
				final Reflections reflections = new Reflections("kieker.tools.bridge.connector");
				final Set<Class<?>> connectors = reflections.getTypesAnnotatedWith(kieker.tools.bridge.connector.ConnectorProperty.class);

				for (final Class<?> connector : connectors) {
					if (connector.getAnnotation(kieker.tools.bridge.connector.ConnectorProperty.class).cmdName().equals(commandLine.getOptionValue(CMD_TYPE))) {
						configuration.setProperty(CLI_CONNECTOR, connector.getCanonicalName());
						break;
					}
				}
			}

			// start service depending on type
			final IServiceConnector connector = CLIServerMain.createService(configuration, lookupEntityMap);
			CLIServerMain.getLog().info("Service " + connector.getClass().getAnnotation(kieker.tools.bridge.connector.ConnectorProperty.class).name());
			CLIServerMain.runService(configuration, connector);

		} catch (final ParseException e) {
			CLIServerMain.usage("Parsing failed.  Reason: " + e.getMessage());
			exitCode = 4;
		} catch (final IOException e) {
			CLIServerMain.usage("Mapping file read error: " + e.getMessage());
			exitCode = 1;
		} catch (final CLIConfigurationErrorException e) {
			CLIServerMain.usage("Configuration error: " + e.getMessage());
			exitCode = 2;
		} catch (final ConnectorDataTransmissionException e) {
			CLIServerMain.usage("Communication error: " + e.getMessage());
			exitCode = 3;
		}
		// finally {
		// // The URLClassLoader does not have a close method in Java 1.5
		// // if (classLoader != null) {
		// // try {
		// // classLoader.close();
		// // } catch (final IOException e) {
		// // LOG.error("Classloader failed on close.");
		// // exitCode = 5;
		// // }
		// // }
		// }
		System.exit(exitCode);
	}

	/**
	 * Execute the bridge service.
	 * 
	 * @param connector
	 * 
	 * @throws ConnectorDataTransmissionException
	 *             if an error occured during connector operations
	 */
	private static void runService(final Configuration configuration, final IServiceConnector connector) throws ConnectorDataTransmissionException {
		if (verbose) {
			final String updateIntervalParam = commandLine.getOptionValue(CMD_VERBOSE);
			container.setListenerUpdateInterval((updateIntervalParam != null) ? Long.parseLong(updateIntervalParam) // NOCS
					: ServiceContainer.DEFAULT_LISTENER_UPDATE_INTERVAL); // NOCS
			container.addListener(new IServiceListener() {
				public void handleEvent(final long count, final String message) {
					CLIServerMain.getLog().info("Received " + count + " records");
				}
			});
		}

		if (stats) {
			startTime = System.nanoTime();
		}

		Runtime.getRuntime().addShutdownHook(new Thread() {
			@Override
			public void run() {
				try {
					CLIServerMain.shutdown();
				} catch (final ConnectorDataTransmissionException e) {
					CLIServerMain.getLog().error("Graceful shutdown failed.");
					CLIServerMain.getLog().error("Cause " + e.getMessage());
				}
			}
		});

		// run the service
		container = new ServiceContainer(configuration, connector, false);
		container.run();

		if (stats) {
			deltaTime = System.nanoTime() - startTime;
		}
		if (verbose) {
			CLIServerMain.getLog().info("Server stopped.");
		}
		if (stats) {
			CLIServerMain.getLog().info("Execution time: " + deltaTime + " ns  " + TimeUnit.SECONDS.convert(deltaTime, TimeUnit.NANOSECONDS) + " s");
			CLIServerMain.getLog().info("Time per records: " + (deltaTime / container.getRecordCount()) + " ns/r");
			CLIServerMain.getLog().info("Records per second: " + (container.getRecordCount() / (double) TimeUnit.SECONDS.convert(deltaTime, TimeUnit.NANOSECONDS)));
		}
	}

	/**
	 * Hook for the shutdwon thread so it can access the container's shutdown routine.
	 * 
	 * @throws ConnectorDataTransmissionException
	 *             if any internal shutdown calls fail
	 */
	protected static void shutdown() throws ConnectorDataTransmissionException {
		container.shutdown();
	}

	/**
	 * Create a record map of classes implementing IMonitoringRecord interface out of libraries with such classes and a textual mapping file.
	 * 
	 * @return A record map. null is never returned as a call of usage terminates the program.
	 * @throws IOException
	 *             if an error occured reading the mapping file
	 * @throws CLIConfigurationErrorException
	 *             if a configuration error occured
	 */
	private static ConcurrentMap<Integer, Class<? extends IMonitoringRecord>> createRecordMap() throws IOException, CLIConfigurationErrorException {
		if (commandLine.hasOption("L")) {
			final String[] libraries = commandLine.getOptionValues("L");

			if (commandLine.hasOption(CMD_MAP_FILE)) {
				final ConcurrentMap<Integer, Class<? extends IMonitoringRecord>> recordMap = CLIServerMain.readMapping(libraries,
						commandLine.getOptionValue(CMD_MAP_FILE));
				if (recordMap.isEmpty()) {
					throw new CLIConfigurationErrorException("At least one mapping must be specified in the mapping file.");
				}
				return recordMap;
			} else {
				throw new CLIConfigurationErrorException("Mapping file is required.");
			}
		} else {
			throw new CLIConfigurationErrorException("At least one library reference is required.");
		}
	}

	/**
	 * Interpret command line type option.
	 * 
	 * @param lookupEntityMap
	 *            the map for ids to Kieker records
	 * 
	 * @return a reference to an ServiceContainer
	 * @throws CLIConfigurationErrorException
	 *             if an error occured in setting up a connector or if an unknown service connector was specified
	 * @throws ConnectorDataTransmissionException
	 */
	private static IServiceConnector createService(final Configuration configuration, final ConcurrentMap<Integer, LookupEntity> lookupEntityMap)
			throws CLIConfigurationErrorException, ConnectorDataTransmissionException {
		try {
			return CLIServerMain.createConnector(ClassLoader.getSystemClassLoader().loadClass(configuration.getStringProperty(CLI_CONNECTOR)), configuration,
					lookupEntityMap);
		} catch (final ClassNotFoundException e) {
			throw new CLIConfigurationErrorException("Specified bridge connector " + configuration.getStringProperty(CLI_CONNECTOR) + " not found.", e);
		}
	}

	/**
	 * Create a connector instance for the given class.
	 * 
	 * @param connector
	 *            the connector class
	 * @param configuration
	 *            configuration for the connector
	 * @param lookupEntityMap
	 *            lookup map
	 * @return o connector instance
	 * @throws CLIConfigurationErrorException
	 *             when the instantiation fails
	 */
	private static IServiceConnector createConnector(final Class<?> connector, final Configuration configuration,
			final ConcurrentMap<Integer, LookupEntity> lookupEntityMap) throws CLIConfigurationErrorException {
		try {
			return (IServiceConnector) connector.getConstructor(Configuration.class, ConcurrentMap.class).newInstance(configuration, lookupEntityMap);
		} catch (final IllegalArgumentException e) {
			throw new CLIConfigurationErrorException("Broken implementation of the selected connector " + connector.getCanonicalName(), e);
		} catch (final SecurityException e) {
			throw new CLIConfigurationErrorException("Cannot access included classes " + connector.getCanonicalName(), e);
		} catch (final InstantiationException e) {
			throw new CLIConfigurationErrorException("Instantiation failed for " + connector.getCanonicalName(), e);
		} catch (final IllegalAccessException e) {
			throw new CLIConfigurationErrorException("Access prohibited to class " + connector.getCanonicalName(), e);
		} catch (final InvocationTargetException e) {
			throw new CLIConfigurationErrorException("Broken implementation of the selected connector " + connector.getCanonicalName(), e);
		} catch (final NoSuchMethodException e) {
			throw new CLIConfigurationErrorException("Broken implementation of the selected connector " + connector.getCanonicalName(), e);
		}
	}

	/**
	 * Print out the server usage and an additional message describing the cause of the failure. Finally terminate the server.
	 * 
	 * @param message
	 *            the message to be printed
	 * @param code
	 *            the exit code
	 */
	private static void usage(final String message) {
		CLIServerMain.getLog().error(message);
		final HelpFormatter formatter = new CLIHelpFormatter();
		formatter.printHelp("cli-kieker-service", options, true);
	}

	/**
	 * Read the CLI server Kieker classes to id mapping file.
	 * 
	 * @param libraries
	 *            array representing a list of library files (*.jar)
	 * @param filename
	 *            the path of the mapping file.
	 * 
	 * @return a complete IMonitoringRecord to id mapping
	 * @throws IOException
	 *             If one or more of the given library URLs is somehow invalid or one of the given files could not be accessed.
	 */
	@SuppressWarnings("unchecked")
	private static ConcurrentMap<Integer, Class<? extends IMonitoringRecord>> readMapping(final String[] libraries, final String filename) throws IOException {
		final ConcurrentMap<Integer, Class<? extends IMonitoringRecord>> map = new ConcurrentHashMap<Integer, Class<? extends IMonitoringRecord>>();
		final URL[] urls = new URL[libraries.length];
		for (int i = 0; i < libraries.length; i++) {
			urls[i] = new File(libraries[i]).toURI().toURL();
		}

		final PrivilegedClassLoaderAction action = new PrivilegedClassLoaderAction(urls);
		classLoader = AccessController.doPrivileged(action);

		BufferedReader in = null;
		try {
			in = new BufferedReader(new InputStreamReader(new FileInputStream(filename), "UTF-8"));
			String line = null;
			do {
				try {
					line = in.readLine();
					if (line != null) {
						final String[] pair = line.split("=");
						if (pair.length == 2) {
							// loadClass returns objects of type Class<?> while we only accept Class<? extends IMonitoringRecord> at the moment. This causes an
							// warning which
							// is suppressed by the SuppressWarning annotation.
							map.put(Integer.parseInt(pair[0]), (Class<IMonitoringRecord>) classLoader.loadClass(pair[1]));
						}
					}
				} catch (final ClassNotFoundException e) {
					CLIServerMain.getLog().warn("Could not load class", e);
				}
			} while (line != null);
		} finally {
			if (in != null) {
				in.close();
			}
		}

		return map;
	}

	/**
	 * Compile the options for the CLI server.
	 * 
	 * @return The composed options for the CLI server
	 */
	private static Options declareOptions() {
		options = new Options();
		Option option;

		// Type selection
		option = new Option(CMD_TYPE, CMD_TYPE_LONG, true, "select the service type: tcp-client, tcp-server, tcp-single-server, jms-client, jms-embedded");
		option.setArgName("type");
		option.setRequired(true);
		options.addOption(option);

		// TCP client
		option = new Option(CMD_HOST, CMD_HOST_LONG, true, "connect to server named <hostname>");
		option.setArgName("hostname");
		options.addOption(option);

		// TCP server
		option = new Option(CMD_PORT, CMD_PORT_LONG, true, "listen at port (tcp-server or jms-embedded) or connect to port (tcp-client)");
		option.setArgName("number");
		option.setType(Number.class);
		options.addOption(option);

		// JMS client
		option = new Option(CMD_USER, CMD_USER_LONG, true, "user name for a JMS service");
		option.setArgName("username");
		options.addOption(option);
		option = new Option(CMD_PASSWORD, CMD_PASSWORD_LONG, true, "password for a JMS service");
		option.setArgName("password");
		options.addOption(option);
		option = new Option(CMD_URL, CMD_URL_LONG, true, "URL for JMS server");
		option.setArgName("jms-url");
		option.setType(URL.class);
		options.addOption(option);

		// kieker configuration file
		option = new Option(CMD_KIEKER_CONFIGURATION, CMD_KIEKER_CONFIGURATION_LONG, true, "kieker configuration file");
		option.setArgName("configuration");
		options.addOption(option);

		// mapping file for TCP and JMS
		option = new Option(CMD_MAP_FILE, CMD_MAP_FILE_LONG, true, "Class name to id (integer or string) mapping");
		option.setArgName("map-file");
		option.setType(File.class);
		option.setRequired(true);
		options.addOption(option);

		// libraries
		option = new Option(CMD_LIBRARIES, CMD_LIBRARIES_LONG, true, "List of library paths separated by " + File.pathSeparatorChar);
		option.setArgName("paths");
		option.setType(File.class);
		option.setRequired(true);
		option.setValueSeparator(File.pathSeparatorChar);
		options.addOption(option);

		// verbose
		option = new Option(CMD_VERBOSE, CMD_VERBOSE_LONG, true, "output processing information");
		option.setRequired(false);
		option.setOptionalArg(true);
		options.addOption(option);

		// statistics
		option = new Option(CMD_STATS, CMD_STATS_LONG, false, "output performance statistics");
		option.setRequired(false);
		options.addOption(option);

		// daemon mode
		option = new Option(CMD_DAEMON, CMD_DAEMON_LONG, false, "detach from console; TCP server allows multiple connections");
		option.setRequired(false);
		options.addOption(option);

		return options;
	}

	/**
	 * Check for pid file.
	 * 
	 * @return A pid file object
	 * @throws IOException
	 *             if file definition fails or the file already exists
	 */
	private static File getPidFile() throws IOException {
		File pidFile = null;
		if (System.getProperty(DAEMON_FILE) != null) {
			pidFile = new File(System.getProperty(DAEMON_FILE));
		} else {
			if (new File(System.getProperty(JAVA_TMP_DIR)).exists()) {
				pidFile = new File(System.getProperty(JAVA_TMP_DIR) + "/kdb.pid");
			} else {
				throw new IOException("Java temp directory " + System.getProperty(JAVA_TMP_DIR) + " does not exist.");
			}
		}
		if (pidFile.exists()) {
			throw new IOException("PID file " + pidFile.getCanonicalPath() + " already exists, indicating the service is already running.");
		}

		return pidFile;
	}

	/**
	 * Return the logger.
	 * 
	 * @return Returns the logger
	 */
	public static Log getLog() {
		return LOG;
	}
}
