/* 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";
// These are all the current lezer node types used in the source editor,
// Add more here as are needed
const nodeTypes = {
  AssignmentExpression: "AssignmentExpression",
  FunctionExpression: "FunctionExpression",
  FunctionDeclaration: "FunctionDeclaration",
  ArrowFunction: "ArrowFunction",
  MethodDeclaration: "MethodDeclaration",
  ClassDeclaration: "ClassDeclaration",
  ClassExpression: "ClassExpression",
  Property: "Property",
  PropertyDeclaration: "PropertyDeclaration",
  PropertyDefinition: "PropertyDefinition",
  PrivatePropertyDefinition: "PrivatePropertyDefinition",
  MemberExpression: "MemberExpression",
  VariableDeclaration: "VariableDeclaration",
  VariableDefinition: "VariableDefinition",
  VariableName: "VariableName",
  this: "this",
  PropertyName: "PropertyName",
  Equals: "Equals",
  ParamList: "ParamList",
  Spread: "Spread",
  Number: "Number",
  Script: "Script",
  Block: "Block",
};

const functionsSet = new Set([
  nodeTypes.FunctionExpression,
  nodeTypes.FunctionDeclaration,
  nodeTypes.ArrowFunction,
  nodeTypes.MethodDeclaration,
]);

const nodeTypeSets = {
  functions: functionsSet,
  expressions: new Set([
    nodeTypes.MemberExpression,
    nodeTypes.VariableDefinition,
    nodeTypes.VariableName,
    nodeTypes.this,
    nodeTypes.PropertyName,
  ]),
  functionExpressions: new Set([
    nodeTypes.ArrowFunction,
    nodeTypes.FunctionExpression,
    nodeTypes.ParamList,
  ]),
  declarations: new Set([
    nodeTypes.MethodDeclaration,
    nodeTypes.PropertyDeclaration,
  ]),
  functionsDeclAndExpr: new Set([
    ...functionsSet,
    nodeTypes.Property,
    nodeTypes.PropertyDeclaration,
    nodeTypes.VariableDeclaration,
    nodeTypes.AssignmentExpression,
  ]),
  functionsVarDecl: new Set([
    ...functionsSet,
    // For anonymous functions we are using the variable name where the function is stored. See `getFunctionName`.
    nodeTypes.VariableDeclaration,
  ]),
  paramList: new Set([nodeTypes.ParamList]),
  variableDefinition: new Set([nodeTypes.VariableDefinition]),
  numberAndProperty: new Set([nodeTypes.PropertyDefinition, nodeTypes.Number]),
  memberExpression: new Set([nodeTypes.MemberExpression]),
  classes: new Set([nodeTypes.ClassDeclaration, nodeTypes.ClassExpression]),
  bindingReferences: new Set([
    nodeTypes.VariableDefinition,
    nodeTypes.VariableName,
    nodeTypes.PropertyName,
  ]),
  expressionProperty: new Set([nodeTypes.PropertyName]),
  variables: new Set([
    nodeTypes.VariableDefinition,
    nodeTypes.VariableDeclaration,
    nodeTypes.this,
  ]),
};

const ast = new Map();

/**
 * Checks if a node has children with any of the node types specified
 *
 * @param {object} node
 * @param {Set} types
 * @returns
 */
function hasChildNodeOfType(node, types) {
  let childNode = node.firstChild;
  while (childNode !== null) {
    if (types.has(childNode.name)) {
      return true;
    }
    childNode = childNode.nextSibling;
  }
  return false;
}

/**
 * Checks if a node has children with any of the node types specified
 *
 * @param {object} node
 * @param {Set} types
 * @returns
 */
function findChildNodeOfType(node, types) {
  let childNode = node.firstChild;
  while (childNode !== null) {
    if (types.has(childNode.name)) {
      return childNode;
    }
    childNode = childNode.nextSibling;
  }
  return null;
}

/**
 * Gets a cached tree or parses the the source content
 *
 * @param {object} parserLanguage - The language parser used to parse the source
 * @param {string} id - A unique identifier for the source
 * @param {string} content - The source text
 * @returns {Tree} - https://lezer.codemirror.net/docs/ref/#common.Tree
 */
function getTree(parserLanguage, id, content) {
  if (ast.has(id)) {
    return ast.get(id);
  }
  const tree = parserLanguage.parser.parse(content);
  ast.set(id, tree);
  return tree;
}

function clear() {
  ast.clear();
}

