(** {1 VM extension registry}

    Holds the live registry of extension opcodes and installs the
    [dispatch] function into [Sx_vm.extension_dispatch_ref] at module
    init time, replacing Phase A's stub.

    See [plans/sx-vm-opcode-extension.md] and [Sx_vm_extension] for the
    extension interface. *)

open Sx_vm_extension

(** The opcode range an extension is allowed to claim.
    Mirrors the partition comment in [Sx_vm]. *)
let extension_min = 200
let extension_max = 247

(** opcode_id → handler *)
let by_id : (int, handler) Hashtbl.t = Hashtbl.create 64

(** opcode_name → opcode_id *)
let by_name : (string, int) Hashtbl.t = Hashtbl.create 64

(** opcode_id → opcode_name (reverse of [by_name]; used by
    [Sx_vm.opcode_name] for disassembly). *)
let name_of_id_table : (int, string) Hashtbl.t = Hashtbl.create 64

(** extension_name → state *)
let states : (string, extension_state) Hashtbl.t = Hashtbl.create 8

(** Registered extension names, newest first. *)
let extensions : string list ref = ref []

(** Dispatch an extension opcode to its registered handler. Raises
    [Sx_vm.Invalid_opcode] if no handler is registered for [op]. *)
let dispatch op vm frame =
  match Hashtbl.find_opt by_id op with
  | Some handler -> handler vm frame
  | None -> raise (Sx_vm.Invalid_opcode op)

(** Register an extension. Fails if the extension name is already
    registered, or if any opcode_id is outside the extension range or
    collides with an already-registered opcode. *)
let register (m : (module EXTENSION)) =
  let module M = (val m) in
  if Hashtbl.mem states M.name then
    failwith (Printf.sprintf
      "Sx_vm_extensions: extension %S already registered" M.name);
  let st = M.init () in
  let ops = M.opcodes st in
  List.iter (fun (id, opname, _h) ->
    if id < extension_min || id > extension_max then
      failwith (Printf.sprintf
        "Sx_vm_extensions: opcode %d (%s) outside extension range %d-%d"
        id opname extension_min extension_max);
    if Hashtbl.mem by_id id then
      failwith (Printf.sprintf
        "Sx_vm_extensions: opcode %d (%s) already registered" id opname);
    if Hashtbl.mem by_name opname then
      failwith (Printf.sprintf
        "Sx_vm_extensions: opcode name %S already registered" opname)
  ) ops;
  Hashtbl.add states M.name st;
  List.iter (fun (id, opname, h) ->
    Hashtbl.add by_id id h;
    Hashtbl.add by_name opname id;
    Hashtbl.add name_of_id_table id opname
  ) ops;
  extensions := M.name :: !extensions

(** Look up the opcode_id for an opcode_name. Returns [None] if no
    extension provides that opcode. *)
let id_of_name name = Hashtbl.find_opt by_name name

(** Look up the opcode_name for an opcode_id. Returns [None] if no
    extension provides that opcode. Used by disassembly. *)
let name_of_id id = Hashtbl.find_opt name_of_id_table id

(** Look up the state of an extension by name. Returns [None] if the
    extension is not registered. *)
let state_of_extension name = Hashtbl.find_opt states name

(** Names of all registered extensions, newest first. *)
let registered_extensions () = !extensions

(** Test-only: clear the registry. Used by unit tests to isolate
    extensions between test cases. The dispatch_ref is left in place. *)
let _reset_for_tests () =
  Hashtbl.clear by_id;
  Hashtbl.clear by_name;
  Hashtbl.clear name_of_id_table;
  Hashtbl.clear states;
  extensions := []

(** Install our [dispatch] into [Sx_vm.extension_dispatch_ref] and our
    [name_of_id] into [Sx_vm.extension_opcode_name_ref], replacing
    the Phase A stubs. Idempotent. Called automatically at module init. *)
let install_dispatch () =
  Sx_vm.extension_dispatch_ref := dispatch;
  Sx_vm.extension_opcode_name_ref := name_of_id

let () = install_dispatch ()

(** Compiler-side opcode lookup: register the [extension-opcode-id]
    primitive. Compilers ([lib/compiler.sx]) call this to emit
    extension opcodes by name. Returns [Integer id] when registered,
    [Nil] otherwise — so missing extensions degrade to a fallback
    rather than failure. *)
let () =
  Sx_primitives.register "extension-opcode-id" (fun args ->
    match args with
    | [Sx_types.String name] ->
      (match id_of_name name with
       | Some id -> Sx_types.Integer id
       | None -> Sx_types.Nil)
    | [Sx_types.Symbol name] ->
      (match id_of_name name with
       | Some id -> Sx_types.Integer id
       | None -> Sx_types.Nil)
    | _ -> raise (Sx_types.Eval_error
        "extension-opcode-id: expected one string or symbol"))
