/**
 * Text für die Sprachausgabe vorbereiten – reine Funktionen, ohne DOM.
 *
 * Zwei Dinge stehen zwischen dem angezeigten Text und einer verständlichen
 * Vorlesung:
 *
 * **Abkürzungen.** „§ 12 Abs. 4 Nr. 2 WaffG“ ist geschrieben lesbar und
 * gesprochen unbrauchbar: Die deutschen Systemstimmen sagen „A-B-S Punkt
 * vier“ und buchstabieren „WaffG“. Für eine Lernanwendung ist das keine
 * Kleinigkeit – die Fundstelle ist oft der Kern der Antwort.
 *
 * **Länge.** Chromium bricht eine Äußerung nach etwa 15 Sekunden ab. Das ist
 * ein bekannter Fehler der Engine, kein Fehler des Aufrufers; die einzige
 * Abhilfe ist, den Text in kurze Stücke zu zerlegen und diese nacheinander
 * zu sprechen. Eine Erklärung dieser Anwendung ist im Mittel 1200 Zeichen
 * lang, würde also verlässlich mittendrin verstummen.
 */

/**
 * Wie eine Abkürzung gesprochen wird.
 *
 * Nicht aus dem Glossar abgeleitet, obwohl es dieselben Kürzel führt: Dort
 * stehen *Suchformen* („C.I.P.“, „VDMA 24992“, „Waffengesetzes“), hier
 * gehören *Sprechformen* hin. Beides zu vermengen ergäbe „nach dem
 * Waffengesetzes“.
 *
 * Damit die beiden Listen nicht auseinanderlaufen, prüft
 * `tests/sprachausgabe.test.ts`, dass jedes Kürzel des Glossars hier steht –
 * entweder mit Sprechform oder ausdrücklich unter {@link BUCHSTABIERT}.
 */
export const SPRECHWEISE: Readonly<Record<string, string>> = Object.freeze({
  WaffG: 'Waffengesetz',
  AWaffV: 'Allgemeine Waffengesetz-Verordnung',
  BeschG: 'Beschussgesetz',
  BeschussV: 'Beschussverordnung',
  SprengG: 'Sprengstoffgesetz',
  StGB: 'Strafgesetzbuch',
  UrhG: 'Urheberrechtsgesetz',
  WBK: 'Waffenbesitzkarte',
  NWR: 'Nationales Waffenregister',
  BVA: 'Bundesverwaltungsamt',
  PTB: 'Physikalisch-Technische Bundesanstalt',
  BAM: 'Bundesanstalt für Materialforschung und -prüfung',
  DSB: 'Deutscher Schützenbund',
  BDMP: 'Bund Deutscher Militär- und Polizeischützen',
});

/**
 * Kürzel, die bewusst als Buchstabenfolge stehen bleiben.
 *
 * „B-K-A“ ist die übliche Aussprache; „Bundeskriminalamt“ vorzulesen, wo
 * „BKA“ steht, wäre eine stille Textänderung. Bei „CIP“ und „VDMA“ kommt
 * hinzu, dass die ausgeschriebene Form kaum jemand erwartet.
 */
export const BUCHSTABIERT: readonly string[] = Object.freeze(['BKA', 'CIP', 'VDMA']);

/** Zeichen, die in einem deutschen Wort vorkommen – wie in `shared/glossar.ts`. */
const WORTZEICHEN = 'A-Za-zÄÖÜäöüßẞ0-9';

/**
 * Feste Wendungen, ersetzt vor den Kürzeln.
 *
 * Die Reihenfolge ist wesentlich: „i. V. m.“ muss weg, bevor die einzelnen
 * Buchstaben irgendwo für sich stehen.
 */
