package com.github.sormuras.bach.api;

import com.github.sormuras.bach.Bach;
import com.github.sormuras.bach.Command;
import com.github.sormuras.bach.Options;
import com.github.sormuras.bach.Recording;
import com.github.sormuras.bach.internal.ModuleLayerBuilder;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.module.ModuleFinder;
import java.time.Duration;
import java.time.Instant;
import java.util.List;
import java.util.ServiceLoader;
import java.util.spi.ToolProvider;
import java.util.stream.Stream;

/** Methods related to finding and running provided tools. */
public interface ToolProviderAPI {

  Bach bach();

  default Stream<ToolProvider> computeToolProviders() {
    var externals = bach().folders().externalModules();
    var layer = new ModuleLayerBuilder().before(ModuleFinder.of(externals)).build();
    return ServiceLoader.load(layer, ToolProvider.class).stream().map(ServiceLoader.Provider::get);
  }

  default ToolProvider computeToolProvider(String name) {
    var provider = computeToolProviders().filter(it -> it.name().equals(name)).findFirst();
    if (provider.isPresent()) return provider.get();
    throw new RuntimeException("No tool provider found for name: " + name);
  }

  default Recording run(Command<?> command) {
    bach().debug("Run %s", command.toLine());
    var provider = command instanceof ToolProvider it ? it : computeToolProvider(command.name());
    var arguments = command.toStrings();
    return run(provider, arguments);
  }

  default List<Recording> run(Command<?> command, Command<?>... commands) {
    return run(Stream.concat(Stream.of(command), Stream.of(commands)).toList());
  }

  default List<Recording> run(List<Command<?>> commands) {
    var size = commands.size();
    bach().debug("Run %d command%s", size, size == 1 ? "" : "s");
    if (size == 0) return List.of();
    if (size == 1) return List.of(run(commands.iterator().next()));
    var sequential = bach().is(Options.Flag.RUN_COMMANDS_SEQUENTIALLY);
    var stream = sequential ? commands.stream() : commands.stream().parallel();
    return stream.map(this::run).toList();
  }

  default Recording run(ToolProvider provider, List<String> arguments) {
    var name = provider.name();
    var currentThread = Thread.currentThread();
    var currentLoader = currentThread.getContextClassLoader();
    currentThread.setContextClassLoader(provider.getClass().getClassLoader());

    var out = new StringWriter();
    var err = new StringWriter();
    var args = arguments.toArray(String[]::new);
    var start = Instant.now();
    int code;
    try {
      var skips = bach().options().values(Options.Property.SKIP_TOOL);
      var skip = skips.contains(name);
      if (skip) bach().debug("Skip run of '%s' due to --skip-tool=%s", name, skips);
      code = skip ? 0 : provider.run(new PrintWriter(out), new PrintWriter(err), args);
    } finally {
      currentThread.setContextClassLoader(currentLoader);
    }

    var duration = Duration.between(start, Instant.now());
    var tid = currentThread.getId();
    var output = out.toString().trim();
    var errors = err.toString().trim();
    var recording = new Recording(name, arguments, tid, duration, code, output, errors);

    bach().record(recording);
    return recording;
  }
}

@@ -705,24 +705,52 @@
     
-var externals = 
+return computeToolProviders(ModuleFinder.of(
 bach
@@ -779,18 +779,100 @@
 odules()
+))
 ;%0A
+  %7D%0A%0A  default Stream%3CToolProvider%3E computeToolProviders(ModuleFinder finder) %7B%0A
     var 
@@ -915,34 +915,14 @@
 ore(
-ModuleFinder.of(externals)
+finder
 ).bu
@@ -2206,32 +2206,350 @@
 .toList();%0A  %7D%0A%0A
+  default Recording run(ModuleFinder finder, Command%3C?%3E command) %7B%0A    bach().debug(%22Run %25s%22, command.toLine());%0A    var provider = computeToolProviders(finder).filter(it -%3E it.name().equals(command.name())).findFirst().orElseThrow();%0A    var arguments = command.toStrings();%0A    return run(provider, arguments);%0A  %7D%0A%0A
   default Record

Extend ToolProviderAPI with run(ModuleFinder, Command) overload