//	---------------------------------------------------------------------------
//	jWebSocket - Copyright (c) 2010 jwebsocket.org
//	---------------------------------------------------------------------------
//	This program is free software; you can redistribute it and/or modify it
//	under the terms of the GNU Lesser General Public License as published by the
//	Free Software Foundation; either version 3 of the License, or (at your
//	option) any later version.
//	This program is distributed in the hope that it will be useful, but WITHOUT
//	ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
//	FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for
//	more details.
//	You should have received a copy of the GNU Lesser General Public License along
//	with this program; if not, see <http://www.gnu.org/licenses/lgpl.html>.
//	---------------------------------------------------------------------------
package org.jwebsocket.factory;

import org.apache.log4j.Logger;
import org.jwebsocket.logging.Logging;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.net.MalformedURLException;
import java.util.List;
import java.util.Map;
import javolution.util.FastList;
import javolution.util.FastMap;

import org.jwebsocket.api.EngineConfiguration;
import org.jwebsocket.api.ServerConfiguration;
import org.jwebsocket.api.WebSocketEngine;
import org.jwebsocket.api.WebSocketFilter;
import org.jwebsocket.api.WebSocketInitializer;
import org.jwebsocket.api.WebSocketPlugIn;
import org.jwebsocket.api.WebSocketServer;
import org.jwebsocket.config.JWebSocketConfig;
import org.jwebsocket.config.LoggingConfig;
import org.jwebsocket.config.xml.EngineConfig;
import org.jwebsocket.config.xml.FilterConfig;
import org.jwebsocket.config.xml.PluginConfig;
import org.jwebsocket.config.xml.ServerConfig;

/**
 * Intialize the engine, servers and plugins based on jWebSocket.xml
 * configuration
 * 
 * @author puran
 * @version $Id: JWebSocketXmlConfigInitializer.java 424 2010-05-01 19:11:04Z
 *          mailtopuran $
 * 
 */