const WENDUNGEN: readonly (readonly [RegExp, string])[] = Object.freeze([
  [/§§/gu, 'Paragrafen'],
  [/§/gu, 'Paragraf'],
  [/\bi\.\s*V\.\s*m\./giu, 'in Verbindung mit'],
  [/\bAbs\./gu, 'Absatz'],
  [/\bNrn\./gu, 'Nummern'],
  [/\bNr\./gu, 'Nummer'],
  [/\bBuchst\./gu, 'Buchstabe'],
  [/\bAnl\./gu, 'Anlage'],
  [/\bAbschn\./gu, 'Abschnitt'],
  [/\bz\.\s*B\./gu, 'zum Beispiel'],
  [/\bd\.\s*h\./gu, 'das heißt'],
  [/\bu\.\s*a\./gu, 'unter anderem'],
  [/\bbzw\./gu, 'beziehungsweise'],
  [/\bggf\./gu, 'gegebenenfalls'],
  [/\bevtl\./gu, 'eventuell'],
  [/\binsb\./gu, 'insbesondere'],
  [/\bmind\./gu, 'mindestens'],
]);

/**
 * Kürzel absteigend nach Länge – „BeschussV“ vor „BeschG“.
 *
 * Der Bindestrich gehört zur Grenze: In „WBK-Nummer“ oder „CIP-Zeichen“ ist
 * das Kürzel Teil eines zusammengesetzten Wortes. Ausgeschrieben ergäbe das
 * „Waffenbesitzkarte-Nummer“ – falsches Deutsch, das die Stimme auch so
 * ausspricht. Buchstabiert ist es dort richtig und üblich.
 */
const KUERZEL_MUSTER = new RegExp(
  `(?<![${WORTZEICHEN}]|[${WORTZEICHEN}]-)(?:${Object.keys(SPRECHWEISE)
    .sort((a, b) => b.length - a.length)
    .join('|')})(?![${WORTZEICHEN}]|-[${WORTZEICHEN}])`,
  'gu',
);

/**
 * Macht aus angezeigtem Text gesprochenen Text.
 *
 * Die Anzeige bleibt unberührt: Der Nutzer sieht weiterhin „§ 12 Abs. 4
 * WaffG“ und hört „Paragraf 12 Absatz 4 Waffengesetz“.
 */
export function fuerSprachausgabe(text: string): string {
  let ergebnis = text;

  for (const [muster, ersatz] of WENDUNGEN) {
    ergebnis = ergebnis.replace(muster, ersatz);
  }

  ergebnis = ergebnis.replace(KUERZEL_MUSTER, (treffer) => SPRECHWEISE[treffer] ?? treffer);

  /* Aufzählungszeichen am Zeilenanfang spräche die Stimme als „Strich“ mit.
     Ein Punkt an ihrer Stelle erzeugt stattdessen die Pause, die dort
     gemeint ist. */
  ergebnis = ergebnis.replace(/^[ \t]*[-–—•*]\s+/gmu, '. ');

  /* Die Lücken der Lückentextfrage.

     Im Katalog stehen sie als Reihe von Unterstrichen – so, wie das amtliche
     PDF an dieser Stelle eine Schreiblinie druckt (`parse_catalog.py`,
     `LUECKENZEICHEN`). Gesprochen ist eine Unterstrichreihe entweder gar
     nichts oder buchstabiertes Rauschen; wer die Frage hört, erführe dann
     nicht einmal, dass dort etwas fehlt – und die Frage ist ohne das nicht
     beantwortbar. */
  ergebnis = ergebnis.replace(/_{3,}/gu, 'Lücke');

  return ergebnis
    .replace(/[ \t]+/gu, ' ')
    .replace(/\s*\n\s*/gu, '\n')
    .trim();
}

/** Höchstlänge eines Stücks. Rund zwölf Sekunden Sprechzeit bei Normaltempo. */
export const HAEPPCHEN_LAENGE = 180;

/* Die Sprechgeschwindigkeit steht in `shared/sprechtempo.ts`: Der
   Hauptprozess muss sie bereinigen, bevor sie in `einstellungen.json` geht,
   und er darf keinen Renderer-Code laden. */

