// 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.buildjar;

import com.google.devtools.build.buildjar.instrumentation.JacocoInstrumentationProcessor;
import com.google.devtools.build.buildjar.jarhelper.JarCreator;
import com.google.devtools.build.buildjar.javac.BlazeJavacArguments;
import com.google.devtools.build.buildjar.javac.BlazeJavacMain;
import com.google.devtools.build.buildjar.javac.BlazeJavacResult;
import com.google.devtools.build.buildjar.javac.JavacRunner;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.HashMap;
import java.util.Map;
import javax.annotation.Nullable;

/** An implementation of the JavaBuilder that uses in-process javac to compile java files. */
public class SimpleJavaLibraryBuilder implements Closeable {

  /** The name of the protobuf meta file. */
  private static final String PROTOBUF_META_NAME = "protobuf.meta";

  /** Cache of opened zip filesystems for srcjars. */
  private final Map<Path, FileSystem> filesystems = new HashMap<>();

  BlazeJavacResult compileSources(JavaLibraryBuildRequest build, JavacRunner javacRunner)
      throws IOException {
    return javacRunner.invokeJavac(build.toBlazeJavacArguments(build.getClassPath()));
  }

  protected void prepareSourceCompilation(JavaLibraryBuildRequest build) throws IOException {
    cleanupDirectory(build.getClassDir());

    setUpSourceJars(build);
    cleanupDirectory(build.getSourceGenDir());
    cleanupDirectory(build.getNativeHeaderDir());
  }

  // Necessary for local builds in order to discard previous outputs
  private static void cleanupDirectory(@Nullable Path directory) throws IOException {
    if (directory == null) {
      return;
    }
    if (!Files.exists(directory)) {
      Files.createDirectories(directory);
      return;
    }
    try {
      // TODO(b/27069912): handle symlinks
      Files.walkFileTree(
          directory,
          new SimpleFileVisitor<Path>() {
            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
                throws IOException {
              Files.delete(file);
              return FileVisitResult.CONTINUE;
            }

            @Override
            public FileVisitResult postVisitDirectory(Path dir, IOException exc)
                throws IOException {
              if (!dir.equals(directory)) {
                Files.delete(dir);
              }
              return FileVisitResult.CONTINUE;
            }
          });
    } catch (IOException e) {
      throw new IOException("Cannot clean '" + directory + "'", e);
    }
  }

  public void buildGensrcJar(JavaLibraryBuildRequest build) throws IOException {
    JarCreator jar = new JarCreator(build.getGeneratedSourcesOutputJar());
    try {
      jar.setNormalize(true);
      jar.setCompression(build.compressJar());
      jar.addDirectory(build.getSourceGenDir());
    } finally {
      jar.execute();
    }
  }

  /**
   * Prepares a compilation run and sets everything up so that the source files in the build request
   * can be compiled. Invokes compileSources to do the actual compilation.
   *
   * @param build A JavaLibraryBuildRequest request object describing what to compile
   */
  public BlazeJavacResult compileJavaLibrary(final JavaLibraryBuildRequest build) throws Exception {
    prepareSourceCompilation(build);
    if (build.getSourceFiles().isEmpty()) {
      return BlazeJavacResult.ok();
    }
    JavacRunner javacRunner =
        new JavacRunner() {
          @Override
          public BlazeJavacResult invokeJavac(BlazeJavacArguments arguments) {
            return BlazeJavacMain.compile(arguments);
          }
        };
    BlazeJavacResult result = compileSources(build, javacRunner);
    return result;
  }

  /** Perform the build. */
  public BlazeJavacResult run(JavaLibraryBuildRequest build) throws Exception {
    BlazeJavacResult result = BlazeJavacResult.error("");
    try {
      result = compileJavaLibrary(build);
      if (result.isOk()) {
        buildJar(build);
        nativeHeaderOutput(build);
      }
      if (!build.getProcessors().isEmpty()) {
        if (build.getGeneratedSourcesOutputJar() != null) {
          buildGensrcJar(build);
        }
      }
    } finally {
      build.getDependencyModule().emitDependencyInformation(build.getClassPath(), result.isOk());
      build.getProcessingModule().emitManifestProto();
    }
    return result;
  }

  public void buildJar(JavaLibraryBuildRequest build) throws IOException {
    JarCreator jar = new JarCreator(build.getOutputJar());
    try {
      jar.setNormalize(true);
      jar.setCompression(build.compressJar());
      jar.addDirectory(build.getClassDir());
      JacocoInstrumentationProcessor processor = build.getJacocoInstrumentationProcessor();
      if (processor != null) {
        processor.processRequest(build, processor.isNewCoverageImplementation() ? jar : null);
      }
    } finally {
      jar.execute();
    }
  }

  public void nativeHeaderOutput(JavaLibraryBuildRequest build) throws IOException {
    if (build.getNativeHeaderOutput() == null) {
      return;
    }
    JarCreator jar = new JarCreator(build.getNativeHeaderOutput());
    try {
      jar.setNormalize(true);
      jar.setCompression(build.compressJar());
      jar.addDirectory(build.getNativeHeaderDir());
    } finally {
      jar.execute();
    }
  }

  /**
   * Extracts the all source jars from the build request into the temporary directory specified in
   * the build request. Empties the temporary directory, if it exists.
   */
  private void setUpSourceJars(JavaLibraryBuildRequest build) throws IOException {
    Path sourcesDir = build.getTempDir();

    cleanupDirectory(sourcesDir);

    if (build.getSourceJars().isEmpty()) {
      return;
    }

    final ByteArrayOutputStream protobufMetadataBuffer = new ByteArrayOutputStream();
    for (Path sourceJar : build.getSourceJars()) {
      for (Path root : getJarFileSystem(sourceJar).getRootDirectories()) {
        Files.walkFileTree(
            root,
            new SimpleFileVisitor<Path>() {
              @Override
              public FileVisitResult visitFile(Path path, BasicFileAttributes attrs)
                  throws IOException {
                String fileName = path.getFileName().toString();
                if (fileName.endsWith(".java")) {
                  build.getSourceFiles().add(path);
                } else if (fileName.equals(PROTOBUF_META_NAME)) {
                  Files.copy(path, protobufMetadataBuffer);
                }
                return FileVisitResult.CONTINUE;
              }
            });
      }
    }
    Path output = build.getClassDir().resolve(PROTOBUF_META_NAME);
    if (protobufMetadataBuffer.size() > 0) {
      try (OutputStream outputStream = Files.newOutputStream(output)) {
        protobufMetadataBuffer.writeTo(outputStream);
      }
    } else if (Files.exists(output)) {
      // Delete stalled meta file.
      Files.delete(output);
    }
  }

  private FileSystem getJarFileSystem(Path sourceJar) throws IOException {
    FileSystem fs = filesystems.get(sourceJar);
    if (fs == null) {
      filesystems.put(sourceJar, fs = FileSystems.newFileSystem(sourceJar, null));
    }
    return fs;
  }

  @Override
  public void close() throws IOException {
    for (FileSystem fs : filesystems.values()) {
      fs.close();
    }
  }
}
