/*
 * Copyright (c) 2013 - present Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 */

package utils;

import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;

import org.junit.rules.TemporaryFolder;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;

import javax.annotation.Nullable;

public class InferRunner {

  public static final String BUGS_FILE_NAME = "report.csv";

  public static final String DOT_FILE_NAME = "icfg.dot";

  public static final String CAPTURED_FOLDER = "captured";

  private static File bugsFile;

  @Nullable
  private static File dotFile;

  private static File resultsDir;

  private static final ImmutableList<String> EMPTY_ARGS = ImmutableList.of();

  private static final String ANDROID =
      "/infer/lib/java/android/android-19.jar";

  private static final String[] LIBRARIES = {
      "/infer/lib/java/models.jar",
      "/infer/annotations/annotations.jar",
      "/dependencies/java/guava/guava-10.0.1-fork.jar",
      "/dependencies/java/jackson/jackson-2.2.3.jar",
  };

  private static final String CXX_INCLUDE_DIR =
      "/facebook-clang-plugins/clang/install/include/c++/v1/";

  private static final String IPHONESIMULATOR_ISYSROOT_SUFFIX =
      "/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk";

  private static HashMap<String, InferResults> inferResultsMap =
      new HashMap<String, InferResults>();

  private static String getXcodeRoot() throws IOException, InterruptedException {
    ProcessBuilder pb = new ProcessBuilder("xcode-select", "-p");
    Process process = pb.start();
    InputStream is = process.getInputStream();
    InputStreamReader isr = new InputStreamReader(is);
    BufferedReader br = new BufferedReader(isr);
    String line = br.readLine();
    process.waitFor();
    return line;
  }

  private static String getXCodeVersion() throws IOException, InterruptedException {
    ProcessBuilder pb = new ProcessBuilder("xcodebuild", "-version");
    Process process = pb.start();
    InputStream is = process.getInputStream();
    InputStreamReader isr = new InputStreamReader(is);
    BufferedReader br = new BufferedReader(isr);
    String line = br.readLine();
    process.waitFor();
    return line;
  }

  private static ImmutableList<String> createInferJavaCommand(
      TemporaryFolder folder,
      ImmutableList<String> sourceFiles,
      String analyzer,
      ImmutableList<String> args) {
    try {
      File resultsDir = createResultsDir(folder);
      String resultsDirName = resultsDir.getAbsolutePath();
      InferRunner.bugsFile = new File(resultsDir, BUGS_FILE_NAME);

      ImmutableList<String> javacCmd = createJavacCommand(
          folder,
          sourceFiles);

      return new ImmutableList.Builder<String>()
          .add("infer")
          .add("--no-progress-bar")
          .add("--no-filtering")
          .addAll(args)
          .add("-o")
          .add(resultsDirName)
          .add("-a")
          .add(analyzer)
          .add("--")
          .addAll(javacCmd)
          .build();
    } catch (IOException e) {
      e.printStackTrace();
      System.exit(1);
      return ImmutableList.of(); // unreachable
    }
  }

  private static ImmutableList<String> createJavacCommand(
      TemporaryFolder folder,
      ImmutableList<String> sourceFiles)
      throws IOException {
    File classesDir = folder.newFolder("classes_out");
    String classesDirName = classesDir.getAbsolutePath();

    ImmutableList<String> javacCmd = new ImmutableList.Builder<String>()
        .add("javac")
        .add("-classpath")
        .add(getClasspath())
        .add("-bootclasspath")
        .add(getBootClasspath())
        .add("-d")
        .add(classesDirName)
        .addAll(sourceFiles).build();
    return javacCmd;
  }

  public static ImmutableList<String> createJavaInferHarnessCommand(
      TemporaryFolder folder,
      ImmutableList<String> sourceFiles) {
    ImmutableList<String> args = new ImmutableList.Builder<String>()
      .add("--android-harness")
      .build();
    return createInferJavaCommand(folder, sourceFiles, "infer", args);
  }

  public static ImmutableList<String> createJavaInferHarnessCommand(
      TemporaryFolder folder,
      String sourceFile) {
    return createJavaInferHarnessCommand(folder, ImmutableList.of(sourceFile));
  }

  public static ImmutableList<String> createJavaInferAngelicHarnessCommand(
      TemporaryFolder folder,
      ImmutableList<String> sourceFiles) {
    ImmutableList<String> args = (new ImmutableList.Builder<String>())
      .add("--android-harness")
      .build();
    return createInferJavaCommand(folder, sourceFiles, "infer", args);
  }

