/*
 * Copyright 2011 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.gwt.dev.javac;

import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.TreeLogger.HelpInfo;
import com.google.gwt.core.ext.TreeLogger.Type;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.dev.CompilerContext;
import com.google.gwt.dev.javac.CompilationUnitBuilder.GeneratedCompilationUnit;
import com.google.gwt.dev.jjs.InternalCompilerException;
import com.google.gwt.dev.jjs.InternalCompilerException.NodeInfo;
import com.google.gwt.dev.jjs.SourceInfo;
import com.google.gwt.dev.util.Messages;
import com.google.gwt.thirdparty.guava.common.collect.Lists;

import org.eclipse.jdt.core.compiler.CategorizedProblem;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Files;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.Set;

/**
 * Handles some details of reporting errors in {@link CompilationUnit}s to the
 * console.
 */
public class CompilationProblemReporter {

  /**
   * Traverses a set of compilation units to record enough information to enable accurate and
   * detailed compilation error cause traces.
   */
  public static void indexErrors(CompilationErrorsIndex compilationErrorsIndex,
      List<CompilationUnit> units) {
    for (CompilationUnit unit : units) {
      if (unit.isError()) {
        Dependencies dependencies = unit.getDependencies();
        compilationErrorsIndex.add(unit.getTypeName(), unit.getResourceLocation(),
            dependencies.getApiRefs(), CompilationProblemReporter.formatErrors(unit));
      }
    }
  }

  /**
   * Used as a convenience to catch all exceptions thrown by the compiler. For
   * instances of {@link InternalCompilerException}, extra diagnostics are
   * printed.
   *
   * @param logger logger used to report errors to the console
   * @param e the exception to analyze and log
   * @return Always returns an instance of {@link UnableToCompleteException} so
   *         that the calling method can declare a more narrow 'throws
   *         UnableToCompleteException'
   */
  public static UnableToCompleteException logAndTranslateException(TreeLogger logger, Throwable e) {
    if (e instanceof UnableToCompleteException) {
      // just rethrow
      return (UnableToCompleteException) e;
    } else if (e instanceof InternalCompilerException) {
      TreeLogger topBranch =
          logger.branch(TreeLogger.ERROR, "An internal compiler exception occurred", e);
      List<NodeInfo> nodeTrace = ((InternalCompilerException) e).getNodeTrace();
      for (NodeInfo nodeInfo : nodeTrace) {
        SourceInfo info = nodeInfo.getSourceInfo();
        String msg;
        if (info != null) {
          String fileName = info.getFileName();
          fileName = fileName.substring(fileName.lastIndexOf('/') + 1);
          fileName = fileName.substring(fileName.lastIndexOf('\\') + 1);
          msg = "at " + fileName + "(" + info.getStartLine() + "): ";
        } else {
          msg = "<no source info>: ";
        }

        String description = nodeInfo.getDescription();
        if (description != null) {
          msg += description;
        } else {
          msg += "<no description available>";
        }
        TreeLogger nodeBranch = topBranch.branch(TreeLogger.ERROR, msg, null);
        String className = nodeInfo.getClassName();
        if (className != null) {
          nodeBranch.log(TreeLogger.INFO, className, null);
        }
      }
      return new UnableToCompleteException();
    } else if (e instanceof VirtualMachineError) {
      // Always rethrow VM errors (an attempt to wrap may fail).
      throw (VirtualMachineError) e;
    } else {
      logger.log(TreeLogger.ERROR, "Unexpected internal compiler error", e);
      return new UnableToCompleteException();
    }
  }

  /**
   * Provides meaningful error messages and hints for types that failed to compile or are otherwise
   * missing.
   */
  public static int logErrorTrace(TreeLogger logger, Type logLevel,
      CompilerContext compilerContext, List<CompilationUnit> units, boolean hint) {
    int errorCount = 0;
    for (CompilationUnit unit : units) {
      if (unit.isError()) {
        logErrorTrace(logger, logLevel, compilerContext, unit.getTypeName(), hint);
        errorCount++;

        if (logger.isLoggable(TreeLogger.INFO) && unit instanceof GeneratedCompilationUnit) {
          CompilationProblemReporter.maybeDumpSource(logger,
              ((GeneratedCompilationUnit) unit).getSource(), unit.getTypeName());
        }
      }
    }
    return errorCount++;
  }

  /**
   * Provides a meaning error message and hints for a type that failed to compile or is otherwise
   * missing.
   */
  public static void logErrorTrace(TreeLogger logger, Type logLevel,
      CompilerContext compilerContext, String typeSourceName, boolean hint) {
    TreeLogger branch = logger.branch(TreeLogger.TRACE,
        "Tracing compile failure path for type '" + typeSourceName + "'");
    if (logErrorChain(branch, logLevel, typeSourceName,
        compilerContext.getCompilationErrorsIndex())) {
      return;
    }

    if (hint) {
      logHints(logger, typeSourceName);
    }
  }

