// Functions to compile 1 or more visitor objects into a single compiled visitor.
//
// # Visitor objects
//
// Visitor objects which are generated by rules' `create` functions have keys being any of:
//
// 1. Name of an AST type
// 2. Arbitrary selector (e.g. `:matches(BinaryExpression, LogicalExpression)`)
// 3. Name of an AST type, postfixed with `:exit`
// 4. Arbitrary selector, postfixed with `:exit`
// 5. CFG handler name (e.g. `onCodePathStart`)
//
// Each property value must be a function that handles that AST node / selector target / CFG event arguments.
//
// e.g.:
//
// ```
// {
//   BinaryExpression(node) {
//     // Called when entering a `BinaryExpression` node
//   },
//   'BinaryExpression:exit'(node) {
//     // Called when exiting a `BinaryExpression` node
//   },
//   ':matches(BinaryExpression, LogicalExpression)'(node) {
//     // Called when entering a `BinaryExpression` or `LogicalExpression` node
//   },
//   '*:exit'(node) {
//     // Called when exiting any node
//   },
//   onCodePathStart(codePath, node) {
//     // Called when CFG event occurs
//   },
// }
// ```
//
// # Compiled visitor
//
// Compiled visitor is an array with `TYPE_IDS_COUNT` length, keyed by the ID of the node type / CFG event.
// `NODE_TYPE_IDS_MAP` maps from type name / CFG event name to ID.
//
// Each element of compiled array is one of:
// * No visitor for this type = `null`.
// * Visitor for leaf node = visit function.
// * Visitor for non-leaf node = object of form `{ enter, exit }`,
//   where each property is either a visitor function or `null`.
// * Visitor for CFG event = visit function.
//
// e.g.:
//
// ```
// [
//   // Leaf nodes
//   function(node) { /* do stuff */ },
//   // ...
//
//   // Non-leaf nodes
//   {
//     enter: function(node) { /* do stuff */ },
//     exit: null,
//   },
//   // ...
//
//   // CFG events
//   function(codePath, node) { /* do stuff */ },
// ]
// ```
//
// # Object reuse
//
// No more than 1 compiled visitor exists at any time, so we reuse a single array `compiledVisitor`,
// rather than creating a new array for each file being linted.
//
// To compile a visitor and use it:
//
// 1. Call `addVisitorToCompiled` with each visitor object.
// 2. Call `finalizeCompiledVisitor` once.
// 3. Walk AST with compiled visitor.
// 4. Call `resetCompiledVisitor` once.
//
// We also recycle:
//
// * `{ enter, exit }` objects which are stored in compiled visitor.
// * `VisitProp` objects which are used while compiling visitor.
// * `{ enter: [], exit: [] }` objects which are used while compiling visitor.
//
// The aim is to reduce pressure on the garbage collector. All these recycled objects are long-lived
// and will graduate to "old space", which leaves as much capacity as possible in "new space"
// for objects created by user code in visitors. If ephemeral user-created objects all fit in new space,
// it will avoid full GC runs, which should greatly improve performance.

import {
  LEAF_NODE_TYPES_COUNT,
  NODE_TYPE_IDS_MAP,
  NODE_TYPES_COUNT,
  TYPE_IDS_COUNT,
} from "../generated/type_ids.ts";
import {
  parseSelector,
  wrapVisitFnWithSelectorMatch,
  EXIT_FLAG,
  IDENTIFIER_COUNT_INCREMENT,
} from "./selector.ts";
import { debugAssert, debugAssertIsNonNull, typeAssertIs } from "../utils/asserts.ts";

import type { Node } from "./types.ts";
import type { VisitorObject } from "../generated/visitor.d.ts";

// Visit function for a specific AST node type.
export type VisitFn = (node: Node) => void;

// Visit function for a specific CFG event.
export type CfgVisitFn = (...args: unknown[]) => void;

// Enter+exit pair, for non-leaf nodes in compiled visitor.
export interface EnterExit {
  enter: VisitFn | null;
  exit: VisitFn | null;
}

