/*
 * Copyright 2009 The Closure Compiler Authors.
 *
 * 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.javascript.jscomp;

import com.google.common.base.Preconditions;
import com.google.common.collect.Iterables;
import com.google.javascript.jscomp.NameReferenceGraph.Name;
import com.google.javascript.jscomp.NameReferenceGraph.Reference;
import com.google.javascript.jscomp.graph.DiGraph.DiGraphEdge;
import com.google.javascript.jscomp.graph.DiGraph.DiGraphNode;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.jstype.JSType;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

/**
 * Generate a nice HTML file describing the name reference graph.
 * For each declaration, list the sites where the declaration's name
 * is referenced, and list all the names that the declaration references.
 * For each, name exactly where use occurs in the source code.
 *
 * <p>This report should be useful both for internal compiler
 * developers and for engineers trying to understand running behavior
 * of their code or who want to understand why the compiler won't
 * move their code into a new module.
 *
 * @author bowdidge@google.com (Robert Bowdidge)
 */

final class NameReferenceGraphReport {
  private NameReferenceGraph graph = null;

  /**
   * Create a NameReferenceGraphReport object.
   *
   * @param g  name reference graph to describe in report.
   */
  NameReferenceGraphReport(NameReferenceGraph g) {
    this.graph = g;
  }

  /**
   * Generate a nice HTML file describing the name reference graph.
   * For each declaration, list the sites where the declaration's name
   * is referenced, and list all the names that the declaration references.
   * For each, name exactly where use occurs in the source code.
   *
   * <p>This report should be useful both for internal compiler
   * developers and for engineers trying to understand running
   * behavior of their code or who want to understand why
   * AbstractCompiler won't move their code into a new module.
   *
   * @return String containing the entire HTML for the report.
   */
  public String getHtmlReport() {
    StringBuilder builder = new StringBuilder();
    List<DiGraphNode<Name, Reference>> nodes = new ArrayList<>();
    Iterables.addAll(nodes, graph.getDirectedGraphNodes());

    generateHtmlReportHeader(builder);

    builder.append("<h1>Name Reference Graph Dump</h1>\n");
    builder.append("OVERALL STATS\n");
    builder.append("<ul>\n");
    builder.append("<li>Total names: " + nodes.size());
    builder.append("</ul>\n");

    builder.append("ALL NAMES\n");
    builder.append("<UL>\n");

    // Sort declarations in alphabetical order.
    Collections.sort(nodes, new DiGraphNodeComparator());

    for (DiGraphNode<Name, Reference> n : nodes) {
      // Generate the HTML describing the declaration itself.
      generateDeclarationReport(builder, n);

      // Next, list the places where this name is used (REFERS TO), and the
      // names that this declaration refers to (REFERENCED BY).
      List<DiGraphEdge<Name, Reference>> outEdges =
          graph.getOutEdges(n.getValue());
      List<DiGraphEdge<Name, Reference>> inEdges =
          graph.getInEdges(n.getValue());

      // Don't bother to create the dotted list if we don't have anything to
      // put in it.
      if (!outEdges.isEmpty() || !inEdges.isEmpty()) {
        builder.append("<ul>");

        if (!outEdges.isEmpty()) {
          builder.append("<li>REFERS TO:<br>\n");
          builder.append("<ul>");
          for (DiGraphEdge<Name, Reference> edge : outEdges) {
            generateEdgeReport(builder, edge.getDestination().getValue(),
                edge);
          }
          builder.append("</ul>\n");
        }

        if (!inEdges.isEmpty()) {
          builder.append("<li>REFERENCED BY:<br>\n");
          builder.append("<ul>");
          for (DiGraphEdge<Name, Reference> edge : inEdges) {
            generateEdgeReport(builder, edge.getSource().getValue(), edge);
          }
          builder.append("</ul>");
        }
        builder.append("</ul>\n");
      }
    }
    builder.append("</ul>\n");
    generateHtmlReportFooter(builder);
    return builder.toString();
  }

  /**
   * Given a node, find the name of the containing source file.
   *
   * @param node Parse tree node whose filename is requested
   * @return String containing name of source file, or empty string if name
   *     cannot be identified.
   */
  private static String getSourceFile(Node node) {
    String filename = node.getSourceFileName();
    if (filename == null) {
      return "";
    }
    return filename;
  }

