/*
 * JBoss, Home of Professional Open Source
 * Copyright 2014, Red Hat and individual contributors
 * by the @authors tag. See the copyright.txt in the distribution for a
 * full listing of individual contributors.
 *
 * This 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 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software 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 software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 *
 * @authors Andrew Dinn
 */

package org.jboss.byteman.contrib.bmunit;

import com.sun.tools.attach.AgentInitializationException;
import org.jboss.byteman.agent.install.Install;
import org.jboss.byteman.agent.install.VMInfo;
import org.jboss.byteman.agent.submit.Submit;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.Properties;
import java.util.jar.Attributes;
import java.util.jar.Manifest;

/**
 * class used to model a specific BMUnit configuration
 */
public class BMUnitConfigState
{
    private static BMUnitConfigState currentConfigState = null;

    /**
     * a global configuration state used to preserve the configuration
     * associated with a test class should the BMUnitRunner temporarily
     * reconfigure the state for a specific test method.
     */
    private static BMUnitConfigState shadowConfigState = null;

    /**
     * a default configuration state used when a test class does not specify
     * a configuration state. this state is initialised using the state
     * associated with the first test class processed during the test run.
     * it is also the state used to define parameters which configure autoload
     * of the Byteman agent.
     */
    public static BMUnitConfigState defaultConfigState = null;

    private String agentHost;
    private int agentPort;
    private String loadDirectory;
    private String resourceLoadDirectory;
    private boolean allowConfigUpdate;
    private boolean verbose;
    private boolean debug;
    private boolean bmunitVerbose;
    private boolean inhibitAgentLoad;
    private boolean policy;
    private boolean dumpGeneratedClasses;
    private String dumpGeneratedClassesDirectory;
    private boolean dumpGeneratedClassesIntermediate;
    private BMUnitConfigState previous;

