/**
 * Copyright 2015-2017 Red Hat, Inc, and individual 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.wildfly.swarm.tools.exec;

import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.stream.Collectors;

import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.exporter.ZipExporter;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.wildfly.swarm.bootstrap.Main;

/**
 * @author Bob McWhirter
 */
public class SwarmExecutor {

    public SwarmExecutor() {
        this.stdout = System.out;
        this.stderr = System.err;
        this.workingDirectory = Paths.get(System.getProperty("user.dir"));
    }

    public SwarmExecutor withStdoutFile(Path stdoutFile) {
        this.stdoutFile = stdoutFile;
        return this;
    }

    public SwarmExecutor withStderrFile(Path stderrFile) {
        this.stderrFile = stderrFile;
        return this;
    }

    public SwarmExecutor withDebug(Integer port) {
        this.debugPort = port;
        return this;
    }

    public SwarmExecutor withJava(Path java) {
        this.java = java;
        return this;
    }

    public Path getJava() {
        if (this.java == null) {
            return findJava();
        }
        return this.java;
    }

    public SwarmExecutor withDefaultSystemProperties() {
        Set<String> names = System.getProperties().stringPropertyNames();

        for (String name : names) {
            if (name.startsWith("jboss") || name.startsWith("swarm") || name.startsWith("wildfly") || name.startsWith("maven")
                    || name.equals("java.io.tmpdir")) {
                String value = System.getProperty(name);
                this.properties.put(name, value);
            }
        }
        return this;
    }

    public SwarmExecutor withProperty(String name, String value) {
        this.properties.put(name, value);
        return this;
    }

    public SwarmExecutor withProperties(Properties props) {
        Set<String> names = props.stringPropertyNames();

        for (String name : names) {
            this.properties.put(name, props.getProperty(name));
        }

        return this;
    }

    public SwarmExecutor withEnvironment(String name, String value) {
        this.environment.put(name, value);
        return this;
    }

    public SwarmExecutor withEnvironment(Properties env) {
        Set<String> names = env.stringPropertyNames();

        for (String name : names) {
            this.environment.put(name, env.getProperty(name));
        }

        return this;
    }

    public SwarmExecutor withClasspathEntry(Path entry) {
        if (this.executable != null && this.executable instanceof ExecutableJar) {
            throw new RuntimeException("Cannot use a classpath with an executable jar");
        }
        this.classpath.add(entry);
        return this;
    }

    public SwarmExecutor withClassPathEntries(List<Path> entries) {
        if (this.executable != null && this.executable instanceof ExecutableJar) {
            throw new RuntimeException("Cannot use a classpath with an executable jar");
        }
        this.classpath.addAll(entries);
        return this;
    }

    public SwarmExecutor withModules(final List<Path> moduleDirs) {
        if (this.executable != null && this.executable instanceof ExecutableJar) {
            throw new RuntimeException("Cannot use modules with an executable jar");
        }
        final File moduleJar = new File(System.getProperty("java.io.tmpdir"),
                                        "swarm-module-overrides.jar");
        final JavaArchive moduleArchive = ShrinkWrap.create(JavaArchive.class);

        boolean modulesAdded = false;
        for (Path moduleDir : moduleDirs) {
            if (moduleDir.toFile().exists()) {
                moduleArchive.addAsResource(moduleDir.toFile(), "modules");
                modulesAdded = true;
            }
        }

        if (modulesAdded) {
            moduleArchive.as(ZipExporter.class)
                    .exportTo(moduleJar, true);

            withClasspathEntry(moduleJar.toPath());
        }

        return this;
    }

    public SwarmExecutor withExecutableJar(Path jar) {
        if (this.executable != null) {
            throw new RuntimeException("Executable already specified: " + this.executable);
        }

        if (!this.classpath.isEmpty()) {
            throw new RuntimeException("Cannot use executable jar with a classpath");
        }

        this.executable = new ExecutableJar(jar);
        return this;
    }

    public SwarmExecutor withMainClass(String className) {
        if (this.executable != null) {
            throw new RuntimeException("Executable already specified: " + this.executable);
        }
        this.executable = new MainClass(className);
        return this;
    }

    public SwarmExecutor withDefaultMainClass() {
        return withMainClass("org.wildfly.swarm.Swarm");
    }

    public SwarmExecutor withArgument(String arg) {
        this.arguments.add(arg);
        return this;
    }

    public SwarmExecutor withArguments(List<String> args) {
        this.arguments.addAll(args);
        return this;
    }

    public SwarmExecutor withJVMArgument(String arg) {
        this.jvmArguments.add(arg);
        return this;
    }

    public SwarmExecutor withJVMArguments(List<String> args) {
        this.jvmArguments.addAll(args);
        return this;
    }

    public SwarmExecutor withWorkingDirectory(Path workingDirectory) {
        this.workingDirectory = workingDirectory;
        return this;
    }

    public SwarmExecutor withProcessFile(File processFile) {
        this.processFile = processFile;
        withProperty(Main.MAIN_PROCESS_FILE, processFile.getAbsolutePath());
        return this;
    }

    public SwarmProcess execute() throws IOException {
        if (this.executable == null) {
            throw new RuntimeException("An executable jar or a main-class must be specified");
        }

        List<String> cli = new ArrayList<>();

        cli.add(getJava().toString());

        if (debugPort != null) {
            // see https://docs.oracle.com/javase/8/docs/technotes/guides/jpda/conninv.html
            cli.add("-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=" + debugPort);
        }

        cli.addAll(this.jvmArguments);

        for (String name : this.properties.keySet()) {
            cli.add("-D" + name + "=" + this.properties.get(name));
        }

        if (!this.classpath.isEmpty()) {
            cli.add("-classpath");
            cli.add(String.join(File.pathSeparator,
                                this.classpath.stream()
                                        .map(e -> e.toString())
                                        .collect(Collectors.toList())));
        }

        cli.addAll(this.executable.toArguments());
        cli.addAll(this.arguments);

        final ProcessBuilder processBuilder = new ProcessBuilder(cli)
                .directory(this.workingDirectory.toFile());
        processBuilder.environment().putAll(environment);
        Process process = processBuilder.start();

        return new SwarmProcess(
                process, processFile,
                this.stdout, this.stdoutFile,
                this.stderr, this.stderrFile);
    }

    protected Path findJava() {
        String javaHome = System.getProperty("java.home");
        if (javaHome == null) {
            throw new RuntimeException("unable to locate java binary");
        }

        Path binDir = FileSystems.getDefault().getPath(javaHome, "bin");

        Path java = binDir.resolve("java.exe");
        if (java.toFile().exists()) {
            return java;
        }

        java = binDir.resolve("java");
        if (java.toFile().exists()) {
            return java;
        }

        throw new RuntimeException("unable to locate java binary");
    }

    private final OutputStream stdout;

    private final OutputStream stderr;

    private Path stdoutFile;

    private Path stderrFile;

    private Path java;

    private Map<String, String> properties = new HashMap<>();

    private Map<String, String> environment = new HashMap<>();

    private List<Path> classpath = new ArrayList<>();

    private List<String> jvmArguments = new ArrayList<>();

    private List<String> arguments = new ArrayList<>();

    private Executable executable;

    private Path workingDirectory;

    private Integer debugPort;

    private File processFile;

}
