/*
 * 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.collect.Sets;
import com.google.javascript.jscomp.ControlFlowGraph.Branch;
import com.google.javascript.jscomp.Scope.Var;
import com.google.javascript.jscomp.graph.DiGraph.DiGraphEdge;
import com.google.javascript.jscomp.graph.LatticeElement;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.Token;

import java.util.BitSet;
import java.util.List;
import java.util.Set;

/**
 * Compute the "liveness" of all local variables. A variable is "live" at a
 * point of a program if the value it is currently holding might be read later.
 * Otherwise, the variable is considered "dead" if we know for sure that it will
 * no longer be read. Dead variables are candidates for dead assignment
 * elimination and variable name sharing. The worst case safe assumption is to
 * assume that all variables are live. In that case, we will have no opportunity
 * for optimizations. This is especially the case within a TRY block when an
 * assignment is not guaranteed to take place. We bail out by assuming that
 * all variables are live.
 * <p>
 * Due to the possibility of inner functions and closures, certain "local"
 * variables can escape the function. These variables will be considered as
 * global and they can be retrieved with {@link #getEscapedLocals()}.
 *
 */
class LiveVariablesAnalysis extends
    DataFlowAnalysis<Node, LiveVariablesAnalysis.LiveVariableLattice> {

  // 100 = ((# of original Power Rangers) ^
  //        (# years of Warren Harding in office)) *
  //       (# of Ninja Turtles)
  static final int MAX_VARIABLES_TO_ANALYZE = 100;

  public static final String ARGUMENT_ARRAY_ALIAS = "arguments";

  private static class LiveVariableJoinOp
      implements JoinOp<LiveVariableLattice> {
    @Override
    public LiveVariableLattice apply(List<LiveVariableLattice> in) {
      LiveVariableLattice result = new LiveVariableLattice(in.get(0));
      for (int i = 1; i < in.size(); i++) {
        result.liveSet.or(in.get(i).liveSet);
      }
      return result;
    }
  }

  /**
   * The lattice that stores the liveness of all local variables at a given
   * point in the program. The whole lattice is the power set of all local
   * variables and a variable is live if it is in the set.
   */
  static class LiveVariableLattice implements LatticeElement {
    private final BitSet liveSet;

    /**
     * @param numVars Number of all local variables.
     */
    private LiveVariableLattice(int numVars) {
      this.liveSet = new BitSet(numVars);
    }

    private LiveVariableLattice(LiveVariableLattice other) {
      Preconditions.checkNotNull(other);
      this.liveSet = (BitSet) other.liveSet.clone();
    }

    @Override
    public boolean equals(Object other) {
      Preconditions.checkNotNull(other);
      return (other instanceof LiveVariableLattice) &&
          this.liveSet.equals(((LiveVariableLattice) other).liveSet);
    }

    public boolean isLive(Var v) {
      Preconditions.checkNotNull(v);
      return liveSet.get(v.index);
    }

    public boolean isLive(int index) {
      return liveSet.get(index);
    }

    @Override
    public String toString() {
      return liveSet.toString();
    }

    @Override
    public int hashCode() {
      return liveSet.hashCode();
    }
  }

  // The scope of the function that we are analyzing.
  private final Scope jsScope;
  private final Set<Var> escaped;

  LiveVariablesAnalysis(ControlFlowGraph<Node> cfg, Scope jsScope,
      AbstractCompiler compiler) {
    super(cfg, new LiveVariableJoinOp());
    this.jsScope = jsScope;
    this.escaped = Sets.newHashSet();
    computeEscaped(jsScope, escaped, compiler);
  }

  public Set<Var> getEscapedLocals() {
    return escaped;
  }

  public int getVarIndex(String var) {
    return jsScope.getVar(var).index;
  }

  @Override
  boolean isForward() {
    return false;
  }

  @Override
  LiveVariableLattice createEntryLattice() {
    return new LiveVariableLattice(jsScope.getVarCount());
  }

  @Override
  LiveVariableLattice createInitialEstimateLattice() {
    return new LiveVariableLattice(jsScope.getVarCount());
  }

  @Override
  LiveVariableLattice flowThrough(Node node, LiveVariableLattice input) {
    final BitSet gen = new BitSet(input.liveSet.size());
    final BitSet kill = new BitSet(input.liveSet.size());

    // Make kills conditional if the node can end abruptly by an exception.
    boolean conditional = false;
    List<DiGraphEdge<Node, Branch>> edgeList = getCfg().getOutEdges(node);
    for (DiGraphEdge<Node, Branch> edge : edgeList) {
      if (Branch.ON_EX.equals(edge.getValue())) {
        conditional = true;
      }
    }
    computeGenKill(node, gen, kill, conditional);
    LiveVariableLattice result = new LiveVariableLattice(input);
    // L_in = L_out - Kill + Gen
    result.liveSet.andNot(kill);
    result.liveSet.or(gen);
    return result;
  }

  /**
   * Computes the GEN and KILL set.
   *
   * @param n Root node.
   * @param gen Local variables that are live because of the instruction at
   *        {@code n} will be added to this set.
   * @param kill Local variables that are killed because of the instruction at
   *        {@code n} will be added to this set.
   * @param conditional {@code true} if any assignments encountered are
   *        conditionally executed. These assignments might not kill a variable.
   */
  private void computeGenKill(Node n, BitSet gen, BitSet kill,
      boolean conditional) {

    switch (n.getType()) {
      case Token.SCRIPT:
      case Token.BLOCK:
      case Token.FUNCTION:
        return;

      case Token.WHILE:
      case Token.DO:
      case Token.IF:
        computeGenKill(NodeUtil.getConditionExpression(n), gen, kill,
            conditional);
        return;

      case Token.FOR:
        if (!NodeUtil.isForIn(n)) {
          computeGenKill(NodeUtil.getConditionExpression(n), gen, kill,
              conditional);
        } else {
          // for(x in y) {...}
          Node lhs = n.getFirstChild();
          Node rhs = lhs.getNext();
          if (lhs.isVar()) {
            // for(var x in y) {...}
            lhs = lhs.getLastChild();
          }

          if (lhs.isName()) {
            addToSetIfLocal(lhs, kill);
            addToSetIfLocal(lhs, gen);
          } else {
            computeGenKill(lhs, gen, kill, conditional);
          }

          // rhs is executed only once so we don't go into it every loop.
        }
        return;

      case Token.VAR:
        for (Node c = n.getFirstChild(); c != null; c = c.getNext()) {
          if (c.hasChildren()) {
            computeGenKill(c.getFirstChild(), gen, kill, conditional);
            if (!conditional) {
              addToSetIfLocal(c, kill);
            }
          }
        }
        return;

      case Token.AND:
      case Token.OR:
        computeGenKill(n.getFirstChild(), gen, kill, conditional);
        // May short circuit.
        computeGenKill(n.getLastChild(), gen, kill, true);
        return;

      case Token.HOOK:
        computeGenKill(n.getFirstChild(), gen, kill, conditional);
        // Assume both sides are conditional.
        computeGenKill(n.getFirstChild().getNext(), gen, kill, true);
        computeGenKill(n.getLastChild(), gen, kill, true);
        return;

      case Token.NAME:
        if (isArgumentsName(n)) {
          markAllParametersEscaped();
        } else {
          addToSetIfLocal(n, gen);
        }
        return;

      default:
        if (NodeUtil.isAssignmentOp(n) && n.getFirstChild().isName()) {
          Node lhs = n.getFirstChild();
          if (!conditional) {
            addToSetIfLocal(lhs, kill);
          }
          if (!n.isAssign()) {
            // assignments such as a += 1 reads a.
            addToSetIfLocal(lhs, gen);
          }
          computeGenKill(lhs.getNext(), gen, kill, conditional);
        } else {
          for (Node c = n.getFirstChild(); c != null; c = c.getNext()) {
            computeGenKill(c, gen, kill, conditional);
          }
        }
        return;
    }
  }

  private void addToSetIfLocal(Node node, BitSet set) {
    Preconditions.checkState(node.isName());
    String name = node.getString();
    if (!jsScope.isDeclared(name, false)) {
      return;
    }
    Var var = jsScope.getVar(name);
    if (!escaped.contains(var)) {
      set.set(var.index);
    }
  }

  /**
   * Give up computing liveness of formal parameter by putting all the parameter
   * names in the escaped set.
   */
  void markAllParametersEscaped() {
    Node lp = jsScope.getRootNode().getFirstChild().getNext();
    for(Node arg = lp.getFirstChild(); arg != null; arg = arg.getNext()) {
      escaped.add(jsScope.getVar(arg.getString()));
    }
  }

  private boolean isArgumentsName(Node n) {
    if (!n.isName()||
        !n.getString().equals(ARGUMENT_ARRAY_ALIAS) ||
        jsScope.isDeclared(ARGUMENT_ARRAY_ALIAS, false)) {
      return false;
    } else {
      return true;
    }
  }
}
