/*
 * Copyright 2012-present Facebook, Inc.
 *
 * 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.facebook.buck.java;


import com.facebook.buck.io.ProjectFilesystem;
import com.facebook.buck.model.BuildTarget;
import com.facebook.buck.rules.BuildRule;
import com.facebook.buck.rules.RuleKeyBuilder;
import com.facebook.buck.rules.SourcePath;
import com.facebook.buck.rules.SourcePathResolver;
import com.facebook.buck.step.ExecutionContext;
import com.facebook.buck.util.Ansi;
import com.facebook.buck.util.Console;
import com.facebook.buck.util.HumanReadableException;
import com.facebook.buck.util.ProcessExecutor;
import com.facebook.buck.util.ProcessExecutorParams;
import com.facebook.buck.util.Verbosity;
import com.facebook.buck.zip.Unzip;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Functions;
import com.google.common.base.Joiner;
import com.google.common.base.Optional;
import com.google.common.base.Predicate;
import com.google.common.base.Strings;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.nio.file.Path;
import java.util.Map;

public class ExternalJavac implements Javac {

  private static final JavacVersion DEFAULT_VERSION = JavacVersion.of("unknown version");

  private final Path pathToJavac;
  private final Supplier<JavacVersion> version;

  public ExternalJavac(final Path pathToJavac) {
    this.pathToJavac = pathToJavac;

    this.version = Suppliers.memoize(
        new Supplier<JavacVersion>() {
          @Override
          public JavacVersion get() {
            ProcessExecutorParams params = ProcessExecutorParams.builder()
                .setCommand(ImmutableList.of(pathToJavac.toString(), "-version"))
                .build();
            ProcessExecutor.Result result;
            try (
                PrintStream stdout = new PrintStream(new ByteArrayOutputStream());
                PrintStream stderr = new PrintStream(new ByteArrayOutputStream())) {
              result = createProcessExecutor(stdout, stderr).launchAndExecute(params);
            } catch (InterruptedException | IOException e) {
              throw new RuntimeException(e);
            }
            Optional<String> stderr = result.getStderr();
            String output = stderr.or("").trim();
            if (Strings.isNullOrEmpty(output)) {
              return DEFAULT_VERSION;
            } else {
              return JavacVersion.of(output);
            }
          }
        });
  }

  @Override
  public ImmutableCollection<BuildRule> getDeps(SourcePathResolver resolver) {
    return ImmutableSortedSet.of();
  }

  @Override
  public ImmutableCollection<SourcePath> getInputs() {
    return ImmutableSortedSet.of();
  }

  @Override
  public ImmutableList<String> getCommandPrefix(SourcePathResolver resolver) {
    return ImmutableList.<String>builder().add(pathToJavac.toString()).build();
  }

  public static Javac createJavac(Path pathToJavac) {
    return new ExternalJavac(pathToJavac);
  }

  @Override
  public JavacVersion getVersion() {
    return version.get();
  }

  @VisibleForTesting
  ProcessExecutor createProcessExecutor(PrintStream stdout, PrintStream stderr) {
    return new ProcessExecutor(
              new Console(
                  Verbosity.SILENT,
                  stdout,
                  stderr,
                  Ansi.withoutTty()));
  }

  @Override
  public String getDescription(
      ImmutableList<String> options,
      ImmutableSet<Path> javaSourceFilePaths,
      Optional<Path> pathToSrcsList) {
    StringBuilder builder = new StringBuilder(pathToJavac.toString());
    builder.append(" ");
    Joiner.on(" ").appendTo(builder, options);
    builder.append(" ");

    if (pathToSrcsList.isPresent()) {
      builder.append("@").append(pathToSrcsList.get());
    } else {
      Joiner.on(" ").appendTo(builder, javaSourceFilePaths);
    }

    return builder.toString();
  }

  @Override
  public String getShortName() {
    return pathToJavac.toString();
  }

  @Override
  public RuleKeyBuilder appendToRuleKey(RuleKeyBuilder builder) {
    if (DEFAULT_VERSION.equals(getVersion())) {
      return builder.setReflectively("javac", pathToJavac);
    }

    return builder.setReflectively("javac.version", getVersion().toString());
  }

  public Path getPath() {
    return pathToJavac;
  }

  @Override
  public int buildWithClasspath(
      ExecutionContext context,
      SourcePathResolver resolver,
      BuildTarget invokingRule,
      ImmutableList<String> options,
      ImmutableSet<Path> javaSourceFilePaths,
      Optional<Path> pathToSrcsList,
      Optional<Path> workingDirectory) throws InterruptedException {
    ImmutableList.Builder<String> command = ImmutableList.builder();
    command.add(pathToJavac.toString());
    command.addAll(options);

    ImmutableList<Path> expandedSources;
    try {
      expandedSources = getExpandedSourcePaths(
          context,
          invokingRule,
          javaSourceFilePaths,
          workingDirectory);
    } catch (IOException e) {
      throw new HumanReadableException(
          "Unable to expand sources for %s into %s",
          invokingRule,
          workingDirectory);
    }
    if (pathToSrcsList.isPresent()) {
      try {
        context.getProjectFilesystem().writeLinesToPath(
            FluentIterable.from(expandedSources)
                .transform(Functions.toStringFunction())
                .transform(ARGFILES_ESCAPER),
            pathToSrcsList.get());
        command.add("@" + pathToSrcsList.get());
      } catch (IOException e) {
        context.logError(
            e,
            "Cannot write list of .java files to compile to %s file! Terminating compilation.",
            pathToSrcsList.get());
        return 1;
      }
    } else {
      for (Path source : expandedSources) {
        command.add(source.toString());
      }
    }

    ProcessBuilder processBuilder = new ProcessBuilder(command.build());

    // Set environment to client environment and add additional information.
    Map<String, String> env = processBuilder.environment();
    env.clear();
    env.putAll(context.getEnvironment());
    env.put("BUCK_INVOKING_RULE", invokingRule.toString());
    env.put("BUCK_TARGET", invokingRule.toString());
    env.put("BUCK_DIRECTORY_ROOT", context.getProjectDirectoryRoot().toString());

    processBuilder.directory(context.getProjectDirectoryRoot().toAbsolutePath().toFile());
    // Run the command
    int exitCode = -1;
    try {
      ProcessExecutor.Result result = context.getProcessExecutor().execute(processBuilder.start());
      exitCode = result.getExitCode();
    } catch (IOException e) {
      e.printStackTrace(context.getStdErr());
      return exitCode;
    }

    return exitCode;
  }

  private ImmutableList<Path> getExpandedSourcePaths(
      ExecutionContext context,
      BuildTarget invokingRule,
      ImmutableSet<Path> javaSourceFilePaths,
      Optional<Path> workingDirectory) throws IOException {
    ProjectFilesystem projectFilesystem = context.getProjectFilesystem();

    // Add sources file or sources list to command
    ImmutableList.Builder<Path> sources = ImmutableList.builder();
    for (Path path : javaSourceFilePaths) {
      String pathString = path.toString();
      if (pathString.endsWith(".java")) {
        sources.add(path);
      } else if (pathString.endsWith(SRC_ZIP) || pathString.endsWith(SRC_JAR)) {
        if (!workingDirectory.isPresent()) {
          throw new HumanReadableException(
              "Attempting to compile target %s which specified a .src.zip input %s but no " +
                  "working directory was specified.",
              invokingRule.toString(),
              path);
        }
        // For a Zip of .java files, create a JavaFileObject for each .java entry.
        ImmutableList<Path> zipPaths = Unzip.extractZipFile(
            projectFilesystem.resolve(path),
            projectFilesystem.resolve(workingDirectory.get()),
            Unzip.ExistingFileMode.OVERWRITE);
        sources.addAll(
            FluentIterable.from(zipPaths)
                .filter(
                    new Predicate<Path>() {
                      @Override
                      public boolean apply(Path input) {
                        return input.toString().endsWith(".java");
                      }
                    }));
      }
    }
    return sources.build();
  }
}
