// SPDX-FileCopyrightText: 2026 Stan Grams <sjg@haxx.space>
//
// SPDX-License-Identifier: GPL-2.0-or-later

import type { DecoderPlugin, PluginRuntimeWindow, TrxPluginRuntime } from "./plugins/runtime-contract";

type QueuedAction =
  | { kind: "message"; payload: unknown }
  | { kind: "batch" | "restore"; payload: unknown[] };

const decoders = new Map<string, DecoderPlugin>();
const queued = new Map<string, QueuedAction[]>();
const MAX_QUEUED_ACTIONS_PER_DECODER = 512;

function enqueue(id: string, action: QueuedAction): void {
  const actions = queued.get(id) ?? [];
  actions.push(action);
  if (actions.length > MAX_QUEUED_ACTIONS_PER_DECODER) actions.splice(0, actions.length - MAX_QUEUED_ACTIONS_PER_DECODER);
  queued.set(id, actions);
}

function deliver(plugin: DecoderPlugin, action: QueuedAction): boolean {
  if (action.kind === "message" && plugin.onMessage) {
    plugin.onMessage(action.payload);
    return true;
  }
  if (action.kind === "batch" && plugin.onBatch) {
    plugin.onBatch(action.payload);
    return true;
  }
  if (action.kind === "restore" && plugin.restore) {
    plugin.restore(action.payload);
    return true;
  }
  return false;
}

function dispatchOrQueue(id: string, action: QueuedAction): boolean {
  const plugin = decoders.get(id);
  if (!plugin) {
    enqueue(id, action);
    return false;
  }
  if (!deliver(plugin, action)) {
    if (action.kind === "batch" && plugin.onMessage) {
      for (const message of action.payload) plugin.onMessage(message);
      return true;
    }
    if (action.kind === "restore" && plugin.onBatch) {
      plugin.onBatch(action.payload);
      return true;
    }
    return false;
  }
  return true;
}

const runtime: TrxPluginRuntime = {
  registerDecoder<TMessage>(plugin: DecoderPlugin<TMessage>): () => void {
    if (decoders.has(plugin.id)) throw new Error(`Decoder plugin already registered: ${plugin.id}`);
    const erased = plugin as DecoderPlugin;
    decoders.set(plugin.id, erased);
    const pending = queued.get(plugin.id) ?? [];
    queued.delete(plugin.id);
    for (const action of pending) deliver(erased, action);
    return () => { if (decoders.get(plugin.id) === erased) decoders.delete(plugin.id); };
  },
  dispatch: (id, message) => dispatchOrQueue(id, { kind: "message", payload: message }),
  dispatchBatch: (id, messages) => dispatchOrQueue(id, { kind: "batch", payload: messages }),
  restore: (id, messages) => dispatchOrQueue(id, { kind: "restore", payload: messages }),
  reset(id) {
    const plugin = decoders.get(id);
    if (!plugin?.reset) return false;
    plugin.reset();
    return true;
  },
  resetAll() { for (const plugin of decoders.values()) plugin.reset?.(); },
  prune(id) {
    const plugin = decoders.get(id);
    if (!plugin?.prune) return false;
    plugin.prune();
    return true;
  },
  clearQueued() { queued.clear(); },
  hasDecoder: (id) => decoders.has(id),
};

(window as unknown as PluginRuntimeWindow).trxPluginRuntime = runtime;
