// Copyright 2014 The Bazel Authors. All rights reserved.
//
// 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
//
//    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 com.google.devtools.build.lib.runtime.commands;

import com.google.common.base.CaseFormat;
import com.google.common.base.Joiner;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.Iterables;
import com.google.common.escape.Escaper;
import com.google.common.html.HtmlEscapers;
import com.google.devtools.build.lib.analysis.BlazeVersionInfo;
import com.google.devtools.build.lib.analysis.ConfiguredRuleClassProvider;
import com.google.devtools.build.lib.analysis.NoBuildEvent;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.runtime.BlazeCommand;
import com.google.devtools.build.lib.runtime.BlazeCommandResult;
import com.google.devtools.build.lib.runtime.BlazeCommandUtils;
import com.google.devtools.build.lib.runtime.BlazeModule;
import com.google.devtools.build.lib.runtime.BlazeRuntime;
import com.google.devtools.build.lib.runtime.Command;
import com.google.devtools.build.lib.runtime.CommandEnvironment;
import com.google.devtools.build.lib.runtime.InfoItem;
import com.google.devtools.build.lib.runtime.commands.proto.BazelFlagsProto;
import com.google.devtools.build.lib.server.FailureDetails;
import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
import com.google.devtools.build.lib.server.FailureDetails.HelpCommand.Code;
import com.google.devtools.build.lib.util.StringUtil;
import com.google.devtools.build.lib.util.io.OutErr;
import com.google.devtools.common.options.Converters;
import com.google.devtools.common.options.Option;
import com.google.devtools.common.options.OptionDefinition;
import com.google.devtools.common.options.OptionDocumentationCategory;
import com.google.devtools.common.options.OptionEffectTag;
import com.google.devtools.common.options.OptionFilterDescriptions;
import com.google.devtools.common.options.OptionMetadataTag;
import com.google.devtools.common.options.OptionsBase;
import com.google.devtools.common.options.OptionsParser;
import com.google.devtools.common.options.OptionsParser.HelpVerbosity;
import com.google.devtools.common.options.OptionsParsingResult;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Predicate;

/** The 'blaze help' command, which prints all available commands as well as specific help pages. */
@Command(
  name = "help",
  options = {HelpCommand.Options.class},
  allowResidue = true,
  mustRunInWorkspace = false,
  shortDescription = "Prints help for commands, or the index.",
  completion = "command|{startup_options,target-syntax,info-keys}",
  help = "resource:help.txt"
)
public final class HelpCommand implements BlazeCommand {
  private static final Joiner SPACE_JOINER = Joiner.on(" ");

  /**
   * Only to be used to escape the internal hard-coded help texts when outputting HTML from help,
   * which don't pose a security risk.
   */
  private static final Escaper HTML_ESCAPER = HtmlEscapers.htmlEscaper();

  public static class Options extends OptionsBase {

    @Option(
      name = "help_verbosity",
      defaultValue = "medium",
      converter = Converters.HelpVerbosityConverter.class,
      documentationCategory = OptionDocumentationCategory.LOGGING,
      effectTags = {OptionEffectTag.AFFECTS_OUTPUTS, OptionEffectTag.TERMINAL_OUTPUT},
      help = "Select the verbosity of the help command."
    )
    public OptionsParser.HelpVerbosity helpVerbosity;

    @Option(
      name = "long",
      abbrev = 'l',
      defaultValue = "null",
      expansion = {"--help_verbosity=long"},
      documentationCategory = OptionDocumentationCategory.LOGGING,
      effectTags = {OptionEffectTag.AFFECTS_OUTPUTS, OptionEffectTag.TERMINAL_OUTPUT},
      help = "Show full description of each option, instead of just its name."
    )
    public Void showLongFormOptions;

    @Option(
      name = "short",
      defaultValue = "null",
      expansion = {"--help_verbosity=short"},
      documentationCategory = OptionDocumentationCategory.LOGGING,
      effectTags = {OptionEffectTag.AFFECTS_OUTPUTS, OptionEffectTag.TERMINAL_OUTPUT},
      help = "Show only the names of the options, not their types or meanings."
    )
    public Void showShortFormOptions;
  }