    private BMUnitConfigState(BMUnitConfig config, BMUnitConfigState previous) throws Exception
    {
        this.previous = previous;
        boolean enforce = config.enforce();
        // we don't need to use the same host and port as the default config
        // although using something different is a tad weird
        // if we have an empty host name and no previous setting then
        // check for an env setting
        agentHost = config.agentHost();
        if (previous == null && (agentHost == null || agentHost.length() == 0)) {
                agentHost = initHost();
        }
        // same story for agentPort as for agentHost
        String agentPortString = config.agentPort();
        if (previous == null && (agentPortString == null || agentPortString.length() == 0)) {
                agentPort = initPort();
                if (agentPort < 0) {
                    agentPort = 0;
                }
        } else {
            try {
                agentPort = Integer.valueOf(agentPortString);
            } catch (NumberFormatException e) {
                agentPort = 0;
            }
            if (agentPort < 0) {
                agentPort = 0;
            }
        }
        // the load dir and resource load dir can be reset but if they are
        // empty and we have no previous setting then check for an env setting
        loadDirectory = config.loadDirectory();
        if (previous == null && (loadDirectory== null || loadDirectory.length() == 0)) {
                loadDirectory = initDefaultLoadDirectory();
        }
        resourceLoadDirectory = config.resourceLoadDirectory();
        if (previous == null && (resourceLoadDirectory== null || resourceLoadDirectory.length() == 0)) {
            resourceLoadDirectory = initDefaultResourceLoadDirectory();
        }
        // whatever previous setting we have for config update cannot be changed
        // so we need to check for enforcing mode
        allowConfigUpdate = config.allowAgentConfigUpdate();
        if (previous != null && allowConfigUpdate != previous.allowConfigUpdate) {
            if (enforce) {
                String message = "BMUnit configuration specifies incompatible settings for allowAgentConfigUpdate";
                throw new Exception(message);
            }
        }
        // if the verbose setting differs from the previous and we cannot update the config
        // then we need to check for enforcing
        // if there is no previous setting check and verbose is not set then allow  for
        // an environment setting
        verbose = config.verbose();
        if (previous != null && verbose != previous.verbose && !allowConfigUpdate) {
            if (enforce) {
                String message = "BMUnit configuration specifies incompatible settings for verbose";
                throw new Exception(message);
            }
        }
        // if we have no previous config allow the env setting to override
        if (previous == null && !verbose) {
            verbose = initVerbose();
        }
        // if the debug setting differs from the previous and we cannot update the config
        // then we need to check for enforcing
        debug = config.debug();
        if (previous != null && debug != previous.debug && !allowConfigUpdate) {
            if (enforce) {
                String message = "BMUnit configuration specifies incompatible settings for debug";
                throw new Exception(message);
            }
        }
        // if we have no previous config allow the env setting to override
        if (previous == null && !debug) {
            debug = initDebug();
        }
        // we can always change the bmunit verbose setting
        // if there is no previus setting and the config does nto set it
        // then allow for an environment setting
        bmunitVerbose = config.bmunitVerbose();
         if (previous == null && !bmunitVerbose) {
             bmunitVerbose = initBMUnitVerbose();
         }
        // if the initial config set inhibitAgentLoad then we stick with it
        // if there is no previous setting check and inhibitAgentLoad is not set
        // then allow  for an environment setting
        inhibitAgentLoad = config.inhibitAgentLoad();
        if (previous != null && inhibitAgentLoad != previous.inhibitAgentLoad) {
            if (enforce) {
                String message = "BMUnit configuration specifies incompatible settings for inhibitAgentLoad";
                throw new Exception(message);
            }
        }
        if (previous == null && !inhibitAgentLoad) {
            inhibitAgentLoad = System.getProperty(AGENT_INHIBIT) != null;
        }
        // whatever previous setting we have for policy cannot be changed
        // so we need to check for enforcing mode
        policy = config.policy();
        if (previous != null && previous.policy != policy) {
            if (enforce) {
                String message = "BMUnit configuration specifies incompatible settings for inhibitAgentLoad";
                throw new Exception(message);
            }
        }
        // if the dumpGeneratedClasses setting differs from the previous and we cannot
        // update the config then we need to check for enforcing
        dumpGeneratedClasses = config.dumpGeneratedClasses();
        if (previous != null && dumpGeneratedClasses != previous.dumpGeneratedClasses && !allowConfigUpdate) {
            if (enforce) {
                String message = "BMUnit configuration specifies incompatible settings for dumpGeneratedClasses";
                throw new Exception(message);
            }
        }
        // if we have no previous config allow the env setting to override
        if (previous == null && !dumpGeneratedClasses) {
            dumpGeneratedClasses = initDumpGeneratedClasses();
        }
        // the dump directory can be reset but if it is empty and we have
        // no previous setting then check for an env setting
        dumpGeneratedClassesDirectory = config.dumpGeneratedClassesDirectory();
        if (previous == null && (dumpGeneratedClassesDirectory == null || dumpGeneratedClassesDirectory.length() > 0)) {
             dumpGeneratedClassesDirectory = initDumpGeneratedClassesDirectory();
        }
        // if the dumpGeneratedClasses setting differs from the previous and we cannot
        // update the config then we need to check for enforcing
        dumpGeneratedClassesIntermediate = config.dumpGeneratedClassesIntermediate();
        if (previous != null && dumpGeneratedClassesIntermediate != previous.dumpGeneratedClassesIntermediate && !allowConfigUpdate) {
            if (enforce) {
                String message = "BMUnit configuration specifies incompatible settings for dumpGeneratedClassesIntermediate";
                throw new Exception(message);
            }
        }
    }

    private BMUnitConfigState(BMUnitConfigState previous) throws Exception
    {
        this.previous = previous;
        if (previous != null ) {
            agentHost = previous.agentHost;
            agentPort = previous.agentPort;
            loadDirectory = previous.loadDirectory;
            resourceLoadDirectory = previous.resourceLoadDirectory;
            allowConfigUpdate = previous.allowConfigUpdate;
            verbose = previous.verbose;
            bmunitVerbose = previous.bmunitVerbose;
            inhibitAgentLoad = previous.inhibitAgentLoad;
            policy = previous.policy;
            dumpGeneratedClasses = previous.dumpGeneratedClasses;
            dumpGeneratedClassesDirectory = previous.dumpGeneratedClassesDirectory;
            dumpGeneratedClassesIntermediate = previous.dumpGeneratedClassesIntermediate;
        } else {
            agentHost = initHost();
            agentPort = initPort();
            loadDirectory = initDefaultLoadDirectory();
            resourceLoadDirectory = initDefaultResourceLoadDirectory();
            // default is to allow sys property updates
            allowConfigUpdate = true;
            verbose = initVerbose();
            debug = initDebug();
            bmunitVerbose = initBMUnitVerbose();
            inhibitAgentLoad = System.getProperty(AGENT_INHIBIT) != null;
            policy = initPolicy();
            dumpGeneratedClasses = initDumpGeneratedClasses();
            dumpGeneratedClassesDirectory = initDumpGeneratedClassesDirectory();
            dumpGeneratedClassesIntermediate = initDumpGeneratedClassesIntermediate();
     }
    }

