<commit_before>/*
 * Bach - Java Shell Builder
 * Copyright (C) 2020 <NAME>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

// default package

import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.UncheckedIOException;
import java.lang.System.Logger;
import java.lang.System.Logger.Level;
import java.lang.module.ModuleDescriptor;
import java.lang.module.ModuleDescriptor.Requires;
import java.lang.module.ModuleDescriptor.Version;
import java.lang.module.ModuleFinder;
import java.lang.module.ModuleReference;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Deque;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.StringJoiner;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.regex.Pattern;
import java.util.spi.ToolProvider;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

/**
 * Java Shell Builder.
 *
 * <p>Requires JDK 11 or later.
 */
public class Bach {

  /** Version of the Java Shell Builder. */
  private static final Version VERSION = Version.parse("11.0-ea");

  /** Default logger instance. */
  private static final Logger LOGGER = System.getLogger("Bach.java");

  /** Default printer instance. */
  private static final Consumer<String> PRINTER = System.out::println;

  /** Default verbosity flag, including {@code -Debug} support. */
  private static final boolean VERBOSE =
      Boolean.getBoolean("verbose") // -D verbose=true
          || Boolean.getBoolean("ebug") // -Debug=true
          || "".equals(System.getProperty("ebug")); // -Debug

  /** Bach.java's main program entry-point. */
  public static void main(String... args) {
    try {
      new Bach().new Main(args).run();
    } catch (Throwable throwable) {
      throw new Error("Main program of Bach.java failed", throwable);
    }
  }

  /** Logger instance. */
  private final Logger logger;

  /** Line-based message printing consumer. */
  private final Consumer<String> printer;

  /** Verbosity flag. */
  private final boolean verbose;

  /** Initialize this instance with default values. */
  public Bach() {
    this(LOGGER, PRINTER, VERBOSE);
  }

  /** Initialize this instance with the specified arguments. */
  public Bach(Logger logger, Consumer<String> printer, boolean verbose) {
    this.logger = logger;
    this.printer = printer;
    this.verbose = verbose;
    logger.log(Level.TRACE, "Initialized Bach.java " + VERSION);
  }

  /** Build project located in the current working directory. */
  public void build() {
    build(project -> {}).assertSuccessful();
  }

  /** Build project based on the given builder instance. */
  public Build.Summary build(Consumer<Project.Builder> consumer) {
    var base = Path.of("");
    return build(Project.Paths.of(base), consumer);
  }

  /** Build project based on the builder initialized by scanning the given base path. */
  public Build.Summary build(Project.Paths paths, Consumer<Project.Builder> consumer) {
    var builder = new Project.Scanner(paths).scan();
    consumer.accept(builder);
    return build(builder.build());
  }

  /** Build the specified project using the default build factory supplying the root build task. */
  public Build.Summary build(Project project) {
    var factory = new Build.Factory(project, verbose);
    return build(project, factory::build);
  }

  /** Build the specified project using the given root task supplier. */
  public Build.Summary build(Project project, Supplier<Build.Task> task) {
    var start = Instant.now();
    logger.log(Level.DEBUG, "Build {0}", project);
    printer.accept("Build " + project.descriptor().toNameAndVersion());
    if (verbose) project.print(printer);

    var summary = new Build.Summary(project);
    Build.execute(this, task.get(), summary);

    var markdown = summary.write();
    var duration =
        Duration.between(start, Instant.now())
            .toString()
            .substring(2)
            .replaceAll("(\\d[HMS])(?!$)", "$1 ")
            .toLowerCase();

    printer.accept("Summary written to " + markdown.toUri());
    printer.accept("Build took " + duration);
    return summary;
  }

  /** Project model. */
  public static final class Project implements Util.Printable {

    /** Base path of the project. */
    private final Paths paths;

    /** Project descriptor. */
    private final ModuleDescriptor descriptor;

    /** Project structure. */
    private final Structure structure;

    /** Library of assets. */
    private final Library library;

    /** Initialize this project model. */
    public Project(Paths paths, ModuleDescriptor descriptor, Structure structure, Library library) {
      this.paths = paths;
      this.descriptor = descriptor;
      this.structure = structure;
      this.library = library;
    }

    /** Project paths. */
    public Paths paths() {
      return paths;
    }

    /** Project model descriptor. */
    public ModuleDescriptor descriptor() {
      return descriptor;
    }

    /** Project structure. */
    public Structure structure() {
      return structure;
    }

    /** Library of assets. */
    public Library library() {
      return library;
    }

    /** Return list of modular units. */
    public List<Unit> units() {
      return structure().units();
    }

    /** Return list of modular realms. */
    public List<Realm> realms() {
      return structure().realms();
    }

    /** Return the project main module. */
    public Optional<String> mainModule() {
      return descriptor.mainClass();
    }

    @Override
    public String toString() {
      return new StringJoiner(", ", "Project [", "]")
          .add("paths=" + paths())
          .add("descriptor=" + descriptor())
          .add("mainModule=" + mainModule())
          .add("loader=" + library())
          .toString();
    }

