package org.implab.gradle.common.tasks;

import java.io.IOException;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ExecutionException;

import org.gradle.api.DefaultTask;
import org.gradle.api.file.DirectoryProperty;
import org.gradle.api.provider.MapProperty;
import org.gradle.api.provider.Property;
import org.gradle.api.tasks.Internal;
import org.gradle.api.tasks.TaskAction;
import org.implab.gradle.common.dsl.RedirectFromSpec;
import org.implab.gradle.common.dsl.RedirectToSpec;
import org.implab.gradle.common.dsl.TaskEnvSpecMixin;
import org.implab.gradle.common.dsl.TaskPipeSpecMixin;
import org.implab.gradle.common.exec.RedirectTo;
import org.implab.gradle.common.exec.ShellExec;
import org.implab.gradle.common.utils.Exceptions;
import org.implab.gradle.common.utils.Values;

public abstract class AbstractShellExecTask
        extends DefaultTask
        implements TaskPipeSpecMixin, TaskEnvSpecMixin {

    private final RedirectToSpec redirectStderr = new RedirectToSpec();

    private final RedirectToSpec redirectStdout = new RedirectToSpec();

    private final RedirectFromSpec redirectStdin = new RedirectFromSpec();

    @Internal
    @Override
    public abstract Property<Boolean> getInheritEnvironment();

    @Internal
    @Override
    public abstract DirectoryProperty getWorkingDirectory();

    @Internal
    @Override
    public abstract MapProperty<String, String> getEnvironment();

    /**
     * STDIN redirection, if not specified, no input will be passed to the command
     */
    @Internal
    @Override
    public RedirectFromSpec getStdin() {
        return redirectStdin;
    }

    /**
     * STDOUT redirection, if not specified, redirected to logger::info
     */
    @Internal
    @Override
    public RedirectToSpec getStdout() {
        return redirectStdout;
    }

    /**
     * STDERR redirection, if not specified, redirected to logger::error
     */
    @Internal
    @Override
    public RedirectToSpec getStderr() {
        return redirectStderr;
    }

    protected abstract ShellExec execBuilder();

    protected Optional<RedirectTo> conventionalStderr() {
        return Optional.of(RedirectTo.eachLine(getLogger()::error));
    }

    protected Optional<RedirectTo> conventionalStdout() {
        return Optional.of(RedirectTo.eachLine(getLogger()::info));
    }

    @TaskAction
    public final void run() throws IOException, InterruptedException, ExecutionException {
        // create new shell process
        var execBuilder = execBuilder();

        // configure environment
        execBuilder
                .workingDirectory(Values.optional(getWorkingDirectory().getAsFile()))
                .environment(getEnvironment().getOrElse(Map.of()));

        // configure redirects
        execBuilder
                .stdout(getStdout().getRedirection().or(this::conventionalStdout))
                .stderr(getStderr().getRedirection().or(this::conventionalStderr))
                .stdin(getStdin().getRedirection());

        // execute
        execBuilder.exec()
                .thenAccept(Exceptions.unchecked(this::assertExitCode))
                .join();
    }

    protected void assertExitCode(Integer code) throws IOException {
        if (code != 0)
            throw new IOException(String.format("The process is terminated with code %s", code));
    }
}