  /**
   * Generate the HTML for describing a specific declaration.
   * @param builder  contents of report to be generated
   * @param declarationNode declaration to describe
   */
  private void generateDeclarationReport(StringBuilder builder,
      DiGraphNode<Name, Reference> declarationNode) {
    // Provide the name and location of declaration,
    // with an anchor to allow navigation to this declaration.
    String declName = declarationNode.getValue().getQualifiedName();
    JSType declType = declarationNode.getValue().getType();

    builder.append("<LI> ");
    builder.append("<A NAME=\"" + declName + "\">");
    builder.append(declName);
    builder.append("\n");

    // Provide the type of the declaration.
    // This is helpful for debugging.
    generateType(builder, declType);

    // List all the definitions of this name that were found in the code.
    // For each, list
    List<DefinitionsRemover.Definition> defs =
        declarationNode.getValue().getDeclarations();

    if (defs.isEmpty()) {
       builder.append("<br>No definitions found<br>");
    } else {
      // Otherwise, provide a list of definitions in a dotted list.
      // For each definition, print the location where that definition is
      // found.
      builder.append("<ul>");
      for (DefinitionsRemover.Definition def : defs) {
        Node fnDef = def.getRValue();
        String sourceFileName = getSourceFile(fnDef);
        builder.append("<li> Defined: ");
        generateSourceReferenceLink(builder,
            sourceFileName, fnDef.getLineno(), fnDef.getCharno());
      }
      builder.append("</ul>");
    }
  }

  /**
   *  Generate the HTML header for the report style.
   * Borrowed straight from NameAnalyzer's report style.
   *
   * @param builder contents of the report to be generated
   */
  private static void generateHtmlReportHeader(StringBuilder builder) {
    builder.append("<!DOCTYPE html>\n" +
        "<html>" +
        "<head>" +
        "<meta http-equiv=\"Content-Type\" " +
        "content=\"text/html;charset=utf-8\" >" +
        "<title>Name Reference Graph Dump</title>" +
        "<style type=\"text/css\">body, td, ");
    builder.append("p {font-family: Arial; font-size: 83%} ");
    builder.append("ul {margin-top:2px; margin-left:0px; padding-left:1em;}");
    builder.append("li {margin-top:3px; margin-left:24px;" +
        "padding-left:0px;padding-bottom: 4px}");
    builder.append("</style></head><body>\n");
  }

  /**
   * Generate the HTML footer for the report style.
   */
  private static void generateHtmlReportFooter(StringBuilder builder) {
    builder.append("</body></html>");
  }

  /**
   * Generate a description of a specific edge between two nodes.
   * For each edge, name the element being linked, the location of the
   * reference in the source file, and the type of the reference.
   *
   * @param builder contents of the report to be generated
   * @param referencedDecl name of the declaration being referenced
   * @param edge the graph edge being described
   */
  private void generateEdgeReport(StringBuilder builder,
      Name referencedDecl, DiGraphEdge<Name, Reference> edge) {
    String srcDeclName = referencedDecl.getQualifiedName();
    builder.append("<li><A HREF=\"#" + srcDeclName + "\">");
    builder.append(srcDeclName);
    builder.append("</a> ");

    Node def = edge.getValue().getSite();
    int lineNumber = def.getLineno();
    int columnNumber = def.getCharno();
    String sourceFile = getSourceFile(def);

    generateSourceReferenceLink(builder, sourceFile, lineNumber, columnNumber);

    JSType defType = edge.getValue().getSite().getJSType();
    generateType(builder, defType);
  }

  /**
   * Generate a link and text for a reference to a particular location
   * in a source file.  Selecting the link should take the programmer
   * to a browsable version of the file.
   *
   * @param builder  contents of the report to be generated
   * @param sourceFile  Path to the file
   * @param lineNumber  line where the object to view is located
   * @param columnNumber  column where the object to highlight is located.
   */
  private static void generateSourceReferenceLink(StringBuilder builder,
      String sourceFile, int lineNumber, int columnNumber) {
    assert(sourceFile != null);

    builder.append("(");


    // Print out the text path so the user knows where things come from.
    builder.append(sourceFile + ":" +
        lineNumber + "," + columnNumber);


    builder.append(")");
  }

  /**
   * Dump a type in a nice, readable way.
   *
   * @param builder contents of the report to be generated.
   * @param defType type to describe
   */
  private static void generateType(StringBuilder builder, JSType defType) {
    if (defType == null) {
      builder.append(" (type: null) ");
    } else if (defType.isUnknownType()) {
      builder.append(" (type: unknown) ");
    } else {
      builder.append(" (type: " + defType + ") ");
    }
  }

  /**
   * DiGraphNodeComparator gives us a way to generate sorted lists
   * of DiGraphNodes.  It provides a compare function used by the
   * String class's sort method.
   */
  class DiGraphNodeComparator implements
      Comparator<DiGraphNode<Name, Reference>> {
    @Override
    public int compare(DiGraphNode<Name, Reference> node1,
        DiGraphNode<Name, Reference> node2) {
      Preconditions.checkNotNull(node1.getValue());
      Preconditions.checkNotNull(node2.getValue());

      if ((node1.getValue().getQualifiedName() == null) &&
          (node2.getValue().getQualifiedName() == null)) {
        return 0;
      }

      // Node 1, if null, comes before node 2.
      if (node1.getValue().getQualifiedName() == null) {
        return -1;
      }

      // Node 2, if null, comes before node 1.
      if (node2.getValue().getQualifiedName() == null) {
        return 1;
      }

      return node1.getValue().getQualifiedName().compareTo(
          node2.getValue().getQualifiedName());
    }
  }

}