// Visitor function and details of its specificity.
interface VisitProp<Visit = VisitFn> {
  // Visit function.
  // Only `null` in between compilations - this field is only nullable to allow `VisitProp` objects to be reused,
  // while allowing the `VisitFn` to be garbage collected.
  fn: Visit | null;
  // Specificity of visitor function.
  specificity: number;
  // Selector string e.g. `Program`, `Program > ExpressionStatement`, `*`.
  // Does not include trailing `:exit`.
  // Used as tie-breaker for specificity.
  // Only `null` in between compilations - this field is only nullable to allow `VisitProp` objects to be reused,
  // while allowing the string to be garbage collected.
  selectorStr: string | null;
}

// Visitor function and details of its specificity, where the visitor function is for a CFG event.
type CfgVisitProp = VisitProp<CfgVisitFn>;

// Entry and exit visitors for a non-leaf node.
interface CompilingNonLeafVisitorEntry {
  // Visitor props for entry
  enter: VisitProp[];
  // Visitor props for exit
  exit: VisitProp[];
}

// Visitor state.
// Returned by `finalizeCompiledVisitor`.
// This a "poor man's enum", which compiles down better than TS enums.
export type VisitorState = typeof VISITOR_EMPTY | typeof VISITOR_NOT_EMPTY | typeof VISITOR_CFG;

export const VISITOR_EMPTY = 0; // Empty visitor (no need to walk AST)
export const VISITOR_NOT_EMPTY = 1; // Not empty visitor (need to walk AST)
export const VISITOR_CFG = 2; // Not empty visitor, and contains CFG event listeners (need to construct CFG)

// Count of non-leaf node types
const NON_LEAF_NODE_TYPES_COUNT = NODE_TYPES_COUNT - LEAF_NODE_TYPES_COUNT;
// Count of CFG event types
const CFG_EVENTS_COUNT = TYPE_IDS_COUNT - NODE_TYPES_COUNT;

// During compilation, arrays of visitor props are stored in these arrays.
// These arrays are initialized with empty arrays, and never modified thereafter.
// Only the arrays they contain are modified - they're filled up during compilation,
// and emptied at the end in `finalizeCompiledVisitor`, so can be reused for next compilation.
const compilingLeafVisitor: VisitProp[][] = [];
const compilingNonLeafVisitor: CompilingNonLeafVisitorEntry[] = [];
const compilingCfgVisitor: CfgVisitProp[][] = [];

for (let i = LEAF_NODE_TYPES_COUNT; i !== 0; i--) {
  compilingLeafVisitor.push([]);
}

for (let i = NON_LEAF_NODE_TYPES_COUNT; i !== 0; i--) {
  compilingNonLeafVisitor.push({ enter: [], exit: [] });
}

for (let i = CFG_EVENTS_COUNT; i !== 0; i--) {
  compilingCfgVisitor.push([]);
}

// Compiled visitor used for visiting each file.
//
// * `VisitFn | null` for leaf nodes.
// * `EnterExit | null` for non-leaf nodes.
//
// Same array is reused for each file.
//
// Initialized with `.push()` to ensure V8 treats the array as "packed" (linear array),
// not "holey" (hash map). This is critical, as looking up elements in this array is a very hot path
// during AST visitation, and holey arrays are much slower.
// https://v8.dev/blog/elements-kinds
export const compiledVisitor: (VisitFn | EnterExit | CfgVisitFn | null)[] = [];

for (let i = TYPE_IDS_COUNT; i !== 0; i--) {
  compiledVisitor.push(null);
}

// Arrays containing type IDs of types which have visit functions defined for them.
// All three share a single underlying buffer.
const activeTypeIdsBuffer = new ArrayBuffer(TYPE_IDS_COUNT);
const activeLeafVisitorTypeIds = new Uint8Array(activeTypeIdsBuffer, 0, LEAF_NODE_TYPES_COUNT);
const activeNonLeafVisitorTypeIds = new Uint8Array(
  activeTypeIdsBuffer,
  LEAF_NODE_TYPES_COUNT,
  NON_LEAF_NODE_TYPES_COUNT,
);
const activeCfgVisitorTypeIds = new Uint8Array(
  activeTypeIdsBuffer,
  LEAF_NODE_TYPES_COUNT + NON_LEAF_NODE_TYPES_COUNT,
  CFG_EVENTS_COUNT,
);

// Debug check that a `u8` is large enough to hold all type IDs
if (DEBUG) {
  const maxTypeId = NODE_TYPE_IDS_MAP.values().reduce((max, id) => Math.max(max, id), 0);
  if (maxTypeId > 255) throw new Error("All type IDs must be <= 255 to fit in a `u8`");
}

