// While working on this file, it is important to have very rigorous errors
// and checking on input data. The goal is to allow people not aware of
// various footguns in JavaScript, C++, and the bindings generator to
// always produce correct code, or bail with an error.
import { expect } from "bun:test";
import assert from "node:assert";
import * as path from "node:path";
import type { FuncOptions, t } from "./bindgen-lib";

export const src = path.join(import.meta.dirname, "../");

export type TypeKind = keyof typeof t;

export let allFunctions: Func[] = [];
export let files = new Map<string, File>();
/** A reachable type is one that is required for code generation */
export let typeHashToReachableType = new Map<string, TypeImpl>();
export let typeHashToStruct = new Map<string, Struct>();
export let typeHashToNamespace = new Map<string, string>();
export let structHashToSelf = new Map<string, Struct>();

/** String literal */
export const str = (v: any) => JSON.stringify(v);
/** Capitalize */
export const cap = (s: string) => s[0].toUpperCase() + s.slice(1);
/** Escape a Zig Identifier */
export const zid = (s: string) => (s.match(/^[a-zA-Z_][a-zA-Z0-9_]*$/) ? s : "@" + str(s));
/** Snake Case */
export const snake = (s: string) =>
  s[0].toLowerCase() +
  s
    .slice(1)
    .replace(/([A-Z])/g, "_$1")
    .replace(/-/g, "_")
    .toLowerCase();
/** Camel Case */
export const camel = (s: string) =>
  s[0].toLowerCase() + s.slice(1).replace(/[_-](\w)?/g, (_, letter) => letter?.toUpperCase() ?? "");
/** Pascal Case */
export const pascal = (s: string) => cap(camel(s));

// Return symbol names of extern values (must be equivalent between C++ and Zig)

/** The JS Host function, aka fn (*JSC.JSGlobalObject, *JSC.CallFrame) JSValue.MaybeException */
export const extJsFunction = (namespaceVar: string, fnLabel: string) =>
  `bindgen_${cap(namespaceVar)}_js${cap(fnLabel)}`;
/** Each variant gets a dispatcher function. */
export const extDispatchVariant = (namespaceVar: string, fnLabel: string, variantNumber: number) =>
  `bindgen_${cap(namespaceVar)}_dispatch${cap(fnLabel)}${variantNumber}`;
export const extInternalDispatchVariant = (namespaceVar: string, fnLabel: string, variantNumber: string | number) =>
  `bindgen_${cap(namespaceVar)}_js${cap(fnLabel)}_v${variantNumber}`;

interface TypeDataDefs {
  /** The name */
  ref: string;

  sequence: {
    element: TypeImpl;
    repr: "slice";
  };
  record: {
    value: TypeImpl;
    repr: "kv-slices";
  };
  zigEnum: {
    file: string;
    impl: string;
  };
  stringEnum: string[];
  oneOf: TypeImpl[];
  dictionary: DictionaryField[];
}
type TypeData<K extends TypeKind> = K extends keyof TypeDataDefs ? TypeDataDefs[K] : any;

export const enum NodeValidator {
  validateInteger = "validateInteger",
}

interface Flags {
  nodeValidator?: NodeValidator;
  optional?: boolean;
  required?: boolean;
  nonNull?: boolean;
  default?: any;
  range?: ["clamp" | "enforce", bigint, bigint] | ["clamp" | "enforce", "abi", "abi"];
  finite?: boolean;
}

export interface DictionaryField {
  key: string;
  type: TypeImpl;
}

export declare const isType: unique symbol;

const numericTypes = new Set(["f64", "i8", "i16", "i32", "i64", "u8", "u16", "u32", "u64", "usize"]);

/**
 * Implementation of the Type interface.  All types are immutable and hashable.
 * Hashes de-duplicate structure and union definitions. Flags do not account for
 * the hash, so `oneOf(A, B)` and `oneOf(A, B).optional` will point to the same
 * generated struct type, the purpose of the flags are to inform receivers like
 * `t.dictionary` and `fn` to mark uses as optional or provide default values.
 */
