package org.implab.gradle.common.exec.runtime;

import java.util.HashMap;
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 org.implab.gradle.common.exec.model.EnvironmentSpec;
import org.implab.gradle.common.exec.model.PipeSpec;

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

import static java.util.Objects.requireNonNull;

/** Command line builder */
@NonNullByDefault
public abstract class AbstractExecBuilder<CS> implements ShellExec {

    private boolean inheritEnvironment = true;

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

    private @Nullable File directory;

    private RedirectFrom inputRedirect;

    private RedirectTo outputRedirect;

    private RedirectTo errorRedirect;

    private final CS command;

    protected AbstractExecBuilder(CS command) {
        this.command = command;
    }


    /** Sets the working directory */
    @Override
    public ShellExec workingDirectory(File directory) {
        requireNonNull(directory, "directory parameter can't be null");

        this.directory = directory;
        return this;
    }

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

        this.directory = directory.orElse(null);
        return this;
    }

    @Override
    public ShellExec inheritEnvironment(boolean inherit) {
        this.inheritEnvironment = inherit;
        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 ShellExec putEnvironment(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 ShellExec 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 ShellExec stdin(RedirectFrom from) {
        requireNonNull(from, "from parameter can't be null");

        inputRedirect = from;
        return this;
    }

    @Override
    public ShellExec 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 ShellExec stdout(RedirectTo out) {
        requireNonNull(out, "out parameter can't be null");
        outputRedirect = out;
        return this;
    }

    @Override
    public ShellExec 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 ShellExec stderr(RedirectTo err) {
        requireNonNull(err, "err parameter can't be null");
        errorRedirect = err;
        return this;
    }

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

    @Override
    public ShellExec from(PipeSpec pipeSpec) {
        ShellExec.super.from(pipeSpec);
        return this;
    }

    @Override
    public ShellExec from(EnvironmentSpec environmentSpec) {
        ShellExec.super.from(environmentSpec);
        return this;
    }

    /** Implement this function to */
    protected abstract CompletableFuture<Integer> startInternal(
            CS command,
            EnvironmentSpec environment,
            PipeSpec redirect) 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> exec() throws IOException {
        return startInternal(
                command,
                new SelfEnvironmentSpec(),
                new SelfPipeSpec());
    }

    private class SelfEnvironmentSpec implements EnvironmentSpec {
        @Override
        public boolean inheritEnvironment() {
            return inheritEnvironment;
        }

        @Override
        public Map<String, String> environment() {
            return environment;
        }

        @Override
        public Optional<File> workingDirectory() {
            return Optional.ofNullable(directory);
        }
    }

    private class SelfPipeSpec implements PipeSpec {

        @Override
        public Optional<RedirectTo> stdout() {
            return Optional.ofNullable(outputRedirect);
        }

        @Override
        public Optional<RedirectTo> stderr() {
            return Optional.ofNullable(errorRedirect);
        }

        @Override
        public Optional<RedirectFrom> stdin() {
            return Optional.ofNullable(inputRedirect);
        }

    }

}