// Number of active visitors of each type
let activeLeafVisitorsCount = 0,
  activeNonLeafVisitorsCount = 0,
  activeCfgVisitorsCount = 0;

// `true` if `addVisitor` has been called with a visitor which visits at least one AST type
let hasActiveVisitors = false;

// Enter+exit object cache.
//
// `compiledVisitor` may contain many `{ enter, exit }` objects.
// Use this cache to reuse those objects across all visitor compilations.
//
// Pre-populate the cache with enough `EnterExit` objects for all non-leaf AST node types.
// This removes the need to check if cache contains enough objects before using it in `finalizeCompiledVisitor`.
//
// More importantly, allocating all these objects in one go will put them together in memory in "new space".
// When these objects graduate to "old space", V8 does not necessarily keep them together in memory,
// but it will tend to - it makes it more likely they'll be grouped together.
// These objects are accessed over and over during AST walk, so having them all close together in memory
// makes L1 cache misses less likely.
//
// `activeNonLeafVisitorsCount` is the number of active non-leaf visitors in `compiledVisitor`,
// and therefore the number of `EnterExit` objects currently in use.
const enterExitObjectCache: EnterExit[] = [];

for (let i = NON_LEAF_NODE_TYPES_COUNT; i !== 0; i--) {
  enterExitObjectCache.push({ enter: null, exit: null });
}

// `VisitProp` object cache.
//
// During compilation, many such objects may be required, and then they're discarded in `finalizeCompiledVisitor`.
// Use this cache to reuse those objects across all visitor compilations.
//
// `visitPropsCacheNextIndex` is the index of first object in cache which is currently unused.
// It may point to the end of the cache array.
const visitPropsCache: VisitProp<VisitFn | CfgVisitFn>[] = [];
let visitPropsCacheNextIndex = 0;

/**
 * Add a visitor to compiled visitor.
 *
 * @param visitor - Visitor object
 */