    /**
     * helper method to check whether we need to update agent properties
     * when we switch config
     * @param newConfigState
     * @param oldConfigState
     * @return
     */
    private static boolean needPropertyReset(BMUnitConfigState newConfigState, BMUnitConfigState oldConfigState)
    {
        if (oldConfigState == null) {
            // we are installing the default config
            // we always install properties for this config
            return true;
        } else if (newConfigState == null) {
            // we are de-installing the default config properties
            // we always leave its properties in place
            return false;
        } else {
            // we are switching from one config to another
            // we need an update if the verbose, debug or
            // dumpGeneratedClasses settings differ
            if (newConfigState.verbose != oldConfigState.verbose) {
                return true;
            }
            if (newConfigState.debug != oldConfigState.debug) {
                return true;
            }
            if (newConfigState.dumpGeneratedClasses != oldConfigState.dumpGeneratedClasses) {
                return true;
            }
            // if we get here and class dumping is enabled in either
            // config (and hence both) then we need an update if the
            // dump directory has changed or if we have enabled
            // intermediate dumping.
            if (newConfigState.dumpGeneratedClasses) {
                String newDir = newConfigState.getDumpGeneratedClassesDirectory();
                String oldDir = oldConfigState.getDumpGeneratedClassesDirectory();
                if (!newDir.equals(oldDir)) {
                    return true;
                }
                if (newConfigState.dumpGeneratedClassesIntermediate != oldConfigState.dumpGeneratedClassesIntermediate) {
                    return true;
                }
            }
            return false;
        }
    }

    /**
     * helper method to configure the properties to be reset
     * when a config change occurs
     * @param newConfigState
     * @param oldConfigState
     * @param props
     * @return
     */
    private static boolean configurePropertyReset(BMUnitConfigState newConfigState, BMUnitConfigState oldConfigState, Properties props)
    {
        boolean modified = false;
        if (oldConfigState == null) {
            // we are installing the default config
            // we always install properties for this config
            if (newConfigState.allowConfigUpdate) {
               props.setProperty(BYTEMAN_ALLOW_CONFIG_UPDATE, "true");
            }
            if (newConfigState.verbose) {
                props.setProperty(BYTEMAN_VERBOSE, "true");
            }
            if (newConfigState.debug) {
                props.setProperty(BYTEMAN_DEBUG, "true");
            }
            if (newConfigState.dumpGeneratedClasses) {
                props.setProperty(BYTEMAN_DUMP_GENERATED_CLASSES, "true");
                if (newConfigState.dumpGeneratedClassesDirectory.length() > 0) {
                    props.setProperty(BYTEMAN_DUMP_GENERATED_CLASSES_DIRECTORY, newConfigState.dumpGeneratedClassesDirectory);
                }
                if (newConfigState.dumpGeneratedClassesIntermediate) {
                    props.setProperty(BYTEMAN_DUMP_GENERATED_CLASSES_INTERMEDIATE, "true");
                }
            }
            return true;
        } else if (newConfigState == null) {
            // we are de-installing the default config properties
            // we always leave its properties in place
            return false;
        } else {
            // we are switching from one config to another
            // we need to reset the verbose, debug or
            // dumpGeneratedClasses settings if they differ
            if (newConfigState.verbose != oldConfigState.verbose) {
                props.setProperty(BYTEMAN_VERBOSE, (newConfigState.verbose ? "true" : ""));
                modified = true;
            }
            if (newConfigState.debug != oldConfigState.debug) {
                props.setProperty(BYTEMAN_DEBUG, (newConfigState.debug ? "true" : ""));
                modified = true;
            }
            // if we are enabling dumping we may also need to set a dump directory
            // and/or enable intermediate dumping
            // if we are disabling dumping we may also need to clear the dump directory
            // and/or disable intermediate dumping
            // if dumping is enabled in both configs we still may need to reset
            // the dump directory and/or reset intermediate dumping
            boolean resetDump = false;
            boolean resetDumpDir = false;
            boolean resetIntermediateDump = false;
            if (newConfigState.dumpGeneratedClasses) {
                if (oldConfigState.dumpGeneratedClasses) {
                    // dump is still enabled so no need to reset dump
                    // but check for change to dump dir and intermediate dumps
                    String newDir = newConfigState.getDumpGeneratedClassesDirectory();
                    String oldDir = oldConfigState.getDumpGeneratedClassesDirectory();
                    resetDumpDir = (!newDir.equals(oldDir));
                    boolean isDumpIntermediate = newConfigState.isDumpGeneratedClassesIntermediate();
                    boolean wasDumpIntermediate = newConfigState.isDumpGeneratedClassesIntermediate();
                    resetIntermediateDump = isDumpIntermediate != wasDumpIntermediate;
                } else {
                    // dump has just been enabled
                    resetDump = true;
                    // also check if we now have a dump dir or intermediate dumps
                    String dumpDir = newConfigState.getDumpGeneratedClassesDirectory();
                    resetDumpDir = dumpDir.length() > 0;
                    resetIntermediateDump = newConfigState.isDumpGeneratedClassesIntermediate();
                }
            } else {
                if (oldConfigState.dumpGeneratedClasses) {
                    // dump is still disabled so no need to reset dump
                    // or the other properties
                } else {
                    // dump has just been disabled
                    resetDump = true;
                    // check if we need to unset a dump dir or intermediate dumps
                    String dumpDir = oldConfigState.getDumpGeneratedClassesDirectory();
                    resetDumpDir = dumpDir.length() > 0;
                    resetIntermediateDump = oldConfigState.isDumpGeneratedClassesIntermediate();
                }
            }
            if (resetDump) {
                props.setProperty(BYTEMAN_DUMP_GENERATED_CLASSES, (newConfigState.dumpGeneratedClasses? "true" : ""));
                modified = true;
            }
            if (resetDumpDir) {
                props.setProperty(BYTEMAN_DUMP_GENERATED_CLASSES_DIRECTORY, newConfigState.getDumpGeneratedClassesDirectory());
                modified = true;
            }
            if (resetIntermediateDump) {
                props.setProperty(BYTEMAN_DUMP_GENERATED_CLASSES_INTERMEDIATE, (newConfigState.dumpGeneratedClassesIntermediate? "true" : ""));
                modified = true;
            }
        }
        return modified;
    }