/**
 * Gets the node and the function name which immediately encloses the node (representing a location)
 *
 * @param {object} doc - The codemirror document used to retrive the part of content
 * @param {object} node - The parser syntax node https://lezer.codemirror.net/docs/ref/#common.SyntaxNode
 * @param {object} options
 * @param {boolean} options.includeAnonymousFunctions - if true, allow matching anonymous functions
 * @returns
 */
function getEnclosingFunction(
  doc,
  node,
  options = { includeAnonymousFunctions: false }
) {
  let parentNode = node.parent;
  while (parentNode !== null) {
    if (nodeTypeSets.functionsVarDecl.has(parentNode.name)) {
      // For anonymous functions, we use variable declarations, but we only care about variable declarations which are part of function expressions
      if (
        parentNode.name == nodeTypes.VariableDeclaration &&
        !hasChildNodeOfType(parentNode.node, nodeTypeSets.functionExpressions)
      ) {
        parentNode = parentNode.parent;
        continue;
      }
      const funcName = getFunctionName(doc, parentNode);
      if (funcName || options.includeAnonymousFunctions) {
        return {
          node: parentNode,
          funcName,
        };
      }
    }
    parentNode = parentNode.parent;
  }
  return null;
}

/**
 * Gets the parent scope node for the specified node.
 * If neither is found then we fallback to the script node for the source.
 *
 * @param {object} node
 * @param {string} scopeType - The scope type specifies what kind of scope node to look for.
 * The types are defined by the platform. See https://firefox-source-docs.mozilla.org/js/Debugger/Debugger.Environment.html#type
 * @returns {object | null} scope node or null if none is found
 */
function getParentScopeOfType(node, scopeType) {
  let parentNode = node.parent;
  let lastParentNode = parentNode;
  while (parentNode !== null) {
    if (scopeType == "block" || scopeType == "object") {
      if (parentNode.name == nodeTypes.Block) {
        return parentNode;
      }
    } else if (nodeTypeSets.functionsVarDecl.has(parentNode.name)) {
      if (
        parentNode.name == nodeTypes.VariableDeclaration &&
        !hasChildNodeOfType(parentNode.node, nodeTypeSets.functionExpressions)
      ) {
        parentNode = parentNode.parent;
        continue;
      }
      return parentNode;
    }
    lastParentNode = parentNode;
    parentNode = parentNode.parent;
  }
  // If no function node was found up to the root node
  if (lastParentNode?.name == nodeTypes.Script) {
    return lastParentNode;
  }
  return null;
}

/**
 * Gets the node at the specified location
 *
 * @param {object} doc - https://codemirror.net/docs/ref/#state.EditorState.doc
 * @param {object} tree - https://lezer.codemirror.net/docs/ref/#common.Tree
 * @param {object} location
 * @returns {object} node - https://lezer.codemirror.net/docs/ref/#common.SyntaxNodeRef
 */
function getTreeNodeAtLocation(doc, tree, location) {
  try {
    const line = doc.line(location.line);
    const pos = line.from + location.column;
    return tree.resolve(pos, 1);
  } catch (e) {
    // if the line is not found in the document doc.line() will throw
    console.warn(e.message);
  }
  return null;
}

/**
 * Converts Codemirror position to valid source location. Used only for CM6
 *
 * @param {object} doc - The Codemirror document used to retrive the part of content
 * @param {number} pos - Codemirror offset
 * @returns
 */
function positionToLocation(doc, pos) {
  if (pos == null) {
    return {
      line: null,
      column: null,
    };
  }
  const line = doc.lineAt(pos);
  return {
    line: line.number,
    column: pos - line.from,
  };
}

/**
 * Gets the name of the function if any exists, returns null
 * for anonymous functions.
 *
 * @param {object} doc - The codemirror document used to retrive the part of content
 * @param {object} node - The parser syntax node https://lezer.codemirror.net/docs/ref/#common.SyntaxNode
 * @returns {string | null}
 */