    /** Source directory tree layout. */
    public enum Layout {
      /**
       * Unnamed or flat realm source directory tree layout.
       *
       * <ul>
       *   <li>{@code ${SRC}/${MODULE}/module-info.java}
       * </ul>
       *
       * Module source path examples:
       *
       * <ul>
       *   <li>{@code --module-source-path .}
       *   <li>{@code --module-source-path src}
       *   <li>{@code --module-source-path src/modules}
       *   <li>{@code --module-source-path src/group-1:src/group-2}
       * </ul>
       *
       * @see <a href="https://openjdk.java.net/projects/jigsaw/quick-start">Project Jigsaw: Module
       *     System Quick-Start Guide</a>
       */
      FLAT,

      /**
       * Source tree layout with main and test realm and nested categories.
       *
       * <ul>
       *   <li>{@code ${SRC}/${MODULE}/${OFFSET}/main/java/module-info.java}
       *   <li>{@code ${SRC}/${MODULE}/${OFFSET}/test/java/module-info.java}
       *   <li>{@code ${SRC}/${MODULE}/${OFFSET}/test/module/module-info.java}
       * </ul>
       *
       * Module source path examples:
       *
       * <ul>
       *   <li>{@code --module-source-path src/ * /main/java}
       *   <li>{@code --module-source-path src/ * /test/java:src/ * /test/module}
       * </ul>
       */
      MAIN_TEST {
        @Override
        public Optional<String> realmOf(Unit unit) {
          var deque = deque(unit);
          var category = deque.pop();
          if (!(category.equals("java")
              || category.matches("java-\\d+")
              || category.equals("module"))) return Optional.empty();
          var realm = deque.pop();
          if (!(realm.equals("main") || realm.equals("test"))) return Optional.empty();
          if (Collections.frequency(deque(unit), unit.name()) != 1) return Optional.empty();
          return Optional.of(realm);
        }

        @Override
        public List<Realm> realmsOf(List<Unit> units) {
          var map = new HashMap<String, List<Unit>>();
          for (var unit : units) {
            var realm = realmOf(unit).orElse(unit.path().toString());
            map.computeIfAbsent(realm, key -> new ArrayList<>()).add(unit);
          }
          if (!map.keySet().equals(Set.of("main", "test")))
            throw new IllegalStateException("Only main and test realm expected: " + map);
          var main =
              new Realm(
                  "main",
                  EnumSet.of(Realm.Modifier.CREATE_JAVADOC),
                  0,
                  moduleSourcePath(map.get("main")),
                  Unit.toMap(units.stream().filter(u -> realmOf(u).orElseThrow().equals("main"))),
                  List.of());
          var test =
              new Realm(
                  "test",
                  // EnumSet.of(Realm.Modifier.LAUNCH_TESTS, Realm.Modifier.ENABLE_PREVIEW),
                  EnumSet.of(Realm.Modifier.LAUNCH_TESTS),
                  0,
                  moduleSourcePath(map.get("test")),
                  Unit.toMap(units.stream().filter(u -> realmOf(u).orElseThrow().equals("test"))),
                  List.of(main));
          return List.of(main, test);
        }
      };

      /** Extract the name of the realm from the given modular unit. */
      public Optional<String> realmOf(Unit unit) {
        if (Collections.frequency(deque(unit), unit.name()) != 1) return Optional.empty();
        return Optional.of("");
      }

      /** Create realms based on the given units. */
      public List<Realm> realmsOf(List<Unit> units) {
        if (units.isEmpty()) return List.of();
        var name = "";
        var modifiers = EnumSet.of(Realm.Modifier.CREATE_JAVADOC);
        var moduleSourcePath = moduleSourcePath(units);
        var modules = Unit.toMap(units.stream());
        return List.of(new Realm(name, modifiers, 0, moduleSourcePath, modules, List.of()));
      }

      /** Convert path element names of the given unit into a reversed deque. */
      public static Deque<String> deque(Unit unit) {
        var path = unit.path();
        var deque = new ArrayDeque<String>();
        path.forEach(name -> deque.addFirst(name.toString()));
        var info = deque.pop();
        if (!info.equals("module-info.java"))
          throw new IllegalArgumentException("No module-info.java?! " + path);
        return deque;
      }

      /** Compute distinct module source path from the given list of units. */
      public static String moduleSourcePath(List<Unit> units) {
        return units.stream()
            .map(Unit::moduleSourcePath)
            .distinct()
            .collect(Collectors.joining(File.pathSeparator));
      }

      /** Scan the given units for a well-known modular source layout. */
      public static Optional<Layout> find(List<Unit> units) {
        for (var layout : List.of(Layout.MAIN_TEST, Layout.FLAT))
          if (units.stream().allMatch(unit -> layout.realmOf(unit).isPresent()))
            return Optional.of(layout);
        return Optional.empty();
      }
    }

    /** Common project-related paths. */
    public static final class Paths implements Util.Printable {

