/*
 * Copyright 2008 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.collect.ImmutableSet;
import com.google.javascript.jscomp.CodingConvention.AssertionFunctionSpec;
import com.google.javascript.jscomp.NodeTraversal.AbstractPostOrderCallback;
import com.google.javascript.jscomp.NodeTraversal.Callback;
import com.google.javascript.rhino.Node;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;

/**
 * <p>Compiler pass that removes Closure-specific code patterns.</p>
 *
 * <p>Currently does the following:</p>
 *
 * <ul>
 *   <li> Instead of setting abstract methods to a function that throws an
 *        informative error, this pass allows some binary size reduction by
 *        removing these methods altogether for production builds.</li>
 *   <li> Remove calls to assertion functions (like goog.asserts.assert).
 *        If the return value of the assertion function is used, then
 *        the first argument (the asserted value) will be directly inlined.
 *        Otherwise, the entire call will be removed. It is well-known that
 *        this is not provably safe, much like the equivalent assert
 *        statement in Java.</li>
 * </ul>
 *
 * @author robbyw@google.com (Robby Walker)
 */
final class ClosureCodeRemoval implements CompilerPass {

  /** Reference to the JS compiler */
  private final AbstractCompiler compiler;

  /** Name used to denote an abstract function */
  static final String ABSTRACT_METHOD_NAME = "goog.abstractMethod";

  private final boolean removeAbstractMethods;
  private final boolean removeAssertionCalls;

  /**
   * List of names referenced in successive generations of finding referenced
   * nodes.
   */
  private final List<RemovableAssignment> abstractMethodAssignmentNodes =
       new ArrayList<>();

  /**
   * List of assertion functions.
   */
  private final List<Node> assertionCalls = new ArrayList<>();


  /**
   * Utility class to track a node and its parent.
   */
  private class RemovableAssignment {
    /**
     * The node
     */
    final Node node;

    /**
     * Its parent
     */
    final Node parent;

    /**
     * Full chain of ASSIGN ancestors
     */
    final List<Node> assignAncestors = new ArrayList<>();

    /**
     * The last ancestor
     */
    final Node lastAncestor;

    /**
     * Data structure for information about a removable assignment.
     *
     * @param nameNode The LHS
     * @param assignNode The parent ASSIGN node
     * @param traversal Access to further levels, assumed to start at 1
     */
    public RemovableAssignment(Node nameNode, Node assignNode,
        NodeTraversal traversal) {
      this.node = nameNode;
      this.parent = assignNode;

      Node ancestor = assignNode;
      do {
        ancestor = ancestor.getParent();
        assignAncestors.add(ancestor);
      } while (ancestor.isAssign() &&
               ancestor.getFirstChild().isQualifiedName());
      lastAncestor = ancestor.getParent();
    }

    /**
     * Remove this node.
     */
    public void remove() {
      Node rhs = node.getNext();
      Node last = parent;
      for (Node ancestor : assignAncestors) {
        if (ancestor.isExprResult()) {
          lastAncestor.removeChild(ancestor);
        } else {
          rhs.detachFromParent();
          ancestor.replaceChild(last, rhs);
        }
        last = ancestor;
      }
      compiler.reportCodeChange();
    }
  }

  /**
   * Identifies all assignments of the abstract method to a variable.
   */
  private class FindAbstractMethods extends AbstractPostOrderCallback {
    @Override
    public void visit(NodeTraversal t, Node n, Node parent) {
      if (n.isAssign()) {
        Node nameNode = n.getFirstChild();
        Node valueNode = n.getLastChild();

        if (nameNode.isQualifiedName() &&
            valueNode.isQualifiedName() &&
            valueNode.matchesQualifiedName(ABSTRACT_METHOD_NAME)) {
          abstractMethodAssignmentNodes.add(new RemovableAssignment(
              n.getFirstChild(), n, t));
        }
      }
    }
  }


  /**
   * Identifies all assertion calls.
   */
  private class FindAssertionCalls extends AbstractPostOrderCallback {
    final Set<String> assertionNames;

    FindAssertionCalls() {
      ImmutableSet.Builder<String> assertionNamesBuilder = ImmutableSet.builder();
      for (AssertionFunctionSpec spec :
               compiler.getCodingConvention().getAssertionFunctions()) {
        assertionNamesBuilder.add(spec.getFunctionName());
      }
      assertionNames = assertionNamesBuilder.build();
    }


    @Override
    public void visit(NodeTraversal t, Node n, Node parent) {
      if (n.isCall()) {
        String fnName = n.getFirstChild().getQualifiedName();
        if (assertionNames.contains(fnName)) {
          assertionCalls.add(n);
        }
      }
    }
  }


  /**
   * Creates a Closure code remover.
   *
   * @param compiler The AbstractCompiler
   * @param removeAbstractMethods Remove declarations of abstract methods.
   * @param removeAssertionCalls Remove calls to goog.assert functions.
   */
  ClosureCodeRemoval(AbstractCompiler compiler, boolean removeAbstractMethods,
                     boolean removeAssertionCalls) {
    this.compiler = compiler;
    this.removeAbstractMethods = removeAbstractMethods;
    this.removeAssertionCalls = removeAssertionCalls;
  }

  @Override
  public void process(Node externs, Node root) {
    List<Callback> passes = new ArrayList<>();
    if (removeAbstractMethods) {
      passes.add(new FindAbstractMethods());
    }
    if (removeAssertionCalls) {
      passes.add(new FindAssertionCalls());
    }
    CombinedCompilerPass.traverse(compiler, root, passes);

    for (RemovableAssignment assignment : abstractMethodAssignmentNodes) {
      assignment.remove();
    }

    for (Node call : assertionCalls) {
      // If the assertion is an expression, just strip the whole thing.
      Node parent = call.getParent();
      if (parent.isExprResult()) {
        parent.getParent().removeChild(parent);
      } else {
        // Otherwise, replace the assertion with its first argument,
        // which is the return value of the assertion.
        Node firstArg = call.getSecondChild();
        if (firstArg == null) {
          parent.replaceChild(call, NodeUtil.newUndefinedNode(call));
        } else {
          parent.replaceChild(call, firstArg.detachFromParent());
        }
      }
      compiler.reportCodeChange();
    }
  }
}