function getFunctionName(doc, node) {
  /**
   * Examples:
   *  - Gets `foo` in `class ESClass { foo(a, b) {}}`
   *  - Gets `bar` in `class ESClass { bar = function () {}}`
   *  - Gets `boo` in `class ESClass { boo = () => {}}`
   *  - Gets `#pfoo` in `class ESClass { #pfoo() => {}}`
   */
  if (
    node.name == nodeTypes.MethodDeclaration ||
    (node.name == nodeTypes.PropertyDeclaration &&
      hasChildNodeOfType(node, nodeTypeSets.functionExpressions))
  ) {
    const propDefNode = findChildNodeOfType(
      node,
      new Set([
        nodeTypes.PropertyDefinition,
        nodeTypes.PrivatePropertyDefinition,
      ])
    );

    if (!propDefNode) {
      return null;
    }
    return doc.sliceString(propDefNode.from, propDefNode.to);
  } else if (
    /**
     * Examples:
     *  - Gets `foo` in `let foo = function () {};`
     *  - Gets `bar` in `const bar = () => {}`
     */
    node.name == nodeTypes.VariableDeclaration &&
    hasChildNodeOfType(node, nodeTypeSets.functionExpressions)
  ) {
    const varDefNode = findChildNodeOfType(
      node,
      nodeTypeSets.variableDefinition
    );

    if (!varDefNode) {
      return null;
    }
    return doc.sliceString(varDefNode.from, varDefNode.to);
  } else if (
    /**
     * Examples:
     *  - Gets `Foo` in `function Foo() {} - FunctionDeclaration`
     *  - Gets `bar` in `function bar(a) {} - Functionexpression`
     */
    node.name == nodeTypes.FunctionDeclaration ||
    node.name == nodeTypes.FunctionExpression
  ) {
    const varDefNode = findChildNodeOfType(
      node,
      nodeTypeSets.variableDefinition
    );

    if (!varDefNode) {
      return null;
    }
    return doc.sliceString(varDefNode.from, varDefNode.to);
  } else if (
    /**
     * Examples:
     *  - Gets `foo` in `const a = { foo(a, ...b) {} }`
     *  - Gets `bar` in `const a = { bar: function () {} }`
     *  - Gets `bla` in `const a = { bla: () => {} }`
     *  - Gets `1234` in `const a = { 1234: () => {} }`
     */
    node.name == nodeTypes.Property &&
    hasChildNodeOfType(node, nodeTypeSets.functionExpressions)
  ) {
    const propDefNode = findChildNodeOfType(
      node,
      nodeTypeSets.numberAndProperty
    );

    if (!propDefNode) {
      return null;
    }
    return doc.sliceString(propDefNode.from, propDefNode.to);
  } else if (
    /**
     * Examples:
     *  - Gets `bar` in `const foo = {}; foo.bar = function() {}`
     *  - Gets `bla` in `const foo = {}; foo.bla = () => {}`
     */
    node.name == nodeTypes.AssignmentExpression &&
    hasChildNodeOfType(node, nodeTypeSets.functionExpressions)
  ) {
    const memExprDefNode = findChildNodeOfType(
      node,
      nodeTypeSets.memberExpression
    );

    if (!memExprDefNode) {
      return null;
    }
    // Get the rightmost part of the member expression i.e for a.b.c get c
    const exprParts = doc
      .sliceString(memExprDefNode.from, memExprDefNode.to)
      .split(".");
    return exprParts.at(-1);
  }

  return null;
}

/**
 * Gets the parameter names of the function as an array
 *
 * @param {object} doc - The codemirror document used to retrieve the part of content
 * @param {object} node - The parser syntax node https://lezer.codemirror.net/docs/ref/#common.SyntaxNode
 * @returns {Array}
 */
function getFunctionParameterNames(doc, node) {
  // Find the parameter list node

  let exprNode = node;

  if (
    // Example: Gets `(a)` in `const foo = {}; foo.bar = function(a) {}`
    node.name == nodeTypes.AssignmentExpression ||
    // Example: Gets `(a, b)` in `let foo = function (a, b) {};`
    node.name == nodeTypes.VariableDeclaration ||
    // Example: Gets `(x, y)` in `class ESClass { bar = function (x, y) {}}`
    node.name == nodeTypes.PropertyDeclaration ||
    // Example: Gets `(foo, ...bar)` in `const a = { foo: (foo, ...bar) {}}`
    (node.name == nodeTypes.Property &&
      !hasChildNodeOfType(node, nodeTypeSets.paramList))
  ) {
    exprNode = findChildNodeOfType(node, nodeTypeSets.functionExpressions);
  }

  /**
   * Others
   *  Function Declarations - Gets `(x, y)` in `function Foo(x, y) {}`
   *  Method Declarations - Gets `(a, b)` in `class ESClass { foo(a, b) {}}`
   */
  const paramListNode = findChildNodeOfType(exprNode, nodeTypeSets.paramList);
  if (paramListNode == null) {
    return [];
  }

  const names = [];
  let currNode = paramListNode.firstChild; // "("
  // Get all the parameter names
  while (currNode !== null && currNode.name !== ")") {
    if (currNode.name == nodeTypes.VariableDefinition) {
      // ignore spread operators i.e foo(...x)
      if (currNode.prevSibling?.name !== nodeTypes.Spread) {
        names.push(doc.sliceString(currNode.from, currNode.to));
      }
    }
    currNode = currNode.nextSibling;
  }
  return names;
}

