/*
 * Copyright 2011 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.HashMultimap;
import com.google.common.collect.Multimap;
import com.google.javascript.jscomp.NodeTraversal.AbstractPostOrderCallback;
import com.google.javascript.jscomp.NodeTraversal.ScopedCallback;
import com.google.javascript.jscomp.RenameVarsWithModuleSupport.Assignment;
import com.google.javascript.rhino.Node;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.SortedSet;

/**
 * Apache Royale copied ShadowVariables and modified it to handle
 * Royale modules.  The only change is to uses classes from
 * RenameVarsWithModuleSupport instead of RenameVars.
 * 
 * Tries to compute a list of variables that can shadow a variable in the
 * outer scope.
 *
 * For example:
 *
 * <code>
 * var a = function() {
 *   var b = getB();
 *   b();
 *   return function(y) {};
 * };
 * </code>
 *
 * Normally, b would be mapped to variable L0, y would be L1.
 *
 * Instead we are going to make y shadows L0 in hope of using less variables
 * and reusing frequently used local names.
 *
 */
class ShadowVariablesWithModuleSupport implements CompilerPass {

  // Keep a map of Upward Referencing name nodes of each scope.
  // A name is upward referencing name of a scope if:
  //
  // 1) It refers to (or defines) a name that is defined in the current
  // scope or any scope above the current scope that isn't the
  // global scope.
  //
  // 2) It is a upward referencing name of a child scope of this scope.
  //
  // Example:
  // var x; var y; function foo(a) { function bar(b) { x, a } }
  // The upward referencing names in scope 'foo' is bar, b, x and a;
  // The key to this map is the root node of the scope.
  //
  // We can see that for any variable x in the current scope, we can shadow
  // a variable y in an outer scope given that y is not a upward referencing
  // name of the current scope.

  // TODO(user): Maps scope to string instead of Node to string.
  // Make sure of scope memorization to minimize scope creation cost.
  private final Multimap<Node, String> scopeUpRefMap = HashMultimap.create();

  // Maps each local variable to all of its referencing NAME nodes in any scope.
  private final Multimap<Var, Reference> varToNameUsage = HashMultimap.create();

  private final AbstractCompiler compiler;

  // All the information used for renaming.
  private final SortedSet<Assignment> varsByFrequency;
  private final Map<String, Assignment> assignments;
  private final Map<Node, String> oldPseudoNameMap;
  private final Map<Node, String> deltaPseudoNameMap;


  /**
   * @param assignments Map of old variable names to its assignment Objects.
   * @param varsByFrequency Sorted variable assignments by Frequency.
   * @param pseudoNameMap The current pseudo name map so this pass can update
   *     it accordingly.
   */
  ShadowVariablesWithModuleSupport(
      AbstractCompiler compiler,
      Map<String, Assignment> assignments,
      SortedSet<Assignment> varsByFrequency,
      Map<Node, String> pseudoNameMap) {
    this.compiler = compiler;
    this.assignments = assignments;
    this.varsByFrequency = varsByFrequency;
    this.oldPseudoNameMap = pseudoNameMap;
    this.deltaPseudoNameMap = new LinkedHashMap<Node, String>();
  }

  @Override
  public void process(Node externs, Node root) {
    // The algorithm is divided into two stages:
    //
    // 1. Information gathering (variable usage, upward referencing)
    //
    // 2. Tries to find shadows for each variables, updates the
    //    variable usage frequency map.
    //
    // 3. Updates the pseudo naming map if needed.
    NodeTraversal.traverse(compiler, root, new GatherReferenceInfo());
    NodeTraversal.traverse(compiler, root, new DoShadowVariables());

    if (oldPseudoNameMap != null) {
      oldPseudoNameMap.putAll(deltaPseudoNameMap);
    }
  }

  private class GatherReferenceInfo extends AbstractPostOrderCallback {
    @Override
    public void visit(NodeTraversal t, Node n, Node parent) {
      // Skipping over non-name nodes and empty function names.
      if (!NodeUtil.isReferenceName(n)) {
        return;
      }

      // We focus on shadowing local variables as their name occurs much more
      // than global names.
      // TODO(user): Alternatively, we could experiment with using a local
      // name to shadow a global variable.
      if (t.inGlobalScope()) {
        return;
      }

      Scope scope = t.getScope();
      Var var = scope.getVar(n.getString());
      if (var == null) {
        // extern name or undefined name.
        return;
      }

      if (var.getScope().isGlobal()) {
        // We will not shadow a global variable name.
        return;
      }

      // Using the definition of upward referencing, fill in the map.
      if (var.getScope() != scope) {
        for (Scope s = scope; s != var.getScope() && s.isLocal(); s = s.getParent()) {
          scopeUpRefMap.put(s.getRootNode(), var.name);
        }
      } else {
        scopeUpRefMap.put(t.getScopeRoot(), var.name);
      }

      // Make sure that we don't shadow function parameters or function names from a function block
      // scope, eg.:
      // function f(a) { ... var a; ... } // Unsafe
      if (scope.isFunctionScope() && var.getScope() == scope) {
        scopeUpRefMap.put(scope.getRootNode().getLastChild(), var.name);
      }

      // Find in the usage map that tracks a var and all of its usage.
      varToNameUsage.put(var, new Reference(n, scope));
    }
  }

