import { KdlStructuredError } from "./rpc.js";
import type { KdlError } from "./types.js";
import type { NativeKdlModule } from "./nativeModule.js";

export const KDL_C_ABI_EXPORTS = [
  "kdl_init",
  "kdl_create_robot",
  "kdl_destroy_robot",
  "kdl_get_robot_info",
  "kdl_fk",
  "kdl_fk_all_links",
  "kdl_jacobian",
  "kdl_ik",
  "kdl_plan_movej",
  "kdl_plan_movel",
  "kdl_plan_movec",
  "kdl_plan_path",
  "kdl_sample_trap",
  "kdl_last_error"
] as const;

export type KdlCAbiExport = (typeof KDL_C_ABI_EXPORTS)[number];

export class KdlNativeAbi {
  constructor(private readonly module: NativeKdlModule) {
    this.assertRuntimeMethods();
  }

  assertExports(names: readonly KdlCAbiExport[] = KDL_C_ABI_EXPORTS): void {
    for (const name of names) {
      try {
        if (typeof this.module.cwrap?.(name, "number", []) === "function") {
          continue;
        }
      } catch {
        // Normalized below.
      }
      {
        throw new KdlStructuredError("KDL_C_ABI_EXPORT_MISSING", `Missing C ABI export: ${name}`);
      }
    }
  }

  callNumber(ident: KdlCAbiExport, argTypes: Array<string | null>, args: unknown[]): number {
    return Number(this.module.ccall(ident, "number", argTypes, args));
  }

  checkReturnCode(returnCode: number): void {
    if (returnCode >= 0) {
      return;
    }

    const error = this.lastError();
    throw new KdlStructuredError(error.code, error.message, error.diagnostics);
  }

  readJsonCall<T>(ident: KdlCAbiExport, argTypes: Array<string | null>, args: unknown[], bytes = 16_384): T {
    const ptr = this.malloc(bytes);
    try {
      const returnCode = this.callNumber(ident, [...argTypes, "number", "number"], [...args, ptr, bytes]);
      this.checkReturnCode(returnCode);
      return JSON.parse(this.module.UTF8ToString?.(ptr) ?? "") as T;
    } finally {
      this.free(ptr);
    }
  }

  lastError(bytes = 16_384): KdlError {
    const ptr = this.malloc(bytes);
    try {
      const returnCode = this.callNumber("kdl_last_error", ["number", "number"], [ptr, bytes]);
      if (returnCode < 0) {
        return {
          code: "KDL_LAST_ERROR_FAILED",
          message: "kdl_last_error failed",
          diagnostics: [
            {
              severity: "error",
              code: "KDL_LAST_ERROR_FAILED",
              message: "kdl_last_error failed"
            }
          ]
        };
      }
      return JSON.parse(this.module.UTF8ToString?.(ptr) ?? "") as KdlError;
    } finally {
      this.free(ptr);
    }
  }

  private assertRuntimeMethods(): void {
    const missing = [
      ["cwrap", this.module.cwrap],
      ["UTF8ToString", this.module.UTF8ToString],
      ["_malloc", this.module._malloc],
      ["_free", this.module._free]
    ].flatMap(([name, value]) => (typeof value === "function" ? [] : [name as string]));

    if (missing.length > 0) {
      throw new KdlStructuredError(
        "KDL_C_ABI_RUNTIME_MISSING",
        `KDL native module is missing runtime methods: ${missing.join(", ")}`
      );
    }
  }

  private malloc(bytes: number): number {
    const ptr = this.module._malloc?.(bytes);
    if (!ptr) {
      throw new KdlStructuredError("KDL_WASM_ALLOC_FAILED", `Failed to allocate ${bytes} bytes`);
    }
    return ptr;
  }

  private free(ptr: number): void {
    this.module._free?.(ptr);
  }
}