  public static String getClangLangOption(Language lang) {
    String langOption = "";
    switch (lang) {
      case C:
        langOption = "c";
        break;

      case ObjC:
        langOption = "objective-c";
        break;

      case CPP:
        langOption = "c++";
        break;

      case ObjCPP:
        langOption = "objective-c++";
        break;

      default:
        throw new RuntimeException(
            "It should be called only with the "
                + "languages (C, C++, ObjC, ObjC++)");
    }
    return langOption;
  }

  public static String getStdParam(Language lang) {
    String stdParam = "";
    switch (lang) {
      case CPP:
        stdParam = "-std=c++11";
        break;
      case ObjCPP:
        stdParam = "-std=c++11";
        break;
    }
    return stdParam;
  }

  private static String getSystemHeaderFlag(Language lang) {
    String headerFlag = "";
    switch (lang) {
      case CPP:
        String current_dir = System.getProperty("user.dir");
        headerFlag = new StringBuilder()
          .append("-isystem")
          .append(current_dir)
          .append(CXX_INCLUDE_DIR)
          .toString();
        break;
    }
    return headerFlag;
  }

  public static ImmutableList<String> createClangCommand(
    String sourceFile,
    Language lang,
    @Nullable String isysroot,
    boolean arc) {
    ImmutableList.Builder<String> isysrootOption =
        new ImmutableList.Builder<>();
    if (isysroot != null) {
      isysrootOption
          .add("-isysroot")
          .add(isysroot)
          .add("-mios-simulator-version-min=8.2")
          .add("--target=x86_64-apple-darwin14");
    }
    ImmutableList.Builder<String> arcOption =
        new ImmutableList.Builder<>();
    if (arc) {
      arcOption.add("-fobjc-arc");
    }
    ImmutableList<String> clangCmd = new ImmutableList.Builder<String>()
        .add("clang")
        .add("-x")
        .add(getClangLangOption(lang))
        .add(getStdParam(lang))
        .add(getSystemHeaderFlag(lang))
        .addAll(isysrootOption.build())
        .addAll(arcOption.build())
        .add("-c")
        .add(sourceFile)
        .add("-o")
        .add(sourceFile + ".o")
        .build();
    return clangCmd;
  }

  public static ImmutableList<String> createClangInferCommand(
      TemporaryFolder folder,
      String sourceFile,
      Language lang,
      @Nullable String isysroot,
      boolean arc,
      ImmutableList<String> inferOptions) {
    File resultsDir = createResultsDir(folder);
    String resultsDirName = resultsDir.getAbsolutePath();
    InferRunner.bugsFile = new File(resultsDir, BUGS_FILE_NAME);

    return new ImmutableList.Builder<String>()
      .add("infer")
      .add("--cxx")
      .add("--no-filtering")
      .add("--out")
      .add(resultsDirName)
      .add("--no-progress-bar")
      .add("--testing-mode")
      .addAll(inferOptions)
      .add("--")
      .addAll(createClangCommand(sourceFile, lang, isysroot, arc))
      .build();
  }

  public static ImmutableList<String> createClangInferCommand(
      TemporaryFolder folder,
      String sourceFile,
      Language lang,
      String analyzer,
      @Nullable String isysroot,
      @Nullable String ml_buckets,
      boolean arc,
      ImmutableList<String> extraInferOptions) {
    ImmutableList.Builder<String> inferOptionsBuilder = new ImmutableList.Builder<String>()
      .addAll(extraInferOptions);
    inferOptionsBuilder.add("--analyzer").add(analyzer);

    inferOptionsBuilder
      .add("--ml_buckets")
      .add(ml_buckets == null ? "all" : ml_buckets);

    return createClangInferCommand(
        folder,
        sourceFile,
        lang,
        isysroot,
        arc,
        inferOptionsBuilder.build());
  }

  public static ImmutableList<String> createCInferCommandFrontend(
      TemporaryFolder folder,
      String sourceFile,
      ImmutableList<String> extraInferOptions) {
    return createClangInferCommand(
        folder,
        sourceFile,
        Language.C,
        "capture",
        null,
        null,
        false,
        extraInferOptions);
  }

  public static ImmutableList<String> createCInferCommandFrontend(
      TemporaryFolder folder,
      String sourceFile) {
    return createCInferCommandFrontend(folder, sourceFile, ImmutableList.<String>of());
  }

