/*
 * 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.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.javascript.jscomp.CodingConvention.SubclassRelationship;
import com.google.javascript.jscomp.ReferenceCollectingCallback.Behavior;
import com.google.javascript.jscomp.ReferenceCollectingCallback.Reference;
import com.google.javascript.jscomp.ReferenceCollectingCallback.ReferenceCollection;
import com.google.javascript.jscomp.ReferenceCollectingCallback.ReferenceMap;
import com.google.javascript.rhino.Node;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Using the infrastructure provided by VariableReferencePass, identify
 * variables that are used only once and in a way that is safe to move, and then
 * inline them.
 *
 * This pass has two "modes." One mode only inlines variables declared as
 * constants, for legacy compiler clients. The second mode inlines any
 * variable that we can provably inline. Note that the second mode is a
 * superset of the first mode. We only support the first mode for
 * backwards-compatibility with compiler clients that don't want
 * --inline_variables.
 *
 * The approach of this pass is similar to {@link CrossModuleCodeMotion}
 *
 * @author kushal@google.com (Kushal Dave)
 * @author nicksantos@google.com (Nick Santos)
 */
class InlineVariables implements CompilerPass {

  private final AbstractCompiler compiler;

  enum Mode {
    // Only inline things explicitly marked as constant.
    CONSTANTS_ONLY,
    // Locals only
    LOCALS_ONLY,
    ALL
  }

  private final Mode mode;

  // Inlines all strings, even if they increase the size of the gzipped binary.
  private final boolean inlineAllStrings;

  private final IdentifyConstants identifyConstants = new IdentifyConstants();

  InlineVariables(
      AbstractCompiler compiler,
      Mode mode,
      boolean inlineAllStrings) {
    this.compiler = compiler;
    this.mode = mode;
    this.inlineAllStrings = inlineAllStrings;
  }

  @Override
  public void process(Node externs, Node root) {
    ReferenceCollectingCallback callback = new ReferenceCollectingCallback(
        compiler, new InliningBehavior(), getFilterForMode());
    callback.process(externs, root);
  }

  private Predicate<Var> getFilterForMode() {
    switch (mode) {
      case ALL:
        return Predicates.alwaysTrue();
      case LOCALS_ONLY:
        return new IdentifyLocals();
      case CONSTANTS_ONLY:
        return new IdentifyConstants();
      default:
        throw new IllegalStateException();
    }
  }

  /**
   * Filters variables declared as "constant", and declares them in the outer
   * declaredConstants map.
   *
   * In Google coding conventions, this means anything declared with @const
   * or named in all caps, and initialized to an immutable value.
   * CheckConsts has already verified that these are truly constants.
   */
  private static class IdentifyConstants implements Predicate<Var> {
    @Override
    public boolean apply(Var var) {
      return var.isInferredConst();
    }
  }

  /**
   * Filters non-global variables.
   */
  private static class IdentifyLocals implements Predicate<Var> {
    @Override
    public boolean apply(Var var) {
      return var.scope.isLocal();
    }
  }

  private static class AliasCandidate {
    private final Var alias;
    private final ReferenceCollection refInfo;

    AliasCandidate(Var alias, ReferenceCollection refInfo) {
      this.alias = alias;
      this.refInfo = refInfo;
    }
  }

  /**
   * Builds up information about nodes in each scope. When exiting the
   * scope, inspects all variables in that scope, and inlines any
   * that we can.
   */
  private class InliningBehavior implements Behavior {

    /**
     * A list of variables that should not be inlined, because their
     * reference information is out of sync with the state of the AST.
     */
    private final Set<Var> staleVars = new HashSet<>();

    /**
     * Stored possible aliases of variables that never change, with
     * all the reference info about those variables. Hashed by the NAME
     * node of the variable being aliased.
     */
    final Map<Node, AliasCandidate> aliasCandidates = new HashMap<>();

    @Override
    public void afterExitScope(NodeTraversal t, ReferenceMap referenceMap) {
      collectAliasCandidates(t, referenceMap);
      doInlinesForScope(t, referenceMap);
    }