    private static void uploadAgentProperties() throws Exception {
        // if any Byteman config changes have been requested and
        // are allowed upload all reconfigured system property
        // settings to the agent.
        BMUnitConfigState previousConfigState = currentConfigState.previous;
        if (needPropertyReset(currentConfigState, previousConfigState)) {
            Submit submit = new Submit(currentConfigState.getHost(), currentConfigState.getPort());
            Properties properties = new Properties();
            if (configurePropertyReset(currentConfigState, previousConfigState, properties)) {
                submit.setSystemProperties(properties);
            }
        }
    }

    private static void resetAgentProperties() throws Exception {
        // if we uploaded any reconfigured system property settings
        // to the agent then revert them
        BMUnitConfigState previousConfigState = currentConfigState.previous;
        if (previousConfigState != null && previousConfigState.isAllowConfigUpdate()) {
            if (needPropertyReset(previousConfigState, currentConfigState)) {
                Submit submit = new Submit(currentConfigState.getHost(), currentConfigState.getPort());
                Properties properties = new Properties();
                if (configurePropertyReset(previousConfigState, currentConfigState, properties)) {
                    submit.setSystemProperties(properties);
                }
            }
        }
    }

    private void checkBMUnitVersion(String agentVersion) throws Exception
    {
        String bmunitVersion = null;

        String className = this.getClass().getSimpleName() + ".class";
        String classPath = this.getClass().getResource(className).toString();
        if (classPath.startsWith("file")) {
            // this happens during building -- just ignore
            return;
        } else if (!classPath.startsWith("jar")) {
            throw new Exception("BMUnit : could not find manifest for bmunit jar");
        }
        try {
            String manifestPath = classPath.substring(0, classPath.lastIndexOf("!") + 1) +
                    "/META-INF/MANIFEST.MF";
            Manifest manifest = new Manifest(new URL(manifestPath).openStream());
            bmunitVersion = manifest.getMainAttributes().getValue(Attributes.Name.IMPLEMENTATION_VERSION);
        } catch (IOException E) {
            // handle
        }
        if (bmunitVersion == null || bmunitVersion.equals("")) {
            throw new Exception("BMUnit : could not find manifest implementation version for bmunit jar");
        }

        if (!bmunitVersion.equals(agentVersion)) {
            throw new Exception("BMUnit : Identified test JVM process but agent version " + agentVersion + " does not match BMUnit version " + bmunitVersion);
        }
    }

