content: /*
   * Licensed to the Apache Software Foundation (ASF) under one
   * or more contributor license agreements.  See the NOTICE file
   * distributed with this work for additional information
   * regarding copyright ownership.  The ASF licenses this file
   * to you 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
   *
   *     http://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.
   */
  
  package org.apache.bookkeeper.tools.framework;
  
  import com.beust.jcommander.JCommander;
  import com.google.common.base.Strings;
  import java.io.PrintStream;
  import java.util.Arrays;
  import java.util.Map;
  import java.util.TreeMap;
  import java.util.function.Function;
  import java.util.stream.IntStream;
  import lombok.AccessLevel;
  import lombok.Getter;
  import lombok.extern.slf4j.Slf4j;
  import org.apache.bookkeeper.tools.framework.CliSpec.Builder;
  
  /**
   * Cli to execute {@link CliSpec}.
   */
  @Slf4j
  public class Cli<CliFlagsT extends CliFlags> {
  
      private final CliSpec<CliFlagsT> spec;
      private final JCommander commander;
      @Getter(AccessLevel.PACKAGE)
      private final Map<String, Command> commandMap;
      private final Function<CliFlagsT, Boolean> runFunc;
      private final String cmdPath;
      private final PrintStream console;
  
      public Cli(CliSpec<CliFlagsT> spec) {
          this.spec = updateSpecWithDefaultValues(spec);
          this.cmdPath = getCmdPath(spec);
          this.commandMap = new TreeMap<>();
          this.console = setupConsole(spec);
          this.commander = setupCli(cmdPath, spec, commandMap);
          if (null == spec.runFunc()) {
              this.runFunc = (args) -> {
                  usage();
                  return false;
              };
          } else {
              this.runFunc = spec.runFunc();
          }
  
          // inject default commands if needed
          if (!spec.commands().isEmpty() && !hasCommand("help")) {
              commandMap.put("help", new HelpCommand<>(this));
          }
      }
  
      static <CliFlagsT extends CliFlags> String getCmdPath(CliSpec<CliFlagsT> spec) {
          if (Strings.isNullOrEmpty(spec.parent())) {
              return spec.name();
          } else {
              return spec.parent() + " " + spec.name();
          }
      }
  
      static <CliFlagsT extends CliFlags> PrintStream setupConsole(CliSpec<CliFlagsT> spec) {
          if (null == spec.console()) {
              return System.out;
          } else {
              return spec.console();
          }
      }
  
      CliSpec<CliFlagsT> updateSpecWithDefaultValues(CliSpec<CliFlagsT> spec) {
          Builder<CliFlagsT> builder = CliSpec.newBuilder(spec);
          if (Strings.isNullOrEmpty(spec.usage())) {
              // simple command
              if (spec.commands().isEmpty()) {
                  builder.withUsage(
                      String.format(
                          "%s [flags] " + spec.argumentsUsage(),
                          getCmdPath(spec)));
              // command group
              } else {
                  builder.withUsage(
                      String.format(
                          "%s [flags] [command] [command options]",
                          getCmdPath(spec)));
              }
          }
  
          if (!spec.commands().isEmpty() && Strings.isNullOrEmpty(spec.tailer())) {
              builder.withTailer(
                  String.format(
                        "Use \"%s [command] --help\" or \"%s help [command]\" for "
                      + "more information about a command",
                      getCmdPath(spec), getCmdPath(spec)));
          }
          return builder.build();
      }
  
      String name() {
          return spec.name();
      }
  
      String cmdPath() {
          return cmdPath;
      }
  
      boolean hasCommand(String command) {
          return commandMap.containsKey(command);
      }
  
      Command getCommand(String command) {
          return commandMap.get(command);
      }
  
      static <CliFlagsT extends CliFlags> JCommander setupCli(String cmdPath,
                                                              CliSpec<CliFlagsT> spec,
                                                              Map<String, Command> commandMap) {
          JCommander commander = new JCommander();
          commander.setProgramName(cmdPath);
          if (null != spec.flags()) {
              commander.addObject(spec.flags());
          }
          for (Command cmd : spec.commands()) {
              commandMap.put(cmd.name(), cmd);
          }
          // trigger command to generate help information
          StringBuilder usageBuilder = new StringBuilder();
          commander.getUsageFormatter().usage(usageBuilder);
          return commander;
      }
  
      protected void console(String msg) {
          console().println(msg);
      }
  
      protected PrintStream console() {
          return console;
      }
  
      void usage() {
          usage("");
      }
  
      void usage(String errorMsg) {
          if (Strings.isNullOrEmpty(errorMsg)) {
              CommandUtils.printDescription(console(), 0, 0, spec.description());
              console("");
          } else {
              CommandUtils.printDescription(console(), 0, 0, "Error : " + errorMsg);
              console("");
          }
  
          // usage section
          CommandUtils.printUsage(console(), spec.usage());
  
          // command section
          CommandUtils.printAvailableCommands(commandMap, console());
  
          // flags section
          if (!spec.isCommandGroup()) {
              CommandUtils.printAvailableFlags(commander, console());
          }
  
          if (!spec.commands().isEmpty()) {
              // tailer section
              CommandUtils.printDescription(console(), 0, 0, spec.tailer());
          }
      }
  
      @SuppressWarnings("unchecked")
      boolean run(String[] args) {
          // the cli has commands and args is empty
          if (!spec.commands().isEmpty() && args.length == 0) {
              usage();
              return false;
          }
  
          int cmdPos = IntStream.range(0, args.length)
              .filter(pos -> commandMap.containsKey(args[pos]))
              .findFirst()
              .orElse(args.length);
  
          String[] flagArgs = Arrays.copyOfRange(args, 0, cmdPos);
  
          // if this cli is a sub-command group, the flag args should be empty
          if (spec.isCommandGroup() && flagArgs.length > 0) {
              usage();
              return false;
          }
  
          // parse flags
          if (flagArgs.length != 0) {
              // skip parsing flags
              try {
                  commander.parse(flagArgs);
              } catch (Exception e) {
                  usage(e.getMessage());
                  return false;
              }
          }
  
          // print help messages
          if (spec.flags().help) {
              usage();
              return false;
          }
  
          // found a sub command
          if (cmdPos == args.length) {
              return runFunc.apply(spec.flags());
          } else {
              String cmd = args[cmdPos];
              Command command = commandMap.get(cmd);
              String[] subCmdArgs = Arrays.copyOfRange(
                  args, cmdPos + 1, args.length);
  
              try {
                  return command.apply(spec.flags(), subCmdArgs);
              } catch (Exception e) {
                  e.printStackTrace(spec.console());
                  usage(e.getMessage());
                  return false;
              }
          }
      }
  
      public static <CliOptsT extends CliFlags> int runCli(
              CliSpec<CliOptsT> spec, String[] args) {
          Cli<CliOptsT> cli = new Cli<>(spec);
          return cli.run(args) ? 0 : -1;
      }
  
      public static <CliOptsT extends CliFlags> void printUsage(CliSpec<CliOptsT> spec) {
          Cli<CliOptsT> cli = new Cli<>(spec);
          cli.usage();
      }
  }
  