  public static ImmutableList<String> createCPPInferCommandFrontend(
      TemporaryFolder folder,
      String sourceFile,
      ImmutableList<String> extraInferOptions) {
    return createClangInferCommand(
        folder,
        sourceFile,
        Language.CPP,
        "capture",
        null,
        null,
        false,
        extraInferOptions);
  }

  public static ImmutableList<String> createCPPInferCommandFrontend(
      TemporaryFolder folder,
      String sourceFile) {
    return createCPPInferCommandFrontend(folder, sourceFile, ImmutableList.<String>of());
  }

  public static ImmutableList<String> createObjCInferCommandFrontend(
      TemporaryFolder folder,
      String sourceFile,
      ImmutableList<String> extraInferOptions) throws IOException, InterruptedException {
    return createClangInferCommand(
        folder,
        sourceFile,
        Language.ObjC,
        "capture",
        getXcodeRoot() + IPHONESIMULATOR_ISYSROOT_SUFFIX,
        null,
        false,
        extraInferOptions);
  }

  public static ImmutableList<String> createObjCInferCommandFrontend(
      TemporaryFolder folder,
      String sourceFile) throws IOException, InterruptedException {
    return createObjCInferCommandFrontend(folder, sourceFile, ImmutableList.<String>of());
  }

  public static ImmutableList<String> createObjCInferCommandFrontendArc(
      TemporaryFolder folder,
      String sourceFile,
      ImmutableList<String> extraInferOptions) throws IOException, InterruptedException {
    return createClangInferCommand(
        folder,
        sourceFile,
        Language.ObjC,
        "capture",
        getXcodeRoot() + IPHONESIMULATOR_ISYSROOT_SUFFIX,
        null,
        true,
        extraInferOptions);
  }

  public static ImmutableList<String> createObjCInferCommandFrontendArc(
      TemporaryFolder folder,
      String sourceFile) throws IOException, InterruptedException {
    return createObjCInferCommandFrontendArc(folder, sourceFile, ImmutableList.<String>of());
  }

  public static ImmutableList<String> createObjCPPInferCommandFrontend(
      TemporaryFolder folder,
      String sourceFile,
      ImmutableList<String> extraInferOptions) throws IOException, InterruptedException {
    return createClangInferCommand(
        folder,
        sourceFile,
        Language.ObjCPP,
        "capture",
        getXcodeRoot() + IPHONESIMULATOR_ISYSROOT_SUFFIX,
        null,
        false,
        extraInferOptions);
  }

  public static ImmutableList<String> createObjCPPInferCommandFrontend(
      TemporaryFolder folder,
      String sourceFile) throws IOException, InterruptedException {
    return createObjCPPInferCommandFrontend(folder, sourceFile, ImmutableList.<String>of());
  }

  public static ImmutableList<String> createCInferCommand(
      TemporaryFolder folder,
      String sourceFile,
      ImmutableList<String> extraInferOptions) {
    return createClangInferCommand(
        folder,
        sourceFile,
        Language.C,
        "infer",
        null,
        null,
        false,
        extraInferOptions);
  }

  public static ImmutableList<String> createCInferCommand(
      TemporaryFolder folder,
      String sourceFile) {
    return createCInferCommand(folder, sourceFile, ImmutableList.<String>of());
  }

  public static ImmutableList<String> createCPPInferCommand(
      TemporaryFolder folder,
      String sourceFile,
      ImmutableList<String> extraInferOptions) {
    ImmutableList<String> args = new ImmutableList.Builder<String> ()
      .add("--no-testing-mode")
      .addAll(extraInferOptions)
      .build();
    return createClangInferCommand(
        folder,
        sourceFile,
        Language.CPP,
        "infer",
        null,
        null,
        false,
        args);
  }

  public static ImmutableList<String> createCPPInferCommand(
      TemporaryFolder folder,
      String sourceFile) {
    return createCPPInferCommand(folder, sourceFile, ImmutableList.<String>of());
  }

  public static ImmutableList<String> createCPPInferCommandIncludeHeaders(
      TemporaryFolder folder,
      String sourceFile) {
    return createCPPInferCommand(
        folder,
        sourceFile,
        ImmutableList.<String>of("--testing-mode", "--headers"));
  }

  public static ImmutableList<String> createCPPInferCommandWithMLBuckets(
      TemporaryFolder folder,
      String sourceFile,
      String ml_bucket) throws IOException, InterruptedException {
    return createClangInferCommand(
        folder,
        sourceFile,
        Language.CPP,
        "infer",
        null,
        ml_bucket,
        false,
        ImmutableList.<String>of());
  }

