/*
 * Copyright 2015 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.j2cl.frontend;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.j2cl.common.Problems;
import com.google.j2cl.common.Problems.FatalError;
import com.google.j2cl.frontend.FrontendUtils.FileInfo;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.eclipse.jdt.core.BindingKey;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.compiler.IProblem;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTParser;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.FileASTRequestor;
import org.eclipse.jdt.core.dom.IBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;

/**
 * A delegator of JDT's ASTParser that provides a more convenient interface for
 * parsing source files into compilation unit.
 */
public class JdtParser {
  private final Problems problems;
  private final Map<String, String> compilerOptions = new HashMap<>();
  private final List<String> classpathEntries;

  /** Create and initialize a JdtParser based on passed parameters. */
  public JdtParser(List<String> classpathEntries, Problems problems) {
    compilerOptions.put(JavaCore.COMPILER_SOURCE, JavaCore.VERSION_9);
    compilerOptions.put(JavaCore.COMPILER_CODEGEN_TARGET_PLATFORM, JavaCore.VERSION_9);
    compilerOptions.put(JavaCore.COMPILER_COMPLIANCE, JavaCore.VERSION_9);

    this.classpathEntries = ImmutableList.copyOf(classpathEntries);
    this.problems = problems;
  }

  private static final List<String> wellKnownClassNames =
      ImmutableList.of(
          "java.io.Serializable",
          "java.lang.CharSequence",
          "java.lang.Class",
          "java.lang.Cloneable",
          "java.lang.Comparable",
          "java.lang.Enum",
          "java.lang.Number",
          "java.lang.Object",
          "java.lang.Runnable",
          "java.lang.String",
          "java.lang.Throwable");

  /** Returns a map from file paths to compilation units after JDT parsing. */
  public CompilationUnitsAndTypeBindings parseFiles(
      List<FileInfo> filePaths, boolean useTargetPath) {

    // Parse and create a compilation unit for every file.
    ASTParser parser = newASTParser(true);

    // The map must be ordered because it will be iterated over later and if it was not ordered then
    // our output would be unstable
    final Map<String, CompilationUnit> compilationUnitsByFilePath = new LinkedHashMap<>();
    final List<ITypeBinding> wellKnownTypeBindings = new ArrayList<>();
    final Map<String, String> targetPathBySourcePath =
        filePaths.stream().collect(Collectors.toMap(FileInfo::sourcePath, FileInfo::targetPath));

    FileASTRequestor astRequestor =
        new FileASTRequestor() {
          @Override
          public void acceptAST(String filePath, CompilationUnit compilationUnit) {
            if (compilationHasErrors(filePath, compilationUnit)) {
              return;
            }
            String filePathKey = filePath;
            if (useTargetPath) {
              filePathKey = targetPathBySourcePath.get(filePath);
            }
            compilationUnitsByFilePath.put(filePathKey, compilationUnit);
          }

          @Override
          public void acceptBinding(String bindingKey, IBinding binding) {
            wellKnownTypeBindings.add((ITypeBinding) binding);
          }
        };
    parser.createASTs(
        filePaths.stream().map(f -> f.sourcePath()).toArray(String[]::new),
        getEncodings(filePaths.size()),
        wellKnownClassNames.stream().map(BindingKey::createTypeBindingKey).toArray(String[]::new),
        astRequestor,
        null);
    return new CompilationUnitsAndTypeBindings(compilationUnitsByFilePath, wellKnownTypeBindings);
  }

  private ASTParser newASTParser(boolean resolveBinding) {
    ASTParser parser = ASTParser.newParser(AST.JLS9);

    parser.setCompilerOptions(compilerOptions);
    parser.setResolveBindings(resolveBinding);
    parser.setEnvironment(
        Iterables.toArray(classpathEntries, String.class), new String[0], new String[0], false);
    return parser;
  }

  private String[] getEncodings(int length) {
    String[] encodings = new String[length];
    Arrays.fill(encodings, "UTF_8");
    return encodings;
  }

  private boolean compilationHasErrors(String filename, CompilationUnit unit) {
    boolean hasErrors = false;
    // Here we check for instances of @GwtIncompatible in the ast. If that is the case, we throw an
    // error since these should have been stripped by the build system already.
    GwtIncompatibleNodeCollector collector = new GwtIncompatibleNodeCollector();
    unit.accept(collector);
    if (!collector.getNodes().isEmpty()) {
      problems.fatal(FatalError.GWT_INCOMPATIBLE_FOUND_IN_COMPILE, filename);
    }
    for (IProblem problem : unit.getProblems()) {
      if (problem.isError()) {
        problems.error(problem.getSourceLineNumber(), filename, problem.getMessage());
        hasErrors = true;
      }
    }
    return hasErrors;
  }

  private ITypeBinding findTypeBindingByName(ITypeBinding typeBinding, String soughtTypeName) {
    if (typeBinding == null) {
      return null;
    }
    if (soughtTypeName.equals(typeBinding.getQualifiedName())) {
      return typeBinding;
    }
    ITypeBinding superTypeBinding =
        findTypeBindingByName(typeBinding.getSuperclass(), soughtTypeName);

    if (superTypeBinding != null) {
      return superTypeBinding;
    }

    for (ITypeBinding superInterfaceBinding : typeBinding.getInterfaces()) {
      superTypeBinding = findTypeBindingByName(superInterfaceBinding, soughtTypeName);
      if (superTypeBinding != null) {
        return superTypeBinding;
      }
    }

    return null;
  }
}

@@ -1958,33 +1958,34 @@
 avaCore.VERSION_
-9
+10
 );%0A    compilerO
@@ -2055,17 +2055,18 @@
 VERSION_
-9
+10
 );%0A    c
@@ -2131,17 +2131,18 @@
 VERSION_
-9
+10
 );%0A%0A    

Switch J2CL to compile code using Java 10 mode.