    /**
     * load the agent into this JVM if not already loaded. unfortunately this can only be done if we have
     * the pid of the current process and we cannot get that in a portable way
     */
    private void loadAgent() throws Exception
    {
        String id = null;
        String agentVersion = null;

        // if we can get a proper pid on Linux  we use it
        int pid = getPid();
        // uncomment to force lookup by name even on Linux
        // pid = 0;

        if (pid > 0) {
            id = Integer.toString(pid);
        } else {
            /*
            VMInfo[] vmInfo = Install.availableVMs();
            // search for a JVM which looks like it is running a JUnit test
            // and install the agent into that JVM
            // it could be run from ant or maven or some other process!!
            for (int i = 0; i < vmInfo.length; i++) {
                String displayName = vmInfo[i].getDisplayName();
                if (displayName.startsWith("org.apache.tools.ant.taskdefs.optional.junit.JUnitTestRunner ")) {
                    // a JUnit test forked by ant
                    id = vmInfo[i].getId();
                    break;
                } else if (displayName.startsWith("org.apache.tools.ant.launch.Launcher ")) {
                    // a JUnit test run directly by ant
                    id = vmInfo[i].getId();
                    break;
                } else {
                    // TODO -- identify a forked maven test and then a test run directly or any other mode of running
                }
                */
            // alternative strategy which will  work everywhere
            // set a unique system property and then check each available VM until we find it
            String prop = "org.jboss.byteman.contrib.bmunit.agent.unique";
            String unique = Long.toHexString(System.currentTimeMillis());
            System.setProperty(prop, unique);
            VMInfo[] vmInfo = Install.availableVMs();
            for (int i = 0; i < vmInfo.length; i++) {
                String nextId = vmInfo[i].getId();
                String value = Install.getSystemProperty(nextId, prop);
                if (unique.equals(value)) {
                    id = nextId;
                    break;
                }
            }
            // last ditch effort to obtain pid on Windows where the availableVMs list may be empty
            if (id == null) {
                String processName = ManagementFactory.getRuntimeMXBean().getName();
                if (processName != null && processName.contains("@")) {
                    id = processName.substring(0, processName.indexOf("@"));
                    // check we actually have an integer
                    try {
                        Integer.parseInt(id);
                        // well, it's a number so now check it identifies the current VM
                        String value = Install.getSystemProperty(id, prop);
                        if (!unique.equals(value)) {
                            // nope, not the right process
                            id = null;
                        }
                    } catch (NumberFormatException e) {
                        // nope, not a number
                        id = null;
                    }
                }
            }
            // make sure we found a process
            if (id == null) {
                throw new Exception("BMUnit : Unable to identify test JVM process during agent load");
            }
        }

        try {
            if (isBMUnitVerbose()) {
                System.out.println("BMUnit : loading agent id = " + id);
            }
            Properties properties = new Properties();
            configurePropertyReset(currentConfigState, null, properties);
            int size = properties.size();
            String[] proparray = new String[size];
            int i = 0;
            for (String key : properties.stringPropertyNames()) {
                proparray[i++] = key + "=" + properties.getProperty(key);
            }
            Install.install(id, true, isPolicy(), getHost(), getPort(), proparray);
        } catch (AgentInitializationException e) {
            // this probably indicates that the agent is already installed
        }
        
        // check that the Byteman agent version property matches the version we expect

        agentVersion = Install.getSystemProperty(id, AGENT_VERSION);

        if (agentVersion == null || agentVersion.equals("")) {
            throw new Exception("BMUnit : found agent with id " + id + " but could not find agent version");
        }

        checkBMUnitVersion(agentVersion);
    }