  @Override
  public BlazeCommandResult exec(CommandEnvironment env, OptionsParsingResult options) {
    env.getEventBus().post(new NoBuildEvent());

    BlazeRuntime runtime = env.getRuntime();
    OutErr outErr = env.getReporter().getOutErr();
    Options helpOptions = options.getOptions(Options.class);
    if (options.getResidue().isEmpty()) {
      emitBlazeVersionInfo(outErr, runtime.getProductName());
      emitGenericHelp(outErr, runtime);
      return BlazeCommandResult.success();
    }
    if (options.getResidue().size() != 1) {
      String message = "You must specify exactly one command";
      env.getReporter().handle(Event.error(message));
      return createFailureResult(message, Code.MISSING_ARGUMENT);
    }
    String helpSubject = options.getResidue().get(0);
    String productName = runtime.getProductName();
    // Go through the custom subjects before going through Bazel commands.
    switch (helpSubject) {
      case "startup_options":
        emitBlazeVersionInfo(outErr, runtime.getProductName());
        emitStartupOptions(outErr, helpOptions.helpVerbosity, runtime);
        return BlazeCommandResult.success();
      case "target-syntax":
        emitBlazeVersionInfo(outErr, runtime.getProductName());
        emitTargetSyntaxHelp(outErr, productName);

        return BlazeCommandResult.success();
      case "info-keys":
        emitInfoKeysHelp(env, outErr);
        return BlazeCommandResult.success();
      case "completion":
        emitCompletionHelp(runtime, outErr);
        return BlazeCommandResult.success();
      case "flags-as-proto":
        emitFlagsAsProtoHelp(runtime, outErr);
        return BlazeCommandResult.success();
      case "everything-as-html":
        new HtmlEmitter(runtime).emit(outErr);
        return BlazeCommandResult.success();
      default: // fall out
    }

    BlazeCommand command = runtime.getCommandMap().get(helpSubject);
    if (command == null) {
      String message = "'" + helpSubject + "' is not a known command";
      env.getReporter().handle(Event.error(null, message));
      return createFailureResult(message, Code.COMMAND_NOT_FOUND);
    }
    emitBlazeVersionInfo(outErr, productName);
    outErr.printOut(
        BlazeCommandUtils.getUsage(
            command.getClass(),
            helpOptions.helpVerbosity,
            runtime.getBlazeModules(),
            runtime.getRuleClassProvider(),
            productName));

    return BlazeCommandResult.success();
  }

  private void emitBlazeVersionInfo(OutErr outErr, String productName) {
    String releaseInfo = BlazeVersionInfo.instance().getReleaseName();
    String line = String.format("[%s %s]", productName, releaseInfo);
    outErr.printOut(String.format("%80s\n", line));
  }

  private void emitStartupOptions(
      OutErr outErr, HelpVerbosity helpVerbosity, BlazeRuntime runtime) {
    outErr.printOut(
        BlazeCommandUtils.expandHelpTopic(
            "startup_options",
            "resource:startup_options.txt",
            getClass(),
            BlazeCommandUtils.getStartupOptions(runtime.getBlazeModules()),
            helpVerbosity,
            runtime.getProductName()));
  }

  private void emitCompletionHelp(BlazeRuntime runtime, OutErr outErr) {
    Map<String, BlazeCommand> commandsByName = getSortedCommands(runtime);

    outErr.printOutLn("BAZEL_COMMAND_LIST=\"" + SPACE_JOINER.join(commandsByName.keySet()) + "\"");

    outErr.printOutLn("BAZEL_INFO_KEYS=\"");
    for (String name : InfoCommand.getHardwiredInfoItemNames(runtime.getProductName())) {
      outErr.printOutLn(name);
    }
    outErr.printOutLn("\"");

    Consumer<OptionsParser> startupOptionVisitor =
        parser -> {
          outErr.printOutLn("BAZEL_STARTUP_OPTIONS=\"");
          outErr.printOut(parser.getOptionsCompletion());
          outErr.printOutLn("\"");
        };
    CommandOptionVisitor commandOptionVisitor =
        (commandName, commandAnnotation, parser) -> {
          String varName = CaseFormat.LOWER_HYPHEN.to(CaseFormat.UPPER_UNDERSCORE, commandName);
          if (!Strings.isNullOrEmpty(commandAnnotation.completion())) {
            outErr.printOutLn(
                "BAZEL_COMMAND_"
                    + varName
                    + "_ARGUMENT=\""
                    + commandAnnotation.completion()
                    + "\"");
          }
          outErr.printOutLn("BAZEL_COMMAND_" + varName + "_FLAGS=\"");
          outErr.printOut(parser.getOptionsCompletion());
          outErr.printOutLn("\"");
        };

    visitAllOptions(runtime, startupOptionVisitor, commandOptionVisitor);
  }