    /**
     * If any of the variables are well-defined and alias other variables,
     * mark them as aliasing candidates.
     */
    private void collectAliasCandidates(NodeTraversal t,
        ReferenceMap referenceMap) {
      if (mode != Mode.CONSTANTS_ONLY) {
        for (Iterator<Var> it = t.getScope().getVars(); it.hasNext();) {
          Var v = it.next();
          ReferenceCollection referenceInfo = referenceMap.getReferences(v);

          // NOTE(nicksantos): Don't handle variables that are never used.
          // The tests are much easier to write if you don't, and there's
          // another pass that handles unused variables much more elegantly.
          if (referenceInfo != null && referenceInfo.references.size() >= 2 &&
              referenceInfo.isWellDefined() &&
              referenceInfo.isAssignedOnceInLifetime()) {
            Reference init = referenceInfo.getInitializingReference();
            Node value = init.getAssignedValue();
            if (value != null && value.isName()
                && !value.getString().equals(v.getName())) {
              aliasCandidates.put(value, new AliasCandidate(v, referenceInfo));
            }
          }
        }
      }
    }

    /**
     * For all variables in this scope, see if they are only used once.
     * If it looks safe to do so, inline them.
     */
    private void doInlinesForScope(NodeTraversal t, ReferenceMap referenceMap) {

      boolean maybeModifiedArguments =
          maybeEscapedOrModifiedArguments(t.getScope(), referenceMap);
      for (Iterator<Var> it = t.getScope().getVars(); it.hasNext();) {
        Var v = it.next();

        ReferenceCollection referenceInfo = referenceMap.getReferences(v);

        // referenceInfo will be null if we're in constants-only mode
        // and the variable is not a constant.
        if (referenceInfo == null || isVarInlineForbidden(v)) {
          // Never try to inline exported variables or variables that
          // were not collected or variables that have already been inlined.
          continue;
        } else if (isInlineableDeclaredConstant(v, referenceInfo)) {
          Reference init = referenceInfo.getInitializingReferenceForConstants();
          Node value = init.getAssignedValue();
          inlineDeclaredConstant(v, value, referenceInfo.references);
          staleVars.add(v);
        } else if (mode == Mode.CONSTANTS_ONLY) {
          // If we're in constants-only mode, don't run more aggressive
          // inlining heuristics. See InlineConstantsTest.
          continue;
        } else {
          inlineNonConstants(v, referenceInfo, maybeModifiedArguments);
        }
      }
    }

    private boolean maybeEscapedOrModifiedArguments(
        Scope scope, ReferenceMap referenceMap) {
      if (scope.isLocal()) {
        Var arguments = scope.getArgumentsVar();
        ReferenceCollection refs = referenceMap.getReferences(arguments);
        if (refs != null && !refs.references.isEmpty()) {
          for (Reference ref : refs.references) {
            Node refNode = ref.getNode();
            Node refParent = ref.getParent();
            // Any reference that is not a read of the arguments property
            // consider a escape of the arguments object.
            if (!(NodeUtil.isGet(refParent)
                && refNode == ref.getParent().getFirstChild()
                && !isLValue(refParent))) {
              return true;
            }
          }
        }
      }
      return false;
    }

    private boolean isLValue(Node n) {
      Node parent = n.getParent();
      return (parent.isInc()
          || parent.isDec()
          || (NodeUtil.isAssignmentOp(parent)
          && parent.getFirstChild() == n));
    }

    private void inlineNonConstants(
        Var v, ReferenceCollection referenceInfo,
        boolean maybeModifiedArguments) {
      int refCount = referenceInfo.references.size();
      Reference declaration = referenceInfo.references.get(0);
      Reference init = referenceInfo.getInitializingReference();
      int firstRefAfterInit = (declaration == init) ? 2 : 3;

      if (refCount > 1 &&
          isImmutableAndWellDefinedVariable(v, referenceInfo)) {
        // if the variable is referenced more than once, we can only
        // inline it if it's immutable and never defined before referenced.
        Node value;
        if (init != null) {
          value = init.getAssignedValue();
        } else {
          // Create a new node for variable that is never initialized.
          Node srcLocation = declaration.getNode();
          value = NodeUtil.newUndefinedNode(srcLocation);
        }
        Preconditions.checkNotNull(value);
        inlineWellDefinedVariable(v, value, referenceInfo.references);
        staleVars.add(v);
      } else if (refCount == firstRefAfterInit) {
        // The variable likely only read once, try some more
        // complex inlining heuristics.
        Reference reference = referenceInfo.references.get(
            firstRefAfterInit - 1);
        if (canInline(declaration, init, reference)) {
          inline(v, declaration, init, reference);
          staleVars.add(v);
        }
      } else if (declaration != init && refCount == 2) {
        if (isValidDeclaration(declaration) && isValidInitialization(init)) {
          // The only reference is the initialization, remove the assignment and
          // the variable declaration.
          Node value = init.getAssignedValue();
          Preconditions.checkNotNull(value);
          inlineWellDefinedVariable(v, value, referenceInfo.references);
          staleVars.add(v);
        }
      }

      // If this variable was not inlined normally, check if we can
      // inline an alias of it. (If the variable was inlined, then the
      // reference data is out of sync. We're better off just waiting for
      // the next pass.)
      if (!maybeModifiedArguments &&
          !staleVars.contains(v) && referenceInfo.isWellDefined() &&
          referenceInfo.isAssignedOnceInLifetime()) {
        List<Reference> refs = referenceInfo.references;
        for (int i = 1 /* start from a read */; i < refs.size(); i++) {
          Node nameNode = refs.get(i).getNode();
          if (aliasCandidates.containsKey(nameNode)) {
            AliasCandidate candidate = aliasCandidates.get(nameNode);
            if (!staleVars.contains(candidate.alias) &&
                !isVarInlineForbidden(candidate.alias)) {
              Reference aliasInit;
              aliasInit = candidate.refInfo.getInitializingReference();
              Node value = aliasInit.getAssignedValue();
              Preconditions.checkNotNull(value);
              inlineWellDefinedVariable(candidate.alias,
                  value,
                  candidate.refInfo.references);
              staleVars.add(candidate.alias);
            }
          }
        }
      }
    }

