core/registry.ts

import { NOT_EDITABLE } from "./flags.ts";
import { Effect } from "./effect.ts";
import { Serializer } from "./serializer.ts";
import { Piece } from "./piece.ts";
import { Item } from "./item.ts";
import { Agent } from "./agent.ts";
import { Terrain } from "./terrain.ts";

/**
 * Registry — singleton cache for all piece instances.
 *
 * Pieces are immutable singletons identified by their serialization key
 * (pipe-delimited: "TypeName|arg1|arg2"). Nested piece references within
 * an arg use ^ as a sub-delimiter (e.g. "Altar^none" deserializes as
 * the piece from key "Altar|none").
 *
 * Usage:
 *   Registry.register("Floor", Floor.SERIALIZER);
 *   const floor = Registry.terrain("Floor");
 *   Registry.serialize(floor); // → "Floor"
 */

const _serializers = new Map<string, Serializer>(); // typeId → Serializer
const _cache = new Map<string, Piece>(); // serialized key → Piece
const _reverse = new Map<Piece, string>(); // Piece → serialized key

export const Registry = {
  /** Register a Serializer for a type ID. */
  register(typeId: string, serializer: Serializer) {
    _serializers.set(typeId, serializer);
  },

  item(key: string | Item): Item {
    return typeof key === "string" ? this.get(key) as Item : key;
  },

  agent(key: string | Agent): Agent {
    return typeof key === "string" ? this.get(key) as Agent : key;
  },

  terrain(key: string | Terrain): Terrain {
    return typeof key === "string" ? this.get(key) as Terrain : key;
  },

  effect(key: string | Effect): Effect {
    return typeof key === "string" ? this.get(key) as Effect : key;
  },

  /**
   * Get (or create and cache) a piece by its serialized key.
   */
  get(key: string): Piece | undefined {
    if (_cache.has(key)) {
      return _cache.get(key);
    }
    const parts = key.split("|");
    const typeId = parts[0];
    const rawArgs = parts.slice(1);

    const serializer = _serializers.get(typeId);
    if (!serializer) {
      throw new Error(`Unknown piece type: "${typeId}"`);
    }
    // Resolve nested piece references: "Altar^none" → Registry.get("Altar|none")
    const args: Array<string | Piece> = rawArgs.map((arg) =>
      arg.includes("^") ? this.get(arg.replace(/\^/g, "|")) ?? arg : arg,
    );

    const piece = serializer.create(args);
    _cache.set(key, piece);
    _reverse.set(piece, key);
    return piece;
  },

  /**
   * Return the serialized key for a piece.
   * Prefers the cached reverse-lookup for pieces created via Registry.get().
   * Falls back to piece.constructor.SERIALIZER.store() for pieces that were
   * constructed directly (e.g. Statue wrappers applied at runtime).
   */
  serialize(piece: Piece): string {
    const key = _reverse.get(piece);
    if (key !== undefined) {
      return key;
    }
    const serializer = (piece.constructor as { SERIALIZER?: Serializer }).SERIALIZER;
    if (!serializer) {
      throw new Error("Piece was not created via Registry");
    }
    return serializer.store(piece);
  },

  /**
   * Parse a serialized key and return the piece. Alias for get().
   */
  deserialize(str: string): Piece | undefined {
    return this.get(str);
  },

  /**
   * Return a Map of tag → Map<typeId, template> for all registered types.
   * Excludes types whose example piece has the NOT_EDITABLE flag or is an Effect.
   * Mirrors Java's Registry.getSerializersByTags().
   */
  getSerializersByTag(): Map<string, Map<string, string>> {
    const result = new Map();
    for (const [typeId, ser] of _serializers) {
      if (typeof ser.example !== "function" || typeof ser.tag !== "function") {
        continue; // structurally missing, not "threw for some other reason"
      }
      const ex = ser.example();
      if (ex.is(NOT_EDITABLE)) continue;
      if (ex instanceof Effect) continue;
      const tag = ser.tag();
      if (!result.has(tag)) result.set(tag, new Map());
      result.get(tag).set(typeId, ser.template(typeId));
    }
    return result;
  },

  /**
   * Return an example piece for the given typeId.
   * Used by the editor to render a preview of each type.
   */
  getExample(typeId: string): Piece {
    const ser = _serializers.get(typeId);
    if (!ser) throw new Error(`Unknown piece type: "${typeId}"`);
    return ser.example();
  },

  /** Clear all registrations and cache. Intended for use in tests only. */
  reset() {
    _serializers.clear();
    _cache.clear();
    _reverse.clear();
  },
};