001// Copyright (c) FIRST and other WPILib contributors.
002// Open Source Software; you can modify and/or share it under the terms of
003// the WPILib BSD license file in the root directory of this project.
004
005package org.wpilib.command3;
006
007import static org.wpilib.util.ErrorMessages.requireNonNullParam;
008
009import java.util.Arrays;
010import java.util.LinkedHashSet;
011import java.util.Set;
012import java.util.function.BooleanSupplier;
013import java.util.stream.Collectors;
014import org.wpilib.annotation.NoDiscard;
015
016/**
017 * A builder class to configure and then create a {@link ParallelGroup}. Like {@link
018 * StagedCommandBuilder}, the final command is created by calling the terminal {@link
019 * #named(String)} method, or with an automatically generated name using {@link
020 * #withAutomaticName()}.
021 */
022@NoDiscard
023public class ParallelGroupBuilder {
024  private final Set<Command> m_optionalCommands = new LinkedHashSet<>();
025  private final Set<Command> m_requiredCommands = new LinkedHashSet<>();
026  private BooleanSupplier m_endCondition;
027
028  /**
029   * Creates a new parallel group builder. The builder will have no commands and have no preapplied
030   * configuration options.
031   */
032  public ParallelGroupBuilder() {}
033
034  /**
035   * Adds one or more optional commands to the group. They will not be required to complete for the
036   * parallel group to exit, and will be canceled once all required commands have finished.
037   *
038   * @param commands The optional commands to add to the group
039   * @return The builder object, for chaining
040   */
041  public ParallelGroupBuilder optional(Command... commands) {
042    requireNonNullParam(commands, "commands", "ParallelGroupBuilder.optional");
043    for (int i = 0; i < commands.length; i++) {
044      requireNonNullParam(commands[i], "commands[" + i + "]", "ParallelGroupBuilder.optional");
045    }
046
047    m_optionalCommands.addAll(Arrays.asList(commands));
048    return this;
049  }
050
051  /**
052   * Adds one or more required commands to the group. All required commands will need to complete
053   * for the group to exit.
054   *
055   * @param commands The required commands to add to the group
056   * @return The builder object, for chaining
057   */
058  public ParallelGroupBuilder requiring(Command... commands) {
059    requireNonNullParam(commands, "commands", "ParallelGroupBuilder.requiring");
060    for (int i = 0; i < commands.length; i++) {
061      requireNonNullParam(commands[i], "commands[" + i + "]", "ParallelGroupBuilder.requiring");
062    }
063
064    m_requiredCommands.addAll(Arrays.asList(commands));
065    return this;
066  }
067
068  /**
069   * Adds a command to the group. The command must complete for the group to exit.
070   *
071   * @param command The command to add to the group
072   * @return The builder object, for chaining
073   */
074  // Note: this primarily exists so users can cleanly chain .alongWith calls to build a
075  //       parallel group, eg `fooCommand().alongWith(barCommand()).alongWith(bazCommand())`
076  public ParallelGroupBuilder alongWith(Command command) {
077    return requiring(command);
078  }
079
080  /**
081   * Adds an end condition to the command group. If this condition is met before all required
082   * commands have completed, the group will exit early. If multiple end conditions are added (e.g.
083   * {@code .until(() -> conditionA()).until(() -> conditionB())}), then the last end condition
084   * added will be used and any previously configured condition will be overridden.
085   *
086   * @param condition The end condition for the group. May be null.
087   * @return The builder object, for chaining
088   */
089  public ParallelGroupBuilder until(BooleanSupplier condition) {
090    m_endCondition = condition;
091    return this;
092  }
093
094  /**
095   * Creates the group, using the provided named. The group will require everything that the
096   * commands in the group require, and will have a priority level equal to the highest priority
097   * among those commands.
098   *
099   * @param name The name of the parallel group
100   * @return The built group
101   */
102  public ParallelGroup named(String name) {
103    requireNonNullParam(name, "name", "ParallelGroupBuilder.named");
104
105    var group = new ParallelGroup(name, m_requiredCommands, m_optionalCommands);
106    if (m_endCondition == null) {
107      // No custom end condition, return the group as is
108      return group;
109    }
110
111    // We have a custom end condition, so we need to wrap the group in a race
112    return new ParallelGroupBuilder()
113        .optional(group, Command.waitUntil(m_endCondition).named("Until Condition"))
114        .named(name);
115  }
116
117  /**
118   * Creates the group, giving it a name based on the commands within the group.
119   *
120   * @return The built group
121   */
122  public ParallelGroup withAutomaticName() {
123    // eg "(CommandA & CommandB & CommandC)"
124    String required =
125        m_requiredCommands.stream().map(Command::name).collect(Collectors.joining(" & ", "(", ")"));
126
127    // eg "(CommandA | CommandB | CommandC)"
128    String optional =
129        m_optionalCommands.stream().map(Command::name).collect(Collectors.joining(" | ", "(", ")"));
130
131    if (m_requiredCommands.isEmpty()) {
132      // No required commands, pure race
133      return named(optional);
134    } else if (m_optionalCommands.isEmpty()) {
135      // Everything required
136      return named(required);
137    } else {
138      // Some form of deadline
139      // eg "[(CommandA & CommandB) * (CommandX | CommandY | ...)]"
140      String name = "[%s * %s]".formatted(required, optional);
141      return named(name);
142    }
143  }
144}