  private void emitFlagsAsProtoHelp(BlazeRuntime runtime, OutErr outErr) {
    Map<String, BazelFlagsProto.FlagInfo.Builder> flags = new HashMap<>();

    Predicate<OptionDefinition> allOptions = option -> true;
    BiConsumer<String, OptionDefinition> visitor =
        (commandName, option) -> {
          if (ImmutableSet.copyOf(option.getOptionMetadataTags())
              .contains(OptionMetadataTag.INTERNAL)) {
            return;
          }
          BazelFlagsProto.FlagInfo.Builder info =
              flags.computeIfAbsent(option.getOptionName(), key -> createFlagInfo(option));
          info.addCommands(commandName);
        };
    Consumer<OptionsParser> startupOptionVisitor =
        parser -> {
          parser.visitOptions(allOptions, option -> visitor.accept("startup", option));
        };
    CommandOptionVisitor commandOptionVisitor =
        (commandName, commandAnnotation, parser) -> {
          parser.visitOptions(allOptions, option -> visitor.accept(commandName, option));
        };

    visitAllOptions(runtime, startupOptionVisitor, commandOptionVisitor);

    BazelFlagsProto.FlagCollection.Builder collectionBuilder =
        BazelFlagsProto.FlagCollection.newBuilder();
    for (BazelFlagsProto.FlagInfo.Builder info : flags.values()) {
      collectionBuilder.addFlagInfos(info);
    }
    outErr.printOut(Base64.getEncoder().encodeToString(collectionBuilder.build().toByteArray()));
  }

  private BazelFlagsProto.FlagInfo.Builder createFlagInfo(OptionDefinition option) {
    BazelFlagsProto.FlagInfo.Builder flagBuilder = BazelFlagsProto.FlagInfo.newBuilder();
    flagBuilder.setName(option.getOptionName());
    flagBuilder.setHasNegativeFlag(option.hasNegativeOption());
    flagBuilder.setDocumentation(option.getHelpText());
    flagBuilder.setAllowsMultiple(option.allowsMultiple());
    if (option.getAbbreviation() != '\0') {
      flagBuilder.setAbbreviation(String.valueOf(option.getAbbreviation()));
    }
    return flagBuilder;
  }

  private void visitAllOptions(
      BlazeRuntime runtime,
      Consumer<OptionsParser> startupOptionVisitor,
      CommandOptionVisitor commandOptionVisitor) {
    // First startup_options
    Iterable<BlazeModule> blazeModules = runtime.getBlazeModules();
    ConfiguredRuleClassProvider ruleClassProvider = runtime.getRuleClassProvider();
    Map<String, BlazeCommand> commandsByName = getSortedCommands(runtime);

    Iterable<Class<? extends OptionsBase>> options =
        BlazeCommandUtils.getStartupOptions(blazeModules);
    startupOptionVisitor.accept(OptionsParser.builder().optionsClasses(options).build());

    for (Map.Entry<String, BlazeCommand> e : commandsByName.entrySet()) {
      BlazeCommand command = e.getValue();
      Command annotation = command.getClass().getAnnotation(Command.class);
      options = BlazeCommandUtils.getOptions(command.getClass(), blazeModules, ruleClassProvider);
      commandOptionVisitor.visit(
          e.getKey(), annotation, OptionsParser.builder().optionsClasses(options).build());
    }
  }

  private static Map<String, BlazeCommand> getSortedCommands(BlazeRuntime runtime) {
    return ImmutableSortedMap.copyOf(runtime.getCommandMap());
  }

  private void emitTargetSyntaxHelp(OutErr outErr, String productName) {
    outErr.printOut(
        BlazeCommandUtils.expandHelpTopic(
            "target-syntax",
            "resource:target-syntax.txt",
            getClass(),
            ImmutableList.<Class<? extends OptionsBase>>of(),
            OptionsParser.HelpVerbosity.MEDIUM,
            productName));
  }

