/*
 * Copyright (C) 2016 Google, 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.google.testing.compile;

import static com.google.common.base.Functions.toStringFunction;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static java.nio.charset.StandardCharsets.UTF_8;
import static javax.tools.ToolProvider.getSystemJavaCompiler;

import com.google.auto.value.AutoValue;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Splitter;
import com.google.common.base.StandardSystemProperty;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.testing.compile.Compilation.Status;
import java.io.File;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.LinkedHashSet;
import java.util.Locale;
import java.util.Optional;
import java.util.Set;
import javax.annotation.processing.Processor;
import javax.tools.DiagnosticCollector;
import javax.tools.JavaCompiler;
import javax.tools.JavaCompiler.CompilationTask;
import javax.tools.JavaFileObject;
import javax.tools.StandardLocation;

/** An object that can {@link #compile} Java source files. */
@AutoValue
// clashes with java.lang.Compiler (which is deprecated for removal in 9)
@SuppressWarnings("JavaLangClash")
public abstract class Compiler {

  /** Returns the {@code javac} compiler. */
  public static Compiler javac() {
    return compiler(getSystemJavaCompiler());
  }

  /** Returns a {@link Compiler} that uses a given {@link JavaCompiler} instance. */
  public static Compiler compiler(JavaCompiler javaCompiler) {
    return new AutoValue_Compiler(
        javaCompiler, ImmutableList.of(), ImmutableList.of(), Optional.empty());
  }

  abstract JavaCompiler javaCompiler();

  /** The annotation processors applied during compilation. */
  public abstract ImmutableList<Processor> processors();

  /** The options passed to the compiler. */
  public abstract ImmutableList<String> options();

  /** The compilation class path. If not present, the system class path is used. */
  public abstract Optional<ImmutableList<File>> classPath();

  /**
   * Uses annotation processors during compilation. These replace any previously specified.
   *
   * <p>Note that most annotation processors cannot be reused for more than one compilation.
   *
   * @return a new instance with the same options and the given processors
   */
  public final Compiler withProcessors(Processor... processors) {
    return withProcessors(ImmutableList.copyOf(processors));
  }

  /**
   * Uses annotation processors during compilation. These replace any previously specified.
   *
   * <p>Note that most annotation processors cannot be reused for more than one compilation.
   *
   * @return a new instance with the same options and the given processors
   */
  public final Compiler withProcessors(Iterable<? extends Processor> processors) {
    return copy(ImmutableList.copyOf(processors), options(), classPath());
  }

  /**
   * Passes command-line options to the compiler. These replace any previously specified.
   *
   * @return a new instance with the same processors and the given options
   */
  public final Compiler withOptions(Object... options) {
    return withOptions(ImmutableList.copyOf(options));
  }

  /**
   * Passes command-line options to the compiler. These replace any previously specified.
   *
   * @return a new instance with the same processors and the given options
   */
  public final Compiler withOptions(Iterable<?> options) {
    return copy(
        processors(),
        FluentIterable.from(options).transform(toStringFunction()).toList(),
        classPath());
  }

  /**
   * Uses the classpath from the passed on classloader (and its parents) for the compilation instead
   * of the system classpath.
   *
   * @throws IllegalArgumentException if the given classloader had classpaths which we could not
   *     determine or use for compilation.
   * @deprecated prefer {@link #withClasspath(Iterable)}. This method only supports {@link
   *     URLClassLoader} and the default system classloader, and {@link File}s are usually a more
   *     natural way to expression compilation classpaths than class loaders.
   */
  @Deprecated
  public final Compiler withClasspathFrom(ClassLoader classloader) {
    return copy(processors(), options(), Optional.of(getClasspathFromClassloader(classloader)));
  }

  /** Uses the given classpath for the compilation instead of the system classpath. */
  public final Compiler withClasspath(Iterable<File> classPath) {
    return copy(processors(), options(), Optional.of(ImmutableList.copyOf(classPath)));
  }

  /**
   * Compiles Java source files.
   *
   * @return the results of the compilation
   */
  public final Compilation compile(JavaFileObject... files) {
    return compile(ImmutableList.copyOf(files));
  }

  /**
   * Compiles Java source files.
   *
   * @return the results of the compilation
   */
  public final Compilation compile(Iterable<? extends JavaFileObject> files) {
    DiagnosticCollector<JavaFileObject> diagnosticCollector = new DiagnosticCollector<>();
    InMemoryJavaFileManager fileManager =
        new InMemoryJavaFileManager(
            javaCompiler().getStandardFileManager(diagnosticCollector, Locale.getDefault(), UTF_8));
    classPath()
        .ifPresent(
            classPath -> {
              try {
                fileManager.setLocation(StandardLocation.CLASS_PATH, classPath);
              } catch (IOException e) {
                // impossible by specification
                throw new UncheckedIOException(e);
              }
            });
    CompilationTask task =
        javaCompiler()
            .getTask(
                null, // use the default because old versions of javac log some output on stderr
                fileManager,
                diagnosticCollector,
                options(),
                ImmutableSet.<String>of(),
                files);
    task.setProcessors(processors());
    boolean succeeded = task.call();
    Compilation compilation =
        new Compilation(
            this,
            files,
            succeeded,
            diagnosticCollector.getDiagnostics(),
            fileManager.getOutputFiles());
    if (compilation.status().equals(Status.FAILURE) && compilation.errors().isEmpty()) {
      throw new CompilationFailureException(compilation);
    }
    return compilation;
  }

  @VisibleForTesting static final ClassLoader platformClassLoader = getPlatformClassLoader();

  private static ClassLoader getPlatformClassLoader() {
    try {
      // JDK >= 9
      return (ClassLoader) ClassLoader.class.getMethod("getPlatformClassLoader").invoke(null);
    } catch (ReflectiveOperationException e) {
      // Java <= 8
      return null;
    }
  }

  /**
   * Returns the current classpaths of the given classloader including its parents.
   *
   * @throws IllegalArgumentException if the given classloader had classpaths which we could not
   *     determine or use for compilation.
   */
  private static ImmutableList<File> getClasspathFromClassloader(ClassLoader currentClassloader) {
    ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();

    // Concatenate search paths from all classloaders in the hierarchy 'till the system classloader.
    Set<String> classpaths = new LinkedHashSet<>();
    while (true) {
      if (currentClassloader == systemClassLoader) {
        Iterables.addAll(
            classpaths,
            Splitter.on(StandardSystemProperty.PATH_SEPARATOR.value())
                .split(StandardSystemProperty.JAVA_CLASS_PATH.value()));
        break;
      }
      if (currentClassloader == platformClassLoader) {
        break;
      }
      if (currentClassloader instanceof URLClassLoader) {
        // We only know how to extract classpaths from URLClassloaders.
        for (URL url : ((URLClassLoader) currentClassloader).getURLs()) {
          if (url.getProtocol().equals("file")) {
            classpaths.add(url.getPath());
          } else {
            throw new IllegalArgumentException(
                "Given classloader consists of classpaths which are "
                    + "unsupported for compilation.");
          }
        }
      } else {
        throw new IllegalArgumentException(
            String.format(
                "Classpath for compilation could not be extracted "
                    + "since %s is not an instance of URLClassloader",
                currentClassloader));
      }
      currentClassloader = currentClassloader.getParent();
    }

    return classpaths.stream().map(File::new).collect(toImmutableList());
  }

  private Compiler copy(
      ImmutableList<Processor> processors,
      ImmutableList<String> options,
      Optional<ImmutableList<File>> classPath) {
    return new AutoValue_Compiler(javaCompiler(), processors, options, classPath);
  }
}