  public static int logWarnings(TreeLogger logger, Type logLevelForWarnings,
      List<CompilationUnit> units) {
    int warningCount = 0;
    for (CompilationUnit unit : units) {
      if (CompilationProblemReporter.logWarnings(logger, logLevelForWarnings, unit)) {
        warningCount++;
      }
    }
    return warningCount++;
  }

  /**
   * Logs errors to the console.
   *
   * @param logger logger for reporting errors to the console
   * @param unit Compilation unit that may have errors
   * @param suppressErrors Controls he log level for logging errors. If <code>false</code> is passed,
   *          compilation errors are logged at TreeLogger.ERROR and warnings logged at
   *          TreeLogger.WARN. If <code>true</code> is passed, compilation errors are logged at
   *          TreeLogger.TRACE and TreeLogger.DEBUG.
   * @return <code>true</code> if an error was logged.
   */
  public static boolean reportErrors(TreeLogger logger, CompilationUnit unit,
      boolean suppressErrors) {
    CategorizedProblem[] problems = unit.getProblems();
    if (problems == null || problems.length == 0) {
      return false;
    }
    String fileName = unit.getResourceLocation();
    boolean isError = unit.isError();
    TreeLogger.Type warnLogLevel;
    TreeLogger.Type errorLogLevel;
    if (suppressErrors) {
      errorLogLevel = TreeLogger.TRACE;
      warnLogLevel = TreeLogger.DEBUG;
    } else {
      errorLogLevel = TreeLogger.ERROR;
      warnLogLevel = TreeLogger.WARN;
    }

    TreeLogger branch = null;
    // Log the errors and GWT warnings.
    for (CategorizedProblem problem : problems) {
      TreeLogger.Type logLevel;
      if (problem.isError()) {
        // Log errors.
        logLevel = errorLogLevel;
        // Only log GWT-specific warnings.
      } else if (problem.isWarning() && problem instanceof GWTProblem) {
        logLevel = warnLogLevel;
      } else {
        // Ignore all other problems.
        continue;
      }

      HelpInfo helpInfo = null;
      if (problem instanceof GWTProblem) {
        GWTProblem gwtProblem = (GWTProblem) problem;
        helpInfo = gwtProblem.getHelpInfo();
      }
      if (branch == null) {
        Type branchType = isError ? errorLogLevel : warnLogLevel;
        String branchString = isError ? "Errors" : "Warnings";
        branch = logger.branch(branchType, branchString + " in '" + fileName + "'", null);
      }
      branch.log(logLevel, toMessageWithLineNumber(problem), null, helpInfo);
    }

    if (branch != null && branch.isLoggable(TreeLogger.INFO)) {
      if (unit instanceof GeneratedCompilationUnit) {
        GeneratedCompilationUnit generatedUnit = (GeneratedCompilationUnit) unit;
        CompilationProblemReporter.maybeDumpSource(branch, generatedUnit.getSource(), unit
            .getTypeName());
      }
    }
    return branch != null;
  }

  private static void addUnitToVisit(CompilationErrorsIndex compilationErrorsIndex,
      String typeSourceName, Queue<String> toVisit, Set<String> visited) {
    if (compilationErrorsIndex.hasCompileErrors(typeSourceName)) {
      if (!visited.contains(typeSourceName)) {
        toVisit.add(typeSourceName);
        visited.add(typeSourceName);
      }
    }
  }

  /**
   * Returns readable compilation error messages for a compilation unit.
   * <p>
   * Should only be run on CompilationUnits that actually have problems.
   */
  private static List<String> formatErrors(CompilationUnit unit) {
    CategorizedProblem[] problems = unit.getProblems();
    assert problems != null && problems.length > 0;

    List<String> errorMessages = Lists.newArrayList();
    for (CategorizedProblem problem : problems) {
      if (!problem.isError()) {
        continue;
      }

      errorMessages.add(toMessageWithLineNumber(problem));
    }

    return errorMessages;
  }

  private static boolean hasWarnings(CompilationUnit unit) {
    CategorizedProblem[] problems = unit.getProblems();
    if (problems == null || problems.length == 0) {
      return false;
    }
    for (CategorizedProblem problem : problems) {
      if (problem.isWarning() && problem instanceof GWTProblem) {
        return true;
      }
    }
    return false;
  }