export class TypeImpl<K extends TypeKind = TypeKind> {
  kind: K;
  data: TypeData<K>;
  flags: Flags;
  /** Access via .name(). */
  nameDeduplicated: string | null | undefined = undefined;
  /** Access via .hash() */
  #hash: string | undefined = undefined;
  ownerFile: string;

  declare [isType]: true;

  constructor(kind: K, data: TypeData<K>, flags: Flags = {}) {
    this.kind = kind;
    this.data = data;
    this.flags = flags;
    this.ownerFile = path.basename(stackTraceFileName(snapshotCallerLocation()), ".bind.ts");
  }

  isVirtualArgument() {
    return this.kind === "globalObject" || this.kind === "zigVirtualMachine";
  }

  hash() {
    if (this.#hash) {
      return this.#hash;
    }
    let h = `${this.kind}:`;
    switch (this.kind) {
      case "ref":
        throw new Error("TODO");
      case "sequence":
        h += this.data.element.hash();
        break;
      case "record":
        h += this.data.value.hash();
        break;
      case "zigEnum":
        h += `${this.data.file}:${this.data.impl}`;
        break;
      case "stringEnum":
        h += this.data.join(",");
        break;
      case "oneOf":
        h += this.data.map(t => t.hash()).join(",");
        break;
      case "dictionary":
        h += this.data.map(({ key, required, type }) => `${key}:${required}:${type.hash()}`).join(",");
        break;
    }
    let hash = String(Bun.hash(h));
    this.#hash = hash;
    return hash;
  }

  /**
   * If this type lowers to a named type (struct, union, enum)
   */
  lowersToNamedType() {
    switch (this.kind) {
      case "ref":
        throw new Error("TODO");
      case "sequence":
      case "record":
      case "oneOf":
      case "dictionary":
      case "stringEnum":
      case "zigEnum":
        return true;
      default:
        return false;
    }
  }

  canDirectlyMapToCAbi(): CAbiType | null {
    let kind = this.kind;
    switch (kind) {
      case "ref":
        throw new Error("TODO");
      case "any":
        return "JSValue";
      case "ByteString":
      case "DOMString":
      case "USVString":
      case "UTF8String":
        return "bun.String";
      case "boolean":
        return "bool";
      case "strictBoolean":
        return "bool";
      case "f64":
      case "i8":
      case "i16":
      case "i32":
      case "i64":
      case "u8":
      case "u16":
      case "u32":
      case "u64":
      case "usize":
        return kind;
      case "globalObject":
      case "zigVirtualMachine":
        return "*JSGlobalObject";
      case "stringEnum":
        return cAbiTypeForEnum(this.data.length);
      case "zigEnum":
        throw new Error("TODO");
      case "undefined":
        return "u0";
      case "oneOf": // `union(enum)`
      case "UTF8String": // []const u8
      case "record": // undecided how to lower records
      case "sequence": // []const T
        return null;
      case "externalClass":
        throw new Error("TODO");
        return "*anyopaque";
      case "dictionary": {
        let existing = typeHashToStruct.get(this.hash());
        if (existing) return existing;
        existing = new Struct();
        for (const { key, type } of this.data as DictionaryField[]) {
          if (type.flags.optional && !("default" in type.flags)) {
            return null; // ?T
          }
          const repr = type.canDirectlyMapToCAbi();
          if (!repr) return null;

          existing.add(key, repr);
        }
        existing.reorderForSmallestSize();
        if (!structHashToSelf.has(existing.hash())) {
          structHashToSelf.set(existing.hash(), existing);
        }
        existing.assignName(this.name());
        typeHashToStruct.set(this.hash(), existing);
        return existing;
      }
      case "sequence": {
        return null;
      }
      default: {
        throw new Error("unexpected: " + (kind satisfies never));
      }
    }
  }

