import type {
  Node as EstreeNode,
  Kind as EstreeNodeKind,
  Path as EstreeNodePath,
} from "estree-sentry";
import type { Situ } from "./source.d.ts";
import type { GetDefault } from "../util/util.d.ts";

export type GlobalDeclarativeRecord = "builtin" | "emulate";

/**
 * A parsed JavaScript program which should adhere to `estree.Program`. Can be
 * generated by:
 * - `acorn.parse(code, {ecmaVersion: 2024})`
 * - `@babel/parser` with `estree` plugin
 */
export type LooseEstreeProgram = {
  type: "Program";
  sourceType: "module" | "script";
  body: unknown[];
};

/**
 * A parsed JavaScript program. Represents both static code and dynamically
 * generated code.
 */
export type File<param extends { FilePath?: unknown } = {}> = {
  /**
   * The `estree.Program` node to instrument.
   */
  root: LooseEstreeProgram;
  /**
   * Indicates how the source will be executed.
   * @defaultValue Either `"script"` or `"module"` based on
   * `file.root.sourceType`.
   */
  kind: "module" | "script" | "eval";
  /**
   * Further precises the context in which the source will be executed. Only
   * relevant when `source.kind` is `"eval"`.
   * - `GlobalSitu`: The source will be executed in the global context. It is
   * the only valid option when `source.kind` is `"module"` or `"script"`.
   * - `RootLocalSitu`: The source will be executed in a local context that is
   * not controlled by Aran -- ie: a direct eval call within non-instrumented
   * code.
   * - `DeepLocalSitu`: The source will be executed in a local context that is
   * controlled by Aran -- ie: direct eval call within instrumented code. This
   * data structure is provided by Aran as argument to the `eval@before` aspect.
   * @defaultValue `{ type: "global" }`
   */
  situ: Situ;
  /**
   * An identifier for the file. It is passed to `conf.digest` to help
   * generating unique node hash.
   */
  path: GetDefault<param, "FilePath", string>;
};

/**
 * Hashing function for estree nodes.
 * @template P The type of `file.path`.
 * @template H
 * The type of the hash. Either a string or a number. Can be branded -- eg:
 * `string & { __brand: "hash" }`.
 * @param node The estree node to hash.
 * @param node_path The json path of the node in starting from `file.root`. It
 * is composed of the properties names that lead to the node concatenated with
 * dots. So integers properties are within bracket. For instance:
 * `"body.0.declarations.0.init"`.
 * @param file_path The path of the file containing the node as provided by
 * `file.path`.
 * @param node_kind The kind of the node -- eg: `"Program`", `"Statement"`,
 * `"Expression"`, ...
 * @returns The hash of the node. It should be unique for each node in the
 * program `file.root`. It is safe to simply returns `node_path`.
 */
export type Digest<
  param extends { FilePath?: unknown; NodeHash?: string | number } = {},
> = (
  node: EstreeNode<{}>,
  node_path: EstreeNodePath,
  file_path: GetDefault<param, "FilePath", string>,
  node_kind: EstreeNodeKind,
) => GetDefault<param, "NodeHash", string>;

/**
 * Configuration object for `transpile`.
 */
export type Config<
  param extends { FilePath?: unknown; NodeHash?: string | number } = {},
> = {
  /**
   * Indicates whether the global declarative record should be emulated or not.
   * NB: The global declarative record is a scope frame that sits right before
   * the global object. For instance, in *script* code (not eval code nor module
   * code): `let x = 123` will cause the creation of a binding in the global
   * declarative record and not in the global object. Unfortunately, this record
   * cannot be accessed inside the language and we are stuck with two imperfect
   * options:
   * - `"builtin"`: The builtin global declarative record is used, access to
   *   global variables will happen via parameter functions such as:
   *   `scope.read`, `scope.writeSloppy`, etc... Tracking values through these
   *   calls requires additional logic.
   * - `"emulate"`: A plain object is used to emulate the global declarative
   *   record. That means that instrumented code will never access the builtin
   *   global declarative record. Hence, every single bit of code should be
   *   instrumented which might be a hard requirement to meet.
   * @defaultValue `"builtin"`
   */
  global_declarative_record: "builtin" | "emulate";
  /**
   * Hashing functions for nodes in `file.root`.
   * @defaultValue `(node, node_path, file_path, node_kind) => file_path + "#" + node_path`
   */
  digest: Digest<param>;
};
