// 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;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.devtools.build.lib.analysis.ConfiguredRuleClassProvider;
import com.google.devtools.build.lib.packages.semantics.BuildLanguageOptions;
import com.google.devtools.build.lib.util.ResourceFileLoader;
import com.google.devtools.common.options.HelpVerbosity;
import com.google.devtools.common.options.OptionsBase;
import com.google.devtools.common.options.OptionsParser;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

/** Utility class for functionality related to Blaze commands. */
public class BlazeCommandUtils {
  /** Options classes used as startup options in Blaze core. */
  private static final ImmutableList<Class<? extends OptionsBase>> DEFAULT_STARTUP_OPTIONS =
      ImmutableList.of(BlazeServerStartupOptions.class, HostJvmStartupOptions.class);

  /** The set of option-classes that are common to all Blaze commands. */
  private static final ImmutableList<Class<? extends OptionsBase>> COMMON_COMMAND_OPTIONS =
      ImmutableList.of(
          UiOptions.class,
          CommonCommandOptions.class,
          KeepStateAfterBuildOption.class,
          ClientOptions.class,
          // Starlark options aren't applicable to all commands, but making them a common option
          // allows users to put them in the common section of the bazelrc. See issue #3538.
          BuildLanguageOptions.class);

  private BlazeCommandUtils() {}

  public static ImmutableList<Class<? extends OptionsBase>> getStartupOptions(
      Iterable<OptionsSupplier> suppliers) {
    Set<Class<? extends OptionsBase>> options = new HashSet<>(DEFAULT_STARTUP_OPTIONS);
    for (OptionsSupplier supplier : suppliers) {
      Iterables.addAll(options, supplier.getStartupOptions());
    }

    return ImmutableList.copyOf(options);
  }

  public static ImmutableSet<Class<? extends OptionsBase>> getCommonOptions(
      Iterable<OptionsSupplier> suppliers) {
    ImmutableSet.Builder<Class<? extends OptionsBase>> builder = ImmutableSet.builder();
    builder.addAll(COMMON_COMMAND_OPTIONS);
    for (OptionsSupplier supplier : suppliers) {
      builder.addAll(supplier.getCommonCommandOptions());
    }
    return builder.build();
  }

  /**
   * Returns the set of all options (including those inherited directly and transitively) for this
   * AbstractCommand's @Command annotation.
   *
   * <p>Why does metaprogramming always seem like such a bright idea in the beginning?
   */
  public static ImmutableList<Class<? extends OptionsBase>> getOptions(
      Class<? extends BlazeCommand> clazz,
      Iterable<OptionsSupplier> suppliers,
      ConfiguredRuleClassProvider ruleClassProvider) {
    Command commandAnnotation = clazz.getAnnotation(Command.class);
    if (commandAnnotation == null) {
      throw new IllegalStateException("@Command missing for " + clazz.getName());
    }

    Set<Class<? extends OptionsBase>> options = new HashSet<>(getCommonOptions(suppliers));
    Collections.addAll(options, commandAnnotation.options());

    if (commandAnnotation.usesConfigurationOptions()) {
      options.addAll(ruleClassProvider.getFragmentRegistry().getOptionsClasses());
    }

    for (OptionsSupplier supplier : suppliers) {
      Iterables.addAll(options, supplier.getCommandOptions(commandAnnotation.name()));
    }

    for (Class<? extends BlazeCommand> base : commandAnnotation.inheritsOptionsFrom()) {
      options.addAll(getOptions(base, suppliers, ruleClassProvider));
    }
    return ImmutableList.copyOf(options);
  }

  /**
   * Returns the expansion of the specified help topic.
   *
   * @param topic the name of the help topic; used in %{command} expansion.
   * @param help the text template of the help message. Certain %{x} variables will be expanded. A
   *     prefix of "resource:" means use the .jar resource of that name.
   * @param helpVerbosity a tri-state verbosity option selecting between just names, names and
   *     syntax, and full description.
   * @param productName the product name
   */
  public static String expandHelpTopic(
      String topic,
      String help,
      Class<? extends BlazeCommand> commandClass,
      Collection<Class<? extends OptionsBase>> options,
      HelpVerbosity helpVerbosity,
      String productName) {
    OptionsParser parser = OptionsParser.builder().optionsClasses(options).build();

    String template;
    if (help.startsWith("resource:")) {
      String resourceName = help.substring("resource:".length());
      try {
        template = ResourceFileLoader.loadResource(commandClass, resourceName);
      } catch (IOException e) {
        throw new IllegalStateException(
            "failed to load help resource '"
                + resourceName
                + "' due to I/O error: "
                + e.getMessage(),
            e);
      }
    } else {
      template = help;
    }

    if (!template.contains("%{options}")) {
      throw new IllegalStateException("Help template for '" + topic + "' omits %{options}!");
    }

    String optionStr = parser.describeOptions(helpVerbosity).replace("%{product}", productName);

    return template
            .replace("%{product}", productName)
            .replace("%{command}", topic)
            .replace("%{options}", optionStr)
            .trim()
        + "\n\n"
        + (helpVerbosity == HelpVerbosity.MEDIUM
            ? "(Use 'help --long' for full details or --short to just enumerate options.)\n"
            : "");
  }

  /**
   * The help page for this command.
   *
   * @param verbosity a tri-state verbosity option selecting between just names, names and syntax,
   *     and full description.
   */
  public static String getUsage(
      Class<? extends BlazeCommand> commandClass,
      HelpVerbosity verbosity,
      Iterable<OptionsSupplier> optionsSuppliers,
      ConfiguredRuleClassProvider ruleClassProvider,
      String productName) {
    Command commandAnnotation = commandClass.getAnnotation(Command.class);
    return BlazeCommandUtils.expandHelpTopic(
        commandAnnotation.name(),
        commandAnnotation.help(),
        commandClass,
        getOptions(commandClass, optionsSuppliers, ruleClassProvider),
        verbosity,
        productName);
  }
}
