// 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.common.collect.ImmutableList;
import com.google.devtools.build.buildjar.OptionsParser.ReduceClasspathMode;
import com.google.devtools.build.buildjar.javac.BlazeJavacResult;
import com.google.devtools.build.buildjar.javac.BlazeJavacResult.Status;
import com.google.devtools.build.buildjar.javac.JavacRunner;
import com.google.devtools.build.buildjar.javac.statistics.BlazeJavacStatistics;
import java.io.IOException;
import java.nio.file.Path;

/**
 * A variant of SimpleJavaLibraryBuilder that attempts to reduce the compile-time classpath right
 * before invoking the compiler, based on extra information from provided .jdeps files. This mode is
 * enabled via the --reduce_classpath flag, only when Blaze runs with --experimental_java_classpath.
 *
 * <p>A fall-back mechanism detects whether javac fails because the classpath is incorrectly
 * discarding required entries, and re-attempts to compile with the full classpath.
 */
public class ReducedClasspathJavaLibraryBuilder extends SimpleJavaLibraryBuilder {

  /**
   * Attempts to minimize the compile-time classpath before invoking javac, falling back to a
   * regular compile.
   *
   * @param build A JavaLibraryBuildRequest request object describing what to compile
   * @throws IOException clean-up up the output directory fails
   */
  @Override
  BlazeJavacResult compileSources(JavaLibraryBuildRequest build, JavacRunner javacRunner)
      throws IOException {
    // Minimize classpath, but only if we're actually compiling some sources (some invocations of
    // JavaBuilder are only building resource jars).
    ImmutableList<Path> compressedClasspath = build.getClassPath();
    if (!build.getSourceFiles().isEmpty()
        && build.reduceClasspathMode() == ReduceClasspathMode.JAVABUILDER_REDUCED) {
      compressedClasspath =
          build.getDependencyModule().computeStrictClasspath(build.getClassPath());
    }

    // Compile!
    BlazeJavacResult result =
        javacRunner.invokeJavac(build.toBlazeJavacArguments(compressedClasspath));

    // If javac errored out and there's any chance that the cause was missing classpath entries,
    // then give it another try with the full classpath.
    boolean fallback = shouldFallBack(result);
    if (fallback) {
      if (build.reduceClasspathMode() == ReduceClasspathMode.BAZEL_REDUCED) {
        return BlazeJavacResult.fallback();
      }
      if (build.reduceClasspathMode() == ReduceClasspathMode.JAVABUILDER_REDUCED) {
        result = fallback(build, javacRunner);
      }
    }

    BlazeJavacStatistics.Builder stats =
        result.statistics().toBuilder()
            .minClasspathLength(build.getDependencyModule().getImplicitDependenciesMap().size());
    build.getProcessors().stream()
        .map(p -> p.substring(p.lastIndexOf('.') + 1))
        .forEachOrdered(stats::addProcessor);

    switch (build.reduceClasspathMode()) {
      case BAZEL_REDUCED:
      case BAZEL_FALLBACK:
        stats.transitiveClasspathLength(build.fullClasspathLength());
        stats.reducedClasspathLength(build.reducedClasspathLength());
        stats.transitiveClasspathFallback(
            build.reduceClasspathMode() == ReduceClasspathMode.BAZEL_FALLBACK);
        break;
      case JAVABUILDER_REDUCED:
        stats.transitiveClasspathLength(build.getClassPath().size());
        stats.reducedClasspathLength(compressedClasspath.size());
        stats.transitiveClasspathFallback(fallback);
        break;
      default:
        throw new AssertionError(build.reduceClasspathMode());
    }
    return result.withStatistics(stats.build());
  }

  private BlazeJavacResult fallback(JavaLibraryBuildRequest build, JavacRunner javacRunner)
      throws IOException {
    // TODO(cushon): warn for transitive classpath fallback

    // Reset output directories
    prepareSourceCompilation(build);

    // Fall back to the regular compile, but add extra checks to catch transitive uses
    return javacRunner.invokeJavac(build.toBlazeJavacArguments(build.getClassPath()));
  }

  private static boolean shouldFallBack(BlazeJavacResult result) {
    if (result.isOk()) {
      return false;
    }
    if (result.status().equals(Status.CRASH)) {
      return true;
    }
    if (result.diagnostics().stream().allMatch(d -> d.isJSpecifyDiagnostic())) {
      return false;
    }
    return true;
  }
}
