/*
 * Copyright 2010-2012 VMware and contributors
 *
 * 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 org.springsource.loaded;

import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.springsource.loaded.agent.SpringPlugin;

/**
 * Encapsulates configurable elements - these are set (to values other than the defaults) in TypeRegistry when the system property
 * springloaded configuration is processed. Some of the options are only used by testcases to make the testcases easier to write and
 * more straightforward.
 * 
 * @author Andy Clement
 * @since 0.5.0
 */
public class GlobalConfiguration {

	private static Logger log = Logger.getLogger(GlobalConfiguration.class.getName());

	/**
	 * Are references to fields being modified - covering both the GETS/SETS and the reflective references.
	 */
	public final static boolean fieldRewriting = true;

	public static boolean catchersOn = true;

	/**
	 * If active, SpringLoaded will be trying to watch for types changing on the file system once they have been made reloadable.
	 */
	public static boolean fileSystemMonitoring = false;

	/**
	 * Global control for loadtime logging
	 */
	public static boolean logging = false;

	/**
	 * verbose mode can trigger extra messages. Enable with 'verbose=true'
	 */
	public static boolean verboseMode = false;
	
	/**
	 * asserts mode will trigger extra checking (performance impact but confirms correctness)
	 */
	public static boolean assertsMode = false;
	
	/**
	 * Can be turned on to enable users to determine the decision process around why
	 * something is not reloadable.
	 */
	public static boolean explainMode = false;

	/**
	 * Global control for runtime logging
	 */
	public static boolean isRuntimeLogging = false;

	public static boolean callsideRewritingOn = true;

	/**
	 * Allows a cache to be cleaned up as the agent starts (effectively starting with a new cache, if 'caching' is true)
	 */
	public static boolean cleanCache = false;

	/**
	 * Determine whether on disk caching will be used.
	 */
	public static boolean isCaching = false;
	
	public static boolean investigateSystemClassReflection = false;
	public static boolean rewriteAllSystemClasses = false;

	/**
	 * A well known profile (e.g. grails) can tweak a lot of the default options in a particular way.
	 */
	public static String profile = null;

	/**
	 * The base directory in which to create any cache (.slcache folder). If null then user.home will be used.
	 */
	public static String cacheDir = null;

	public final static boolean logNonInterceptedReflectiveCalls = false;

	/**
	 * Global control for checking assertions
	 */
	public final static boolean isProfiling = false;

	public static boolean directlyDefineTypes = true;

	public final static boolean interceptReflection = true;
	
	// max number of values before we prevent them being reloaded (the clinit rewrite blows the codesize limit)
	public static int enumLimit = 1000;

	public static boolean reloadMessages = false;// can be forced on for testing

	/**
	 * When a reload is attempted, if this is true it will be checked to confirm it is allowed and does not violate the supported
	 * reloadable changes that can be made to a type.
	 */
	public static boolean verifyReloads = true;

	/**
	 * When classes are dumped by Utils.dump() this specifies where. A null value will cause us to dump into the default temp
	 * folder.
	 */
	public static String dumpFolder = null;

	/**
	 * Global configuration properties set based on the value of system property 'springloaded'. If null then not yet initialized
	 * (and a call to initializeFromSystemProperty()) is needed. If settings are truely once per VM, they are set directly in
	 * GlobalConfiguration whereas if they may be overridden on a per classloader level, they are set in this properties object and
	 * may be overridden by the springloaded.properties files accessible through each classloader.
	 */
	public static Properties globalConfigurationProperties;

	/**
	 * List of slashed classnames for types we should 'dump' during processing (for debugging purposes).
	 */
	public static List<String> classesToDump;

	public static int maxClassDefinitions = 100;

	/**
	 * List of dotted classnames representing classnames of plugins that should be loaded.
	 */
	public static List<String> pluginClassnameList;

	public final static boolean debugplugins;

	
	private static void printUsage() {
		System.out.println("SpringLoaded");
		System.out.println("============");
		System.out.println();
		System.out.println("Usage: java -noverify -javaagent:<pathto>/springloaded.jar");
		System.out.println("Optionally specify configuration through -Dspringloaded=<options>");
		System.out.println("<options> is a ';' separated list of directives or name=value options");
		System.out.println("Example: -Dspringloaded=verbose;cacheDir=/tmp");
		System.out.println();
		System.out.println("Directives:");
		System.out.println("  ? - print this usage text");
		System.out.println("  verbose - the reloader will log important lifecycle events");
		System.out.println("Options:");
		System.exit(0);
	}
	
