import type { ReactNode } from 'react';
import type { TTSObject } from '@tts/shared';

/**
 * A viewer renders a single selected object. Custom viewers can be registered
 * per object class (`Name`) and will take precedence over the default viewer.
 */
export interface ObjectViewer {
  /** The object class this viewer handles, e.g. `Card`, `Bag`. */
  name: string;
  component: (props: ViewerProps) => ReactNode;
}

/** Props passed to every object viewer. */
export interface ViewerProps {
  object: TTSObject;
  /** Expand the 3D scene to fill its container instead of the default frame. */
  fill?: boolean;
}

const registry = new Map<string, ObjectViewer['component']>();

/** Register a viewer for a specific object class. */
export function registerViewer(name: string, component: ObjectViewer['component']) {
  registry.set(name, component);
}

/** Resolve the viewer for an object, falling back to the default viewer. */
export function resolveViewer(object: TTSObject): ObjectViewer['component'] {
  return registry.get(object.Name) ?? DefaultViewer;
}

/** The default viewer: a plain inspection of the object's fields. */
export function DefaultViewer({ object }: ViewerProps) {
  return (
    <dl className="grid grid-cols-1 gap-2 text-sm sm:grid-cols-2">
      {Object.entries(object).map(([key, value]) => {
        // `Parent` is a back-reference added during traversal; it creates a
        // cycle with `ContainedObjects`, so skip it in the default view.
        if (key === 'Parent') return null;
        return (
          <div key={key} className="rounded-lg border border-zinc-800 bg-zinc-900 p-3">
            <dt className="font-mono text-xs uppercase text-zinc-500">{key}</dt>
            <dd className="mt-1 wrap-break-words text-zinc-200">
              {typeof value === 'string' || typeof value === 'number' ? (
                value
              ) : (
                <pre className="whitespace-pre-wrap font-mono text-xs">
                  {safeStringify(value)}
                </pre>
              )}
            </dd>
          </div>
        );
      })}
    </dl>
  );
}

/** JSON-stringify a value, replacing any circular references with a marker. */
function safeStringify(value: unknown): string {
  const seen = new Set<object>();
  return JSON.stringify(
    value,
    (_key, v) => {
      if (typeof v === 'object' && v !== null) {
        if (seen.has(v)) return '[Circular]';
        seen.add(v);
      }
      return v;
    },
    2,
  );
}
