package org.implab.gradle.common.exec;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Stream;
import java.util.Collection;

import org.eclipse.jdt.annotation.NonNullByDefault;

import java.io.File;
import java.io.IOException;

/** Command line builder */
@NonNullByDefault
public abstract class ExecBuilder {

    private final List<String> commandLine = new ArrayList<>();

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

    private File directory;

    private RedirectFrom inputRedirect;

    private RedirectTo outputRedirect;

    private RedirectTo errorRedirect;

    /** Sets command line, clears previous one if any */
    public ExecBuilder command(String cmd, String... args) {
        commandLine.clear();
        commandLine.add(cmd);
        Stream.of(args).forEach(commandLine::add);
        return this;
    }

    public ExecBuilder command(Collection<String> cmd) {
        this.commandLine.clear();
        this.commandLine.addAll(cmd);
        return this;
    }

    /** Adds an argument to the command line */
    public ExecBuilder argument(String arg) {
        Objects.requireNonNull(arg, "arg parameter can't be null");
        commandLine.add(arg);
        return this;
    }

    /** Sets the working directory */
    public ExecBuilder directory(File directory) {
        Objects.requireNonNull(directory, "directory parameter can't be null");
        this.directory = directory;
        return this;
    }

    /**
     * Sets the environment value. The value cannot be null.
     * 
     * @param envVar The name of the environment variable
     * @param value  The value to set,
     */
    public ExecBuilder setEnvironment(String envVar, String value) {
        Objects.requireNonNull(value, "Value can't be null");
        Objects.requireNonNull(envVar, "envVar parameter can't be null");
        environment.put(envVar, value);
        return this;
    }

    public ExecBuilder setEnvironment(Map<String, String> env) {
        Objects.requireNonNull(env, "env parameter can't be null");

        environment.clear();
        environment.putAll(env);
        return this;
    }

    public ExecBuilder unsetEnvironment(String envVar) {
        environment.remove(envVar);
        return this;
    }

    /**
     * Sets redirection for the stdin, {@link RedirectFrom} will be applied
     * every time the process is started.
     * 
     * <p>
     * If redirection
     */
    public ExecBuilder stdin(RedirectFrom from) {
        Objects.requireNonNull(from, "from parameter can't be null");
        inputRedirect = from;
        return this;
    }

    /**
     * Sets redirection for the stdout, {@link RedirectTo} will be applied
     * every time the process is started.
     */
    public ExecBuilder stdout(RedirectTo out) {
        Objects.requireNonNull(out, "out parameter can't be null");
        outputRedirect = out;
        return this;
    }

    /**
     * Sets redirection for the stderr, {@link RedirectTo} will be applied
     * every time the process is started.
     */
    public ExecBuilder stderr(RedirectTo err) {
        Objects.requireNonNull(err, "err parameter can't be null");
        errorRedirect = err;
        return this;
    }

    /** Implement this function to  */
    protected abstract CompletableFuture<Integer> startInternal(
            Map<String, String> environment,
            File directory,
            List<String> commandLine,
            Optional<RedirectFrom> stdinRedirect,
            Optional<RedirectTo> stdoutRedirect,
            Optional<RedirectTo> stderrRedirect) throws IOException;

    /**
     * Creates and starts new process and returns {@link CompletableFuture}. The
     * process may be a remote process, or a shell process or etc.
     * 
     * @return
     * @throws IOException
     */
    public CompletableFuture<Integer> start() throws IOException {
        return startInternal(
                environment,
                directory,
                commandLine,
                Optional.ofNullable(inputRedirect),
                Optional.ofNullable(outputRedirect),
                Optional.ofNullable(errorRedirect));
    }

}