    /**
     * If there are any variable references in the given node tree, blacklist
     * them to prevent the pass from trying to inline the variable.
     */
    private void blacklistVarReferencesInTree(Node root, Scope scope) {
      for (Node c = root.getFirstChild(); c != null; c = c.getNext()) {
        blacklistVarReferencesInTree(c, scope);
      }

      if (root.isName()) {
        staleVars.add(scope.getVar(root.getString()));
      }
    }

    /**
     * Whether the given variable is forbidden from being inlined.
     */
    private boolean isVarInlineForbidden(Var var) {
      // A variable may not be inlined if:
      // 1) The variable is exported,
      // 2) A reference to the variable has been inlined. We're downstream
      //    of the mechanism that creates variable references, so we don't
      //    have a good way to update the reference. Just punt on it.
      // 3) Don't inline the special RENAME_PROPERTY_FUNCTION_NAME
      return var.isExtern()
          || compiler.getCodingConvention().isExported(var.name)
          || RenameProperties.RENAME_PROPERTY_FUNCTION_NAME.equals(var.name)
          || staleVars.contains(var);
    }

    /**
     * Do the actual work of inlining a single declaration into a single
     * reference.
     */
    private void inline(Var v, Reference decl, Reference init, Reference ref) {
      Node value = init.getAssignedValue();
      Preconditions.checkState(value != null);
      // Check for function declarations before the value is moved in the AST.
      boolean isFunctionDeclaration = NodeUtil.isFunctionDeclaration(value);
      compiler.reportChangeToEnclosingScope(ref.getNode());
      inlineValue(v, ref, value.detachFromParent());
      if (decl != init) {
        Node expressRoot = init.getGrandparent();
        Preconditions.checkState(expressRoot.isExprResult());
        NodeUtil.removeChild(expressRoot.getParent(), expressRoot);
      }
      // Function declarations have already been removed.
      if (!isFunctionDeclaration) {
        compiler.reportChangeToEnclosingScope(decl.getNode());
        removeDeclaration(decl);
      }
    }

    /**
     * Inline an immutable variable into all of its references.
     */
    private void inlineWellDefinedVariable(Var v, Node value,
        List<Reference> refSet) {
      Reference decl = refSet.get(0);
      for (int i = 1; i < refSet.size(); i++) {
        inlineValue(v, refSet.get(i), value.cloneTree());
      }
      removeDeclaration(decl);
    }

    /**
     * Inline a declared constant.
     */
    private void inlineDeclaredConstant(Var v, Node value,
        List<Reference> refSet) {
      // Replace the references with the constant value
      Reference decl = null;

      for (Reference r : refSet) {
        if (r.getNode() == v.getNameNode()) {
          decl = r;
        } else {
          inlineValue(v, r, value.cloneTree());
        }
      }

      removeDeclaration(decl);
    }

    /**
     * Remove the given VAR declaration.
     */
    private void removeDeclaration(Reference decl) {
      Node varNode = decl.getParent();
      Node grandparent = decl.getGrandparent();

      compiler.reportChangeToEnclosingScope(decl.getNode());
      varNode.removeChild(decl.getNode());
      // Remove var node if empty
      if (!varNode.hasChildren()) {
        Preconditions.checkState(varNode.isVar());
        NodeUtil.removeChild(grandparent, varNode);
      }
    }