export function addVisitorToCompiled(visitor: VisitorObject): void {
  if (visitor === null || typeof visitor !== "object") {
    throw new TypeError("Visitor returned from `create` method must be an object");
  }

  // Exit if is empty visitor
  const keys = Object.keys(visitor),
    keysLen = keys.length;
  if (keysLen === 0) return;

  hasActiveVisitors = true;

  // Populate cache of `VisitProp` objects with enough for all properties of `visitor`.
  // After warming up over first few files, the cache will be large enough to service all files,
  // and this loop will be skipped. This avoids the main loop below from having to branch repeatedly
  // on whether there are enough `VisitProp` objects in cache, and to create one if not.
  const visitPropsCacheRequiredLen = visitPropsCacheNextIndex + keysLen;
  while (visitPropsCache.length < visitPropsCacheRequiredLen) {
    visitPropsCache.push({ fn: null, specificity: 0, selectorStr: null });
  }

  // Populate visitors array from provided object
  for (let i = 0; i < keysLen; i++) {
    let name = keys[i];

    const visitFn = visitor[name] as VisitFn | CfgVisitFn;
    if (typeof visitFn !== "function") {
      throw new TypeError(`'${name}' property of visitor object is not a function`);
    }

    // If this is an exit visit fn, set `specificity` initially to `EXIT_FLAG`, otherwise 0.
    // Visit fns are sorted in ascending order of `specificity`.
    // For leaf nodes, the high `specificity` ensures that exit visit fns are sorted after enter visit fns,
    // and therefore `exit` fns are called after `enter` fns.
    let specificity = 0;

    const isExit = name.endsWith(":exit");
    if (isExit) {
      name = name.slice(0, -5);
      specificity = EXIT_FLAG;
    }

    // Set up `VisitProp` object.
    // Reuse `VisitProp` object from cache. Loop above ensures cache is filled with enough objects.
    const visitProp = visitPropsCache[visitPropsCacheNextIndex++];
    visitProp.fn = visitFn;
    visitProp.specificity = specificity;
    visitProp.selectorStr = name;

    // TODO: Combine the two hashmaps `NODE_TYPE_IDS_MAP` and selectors cache into one `Map`
    // to avoid 2 hashmap lookups for selectors?
    let typeId = NODE_TYPE_IDS_MAP.get(name);
    if (typeId !== undefined) {
      // Single type visit function e.g. `Program`.
      // Add a single identifier to `specificity`. Use `|=` to keep exit flag.
      visitProp.specificity |= IDENTIFIER_COUNT_INCREMENT;

      if (typeId < LEAF_NODE_TYPES_COUNT) {
        addLeafVisitFn(typeId, visitProp as VisitProp);
      } else if (typeId < NODE_TYPES_COUNT) {
        addNonLeafVisitFn(typeId, visitProp as VisitProp, isExit);
      } else {
        addCfgVisitFn(typeId, visitProp as CfgVisitProp, isExit);
      }

      continue;
    }

    // CFG event handlers have been dealt with above
    typeAssertIs<VisitFn>(visitFn);
    typeAssertIs<VisitProp>(visitProp);

    // `*` matches any node without any filtering, so no need to wrap it
    if (name !== "*") {
      // Selector.
      // Parse selector.
      // Wrap `visitFn` so it only executes if the selector matches.
      // If selector is simple (unconditionally matches certain types e.g. `:matches(X, Y)`), skip wrapping.
      const selector = parseSelector(name);

      // Update specificity. Use `|=` to keep exit flag.
      visitProp.specificity |= selector.specificity;

      if (selector.isComplex) {
        visitProp.fn = wrapVisitFnWithSelectorMatch(visitFn, selector.esquerySelector);
      }

      const { typeIds } = selector;
      if (typeIds !== null) {
        // Selector matches a specific set of node types.
        // Note: If `typeIds` is empty, `visitProp` will not be used.
        // We could return it to the cache, but this should be a very rare case, so don't bother.
        for (let i = 0, len = typeIds.length; i < len; i++) {
          const typeId = typeIds[i];
          debugAssert(typeId < NODE_TYPES_COUNT, "`typeId` should be of a leaf or non-leaf node");
          if (typeId < LEAF_NODE_TYPES_COUNT) {
            addLeafVisitFn(typeId, visitProp);
          } else {
            addNonLeafVisitFn(typeId, visitProp, isExit);
          }
        }
        continue;
      }
    }

    // `*` selector or some other selector that matches all node types
    // TODO: Add 2 arrays for "enter all" and "exit all" selectors.
    // Where visitor contains multiple selectors which match all node types, this would avoid merging them
    // into a single visit function 100+ times for each node type. Could just merge them once and assign the same
    // merged function to all node types (except those which have other visit functions).
    for (typeId = 0; typeId < LEAF_NODE_TYPES_COUNT; typeId++) {
      addLeafVisitFn(typeId, visitProp);
    }
    for (; typeId < NODE_TYPES_COUNT; typeId++) {
      addNonLeafVisitFn(typeId, visitProp, isExit);
    }
  }
}

/**
 * Add visit function for a leaf node to compiled visitor.
 *
 * Stored as just 1 function, not enter+exit pair.
 *
 * @param typeId - Node type ID
 * @param visitProp - Visitor property
 */
function addLeafVisitFn(typeId: number, visitProp: VisitProp): void {
  const visitProps = compilingLeafVisitor[typeId]!;
  if (visitProps.length === 0) activeLeafVisitorTypeIds[activeLeafVisitorsCount++] = typeId;
  visitProps.push(visitProp);
}

/**
 * Add visit function for a non-leaf node to compiled visitor.
 *
 * Stored as enter+exit pair.
 *
 * @param typeId - Node type ID
 * @param visitProp - Visitor property
 * @param isExit - `true` if is an exit visit fn
 */
function addNonLeafVisitFn(typeId: number, visitProp: VisitProp, isExit: boolean): void {
  const { enter, exit } = compilingNonLeafVisitor[typeId - LEAF_NODE_TYPES_COUNT]!;
  if (enter.length === 0 && exit.length === 0) {
    activeNonLeafVisitorTypeIds[activeNonLeafVisitorsCount++] = typeId;
  }

  if (isExit) {
    exit.push(visitProp);
  } else {
    enter.push(visitProp);
  }
}

/**
 * Add visit function for a CFG event to compiled visitor.
 *
 * Stored as just 1 function, not enter+exit pair.
 *
 * @param typeId - Node type ID
 * @param visitProp - Visitor property
 * @param isExit - `true` if is an exit visit fn
 */
