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.commands3;
006
007import static edu.wpi.first.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_commands = 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_commands.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    m_commands.addAll(m_requiredCommands);
066    return this;
067  }
068
069  /**
070   * Adds a command to the group. The command must complete for the group to exit.
071   *
072   * @param command The command to add to the group
073   * @return The builder object, for chaining
074   */
075  // Note: this primarily exists so users can cleanly chain .alongWith calls to build a
076  //       parallel group, eg `fooCommand().alongWith(barCommand()).alongWith(bazCommand())`
077  public ParallelGroupBuilder alongWith(Command command) {
078    return requiring(command);
079  }
080
081  /**
082   * Adds an end condition to the command group. If this condition is met before all required
083   * commands have completed, the group will exit early. If multiple end conditions are added (e.g.
084   * {@code .until(() -> conditionA()).until(() -> conditionB())}), then the last end condition
085   * added will be used and any previously configured condition will be overridden.
086   *
087   * @param condition The end condition for the group. May be null.
088   * @return The builder object, for chaining
089   */
090  public ParallelGroupBuilder until(BooleanSupplier condition) {
091    m_endCondition = condition;
092    return this;
093  }
094
095  /**
096   * Creates the group, using the provided named. The group will require everything that the
097   * commands in the group require, and will have a priority level equal to the highest priority
098   * among those commands.
099   *
100   * @param name The name of the parallel group
101   * @return The built group
102   */
103  public ParallelGroup named(String name) {
104    requireNonNullParam(name, "name", "ParallelGroupBuilder.named");
105
106    var group = new ParallelGroup(name, m_commands, m_requiredCommands);
107    if (m_endCondition == null) {
108      // No custom end condition, return the group as is
109      return group;
110    }
111
112    // We have a custom end condition, so we need to wrap the group in a race
113    return new ParallelGroupBuilder()
114        .optional(group, Command.waitUntil(m_endCondition).named("Until Condition"))
115        .named(name);
116  }
117
118  /**
119   * Creates the group, giving it a name based on the commands within the group.
120   *
121   * @return The built group
122   */
123  public ParallelGroup withAutomaticName() {
124    // eg "(CommandA & CommandB & CommandC)"
125    String required =
126        m_requiredCommands.stream().map(Command::name).collect(Collectors.joining(" & ", "(", ")"));
127
128    var optionalCommands = new LinkedHashSet<>(m_commands);
129    optionalCommands.removeAll(m_requiredCommands);
130    // eg "(CommandA | CommandB | CommandC)"
131    String optional =
132        optionalCommands.stream().map(Command::name).collect(Collectors.joining(" | ", "(", ")"));
133
134    if (m_requiredCommands.isEmpty()) {
135      // No required commands, pure race
136      return named(optional);
137    } else if (optionalCommands.isEmpty()) {
138      // Everything required
139      return named(required);
140    } else {
141      // Some form of deadline
142      // eg "[(CommandA & CommandB) * (CommandX | CommandY | ...)]"
143      String name = "[%s * %s]".formatted(required, optional);
144      return named(name);
145    }
146  }
147}