    /**
     * Replace the given reference with the given value node.
     *
     * @param v The variable that's referenced.
     * @param ref The reference to replace.
     * @param value The node tree to replace it with. This tree should be safe
     *     to re-parent.
     */
    private void inlineValue(Var v, Reference ref, Node value) {
      compiler.reportChangeToEnclosingScope(ref.getNode());
      if (ref.isSimpleAssignmentToName()) {
        // This is the initial assignment.
        ref.getGrandparent().replaceChild(ref.getParent(), value);
      } else {
        ref.getParent().replaceChild(ref.getNode(), value);
      }
      blacklistVarReferencesInTree(value, v.scope);
    }

    /**
     * Determines whether the given variable is declared as a constant
     * and may be inlined.
     */
    private boolean isInlineableDeclaredConstant(Var var,
        ReferenceCollection refInfo) {
      if (!identifyConstants.apply(var)) {
        return false;
      }

      if (!refInfo.isAssignedOnceInLifetime()) {
        return false;
      }

      Reference init = refInfo.getInitializingReferenceForConstants();
      if (init == null) {
        return false;
      }

      Node value = init.getAssignedValue();
      if (value == null) {
        // This constant is either externally defined or initialized indirectly
        // (e.g. in an function expression used to hide
        // temporary variables), so the constant is ineligible for inlining.
        return false;
      }

      // Is the constant's value immutable?
      if (!NodeUtil.isImmutableValue(value)) {
        return false;
      }

      // Determine if we should really inline a String or not.
      return !value.isString() ||
          isStringWorthInlining(var, refInfo.references);
    }

    /**
     * Compute whether the given string is worth inlining.
     */
    private boolean isStringWorthInlining(Var var, List<Reference> refs) {
      if (!inlineAllStrings && !var.isDefine()) {
        int len = var.getInitialValue().getString().length() + "''".length();

        // if not inlined: var xx="value"; .. xx .. xx ..
        // The 4 bytes per reference is just a heuristic:
        // 2 bytes per var name plus maybe 2 bytes if we don't inline, e.g.
        // in the case of "foo " + CONST + " bar"
        int noInlineBytes = "var xx=;".length() + len +
                            4 * (refs.size() - 1);

        // if inlined:
        // I'm going to assume that half of the quotes will be eliminated
        // thanks to constant folding, therefore I subtract 1 (2/2=1) from
        // the string length.
        int inlineBytes = (len - 1) * (refs.size() - 1);

        // Not inlining if doing so uses more bytes, or this constant is being
        // defined.
        return noInlineBytes >= inlineBytes;
      }

      return true;
    }

    /**
     * @return true if the provided reference and declaration can be safely
     *         inlined according to our criteria
     */
    private boolean canInline(
        Reference declaration,
        Reference initialization,
        Reference reference) {
      if (!isValidDeclaration(declaration)
          || !isValidInitialization(initialization)
          || !isValidReference(reference)) {
        return false;
      }

      // If the value is read more than once, skip it.
      // VAR declarations and EXPR_RESULT don't need the value, but other
      // ASSIGN expressions parents do.
      if (declaration != initialization &&
          !initialization.getGrandparent().isExprResult()) {
        return false;
      }

      // Be very conservative and do no cross control structures or
      // scope boundaries
      if (declaration.getBasicBlock() != initialization.getBasicBlock()
          || declaration.getBasicBlock() != reference.getBasicBlock()) {
        return false;
      }

      // Do not inline into a call node. This would change
      // the context in which it was being called. For example,
      //   var a = b.c;
      //   a();
      // should not be inlined, because it calls a in the context of b
      // rather than the context of the window.
      //   var a = b.c;
      //   f(a)
      // is OK.
      Node value = initialization.getAssignedValue();
      Preconditions.checkState(value != null);
      if (value.isGetProp()
          && reference.getParent().isCall()
          && reference.getParent().getFirstChild() == reference.getNode()) {
        return false;
      }

      if (value.isFunction()) {
        Node callNode = reference.getParent();
        if (reference.getParent().isCall()) {
          CodingConvention convention = compiler.getCodingConvention();
          // Bug 2388531: Don't inline subclass definitions into class defining
          // calls as this confused class removing logic.
          SubclassRelationship relationship =
              convention.getClassesDefinedByCall(callNode);
          if (relationship != null) {
            return false;
          }

          // issue 668: Don't inline singleton getter methods
          // calls as this confused class removing logic.
          if (convention.getSingletonGetterClassName(callNode) != null) {
            return false;
          }
        }
      }

      return canMoveAggressively(value) ||
          canMoveModerately(initialization, reference);
    }

