/**
 * Copyright (c) 2014, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * https://raw.github.com/facebook/regenerator/master/LICENSE file. An
 * additional grant of patent rights can be found in the PATENTS file in
 * the same directory.
 */

var assert = require("assert");
var types = require("recast").types;
var n = types.namedTypes;
var b = types.builders;
var hasOwn = Object.prototype.hasOwnProperty;

// The hoist function takes a FunctionExpression or FunctionDeclaration
// and replaces any Declaration nodes in its body with assignments, then
// returns a VariableDeclaration containing just the names of the removed
// declarations.
exports.hoist = function(funPath) {
  assert.ok(funPath instanceof types.NodePath);
  n.Function.assert(funPath.value);

  var vars = {};

  function varDeclToExpr(vdec, includeIdentifiers) {
    n.VariableDeclaration.assert(vdec);
    var exprs = [];

    vdec.declarations.forEach(function(dec) {
      vars[dec.id.name] = dec.id;

      if (dec.init) {
        exprs.push(b.assignmentExpression(
          "=", dec.id, dec.init
        ));
      } else if (includeIdentifiers) {
        exprs.push(dec.id);
      }
    });

    if (exprs.length === 0)
      return null;

    if (exprs.length === 1)
      return exprs[0];

    return b.sequenceExpression(exprs);
  }

  types.visit(funPath.get("body"), {
    visitVariableDeclaration: function(path) {
      var expr = varDeclToExpr(path.value, false);
      if (expr === null) {
        path.replace();
      } else {
        // We don't need to traverse this expression any further because
        // there can't be any new declarations inside an expression.
        return b.expressionStatement(expr);
      }

      // Since the original node has been either removed or replaced,
      // avoid traversing it any further.
      return false;
    },

    visitForStatement: function(path) {
      var init = path.value.init;
      if (n.VariableDeclaration.check(init)) {
        path.get("init").replace(varDeclToExpr(init, false));
      }
      this.traverse(path);
    },

    visitForInStatement: function(path) {
      var left = path.value.left;
      if (n.VariableDeclaration.check(left)) {
        path.get("left").replace(varDeclToExpr(left, true));
      }
      this.traverse(path);
    },

    visitFunctionDeclaration: function(path) {
      var node = path.value;
      vars[node.id.name] = node.id;

      var parentNode = path.parent.node;
      var assignment = b.expressionStatement(
        b.assignmentExpression(
          "=",
          node.id,
          b.functionExpression(
            node.id,
            node.params,
            node.body,
            node.generator,
            node.expression
          )
        )
      );

      if (n.BlockStatement.check(path.parent.node)) {
        // Insert the assignment form before the first statement in the
        // enclosing block.
        path.parent.get("body").unshift(assignment);

        // Remove the function declaration now that we've inserted the
        // equivalent assignment form at the beginning of the block.
        path.replace();

      } else {
        // If the parent node is not a block statement, then we can just
        // replace the declaration with the equivalent assignment form
        // without worrying about hoisting it.
        path.replace(assignment);
      }

      // Don't hoist variables out of inner functions.
      return false;
    },

    visitFunctionExpression: function(path) {
      // Don't descend into nested function expressions.
      return false;
    }
  });

  var paramNames = {};
  funPath.get("params").each(function(paramPath) {
    var param = paramPath.value;
    if (n.Identifier.check(param)) {
      paramNames[param.name] = param;
    } else {
      // Variables declared by destructuring parameter patterns will be
      // harmlessly re-declared.
    }
  });

  var declarations = [];

  Object.keys(vars).forEach(function(name) {
    if (!hasOwn.call(paramNames, name)) {
      declarations.push(b.variableDeclarator(vars[name], null));
    }
  });

  if (declarations.length === 0) {
    return null; // Be sure to handle this case!
  }

  return b.variableDeclaration("var", declarations);
};
