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.Optional;
import java.util.concurrent.CompletableFuture;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;

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

import static java.util.Objects.requireNonNull;

/** Command line builder */
@NonNullByDefault
public abstract class ExecBuilder implements
    CommandBuilder,
    PipeBuilder,
    EnvironmentBuilder,
    Runnable
{
    private String executable;

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

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

    private @Nullable File directory;

    private RedirectFrom inputRedirect;

    private RedirectTo outputRedirect;

    private RedirectTo errorRedirect;

    @Override
    public ExecBuilder executable(String executable) {
        requireNonNull(executable, "cmd can't be null");
        this.executable = executable;
        return this;
    }

    @Override
    public ExecBuilder arguments(Iterable<String> args) {
        requireNonNull(args, "Args must not be null");
        arguments.clear();
        for(var arg: args)
            arguments.add(arg);
        return this;
    }

    public ExecBuilder commandLine(Iterable<? extends String> cmd) {
        requireNonNull(cmd, "cmd can't be null");

        this.arguments.clear();
        for (var arg : cmd)
            this.arguments.add(arg);
        return this;
    }

    @Override
    public ExecBuilder arg(String arg0, String... args) {
        requireNonNull(arg0, "arg0 parameter can't be null");
        arguments.add(arg0);

        for (var arg : args)
            arguments.add(arg);

        return this;
    }

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

    @Override
    public ExecBuilder workingDirectory(Optional<? extends File> directory) {
        requireNonNull(directory, "directory parameter can't be null");

        this.directory = directory.orElse(null);
        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,
     */
    @Override
    public ExecBuilder setVariable(String envVar, String value) {
        requireNonNull(value, "Value can't be null");
        requireNonNull(envVar, "envVar parameter can't be null");

        environment.put(envVar, value);
        return this;
    }

    @Override
    public ExecBuilder environment(Map<String,? extends String> env) {
        requireNonNull(env, "env parameter can't be null");

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

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

    @Override
    public PipeBuilder stdin(Optional<? extends RedirectFrom> from) {
        requireNonNull(from, "from parameter can't be null");
        inputRedirect = from.orElse(null);
        return this;
    }

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

    @Override
    public PipeBuilder stdout(Optional<? extends RedirectTo> to) {
        requireNonNull(to, "from parameter can't be null");
        outputRedirect = to.orElse(null);
        return this;
    }

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

    @Override
    public PipeBuilder stderr(Optional<? extends RedirectTo> to) {
        requireNonNull(to, "from parameter can't be null");
        errorRedirect = to.orElse(null);
        return this;
    }

    /** Implement this function to */
    protected abstract CompletableFuture<Integer> startInternal(
            Map<String, String> environment,
            Optional<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> run() throws IOException {
        if (executable == null || executable.isEmpty())
            throw new IllegalStateException("The executable isn't set");

        var commandLine = new ArrayList<String>();
        commandLine.add(executable);
        commandLine.addAll(arguments);

        return startInternal(
                environment,
                Optional.ofNullable(directory),
                commandLine,
                Optional.ofNullable(inputRedirect),
                Optional.ofNullable(outputRedirect),
                Optional.ofNullable(errorRedirect));
    }

}
