package org.implab.gradle.common.tasks;

import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.stream.Stream;

import org.gradle.api.DefaultTask;
import org.gradle.api.file.DirectoryProperty;
import org.gradle.api.provider.ListProperty;
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.TaskCommandSpecMixin;
import org.implab.gradle.common.dsl.TaskPipeSpecMixin;
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.exec.ExecBuilder;
import org.implab.gradle.common.utils.ObjectsMixin;
import org.implab.gradle.common.utils.Strings;
import org.implab.gradle.common.utils.ThrowingConsumer;

public abstract class ShellExecTask
        extends DefaultTask
        implements TaskCommandSpecMixin, TaskPipeSpecMixin, TaskEnvSpecMixin, ObjectsMixin {

    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();

    @Internal
    @Override
    public abstract ListProperty<String> getCommandLine();

    /**
     * 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;
    }

    @Override
    public void commandLine(Object arg0, Object... args) {
        getCommandLine().set(provider(() -> Stream.concat(
                Stream.of(arg0),
                Stream.of(args))
                .map(Strings::asString).toList()));

    }

    @Override
    public void args(Object arg0, Object... args) {
        getCommandLine().addAll(provider(() -> Stream.concat(
                Stream.of(arg0),
                Stream.of(args))
                .map(Strings::asString).toList()));
    }

    protected abstract ExecBuilder execBuilder();

    @TaskAction
    public final void run() throws IOException, InterruptedException, ExecutionException {
        var execBuilder = execBuilder();
        execBuilder.workingDirectory(getWorkingDirectory().get().getAsFile());
        execBuilder.environment(getEnvironment().getOrElse(Map.of()));
        execBuilder.commandLine(getCommandLine().get());

        getStdout().getRedirection().ifPresent(execBuilder::stdout);
        getStderr().getRedirection().ifPresent(execBuilder::stderr);
        getStdin().getRedirection().ifPresent(execBuilder::stdin);

        execBuilder.exec().thenAccept(ThrowingConsumer.guard(this::checkRetCode)).join();
    }

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

}