      public static final Path CLASSES = Path.of("classes");
      public static final Path MODULES = Path.of("modules");
      public static final Path SOURCES = Path.of("sources");
      public static final Path DOCUMENTATION = Path.of("documentation");
      public static final Path JAVADOC = DOCUMENTATION.resolve("javadoc");

      public static Paths of(Path base) {
        return new Paths(base, base.resolve(".bach"), base.resolve("lib"));
      }

      private final Path base;
      private final Path out;
      private final Path lib;

      public Paths(Path base, Path out, Path lib) {
        this.base = base;
        this.out = out;
        this.lib = lib;
      }

      public Path base() {
        return base;
      }

      public Path out() {
        return out;
      }

      public Path classes(Realm realm) {
        return out.resolve(CLASSES).resolve(realm.name());
      }

      public Path javadoc() {
        return out.resolve(JAVADOC);
      }

      public Path modules(Realm realm) {
        return out.resolve(MODULES).resolve(realm.name());
      }

      public Path sources(Realm realm) {
        return out.resolve(SOURCES).resolve(realm.name());
      }

      public Path lib() {
        return lib;
      }

      @Override
      public String toString() {
        return new StringJoiner(", ", Paths.class.getSimpleName() + "[", "]")
            .add("base='" + base() + "'")
            .add("out='" + out() + "'")
            .add("lib='" + lib() + "'")
            .toString();
      }
    }

    /** Single source path with optional release directive. */
    public static final class Source {

      /** Source-specific modifier enumeration. */
      public enum Modifier {
        /** Store binary assets in {@code META-INF/versions/${release}/} directory of the jar. */
        VERSIONED
      }

      /** Create default non-targeted source for the specified path and optional modifiers. */
      public static Source of(Path path, Modifier... modifiers) {
        return new Source(path, 0, Set.of(modifiers));
      }

      private final Path path;
      private final int release;
      private final Set<Modifier> modifiers;

      public Source(Path path, int release, Set<Modifier> modifiers) {
        this.path = path;
        this.release = release;
        this.modifiers = modifiers.isEmpty() ? Set.of() : EnumSet.copyOf(modifiers);
      }

      /** Source path. */
      public Path path() {
        return path;
      }

      /** Java feature release target number, with zero indicating the current runtime release. */
      public int release() {
        return release;
      }

      /** This source modifiers. */
      public Set<Modifier> modifiers() {
        return modifiers;
      }

      public boolean isTargeted() {
        return release != 0;
      }

      /** Optional Java feature release target number. */
      public OptionalInt target() {
        return isTargeted() ? OptionalInt.of(release) : OptionalInt.empty();
      }

      public boolean isVersioned() {
        return modifiers.contains(Modifier.VERSIONED);
      }

      @Override
      public String toString() {
        return new StringJoiner(", ", "Source { ", " }")
            .add("path=" + path())
            .add("release=" + release())
            .add("target=" + target())
            .add("modifiers=" + modifiers())
            .toString();
      }
    }

    /** A modular project structure. */
    public static final class Structure implements Util.Printable {

      /** List of all declared modular units. */
      private final List<Unit> units;

      /** List of all realms. */
      private final List<Realm> realms;

      public Structure(List<Unit> units, List<Realm> realms) {
        this.units = List.copyOf(units);
        this.realms = List.copyOf(realms);
      }

      public List<Unit> units() {
        return units;
      }

      public List<Realm> realms() {
        return realms;
      }

      @Override
      public String toString() {
        return new StringJoiner(", ", "Structure { ", " }")
            .add("units=" + units())
            .add("realms=" + realms())
            .toString();
      }
    }

    /** A module source description unit. */
    public static final class Unit implements Util.Printable {

      public static Map<String, Unit> toMap(Stream<Unit> units) {
        return units.collect(Collectors.toMap(Unit::name, Function.identity()));
      }

      private final Path path;
      private final ModuleDescriptor descriptor;
      private final String moduleSourcePath;
      private final List<Source> sources;
      private final List<Path> resources;

      private Unit(Path info) {
        this(info, Util.Modules.describe(info));
      }

      private Unit(Path info, ModuleDescriptor descriptor) {
        this(
            info,
            descriptor,
            Util.Modules.moduleSourcePath(info, descriptor.name()),
            List.of(Source.of(info.getParent())),
            List.of());
      }

      public Unit(
          Path path,
          ModuleDescriptor descriptor,
          String moduleSourcePath,
          List<Source> sources,
          List<Path> resources) {
        this.path = path;
        this.descriptor = descriptor;
        this.moduleSourcePath = moduleSourcePath;
        this.sources = sources;
        this.resources = resources;
      }

      public Path path() {
        return path;
      }

      public ModuleDescriptor descriptor() {
        return descriptor;
      }

      public String moduleSourcePath() {
        return moduleSourcePath;
      }

      public List<Source> sources() {
        return sources;
      }

      public List<Path> resources() {
        return resources;
      }

