core/registry.js

import { NOT_EDITABLE } from "./flags.js";
import { Effect } from "./effect.js";

/**
 * 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.get("Floor");
 *   Registry.serialize(floor); // → "Floor"
 */

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

export const Registry = {
  /**
   * Register a Serializer for a type ID.
   * Also accepts a bare factory function for backward compatibility during
   * migration — these are wrapped in a minimal shim.
   * @param {string} typeId
   * @param {Serializer|function} serializerOrFactory
   */
  register(typeId, serializerOrFactory) {
    if (typeof serializerOrFactory === "function") {
      // Legacy bare factory — wrap automatically
      _serializers.set(typeId, _wrapFactory(typeId, serializerOrFactory));
    } else {
      _serializers.set(typeId, serializerOrFactory);
    }
  },

  /**
   * Get (or create and cache) a piece by its serialized key.
   * @param {string} key  e.g. "Floor" or "TriggerOnceOnDrop|Altar^none|Blue|Chalice"
   * @returns {Piece}
   */
  get(key) {
    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 = rawArgs.map((arg) =>
      arg.includes("^") ? this.get(arg.replace(/\^/g, "|")) : arg,
    );

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

  /**
   * Return the serialized key for a piece created via Registry.get().
   * Throws if the piece was not created through the registry.
   * @param {Piece} piece
   * @returns {string}
   */
  serialize(piece) {
    const key = _reverse.get(piece);
    if (key === undefined)
      throw new Error("Piece was not created via Registry");
    return key;
  },

  /**
   * Parse a serialized key and return the piece. Alias for get().
   * @param {string} str
   * @returns {Piece}
   */
  deserialize(str) {
    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().
   * @returns {Map<string, Map<string, string>>}
   */
  getSerializersByTag() {
    const result = new Map();
    for (const [typeId, ser] of _serializers) {
      let ex;
      try {
        ex = ser.example();
      } catch {
        continue; // legacy shim with no example
      }
      if (ex?.is?.(NOT_EDITABLE)) continue;
      if (ex instanceof Effect) continue;
      let tag;
      try {
        tag = ser.tag();
      } catch {
        continue;
      }
      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.
   * @param {string} typeId
   * @returns {Piece}
   */
  getExample(typeId) {
    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();
  },
};

// ── Helpers ───────────────────────────────────────────────────────────────────

/**
 * Wrap a legacy bare factory function in a minimal Serializer-shaped object.
 * Provides create() only; tag/example/template/store throw (Uncategorised).
 * @param {string} typeId
 * @param {function} factory
 */
function _wrapFactory(typeId, factory) {
  return {
    create: factory,
    store() {
      throw new Error(`${typeId}: no Serializer (legacy factory)`);
    },
    example() {
      throw new Error(`${typeId}: no Serializer (legacy factory)`);
    },
    template() {
      return typeId;
    },
    tag() {
      return "Uncategorised";
    },
  };
}