/**
 * Copyright 2013 Facebook, Inc.
 *
 * 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.
 */
/*global exports:true*/
/*jslint node: true*/
"use strict";

/**
 * Syntax transfomer for javascript. Takes the source in, spits the source
 * out. Tries to maintain readability and preserve whitespace and line numbers
 * where posssible.
 *
 * Support
 * - ES6 class transformation + private property munging, see ./classes.js
 * - React XHP style syntax transformations, see ./react.js
 * - Bolt XHP style syntax transformations, see ./bolt.js
 *
 * The general flow is the following:
 * - Parse the source with our customized esprima-parser
 *   https://github.com/voloko/esprima. We have to customize the parser to
 *   support non-standard XHP-style syntax. We parse the source range: true
 *   option that forces esprima to return positions in the source within
 *   resulting parse tree.
 *
 * - Traverse resulting syntax tree, trying to apply a set of visitors to each
 *   node. Each visitor should provide a .test() function that tests if the
 *   visitor can process a given node.
 *
 * - Visitor is responsible for code generation for a given syntax node.
 *   Generated code is stored in state.g.buffer that is passed to every
 *   visitor. It's up to the visitor to process the code the way it sees fit.
 *   All of the current visitors however use both the node and the original
 *   source to generate transformed code. They use nodes to generate new
 *   code and they copy the original source, preserving whitespace and comments,
 *   for the parts they don't care about.
 */
var esprima = require('esprima');

var createState = require('./utils').createState;
var catchup = require('./utils').catchup;

/**
 * @param {object} object
 * @param {function} visitor
 * @param {array} path
 * @param {object} state
 */
function traverse(object, path, state) {
  var key, child;

  if (walker(traverse, object, path, state) === false) {
    return;
  }
  path.unshift(object);
  for (key in object) {
    // skip obviously wrong attributes
    if (key === 'range' || key === 'loc') {
      continue;
    }
    if (object.hasOwnProperty(key)) {
      child = object[key];
      if (typeof child === 'object' && child !== null) {
        child.range && catchup(child.range[0], state);
        traverse(child, path, state);
        child.range && catchup(child.range[1], state);
      }
    }
  }
  path.shift();
}

function walker(traverse, object, path, state) {
  var visitors = state.g.visitors;
  for (var i = 0; i < visitors.length; i++) {
    if (visitors[i].test(object, path, state)) {
      return visitors[i](traverse, object, path, state);
    }
  }
}

function runPass(source, visitors, options) {
  var ast = esprima.parse(source, { comment: true, loc: true, range: true });
  var state = createState(source, options);
  state.g.originalProgramAST = ast;
  state.g.visitors = visitors;

  if (options.sourceMap) {
    var SourceMapGenerator = require('source-map').SourceMapGenerator;
    state.g.sourceMap = new SourceMapGenerator({ file: 'transformed.js' });
  }
  traverse(ast, [], state);
  catchup(source.length, state);
  return state;
}

/**
 * Applies all available transformations to the source
 * @param {array} visitors
 * @param {string} source
 * @param {?object} options
 * @return {object}
 */
function transform(visitors, source, options) {
  options = options || {};

  var state = runPass(source, visitors, options);
  var sourceMap = state.g.sourceMap;

  if (sourceMap) {
    return {
      sourceMap: sourceMap,
      sourceMapFilename: options.filename || 'source.js',
      code: state.g.buffer
    };
  } else {
    return {
      code: state.g.buffer
    };
  }
}


exports.transform = transform;
