// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import type * as Protocol from '../../generated/protocol.js';

import type {FragmentImpl, FrameImpl} from './StackTraceImpl.js';

export interface ParsedFrameInfo {
  readonly isAsync?: boolean;
  readonly isConstructor?: boolean;
  readonly isEval?: boolean;
  readonly evalOrigin?: RawFrame;
  readonly isWasm?: boolean;
  readonly wasmModuleName?: string;
  readonly wasmFunctionIndex?: number;
  readonly typeName?: string;
  readonly methodName?: string;
  readonly promiseIndex?: number;
}

/**
 * Intentionally very close to a {@link Protocol.Runtime.CallFrame} but with optional `scriptId`.
 */
export interface RawFrame {
  readonly scriptId?: Protocol.Runtime.ScriptId;
  readonly url?: string;
  readonly functionName?: string;
  readonly lineNumber: number;
  readonly columnNumber: number;

  readonly parsedFrameInfo?: ParsedFrameInfo;
}

/**
 * @returns whether the frame is a V8 builtin frame e.g. Array.map. Builtin frames
 * have neither source position nor script or URL. They only have a name.
 */
export function isBuiltinFrame(rawFrame: RawFrame): boolean {
  return rawFrame.lineNumber === -1 && rawFrame.columnNumber === -1 && !Boolean(rawFrame.scriptId) &&
      !Boolean(rawFrame.url);
}

interface FrameNodeBase<ChildT, ParentT> {
  readonly parent: ParentT;
  readonly children: ChildT[];
}

type RootFrameNode = FrameNodeBase<WeakRef<FrameNode>, null>;
type AnyFrameNode = FrameNode|RootFrameNode;

export class FrameNode implements FrameNodeBase<FrameNode, AnyFrameNode> {
  readonly parent: AnyFrameNode;
  readonly children: FrameNode[] = [];

  readonly rawFrame: RawFrame;
  frames: FrameImpl[] = [];

  fragment?: FragmentImpl;
  parsedFrameInfo?: ParsedFrameInfo;
  evalOriginFrames?: FrameImpl[];

  constructor(rawFrame: RawFrame, parent: AnyFrameNode) {
    this.rawFrame = rawFrame;
    this.parent = parent;
    this.parsedFrameInfo = rawFrame.parsedFrameInfo;
  }

  /**
   * Produces the ancestor chain. Including `this` but excluding the `RootFrameNode`.
   */
  * getCallStack(): Generator<FrameNode> {
    // The `RootFrameNode` doesn't have an actual frame attached, that's why we check for `node.parent` instead of `node`.
    for (let node: AnyFrameNode|null = this; node.parent; node = node.parent) {
      yield node;
    }
  }
}

/**
 * Stores stack trace fragments in a trie, but does not own them/keep them alive.
 */
export class Trie {
  readonly #root: RootFrameNode = {parent: null, children: []};

  /**
   * Most sources produce stack traces in "top-to-bottom" order, so that is what this method expects.
   *
   * @returns The {@link FrameNode} corresponding to the top-most stack frame.
   */
  insert(frames: RawFrame[]): FrameNode {
    if (frames.length === 0) {
      throw new Error('Trie.insert called with an empty frames array.');
    }

    let currentNode: AnyFrameNode = this.#root;
    for (let i = frames.length - 1; i >= 0; --i) {
      currentNode = this.#insert(currentNode, frames[i]);
    }
    return currentNode as FrameNode;
  }

  /**
   * Inserts `rawFrame` into the children of the provided node if not already there.
   *
   * @returns the child node corresponding to `rawFrame`.
   */
  #insert(node: AnyFrameNode, rawFrame: RawFrame): FrameNode {
    let i = 0;
    for (; i < node.children.length; ++i) {
      const maybeChild = node.children[i];
      const child = maybeChild instanceof WeakRef ? maybeChild.deref() : maybeChild;
      if (!child) {
        continue;
      }

      const compareResult = compareRawFrames(child.rawFrame, rawFrame);
      if (compareResult === 0) {
        if (rawFrame.parsedFrameInfo && !child.parsedFrameInfo) {
          child.parsedFrameInfo = rawFrame.parsedFrameInfo;
        }
        return child;
      }
      if (compareResult > 0) {
        break;
      }
    }

    const newNode = new FrameNode(rawFrame, node);
    if (node.parent) {
      node.children.splice(i, 0, newNode);
    } else {
      node.children.splice(i, 0, new WeakRef(newNode));
    }
    return newNode;
  }

  /**
   * Traverses the trie in pre-order.
   *
   * @param node Start at `node` or `null` to start with the children of the root.
   * @param visit Called on each node in the trie. Return `true` if the visitor should descend into child nodes of the provided node.
   */
  walk(node: FrameNode|null, visit: (node: FrameNode) => boolean): void {
    const stack =
        node ? [node] : [...this.#root.children].map(ref => ref.deref()).filter(node => Boolean(node)) as FrameNode[];

    for (let node = stack.pop(); node; node = stack.pop()) {
      const visitChildren = visit(node);
      if (visitChildren) {
        // Pushing the children in reverse means the "left-most" child is visited first (i.e. pre-order).
        for (let i = node.children.length - 1; i >= 0; --i) {
          stack.push(node.children[i]);
        }
      }
    }
  }
}

/**
 * @returns a number < 0, 0 or > 0, if the `a` is smaller then, equal or greater then `b`.
 */
export function compareRawFrames(a: RawFrame, b: RawFrame): number {
  const scriptIdCompare = (a.scriptId ?? '').localeCompare(b.scriptId ?? '');
  if (scriptIdCompare !== 0) {
    return scriptIdCompare;
  }

  const urlCompare = (a.url ?? '').localeCompare(b.url ?? '');
  if (urlCompare !== 0) {
    return urlCompare;
  }

  const nameCompare = (a.functionName ?? '').localeCompare(b.functionName ?? '');
  if (nameCompare !== 0) {
    return nameCompare;
  }

  if (a.lineNumber !== b.lineNumber) {
    return a.lineNumber - b.lineNumber;
  }

  return a.columnNumber - b.columnNumber;
}