  public static ImmutableList<String> createObjCInferCommand(
      TemporaryFolder folder,
      String sourceFile) throws IOException, InterruptedException {
    return createClangInferCommand(
        folder,
        sourceFile,
        Language.ObjC,
        "infer",
        getXcodeRoot() + IPHONESIMULATOR_ISYSROOT_SUFFIX,
        null,
        false,
        ImmutableList.<String>of());
  }

  public static ImmutableList<String> createObjCInferCommandWithMLBuckets(
      TemporaryFolder folder,
      String sourceFile,
      String ml_bucket,
      boolean arc) throws IOException, InterruptedException {
    return createClangInferCommand(
        folder,
        sourceFile,
        Language.ObjC,
        "infer",
        getXcodeRoot() + IPHONESIMULATOR_ISYSROOT_SUFFIX,
        ml_bucket,
        arc,
        ImmutableList.<String>of());
  }

  public static ImmutableList<String> createObjCPPInferCommand(
      TemporaryFolder folder,
      String sourceFile) throws IOException, InterruptedException {
    return createClangInferCommand(
        folder,
        sourceFile,
        Language.ObjCPP,
        "infer",
        getXcodeRoot() + IPHONESIMULATOR_ISYSROOT_SUFFIX,
        null,
        false,
        ImmutableList.<String>of());
  }

  public static ImmutableList<String> createiOSInferCommandFrontend(
      TemporaryFolder folder,
      String sourceFile) throws IOException, InterruptedException {
    return createClangInferCommand(
        folder,
        sourceFile,
        Language.ObjC,
        "infer",
        getXcodeRoot() + IPHONESIMULATOR_ISYSROOT_SUFFIX,
        null,
        false,
        ImmutableList.<String>of());
  }

  public static ImmutableList<String> createiOSInferCommand(
      TemporaryFolder folder,
      String sourceFile) throws IOException, InterruptedException {
    return createClangInferCommand(
        folder,
        sourceFile,
        Language.ObjC,
        "infer",
        getXcodeRoot() + IPHONESIMULATOR_ISYSROOT_SUFFIX,
        null,
        false,
        ImmutableList.<String>of());
  }

  public static ImmutableList<String> createiOSInferCommandWithMLBuckets(
      TemporaryFolder folder,
      String sourceFile,
      String bucket,
      boolean arc) throws IOException, InterruptedException {
    return createClangInferCommand(
        folder,
        sourceFile,
        Language.ObjC,
        "infer",
        getXcodeRoot() + IPHONESIMULATOR_ISYSROOT_SUFFIX,
        bucket,
        arc,
        ImmutableList.<String>of());
  }

  public static ImmutableList<String> createLintersCommand(
      TemporaryFolder folder,
      String sourceFile,
      Language lang) throws IOException, InterruptedException {
    return createClangInferCommand(
        folder,
        sourceFile,
        lang,
        "linters",
        getXcodeRoot() + IPHONESIMULATOR_ISYSROOT_SUFFIX,
        null,
        true,
        ImmutableList.<String>of());
  }

  public static ImmutableList<String> createObjCLintersCommand(
      TemporaryFolder folder,
      String sourceFile) throws IOException, InterruptedException {
    return createLintersCommand(folder, sourceFile, Language.ObjC);
  }

  public static ImmutableList<String> createObjCLintersCommandSimple(
      TemporaryFolder folder,
      String sourceFile) throws IOException, InterruptedException {
    return createClangInferCommand(
        folder,
        sourceFile,
        Language.ObjC,
        "linters",
        null,
        null,
        true,
        ImmutableList.<String>of());
  }

  public static ImmutableList<String> createObjCPPLintersCommand(
      TemporaryFolder folder,
      String sourceFile) throws IOException, InterruptedException {
    return createLintersCommand(folder, sourceFile, Language.ObjCPP);
  }


  @Nullable
  public static File runInferFrontend(ImmutableList<String> inferCmd)
      throws IOException, InterruptedException, InferException {
    runCommand(inferCmd, TestType.FRONTEND);
    return dotFile;
  }

