// 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.util.ResourceFileLoader;
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.Map;
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.<Class<? extends OptionsBase>>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(CommonCommandOptions.class, BlazeCommandEventHandler.Options.class);


  private BlazeCommandUtils() {}

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

    return ImmutableList.copyOf(options);
  }

  public static ImmutableSet<Class<? extends OptionsBase>> getCommonOptions(
      Iterable<BlazeModule> modules) {
    ImmutableSet.Builder<Class<? extends OptionsBase>> builder = ImmutableSet.builder();
    builder.addAll(COMMON_COMMAND_OPTIONS);
    for (BlazeModule blazeModule : modules) {
      builder.addAll(blazeModule.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<BlazeModule> modules,
      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<>();
    options.addAll(getCommonOptions(modules));
    Collections.addAll(options, commandAnnotation.options());

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

    for (BlazeModule blazeModule : modules) {
      Iterables.addAll(options, blazeModule.getCommandOptions(commandAnnotation));
    }

    for (Class<? extends BlazeCommand> base : commandAnnotation.inherits()) {
      options.addAll(getOptions(base, modules, 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 categoryDescriptions a mapping from option category names to
   *        descriptions, passed to {@link OptionsParser#describeOptions}.
   * @param helpVerbosity a tri-state verbosity option selecting between just
   *        names, names and syntax, and full description.
   * @param productName the product name
   */
  public static final String expandHelpTopic(String topic, String help,
                                      Class<? extends BlazeCommand> commandClass,
                                      Collection<Class<? extends OptionsBase>> options,
                                      Map<String, String> categoryDescriptions,
                                      OptionsParser.HelpVerbosity helpVerbosity,
                                      String productName) {
    OptionsParser parser = OptionsParser.newOptionsParser(options);

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

  /**
   * The help page for this command.
   *
   * @param categoryDescriptions a mapping from option category names to
   *        descriptions, passed to {@link OptionsParser#describeOptions}.
   * @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,
      Map<String, String> categoryDescriptions,
      OptionsParser.HelpVerbosity verbosity,
      Iterable<BlazeModule> blazeModules,
      ConfiguredRuleClassProvider ruleClassProvider,
      String productName) {
    Command commandAnnotation = commandClass.getAnnotation(Command.class);
    return BlazeCommandUtils.expandHelpTopic(
        commandAnnotation.name(),
        commandAnnotation.help(),
        commandClass,
        BlazeCommandUtils.getOptions(commandClass, blazeModules, ruleClassProvider),
        categoryDescriptions,
        verbosity,
        productName);
  }
}