  private static boolean logErrorChain(TreeLogger logger, Type logLevel,
      String typeSourceName, CompilationErrorsIndex compilationErrorsIndex) {
    final Set<String> visited = new HashSet<String>();
    final Queue<String> toVisit = new LinkedList<String>();

    /*
     * Traverses CompilationUnits enqueued in toVisit(), calling {@link
     * #addUnitsToVisit(String)} as it encounters dependencies on the node. Each
     * CompilationUnit is visited only once, and only if it is reachable via the
     * {@link Dependencies} graph.
     */
    addUnitToVisit(compilationErrorsIndex, typeSourceName, toVisit, visited);

    while (!toVisit.isEmpty()) {
      String dependentTypeSourceName = toVisit.remove();

      Set<String> compileErrors = compilationErrorsIndex.getCompileErrors(dependentTypeSourceName);
      TreeLogger branch = logger.branch(logLevel,
          "Errors in '" + compilationErrorsIndex.getFileName(dependentTypeSourceName) + "'");
      for (String compileError : compileErrors) {
        branch.log(logLevel, compileError);
      }

      Set<String> typeReferences =
          compilationErrorsIndex.getTypeReferences(dependentTypeSourceName);
      if (typeReferences != null) {
        for (String typeReference : typeReferences) {
          addUnitToVisit(compilationErrorsIndex, typeReference, toVisit, visited);
        }
      }
    }
    logger.log(TreeLogger.DEBUG, "Checked " + visited.size() + " dependencies for errors.");
    return visited.size() > 1;
  }

  private static void logHints(TreeLogger logger, String typeSourceName) {
    URL sourceURL = findSourceInClassPath(typeSourceName);
    if (sourceURL != null) {
      if (typeSourceName.indexOf(".client.") != -1) {
        Messages.HINT_CHECK_MODULE_INHERITANCE.log(logger, null);
      } else {
        Messages.HINT_CHECK_MODULE_NONCLIENT_SOURCE_DECL.log(logger, null);
      }
    } else if (!typeSourceName.equals("java.lang.Object")) {
      Messages.HINT_CHECK_TYPENAME.log(logger, typeSourceName, null);
      Messages.HINT_CHECK_CLASSPATH_SOURCE_ENTRIES.log(logger, null);
    }

    /*
     * For missing JRE emulated classes (e.g. Object), or the main GWT libraries, there are special
     * warnings.
     */
    if (typeSourceName.indexOf("java.lang.") == 0
        || typeSourceName.indexOf("com.google.gwt.core.") == 0) {
      Messages.HINT_CHECK_INHERIT_CORE.log(logger, null);
    } else if (typeSourceName.indexOf("com.google.gwt.user.") == 0) {
      Messages.HINT_CHECK_INHERIT_USER.log(logger, null);
    }
  }

  private static URL findSourceInClassPath(String sourceTypeName) {
    String toTry = sourceTypeName.replace('.', '/') + ".java";
    URL foundURL = Thread.currentThread().getContextClassLoader().getResource(toTry);
    if (foundURL != null) {
      return foundURL;
    }
    int i = sourceTypeName.lastIndexOf('.');
    if (i != -1) {
      return findSourceInClassPath(sourceTypeName.substring(0, i));
    } else {
      return null;
    }
  }

  private static boolean logWarnings(TreeLogger logger, TreeLogger.Type logLevel,
      CompilationUnit unit) {
    if (!hasWarnings(unit)) {
      return false;
    }

    TreeLogger branch =
        logger.branch(logLevel, "Warnings in '" + unit.getResourceLocation() + "'", null);
    for (CategorizedProblem problem : unit.getProblems()) {
      if (!problem.isWarning() || !(problem instanceof GWTProblem)) {
        continue;
      }

      branch.log(logLevel, toMessageWithLineNumber(problem), null,
          ((GWTProblem) problem).getHelpInfo());
    }

    if (branch.isLoggable(TreeLogger.INFO) && unit instanceof GeneratedCompilationUnit) {
      CompilationProblemReporter.maybeDumpSource(branch,
          ((GeneratedCompilationUnit) unit).getSource(), unit.getTypeName());
    }
    return true;
  }

  /**
   * Give the developer a chance to see the in-memory source that failed.
   */
  private static void maybeDumpSource(TreeLogger logger, String source, String typeName) {
    File tmpSrc;
    Throwable caught = null;
    try {
      // The tempFile prefix must be at least 3 characters
      while (typeName.length() < 3) {
        typeName = "_" + typeName;
      }
      tmpSrc = File.createTempFile(typeName, ".java");
      Files.writeString(tmpSrc.toPath(), source);
      String dumpPath = tmpSrc.getAbsolutePath();
      if (logger.isLoggable(TreeLogger.INFO)) {
        logger.log(TreeLogger.INFO, "See snapshot: " + dumpPath, null);
      }
      return;
    } catch (IOException e) {
      caught = e;
    }
    logger.log(TreeLogger.INFO, "Unable to dump source to disk", caught);
  }

  private static String toMessageWithLineNumber(CategorizedProblem problem) {
    int lineNumber = problem.getSourceLineNumber();
    return (lineNumber > 0 ? "Line " + lineNumber + ": " : "") + problem.getMessage();
  }
}
