text: import * as spec from '@jsii/spec';
  import * as api from './api';
  import { EMPTY_OBJECT_FQN } from './serialization';
  
  /**
   * Symbol under which we store the { type -> objid } map on object instances
   */
  const OBJID_SYMBOL = Symbol.for('$__jsii__objid__$');
  
  /**
   * Symbol under which we store the interfaces implemented by instances
   */
  const IFACES_SYMBOL = Symbol.for('$__jsii__interfaces__$');
  
  /**
   * Symbol we use to tag the constructor of a JSII class
   */
  const JSII_SYMBOL = Symbol.for('__jsii__');
  
  /**
   * Get the JSII fqn for an object (if available)
   *
   * This will return something if the object was constructed from a JSII-enabled
   * class/constructor, or if a literal object was annotated with type
   * information.
   */
  export function jsiiTypeFqn(obj: any): string | undefined {
    return obj.constructor[JSII_SYMBOL]?.fqn;
  }
  
  /**
   * If this object was previously serialized under a given reference, return the same reference
   *
   * This is to retain object identity across invocations.
   */
  export function objectReference(obj: unknown): api.AnnotatedObjRef | undefined {
    // If this object as already returned
    if ((obj as any)[OBJID_SYMBOL]) {
      return {
        [api.TOKEN_REF]: (obj as ManagedObject)[OBJID_SYMBOL],
        [api.TOKEN_INTERFACES]: (obj as ManagedObject)[IFACES_SYMBOL],
      };
    }
  
    return undefined;
  }
  
  type ManagedObject = {
    [OBJID_SYMBOL]: string;
    [IFACES_SYMBOL]?: string[];
  };
  
  function tagObject(obj: unknown, objid: string, interfaces?: string[]) {
    const managed = obj as ManagedObject;
    managed[OBJID_SYMBOL] = objid;
    managed[IFACES_SYMBOL] = interfaces;
  }
  
  /**
   * Set the JSII FQN for classes produced by a given constructor
   */
  export function tagJsiiConstructor(constructor: any, fqn: string) {
    Object.defineProperty(constructor, JSII_SYMBOL, {
      configurable: false,
      enumerable: false,
      writable: false,
      value: { fqn },
    });
  }
  
  /**
   * Table of JSII objects
   *
   * There can be multiple references to the same object, each under a different requested
   * type.
   */
  export class ObjectTable {
    private objects: { [objid: string]: RegisteredObject } = {};
    private nextid = 10000;
  
    public constructor(
      private readonly resolveType: (fqn: string) => spec.Type,
    ) {}
  
    /**
     * Register the given object with the given type
     *
     * Return the existing registration if available.
     */
    public registerObject(
      obj: unknown,
      fqn: string,
      interfaces?: string[],
    ): api.AnnotatedObjRef {
      if (fqn === undefined) {
        throw new Error('FQN cannot be undefined');
      }
  
      const existingRef = objectReference(obj);
      if (existingRef) {
        if (interfaces) {
          const allIfaces = new Set(interfaces);
          for (const iface of existingRef[api.TOKEN_INTERFACES] ?? []) {
            allIfaces.add(iface);
          }
          this.objects[existingRef[api.TOKEN_REF]].interfaces = (obj as any)[
            IFACES_SYMBOL
          ] = existingRef[
            api.TOKEN_INTERFACES
          ] = interfaces = this.removeRedundant(Array.from(allIfaces), fqn);
        }
        return existingRef;
      }
  
      interfaces = this.removeRedundant(interfaces, fqn);
  
      const objid = this.makeId(fqn);
      this.objects[objid] = { instance: obj, fqn, interfaces };
      tagObject(obj, objid, interfaces);
  
      return { [api.TOKEN_REF]: objid, [api.TOKEN_INTERFACES]: interfaces };
    }
  
    /**
     * Find the object and registered type for the given ObjRef
     */
    public findObject(objref: api.ObjRef): RegisteredObject {
      if (typeof objref !== 'object' || !(api.TOKEN_REF in objref)) {
        throw new Error(`Malformed object reference: ${JSON.stringify(objref)}`);
      }
  
      const objid = objref[api.TOKEN_REF];
      const obj = this.objects[objid];
      if (!obj) {
        throw new Error(`Object ${objid} not found`);
      }
      return obj;
    }
  
    /**
     * Delete the registration with the given objref
     */
    public deleteObject(objref: api.ObjRef) {
      this.findObject(objref); // make sure object exists
      delete this.objects[objref[api.TOKEN_REF]];
    }
  
    public get count(): number {
      return Object.keys(this.objects).length;
    }
  
    private makeId(fqn: string) {
      return `${fqn}@${this.nextid++}`;
    }
  
    private removeRedundant(
      interfaces: string[] | undefined,
      fqn: string,
    ): string[] | undefined {
      if (!interfaces || interfaces.length === 0) {
        return undefined;
      }
  
      const result = new Set(interfaces);
      const builtIn = new InterfaceCollection(this.resolveType);
  
      if (fqn !== EMPTY_OBJECT_FQN) {
        builtIn.addFromClass(fqn);
      }
      interfaces.forEach(builtIn.addFromInterface.bind(builtIn));
  
      for (const iface of builtIn) {
        result.delete(iface);
      }
  
      return result.size > 0 ? Array.from(result).sort() : undefined;
    }
  }
  
  export interface RegisteredObject {
    instance: any;
    fqn: string;
    interfaces?: string[];
  }
  
  class InterfaceCollection implements Iterable<string> {
    private readonly interfaces = new Set<string>();
  
    public constructor(
      private readonly resolveType: (fqn: string) => spec.Type,
    ) {}
  
    public addFromClass(fqn: string): void {
      const ti = this.resolveType(fqn);
      if (!spec.isClassType(ti)) {
        throw new Error(
          `Expected a class, but received ${spec.describeTypeReference(ti)}`,
        );
      }
      if (ti.base) {
        this.addFromClass(ti.base);
      }
      if (ti.interfaces) {
        for (const iface of ti.interfaces) {
          if (this.interfaces.has(iface)) {
            continue;
          }
          this.interfaces.add(iface);
          this.addFromInterface(iface);
        }
      }
    }
  
    public addFromInterface(fqn: string): void {
      const ti = this.resolveType(fqn);
      if (!spec.isInterfaceType(ti)) {
        throw new Error(
          `Expected an interface, but received ${spec.describeTypeReference(ti)}`,
        );
      }
      if (!ti.interfaces) {
        return;
      }
      for (const iface of ti.interfaces) {
        if (this.interfaces.has(iface)) {
          continue;
        }
        this.interfaces.add(iface);
        this.addFromInterface(iface);
      }
    }
  
    public [Symbol.iterator]() {
      return this.interfaces[Symbol.iterator]();
    }
  }
  