function addCfgVisitFn(typeId: number, visitProp: CfgVisitProp, isExit: boolean): void {
  if (isExit) throw new Error(`Invalid visitor key: \`${visitProp.selectorStr}:exit\``);

  const visitProps = compilingCfgVisitor[typeId - NODE_TYPES_COUNT]!;
  if (visitProps.length === 0) activeCfgVisitorTypeIds[activeCfgVisitorsCount++] = typeId;
  visitProps.push(visitProp);
}

/**
 * Finalize compiled visitor.
 *
 * After calling this function, `compiledVisitor` is ready to be used to walk the AST.
 *
 * @returns - `true` if compiled visitor visits at least 1 AST type
 */
export function finalizeCompiledVisitor(): VisitorState {
  if (hasActiveVisitors === false) return VISITOR_EMPTY;

  // Merge visitors for leaf nodes
  for (let i = activeLeafVisitorsCount - 1; i >= 0; i--) {
    const typeId = activeLeafVisitorTypeIds[i];
    compiledVisitor[typeId] = mergeVisitFns(compilingLeafVisitor[typeId]);
  }

  // Merge visitors for non-leaf nodes
  for (let i = 0; i < activeNonLeafVisitorsCount; i++) {
    const typeId = activeNonLeafVisitorTypeIds[i];
    const entry = compilingNonLeafVisitor[typeId - LEAF_NODE_TYPES_COUNT];

    // Reuse `EnterExit` object from cache.
    // Cache is pre-populated with enough objects that `i` cannot be out of bounds.
    const enterExit = enterExitObjectCache[i];
    debugAssertIsNonNull(enterExit, "`enterExit` should not be null");

    // Merge enter and exit visitors
    const enterArr = entry.enter;
    if (enterArr.length !== 0) enterExit.enter = mergeVisitFns(enterArr);

    const exitArr = entry.exit;
    if (exitArr.length !== 0) enterExit.exit = mergeVisitFns(exitArr);

    // Store merged enter-exit object in compiled visitor
    compiledVisitor[typeId] = enterExit;
  }

  // Merge visitors for CFG events
  let visitState: VisitorState = VISITOR_NOT_EMPTY;

  if (activeCfgVisitorsCount > 0) {
    visitState = VISITOR_CFG;

    for (let i = activeCfgVisitorsCount - 1; i >= 0; i--) {
      const typeId = activeCfgVisitorTypeIds[i];
      compiledVisitor[typeId] = mergeCfgVisitFns(compilingCfgVisitor[typeId - NODE_TYPES_COUNT]);
    }

    // Reset state, ready for next time
    activeCfgVisitorsCount = 0;
  }

  // Reset `visitPropsCache`.
  // Setting these properties to `null` is not necessary for correctness, but it allows them to be garbage collected.
  // `specificity` is only an SMI (stored inline), and is overwritten in `addVisitorToCompiled`, so no need to reset it.
  for (let i = visitPropsCacheNextIndex - 1; i >= 0; i--) {
    const visitProp = visitPropsCache[i];
    visitProp.fn = null;
    visitProp.selectorStr = null;
  }
  visitPropsCacheNextIndex = 0;

  // Reset state, ready for next time.
  // `activeNonLeafVisitorsCount` is not reset, as it's used later in `resetCompiledVisitor`.
  activeLeafVisitorsCount = 0;

  hasActiveVisitors = false;

  return visitState;
}

/**
 * Reset compiled visitor.
 *
 * This frees visit functions stored in `compiledVisitor`, and makes them eligible for garbage collection.
 *
 * After calling this function, `compiledVisitor` is in a clean state, ready for next file's visitor to be compiled.
 */
export function resetCompiledVisitor(): void {
  // Reset `compiledVisitor` array
  compiledVisitor.fill(null);

  // Reset `EnterExit` objects
  for (let i = 0; i < activeNonLeafVisitorsCount; i++) {
    const enterExit = enterExitObjectCache[i];
    enterExit.enter = null;
    enterExit.exit = null;
  }
  activeNonLeafVisitorsCount = 0;
}

/**
 * Merger that merges visit functions for an AST node type.
 */
type Merger = (visitProps: VisitProp[]) => VisitFn;

/**
 * Merger that merges handler functions for a CFG event.
 */
type CfgMerger = (visitProps: CfgVisitProp[]) => CfgVisitFn;

