import Parser from "web-tree-sitter";

const TREE_SITTER_WASM_URL = new URL(
  "/node_modules/web-tree-sitter/tree-sitter.wasm",
  import.meta.url
);

const TREE_SITTER_DEVICETREE_WASM_URL = new URL(
  "/node_modules/tree-sitter-devicetree/tree-sitter-devicetree.wasm",
  import.meta.url
);

export let Devicetree: Parser.Language;

export async function initParser() {
  await Parser.init({
    locateFile: (path: string, prefix: string) => {
      // When locating tree-sitter.wasm, use a path that Webpack can map to the correct URL.
      if (path == "tree-sitter.wasm") {
        return TREE_SITTER_WASM_URL.href;
      }
      return prefix + path;
    },
  });
  Devicetree = await Parser.Language.load(TREE_SITTER_DEVICETREE_WASM_URL.href);
}

export function createParser() {
  if (!Devicetree) {
    throw new Error("Parser not loaded. Call initParser() first.");
  }

  const parser = new Parser();
  parser.setLanguage(Devicetree);
  return parser;
}

/**
 * Returns the node for the named capture.
 */
export function findCapture(name: string, captures: Parser.QueryCapture[]) {
  for (const c of captures) {
    if (c.name === name) {
      return c.node;
    }
  }

  return null;
}

/**
 * Returns whether the node for the named capture exists and has the given text.
 */
export function captureHasText(
  name: string,
  captures: Parser.QueryCapture[],
  text: string
) {
  const node = findCapture(name, captures);
  return node?.text === text;
}

/**
 * Get a list of SyntaxNodes representing a devicetree node with the given path.
 * The same node may be listed multiple times within a file.
 *
 * This function does not evaluate which node a reference points to, so given
 * a file containing "/ { foo: bar {}; }; &foo {};" searching for "&foo" will
 * return the "&foo {}" node but not "foo: bar {}".
 *
 * @param path Path to the node to find. May be an absolute path such as
 * "/foo/bar", a node reference such as "&foo", or a node reference followed by
 * a relative path such as "&foo/bar".
 */
export function findDevicetreeNode(
  tree: Parser.Tree,
  path: string
): Parser.SyntaxNode[] {
  const query = Devicetree.query("(node) @node");
  const matches = query.matches(tree.rootNode);

  const result: Parser.SyntaxNode[] = [];

  for (const { captures } of matches) {
    const node = findCapture("node", captures);

    if (node && getDevicetreeNodePath(node) === path) {
      result.push(node);
    }
  }

  return result;
}

export interface FindPropertyOptions {
  /** Search in children of the given node as well */
  recursive?: boolean;
}

/**
 * Find all instances of a devicetree property with the given name which are
 * descendants of the given syntax node.
 *
 * @param node Any syntax node
 */
export function findDevicetreeProperty(
  node: Parser.SyntaxNode,
  name: string,
  options: FindPropertyOptions & { recursive: true }
): Parser.SyntaxNode[];

/**
 * Find a devicetree node's property with the given name, or null if it doesn't
 * have one.
 *
 * @note If the node contains multiple instances of the same property, this
 * returns the last once, since that is the one that will actually be applied.
 *
 * @param node A syntax node for a devicetree node
 */
export function findDevicetreeProperty(
  node: Parser.SyntaxNode,
  name: string,
  options?: FindPropertyOptions
): Parser.SyntaxNode | null;

export function findDevicetreeProperty(
  node: Parser.SyntaxNode,
  name: string,
  options?: FindPropertyOptions
): Parser.SyntaxNode[] | Parser.SyntaxNode | null {
  const query = Devicetree.query(
    `(property name: (identifier) @name (#eq? @name "${name}")) @prop`
  );
  const matches = query.matches(node);
  const props = matches
    .map(({ captures }) => findCapture("prop", captures))
    .filter((node) => node !== null);

  if (options?.recursive) {
    return props;
  }

  // The query finds all descendants. Filter to just the properties that belong
  // to the given devicetree node.
  const childProps = props.filter((prop) =>
    getContainingDevicetreeNode(prop)?.equals(node)
  );

  // Sort in descending order to select the last instance of the property.
  childProps.sort((a, b) => b.startIndex - a.startIndex);
  return childProps[0] ?? null;
}

export function getDevicetreeNodePath(node: Parser.SyntaxNode | null) {
  const parts = getDevicetreeNodePathParts(node);

  if (parts.length === 0) {
    return "";
  }

  if (parts.length === 1) {
    return parts[0];
  }

  const path = parts.join("/");

  // The top-level node should be named "/", which is a special case since the
  // path should not start with "//".
  return parts[0] === "/" ? path.substring(1) : path;
}

function getDevicetreeNodePathParts(node: Parser.SyntaxNode | null): string[] {
  // There may be intermediate syntax nodes between devicetree nodes, such as
  // #if blocks, so if we aren't currently on a "node" node, traverse up the
  // tree until we find one.
  const dtnode = getContainingDevicetreeNode(node);
  if (!dtnode) {
    return [];
  }

  const name = dtnode.childForFieldName("name")?.text ?? "";

  return [...getDevicetreeNodePathParts(dtnode.parent), name];
}

export function getContainingDevicetreeNode(node: Parser.SyntaxNode | null) {
  while (node && node.type !== "node") {
    node = node.parent;
  }
  return node;
}
