// Copyright 2017 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.bfg;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.devtools.build.bfg.BuildozerCommandCreator.computeBuildozerCommands;
import static com.google.devtools.build.bfg.BuildozerCommandCreator.getBuildFilesForBuildozer;
import static com.google.devtools.build.bfg.ClassGraphPreprocessor.preProcessClassGraph;
import static java.nio.charset.StandardCharsets.UTF_8;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.graph.GraphBuilder;
import com.google.common.graph.ImmutableGraph;
import com.google.common.graph.MutableGraph;
import com.google.re2j.Pattern;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Map;
import org.kohsuke.args4j.CmdLineParser;
import org.kohsuke.args4j.Option;
import protos.com.google.devtools.build.bfg.Bfg.ParserOutput;

/** Entry point to the BUILD file generator. */
public class Bfg {

  @Option(name = "--buildozer", usage = "Path to the Buildozer binary")
  private String buildozerPath = "/usr/bin/buildozer";

  @Option(
    name = "--dry_run",
    usage =
        "Boolean flag indicating whether to execute the generated buildozer commands. "
            + "If true, commands will be printed but not executed."
            + "If false, they will be executed but not printed."
  )
  private boolean isDryRun = false;

  @Option(
    name = "--user_defined_mapping",
    usage = "Path to a file mapping class names to build rules"
  )
  private String userMapping = "";

  @Option(name = "--workspace", usage = "Path to the project's bazel WORKSPACE file")
  private String workspacePath = getCurrentWorkingDirectory();

  @Option(
    name = "--whitelist",
    usage =
        "Regular expression used to determine which classes to generate BUILD rules for. BUILD-file "
            + "generator will generate rules for any class-names whose fully-qualified name matches "
            + "this flag, where 'matches' means substring matching, e.g. 'bfg' matches "
            + "'com.bfg.Foo'."
  )
  private String whiteListRegex = "";

  @Option(
    name = "--blacklist",
    usage = "Regular expression used to determine which classes to ignore."
  )
  private String blackListRegex = "AutoValue_";

  @Option(
    name = "--external_resolvers",
    usage =
        "Comma-separated list of executables that will be used to resolve any classname --> BUILD "
            + "rules that couldn't be resolved by us. For example, looking up an external index."
  )
  private String externalResolvers = "";

  public static void main(String[] args) throws Exception {
    new Bfg().run(args);
  }

  private void run(String[] args) throws Exception {
    CmdLineParser cmdLineParser = new CmdLineParser(this);
    cmdLineParser.parseArgument(args);

    ParserOutput parserOutput = ParserOutput.parseFrom(System.in);
    if (parserOutput.getClassToClassMap().isEmpty()) {
      explainUsageErrorAndExit(cmdLineParser, "Expected nonempty class graph as input");
    }
    if (whiteListRegex.isEmpty()) {
      explainUsageErrorAndExit(cmdLineParser, "The --whitelist flag is required.");
    }
    Pattern whiteList = compilePattern(cmdLineParser, whiteListRegex);
    Pattern blackList = compilePattern(cmdLineParser, blackListRegex);

    ImmutableGraph<String> classGraph =
        preProcessClassGraph(
            protoMultimapToGraph(parserOutput.getClassToClassMap()), whiteList, blackList);
    ImmutableMap<String, Path> classToFiles =
        protoMultimapToPathsMap(parserOutput.getClassToFileMap());

    Path workspace = Paths.get(workspacePath);

    ImmutableList<String> userDefinedMapping;
    if (userMapping.isEmpty()) {
      userDefinedMapping = ImmutableList.of();
    } else {
      userDefinedMapping = ImmutableList.copyOf(Files.readAllLines(Paths.get(userMapping)));
    }

    ImmutableList.Builder<ClassToRuleResolver> resolvers =
        ImmutableList.<ClassToRuleResolver>builder()
            .add(new ProjectClassToRuleResolver(classGraph, whiteList, classToFiles, workspace))
            .add(new UserDefinedResolver(userDefinedMapping));
    for (String r : Splitter.on(',').omitEmptyStrings().split(externalResolvers)) {
      resolvers.add(new ExternalResolver(r));
    }

    ImmutableGraph<BuildRule> buildRuleGraph =
        new GraphProcessor(classGraph).createBuildRuleDAG(resolvers.build());

    executeBuildozerCommands(buildRuleGraph, workspace, isDryRun, buildozerPath);
  }