      @Override
      public String toString() {
        @SuppressWarnings("StringBufferReplaceableByString")
        var sb = new StringBuilder("Unit{");
        sb.append("path=").append(path());
        sb.append(", descriptor=").append(descriptor());
        sb.append(", moduleSourcePath=").append(moduleSourcePath());
        sb.append(", sources=").append(sources());
        sb.append(", resources=").append(resources());
        sb.append(", isMultiRelease=").append(isMultiRelease());
        sb.append(", isMainClassPresent=").append(isMainClassPresent());
        sb.append('}');
        return sb.toString();
      }

      public String name() {
        return descriptor.name();
      }

      @Override
      public String printCaption() {
        return "Unit '" + name() + "'";
      }

      public <T> List<T> sources(Function<Source, T> mapper) {
        return sources.stream().map(mapper).collect(Collectors.toList());
      }

      public boolean isMultiRelease() {
        return !sources.isEmpty() && sources.stream().allMatch(Source::isTargeted);
      }

      public boolean isMainClassPresent() {
        return descriptor.mainClass().isPresent();
      }
    }

    /** A realm of modular sources. */
    public static final class Realm implements Util.Printable {

      /** Realm-related flags controlling the build process. */
      public enum Modifier {
        ENABLE_PREVIEW,
        CREATE_JAVADOC,
        LAUNCH_TESTS
      }

      private final String name;
      private final Set<Modifier> modifiers;
      private final int release;
      private final String moduleSourcePath;
      private final Map<String, Unit> units;
      private final List<Realm> dependencies;

      public Realm(
          String name,
          Set<Modifier> modifiers,
          int release,
          String moduleSourcePath,
          Map<String, Unit> units,
          List<Realm> dependencies) {
        this.name = name;
        this.modifiers = modifiers.isEmpty() ? Set.of() : EnumSet.copyOf(modifiers);
        this.release = release;
        this.moduleSourcePath = moduleSourcePath;
        this.units = Map.copyOf(units);
        this.dependencies = List.copyOf(dependencies);
      }

      public String name() {
        return name;
      }

      public Set<Modifier> modifiers() {
        return modifiers;
      }

      public OptionalInt release() {
        return release == 0 ? OptionalInt.empty() : OptionalInt.of(release);
      }

      public String moduleSourcePath() {
        return moduleSourcePath;
      }

      public Map<String, Unit> units() {
        return units;
      }

      public List<Realm> dependencies() {
        return dependencies;
      }

      @Override
      public String toString() {
        return new StringJoiner(", ", "Realm { ", " }")
            .add("name='" + name + "'")
            .add("modifiers=" + modifiers)
            .add("release=" + release)
            .add("units=" + units)
            .add("dependencies=" + dependencies)
            .toString();
      }

      public boolean lacks(Modifier modifier) {
        return !modifiers.contains(modifier);
      }

      public boolean test(Modifier modifier) {
        return modifiers.contains(modifier);
      }

      /** Generate {@code --patch-module} strings for this realm. */
      public List<String> patches(BiFunction<Realm, Unit, List<Path>> patcher) {
        if (dependencies().isEmpty()) return List.of();
        var patches = new ArrayList<String>();
        for (var unit : units().values()) {
          var module = unit.name();
          for (var dependency : dependencies()) {
            var other = dependency.units().get(module);
            if (other == null) continue;
            var paths = Util.Strings.join(patcher.apply(dependency, other));
            patches.add(module + '=' + paths);
          }
        }
        return patches.isEmpty() ? List.of() : List.copyOf(patches);
      }

      @Override
      public String printCaption() {
        return "Realm '" + name() + "'";
      }

      @Override
      public boolean printTest(String name, Object value) {
        return !name.equals("units");
      }
    }

    /** Library of assets. */
    public static final class Library implements Util.Printable {
      private final ModuleMapper mapper;

      public Library(ModuleMapper mapper) {
        this.mapper = mapper;
      }

      public ModuleMapper mapper() {
        return mapper;
      }

      @Override
      public String toString() {
        return new StringJoiner(", ", Library.class.getSimpleName() + "[", "]")
            .add("mapper=" + mapper())
            .toString();
      }
    }

    /** Mapping module names to their related coordinates. */
    public interface ModuleMapper extends BiFunction<String, Version, ModuleMapper.Mapping> {

      /** Module and version to URI and more other-system-property mapping. */
      final class Mapping implements Util.Printable {

        public static Mapping ofMavenCentral(
            String module, Version version, String group, String artifact, String classifier) {
          var repository = "https://repo.apache.maven.org/maven2";
          var uri = uri(repository, group, artifact, version.toString(), classifier, "jar");
          return new Mapping(module, version, uri);
        }

