export type ArchivedFontStatus = "idle" | "loading" | "ready" | "unavailable";

interface LoadableFontFace {
  load: () => Promise<unknown>;
}

interface FontSetPort {
  add: (face: unknown) => unknown;
  check: (font: string) => boolean;
  ready: Promise<unknown>;
}

export interface ArchivedFontReference {
  fontId: string;
  url: string;
}

export function fontFamilyName(fontId: string) {
  if (!/^[A-Za-z0-9][A-Za-z0-9_.:-]{0,119}$/.test(fontId)) throw new Error("font_id_invalid");
  return `Dada_${fontId.replaceAll(/[^A-Za-z0-9_]/g, "_")}`;
}

export class ArchivedFontLoader {
  private readonly createFace: (family: string, source: string) => LoadableFontFace;
  private readonly fontSet: FontSetPort;
  private readonly pending = new Map<string, Promise<ArchivedFontStatus>>();
  private readonly statuses = new Map<string, ArchivedFontStatus>();

  constructor(input: { createFace: (family: string, source: string) => LoadableFontFace; fontSet: FontSetPort }) {
    this.createFace = input.createFace;
    this.fontSet = input.fontSet;
  }

  status(fontId: string): ArchivedFontStatus {
    return this.statuses.get(fontId) ?? "idle";
  }

  ensure(reference: ArchivedFontReference): Promise<ArchivedFontStatus> {
    const existing = this.pending.get(reference.fontId);
    if (existing) return existing;
    const operation = this.load(reference);
    this.pending.set(reference.fontId, operation);
    return operation;
  }

  retry(reference: ArchivedFontReference) {
    this.pending.delete(reference.fontId);
    this.statuses.delete(reference.fontId);
    return this.ensure(reference);
  }

  private async load(reference: ArchivedFontReference): Promise<ArchivedFontStatus> {
    const family = fontFamilyName(reference.fontId);
    this.statuses.set(reference.fontId, "loading");
    try {
      const face = this.createFace(family, `url("${reference.url}")`);
      const loaded = await face.load();
      this.fontSet.add(loaded);
      await this.fontSet.ready;
      if (!this.fontSet.check(`16px "${family}"`)) throw new Error("font_not_ready");
      this.statuses.set(reference.fontId, "ready");
      return "ready";
    } catch {
      this.statuses.set(reference.fontId, "unavailable");
      return "unavailable";
    }
  }
}

export function createBrowserArchivedFontLoader() {
  return new ArchivedFontLoader({
    createFace: (family, source) => new FontFace(family, source),
    fontSet: document.fonts as unknown as FontSetPort,
  });
}