  name() {
    if (this.nameDeduplicated) {
      return this.nameDeduplicated;
    }
    const hash = this.hash();
    const existing = typeHashToReachableType.get(hash);
    if (existing) return (this.nameDeduplicated = existing.nameDeduplicated ??= this.#generateName());
    return (this.nameDeduplicated = `anon_${this.kind}_${hash}`);
  }

  cppInternalName() {
    const name = this.name();
    const cAbiType = this.canDirectlyMapToCAbi();
    const namespace = typeHashToNamespace.get(this.hash());
    if (cAbiType) {
      if (typeof cAbiType === "string") {
        return cAbiType;
      }
    }
    return namespace ? `${namespace}${name}` : name;
  }

  cppClassName() {
    assert(this.lowersToNamedType(), `Does not lower to named type: ${inspect(this)}`);
    const name = this.name();
    const namespace = typeHashToNamespace.get(this.hash());
    return namespace ? `${namespace}::${cap(name)}` : name;
  }

  cppName() {
    const name = this.name();
    const cAbiType = this.canDirectlyMapToCAbi();
    const namespace = typeHashToNamespace.get(this.hash());
    if (cAbiType && typeof cAbiType === "string" && this.kind !== "zigEnum" && this.kind !== "stringEnum") {
      return cAbiTypeName(cAbiType);
    }
    return namespace ? `${namespace}::${cap(name)}` : name;
  }

  #generateName() {
    return `bindgen_${this.ownerFile}_${this.hash()}`;
  }

  /**
   * Name assignment is done to give readable names.
   * The first name to a unique hash wins.
   */
  assignName(name: string) {
    if (this.nameDeduplicated) return;
    const hash = this.hash();
    const existing = typeHashToReachableType.get(hash);
    if (existing) {
      this.nameDeduplicated = existing.nameDeduplicated ??= name;
      return;
    }
    this.nameDeduplicated = name;
  }

  markReachable() {
    if (!this.lowersToNamedType()) return;
    const hash = this.hash();
    const existing = typeHashToReachableType.get(hash);
    this.nameDeduplicated ??= existing?.name() ?? `anon_${this.kind}_${hash}`;
    if (!existing) typeHashToReachableType.set(hash, this);

    switch (this.kind) {
      case "ref":
        throw new Error("TODO");
      case "sequence":
        this.data.element.markReachable();
        break;
      case "record":
        this.data.value.markReachable();
        break;
      case "oneOf":
        for (const type of this.data as TypeImpl[]) {
          type.markReachable();
        }
        break;
      case "dictionary":
        for (const { type } of this.data as DictionaryField[]) {
          type.markReachable();
        }
        break;
    }
  }

  #rangeModifier(min: undefined | number | bigint, max: undefined | number | bigint, kind: "clamp" | "enforce") {
    if (this.flags.range) {
      throw new Error("This type already has a range modifier set");
    }

    // cAbiIntegerLimits throws on non-integer types
    const range = cAbiIntegerLimits(this.kind as CAbiType);
    const abiMin = BigInt(range[0]);
    const abiMax = BigInt(range[1]);
    if (min === undefined) {
      min = abiMin;
      max = abiMax;
    } else {
      if (max === undefined) {
        throw new Error("Expected min and max to be both set or both unset");
      }
      min = BigInt(min);
      max = BigInt(max);

      if (min < abiMin || min > abiMax) {
        throw new Error(`Expected integer in range ${range}, got ${inspect(min)}`);
      }
      if (max < abiMin || max > abiMax) {
        throw new Error(`Expected integer in range ${range}, got ${inspect(max)}`);
      }
      if (min > max) {
        throw new Error(`Expected min <= max, got ${inspect(min)} > ${inspect(max)}`);
      }
    }