function getFunctionClass(doc, node) {
  /**
   * Examples (Class Methods and Properties):
   *  Gets `ESClass` in `class ESClass { foo(a, b) {}}`
   *  Gets `ESClass` in `class ESClass { bar = function () {}}`
   */
  if (!nodeTypeSets.declarations.has(node.name)) {
    return null;
  }
  return doc.sliceString(
    node.parent.prevSibling.from,
    node.parent.prevSibling.to
  );
}

/**
 * Gets the meta data for member expression nodes
 *
 * @param {object} doc - The codemirror document used to retrieve the part of content
 * @param {object} node - The parser syntax node https://lezer.codemirror.net/docs/ref/#common.SyntaxNode
 * @returns
 */
function getMetaBindings(doc, node) {
  if (!node || node.name !== nodeTypes.MemberExpression) {
    return null;
  }

  const memExpr = doc.sliceString(node.from, node.to).split(".");
  return {
    type: "member",
    start: positionToLocation(doc, node.from),
    end: positionToLocation(doc, node.to),
    property: memExpr.at(-1),
    parent: getMetaBindings(doc, node.parent),
  };
}

/**
 * Walk the syntax tree of the langauge provided
 *
 * @param {object}   view - Codemirror view (https://codemirror.net/docs/ref/#view)
 * @param {object}   language - Codemirror Language (https://codemirror.net/docs/ref/#language)
 * @param {object}   options
 *        {Tree}     options.tree - https://lezer.codemirror.net/docs/ref/#common.Tree
 *        {Boolean}  options.forceParseTo - Force parsing the document up to a certain point.
 *        {Function} options.enterVisitor - A function that is called when a node is entered
 *        {Set}      options.filterSet - A set of node types which should be visited, all others should be ignored
 *        {Number}   options.walkFrom - Determine the location in the AST where the iteration of the syntax tree should start
 *        {Number}   options.walkTo - Determine the location in the AST where the iteration of the syntax tree should end
 */
async function walkTree(view, language, options) {
  const { forceParsing, syntaxTree } = language;
  if (options.forceParseTo && !options.tree) {
    // Force parsing the source, up to the end of the current viewport,
    // Also increasing the timeout threshold so we make sure
    // all required content is parsed (this is mostly needed for larger sources).
    await forceParsing(view, options.forceParseTo, 10000);
  }
  const tree = options.tree || syntaxTree(view.state);
  await tree.iterate({
    enter: node => {
      if (options.filterSet?.has(node.name)) {
        options.enterVisitor(node);
      }
    },
    from: options.walkFrom,
    to: options.walkTo,
  });
}

/**
 * This enables walking a specific part of the syntax tree using the cursor
 * provided by the node (which is the parent)
 *
 * @param {object} cursor - https://lezer.codemirror.net/docs/ref/#common.TreeCursor
 * @param {object} options
 *        {Function} options.enterVisitor - A function that is called when a node is entered
 *        {Set}      options.filterSet - A set of node types which should be visited, all others should be ignored
 */
async function walkCursor(cursor, options) {
  await cursor.iterate(node => {
    if (options.filterSet?.has(node.name)) {
      options.enterVisitor(node);
    }
  });
}

/**
 * Merge variables, arguments and child properties of member expressions
 * into a unique "bindings" objects where arguments overrides variables.
 *
 * @param {object} scopeBindings
 * @returns {object} bindings
 */
function getScopeBindings(scopeBindings) {
  const bindings = { ...scopeBindings.variables };
  scopeBindings.arguments.forEach(argument => {
    Object.keys(argument).forEach(key => {
      bindings[key] = argument[key];
    });
  });
  // Find and add child properties of member expressions as bindings
  for (const v in scopeBindings.variables) {
    const ownProps = scopeBindings.variables[v]?.value?.preview?.ownProperties;
    if (ownProps) {
      Object.keys(ownProps).forEach(k => {
        bindings[k] = ownProps[k];
      });
    }
  }
  return bindings;
}

module.exports = {
  getFunctionName,
  getFunctionParameterNames,
  getFunctionClass,
  getEnclosingFunction,
  getTreeNodeAtLocation,
  getMetaBindings,
  nodeTypes,
  nodeTypeSets,
  walkTree,
  getTree,
  clear,
  walkCursor,
  positionToLocation,
  getParentScopeOfType,
  getScopeBindings,
};