  private void emitInfoKeysHelp(CommandEnvironment env, OutErr outErr) {
    for (InfoItem item :
        InfoCommand.getInfoItemMap(env, OptionsParser.builder().build()).values()) {
      outErr.printOut(String.format("%-23s %s\n", item.getName(), item.getDescription()));
    }
  }

  private void emitGenericHelp(OutErr outErr, BlazeRuntime runtime) {
    outErr.printOut(String.format("Usage: %s <command> <options> ...\n\n",
            runtime.getProductName()));
    outErr.printOut("Available commands:\n");

    Map<String, BlazeCommand> commandsByName = runtime.getCommandMap();
    List<String> namesInOrder = new ArrayList<>(commandsByName.keySet());
    Collections.sort(namesInOrder);

    for (String name : namesInOrder) {
      BlazeCommand command = commandsByName.get(name);
      Command annotation = command.getClass().getAnnotation(Command.class);
      if (annotation.hidden()) {
        continue;
      }

      String shortDescription = annotation.shortDescription().
          replace("%{product}", runtime.getProductName());
      outErr.printOut(String.format("  %-19s %s\n", name, shortDescription));
    }

    outErr.printOut("\n");
    outErr.printOut("Getting more help:\n");
    outErr.printOut(String.format("  %s help <command>\n", runtime.getProductName()));
    outErr.printOut("                   Prints help and options for <command>.\n");
    outErr.printOut(String.format("  %s help startup_options\n", runtime.getProductName()));
    outErr.printOut(String.format("                   Options for the JVM hosting %s.\n",
        runtime.getProductName()));
    outErr.printOut(String.format("  %s help target-syntax\n", runtime.getProductName()));
    outErr.printOut("                   Explains the syntax for specifying targets.\n");
    outErr.printOut(String.format("  %s help info-keys\n", runtime.getProductName()));
    outErr.printOut("                   Displays a list of keys used by the info command.\n");
  }

  private static final class HtmlEmitter {
    private final BlazeRuntime runtime;

    private HtmlEmitter(BlazeRuntime runtime) {
      this.runtime = runtime;
    }