    return new TypeImpl(this.kind, this.data, {
      ...this.flags,
      range: min === BigInt(range[0]) && max === BigInt(range[1]) ? [kind, "abi", "abi"] : [kind, min, max],
    });
  }

  assertDefaultIsValid(value: unknown) {
    switch (this.kind) {
      case "DOMString":
      case "ByteString":
      case "USVString":
      case "UTF8String":
        if (typeof value !== "string") {
          throw new Error(`Expected string, got ${inspect(value)}`);
        }
        break;
      case "boolean":
        if (typeof value !== "boolean") {
          throw new Error(`Expected boolean, got ${inspect(value)}`);
        }
        break;
      case "f64":
        if (typeof value !== "number") {
          throw new Error(`Expected number, got ${inspect(value)}`);
        }
        break;
      case "usize":
      case "u8":
      case "u16":
      case "u32":
      case "u64":
      case "i8":
      case "i16":
      case "i32":
      case "i64":
        const range = this.flags.range?.slice(1) ?? cAbiIntegerLimits(this.kind);
        if (typeof value === "number") {
          if (value % 1 !== 0) {
            throw new Error(`Expected integer, got ${inspect(value)}`);
          }
          if (value >= Number.MAX_SAFE_INTEGER || value <= Number.MIN_SAFE_INTEGER) {
            throw new Error(
              `Specify default ${this.kind} outside of max safe integer range as a BigInt to avoid precision loss`,
            );
          }
          if (value < Number(range[0]) || value > Number(range[1])) {
            throw new Error(`Expected integer in range [${range[0]}, ${range[1]}], got ${inspect(value)}`);
          }
        } else if (typeof value === "bigint") {
          if (value < BigInt(range[0]) || value > BigInt(range[1])) {
            throw new Error(`Expected integer in range [${range[0]}, ${range[1]}], got ${inspect(value)}`);
          }
        } else {
          throw new Error(`Expected integer, got ${inspect(value)}`);
        }
        break;
      case "dictionary":
        if (typeof value !== "object" || value === null) {
          throw new Error(`Expected object, got ${inspect(value)}`);
        }
        for (const { key, type } of this.data as DictionaryField[]) {
          if (key in value) {
            type.assertDefaultIsValid(value[key]);
          } else if (type.flags.required) {
            throw new Error(`Missing key ${key} in dictionary`);
          }
        }
        break;
      case "undefined":
        assert(value === undefined, `Expected undefined, got ${inspect(value)}`);
        break;
      default:
        throw new Error(`TODO: set default value on type ${this.kind}`);
    }
  }

  emitCppDefaultValue(w: CodeWriter) {
    const value = this.flags.default;
    switch (this.kind) {
      case "boolean":
        w.add(value ? "true" : "false");
        break;
      case "f64":
        w.add(String(value));
        break;
      case "usize":
      case "u8":
      case "u16":
      case "u32":
      case "u64":
      case "i8":
      case "i16":
      case "i32":
      case "i64":
        w.add(String(value));
        break;
      case "dictionary":
        const struct = this.structType();
        w.line(`${this.cppName()} {`);
        w.indent();
        for (const { name } of struct.fields) {
          w.add(`.${name} = `);
          const type = this.data.find(f => f.key === name)!.type;
          type.emitCppDefaultValue(w);
          w.line(",");
        }
        w.dedent();
        w.add(`}`);
        break;
      case "DOMString":
      case "ByteString":
      case "USVString":
      case "UTF8String":
        if (typeof value === "string") {
          w.add("Bun::BunStringEmpty");
        } else {
          throw new Error(`TODO: non-empty string default`);
        }
        break;
      case "undefined":
        throw new Error("Zero-sized type");
      default:
        throw new Error(`TODO: set default value on type ${this.kind}`);
    }
  }

  structType() {
    const direct = this.canDirectlyMapToCAbi();
    assert(typeof direct !== "string");
    if (direct) return direct;
    throw new Error("TODO: generate non-extern struct for representing this data type");
  }

  isIgnoredUndefinedType() {
    return this.kind === "undefined";
  }

  isStringType() {
    return (
      this.kind === "DOMString" || this.kind === "ByteString" || this.kind === "USVString" || this.kind === "UTF8String"
    );
  }

  isNumberType() {
    return numericTypes.has(this.kind);
  }

  isObjectType() {
    return this.kind === "externalClass" || this.kind === "dictionary";
  }

  [Symbol.toStringTag] = "Type";
  [Bun.inspect.custom](depth, options, inspect) {
    return (
      `${options.stylize("Type", "special")} ${
        this.lowersToNamedType() && this.nameDeduplicated
          ? options.stylize(JSON.stringify(this.nameDeduplicated), "string") + " "
          : ""
      }${options.stylize(
        `[${this.kind}${["required", "optional", "nullable"]
          .filter(k => this.flags[k])
          .map(x => ", " + x)
          .join("")}]`,
        "regexp",
      )}` +
      (this.data
        ? " " +
          inspect(this.data, {
            ...options,
            depth: options.depth === null ? null : options.depth - 1,
          }).replace(/\n/g, "\n")
        : "")
    );
  }

  // Public interface definition API
  get optional() {
    if (this.flags.required) {
      throw new Error("Cannot derive optional on a required type");
    }
    if (this.flags.default) {
      throw new Error("Cannot derive optional on a something with a default value (default implies optional)");
    }
    return new TypeImpl(this.kind, this.data, {
      ...this.flags,
      optional: true,
    });
  }

  get finite() {
    if (this.kind !== "f64") {
      throw new Error("finite can only be used on f64");
    }
    if (this.flags.finite) {
      throw new Error("This type already has finite set");
    }
    return new TypeImpl(this.kind, this.data, {
      ...this.flags,
      finite: true,
    });
  }

  get required() {
    if (this.flags.required) {
      throw new Error("This type already has required set");
    }
    if (this.flags.required) {
      throw new Error("Cannot derive required on an optional type");
    }
    return new TypeImpl(this.kind, this.data, {
      ...this.flags,
      required: true,
    });
  }

  default(def: any) {
    if ("default" in this.flags) {
      throw new Error("This type already has a default value");
    }
    if (this.flags.required) {
      throw new Error("Cannot derive default on a required type");
    }
    this.assertDefaultIsValid(def);
    return new TypeImpl(this.kind, this.data, {
      ...this.flags,
      default: def,
    });
  }

  clamp(min?: number | bigint, max?: number | bigint) {
    return this.#rangeModifier(min, max, "clamp");
  }

  enforceRange(min?: number | bigint, max?: number | bigint) {
    return this.#rangeModifier(min, max, "enforce");
  }

  get nonNull() {
    if (this.flags.nonNull) {
      throw new Error("Cannot derive nonNull on a nonNull type");
    }
    return new TypeImpl(this.kind, this.data, {
      ...this.flags,
      nonNull: true,
    });
  }

  validateInt32(min?: number, max?: number) {
    if (this.kind !== "i32") {
      throw new Error("validateInt32 can only be used on i32 or u32");
    }
    const rangeInfo = cAbiIntegerLimits("i32");
    return this.validateInteger(min ?? rangeInfo[0], max ?? rangeInfo[1]);
  }

  validateUint32(min?: number, max?: number) {
    if (this.kind !== "u32") {
      throw new Error("validateUint32 can only be used on i32 or u32");
    }
    const rangeInfo = cAbiIntegerLimits("u32");
    return this.validateInteger(min ?? rangeInfo[0], max ?? rangeInfo[1]);
  }

  validateInteger(min?: number | bigint, max?: number | bigint) {
    min ??= Number.MIN_SAFE_INTEGER;
    max ??= Number.MAX_SAFE_INTEGER;
    const enforceRange = this.#rangeModifier(min, max, "enforce") as TypeImpl;
    enforceRange.flags.nodeValidator = NodeValidator.validateInteger;
    return enforceRange;
  }
}