/**
 * Merge array of visit functions into a single function, which calls each of input functions in turn.
 *
 * The array passed is cleared (length set to 0), so the array can be reused.
 *
 * The merged function is statically defined and does not contain a loop, to hopefully allow
 * JS engine to heavily optimize it.
 *
 * `mergers` contains pre-defined functions to merge up to 5 visit functions.
 * Merger functions for merging more than 5 visit functions are created dynamically on demand.
 *
 * @param visitProps - Array of `VisitProp` objects
 * @returns Function which calls all visit functions in turn
 */
function mergeVisitFns(visitProps: VisitProp[]): VisitFn {
  const numVisitFns = visitProps.length;

  debugAssert(numVisitFns > 0, "`visitProps` should have at least 1 element");

  let mergedFn: VisitFn;
  if (numVisitFns === 1) {
    // Only 1 visit function, so no need to merge
    debugAssertIsNonNull(visitProps[0].fn);
    mergedFn = visitProps[0].fn;
  } else {
    // Sort in ascending order of specificity.
    // Selector string as tie-breaker.
    visitProps.sort((a, b) => {
      const diff = a.specificity - b.specificity;
      if (diff !== 0) return diff;

      const strA = a.selectorStr,
        strB = b.selectorStr;
      debugAssertIsNonNull(strA);
      debugAssertIsNonNull(strB);
      // Note: It's possible for different rules to use the same selector,
      // so we need to handle equality here, as well as less than / greater than
      return strA === strB ? 0 : strA < strB ? -1 : 1;
    });

    // Get or create merger for merging `numVisitFns` functions
    while (mergers.length <= numVisitFns) {
      mergers.push(null);
    }

    let merger = mergers[numVisitFns];
    if (merger === null) merger = mergers[numVisitFns] = createMerger(numVisitFns, false);

    // Merge functions
    mergedFn = merger(visitProps);
  }

  // Empty `visitProps` array, so it can be reused
  visitProps.length = 0;

  return mergedFn;
}

/**
 * Merge array of CFG visit functions into a single function, which calls each of input functions in turn.
 *
 * The difference between this function and `mergeVisitFns` is that this function is for CFG event handlers.
 * Unlike all other visit functions, CFG event handlers are called with more than 1 argument.
 * We keep this separate from `mergeVisitFns` because the merger functions use `...args` to pass all arguments,
 * which is likely less performant than the simpler version which passes a single argument.
 * AST node visitation is a lot more common than CFG event visitation, and we want to keep the common case fast.
 *
 * The array passed is cleared (length set to 0), so the array can be reused.
 *
 * The merged function is statically defined and does not contain a loop, to hopefully allow
 * JS engine to heavily optimize it.
 *
 * `cfgMergers` contains pre-defined functions to merge up to 3 CFG visit functions.
 * Merger functions for merging more than 3 visit functions are created dynamically on demand.
 *
 * @param visitProps - Array of `CfgVisitProp` objects
 * @returns Function which calls all CFG visit functions in turn
 */
function mergeCfgVisitFns(visitProps: CfgVisitProp[]): CfgVisitFn {
  const numVisitFns = visitProps.length;

  debugAssert(numVisitFns > 0, "`visitProps` should have at least 1 element");

  let mergedFn: CfgVisitFn;
  if (numVisitFns === 1) {
    // Only 1 visit function, so no need to merge
    debugAssertIsNonNull(visitProps[0].fn);
    mergedFn = visitProps[0].fn;
  } else {
    // No need to sort in order of specificity, because each rule can only have 1 handler for each CFG event

    // Get or create merger for merging `numVisitFns` functions
    while (cfgMergers.length <= numVisitFns) {
      cfgMergers.push(null);
    }

    let merger = cfgMergers[numVisitFns];
    if (merger === null) merger = cfgMergers[numVisitFns] = createMerger(numVisitFns, true);

    // Merge functions
    mergedFn = merger(visitProps);
  }

  // Empty `visitProps` array, so it can be reused
  visitProps.length = 0;

  return mergedFn;
}

