/**
 * @fileoverview A collection of helper functions.
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */
"use strict";

var escope = require("escope");
var espree = require("espree");
var path = require("path");

var regexes = [
  /^(?:Cu|Components\.utils)\.import\(".*\/(.*?)\.jsm?"\);?$/,
  /^loader\.lazyImporter\(\w+, "(\w+)"/,
  /^loader\.lazyRequireGetter\(\w+, "(\w+)"/,
  /^loader\.lazyServiceGetter\(\w+, "(\w+)"/,
  /^XPCOMUtils\.defineLazyModuleGetter\(\w+, "(\w+)"/,
  /^loader\.lazyGetter\(\w+, "(\w+)"/,
  /^XPCOMUtils\.defineLazyGetter\(\w+, "(\w+)"/,
  /^XPCOMUtils\.defineLazyServiceGetter\(\w+, "(\w+)"/
];

module.exports = {
  /**
   * Gets the abstract syntax tree (AST) of the JavaScript source code contained
   * in sourceText.
   *
   * @param  {String} sourceText
   *         Text containing valid JavaScript.
   *
   * @return {Object}
   *         The resulting AST.
   */
  getAST: function(sourceText) {
    // Use a permissive config file to allow parsing of anything that Espree
    // can parse.
    var config = this.getPermissiveConfig();

    return espree.parse(sourceText, config);
  },

  /**
   * Gets the source text of an AST node.
   *
   * @param  {ASTNode} node
   *         The AST node representing the source text.
   * @param  {ASTContext} context
   *         The current context.
   *
   * @return {String}
   *         The source text representing the AST node.
   */
  getSource: function(node, context) {
    return context.getSource(node).replace(/[\r\n]+\s*/g, " ")
                                  .replace(/\s*=\s*/g, " = ")
                                  .replace(/\s+\./g, ".")
                                  .replace(/,\s+/g, ", ")
                                  .replace(/;\n(\d+)/g, ";$1")
                                  .replace(/\s+/g, " ");
  },

  /**
   * Gets the variable name from an import source
   * e.g. Cu.import("path/to/someName") will return "someName."
   *
   * Some valid input strings:
   *  - Cu.import("resource://devtools/client/shared/widgets/ViewHelpers.jsm");
   *  - loader.lazyImporter(this, "name1");
   *  - loader.lazyRequireGetter(this, "name2"
   *  - loader.lazyServiceGetter(this, "name3"
   *  - XPCOMUtils.defineLazyModuleGetter(this, "setNamedTimeout", ...)
   *  - loader.lazyGetter(this, "toolboxStrings"
   *  - XPCOMUtils.defineLazyGetter(this, "clipboardHelper"
   *
   * @param  {String} source
   *         The source representing an import statement.
   *
   * @return {String}
   *         The variable name imported.
   */
  getVarNameFromImportSource: function(source) {
    for (var i = 0; i < regexes.length; i++) {
      var regex = regexes[i];
      var matches = source.match(regex);

      if (matches) {
        var name = matches[1];

        return name;
      }
    }
  },

  /**
   * Get an array of globals from an AST.
   *
   * @param  {Object} ast
   *         The AST for which the globals are to be returned.
   *
   * @return {Array}
   *         An array of variable names.
   */
  getGlobals: function(ast) {
    var scopeManager = escope.analyze(ast);
    var globalScope = scopeManager.acquire(ast);
    var result = [];

    for (var variable in globalScope.variables) {
      var name = globalScope.variables[variable].name;
      result.push(name);
    }

    return result;
  },

  /**
   * Add a variable to the current scope.
   * HACK: This relies on eslint internals so it could break at any time.
   *
   * @param {String} name
   *        The variable name to add to the current scope.
   * @param {ASTContext} context
   *        The current context.
   */
  addVarToScope: function(name, context) {
    var scope = context.getScope();
    var variables = scope.variables;
    var variable = new escope.Variable(name, scope);

    variable.eslintExplicitGlobal = false;
    variable.writeable = true;
    variables.push(variable);

    // Since eslint 1.10.3, scope variables are now duplicated in the scope.set
    // map, so we need to store them there too if it exists.
    // See https://groups.google.com/forum/#!msg/eslint/Y4_oHMWwP-o/5S57U8jXd8kJ
    if (scope.set) {
      scope.set.set(name, variable);
    }
  },

  /**
   * Get the single line text represented by a particular AST node.
   *
   * @param  {ASTNode} node
   *         The AST node representing the source text.
   * @param  {String} text
   *         The text representing the AST node.
   *
   * @return {String}
   *         A single line version of the string represented by node.
   */
  getTextForNode: function(node, text) {
    var source = text.substr(node.range[0], node.range[1] - node.range[0]);

    return source.replace(/[\r\n]+\s*/g, "")
                 .replace(/\s*=\s*/g, " = ")
                 .replace(/\s+\./g, ".")
                 .replace(/,\s+/g, ", ")
                 .replace(/;\n(\d+)/g, ";$1");
  },

  /**
   * To allow espree to parse almost any JavaScript we need as many features as
   * possible turned on. This method returns that config.
   *
   * @return {Object}
   *         Espree compatible permissive config.
   */
  getPermissiveConfig: function() {
    return {
      range: true,
      loc: true,
      tolerant: true,
      ecmaFeatures: {
        arrowFunctions: true,
        binaryLiterals: true,
        blockBindings: true,
        classes: true,
        defaultParams: true,
        destructuring: true,
        forOf: true,
        generators: true,
        globalReturn: true,
        modules: true,
        objectLiteralComputedProperties: true,
        objectLiteralDuplicateProperties: true,
        objectLiteralShorthandMethods: true,
        objectLiteralShorthandProperties: true,
        octalLiterals: true,
        regexUFlag: true,
        regexYFlag: true,
        restParams: true,
        spread: true,
        superInFunctions: true,
        templateStrings: true,
        unicodeCodePointEscapes: true,
      }
    };
  },

  /**
   * Check whether the context is the global scope.
   *
   * @param {ASTContext} context
   *        The current context.
   *
   * @return {Boolean}
   *         True or false
   */
  getIsGlobalScope: function(context) {
    var ancestors = context.getAncestors();
    var parent = ancestors.pop();

    if (parent.type == "ExpressionStatement") {
      parent = ancestors.pop();
    }

    return parent.type == "Program";
  },

  /**
   * Check whether we are in a browser mochitest.
   *
   * @param  {RuleContext} scope
   *         You should pass this from within a rule
   *         e.g. helpers.getIsBrowserMochitest(this)
   *
   * @return {Boolean}
   *         True or false
   */
  getIsBrowserMochitest: function(scope) {
    var pathAndFilename = scope.getFilename();

    return /.*[\\/]browser_.+\.js$/.test(pathAndFilename);
  },

  /**
   * ESLint may be executed from various places: from mach, at the root of the
   * repository, or from a directory in the repository when, for instance,
   * executed by a text editor's plugin.
   * The value returned by context.getFileName() varies because of this.
   * This helper function makes sure to return an absolute file path for the
   * current context, by looking at process.cwd().
   * @param {Context} context
   * @return {String} The absolute path
   */
  getAbsoluteFilePath: function(context) {
    var fileName = context.getFilename();
    var cwd = process.cwd();

    if (path.isAbsolute(fileName)) {
      // Case 2: executed from the repo's root with mach:
      //   fileName: /path/to/mozilla/repo/a/b/c/d.js
      //   cwd: /path/to/mozilla/repo
      return fileName;
    } else {
      // Case 1: executed form in a nested directory, e.g. from a text editor:
      //   fileName: a/b/c/d.js
      //   cwd: /path/to/mozilla/repo/a/b/c
      var dirName = path.dirname(fileName);
      return cwd.slice(0, cwd.length - dirName.length) + fileName;
    }
  }
};