export function cAbiIntegerLimits(type: CAbiType) {
  switch (type) {
    case "u8":
      return [0, 255];
    case "u16":
      return [0, 65535];
    case "u32":
      return [0, 4294967295];
    case "u64":
      return [0, 18446744073709551615n];
    case "usize":
      return [0, 18446744073709551615n];
    case "i8":
      return [-128, 127];
    case "i16":
      return [-32768, 32767];
    case "i32":
      return [-2147483648, 2147483647];
    case "i64":
      return [-9223372036854775808n, 9223372036854775807n];
    case "f64":
      return [-Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER];
    default:
      throw new Error(`Unexpected type ${type}`);
  }
}

export function cAbiTypeForEnum(length: number): CAbiType {
  return ("u" + alignForward(length, 8)) as CAbiType;
}

export function inspect(value: any) {
  return Bun.inspect(value, { colors: Bun.enableANSIColors });
}

export function oneOfImpl(types: TypeImpl[]): TypeImpl {
  const out: TypeImpl[] = [];
  for (const type of types) {
    if (type.kind === "oneOf") {
      out.push(...type.data);
    } else {
      if (type.flags.default) {
        throw new Error(
          "Union type cannot include a default value. Instead, set a default value on the union type itself",
        );
      }
      if (type.isVirtualArgument()) {
        throw new Error(`t.${type.kind} can only be used as a function argument type`);
      }
      out.push(type);
    }
  }
  return new TypeImpl("oneOf", out);
}