    /**
     * If the value is a literal, we can cross more boundaries to inline it.
     */
    private boolean canMoveAggressively(Node value) {
      // Function expressions and other mutable objects can move within
      // the same basic block.
      return NodeUtil.isLiteralValue(value, true)
          || value.isFunction();
    }

    /**
     * If the value of a variable is not constant, then it may read or modify
     * state. Therefore it cannot be moved past anything else that may modify
     * the value being read or read values that are modified.
     */
    private boolean canMoveModerately(
        Reference initialization,
        Reference reference) {
      // Check if declaration can be inlined without passing
      // any side-effect causing nodes.
      Iterator<Node> it;
      if (initialization.getParent().isVar()) {
        it = NodeIterators.LocalVarMotion.forVar(
            initialization.getNode(),     // NAME
            initialization.getParent(),       // VAR
            initialization.getGrandparent()); // VAR container
      } else if (initialization.getParent().isAssign()) {
        Preconditions.checkState(
            initialization.getGrandparent().isExprResult());
        it = NodeIterators.LocalVarMotion.forAssign(
            initialization.getNode(),     // NAME
            initialization.getParent(),       // ASSIGN
            initialization.getGrandparent(),  // EXPR_RESULT
            initialization.getGrandparent().getParent()); // EXPR container
      } else {
        throw new IllegalStateException("Unexpected initialization parent " +
            initialization.getParent().toStringTree());
      }
      Node targetName = reference.getNode();
      while (it.hasNext()) {
        Node curNode = it.next();
        if (curNode == targetName) {
          return true;
        }
      }

      return false;
    }

    /**
     * @return true if the reference is a normal VAR or FUNCTION declaration.
     */
    private boolean isValidDeclaration(Reference declaration) {
      return (declaration.getParent().isVar()
          && !declaration.getGrandparent().isFor())
          || NodeUtil.isFunctionDeclaration(declaration.getParent());
    }

    /**
     * @return Whether there is a initial value.
     */
    private boolean isValidInitialization(Reference initialization) {
      if (initialization == null) {
        return false;
      } else if (initialization.isDeclaration()) {
        // The reference is a FUNCTION declaration or normal VAR declaration
        // with a value.
        if (!NodeUtil.isFunctionDeclaration(initialization.getParent())
            && initialization.getNode().getFirstChild() == null) {
          return false;
        }
      } else {
        Node parent = initialization.getParent();
        Preconditions.checkState(
            parent.isAssign()
            && parent.getFirstChild() == initialization.getNode());
      }

      Node n = initialization.getAssignedValue();
      if (n.isFunction()) {
        return compiler.getCodingConvention().isInlinableFunction(n);
      }

      return true;
    }

    /**
     * @return true if the reference is a candidate for inlining
     */
    private boolean isValidReference(Reference reference) {
      return !reference.isDeclaration() && !reference.isLvalue();
    }

    /**
     * Determines whether the reference collection describes a variable that
     * is initialized to an immutable value, never modified, and defined before
     * every reference.
     */
    private boolean isImmutableAndWellDefinedVariable(Var v,
        ReferenceCollection refInfo) {
      List<Reference> refSet = refInfo.references;
      int startingReadRef = 1;
      Reference refDecl = refSet.get(0);
      if (!isValidDeclaration(refDecl)) {
        return false;
      }

      boolean isNeverAssigned = refInfo.isNeverAssigned();
      // For values that are never assigned, only the references need to be
      // checked.
      if (!isNeverAssigned) {
        Reference refInit = refInfo.getInitializingReference();
        if (!isValidInitialization(refInit)) {
          return false;
        }

        if (refDecl != refInit) {
          Preconditions.checkState(refInit == refSet.get(1));
          startingReadRef = 2;
        }

        if (!refInfo.isWellDefined()) {
          return false;
        }

        Node value = refInit.getAssignedValue();
        Preconditions.checkNotNull(value);

        boolean isImmutableValueWorthInlining =
            NodeUtil.isImmutableValue(value) &&
            (!value.isString() ||
                isStringWorthInlining(v, refInfo.references));
        boolean isInlinableThisAlias =
            value.isThis() &&
            !refInfo.isEscaped();
        if (!isImmutableValueWorthInlining && !isInlinableThisAlias) {
          return false;
        }
      }

      for (int i = startingReadRef; i < refSet.size(); i++) {
        Reference ref = refSet.get(i);
        if (!isValidReference(ref)) {
          return false;
        }
      }

      return true;
    }
  }
}