/**
 * Create a merger function that merges `fnCount` functions.
 *
 * Generated code for `fnCount: 3`, `isCfg: false` (AST node visit function merger):
 * ```js
 * function(p) {
 *   var v0 = p[0].fn,
 *     v1 = p[1].fn,
 *     v2 = p[2].fn;
 *   return (n) => {
 *     v0(n);
 *     v1(n);
 *     v2(n);
 *   }
 * }
 * ```
 *
 * Generated code for `fnCount: 2`, `isCfg: true` (CFG event handler merger):
 * ```js
 * function(p) {
 *   var v0 = p[0].fn,
 *     v1 = p[1].fn;
 *   return (...a) => {
 *     v0(...a);
 *     v1(...a);
 *   }
 * }
 * ```
 *
 * @param fnCount - Number of functions to be merged
 * @param isCfg - `true` if creating a merger for CFG event handlers
 * @returns Function to merge `fnCount` functions
 */
function createMerger(fnCount: number, isCfg: false): Merger;
function createMerger(fnCount: number, isCfg: true): CfgMerger;
function createMerger(fnCount: number, isCfg: boolean): Merger | CfgMerger {
  const visitArgs = isCfg ? "...a" : "n";
  let body = "var ";
  let fnBody = `;return (${visitArgs})=>{`;
  for (let i = 0; i < fnCount; i++) {
    if (i !== 0) body += ",";
    body += `v${i}=p[${i}].fn`;
    fnBody += `v${i}(${visitArgs});`;
  }
  fnBody += "}";

  body += fnBody;

  // oxlint-disable-next-line typescript-eslint/no-implied-eval
  return new Function("p", body) as Merger | CfgMerger;
}

// Pre-defined mergers for merging up to 5 functions.
//
// We use `var` statements instead of `const` to avoid the cost of TDZ checks, because merged functions
// can be on a very hot path.
const mergers: (Merger | null)[] = [
  null, // No merger for 0 functions
  null, // No merger for 1 function
  (visitProps: VisitProp[]) => {
    debugAssert(visitProps.length === 2, "`visitProps` should have 2 elements");
    var visit1 = visitProps[0].fn!;
    var visit2 = visitProps[1].fn!;
    return (node: Node) => {
      visit1(node);
      visit2(node);
    };
  },
  (visitProps: VisitProp[]) => {
    debugAssert(visitProps.length === 3, "`visitProps` should have 3 elements");
    var visit1 = visitProps[0].fn!;
    var visit2 = visitProps[1].fn!;
    var visit3 = visitProps[2].fn!;
    return (node: Node) => {
      visit1(node);
      visit2(node);
      visit3(node);
    };
  },
  (visitProps: VisitProp[]) => {
    debugAssert(visitProps.length === 4, "`visitProps` should have 4 elements");
    var visit1 = visitProps[0].fn!;
    var visit2 = visitProps[1].fn!;
    var visit3 = visitProps[2].fn!;
    var visit4 = visitProps[3].fn!;
    return (node: Node) => {
      visit1(node);
      visit2(node);
      visit3(node);
      visit4(node);
    };
  },
  (visitProps: VisitProp[]) => {
    debugAssert(visitProps.length === 5, "`visitProps` should have 5 elements");
    var visit1 = visitProps[0].fn!;
    var visit2 = visitProps[1].fn!;
    var visit3 = visitProps[2].fn!;
    var visit4 = visitProps[3].fn!;
    var visit5 = visitProps[4].fn!;
    return (node: Node) => {
      visit1(node);
      visit2(node);
      visit3(node);
      visit4(node);
      visit5(node);
    };
  },
];

// Pre-defined CFG mergers for merging up to 3 functions.
//
// CFG event handlers are rarely used, so we only pre-define mergers for up to 3 functions,
// unlike the AST node visit fns, where we pre-define 5.
//
// We use `var` statements instead of `const` to avoid the cost of TDZ checks, because merged functions
// can be on a very hot path.
const cfgMergers: (CfgMerger | null)[] = [
  null, // No merger for 0 functions
  null, // No merger for 1 function
  (visitProps: CfgVisitProp[]) => {
    debugAssert(visitProps.length === 2, "`visitProps` should have 2 elements");
    var visit1 = visitProps[0].fn!;
    var visit2 = visitProps[1].fn!;
    return (...args: unknown[]) => {
      visit1(...args);
      visit2(...args);
    };
  },
  (visitProps: CfgVisitProp[]) => {
    debugAssert(visitProps.length === 3, "`visitProps` should have 3 elements");
    var visit1 = visitProps[0].fn!;
    var visit2 = visitProps[1].fn!;
    var visit3 = visitProps[2].fn!;
    return (...args: unknown[]) => {
      visit1(...args);
      visit2(...args);
      visit3(...args);
    };
  },
];