export function dictionaryImpl(record: Record<string, TypeImpl>): TypeImpl {
  const out: DictionaryField[] = [];
  for (const key in record) {
    const type = record[key];
    if (type.isVirtualArgument()) {
      throw new Error(`t.${type.kind} can only be used as a function argument type`);
    }
    out.push({
      key,
      type: type,
    });
  }
  return new TypeImpl("dictionary", out);
}

export const isFunc = Symbol("isFunc");

export interface Func {
  [isFunc]: true;
  name: string;
  zigPrefix: string;
  snapshot: string;
  zigFile: string;
  variants: Variant[];
}

export interface Variant {
  suffix: string;
  args: Arg[];
  ret: TypeImpl;
  returnStrategy?: ReturnStrategy;
  argStruct?: Struct;
  globalObjectArg?: number | "hidden";
  minRequiredArgs: number;
  communicationStruct?: Struct;
}

export interface Arg {
  name: string;
  type: TypeImpl;
  loweringStrategy?: ArgStrategy;
  zigMappedName?: string;
}

/**
 * The strategy for moving arguments over the ABI boundary are computed before
 * any code is generated so that the proper definitions can be easily made,
 * while allow new special cases to be added.
 */
export type ArgStrategy =
  // The argument is communicated as a C parameter
  | { type: "c-abi-pointer"; abiType: CAbiType }
  // The argument is communicated as a C parameter
  | { type: "c-abi-value"; abiType: CAbiType }
  // The data is added as a field on `.communicationStruct`
  | {
      type: "uses-communication-buffer";
      /**
       * Unique prefix for fields. For example, moving an optional over the ABI
       * boundary uses two fields, `bool {prefix}_set` and `T {prefix}_value`.
       */
      prefix: string;
      /**
       * For compound complex types, such as `?union(enum) { a: u32, b:
       * bun.String }`, the child item is assigned the prefix
       * `{prefix_of_optional}_value`. The interpretation of this array depends
       * on `arg.type.kind`.
       */
      children: ArgStrategyChildItem[];
    };

export type ArgStrategyChildItem =
  | {
      type: "c-abi-compatible";
      abiType: CAbiType;
    }
  | {
      type: "uses-communication-buffer";
      prefix: string;
      children: ArgStrategyChildItem[];
    };
/**
 * In addition to moving a payload over, an additional bit of information
 * crosses the ABI boundary indicating if the function threw an exception.
 *
 * For simplicity, the possibility of any Zig binding returning an error/calling
 * `throw` is assumed and there isnt a way to disable the exception check.
 */
export type ReturnStrategy =
  // JSValue is special cased because it encodes exception as 0x0
  | { type: "jsvalue" }
  // Return value doesnt exist. function returns a boolean indicating success/error.
  | { type: "void" }
  // For primitives and simple structures where direct assignment into a
  // pointer is possible. function returns a boolean indicating success/error.
  | { type: "basic-out-param"; abiType: CAbiType };

export interface File {
  functions: Func[];
  typedefs: TypeDef[];
}

export interface TypeDef {
  name: string;
  type: TypeImpl;
}