  private ImmutableMap<String, Path> protoMultimapToPathsMap(
      Map<String, protos.com.google.devtools.build.bfg.Bfg.Strings> classToFileMap) {
    ImmutableMap.Builder<String, Path> result = ImmutableMap.builder();
    classToFileMap.forEach(
        (classname, filenames) -> {
          checkState(
              filenames.getElementsList().size() == 1,
              "BFG currently only supports a single file per class, got %s --> %s",
              classname,
              filenames);
          for (String s : filenames.getElementsList()) {
            result.put(classname, Paths.get(s));
          }
        });
    return result.build();
  }

  private ImmutableGraph<String> protoMultimapToGraph(
      Map<String, protos.com.google.devtools.build.bfg.Bfg.Strings> m) {
    MutableGraph<String> result = GraphBuilder.directed().build();
    m.forEach(
        (u, deps) -> {
          for (String s : deps.getElementsList()) {
            result.putEdge(u, s);
          }
        });
    return ImmutableGraph.copyOf(result);
  }

  private static Pattern compilePattern(CmdLineParser cmdLineParser, String patternString) {
    try {
      return Pattern.compile(patternString);
    } catch (IllegalArgumentException e) {
      explainUsageErrorAndExit(cmdLineParser, String.format("Invalid regex: %s", e.getMessage()));
      return null;
    }
  }

  private static void explainUsageErrorAndExit(CmdLineParser cmdLineParser, String message) {
    System.err.println(message);
    cmdLineParser.printUsage(System.err);
    System.exit(1);
  }

  /**
   * Given a graph of build rules, some project specific and some external, this method constructs a
   * list of buildozer commands, that are then used to write various BUILD files to disk.
   */
  static void executeBuildozerCommands(
      ImmutableGraph<BuildRule> ruleDAG, Path workspace, boolean isDryRun, String buildozerBinary)
      throws IOException, InterruptedException {
    ImmutableList<String> commands = computeBuildozerCommands(ruleDAG);
    if (isDryRun) {
      commands.stream().forEach(command -> System.out.println(command));
      return;
    }

    for (Path buildFileDir : getBuildFilesForBuildozer(ruleDAG, workspace)) {
      generateBuildFileIfNecessary(buildFileDir);
    }
    File tempFile = File.createTempFile("bfgOutput", ".txt");
    try {
      Files.write(tempFile.toPath(), commands, StandardCharsets.US_ASCII);
      ProcessBuilder pb = new ProcessBuilder();
      pb.command(buildozerBinary, "-f", tempFile.toPath().toString(), "-k");
      pb.environment().clear();
      Process p = pb.start();
      if (p.waitFor() != 0) {
        System.err.println("Error from executing buildozer:");
        try (BufferedReader stderr =
            new BufferedReader(new InputStreamReader(p.getErrorStream(), UTF_8))) {
          String line;
          while ((line = stderr.readLine()) != null) {
            System.err.println(line);
          }
        }
      }
    } finally {
      Files.delete(tempFile.toPath());
    }
  }

  /**
   * Checks if a BUILD file exists at a given path. If it does not, then it generates the BUILD and
   * any necessary intermediary directories.
   */
  @VisibleForTesting
  static void generateBuildFileIfNecessary(Path buildFileDirectory)
      throws InterruptedException, IOException {
    checkState(Files.isDirectory(buildFileDirectory) || !Files.exists(buildFileDirectory));
    if (Files.exists(buildFileDirectory.resolve("BUILD"))) {
      return;
    }
    if (!Files.exists(buildFileDirectory)) {
      Files.createDirectories(buildFileDirectory);
    }
    Files.createFile(buildFileDirectory.resolve("BUILD"));
  }

  private static String getCurrentWorkingDirectory() {
    return Paths.get(checkNotNull(System.getProperty("user.dir"))).toString();
  }
}