package com.lazerycode.jmeter;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.text.SimpleDateFormat;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

import org.apache.commons.io.FileUtils;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.project.MavenProject;

import com.lazerycode.jmeter.configuration.JMeterArgumentsArray;
import com.lazerycode.jmeter.configuration.ProxyConfiguration;
import com.lazerycode.jmeter.configuration.RemoteConfiguration;
import com.lazerycode.jmeter.properties.PropertyHandler;
import com.lazerycode.jmeter.threadhandling.JMeterPluginSecurityManager;
import com.lazerycode.jmeter.threadhandling.JMeterPluginUncaughtExceptionHandler;

/**
 * JMeter Maven plugin. This is a base class for the JMeter mojos.
 * 
 * @author Tim McCune
 */
@SuppressWarnings({"UnusedDeclaration", "FieldCanBeLocal", "JavaDoc" })
// Mojos get their fields set via reflection
public abstract class JMeterAbstractMojo extends AbstractMojo {

    /**
     * Sets the list of include patterns to use in directory scan for JMX files.
     * Relative to testFilesDirectory.
     * 
     * @parameter
     */
    protected List<String> testFilesIncluded;

    /**
     * Sets the list of exclude patterns to use in directory scan for Test
     * files. Relative to testFilesDirectory.
     * 
     * @parameter
     */
    protected List<String> testFilesExcluded;

    /**
     * Path under which JMX files are stored.
     * 
     * @parameter expression="${jmeter.testfiles.basedir}"
     *            default-value="${basedir}/src/test/jmeter"
     */
    protected File testFilesDirectory;

    /**
     * Timestamp the test results.
     * 
     * @parameter default-value="true"
     */
    protected boolean testResultsTimestamp;

    /**
     * Set the SimpleDateFormat appended to the results filename. (This assumes
     * that testResultsTimestamp is set to 'true')
     * 
     * @parameter default-value="yyMMdd"
     */
    protected String resultsFileNameDateFormat;

    /**
     * Absolute path to JMeter custom (test dependent) properties file. .
     * 
     * @parameter
     */
    protected Map<String, String> propertiesJMeter;

    /**
     * JMeter Properties that are merged with precedence into default JMeter
     * file in saveservice.properties
     * 
     * @parameter
     */
    protected Map<String, String> propertiesSaveService;

    /**
     * JMeter Properties that are merged with precedence into default JMeter
     * file in upgrade.properties
     * 
     * @parameter
     * @description JMeter Properties that are merged with precedence into
     *              default JMeter file in 'upgrade.properties'.
     */
    protected Map<String, String> propertiesUpgrade;

    /**
     * JMeter Properties that are merged with precedence into default JMeter
     * file in user.properties user.properties takes precedence over
     * jmeter.properties
     * 
     * @parameter
     * @description JMeter Properties that are merged with precedence into
     *              default JMeter file in 'user.properties' user.properties
     *              takes precedence over 'jmeter.properties'.
     */
    protected Map<String, String> propertiesUser;

    /**
     * JMeter Global Properties that override those given in jmeterProps. <br>
     * This sets local and remote properties (JMeter's definition of global
     * properties is actually remote properties) and overrides any local/remote
     * properties already set
     * 
     * @description JMeter Global Properties that override those given in
     *              jmeterProps. <br>
     *              This sets local and remote properties (JMeter's definition
     *              of global properties is actually remote properties) and
     *              overrides any local/remote properties already set.
     */
    protected Map<String, String> propertiesGlobal;

    /**
     * (Java) System properties set for the test run. Properties are merged with
     * precedence into default JMeter file system.properties
     * 
     * @parameter Java merged with precedence into default JMeter file
     *            system.properties.
     */
    protected Map<String, String> propertiesSystem;

    /**
     * Replace the default JMeter properties with any custom properties files
     * supplied. (If set to false any custom properties files will be merged
     * with the default JMeter properties files, custom properties will
     * overwrite default ones)
     * 
     * @parameter default-value="true"
     */
    protected boolean propertiesReplacedByCustomFiles;