public final class JWebSocketXmlConfigInitializer implements
		WebSocketInitializer {

	// don't initialize logger here! Will be initialized with loaded settings!
	private static Logger mLog = null;
	private final JWebSocketJarClassLoader mClassLoader = new JWebSocketJarClassLoader();
	private JWebSocketConfig mConfig;

	/**
	 * private constructor
	 */
	private JWebSocketXmlConfigInitializer(JWebSocketConfig aConfig) {
		this.mConfig = aConfig;
	}

	/**
	 * Returns the initializer object
	 * 
	 * @param aConfig
	 *            the jWebSocket config
	 * @return the initializer object
	 */
	public static JWebSocketXmlConfigInitializer getInitializer(
			JWebSocketConfig aConfig) {
		return new JWebSocketXmlConfigInitializer(aConfig);
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void initializeLogging() {
		LoggingConfig lLoggingConfig = mConfig.getLoggingConfig();
		// initialize log4j logging engine
		// BEFORE instantiating any jWebSocket classes
		Logging.initLogs(lLoggingConfig.getLevel(), lLoggingConfig.getAppender(),
				lLoggingConfig.getFilename(), lLoggingConfig.getPattern(),
				lLoggingConfig.getBufferSize(),
				new String[]{"%JWEBSOCKET_HOME%/logs", "%CATALINA_HOME%/logs"});
		mLog = Logging.getLogger(JWebSocketXmlConfigInitializer.class);
		if (mLog.isDebugEnabled()) {
			mLog.debug("Logging settings"
					+ ": appender: " + lLoggingConfig.getAppender()
					+ ", filename: " + lLoggingConfig.getFilename()
					+ ", level: " + lLoggingConfig.getLevel()
					+ ", buffersize: " + lLoggingConfig.getBufferSize()
					+ ", pattern: " + lLoggingConfig.getPattern());
		}
		if (mLog.isDebugEnabled()) {
			mLog.debug("Starting jWebSocket Server Sub System...");
		}
	}

	/**
	 * {@inheritDoc}
	 */
	@SuppressWarnings("unchecked")
	@Override
	public WebSocketEngine initializeEngine() {
		WebSocketEngine newEngine = null;
		EngineConfig engineConfig = mConfig.getEngines().get(0);
		String jarFilePath = "-";
		try {
			Class lEngineClass = null;

			// try to load engine from classpath first, 
			// could be located in server bundle
			try {
				lEngineClass = Class.forName(engineConfig.getName());
				if (mLog.isDebugEnabled()) {
					mLog.debug("Engine '" + engineConfig.getName() + "' loaded from classpath.");
				}
			} catch (ClassNotFoundException ex) {
				// in case of a class not found exception we DO NOT want to
				// show the exception but subsequently load the class from
				if (mLog.isDebugEnabled()) {
					mLog.debug("Engine '" + engineConfig.getName() + "' not yet in classpath, hence trying to load from file...");
				}
			}

			// if not in classpath...
			// try to load engine from given .jar file
			if (lEngineClass == null) {
				jarFilePath = JWebSocketConfig.getLibraryFolderPath(engineConfig.getJar());
				// jarFilePath may be null if .jar is included in server bundle
				if (jarFilePath != null) {
					if (mLog.isDebugEnabled()) {
						mLog.debug("Loading engine '" + engineConfig.getName() + "' from '" + jarFilePath + "'...");
					}
					mClassLoader.addFile(jarFilePath);
					lEngineClass = (Class<WebSocketEngine>) mClassLoader.loadClass(engineConfig.getName());
				}
			}

			// if class found
			// try to create an instance
			if (lEngineClass != null) {
				Constructor<WebSocketEngine> ctor = lEngineClass.getDeclaredConstructor(EngineConfiguration.class);
				ctor.setAccessible(true);
				newEngine = ctor.newInstance(new Object[]{engineConfig});
				if (mLog.isDebugEnabled()) {
					mLog.debug("Engine '" + engineConfig.getId() + "' successfully instantiated.");
				}
			} else {
				mLog.error("jWebSocket engine class " + engineConfig.getName() + " could not be loaded.");
			}
		} catch (MalformedURLException e) {
			mLog.error("Couldn't load the jar file for engine, make sure jar file exists or name is correct", e);
		} catch (ClassNotFoundException e) {
			mLog.error("Engine class '" + engineConfig.getName() + "'@'" + jarFilePath + "' not found", e);
		} catch (InstantiationException e) {
			mLog.error("Engine class could not be instantiated", e);
		} catch (IllegalAccessException e) {
			mLog.error("Illegal Access Exception while intializing engine", e);
		} catch (NoSuchMethodException e) {
			mLog.error("No Constructor found with given 3 arguments", e);
		} catch (InvocationTargetException e) {
			mLog.error("Exception invoking engine object", e);
		}

		return newEngine;
	}

	/**
	 * {@inheritDoc}
	 */
	@SuppressWarnings("unchecked")
	@Override
	public List<WebSocketServer> initializeServers() {
		List<WebSocketServer> lServers = new FastList<WebSocketServer>();
		List<ServerConfig> lServerConfigs = mConfig.getServers();
		for (ServerConfig lServerConfig : lServerConfigs) {
			WebSocketServer lServer = null;
			String lJarFilePath = "-";
			try {
				Class lServerClass = null;

				// try to load server from classpath first,
				// could be located in server bundle
				try {
					lServerClass = Class.forName(lServerConfig.getName());
					if (mLog.isDebugEnabled()) {
						mLog.debug("Server '" + lServerConfig.getName() + "' loaded from classpath.");
					}
				} catch (ClassNotFoundException ex) {
					// in case of a class not found exception we DO NOT want to
					// show the exception but subsequently load the class from
					if (mLog.isDebugEnabled()) {
						mLog.debug("Server '" + lServerConfig.getName() + "' not yet in classpath, hence trying to load from file...");
					}
				}

				// if not in classpath...
				// try to load server from given .jar file
				if (lServerClass == null) {
					lJarFilePath = JWebSocketConfig.getLibraryFolderPath(lServerConfig.getJar());
					// jarFilePath may be null if .jar is included in server bundle
					if (lJarFilePath != null) {
						if (mLog.isDebugEnabled()) {
							mLog.debug("Loading server '" + lServerConfig.getName() + "' from '" + lJarFilePath + "'...");
						}
						mClassLoader.addFile(lJarFilePath);
						lServerClass = (Class<WebSocketServer>) mClassLoader.loadClass(lServerConfig.getName());
					}
				}

				// if class found
				// try to create an instance
				if (lServerClass != null) {
					Constructor<WebSocketServer> ctor = lServerClass.getDeclaredConstructor(ServerConfiguration.class);
					ctor.setAccessible(true);
					lServer = ctor.newInstance(new Object[]{lServerConfig});
					if (mLog.isDebugEnabled()) {
						mLog.debug("Server '" + lServerConfig.getId() + "' successfully instantiated.");
					}
					// add the initialized server to the list
					lServers.add(lServer);
				} else {
					mLog.error("jWebSocket server class " + lServerConfig.getName() + " could not be loaded.");
				}
			} catch (MalformedURLException e) {
				mLog.error(
						"Couldn't load the jar file for server, make sure jar file '" + lJarFilePath + "' exists and name is correct.",
						e);
			} catch (ClassNotFoundException e) {
				mLog.error(
						"Server class '" + lServerConfig.getName() + "'@'" + lJarFilePath + "' not found.", e);
			} catch (InstantiationException e) {
				mLog.error(
						"Server class '" + lServerConfig.getName() + "' could not be instantiated.", e);
			} catch (IllegalAccessException e) {
				mLog.error(
						"Illegal Access Exception while intializing server '" + lServerConfig.getName() + "'.", e);
			} catch (NoSuchMethodException e) {
				mLog.error(
						"No constructor found with given 1 arguments", e);
			} catch (InvocationTargetException e) {
				mLog.error(
						"Exception invoking server object.", e);
			}
		}
		return lServers;
	}

	/**
	 * {@inheritDoc}
	 */
	@SuppressWarnings("unchecked")
	@Override
	public Map<String, List<WebSocketPlugIn>> initializePlugins() {
		Map<String, List<WebSocketPlugIn>> lPluginMap = new FastMap<String, List<WebSocketPlugIn>>();

		// populate the plugin FastMap with server id and empty list
		for (ServerConfig lServerConfig : mConfig.getServers()) {
			lPluginMap.put(lServerConfig.getId(), new FastList<WebSocketPlugIn>());
		}
		// now initialize the pluin
		for (PluginConfig lPluginConfig : mConfig.getPlugins()) {
			try {
				Class lPluginClass = null;

				// try to load plug-in from classpath first,
				// could be located in server bundle
				try {
					lPluginClass = Class.forName(lPluginConfig.getName());
					if (mLog.isDebugEnabled()) {
						mLog.debug("Plug-in '" + lPluginConfig.getName() + "' loaded from classpath.");
					}
				} catch (ClassNotFoundException ex) {
					// in case of a class not found exception we DO NOT want to
					// show the exception but subsequently load the class from
					if (mLog.isDebugEnabled()) {
						mLog.debug("Plug-in '" + lPluginConfig.getName() + "' not yet in classpath, hence trying to load from file...");
					}
				}

				// if not in classpath...
				// try to load plug-in from given .jar file
				if (lPluginClass == null) {
					String jarFilePath = JWebSocketConfig.getLibraryFolderPath(lPluginConfig.getJar());
					// jarFilePath may be null if .jar is included in server bundle
					if (jarFilePath != null) {
						mClassLoader.addFile(jarFilePath);
						if (mLog.isDebugEnabled()) {
							mLog.debug("Loading plug-in '" + lPluginConfig.getName() + "' from '" + jarFilePath + "'...");
						}
						lPluginClass = (Class<WebSocketPlugIn>) mClassLoader.loadClass(lPluginConfig.getName());
					}
				}

				// if class found
				// try to create an instance
				if (lPluginClass != null) {
					/*
					Constructor<WebSocketPlugIn> ctor = pluginClass.getDeclaredConstructor();
					ctor.setAccessible(true);
					Object lObj = ctor.newInstance(new Object[]{});
					log.debug("lObj.classname = " + lObj.getClass().getName());
					Object plugin = null;
					try {
					plugin = lObj;
					log.info(
					"lObj instanceof WebSocketPlugIn " + ( lObj instanceof WebSocketPlugIn ? "YES" : "NO" )
					);
					} catch (Exception ex) {
					log.error(
					ex.getClass().getSimpleName() + " while instantiating class '" + pluginConfig.getName() + "'.");
					}
					 */
					WebSocketPlugIn lPlugIn = (WebSocketPlugIn) lPluginClass.newInstance();
					// TODO: Also set id and name once these are available
					lPlugIn.addAllSettings(lPluginConfig.getSettings());

					if (mLog.isDebugEnabled()) {
						mLog.debug("Plug-in '" + lPluginConfig.getId() + "' successfully instantiated.");
					}

					// now add the plugin to plugin map based on server ids
					for (String lServerId : lPluginConfig.getServers()) {
						List<WebSocketPlugIn> lPlugIns = lPluginMap.get(lServerId);
						if (lPlugIns != null) {
							lPlugIns.add((WebSocketPlugIn) lPlugIn);
						}
					}
				}

			} catch (MalformedURLException ex) {
				mLog.error(
						"Couldn't load the jar file for plugin, make sure the jar file exists and the name is correct.", ex);
			} catch (ClassNotFoundException ex) {
				mLog.error(
						"Plugin class '" + lPluginConfig.getName() + "' not found.", ex);
			} catch (InstantiationException ex) {
				mLog.error(
						"Plugin class '" + lPluginConfig.getName() + "' could not be instantiated.", ex);
			} catch (IllegalAccessException ex) {
				mLog.error(
						"Illegal Access Exception while instantiating plugin.", ex);
			} catch (Exception ex) {
				mLog.error(
						ex.getClass().getSimpleName() + " while instantiating plugin '" + lPluginConfig.getName() + "'.", ex);
			}
		}
		return lPluginMap;
	}

	/**
	 * {@inheritDoc}
	 */
	@SuppressWarnings("unchecked")
	@Override
	public Map<String, List<WebSocketFilter>> initializeFilters() {
		Map<String, List<WebSocketFilter>> lFilterMap = new FastMap<String, List<WebSocketFilter>>();

		// populate the filter FastMap with server id and empty list
		for (ServerConfig lServerConfig : mConfig.getServers()) {
			lFilterMap.put(lServerConfig.getId(),
					new FastList<WebSocketFilter>());
		}
		// now initialize the filter
		for (FilterConfig lFilterConfig : mConfig.getFilters()) {
			try {
				Class lFilterClass = null;

				// try to load filter from classpath first,
				// could be located in server bundle
				try {
					lFilterClass = Class.forName(lFilterConfig.getName());
					if (mLog.isDebugEnabled()) {
						mLog.debug("Filter '" + lFilterConfig.getName() + "' loaded from classpath.");
					}
				} catch (ClassNotFoundException ex) {
					// in case of a class not found exception we DO NOT want to
					// show the exception but subsequently load the class from
					if (mLog.isDebugEnabled()) {
						mLog.debug("Filter '" + lFilterConfig.getName() + "' not yet in classpath, hence trying to load from file...");
					}
				}

				// if not in classpath...
				// try to load plug-in from given .jar file
				if (lFilterClass == null) {
					String jarFilePath = JWebSocketConfig.getLibraryFolderPath(lFilterConfig.getJar());
					// jarFilePath may be null if .jar is included in server bundle
					if (jarFilePath != null) {
						mClassLoader.addFile(jarFilePath);
						if (mLog.isDebugEnabled()) {
							mLog.debug("Loading filter '" + lFilterConfig.getName() + "' from '" + jarFilePath + "'...");
						}
						lFilterClass = (Class<WebSocketFilter>) mClassLoader.loadClass(lFilterConfig.getName());
					}
				}

				// if class found
				// try to create an instance
				if (lFilterClass != null) {
					Constructor<WebSocketFilter> lConstr = lFilterClass.getDeclaredConstructor(String.class);
					lConstr.setAccessible(true);

					WebSocketFilter lFilter = lConstr.newInstance(new Object[]{lFilterConfig.getId()});
					// TODO: Also set settings, id and name once these are available
					// filter.addAllSettings(filterConfig.getSettings());

					if (mLog.isDebugEnabled()) {
						mLog.debug("Filter '" + lFilterConfig.getName() + "' successfully instantiated.");
					}
					// now add the filter to filter FastMap based on server ids
					for (String lServerId : lFilterConfig.getServers()) {
						List<WebSocketFilter> lFilters = lFilterMap.get(lServerId);
						if (lFilters != null) {
							lFilters.add(lFilter);
						}
					}
				}

			} catch (MalformedURLException e) {
				mLog.error(
						"Couldn't Load the jar file for filter, make sure jar file exists and name is correct.",
						e);
			} catch (ClassNotFoundException e) {
				mLog.error(
						"Filter class not found.", e);
			} catch (InstantiationException e) {
				mLog.error(
						"Filter class could not be instantiated.", e);
			} catch (IllegalAccessException e) {
				mLog.error(
						"Illegal Access Exception while intializing filter.", e);
			} catch (NoSuchMethodException e) {
				mLog.error(
						"No Constructor found with given 3 arguments.", e);
			} catch (InvocationTargetException e) {
				mLog.error(
						"Exception invoking filter object.", e);
			}
		}
		return lFilterMap;
	}

}
