// Copyright 2018 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.skydoc;

import static java.nio.charset.StandardCharsets.UTF_8;

import com.google.common.collect.ImmutableMap;
import com.google.devtools.build.lib.events.EventHandler;
import com.google.devtools.build.lib.events.Location;
import com.google.devtools.build.lib.skylarkbuildapi.TopLevelBootstrap;
import com.google.devtools.build.lib.syntax.BuildFileAST;
import com.google.devtools.build.lib.syntax.Environment;
import com.google.devtools.build.lib.syntax.Environment.Extension;
import com.google.devtools.build.lib.syntax.Environment.GlobalFrame;
import com.google.devtools.build.lib.syntax.MethodLibrary;
import com.google.devtools.build.lib.syntax.Mutability;
import com.google.devtools.build.lib.syntax.ParserInputSource;
import com.google.devtools.build.lib.syntax.Runtime;
import com.google.devtools.build.lib.vfs.PathFragment;
import com.google.devtools.build.skydoc.fakebuildapi.FakeActionsInfoProvider;
import com.google.devtools.build.skydoc.fakebuildapi.FakeBuildApiGlobals;
import com.google.devtools.build.skydoc.fakebuildapi.FakeDefaultInfoProvider;
import com.google.devtools.build.skydoc.fakebuildapi.FakeOutputGroupInfo.FakeOutputGroupInfoProvider;
import com.google.devtools.build.skydoc.fakebuildapi.FakeSkylarkAttrApi;
import com.google.devtools.build.skydoc.fakebuildapi.FakeSkylarkCommandLineApi;
import com.google.devtools.build.skydoc.fakebuildapi.FakeSkylarkNativeModuleApi;
import com.google.devtools.build.skydoc.fakebuildapi.FakeSkylarkRuleFunctionsApi;
import com.google.devtools.build.skydoc.fakebuildapi.FakeStructApi.FakeStructProviderApi;
import com.google.devtools.build.skydoc.rendering.RuleInfo;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Main entry point for the Skydoc binary.
 *
 * <p>Skydoc generates human-readable documentation for relevant details of skylark files by
 * running a skylark interpreter with a fake implementation of the build API.</p>
 *
 * <p>Currently, Skydoc generates documentation for skylark rule definitions (discovered by
 * invocations of the build API function {@code rule()}.</p>
 *
 * <p>Usage:</p>
 * <pre>
 *   skydoc {target_skylark_file} {output_file}
 * </pre>
 */
public class SkydocMain {

  // Pattern to match the assignment of a variable to a rule definition
  // For example, 'my_rule = rule(' will match and have 'my_rule' available as group(1).
  private static final Pattern ruleDefinitionLinePattern =
      Pattern.compile("([^\\s]+) = rule\\(");

  private final EventHandler eventHandler = new SystemOutEventHandler();

  public static void main(String[] args) throws IOException, InterruptedException {
    if (args.length != 2) {
      throw new IllegalArgumentException("Expected two arguments. Usage:\n"
          + "{skydoc_bin} {target_skylark_file} {output_file}");
    }

    String bzlPath = args[0];
    String outputPath = args[1];

    Path path = Paths.get(bzlPath);
    byte[] content = Files.readAllBytes(path);

    ParserInputSource parserInputSource =
        ParserInputSource.create(content, PathFragment.create(path.toString()));

    List<RuleInfo> ruleInfoList = new SkydocMain().eval(parserInputSource);

    try (PrintWriter printWriter = new PrintWriter(outputPath, "UTF-8")) {
      printRuleInfos(printWriter, ruleInfoList);
    }
  }

  // TODO(cparsons): Improve output (markdown or HTML).
  private static void printRuleInfos(
      PrintWriter printWriter, List<RuleInfo> ruleInfos) throws IOException {
    for (RuleInfo ruleInfo : ruleInfos) {
      Location location = ruleInfo.getLocation();
      Path filePath = Paths.get(location.getPath().getPathString());
      List<String> lines = Files.readAllLines(filePath, UTF_8);
      String definingString = lines.get(location.getStartLine() - 1);
      // Rule definitions don't specify their own visible name directly. Instead, the name of
      // a rule is dependent on the name of the variable assigend to the return value of rule().
      // This attempts to find a line of the form 'foo = rule(' and thus label the rule as
      // named 'foo'.
      // TODO(cparsons): Inspect the global bindings of the environment instead of using string
      // matching.
      Matcher matcher = ruleDefinitionLinePattern.matcher(definingString);
      if (matcher.matches()) {
        printWriter.println(matcher.group(1));
      } else {
        printWriter.println("<unknown name>");
      }
      printWriter.println(ruleInfo.getDescription());
    }
  }

  /**
   * Evaluates/interprets the skylark file at the given input source using a fake build API and
   * collects information about all rule definitions made in that file.
   *
   * @param parserInputSource the input source representing the input skylark file
   * @return a list of {@link RuleInfo} objects describing the rule definitions
   * @throws InterruptedException if evaluation is interrupted
   */
  // TODO(cparsons): Evaluate load statements recursively.
  public List<RuleInfo> eval(ParserInputSource parserInputSource)
      throws InterruptedException {
    List<RuleInfo> ruleInfoList = new ArrayList<>();

    BuildFileAST buildFileAST = BuildFileAST.parseSkylarkFile(
        parserInputSource, eventHandler);

    Environment env = createEnvironment(
        eventHandler,
        globalFrame(ruleInfoList),
        /* imports= */ ImmutableMap.of());

    if (!buildFileAST.exec(env, eventHandler)) {
      throw new RuntimeException("Error loading file");
    }

    env.mutability().freeze();

    return ruleInfoList;
  }

  /**
   * Initialize and return a global frame containing the fake build API.
   *
   * @param ruleInfoList the list of {@link RuleInfo} objects, to which rule() invocation
   *     information will be added
   */
  private static GlobalFrame globalFrame(List<RuleInfo> ruleInfoList) {
    // TODO(cparsons): Complete the Fake Build API stubs. For example, implement provider(),
    // and include the other bootstraps.
    TopLevelBootstrap topLevelBootstrap =
        new TopLevelBootstrap(new FakeBuildApiGlobals(),
            new FakeSkylarkAttrApi(),
            new FakeSkylarkCommandLineApi(),
            new FakeSkylarkNativeModuleApi(),
            new FakeSkylarkRuleFunctionsApi(ruleInfoList),
            new FakeStructProviderApi(),
            new FakeOutputGroupInfoProvider(),
            new FakeActionsInfoProvider(),
            new FakeDefaultInfoProvider());

    ImmutableMap.Builder<String, Object> envBuilder = ImmutableMap.builder();

    Runtime.addConstantsToBuilder(envBuilder);
    MethodLibrary.addBindingsToBuilder(envBuilder);
    topLevelBootstrap.addBindingsToBuilder(envBuilder);

    return GlobalFrame.createForBuiltins(envBuilder.build());
  }

  private static Environment createEnvironment(EventHandler eventHandler, GlobalFrame globals,
      Map<String, Extension> imports) {
    return Environment.builder(Mutability.create("Skydoc"))
        .useDefaultSemantics()
        .setGlobals(globals)
        .setImportedExtensions(imports)
        .setEventHandler(eventHandler)
        .build();
  }
}

{'avg_line_length': 41.403141361256544, 'max_line_length': 101, 'alphanum_fraction': 0.7427921092564491, 'licenses': ['Apache-2.0'], 'repository_name': 'nikhita/bazel', 'path': 'src/main/java/com/google/devtools/build/skydoc/SkydocMain.java', 'size': 7908, 'lang': 'Java'}