    /**
     * Absolute path to JMeter custom (test dependent) properties file.
     * 
     * @parameter
     */
    protected File customPropertiesFile;

    /**
     * Sets whether ErrorScanner should ignore failures in JMeter result file.
     * <p/>
     * Failures are for example failed requests
     * 
     * @parameter expression="${jmeter.ignore.failure}" default-value=false
     */
    protected boolean ignoreResultFailures;

    // TODO: what is an error?
    /**
     * Sets whether ErrorScanner should ignore errors in JMeter result file.
     * 
     * @parameter expression="${jmeter.ignore.error}" default-value=false
     */
    protected boolean ignoreResultErrors;

    /**
     * Value class that wraps all proxy configurations.
     * 
     * @parameter default-value="${proxyConfig}"
     */
    protected ProxyConfiguration proxyConfig;

    /**
     * Value class that wraps all remote configurations.
     * 
     * @parameter default-value="${remoteConfig}"
     */
    protected RemoteConfiguration remoteConfig;

    /**
     * Suppress JMeter output
     * 
     * @parameter default-value="true"
     */
    protected boolean suppressJMeterOutput;

    /**
     * @parameter expression="${project}"
     * @required
     * @readonly
     */
    protected MavenProject mavenProject;

    /**
     * Get a list of artifacts used by this plugin
     * 
     * @parameter default-value="${plugin.artifacts}"
     * @required
     * @readonly
     */
    protected List<Artifact> pluginArtifacts;

    // ---------------------------------------------------

    /**
     * JMeter outputs.
     * 
     * @parameter expression="${project.build.directory}/jmeter"
     * @description Place where the JMeter files will be generated.
     */
    protected transient File workDir;

    /**
     * The dir where the compiled classes are placed
     * 
     * @parameter expression="${jmeter.compiledClasses}"
     *            default-value="${basedir}/target/classes"
     */
    protected transient File compiledClasses;

    /**
     * The dir where the compiled test classes are placed
     * 
     * @parameter expression="${jmeter.compiledTestClasses}"
     *            default-value="${basedir}/target/test-classes"
     */
    protected transient File compiledTestClasses;

    /**
     * The file where the classpath is written
     * 
     * @parameter expression="${jmeter.classpathDump}"
     *            default-value="${basedir}/target/classpath"
     */
    protected transient File classpathDump;

    /**
     * Other directories will be created by this plugin and used by JMeter
     */
    protected File binDir;

    protected File libDir;

    protected File libExtDir;

    protected File logsDir;

    protected File resultsDir;

    /**
     * All property files are stored in this artifact, comes with JMeter library
     */
    protected String jmeterConfigArtifact = "ApacheJMeter_config";

    protected JMeterArgumentsArray testArgs;

    protected PropertyHandler pluginProperties;

    protected static final String BASE_DIR_PATH = System
        .getProperty("user.dir");

    protected static final String JAVA_CLASS_PATH = "java.class.path";

    protected static final String DOT_JAR = ".jar";

    // ==================================================================================================================

    /**
     * Generate the directory tree utilised by JMeter.
     */
    protected void generateJMeterDirectoryTree() {
        logsDir = new File(workDir, "logs");
        logsDir.mkdirs();
        binDir = new File(workDir, "bin");
        binDir.mkdirs();
        libDir = new File(workDir, "lib");
        resultsDir = new File(workDir, "results");
        resultsDir.mkdirs();
        libExtDir = new File(libDir, "ext");
        libExtDir.mkdirs();

        // JMeter expects a <workdir>/lib/junit directory and complains if it
        // can't find it.
        new File(libDir, "junit").mkdirs();
    }