export function registerFunction(opts: FuncOptions) {
  const snapshot = snapshotCallerLocation();
  const filename = stackTraceFileName(snapshot);
  expect(filename).toEndWith(".bind.ts");
  const zigFile = path.relative(src, filename.replace(/\.bind\.ts$/, ".zig"));
  let file = files.get(zigFile);
  if (!file) {
    file = { functions: [], typedefs: [] };
    files.set(zigFile, file);
  }
  const variants: Variant[] = [];
  if ("variants" in opts) {
    let i = 1;
    for (const variant of opts.variants) {
      const { minRequiredArgs } = validateVariant(variant);
      variants.push({
        args: Object.entries(variant.args).map(([name, type]) => ({ name, type })) as Arg[],
        ret: variant.ret as TypeImpl,
        suffix: `${i}`,
        minRequiredArgs,
      } as unknown as Variant);
      i++;
    }
  } else {
    const { minRequiredArgs } = validateVariant(opts);
    variants.push({
      suffix: "",
      args: Object.entries(opts.args).map(([name, type]) => ({ name, type })) as Arg[],
      ret: opts.ret as TypeImpl,
      minRequiredArgs,
    });
  }

  const func: Func = {
    [isFunc]: true,
    name: "",
    zigPrefix: opts.implNamespace ? `${opts.implNamespace}.` : "",
    snapshot,
    zigFile,
    variants,
  };
  allFunctions.push(func);
  file.functions.push(func);
  return func;
}

function validateVariant(variant: any) {
  let minRequiredArgs = 0;
  let seenOptionalArgument = false;
  let i = 0;

  for (const [name, type] of Object.entries(variant.args) as [string, TypeImpl][]) {
    if (!(type instanceof TypeImpl)) {
      throw new Error(`Expected type for argument ${name}, got ${inspect(type)}`);
    }
    i += 1;
    if (type.isVirtualArgument()) {
      continue;
    }
    if (!type.flags.optional && !("default" in type.flags)) {
      if (seenOptionalArgument) {
        throw new Error(`Required argument ${name} cannot follow an optional argument`);
      }
      minRequiredArgs++;
    } else {
      seenOptionalArgument = true;
    }
  }

  return { minRequiredArgs };
}

function snapshotCallerLocation(): string {
  const stack = new Error().stack!;
  const lines = stack.split("\n");
  let i = 1;
  for (; i < lines.length; i++) {
    if (!lines[i].includes(import.meta.dir)) {
      return lines[i];
    }
  }
  throw new Error("Couldn't find caller location in stack trace");
}

