text: 'use strict'
  
  const acorn = require('acorn')
  const jsx = require('acorn-jsx')
  const commentAttachment = require('./comment-attachment')
  const TokenTranslator = require('./token-translator')
  
  const DEFAULT_ECMA_VERSION = 5
  const STATE = Symbol("espree's internal state")
  const ESPRIMA_FINISH_NODE = Symbol("espree's esprimaFinishNode")
  const tokTypes = Object.assign({}, acorn.tokTypes, jsx.tokTypes)
  
  /**
   * Normalize ECMAScript version from the initial config
   * @param {number} ecmaVersion ECMAScript version from the initial config
   * @returns {number} normalized ECMAScript version
   */
  function normalizeEcmaVersion (ecmaVersion) {
    if (typeof ecmaVersion === 'number') {
      let version = ecmaVersion
  
      // Calculate ECMAScript edition number from official year version starting with
      // ES2015, which corresponds with ES6 (or a difference of 2009).
      if (version >= 2015) {
        version -= 2009
      }
  
      switch (version) {
        case 3:
        case 5:
        case 6:
        case 7:
        case 8:
        case 9:
        case 10:
          return version
  
        default:
          throw new Error('Invalid ecmaVersion.')
      }
    } else {
      return DEFAULT_ECMA_VERSION
    }
  }
  
  /**
   * Converts an Acorn comment to a Esprima comment.
   * @param {boolean} block True if it's a block comment, false if not.
   * @param {string} text The text of the comment.
   * @param {int} start The index at which the comment starts.
   * @param {int} end The index at which the comment ends.
   * @param {Location} startLoc The location at which the comment starts.
   * @param {Location} endLoc The location at which the comment ends.
   * @returns {Object} The comment object.
   * @private
   */
  function convertAcornCommentToEsprimaComment (block, text, start, end, startLoc, endLoc) {
    const comment = {
      type: block ? 'Block' : 'Line',
      value: text
    }
  
    if (typeof start === 'number') {
      comment.start = start
      comment.end = end
      comment.range = [start, end]
    }
  
    if (typeof startLoc === 'object') {
      comment.loc = {
        start: startLoc,
        end: endLoc
      }
    }
  
    return comment
  }
  
  module.exports = () => Parser => class Espree extends Parser {
    constructor (options, code) {
      if (typeof options !== 'object' || options === null) {
        options = {}
      }
      if (typeof code !== 'string' && !(code instanceof String)) {
        code = String(code)
      }
  
      const ecmaFeatures = options.ecmaFeatures || {}
      const ecmaVersion = normalizeEcmaVersion(options.ecmaVersion)
      const isModule = options.sourceType === 'module'
      const tokenTranslator =
              options.tokens === true
                ? new TokenTranslator(tokTypes, code)
                : null
  
      // Initialize acorn parser.
      super({
        ecmaVersion: isModule ? Math.max(6, ecmaVersion) : ecmaVersion,
        sourceType: isModule ? 'module' : 'script',
        ranges: options.range === true || options.attachComment === true,
        locations: options.loc === true,
  
        // Truthy value is true for backward compatibility.
        allowReturnOutsideFunction: Boolean(ecmaFeatures.globalReturn),
  
        // Collect tokens
        onToken: (token) => {
          if (tokenTranslator) {
            // Use `tokens`, `ecmaVersion`, and `jsxAttrValueToken` in the state.
            tokenTranslator.onToken(token, this[STATE])
          }
          if (token.type !== tokTypes.eof) {
            this[STATE].lastToken = token
          }
        },
  
        // Collect comments
        onComment: (block, text, start, end, startLoc, endLoc) => {
          if (this[STATE].comments) {
            const comment = convertAcornCommentToEsprimaComment(block, text, start, end, startLoc, endLoc)
            this[STATE].comments.push(comment)
  
            if (options.attachComment === true) {
              commentAttachment.addComment(comment)
            }
          }
        }
      }, code)
  
      // TODO: remove global state.
      commentAttachment.reset()
  
      // Initialize internal state.
      this[STATE] = {
        tokens: tokenTranslator ? [] : null,
        comments: options.comment === true || options.attachComment === true ? [] : null,
        attachComment: options.attachComment === true,
        impliedStrict: ecmaFeatures.impliedStrict === true && this.options.ecmaVersion >= 5,
        ecmaVersion: this.options.ecmaVersion,
        jsxAttrValueToken: false,
        lastToken: null
      }
    }
  
    tokenize () {
      do {
        this.next()
      } while (this.type !== tokTypes.eof)
  
      const extra = this[STATE]
      const tokens = extra.tokens
  
      if (extra.comments) {
        tokens.comments = extra.comments
      }
  
      return tokens
    }
  
    finishNode (...args) {
      const result = super.finishNode(...args)
      return this[ESPRIMA_FINISH_NODE](result)
    }
  
    finishNodeAt (...args) {
      const result = super.finishNodeAt(...args)
      return this[ESPRIMA_FINISH_NODE](result)
    }
  
    parse () {
      const extra = this[STATE]
      const program = super.parse()
  
      program.sourceType = this.options.sourceType
  
      if (extra.comments) {
        program.comments = extra.comments
      }
      if (extra.tokens) {
        program.tokens = extra.tokens
      }
  
      /*
           * Adjust opening and closing position of program to match Esprima.
           * Acorn always starts programs at range 0 whereas Esprima starts at the
           * first AST node's start (the only real difference is when there's leading
           * whitespace or leading comments). Acorn also counts trailing whitespace
           * as part of the program whereas Esprima only counts up to the last token.
           */
      if (program.range) {
        program.range[0] = program.body.length ? program.body[0].range[0] : program.range[0]
        program.range[1] = extra.lastToken ? extra.lastToken.range[1] : program.range[1]
      }
      if (program.loc) {
        program.loc.start = program.body.length ? program.body[0].loc.start : program.loc.start
        program.loc.end = extra.lastToken ? extra.lastToken.loc.end : program.loc.end
      }
  
      return program
    }
  
    parseTopLevel (node) {
      if (this[STATE].impliedStrict) {
        this.strict = true
      }
      return super.parseTopLevel(node)
    }
  
    /**
       * Overwrites the default raise method to throw Esprima-style errors.
       * @param {int} pos The position of the error.
       * @param {string} message The error message.
       * @throws {SyntaxError} A syntax error.
       * @returns {void}
       */
    raise (pos, message) {
      const loc = acorn.getLineInfo(this.input, pos)
      const err = new SyntaxError(message)
      err.index = pos
      err.lineNumber = loc.line
      err.column = loc.column + 1 // acorn uses 0-based columns
      throw err
    }
  
    /**
       * Overwrites the default raise method to throw Esprima-style errors.
       * @param {int} pos The position of the error.
       * @param {string} message The error message.
       * @throws {SyntaxError} A syntax error.
       * @returns {void}
       */
    raiseRecoverable (pos, message) {
      this.raise(pos, message)
    }
  
    /**
       * Overwrites the default unexpected method to throw Esprima-style errors.
       * @param {int} pos The position of the error.
       * @throws {SyntaxError} A syntax error.
       * @returns {void}
       */
    unexpected (pos) {
      let message = 'Unexpected token'
  
      if (pos !== null && pos !== void 0) {
        this.pos = pos
  
        if (this.options.locations) {
          while (this.pos < this.lineStart) {
            this.lineStart = this.input.lastIndexOf('\n', this.lineStart - 2) + 1
            --this.curLine
          }
        }
  
        this.nextToken()
      }
  
      if (this.end > this.start) {
        message += ' ' + this.input.slice(this.start, this.end)
      }
  
      this.raise(this.start, message)
    }
  
    /*
       * Esprima-FB represents JSX strings as tokens called "JSXText", but Acorn-JSX
       * uses regular tt.string without any distinction between this and regular JS
       * strings. As such, we intercept an attempt to read a JSX string and set a flag
       * on extra so that when tokens are converted, the next token will be switched
       * to JSXText via onToken.
       */
    jsx_readString (quote) { // eslint-disable-line camelcase
      const result = super.jsx_readString(quote)
      if (this.type === tokTypes.string) {
        this[STATE].jsxAttrValueToken = true
      }
      return result
    }
  
    /**
       * Performs last-minute Esprima-specific compatibility checks and fixes.
       * @param {ASTNode} result The node to check.
       * @returns {ASTNode} The finished node.
       */
    [ESPRIMA_FINISH_NODE] (result) {
      // Acorn doesn't count the opening and closing backticks as part of templates
      // so we have to adjust ranges/locations appropriately.
      if (result.type === 'TemplateElement') {
        // additional adjustment needed if ${ is the last token
        const terminalDollarBraceL = this.input.slice(result.end, result.end + 2) === '${'
  
        if (result.range) {
          result.range[0]--
          result.range[1] += (terminalDollarBraceL ? 2 : 1)
        }
  
        if (result.loc) {
          result.loc.start.column--
          result.loc.end.column += (terminalDollarBraceL ? 2 : 1)
        }
      }
  
      if (this[STATE].attachComment) {
        commentAttachment.processComment(result)
      }
  
      if (result.type.indexOf('Function') > -1 && !result.generator) {
        result.generator = false
      }
  
      return result
    }
  }
  