	/**
	 * Look for a springloaded system property and initialize the 'default system wide' configuration based upon it.
	 * Support configuration options:
	 * <ul>
	 * <li><tt>info</tt> - print usage information on the options
	 * <li><tt>verbose</tt> - this directive causes SpringLoaded to report on decisions it is making.
	 * </ul>
	 */
	static {
		globalConfigurationProperties = new Properties();
		boolean debugPlugins = false;
		try {
			boolean specifiedCaching = false;
			String value = System.getProperty("springloaded");
			// value is a ';' separated list of configuration options which either may be name=value settings or directives (just a name)
			if (value != null) {
				StringTokenizer st = new StringTokenizer(value, ";");
				while (st.hasMoreTokens()) {
					String kv = st.nextToken();
					int equals = kv.indexOf('=');
					if (equals != -1) {
						// key=value
						String key = kv.substring(0, equals);
						// Supported settings:

						// dump=XX,YYY,ZZZ
						// - this option lists classes for which we should dump the bytecode, names are dotted
						if (key.equals("dump")) {
							String classList = kv.substring(equals + 1);
							StringTokenizer clSt = new StringTokenizer(classList, ",");
							classesToDump = new ArrayList<String>();
							while (clSt.hasMoreTokens()) {
								classesToDump.add(clSt.nextToken().replace('.', '/'));
							}
							if (isRuntimeLogging && log.isLoggable(Level.INFO)) {
								log.info("configuration: dumping: " + classesToDump);
							}
							//						} else if (key.equals("interceptReflection")) { // global setting
							//							interceptReflection = kv.substring(equals + 1).equalsIgnoreCase("true");
							//							if (isRuntimeLogging && log.isLoggable(Level.INFO)) {
							//								log.info("configuration: interceptReflection = " + interceptReflection);
							//							}
						} else if (key.equals("cleanCache")) {
							cleanCache = kv.substring(equals + 1).equalsIgnoreCase("true");
						} else if (key.equals("caching")) {
							specifiedCaching = true;
							isCaching = kv.substring(equals + 1).equalsIgnoreCase("true");
						} else if (key.equals("debugplugins")) {
							debugPlugins = true;
						}
						else if(key.equals("enumlimit")) {
							enumLimit = toInt(kv.substring(equals+1),enumLimit);
						} else if (key.equals("profile")) {
							profile = kv.substring(equals + 1);
						} else if (key.equals("cacheDir")) {
							cacheDir = kv.substring(equals + 1);
						} else if (key.equals("callsideRewritingOn")) { // global setting
							callsideRewritingOn = kv.substring(equals + 1).equalsIgnoreCase("true");
							if (isRuntimeLogging && log.isLoggable(Level.INFO)) {
								log.info("configuration: callsideRewritingOn = " + callsideRewritingOn);
							}
							//						} else if (key.equals("logNonInterceptedReflectiveCalls")) { // global setting
							//							logNonInterceptedReflectiveCalls = kv.substring(equals + 1).equalsIgnoreCase("true");
							//							if (isRuntimeLogging && log.isLoggable(Level.INFO)) {
							//								log.info("configuration: logNonInterceptedReflectiveCalls = " + logNonInterceptedReflectiveCalls);
							//							}
						} else if (key.equals("verifyReloads")) { // global setting
							verifyReloads = kv.substring(equals + 1).equalsIgnoreCase("true");
							if (isRuntimeLogging && log.isLoggable(Level.INFO)) {
								log.info("configuration: verifyReloads = " + verifyReloads);
							}
						} else if (key.equals("dumpFolder")) { // global setting
							dumpFolder = kv.substring(equals + 1);
							if (isRuntimeLogging && log.isLoggable(Level.INFO)) {
								log.info("configuration: dumpFolder = " + dumpFolder);
							}
						} else if (key.equals("maxClassDefinitions")) {
							try {
								maxClassDefinitions = Integer.parseInt(kv.substring(equals + 1));
								if (isRuntimeLogging && log.isLoggable(Level.INFO)) {
									log.info("configuration: maxClassDefinitions = " + maxClassDefinitions);
								}
							} catch (NumberFormatException nfe) {
								System.err.println("ERROR: unable to parse " + kv.substring(equals + 1) + " as a integer");
							}
						} else if (key.equals("logging")) {
							GlobalConfiguration.isRuntimeLogging = kv.substring(equals + 1).equalsIgnoreCase("true");
							GlobalConfiguration.logging = kv.substring(equals + 1).equalsIgnoreCase("true");
							System.out.println("Spring-Loaded logging = (" + GlobalConfiguration.isRuntimeLogging + ","
									+ GlobalConfiguration.logging + ")");
						} else if (key.equals("verbose")) {
							verboseMode = kv.substring(equals + 1).equalsIgnoreCase("true");
							reloadMessages = verboseMode;
						}
						else if (key.equals("asserts")) {
							assertsMode = kv.substring(equals + 1).equalsIgnoreCase("true");
						}
						else if (key.equals("rebasePaths")) {
							// value is a series of "a=b,c=d,e=f" indicating from and to
							globalConfigurationProperties.put("rebasePaths", kv.substring(equals + 1));
						} else if (key.equals("inclusions")) {
							globalConfigurationProperties.put("inclusions", kv.substring(equals + 1));
						} else if (key.equals("exclusions")) {
							globalConfigurationProperties.put("exclusions", kv.substring(equals + 1));
						} else if (key.equals("plugins")) {
							// plugins=com.myplugin.Plugin,com.somethingelse.SomeOtherPlugin
							String pluginList = kv.substring(equals + 1);
							StringTokenizer pluginListTokenizer = new StringTokenizer(pluginList, ",");
							pluginClassnameList = new ArrayList<String>();
							while (pluginListTokenizer.hasMoreTokens()) {
								pluginClassnameList.add(pluginListTokenizer.nextToken());
							}
						}
					} else {
						if (kv.equals("?")) {
							printUsage();
						}
						else if (kv.equals("verbose")) {
							Log.log("[verbose mode on] Full configuration is:"+value);
							verboseMode = true;
							reloadMessages = true;
						}
						else if (kv.equals("investigateSystemClassReflection")) {
							investigateSystemClassReflection = true;
						}
						else if (kv.equals("rewriteAllSystemClasses")) {
							rewriteAllSystemClasses = true;
						}
						else if (kv.equals("asserts")) {
							Log.log("[asserts mode on] Will verify system coherence");
							assertsMode = true;
						}
						else if (kv.equals("explain")) {
							Log.log("[explain mode on] Reporting on the decision making process within SpringLoaded");
							explainMode = true;
						}
					}
				}
			}

			// Profile support.  A profile is a shortcut for configuring a bunch of options
			if (profile != null) {
				if (profile.equals("grails")) {
					// Configure options based on a grails profile
					// turn on caching if we have a cacheDir set or can put one in the .grails folder under user.home
					if (cacheDir == null) {
						try {
							String userhome = System.getProperty("user.home");
							if (userhome != null) {
								cacheDir = new StringBuilder(userhome).append(File.separator).append(".grails").toString();
								new File(cacheDir).mkdir();
							}
						} catch (Throwable t) {
							System.err.println("looks like user.home is not set, or cannot write to it: cannot create cache.");
							t.printStackTrace(System.err);
						}
					}
					if (!specifiedCaching) {
						if (cacheDir != null) {
							isCaching = true;
						}
					}
					if (pluginClassnameList == null) {
						pluginClassnameList = new ArrayList<String>();
					}
					pluginClassnameList.add("org.springsource.loaded.SystemPropertyConfiguredIsReloadableTypePlugin");
					// turn off the 3.0 reloading, for now (just because it hasn't been tested)
					SpringPlugin.support305 = false;
				}
			} else {
				if (isCaching) {
					if (cacheDir == null) {
						try {
							String userhome = System.getProperty("user.home");
							if (userhome != null) {
								cacheDir = userhome;
							}
						} catch (Throwable t) {
							System.err.println("looks like user.home is not set: cannot create cache.");
							t.printStackTrace(System.err);
						}
					}
				}
			}
			if (isCaching) {
				// Ensure cache folder exists
				try {
					File cacheDirFile = new File(cacheDir);
					if (!cacheDirFile.exists()) {
						boolean created = cacheDirFile.mkdirs();
						if (!created) {
							System.err.println("Caching deactivated: failed to create cache directory: " + cacheDir);
							isCaching = false;
						}
					} else {
						if (!cacheDirFile.isDirectory()) {
							System.err.println("Caching deactivated: unable to use specified cache area, it is not a directory: "
									+ cacheDirFile);
							isCaching = false;
						}
					}
				} catch (Exception e) {
					System.err.println("Unexpected problem creating specified cachedir: " + cacheDir);
					e.printStackTrace();
				}
			}
			
			// Alternative route for specifying values
			value = System.getProperty("springloaded.enumlimit");
			if (value != null) {
				enumLimit = toInt(value,enumLimit);
			}
		} catch (Throwable t) {
			System.err.println("Unexpected problem reading global configuration setting:" + t.toString());
			t.printStackTrace();
		}
		debugplugins = debugPlugins;
	}
	
	private static int toInt(String value, int defaultValue) {
		try {
			return Integer.parseInt(value);
		} catch (NumberFormatException nfe) {
			return defaultValue;
		}
	}
	
	public final static boolean isJava18orHigher;
	
	static {
		String version = System.getProperty("java.version");
		if (version.startsWith("1.8")) {
			isJava18orHigher = true;
		}
		else {
			isJava18orHigher = false;
		}
	}
}