        public static URI uri(
            String repository,<commit_msg>Add convenient null-handling URI look-up helper<commit_after>/*
 * Bach - Java Shell Builder
 * Copyright (C) 2020 <NAME>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

// default package

import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.UncheckedIOException;
import java.lang.System.Logger;
import java.lang.System.Logger.Level;
import java.lang.module.ModuleDescriptor;
import java.lang.module.ModuleDescriptor.Requires;
import java.lang.module.ModuleDescriptor.Version;
import java.lang.module.ModuleFinder;
import java.lang.module.ModuleReference;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Deque;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.StringJoiner;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.regex.Pattern;
import java.util.spi.ToolProvider;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

/**
 * Java Shell Builder.
 *
 * <p>Requires JDK 11 or later.
 */
public class Bach {

  /** Version of the Java Shell Builder. */
  private static final Version VERSION = Version.parse("11.0-ea");

  /** Default logger instance. */
  private static final Logger LOGGER = System.getLogger("Bach.java");

  /** Default printer instance. */
  private static final Consumer<String> PRINTER = System.out::println;

  /** Default verbosity flag, including {@code -Debug} support. */
  private static final boolean VERBOSE =
      Boolean.getBoolean("verbose") // -D verbose=true
          || Boolean.getBoolean("ebug") // -Debug=true
          || "".equals(System.getProperty("ebug")); // -Debug

  /** Bach.java's main program entry-point. */
  public static void main(String... args) {
    try {
      new Bach().new Main(args).run();
    } catch (Throwable throwable) {
      throw new Error("Main program of Bach.java failed", throwable);
    }
  }

  /** Logger instance. */
  private final Logger logger;

  /** Line-based message printing consumer. */
  private final Consumer<String> printer;

  /** Verbosity flag. */
  private final boolean verbose;

  /** Initialize this instance with default values. */
  public Bach() {
    this(LOGGER, PRINTER, VERBOSE);
  }

  /** Initialize this instance with the specified arguments. */
  public Bach(Logger logger, Consumer<String> printer, boolean verbose) {
    this.logger = logger;
    this.printer = printer;
    this.verbose = verbose;
    logger.log(Level.TRACE, "Initialized Bach.java " + VERSION);
  }

  /** Build project located in the current working directory. */
  public void build() {
    build(project -> {}).assertSuccessful();
  }

  /** Build project based on the given builder instance. */
  public Build.Summary build(Consumer<Project.Builder> consumer) {
    var base = Path.of("");
    return build(Project.Paths.of(base), consumer);
  }

  /** Build project based on the builder initialized by scanning the given base path. */
  public Build.Summary build(Project.Paths paths, Consumer<Project.Builder> consumer) {
    var builder = new Project.Scanner(paths).scan();
    consumer.accept(builder);
    return build(builder.build());
  }

  /** Build the specified project using the default build factory supplying the root build task. */
  public Build.Summary build(Project project) {
    var factory = new Build.Factory(project, verbose);
    return build(project, factory::build);
  }

  /** Build the specified project using the given root task supplier. */
  public Build.Summary build(Project project, Supplier<Build.Task> task) {
    var start = Instant.now();
    logger.log(Level.DEBUG, "Build {0}", project);
    printer.accept("Build " + project.descriptor().toNameAndVersion());
    if (verbose) project.print(printer);

    var summary = new Build.Summary(project);
    Build.execute(this, task.get(), summary);

    var markdown = summary.write();
    var duration =
        Duration.between(start, Instant.now())
            .toString()
            .substring(2)
            .replaceAll("(\\d[HMS])(?!$)", "$1 ")
            .toLowerCase();

    printer.accept("Summary written to " + markdown.toUri());
    printer.accept("Build took " + duration);
    return summary;
  }

  /** Project model. */
  public static final class Project implements Util.Printable {

    /** Base path of the project. */
    private final Paths paths;

    /** Project descriptor. */
    private final ModuleDescriptor descriptor;

    /** Project structure. */
    private final Structure structure;

    /** Library of assets. */
    private final Library library;

    /** Initialize this project model. */
    public Project(Paths paths, ModuleDescriptor descriptor, Structure structure, Library library) {
      this.paths = paths;
      this.descriptor = descriptor;
      this.structure = structure;
      this.library = library;
    }

    /** Project paths. */
    public Paths paths() {
      return paths;
    }

    /** Project model descriptor. */
    public ModuleDescriptor descriptor() {
      return descriptor;
    }

    /** Project structure. */
    public Structure structure() {
      return structure;
    }

    /** Library of assets. */
    public Library library() {
      return library;
    }

    /** Return list of modular units. */
    public List<Unit> units() {
      return structure().units();
    }

    /** Return list of modular realms. */
    public List<Realm> realms() {
      return structure().realms();
    }

    /** Return the project main module. */
    public Optional<String> mainModule() {
      return descriptor.mainClass();
    }

    @Override
    public String toString() {
      return new StringJoiner(", ", "Project [", "]")
          .add("paths=" + paths())
          .add("descriptor=" + descriptor())
          .add("mainModule=" + mainModule())
          .add("loader=" + library())
          .toString();
    }

    /** Source directory tree layout. */
    public enum Layout {
      /**
       * Unnamed or flat realm source directory tree layout.
       *
       * <ul>
       *   <li>{@code ${SRC}/${MODULE}/module-info.java}
       * </ul>
       *
       * Module source path examples:
       *
       * <ul>
       *   <li>{@code --module-source-path .}
       *   <li>{@code --module-source-path src}
       *   <li>{@code --module-source-path src/modules}
       *   <li>{@code --module-source-path src/group-1:src/group-2}
       * </ul>
       *
       * @see <a href="https://openjdk.java.net/projects/jigsaw/quick-start">Project Jigsaw: Module
       *     System Quick-Start Guide</a>
       */
      FLAT,

      /**
       * Source tree layout with main and test realm and nested categories.
       *
       * <ul>
       *   <li>{@code ${SRC}/${MODULE}/${OFFSET}/main/java/module-info.java}
       *   <li>{@code ${SRC}/${MODULE}/${OFFSET}/test/java/module-info.java}
       *   <li>{@code ${SRC}/${MODULE}/${OFFSET}/test/module/module-info.java}
       * </ul>
       *
       * Module source path examples:
       *
       * <ul>
       *   <li>{@code --module-source-path src/ * /main/java}
       *   <li>{@code --module-source-path src/ * /test/java:src/ * /test/module}
       * </ul>
       */
      MAIN_TEST {
        @Override
        public Optional<String> realmOf(Unit unit) {
          var deque = deque(unit);
          var category = deque.pop();
          if (!(category.equals("java")
              || category.matches("java-\\d+")
              || category.equals("module"))) return Optional.empty();
          var realm = deque.pop();
          if (!(realm.equals("main") || realm.equals("test"))) return Optional.empty();
          if (Collections.frequency(deque(unit), unit.name()) != 1) return Optional.empty();
          return Optional.of(realm);
        }

        @Override
        public List<Realm> realmsOf(List<Unit> units) {
          var map = new HashMap<String, List<Unit>>();
          for (var unit : units) {
            var realm = realmOf(unit).orElse(unit.path().toString());
            map.computeIfAbsent(realm, key -> new ArrayList<>()).add(unit);
          }
          if (!map.keySet().equals(Set.of("main", "test")))
            throw new IllegalStateException("Only main and test realm expected: " + map);
          var main =
              new Realm(
                  "main",
                  EnumSet.of(Realm.Modifier.CREATE_JAVADOC),
                  0,
                  moduleSourcePath(map.get("main")),
                  Unit.toMap(units.stream().filter(u -> realmOf(u).orElseThrow().equals("main"))),
                  List.of());
          var test =
              new Realm(
                  "test",
                  // EnumSet.of(Realm.Modifier.LAUNCH_TESTS, Realm.Modifier.ENABLE_PREVIEW),
                  EnumSet.of(Realm.Modifier.LAUNCH_TESTS),
                  0,
                  moduleSourcePath(map.get("test")),
                  Unit.toMap(units.stream().filter(u -> realmOf(u).orElseThrow().equals("test"))),
                  List.of(main));
          return List.of(main, test);
        }
      };

      /** Extract the name of the realm from the given modular unit. */
      public Optional<String> realmOf(Unit unit) {
        if (Collections.frequency(deque(unit), unit.name()) != 1) return Optional.empty();
        return Optional.of("");
      }

      /** Create realms based on the given units. */
      public List<Realm> realmsOf(List<Unit> units) {
        if (units.isEmpty()) return List.of();
        var name = "";
        var modifiers = EnumSet.of(Realm.Modifier.CREATE_JAVADOC);
        var moduleSourcePath = moduleSourcePath(units);
        var modules = Unit.toMap(units.stream());
        return List.of(new Realm(name, modifiers, 0, moduleSourcePath, modules, List.of()));
      }

      /** Convert path element names of the given unit into a reversed deque. */
      public static Deque<String> deque(Unit unit) {
        var path = unit.path();
        var deque = new ArrayDeque<String>();
        path.forEach(name -> deque.addFirst(name.toString()));
        var info = deque.pop();
        if (!info.equals("module-info.java"))
          throw new IllegalArgumentException("No module-info.java?! " + path);
        return deque;
      }

      /** Compute distinct module source path from the given list of units. */
      public static String moduleSourcePath(List<Unit> units) {
        return units.stream()
            .map(Unit::moduleSourcePath)
            .distinct()
            .collect(Collectors.joining(File.pathSeparator));
      }

      /** Scan the given units for a well-known modular source layout. */
      public static Optional<Layout> find(List<Unit> units) {
        for (var layout : List.of(Layout.MAIN_TEST, Layout.FLAT))
          if (units.stream().allMatch(unit -> layout.realmOf(unit).isPresent()))
            return Optional.of(layout);
        return Optional.empty();
      }
    }

    /** Common project-related paths. */
    public static final class Paths implements Util.Printable {

      public static final Path CLASSES = Path.of("classes");
      public static final Path MODULES = Path.of("modules");
      public static final Path SOURCES = Path.of("sources");
      public static final Path DOCUMENTATION = Path.of("documentation");
      public static final Path JAVADOC = DOCUMENTATION.resolve("javadoc");

      public static Paths of(Path base) {
        return new Paths(base, base.resolve(".bach"), base.resolve("lib"));
      }

      private final Path base;
      private final Path out;
      private final Path lib;

      public Paths(Path base, Path out, Path lib) {
        this.base = base;
        this.out = out;
        this.lib = lib;
      }

      public Path base() {
        return base;
      }

      public Path out() {
        return out;
      }

      public Path classes(Realm realm) {
        return out.resolve(CLASSES).resolve(realm.name());
      }

      public Path javadoc() {
        return out.resolve(JAVADOC);
      }

      public Path modules(Realm realm) {
        return out.resolve(MODULES).resolve(realm.name());
      }

      public Path sources(Realm realm) {
        return out.resolve(SOURCES).resolve(realm.name());
      }

      public Path lib() {
        return lib;
      }

      @Override
      public String toString() {
        return new StringJoiner(", ", Paths.class.getSimpleName() + "[", "]")
            .add("base='" + base() + "'")
            .add("out='" + out() + "'")
            .add("lib='" + lib() + "'")
            .toString();
      }
    }

    /** Single source path with optional release directive. */
    public static final class Source {

      /** Source-specific modifier enumeration. */
      public enum Modifier {
        /** Store binary assets in {@code META-INF/versions/${release}/} directory of the jar. */
        VERSIONED
      }

      /** Create default non-targeted source for the specified path and optional modifiers. */
      public static Source of(Path path, Modifier... modifiers) {
        return new Source(path, 0, Set.of(modifiers));
      }

      private final Path path;
      private final int release;
      private final Set<Modifier> modifiers;

      public Source(Path path, int release, Set<Modifier> modifiers) {
        this.path = path;
        this.release = release;
        this.modifiers = modifiers.isEmpty() ? Set.of() : EnumSet.copyOf(modifiers);
      }

      /** Source path. */
      public Path path() {
        return path;
      }

      /** Java feature release target number, with zero indicating the current runtime release. */
      public int release() {
        return release;
      }

      /** This source modifiers. */
      public Set<Modifier> modifiers() {
        return modifiers;
      }

      public boolean isTargeted() {
        return release != 0;
      }

      /** Optional Java feature release target number. */
      public OptionalInt target() {
        return isTargeted() ? OptionalInt.of(release) : OptionalInt.empty();
      }

      public boolean isVersioned() {
        return modifiers.contains(Modifier.VERSIONED);
      }

      @Override
      public String toString() {
        return new StringJoiner(", ", "Source { ", " }")
            .add("path=" + path())
            .add("release=" + release())
            .add("target=" + target())
            .add("modifiers=" + modifiers())
            .toString();
      }
    }

    /** A modular project structure. */
    public static final class Structure implements Util.Printable {

      /** List of all declared modular units. */
      private final List<Unit> units;

      /** List of all realms. */
      private final List<Realm> realms;

      public Structure(List<Unit> units, List<Realm> realms) {
        this.units = List.copyOf(units);
        this.realms = List.copyOf(realms);
      }

      public List<Unit> units() {
        return units;
      }

      public List<Realm> realms() {
        return realms;
      }

      @Override
      public String toString() {
        return new StringJoiner(", ", "Structure { ", " }")
            .add("units=" + units())
            .add("realms=" + realms())
            .toString();
      }
    }

    /** A module source description unit. */
    public static final class Unit implements Util.Printable {

      public static Map<String, Unit> toMap(Stream<Unit> units) {
        return units.collect(Collectors.toMap(Unit::name, Function.identity()));
      }

      private final Path path;
      private final ModuleDescriptor descriptor;
      private final String moduleSourcePath;
      private final List<Source> sources;
      private final List<Path> resources;

      private Unit(Path info) {
        this(info, Util.Modules.describe(info));
      }

      private Unit(Path info, ModuleDescriptor descriptor) {
        this(
            info,
            descriptor,
            Util.Modules.moduleSourcePath(info, descriptor.name()),
            List.of(Source.of(info.getParent())),
            List.of());
      }

      public Unit(
          Path path,
          ModuleDescriptor descriptor,
          String moduleSourcePath,
          List<Source> sources,
          List<Path> resources) {
        this.path = path;
        this.descriptor = descriptor;
        this.moduleSourcePath = moduleSourcePath;
        this.sources = sources;
        this.resources = resources;
      }

      public Path path() {
        return path;
      }

      public ModuleDescriptor descriptor() {
        return descriptor;
      }

      public String moduleSourcePath() {
        return moduleSourcePath;
      }

      public List<Source> sources() {
        return sources;
      }

      public List<Path> resources() {
        return resources;
      }

      @Override
      public String toString() {
        @SuppressWarnings("StringBufferReplaceableByString")
        var sb = new StringBuilder("Unit{");
        sb.append("path=").append(path());
        sb.append(", descriptor=").append(descriptor());
        sb.append(", moduleSourcePath=").append(moduleSourcePath());
        sb.append(", sources=").append(sources());
        sb.append(", resources=").append(resources());
        sb.append(", isMultiRelease=").append(isMultiRelease());
        sb.append(", isMainClassPresent=").append(isMainClassPresent());
        sb.append('}');
        return sb.toString();
      }

      public String name() {
        return descriptor.name();
      }

      @Override
      public String printCaption() {
        return "Unit '" + name() + "'";
      }

      public <T> List<T> sources(Function<Source, T> mapper) {
        return sources.stream().map(mapper).collect(Collectors.toList());
      }

      public boolean isMultiRelease() {
        return !sources.isEmpty() && sources.stream().allMatch(Source::isTargeted);
      }

      public boolean isMainClassPresent() {
        return descriptor.mainClass().isPresent();
      }
    }

    /** A realm of modular sources. */
    public static final class Realm implements Util.Printable {

      /** Realm-related flags controlling the build process. */
      public enum Modifier {
        ENABLE_PREVIEW,
        CREATE_JAVADOC,
        LAUNCH_TESTS
      }

      private final String name;
      private final Set<Modifier> modifiers;
      private final int release;
      private final String moduleSourcePath;
      private final Map<String, Unit> units;
      private final List<Realm> dependencies;

      public Realm(
          String name,
          Set<Modifier> modifiers,
          int release,
          String moduleSourcePath,
          Map<String, Unit> units,
          List<Realm> dependencies) {
        this.name = name;
        this.modifiers = modifiers.isEmpty() ? Set.of() : EnumSet.copyOf(modifiers);
        this.release = release;
        this.moduleSourcePath = moduleSourcePath;
        this.units = Map.copyOf(units);
        this.dependencies = List.copyOf(dependencies);
      }

      public String name() {
        return name;
      }

      public Set<Modifier> modifiers() {
        return modifiers;
      }

      public OptionalInt release() {
        return release == 0 ? OptionalInt.empty() : OptionalInt.of(release);
      }

      public String moduleSourcePath() {
        return moduleSourcePath;
      }

      public Map<String, Unit> units() {
        return units;
      }

      public List<Realm> dependencies() {
        return dependencies;
      }

      @Override
      public String toString() {
        return new StringJoiner(", ", "Realm { ", " }")
            .add("name='" + name + "'")
            .add("modifiers=" + modifiers)
            .add("release=" + release)
            .add("units=" + units)
            .add("dependencies=" + dependencies)
            .toString();
      }

      public boolean lacks(Modifier modifier) {
        return !modifiers.contains(modifier);
      }

      public boolean test(Modifier modifier) {
        return modifiers.contains(modifier);
      }

      /** Generate {@code --patch-module} strings for this realm. */
      public List<String> patches(BiFunction<Realm, Unit, List<Path>> patcher) {
        if (dependencies().isEmpty()) return List.of();
        var patches = new ArrayList<String>();
        for (var unit : units().values()) {
          var module = unit.name();
          for (var dependency : dependencies()) {
            var other = dependency.units().get(module);
            if (other == null) continue;
            var paths = Util.Strings.join(patcher.apply(dependency, other));
            patches.add(module + '=' + paths);
          }
        }
        return patches.isEmpty() ? List.of() : List.copyOf(patches);
      }

      @Override
      public String printCaption() {
        return "Realm '" + name() + "'";
      }

      @Override
      public boolean printTest(String name, Object value) {
        return !name.equals("units");
      }
    }

    /** Library of assets. */
    public static final class Library implements Util.Printable {
      private final ModuleMapper mapper;

      public Library(ModuleMapper mapper) {
        this.mapper = mapper;
      }

      public ModuleMapper mapper() {
        return mapper;
      }

      @Override
      public String toString() {
        return new StringJoiner(", ", Library.class.getSimpleName() + "[", "]")
            .add("mapper=" + mapper())
            .toString();
      }

      /** Look up the mapped URI for the given pair of a module name and its version. */
      public URI uri(String module, Version version) {
        var mapping = mapper().apply(module, version);
        if (mapping == null) throw new Util.Modules.UnmappedModuleException(module);
        return mapping.uri();
      }
    }

    /** Mapping module names to their related coordinates. */
    public interface ModuleMapper extends BiFunction<String, Version, ModuleMapper.Mapping> {

      /** Return mapping instance for the pair of module name and its version - {@code null}able. */
      @Override
      Mapping apply(String module, Version version);

      /** Module and version to URI and more other-system-property mapping. */
      final class Mapping implements Util.Printable {

        public static Mapping ofMavenCentral(
            String module, Version version, String group, String artifact, String classifier) {
          var repository = "https://repo.apache.maven.org/maven2";
          var uri = uri(repository, group, artifact, version.toString(), classifier, "jar");
          return new Mapping(module, version, uri);
        }

        public static URI uri(
            String repository,
{'fraction_non_alphanumeric': 0.1264667808076168, 'fraction_numerical': 0.0011274898734705808, 'mean_word_length': 2.342288904396371, 'pattern_counts': {'":': 0, '<': 161, '<?xml version=': 0, '>': 173, 'https://': 6, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1742303', 'n_tokens_mistral': 13146, 'n_tokens_neox': 12842, 'n_words': 3524}