    protected void propertyConfiguration() throws MojoExecutionException {
        pluginProperties =
            new PropertyHandler(testFilesDirectory, binDir,
                getArtifactNamed(jmeterConfigArtifact),
                propertiesReplacedByCustomFiles);
        pluginProperties.setJMeterProperties(propertiesJMeter);
        pluginProperties.setJMeterGlobalProperties(propertiesGlobal);
        pluginProperties
            .setJMeterSaveServiceProperties(propertiesSaveService);
        pluginProperties.setJMeterUpgradeProperties(propertiesUpgrade);
        pluginProperties.setJmeterUserProperties(propertiesUser);
        pluginProperties.setJMeterSystemProperties(propertiesSystem);
        pluginProperties.configureJMeterPropertiesFiles();
        pluginProperties.setDefaultPluginProperties(binDir
            .getAbsolutePath());
    }

    /**
     * Copy jars/files to correct place in the JMeter directory tree.
     * 
     * @throws MojoExecutionException
     */
    protected void populateJMeterDirectoryTree()
            throws MojoExecutionException {
        for (Artifact artifact : pluginArtifacts) {
            try {
                if (artifact.getArtifactId().startsWith("ApacheJMeter_")) {
                    if (artifact.getArtifactId().startsWith(
                        "ApacheJMeter_config")) {
                        JarFile configSettings =
                            new JarFile(artifact.getFile());
                        Enumeration<JarEntry> entries =
                            configSettings.entries();
                        while (entries.hasMoreElements()) {
                            JarEntry jarFileEntry = entries.nextElement();
                            // Only interested in files in the /bin directory
                            // that are not properties files
                            if (jarFileEntry.getName().startsWith("bin")
                                && !jarFileEntry.getName().endsWith(
                                    ".properties")) {
                                if (!jarFileEntry.isDirectory()) {
                                    InputStream is =
                                        configSettings
                                            .getInputStream(jarFileEntry); // get
                                                                           // the
                                                                           // input
                                                                           // stream
                                    OutputStream os =
                                        new FileOutputStream(new File(
                                            workDir.getCanonicalPath()
                                                + File.separator
                                                + jarFileEntry.getName()));
                                    while (is.available() > 0) {
                                        os.write(is.read());
                                    }
                                    os.close();
                                    is.close();
                                }
                            }
                        }
                        configSettings.close();
                    } else {
                        FileUtils.copyFile(artifact.getFile(), new File(
                            libExtDir + File.separator
                                + artifact.getFile().getName()));
                    }
                } else {
                    /**
                     * TODO: exclude jars that maven put in #pluginArtifacts for
                     * maven run? (e.g. plexus jars, the plugin artifact itself)
                     * Need more info on above, how do we know which ones to
                     * exclude?? Most of the files pulled down by maven are
                     * required in /lib to match standard JMeter install
                     */
                    FileUtils.copyFile(artifact.getFile(), new File(libDir
                        + File.separator + artifact.getFile().getName()));
                }
            } catch (IOException e) {
                throw new MojoExecutionException(
                    "Unable to populate the JMeter directory tree: " + e);
            }
        }
        // TODO Check if we really need to do this
        // empty classpath, JMeter will automatically assemble and add all JARs
        // in #libDir and #libExtDir and add them to the classpath. Otherwise
        // all jars will be in the classpath twice.
        System.setProperty("java.class.path", "");
    }

    /**
     * Search the list of plugin artifacts for an artifact with a specific name
     * 
     * @param artifactName
     * @return
     * @throws MojoExecutionException
     */
    protected Artifact getArtifactNamed(final String artifactName)
            throws MojoExecutionException {
        for (Artifact artifact : pluginArtifacts) {
            if (artifact.getArtifactId().equals(artifactName)) {
                return artifact;
            }
        }
        throw new MojoExecutionException("Unable to find artifact '"
            + artifactName + "'!");
    }

