import path from "node:path";
import NodeErrors from "../bun.js/bindings/ErrorCode.ts";
import { writeIfNotChanged } from "./helpers.ts";
const outputDir = process.argv[2];

if (!outputDir) {
  throw new Error("Missing output directory");
}

const extra_count = NodeErrors.map(x => x.slice(3))
  .filter(x => x.length > 0)
  .reduce((ac, cv) => ac + cv.length, 0);
const count = NodeErrors.length + extra_count;

if (count > 1 << 16) {
  // increase size of the enums below to have more tags
  throw new Error(`NodeError can't fit ${count} codes in a u16`);
}

let enumHeader = ``;
let listHeader = ``;
let zig = ``;

enumHeader = `
// clang-format off
// Generated by: src/codegen/generate-node-errors.ts
// Input:        src/bun.js/bindings/ErrorCode.ts
#pragma once

#include <cstdint>

namespace Bun {
  static constexpr size_t NODE_ERROR_COUNT = ${count};
  enum class ErrorCode : uint16_t {
`;

listHeader = `
// clang-format off
// Generated by: src/codegen/generate-node-errors.ts
#pragma once

#include <JavaScriptCore/ErrorType.h>

struct ErrorCodeData {
    JSC::ErrorType type;
    WTF::ASCIILiteral name;
    WTF::ASCIILiteral code;
};
static constexpr ErrorCodeData errors[${count}] = {
`;

zig = `
// Generated by: src/codegen/generate-node-errors.ts
const std = @import("std");
const bun = @import("bun");
const JSC = bun.JSC;

pub fn ErrorBuilder(comptime code: Error, comptime fmt: [:0]const u8, Args: type) type {
  return struct {
      global: *JSC.JSGlobalObject,
      args: Args,

      // Throw this error as a JS exception
      pub inline fn throw(this: @This()) bun.JSError {
        return code.throw(this.global, fmt, this.args);
      }

      /// Turn this into a JSValue
      pub inline fn toJS(this: @This()) JSC.JSValue {
        return code.fmt(this.global, fmt, this.args);
      }

      /// Turn this into a JSPromise that is already rejected.
      pub inline fn reject(this: @This()) JSC.JSValue {
        if (comptime bun.FeatureFlags.breaking_changes_1_3) {
          return JSC.JSPromise.rejectedPromise(this.globalThis, code.fmt(this.global, fmt, this.args)).toJS();
        } else {
          return JSC.JSPromise.dangerouslyCreateRejectedPromiseValueWithoutNotifyingVM(this.global, code.fmt(this.global, fmt, this.args));
        }
      }
  };
}

pub const Error = enum(u16) {

`;

let i = 0;
for (let [code, constructor, name, ...other_constructors] of NodeErrors) {
  if (name == null) name = constructor.name;

  // it's useful to avoid the prefix, but module not found has a prefixed and unprefixed version
  const codeWithoutPrefix = code === "ERR_MODULE_NOT_FOUND" ? code : code.replace(/^ERR_/, "");

  enumHeader += `    ${code} = ${i},\n`;
  listHeader += `    { JSC::ErrorType::${constructor.name}, "${name}"_s, "${code}"_s },\n`;
  zig += `    /// ${name}: ${code} (instanceof ${constructor.name})\n`;
  zig += `    ${codeWithoutPrefix} = ${i},\n`;
  i++;

  for (const con of other_constructors) {
    if (con == null) continue;
    if (name == null) name = con.name;
    enumHeader += `    ${code}_${con.name} = ${i},\n`;
    listHeader += `    { JSC::ErrorType::${con.name}, "${con.name}"_s, "${code}"_s },\n`;
    zig += `    /// ${name}: ${code} (instanceof ${con.name})\n`;
    zig += `    ${codeWithoutPrefix}_${con.name} = ${i},\n`;
    i++;
  }
}

enumHeader += `
};
} // namespace Bun
`;

listHeader += `
};
`;

zig += `


  extern fn Bun__createErrorWithCode(globalThis: *JSC.JSGlobalObject, code: Error, message: *bun.String) JSC.JSValue;

  /// Creates an Error object with the given error code.
  /// Derefs the message string.
  pub fn toJS(this: Error, globalThis: *JSC.JSGlobalObject, message: *bun.String) JSC.JSValue {
    defer message.deref();
    return Bun__createErrorWithCode(globalThis, this, message);
  }

  pub fn fmt(this: Error, globalThis: *JSC.JSGlobalObject, comptime fmt_str: [:0]const u8, args: anytype) JSC.JSValue {
    if (comptime std.meta.fieldNames(@TypeOf(args)).len == 0) {
      var message = bun.String.static(fmt_str);
      return toJS(this, globalThis, &message);
    }

    var message = bun.String.createFormat(fmt_str, args) catch bun.outOfMemory();
    return toJS(this, globalThis, &message);
  }

  pub fn throw(this: Error, globalThis: *JSC.JSGlobalObject, comptime fmt_str: [:0]const u8, args: anytype) bun.JSError {
    return globalThis.throwValue(fmt(this, globalThis, fmt_str, args));
  }

};
`;

let builtindtsPath = path.join(import.meta.dir, "..", "..", "src", "js", "builtins.d.ts");
let builtindts = await Bun.file(builtindtsPath).text();

let dts = `
// Generated by: src/codegen/generate-node-errors.ts
// Input:        src/bun.js/bindings/ErrorCode.ts

// Global error code functions for TypeScript
`;
for (const [code, constructor, name, ...other_constructors] of NodeErrors) {
  const hasExistingOverride = builtindts.includes(`declare function $${code}`);
  if (hasExistingOverride) {
    continue;
  }

  const namedError =
    name && name !== constructor.name
      ? `${constructor.name} & { name: "${name}", code: "${code}" }`
      : `${constructor.name} & { code: "${code}" }`;
  dts += `
/**
 * Construct an {@link ${constructor.name} ${constructor.name}} with the \`"${code}"\` error code.
 *
 * To override this, update ErrorCode.cpp. To remove this generated type, mention \`"${code}"\` in builtins.d.ts.
 */
declare function $${code}(message: string): ${namedError};\n`;
}

writeIfNotChanged(path.join(outputDir, "ErrorCode+List.h"), enumHeader);
writeIfNotChanged(path.join(outputDir, "ErrorCode+Data.h"), listHeader);
writeIfNotChanged(path.join(outputDir, "ErrorCode.zig"), zig);
writeIfNotChanged(path.join(outputDir, "ErrorCode.d.ts"), dts);
