var esprima = require('esprima'),
    estraverse = require('estraverse');

/**
 * @param {!Object} node An Esprima node
 * @returns {boolean} Whether the given node represents a block statement
 */
function isBlockStatement(node) {
  return node.type === 'BlockStatement';
}

/**
 * @param {!Object} node An Esprima node
 * @returns {boolean} Whether the given node represents a define() call
 */
function isDefineCall(node) {
  return node.type === 'CallExpression'
      && node.callee.type === 'Identifier'
      && node.callee.name === 'define';
}

/**
 * @param {!Object} node An Esprima node
 * @returns {boolean} Whether the given node represents a require() call
 */
function isRequireCall(node) {
  return node.type === 'CallExpression'
      && node.callee.type === 'Identifier'
      && node.callee.name === 'require';
}

/**
 * @param {string} code A string of JavaScript
 * @returns {!Object} An Esprima AST for the given JavaScript
 */
function parseAst(code) {
  return esprima.parse(code, {range: true});
}

/**
 * @param {string} code A string of JavaScript
 * @returns {string} {@code code} with {@code define()} and {@code require()}
 *     calls transformed into IIFEs
 */
function transform(code) {
  return new Transformer(code).transform();
}

/**
 * @constructor
 * @param {string} code A string of JavaScript
 */
function Transformer(code) {
  /**
   * The code being transformed
   *
   * @private
   * @type {string}
   */
  this.code = code;

  /**
   * The index in {@code code} up to which we have transformed
   *
   * @private
   * @type {number}
   */
  this.index = 0;

  /**
   * If we're waiting to enter a block statement
   *
   * i.e. Skipping define() or require() dependencies, function parameters, etc.
   *
   * @private
   * @type {boolean}
   */
  this.isWaitingForBlock = false;

  /**
   * The transformed code
   *
   * @private
   * @type {string}
   */
  this.result = '';
}

/**
 * @param {!Object} node An Esprima node
 * @private
 */
Transformer.prototype.onEnterNode = function (node) {
  if (isDefineCall(node) || isRequireCall(node)) {
    this.isWaitingForBlock = true;
    this.result += '(function () ';
  } else if (this.isWaitingForBlock) {
    if (isBlockStatement(node)) {
      this.index = node.range[0];
      this.isWaitingForBlock = false;
    }
  } else {
    this.result += this.code.substring(this.index, node.range[0]);
    this.index = node.range[0];
  }
};

/**
 * @param {!Object} node An Esprima node
 * @private
 */
Transformer.prototype.onLeaveNode = function (node) {
  if (isDefineCall(node) || isRequireCall(node)) {
    this.index = node.range[1];
    this.result += '())';
  } else if (!this.isWaitingForBlock) {
    this.result += this.code.substring(this.index, node.range[1]);
    this.index = node.range[1];
  }
};

/**
 * @return {string} The code passed to the constructor with {@code define()} and
 *     {@code require()} calls transformed into IIFEs
 */
Transformer.prototype.transform = function () {
  var ast = parseAst(this.code);

  estraverse.traverse(ast, {
    enter: this.onEnterNode.bind(this),
    leave: this.onLeaveNode.bind(this)
  });

  return this.result;
};

module.exports = transform;