    /**
     * Generate the initial JMeter Arguments array that is used to create the
     * command line that we pass to JMeter.
     * 
     * @throws MojoExecutionException
     */
    protected void initialiseJMeterArgumentsArray()
            throws MojoExecutionException {
        testArgs = new JMeterArgumentsArray();
        testArgs.setResultsDirectory(resultsDir.getAbsolutePath());
        testArgs.setResultsTimestamp(testResultsTimestamp);
        testArgs.setJMeterHome(workDir.getAbsolutePath());
        testArgs.setProxyConfig(proxyConfig);
        testArgs.setACustomPropertiesFile(customPropertiesFile);
        try {
            testArgs.setResultsFileNameDateFormat(new SimpleDateFormat(
                resultsFileNameDateFormat));
        } catch (Exception ex) {
            getLog()
                .error(
                    "'"
                        + resultsFileNameDateFormat
                        + "' is an invalid date format.  Defaulting to 'yyMMdd'.");
        }
    }

    /**
     * Wait for one of the JMeterThreads in the list to stop.
     */
    protected void waitForTestToFinish(final List<String> threadNames)
            throws InterruptedException {
        Thread waitThread = null;
        Set<Thread> threadSet = Thread.getAllStackTraces().keySet();
        for (Thread thread : threadSet) {
            for (String threadName : threadNames) {
                if (threadName.equals(thread.getName())) {
                    waitThread = thread;
                    break;
                }
            }
        }
        if (waitThread != null) {
            waitThread
                .setUncaughtExceptionHandler(new JMeterPluginUncaughtExceptionHandler());
            waitThread.join();
        }
    }

    /**
     * Capture System.exit commands so that we can check to see if JMeter is
     * trying to kill us without warning.
     * 
     * @return old SecurityManager so that we can switch back to normal
     *         behaviour.
     */
    protected SecurityManager overrideSecurityManager() {
        SecurityManager oldManager = System.getSecurityManager();
        System.setSecurityManager(new JMeterPluginSecurityManager());
        return oldManager;
    }

    /**
     * Override System.exit(0) to ensure JMeter doesn't kill us without warning.
     * 
     * @return old UncaughtExceptionHandler so that we can switch back to normal
     *         behaviour.
     */
    protected Thread.UncaughtExceptionHandler overrideUncaughtExceptionHandler() {
        Thread.UncaughtExceptionHandler oldHandler =
            Thread.getDefaultUncaughtExceptionHandler();
        Thread
            .setDefaultUncaughtExceptionHandler(new JMeterPluginUncaughtExceptionHandler());
        return oldHandler;
    }

    /** NOT NEEDED ANYMORE? **/