/** Satzende: Satzzeichen und Leerraum – aber nicht nach einer Ziffer („6. Juli“). */
const SATZENDE = /(?<![0-9])([.!?:;])\s+/gu;

/** Trennstelle innerhalb eines überlangen Satzes. */
function hartTeilen(satz: string, hoechstens: number): string[] {
  const teile: string[] = [];
  let rest = satz;

  while (rest.length > hoechstens) {
    const fenster = rest.slice(0, hoechstens + 1);
    /* Möglichst an einem Komma trennen, sonst am letzten Leerzeichen, sonst
       stumpf – Letzteres nur bei einem Wort ohne jede Lücke. */
    const beiKomma = fenster.lastIndexOf(', ');
    const schnitt = beiKomma > hoechstens / 2 ? beiKomma + 1 : fenster.lastIndexOf(' ');
    const stelle = schnitt > 0 ? schnitt : hoechstens;
    teile.push(rest.slice(0, stelle).trim());
    rest = rest.slice(stelle).trim();
  }

  if (rest.length > 0) {
    teile.push(rest);
  }
  return teile;
}

/**
 * Zerlegt Text in Stücke, die eine Äußerung sicher zu Ende spricht.
 *
 * Getrennt wird zuerst an Satzgrenzen; kurze Sätze werden wieder gebündelt,
 * damit die Vorlesung nicht stockt. Nur ein einzelner überlanger Satz wird
 * innerhalb geteilt.
 */
export function inHaeppchen(text: string, hoechstens = HAEPPCHEN_LAENGE): string[] {
  const bereinigt = text.trim();
  if (bereinigt.length === 0) {
    return [];
  }

  /* U+E000 als Trennmarke: ein Zeichen der privaten Nutzungszone, das in
     keinem Anzeigetext vorkommen kann. Nach dem Leerzeichen zu teilen
     ergäbe eine Wortliste statt Sätzen; ein Steuerzeichen wie U+0000
     hat in einem regulären Ausdruck nichts zu suchen. */
  const saetze = bereinigt
    .replace(SATZENDE, '$1\uE000')
    .split(/[\uE000\n]/u)
    .map((satz) => satz.trim())
    .filter((satz) => satz.length > 0);

  const haeppchen: string[] = [];
  let puffer = '';

  for (const satz of saetze) {
    const stuecke = satz.length > hoechstens ? hartTeilen(satz, hoechstens) : [satz];
    for (const stueck of stuecke) {
      if (puffer.length === 0) {
        puffer = stueck;
      } else if (puffer.length + 1 + stueck.length <= hoechstens) {
        puffer = `${puffer} ${stueck}`;
      } else {
        haeppchen.push(puffer);
        puffer = stueck;
      }
    }
  }

  if (puffer.length > 0) {
    haeppchen.push(puffer);
  }
  return haeppchen;
}

/** Was {@link stimmeWaehlen} von einer Stimme wissen muss. */
export interface Stimmenangabe {
  readonly lang: string;
  readonly name: string;
  readonly localService: boolean;
}

/**
 * Sucht die passendste deutsche Stimme.
 *
 * Vorrang hat eine lokale Stimme: Sie spricht ohne Verzögerung, und diese
 * Anwendung überträgt keine Daten – eine Stimme aus der Cloud täte genau
 * das und bräche die Zusage, dass alles auf dem Gerät bleibt.
 *
 * Ohne deutsche Stimme ist `null` die richtige Antwort: Dann spricht die
 * Voreinstellung des Systems, geführt von `utterance.lang`. Eine englische
 * Stimme deutschen Text sprechen zu lassen wäre schlechter als keine Wahl.
 */
export function stimmeWaehlen<T extends Stimmenangabe>(
  stimmen: readonly T[],
  sprache = 'de',
): T | null {
  const passende = stimmen.filter((stimme) =>
    stimme.lang.toLowerCase().startsWith(sprache.toLowerCase()),
  );
  return passende.find((stimme) => stimme.localService) ?? passende[0] ?? null;
}