  private static InferResults runInfer(
      ImmutableList<String> inferCmd,
      Language lang)
      throws IOException, InterruptedException, InferException {
    String inferCmdString = Joiner.on(' ').join(inferCmd);
    InferResults results = inferResultsMap.get(inferCmdString);
    if (results == null) {
      if (lang == Language.Java) checkLibraries();

      runCommand(inferCmd, TestType.END_TO_END);
      String errorString = getErrors(bugsFile);
      results = new InferResults(inferCmd);
      if (lang == Language.Java)
        results.parseJavaInferResultsFromString(errorString);
      else if (lang == Language.C || lang == Language.CPP || lang == Language.ObjCPP)
        results.parseCInferResultsFromString(errorString);
      else if (lang == Language.ObjC)
        results.parseObjCInferResultsFromString(errorString);
      inferResultsMap.put(inferCmdString, results);
    }
    return results;
  }

  public static InferResults runInferJava(ImmutableList<String> inferCmd)
      throws IOException, InterruptedException, InferException {
    return runInfer(inferCmd, Language.Java);
  }

  public static InferResults runInferC(ImmutableList<String> inferCmd)
      throws IOException, InterruptedException, InferException {
    return runInfer(inferCmd, Language.C);
  }

  public static InferResults runInferCPP(ImmutableList<String> inferCmd)
      throws IOException, InterruptedException, InferException {
    return runInfer(inferCmd, Language.CPP);
  }

  public static InferResults runInferObjC(ImmutableList<String> inferCmd)
      throws IOException, InterruptedException, InferException {
    return runInfer(inferCmd, Language.ObjC);
  }

  public static InferResults runInferObjCPP(ImmutableList<String> inferCmd)
      throws IOException, InterruptedException, InferException {
    return runInfer(inferCmd, Language.ObjCPP);
  }


  private static void runCommand(
      ImmutableList<String> inferCmd,
      TestType testType)
      throws IOException, InterruptedException, InferException {
    ProcessBuilder pb = new ProcessBuilder(inferCmd);

    Map<String, String> env = pb.environment();
    env.put("INFER_REPORT_CUSTOM_ERROR", "1");

    Process process = pb.start();
    StringBuilder stderr = new StringBuilder();
    try (BufferedReader bufferedReader =
             new BufferedReader(
                 new InputStreamReader(
                     process.getErrorStream()))) {
      String line = null;
      while ((line = bufferedReader.readLine()) != null) {
        stderr.append(line + "\n");
      }
    }
    process.waitFor();

    File file = null;
    if (testType == TestType.END_TO_END) {
      file = bugsFile;
    } else if (testType == TestType.FRONTEND) {
      getDotFile();
      file = dotFile;
    }
    if (file == null || !file.exists()) {
      String fileNotFoundMessage = file == null ?
          "" : "\n\nFile " + file + " not found.";
      throw new InferException(
          "There was an error while calling Infer." +
              "\n\nCommand output:\n" + stderr +
              "\nCommand: " + Joiner.on(' ').join(inferCmd) +
              fileNotFoundMessage +
              "\n==========================================================================");
    }
  }

  static String getErrors(File file) throws IOException {
    String s = "";
    String line = "";
    try (BufferedReader br = new BufferedReader(new FileReader(file))) {
      br.readLine();
      while ((line = br.readLine()) != null) {
        s += line + "\n";
      }
    }
    return s;
  }

  private static String getBootClasspath() {
    String current_dir = System.getProperty("user.dir");
    String bootclasspath = current_dir + ANDROID;
    return bootclasspath;
  }

  private static String getClasspath() {
    String current_dir = System.getProperty("user.dir");
    StringBuilder classpath = new StringBuilder();
    for (String library : LIBRARIES) {
      classpath.append(current_dir);
      classpath.append(library);
      classpath.append(":");
    }
    classpath.deleteCharAt(classpath.length() - 1);
    return classpath.toString();
  }

  private static void checkLibraries() throws InferException {
    String current_dir = System.getProperty("user.dir");
    for (String lib_file : LIBRARIES) {
      File lib = new File(current_dir + lib_file);
      if (!lib.exists()) {
        throw new InferException("File " + lib_file + " not found.");
      }
    }
  }

  private static File createResultsDir(TemporaryFolder folder) {
    try {
      resultsDir = folder.newFolder("infer_out");
      return resultsDir;
    } catch (IOException e) {
      e.printStackTrace();
      System.exit(1);
      return null; // unreachable
    }
  }

  private static void getDotFile() {
    File captured = new File(resultsDir, CAPTURED_FOLDER);
    if (captured.exists()) {
      File f = new File(captured, captured.list()[0]);
      dotFile = new File(f, DOT_FILE_NAME);
    } else {
      dotFile = null;
    }
  }
}