    // protected void addDependenciesToClasspath() {
    //
    // final String initialClassptah =
    // System.getProperty(JAVA_CLASS_PATH, "");
    //
    // // Load jmeter driver class and add it to java class path property
    // try {
    // File jmeterDriverJar =
    // new File(libDir + File.separator + "ApacheJMeter.jar");
    // if (!jmeterDriverJar.exists()) {
    // getLog().warn(
    // "Cannot found jmeter driver jar: " + jmeterDriverJar);
    // } else {
    // getLog().debug("Adding depedency: " + jmeterDriverJar);
    // ClassPathHacker.addFile(jmeterDriverJar);
    // System.setProperty(JAVA_CLASS_PATH,
    // jmeterDriverJar.getPath());
    // }
    // } catch (IOException e) {
    // getLog().warn(
    // "An error occured reading jmeter driver jar file", e);
    // }
    //
    // // Load jmeter libraries
    // try {
    // File[] jarDirs =
    // {libDir, new File(libDir, "ext"),
    // new File(libDir, "junit") };
    // List<File> jars = getJarsFromDirs(jarDirs);
    //
    // for (File jar : jars) {
    // ClassPathHacker.addFile(jar);
    // addToClasspath(jar.getAbsolutePath());
    // }
    //
    // } catch (IOException e) {
    // getLog()
    // .warn("An error occured reading jmeter jar modules", e);
    // }
    //
    // try {
    //
    // if (compiledClasses != null) {
    // getLog().debug("Adding depedency: " + compiledClasses);
    // ClassPathHacker.addFile(compiledClasses);
    // } else {
    // getLog().warn("No compiled classes directory specified");
    // }
    //
    // if (compiledTestClasses != null) {
    // getLog().debug("Adding depedency: " + compiledTestClasses);
    // ClassPathHacker.addFile(compiledTestClasses);
    // } else {
    // getLog().warn(
    // "No compiled test classes directory specified");
    // }
    //
    // String[] dependencies = getClasspathFiles();
    //
    // String[] javaClassPath =
    // System.getProperty(JAVA_CLASS_PATH, "").split(
    // File.pathSeparator);
    //
    // if (dependencies != null) {
    // for (String dependency : dependencies) {
    // if (!dependency.contains("maven-")
    // && jarNotIn(dependency, javaClassPath)) {
    // getLog().debug("Adding depedency: " + dependency);
    // ClassPathHacker.addFile(dependency);
    // } else {
    // getLog().warn("Avoiding depedency: " + dependency);
    // }
    // }
    // }
    //
    // } catch (IOException e) {
    // getLog().warn(
    // "An error occured reading the classpath dump file", e);
    // }
    //
    // // Adding initial java class path at the end
    // if (!initialClassptah.isEmpty()) {
    // System.setProperty(JAVA_CLASS_PATH,
    // System.getProperty(JAVA_CLASS_PATH) + File.pathSeparator
    // + initialClassptah);
    // }
    // }
    //
    // protected boolean jarNotIn(final String jarPath, final String[] paths) {
    // String jarName = new File(jarPath).getName();
    //
    // for (String p : paths) {
    // if (p.endsWith(jarName)) {
    // getLog().warn(jarName + " is already loaded");
    // return false;
    // }
    // }
    // return true;
    // }
    //
    // protected void addToClasspath(final String path) {
    // StringBuilder classpath =
    // new StringBuilder(System.getProperty(JAVA_CLASS_PATH, ""));
    // if (!classpath.toString().isEmpty()
    // && !classpath.toString().endsWith(File.pathSeparator)) {
    // classpath.append(File.pathSeparator);
    // }
    // classpath.append(path);
    // System.setProperty(JAVA_CLASS_PATH, classpath.toString());
    // }
    //
    // protected List<File> getJarsFromDirs(final File[] dirs) {
    // List<File> ret = new LinkedList<File>();
    // if (dirs != null) {
    // for (File dir : dirs) {
    // if (!dir.exists()) {
    // getLog().warn(dir + " directory does not exist");
    // } else if (!dir.isDirectory()) {
    // getLog().warn(dir + " is not a directory");
    // } else if (!dir.canRead()) {
    // getLog().warn(dir + " cannot read");
    // } else {
    // File[] jars = dir.listFiles(new FileFilter() {
    // public boolean accept(final File pathname) {
    // return pathname.getName().endsWith(DOT_JAR);
    // }
    // });
    //
    // if (jars.length == 0) {
    // getLog().warn(
    // dir + " directory does not contain jar files");
    // } else {
    // for (File jar : jars) {
    // ret.add(jar);
    // }
    // }
    // }
    // }
    // }
    // return ret;
    // }
    //
    // protected String[] getClasspathFiles() throws IOException {
    // if (classpathDump == null) {
    // getLog().warn("The classpath dump file has not been set");
    //
    // return null;
    // }
    //
    // StringBuffer fileData = new StringBuffer(1000);
    // BufferedReader reader =
    // new BufferedReader(new InputStreamReader(new FileInputStream(
    // classpathDump)));
    //
    // char[] buf = new char[1024];
    // int numRead = 0;
    // String readData;
    // while ((numRead = reader.read(buf)) != -1) {
    // readData = String.valueOf(buf, 0, numRead);
    // fileData.append(readData);
    // buf = new char[1024];
    // }
    // reader.close();
    //
    // String rtn[] =
    // fileData.toString()
    // .split(System.getProperty("path.separator"));
    // getLog().debug("Dependencies found : " + rtn.length);
    //
    // return rtn;
    // }

}
