// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
 * @param {string} content
 */
export function javaScriptOutline(content) {
  const chunkSize = 100000;
  let outlineChunk = [];
  let lastReportedOffset = 0;

  let ast;
  try {
    ast = acorn.parse(content, {ranges: false});
  } catch (e) {
    ast = acorn.loose.parse(content, {ranges: false});
  }

  const textCursor = new TextUtils.TextCursor(content.computeLineEndings());
  const walker = new FormatterWorker.ESTreeWalker(beforeVisit);
  walker.walk(ast);
  postMessage({chunk: outlineChunk, isLastChunk: true});

  /**
   * @param {!ESTree.Node} node
   */
  function beforeVisit(node) {
    if (node.type === 'ClassDeclaration') {
      reportClass(/** @type {!ESTree.Node} */ (node.id));
    } else if (node.type === 'VariableDeclarator' && isClassNode(node.init)) {
      reportClass(/** @type {!ESTree.Node} */ (node.id));
    } else if (node.type === 'AssignmentExpression' && isNameNode(node.left) && isClassNode(node.right)) {
      reportClass(/** @type {!ESTree.Node} */ (node.left));
    } else if (node.type === 'Property' && isNameNode(node.key) && isClassNode(node.value)) {
      reportClass(/** @type {!ESTree.Node} */ (node.key));
    } else if (node.type === 'FunctionDeclaration') {
      reportFunction(/** @type {!ESTree.Node} */ (node.id), node);
    } else if (node.type === 'VariableDeclarator' && isFunctionNode(node.init)) {
      reportFunction(/** @type {!ESTree.Node} */ (node.id), /** @type {!ESTree.Node} */ (node.init));
    } else if (node.type === 'AssignmentExpression' && isNameNode(node.left) && isFunctionNode(node.right)) {
      reportFunction(/** @type {!ESTree.Node} */ (node.left), /** @type {!ESTree.Node} */ (node.right));
    } else if (
        (node.type === 'MethodDefinition' || node.type === 'Property') && isNameNode(node.key) &&
        isFunctionNode(node.value)) {
      const namePrefix = [];
      if (node.kind === 'get' || node.kind === 'set') {
        namePrefix.push(node.kind);
      }
      if (node.static) {
        namePrefix.push('static');
      }
      reportFunction(node.key, node.value, namePrefix.join(' '));
    }
  }

  /**
   * @param {!ESTree.Node} nameNode
   */
  function reportClass(nameNode) {
    const name = 'class ' + stringifyNameNode(nameNode);
    textCursor.advance(nameNode.start);
    addOutlineItem({
      name: name,
      line: textCursor.lineNumber(),
      column: textCursor.columnNumber(),
    });
  }

  /**
   * @param {!ESTree.Node} nameNode
   * @param {!ESTree.Node} functionNode
   * @param {string=} namePrefix
   */
  function reportFunction(nameNode, functionNode, namePrefix) {
    let name = stringifyNameNode(nameNode);
    if (functionNode.generator) {
      name = '*' + name;
    }
    if (namePrefix) {
      name = namePrefix + ' ' + name;
    }
    if (functionNode.async) {
      name = 'async ' + name;
    }

    textCursor.advance(nameNode.start);
    addOutlineItem({
      name: name,
      line: textCursor.lineNumber(),
      column: textCursor.columnNumber(),
      arguments: stringifyArguments(/** @type {!Array<!ESTree.Node>} */ (functionNode.params))
    });
  }

  /**
   * @param {(!ESTree.Node|undefined)} node
   * @return {boolean}
   */
  function isNameNode(node) {
    if (!node) {
      return false;
    }
    if (node.type === 'MemberExpression') {
      return !node.computed && node.property.type === 'Identifier';
    }
    return node.type === 'Identifier';
  }

  /**
   * @param {(!ESTree.Node|undefined)} node
   * @return {boolean}
   */
  function isFunctionNode(node) {
    if (!node) {
      return false;
    }
    return node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression';
  }

  /**
   * @param {(!ESTree.Node|undefined)} node
   * @return {boolean}
   */
  function isClassNode(node) {
    return !!node && node.type === 'ClassExpression';
  }

  /**
   * @param {!ESTree.Node} node
   * @return {string}
   */
  function stringifyNameNode(node) {
    if (node.type === 'MemberExpression') {
      node = /** @type {!ESTree.Node} */ (node.property);
    }
    console.assert(node.type === 'Identifier', 'Cannot extract identifier from unknown type: ' + node.type);
    return /** @type {string} */ (node.name);
  }

  /**
   * @param {!Array<!ESTree.Node>} params
   * @return {string}
   */
  function stringifyArguments(params) {
    const result = [];
    for (const param of params) {
      if (param.type === 'Identifier') {
        result.push(param.name);
      } else if (param.type === 'RestElement' && param.argument.type === 'Identifier') {
        result.push('...' + param.argument.name);
      } else {
        console.error('Error: unexpected function parameter type: ' + param.type);
      }
    }
    return '(' + result.join(', ') + ')';
  }

  /**
   * @param {{name: string, line: number, column: number, arguments: (string|undefined)}} item
   */
  function addOutlineItem(item) {
    outlineChunk.push(item);
    if (textCursor.offset() - lastReportedOffset < chunkSize) {
      return;
    }
    postMessage({chunk: outlineChunk, isLastChunk: false});
    outlineChunk = [];
    lastReportedOffset = textCursor.offset();
  }
}

/* Legacy exported object */
self.FormatterWorker = self.FormatterWorker || {};

/* Legacy exported object */
FormatterWorker = FormatterWorker || {};

FormatterWorker.javaScriptOutline = javaScriptOutline;