  private class DoShadowVariables extends AbstractPostOrderCallback
      implements ScopedCallback {

    @Override
    public void enterScope(NodeTraversal t) {
      if (t.inGlobalScope()) {
        return;
      }

      // Since we don't shadow global, there is nothing to be done in the
      // first immediate local scope as well.
      Node scopeRoot = t.getScopeRoot();
      if ((scopeRoot.isFunction()
              && NodeUtil.getEnclosingFunction(scopeRoot.getParent()) == null)
          || (NodeUtil.isFunctionBlock(scopeRoot)
              && NodeUtil.getEnclosingFunction(scopeRoot.getGrandparent()) == null)) {
        return;
      }

      Scope s = t.getScope();
      for (Var var : s.getVarIterable()) {
        // Don't shadow variables that are bleed-out functions or caught exceptions to workaround
        // IE8 bugs.
        // TODO(moz): Gate this behind languageMode=ES3.
        if (var.isBleedingFunction() || var.isCatch()) {
          continue;
        }

        // Don't shadow an exported local.
        if (compiler.getCodingConvention().isExported(var.name, s.isLocal())) {
          continue;
        }

        // The name assignment being shadowed.
        Assignment localAssignment = assignments.get(var.getName());
        if (localAssignment == null) {
          continue;
        }
        // Try to run this check last as it is more expensive than the above checks.

        // Try to look for the best shadow for the current candidate.
        Assignment bestShadow = findBestShadow(s, var);
        if (bestShadow == null) {
          continue;
        }

        // Only shadow if this increases the number of occurrences of the
        // shadowed variable.
        if (bestShadow.count < localAssignment.count) {
          continue; // Hope the next local variable would have a smaller count.
        }

        doShadow(localAssignment, bestShadow, var);

        if (oldPseudoNameMap != null) {
          String targetPseudoName =
            oldPseudoNameMap.get(s.getVar(bestShadow.oldName).nameNode);
          for (Reference use : varToNameUsage.get(var)) {
            deltaPseudoNameMap.put(use.nameNode, targetPseudoName);
          }
        }
      }
    }

    @Override
    public void exitScope(NodeTraversal t) {}

    @Override
    public void visit(NodeTraversal t, Node n, Node parent) {}

    /**
     * @return An assignment that can be used as a shadow for a local variable
     *     in the scope defined by curScopeRoot.
     */
    private Assignment findBestShadow(Scope curScope, Var var) {
      // Search for the candidate starting from the most used local.
      for (Assignment assignment : varsByFrequency) {
        if (assignment.isLocal) {
          if (!scopeUpRefMap.containsEntry(curScope.getRootNode(), assignment.oldName)) {
            if (curScope.hasSlot(assignment.oldName)) {
              // Don't shadow if the scopes are the same eg.:
              // function f() { var a = 1; { var a = 2; } } // Unsafe
              Var toShadow = curScope.getVar(assignment.oldName);
              if (var.getScope() != toShadow.getScope()) {
                return assignment;
              }
            }
          }
        }
      }
      return null;
    }

    private void doShadow(Assignment original, Assignment toShadow, Var var) {
      Scope s = var.getScope();
      // We are now shadowing 'bestShadow' with localAssignment.
      // All of the reference NAME node of this variable.
      Collection<Reference> references = varToNameUsage.get(var);

      // First remove both assignments from the sorted list since they need
      // to be re-sorted.
      varsByFrequency.remove(original);
      varsByFrequency.remove(toShadow);

      // Adjust the count offset by the inner scope variable.
      original.count -= references.size();
      toShadow.count += references.size();

      // Add it back to the sorted list after re-adjustment.
      varsByFrequency.add(original);
      varsByFrequency.add(toShadow);

      // This is an important step. If variable L7 is going to be renamed to
      // L1, by definition of upward referencing, The name L1 is now in the
      // set of upward referencing names of the current scope up to the
      // declaring scope of the best shadow variable.
      Var shadowed = s.getVar(toShadow.oldName);
      if (shadowed != null) {
        if (s.isFunctionScope() && s.getRootNode().getLastChild().isBlock()) {
          scopeUpRefMap.put(s.getRootNode().getLastChild(), toShadow.oldName);
          scopeUpRefMap.remove(s.getRootNode().getLastChild(), original.oldName);
        }
        for (Scope curScope = s; curScope != shadowed.scope; curScope = curScope.getParent()) {
          scopeUpRefMap.put(curScope.getRootNode(), toShadow.oldName);
          scopeUpRefMap.remove(curScope.getRootNode(), original.oldName);
        }
      }

      // Mark all the references as shadowed.
      for (Reference ref : references) {
        Node n = ref.nameNode;
        n.setString(toShadow.oldName);
        if (ref.scope.getRootNode() == s.getRootNode()) {
          if (var.getNameNode() != ref.nameNode) {
            scopeUpRefMap.put(s.getRootNode(), toShadow.oldName);
            scopeUpRefMap.remove(s.getRootNode(), original.oldName);
          }
        } else {
          for (Scope curScope = ref.scope;
              curScope.getRootNode() != s.getRootNode();
              curScope = curScope.getParent()) {
            scopeUpRefMap.put(curScope.getRootNode(), toShadow.oldName);
            scopeUpRefMap.remove(curScope.getRootNode(), original.oldName);
          }
        }
      }
    }
  }

  private static final class Reference {
    private final Node nameNode;
    private final Scope scope;

    private Reference(Node nameNode, Scope scope) {
      this.nameNode = nameNode;
      this.scope = scope;
    }
  }
}