    private void emit(OutErr outErr) {
      Map<String, BlazeCommand> commandsByName = getSortedCommands(runtime);
      StringBuilder result = new StringBuilder();
      result.append("<h2>Commands</h2>\n");
      result.append("<table>\n");
      for (Map.Entry<String, BlazeCommand> e : commandsByName.entrySet()) {
        BlazeCommand command = e.getValue();
        Command annotation = command.getClass().getAnnotation(Command.class);
        if (annotation.hidden()) {
          continue;
        }
        String shortDescription = annotation.shortDescription().
            replace("%{product}", runtime.getProductName());

        result.append("<tr>\n");
        result.append(
            String.format(
                "  <td><a href=\"#%s\"><code>%s</code></a></td>\n", e.getKey(), e.getKey()));
        result.append("  <td>").append(HTML_ESCAPER.escape(shortDescription)).append("</td>\n");
        result.append("</tr>\n");
      }
      result.append("</table>\n");
      result.append("\n");

      result.append("<h2>Startup Options</h2>\n");
      appendOptionsHtml(result, BlazeCommandUtils.getStartupOptions(runtime.getBlazeModules()));
      result.append("\n");

      result.append("<h2><a name=\"common_options\">Options Common to all Commands</a></h2>\n");
      appendOptionsHtml(result, BlazeCommandUtils.getCommonOptions(runtime.getBlazeModules()));
      result.append("\n");

      for (Map.Entry<String, BlazeCommand> e : commandsByName.entrySet()) {
        result.append(
            String.format(
                "<h2><a name=\"%s\">%s Options</a></h2>\n", e.getKey(), capitalize(e.getKey())));
        BlazeCommand command = e.getValue();
        Command annotation = command.getClass().getAnnotation(Command.class);
        if (annotation.hidden()) {
          continue;
        }
        List<String> inheritedCmdNames = new ArrayList<>();
        for (Class<? extends BlazeCommand> base : annotation.inherits()) {
          String name = base.getAnnotation(Command.class).name();
          inheritedCmdNames.add(String.format("<a href=\"#%s\">%s</a>", name, name));
        }
        if (!inheritedCmdNames.isEmpty()) {
          result.append("<p>Inherits all options from ");
          result.append(StringUtil.joinEnglishList(inheritedCmdNames, "and"));
          result.append(".</p>\n\n");
        }
        Set<Class<? extends OptionsBase>> options = new HashSet<>();
        Collections.addAll(options, annotation.options());
        for (BlazeModule blazeModule : runtime.getBlazeModules()) {
          Iterables.addAll(options, blazeModule.getCommandOptions(annotation));
        }
        appendOptionsHtml(result, options);
        result.append("\n");

        // For now, we print all the configuration options in a list after all the non-configuration
        // options. Note that usesConfigurationOptions is only true for the build command right now.
        if (annotation.usesConfigurationOptions()) {
          options.clear();
          Collections.addAll(options, annotation.options());
          if (annotation.usesConfigurationOptions()) {
            options.addAll(runtime.getRuleClassProvider().getConfigurationOptions());
          }
          appendOptionsHtml(result, options);
          result.append("\n");
        }
      }

      // Describe the tags once, any mentions above should link to these descriptions.
        String productName = runtime.getProductName();
        ImmutableMap<OptionEffectTag, String> effectTagDescriptions =
            OptionFilterDescriptions.getOptionEffectTagDescription(productName);
        result.append("<h3>Option Effect Tags</h3>\n");
        result.append("<table>\n");
        for (OptionEffectTag tag : OptionEffectTag.values()) {
          String tagDescription = effectTagDescriptions.get(tag);

          result.append("<tr>\n");
          result.append(
              String.format(
                  "<td id=\"effect_tag_%s\"><code>%s</code></td>\n",
                  tag, tag.name().toLowerCase()));
          result.append(String.format("<td>%s</td>\n", HTML_ESCAPER.escape(tagDescription)));
          result.append("</tr>\n");
        }
        result.append("</table>\n");

        ImmutableMap<OptionMetadataTag, String> metadataTagDescriptions =
            OptionFilterDescriptions.getOptionMetadataTagDescription(productName);
        result.append("<h3>Option Metadata Tags</h3>\n");
        result.append("<table>\n");
        for (OptionMetadataTag tag : OptionMetadataTag.values()) {
          // skip the tags that are reserved for undocumented flags.
          if (!tag.equals(OptionMetadataTag.HIDDEN) && !tag.equals(OptionMetadataTag.INTERNAL)) {
            String tagDescription = metadataTagDescriptions.get(tag);

            result.append("<tr>\n");
            result.append(
                String.format(
                    "<td id=\"metadata_tag_%s\"><code>%s</code></td>\n",
                    tag, tag.name().toLowerCase()));
            result.append(String.format("<td>%s</td>\n", HTML_ESCAPER.escape(tagDescription)));
            result.append("</tr>\n");
          }
        }
        result.append("</table>\n");


      outErr.printOut(result.toString());
    }

    private void appendOptionsHtml(
        StringBuilder result, Iterable<Class<? extends OptionsBase>> optionsClasses) {
      OptionsParser parser = OptionsParser.builder().optionsClasses(optionsClasses).build();
      String productName = runtime.getProductName();
        result.append(
            parser
                .describeOptionsHtml(HTML_ESCAPER, productName)
                .replace("%{product}", productName));
    }

    private static String capitalize(String s) {
      return s.substring(0, 1).toUpperCase(Locale.US) + s.substring(1);
    }
  }

  /** A visitor for Blaze commands and their respective command line options. */
  @FunctionalInterface
  interface CommandOptionVisitor {

    /**
     * Visits a Blaze command by providing access to its name, its meta-data and its command line
     * options (via an {@link OptionsParser} instance).
     *
     * @param commandName name of the command, e.g. "help".
     * @param commandAnnotation {@link Command} that contains addition information about the
     *     command.
     * @param parser an {@link OptionsParser} instance that provides access to all options supported
     *     by the command.
     */
    void visit(String commandName, Command commandAnnotation, OptionsParser parser);
  }

  private static BlazeCommandResult createFailureResult(String message, Code detailedCode) {
    return BlazeCommandResult.failureDetail(
        FailureDetail.newBuilder()
            .setMessage(message)
            .setHelpCommand(FailureDetails.HelpCommand.newBuilder().setCode(detailedCode))
            .build());
  }
}