function stackTraceFileName(line: string): string {
  const match = /(?:at\s+|\()(.:?[^:\n(\)]*)[^(\n]*$/i.exec(line);
  assert(match, `Couldn't extract filename from stack trace line: ${line}`);
  return match[1].replaceAll("\\", "/");
}

export type CAbiType =
  | "*anyopaque"
  | "*JSGlobalObject"
  | "JSValue"
  | "JSValue.MaybeException"
  | "u0"
  | "bun.String"
  | "bool"
  | "u8"
  | "u16"
  | "u32"
  | "u64"
  | "usize"
  | "i8"
  | "i16"
  | "i32"
  | "i64"
  | "f64"
  | Struct;

export function cAbiTypeInfo(type: CAbiType): [size: number, align: number] {
  if (typeof type !== "string") {
    return type.abiInfo();
  }
  switch (type) {
    case "u0":
      return [0, 0]; // no-op
    case "bool":
    case "u8":
    case "i8":
      return [1, 1];
    case "u16":
    case "i16":
      return [2, 2];
    case "u32":
    case "i32":
      return [4, 4];
    case "usize":
    case "u64":
    case "i64":
    case "f64":
      return [8, 8];
    case "*anyopaque":
    case "*JSGlobalObject":
    case "JSValue":
    case "JSValue.MaybeException":
      return [8, 8]; // pointer size
    case "bun.String":
      return [24, 8];
    default:
      throw new Error("unexpected: " + (type satisfies never));
  }
}

export function cAbiTypeName(type: CAbiType) {
  if (typeof type !== "string") {
    return type.name();
  }
  return (
    {
      "*anyopaque": "void*",
      "*JSGlobalObject": "JSC::JSGlobalObject*",
      "JSValue": "JSValue",
      "JSValue.MaybeException": "JSValue",
      "bool": "bool",
      "u8": "uint8_t",
      "u16": "uint16_t",
      "u32": "uint32_t",
      "u64": "uint64_t",
      "i8": "int8_t",
      "i16": "int16_t",
      "i32": "int32_t",
      "i64": "int64_t",
      "f64": "double",
      "usize": "size_t",
      "bun.String": "BunString",
      u0: "void",
    } satisfies Record<Extract<CAbiType, string>, string>
  )[type];
}

export function alignForward(size: number, alignment: number) {
  return Math.floor((size + alignment - 1) / alignment) * alignment;
}

export class Struct {
  fields: StructField[] = [];
  #hash?: string;
  #name?: string;
  namespace?: string;

  abiInfo(): [size: number, align: number] {
    let size = 0;
    let align = 0;
    for (const field of this.fields) {
      size = alignForward(size, field.naturalAlignment);
      size += field.size;
      align = Math.max(align, field.naturalAlignment);
    }
    return [size, align];
  }

  reorderForSmallestSize() {
    // for conistency sort by alignment, then size, then name
    this.fields.sort((a, b) => {
      if (a.naturalAlignment !== b.naturalAlignment) {
        return a.naturalAlignment - b.naturalAlignment;
      }
      if (a.size !== b.size) {
        return a.size - b.size;
      }
      return a.name.localeCompare(b.name);
    });
  }

  hash() {
    return (this.#hash ??= String(
      Bun.hash(
        this.fields
          .map(f => {
            if (f.type instanceof Struct) {
              return f.name + `:` + f.type.hash();
            }
            return f.name + `:` + f.type;
          })
          .join(","),
      ),
    ));
  }

  name() {
    if (this.#name) return this.#name;
    const hash = this.hash();
    const existing = structHashToSelf.get(hash);
    if (existing && existing !== this) return (this.#name = existing.name());
    return (this.#name = `anon_extern_struct_${hash}`);
  }

  toString() {
    return this.namespace ? `${this.namespace}.${this.name()}` : this.name();
  }

  assignName(name: string) {
    if (this.#name) return;
    const hash = this.hash();
    const existing = structHashToSelf.get(hash);
    if (existing && existing.#name) name = existing.#name;
    this.#name = name;
    if (existing) existing.#name = name;
  }

  assignGeneratedName(name: string) {
    if (this.#name) return;
    this.assignName(name);
  }

  add(name: string, cType: CAbiType) {
    const [size, naturalAlignment] = cAbiTypeInfo(cType);
    this.fields.push({ name, type: cType, size, naturalAlignment });
  }

  emitZig(zig: CodeWriter, semi: "with-semi" | "no-semi") {
    zig.line("extern struct {");
    zig.indent();
    for (const field of this.fields) {
      zig.line(`${snake(field.name)}: ${field.type},`);
    }
    zig.dedent();
    zig.line("}" + (semi === "with-semi" ? ";" : ""));
  }

  emitCpp(cpp: CodeWriter, structName: string) {
    cpp.line(`struct ${structName} {`);
    cpp.indent();
    for (const field of this.fields) {
      cpp.line(`${cAbiTypeName(field.type)} ${field.name};`);
    }
    cpp.dedent();
    cpp.line("};");
  }
}

export interface StructField {
  /** camel case */
  name: string;
  type: CAbiType;
  size: number;
  naturalAlignment: number;
}

export class CodeWriter {
  level = 0;
  buffer = "";

  temporaries = new Set<string>();

  line(s?: string) {
    this.add((s ?? "") + "\n");
  }

  add(s: string) {
    this.buffer += (this.buffer.endsWith("\n") ? "    ".repeat(this.level) : "") + s;
  }

  indent() {
    this.level += 1;
  }

  dedent() {
    this.level -= 1;
  }

  trimLastNewline() {
    this.buffer = this.buffer.trimEnd();
  }

  resetTemporaries() {
    this.temporaries.clear();
  }

  nextTemporaryName(label: string) {
    let i = 0;
    let name = `${label}_${i}`;
    while (this.temporaries.has(name)) {
      i++;
      name = `${label}_${i}`;
    }
    this.temporaries.add(name);
    return name;
  }
}