    /**
     * return the integer process id of the current  process. n.b. only works on Linux.
     * @return
     */
    private static int getPid()
    {
        File file = new File("/proc/self/stat");
        if (!file.exists()  || !file.canRead()) {
            return 0;
        }

        FileInputStream fis = null;
        int  pid = 0;

        try {
            fis = new FileInputStream(file);
            byte[] bytes = new byte[10];
            StringBuilder builder = new StringBuilder();
            fis.read(bytes);
            for (int i = 0; i < 10; i++) {
                char c = (char)bytes[i];
                if (Character.isDigit(c)) {
                    builder.append(c);
                } else {
                    break;
                }
            }
            pid = Integer.valueOf(builder.toString());
        } catch (Exception e) {
            // ignore
        } finally {
            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException e1) {
                    // ignore
                }
            }
        }
        return pid;
    }

    /**
     * System property which identifies the directory from which to
     * start searching for rule script. If unset the current working
     * directory of the test is used.
     */
    public final static String LOAD_DIRECTORY = "org.jboss.byteman.contrib.bmunit.load.directory";

    /**
     * System property which identifies the resource load directory
     * from which to start searching for rule script.
     */
    public final static String RESOURCE_LOAD_DIRECTORY = "org.jboss.byteman.contrib.bmunit.resource.load.directory";

    /**
     * System property specifying the port to be used when starting the agent and when submitting
     * rules to it. You can normally just use the default port.
     */
    public final static String AGENT_PORT = "org.jboss.byteman.contrib.bmunit.agent.port";

    /**
     * System property specifying the host to be used when starting the agent and when submitting
     * rules to it. You can normally just use the default host.
     */
    public final static String AGENT_HOST = "org.jboss.byteman.contrib.bmunit.agent.host";

    /**
     * System property specifying whether to set a security policy when loading the agent.
     */
    public final static String AGENT_POLICY = "org.jboss.byteman.contrib.bmunit.agent.policy";
    /**
     * System property which inhibits automatic loading of the agent. If you set this then you have to load
     * the agent yourself using the Install API or ensure JUnit loads by forking a JVM and passing
     * the necessary -javaagent options on the command line. You may also want to set this if you you have
     * loaded the agent into a remote service in another JVM driven by your unit test.
     */
    public final static String AGENT_INHIBIT = "org.jboss.byteman.contrib.bmunit.agent.inhibit";
    /**
     * system property set by the agent to advertise the version of the loaded agent
     */
    public final static String AGENT_VERSION = "org.jboss.byteman.agent.version";
    /**
     * System property which enables tracing of Byteman activity
     */
    public final static String BYTEMAN_ALLOW_CONFIG_UPDATE = "org.jboss.byteman.allow.config.update";

    /**
     * System property which enables tracing of Byteman activity
     */
    public final static String BYTEMAN_VERBOSE = "org.jboss.byteman.verbose";

    /**
     * System property which enables printing of Byteman rule debug statements
     */
    public final static String BYTEMAN_DEBUG = "org.jboss.byteman.debug";

    /**
     * System property which enables tracing of bmunit activity
     */
    public final static String BMUNIT_VERBOSE = "org.jboss.byteman.contrib.bmunit.verbose";

    /**
     * System property which enables dumping of generated classes
     */
    public final static String BYTEMAN_DUMP_GENERATED_CLASSES = "org.jboss.byteman.dump.generated.classes";

    /**
     * System property which configures directory path for files used for dumping of generated classes
     */
    public final static String BYTEMAN_DUMP_GENERATED_CLASSES_DIRECTORY = "org.jboss.byteman.dump.generated.classes.directory";

    /**
     * System property which configures dumping of intermediate versions of generated classes
     */
    public final static String BYTEMAN_DUMP_GENERATED_CLASSES_INTERMEDIATE = "org.jboss.byteman.dump.generated.classes.intermediate";

    /**
     * this is only provided for backward compatibility in case some app was
     * using this constant string to configure the required property.
     */
    public final static String VERBOSE = BMUNIT_VERBOSE;

    /**
     * install the configuration for a specific test class, possibly
     * also adopting it as the default configuration
     *
     * this method is not thread-safe. BMUnit assumes that only one
     * JUnit/TestNG test is run at a time.
     * @param config the config to install or null if no config is available
     * @param testClass the test class which may or may not have an
     * associated config
     * @throws Exception if the config cannot be installed or an agent
     * load error occurs
     */
    public static void pushConfigurationState(BMUnitConfig config, Class<?> testClass) throws Exception
    {
        // current config should be null when this is called
        if (currentConfigState != null) {
            throw new Exception("BMUnit test class configuration pushed without prior pop!");
        }
        if (config != null) {
            currentConfigState = new BMUnitConfigState(config, defaultConfigState);
        } else {
            currentConfigState = new BMUnitConfigState(defaultConfigState);
        }
        // if this is the first config we have seen then use it as the default
        // and auto load the agent if needed
        if (defaultConfigState == null) {
            defaultConfigState = currentConfigState;
            if (!defaultConfigState.inhibitAgentLoad) {
                defaultConfigState.loadAgent();
            }
        } else {
            // upload any properties changed by this config
            uploadAgentProperties();

        }
    }

    public static void popConfigurationState(Class<?> testClass) throws Exception
    {
        // current config should be non-null and shadow config null when this is called
        if (currentConfigState == null) {
            // should not happen
            throw new Exception("BMUnit test class configuration popped without prior push!");
        }
        if (shadowConfigState != null) {
            // should not happen
            throw new Exception("BMUnit test class configuration popped without popping method configuration!");
        }
        // reset any properties set by this config
        resetAgentProperties();

        currentConfigState = null;
    }

    public static void pushConfigurationState(BMUnitConfig config, Method method) throws Exception
    {
        // current config should be non-null and shadow config null when this is called
        if (currentConfigState == null) {
            // should not happen
            throw new Exception("BMUnit method configuration pushed without prior test configuration push!");
        }
        if (shadowConfigState != null) {
            // should not happen
            throw new Exception("BMUnit method configuration pushed without prior method configuration pop!");
        }
        shadowConfigState = currentConfigState;
        if (config != null) {
            currentConfigState = new BMUnitConfigState(config, currentConfigState);
            // agent properties may need updating
            uploadAgentProperties();
        } else {
            // agent properties will not have changed
            currentConfigState = new BMUnitConfigState(currentConfigState);
        }
    }

    public static void popConfigurationState(Method method) throws Exception
    {
        // shadow config should be non-null when this is called
        if (shadowConfigState == null) {
            // should not happen
            throw new Exception("BMUnit method configuration pushed without prior method configuration pop!");
        }
        // agent properties may need resetting
        resetAgentProperties();

        currentConfigState = shadowConfigState;
        shadowConfigState = null;
    }


    public static void resetConfigurationState(Method method) throws Exception
    {
        if (shadowConfigState != null) {
            popConfigurationState(method);
        }
    }

    public static void resetConfigurationState(Class<?> testClass) throws Exception
    {
        if (currentConfigState != null) {
            popConfigurationState(testClass);
        }
    }
  /**
     * the global configuration state which configures the operation
     * of BMUnit when running tests in a specific class or a method
     * in that class. this value is set by BMUnitRunner before it
     * starts running the tests in a test class. it may be reset
     * temporarily by BMUnitRunner before executing a specific test
     * method but should then be set back to the previous
     * configuration.
     * @return the current configuration
     */
    public static BMUnitConfigState getCurrentConfigState() {
        return currentConfigState;
    }

    public BMUnitConfigState currentConfigState()
    {
        return currentConfigState;
    }

    /**
     * getter for currently configured agent host setting
     * @return the current host setting
     */
    public String getHost() {
        // agentHost can be overridden so don't look up previous
        return agentHost;
    }

    /**
     * getter for currently configured agent port setting
     * @return the current port setting
     */
    public int getPort() {
        // agentPort can be overridden so don't look up previous
        // unlesss current port is 0
        if (agentPort == 0 && previous != null) {
            return previous.getPort();
        }
        return agentPort;
    }

    /**
     * getter for currently configured load directory setting
     * @return the current load directory setting
     */
    public String getLoadDirectory() {
        // loadDirectory can be overridden so don't look up previous
        // unless it is empty or null
        if ((loadDirectory != null || loadDirectory.length() == 0) && previous != null) {
            return previous.getLoadDirectory();
        }
        return loadDirectory;
    }

    /**
     * getter for currently configured resource directory setting
     * @return the current resource directory setting
     */
    public String getResourceLoadDirectory() {
        // loadDirectory can be overridden so don't look up previous
        // unless it is empty or null
        if ((resourceLoadDirectory != null || resourceLoadDirectory.length() == 0) && previous != null) {
            return previous.getResourceLoadDirectory();
        }
        return resourceLoadDirectory;
    }

    /**
     * smart getter for currently configured allowAgentConfigUpdate setting
     * which redirects through to the previous config to ensure that
     * we employ the setting used for the initial agent load
     * @return the current allowAgentConfigUpdate setting
     */
    public boolean isAllowConfigUpdate() {
        // allowAgentConfigUpdate cannot be overridden so check for previous setting
        if (previous != null) {
            return previous.isAllowConfigUpdate();
        }
        return allowConfigUpdate;
    }

    /**
     * smart getter for currently configured Byteman verbose setting
     * which redirects through to the previous config if config
     * updates are not allowed but otherwise returns the currently
     * configured Byteman verbose setting
     * @return the current Byteman verbose setting
     */
    public boolean isVerbose() {
        // verbose can be overridden if allowAgentConfigUpdate is true
        if (previous == null || previous.isAllowConfigUpdate()) {
            return verbose;
        } else {
            return previous.isVerbose();
        }
    }

    /**
     * smart getter for currently configured Byteman debug setting
     * which redirects through to the previous config if config
     * updates are not allowed but otherwise returns the currently
     * configured Byteman debug setting
     * @return the current Byteman debug setting
     */
    public boolean isDebug() {
        // debug can be overridden if allowAgentConfigUpdate is true
        if (previous == null || previous.isAllowConfigUpdate()) {
            return debug;
        } else {
            return previous.isDebug();
        }
    }

    /**
     * getter for currently configured BMUnit verbose setting
     * @return the current BMUnit verbose setting
     */
    public boolean isBMUnitVerbose() {
        // bmunitVerbose can be overridden so don't look up previous
        return bmunitVerbose;
    }

    /**
     * smart getter for currently configured inhibitAgentLoad setting
     * which redirects through to the previous config to ensure that
     * we employ the setting used for the initial agent load
     * @return the current inhibitAgentLoad setting
     */
    public boolean isInhibitAgentLoad() {
        // inhibitAgentLoad cannot be overridden so check for previous setting
        if (previous != null) {
            return previous.isInhibitAgentLoad();
        }
        return inhibitAgentLoad;
    }

    /**
     * smart getter for currently configured policy setting
     * which redirects through to the previous config to ensure that
     * we employ the setting used for the initial agent load
     * @return the current policy setting
     */
    public boolean isPolicy() {
        // isPolicy cannot be overridden so check for previous setting
        if (previous != null) {
            return previous.isPolicy();
        }
        return policy;
    }

    /**
     * getter for current dumpGeneratedClasses setting
     * @return the current dumpGeneratedClasses setting
     */
    public boolean isDumpGeneratedClasses()
    {
        return dumpGeneratedClasses;
    }

    /**
     * smart getter for current dumpGeneratedClassesDirectory setting
     * which only returns a directory when dumpGeneratedClasses is set
     * in which case it uses any current setting but delegates to previous
     * if no value has been set.
     * @return the current dumpGeneratedClasseDirectory setting
     */
    public String getDumpGeneratedClassesDirectory()
    {
        if (!dumpGeneratedClasses) {
            return "";
        }
        if (dumpGeneratedClassesDirectory != null && dumpGeneratedClassesDirectory.length() != 0) {
            return dumpGeneratedClassesDirectory;
        }
        if (previous != null) {
            return previous.getDumpGeneratedClassesDirectory();
        }
        return "";
    }

    /**
     * smart getter for current dumpGeneratedClassesIntermediate setting
     * which only returns the attribute setting if dumpGeneratedClasses
     * has also been set.
     * @return the current dumpGeneratedClassesIntermediate
     */
    public boolean isDumpGeneratedClassesIntermediate()
    {
        if (!dumpGeneratedClasses) {
            return false;
        }
        return dumpGeneratedClassesIntermediate;
    }

    /**
     * return the String configured for the agent host or null if it
     * was not configured
     * @return the iniital agent host
     */
    private static String initHost()
    {
        String host = System.getProperty(AGENT_HOST);
        return (host != null ? host : "");
    }

    /**
     * return the integer port configured for the agent port or 0 if
     * it was not configured or was misconfigured
     * @return the initial agent port
     */
    private static int initPort()
    {
        String portString = System.getProperty(AGENT_PORT);
        return (portString == null ? 0 : Integer.valueOf(portString));
    }

    /**
     * computes the default load directory from system property
     * org.jboss.byteman.contrib.bmunit.load.directory or defaults it
     * to ""
     * @return the initial load directory
     */
    private static String initDefaultLoadDirectory()
    {
        String dir = System.getProperty(LOAD_DIRECTORY);
        if (dir == null) {
            return "";
        }
        return dir;
    }

    /**
     * computes the default resource load directory from system
     * property
     * org.jboss.byteman.contrib.bmunit.resource.load.directory or
     * defaults it to the load directory
     * @return the initial resource load directory
     */
    private static String initDefaultResourceLoadDirectory()
    {
        String dir = System.getProperty(RESOURCE_LOAD_DIRECTORY);
        if (dir == null) {
            dir = System.getProperty(LOAD_DIRECTORY);
            if (dir == null) {
                dir = "";
            }
        }
        int l = dir.length();
        if (l > 0 && dir.charAt(l - 1) != '/') {
            dir = dir + "/";
        }
        return dir;
    }

    private static boolean initVerbose()
    {
        return System.getProperty(BYTEMAN_VERBOSE) != null;
    }

    private static boolean initDebug()
    {
        return System.getProperty(BYTEMAN_DEBUG) != null;
    }

    private static boolean initBMUnitVerbose()
    {
        return System.getProperty(BMUNIT_VERBOSE) != null;
    }

    /**
     * test whether a security policy should be set for agent code
     * when the agent is installed
     * @return the initial policy setting
     */
    private static boolean initPolicy()
    {
        String policyString= System.getProperty(AGENT_POLICY);
        return (policyString == null ? false : Boolean.valueOf(policyString));
    }

    private static boolean initDumpGeneratedClasses()
    {
        return System.getProperty(BYTEMAN_DUMP_GENERATED_CLASSES) != null;
    }

    private static String initDumpGeneratedClassesDirectory()
    {
        String dir = System.getProperty(BYTEMAN_DUMP_GENERATED_CLASSES_DIRECTORY);
        if (dir == null) {
            dir = "";
        }
        return dir;
    }

    private static boolean initDumpGeneratedClassesIntermediate()
    {
        return System.getProperty(BYTEMAN_DUMP_GENERATED_CLASSES_INTERMEDIATE) != null;
    }
}
