/**
 * Fundstellen im amtlichen Wortlaut markieren.
 *
 * ## Warum das ein eigenes Modul ist
 *
 * Weil hier zwei Koordinatensysteme aufeinandertreffen. Gesucht wird in der
 * Kanonform („schiessstate“), angezeigt wird der Wortlaut („Schießstätte“) –
 * und die Faltung ändert Längen. Aus „ß“ wird ein Zeichen, aus „ü“ eines,
 * aus „tt“ eines. Wer die Stelle aus der Kanonform unbesehen auf den
 * Ursprungstext anwendet, markiert daneben. Nachgemessen enthalten 91,7
 * Prozent der Fragen Umlaute oder ß; danebenzumarkieren wäre also der
 * Regelfall, nicht die Ausnahme.
 *
 * Eine falsch markierte Stelle ist schlimmer als gar keine: Sie behauptet,
 * das gesuchte Wort stehe dort, wo es nicht steht. Deshalb liefert
 * {@link spannenFinden} im Zweifel nichts, statt zu raten.
 *
 * ## Warum das ohne zweiten Parser geht
 *
 * Der Katalog führt jeden Textblock doppelt: als `text` und als `segmente`.
 * Nachgemessen über alle 2.109 Blöcke ergibt das Aneinanderhängen der
 * Segmente **exakt** den Text – ohne eine einzige Abweichung. Eine Spanne in
 * Zeichen lässt sich damit verlustfrei auf die Segmente abbilden.
 */

import type { RichText } from './katalog';
import { aufUrsprung, faltenMitZuordnung, fundstellen } from './suchtext';

/** Eine Fundstelle im Ursprungstext: `[von, bis)`. */
export interface Spanne {
  readonly von: number;
  readonly bis: number;
}

/**
 * Alle Fundstellen der Suchwörter im Ursprungstext, zusammengefasst.
 *
 * Überlappende und aneinandergrenzende Spannen werden verschmolzen – sonst
 * entstünden bei „waffe waffen“ zwei ineinanderliegende `<mark>`, und
 * Bildschirmleser sagten die Hervorhebung doppelt an.
 */
export function spannenFinden(roh: string, woerter: readonly string[]): Spanne[] {
  if (woerter.length === 0 || roh.length === 0) {
    return [];
  }

  const faltung = faltenMitZuordnung(roh);
  const roh_spannen: Spanne[] = [];

  for (const wort of woerter) {
    if (wort.length === 0) {
      continue;
    }
    for (const stelle of fundstellen(faltung.gefaltet, wort)) {
      const spanne = aufUrsprung(faltung, stelle, stelle + wort.length);
      if (spanne !== null) {
        roh_spannen.push(spanne);
      }
    }
  }

  roh_spannen.sort((a, b) => a.von - b.von || a.bis - b.bis);

  const aus: Spanne[] = [];
  for (const spanne of roh_spannen) {
    const letzte = aus[aus.length - 1];
    if (letzte !== undefined && spanne.von <= letzte.bis) {
      aus[aus.length - 1] = { von: letzte.von, bis: Math.max(letzte.bis, spanne.bis) };
      continue;
    }
    aus.push(spanne);
  }
  return aus;
}

/** Ein Stück Text mit seinen beiden Auszeichnungen. */
export interface Stueck {
  readonly t: string;
  /** Auszeichnung des amtlichen Wortlauts (Verneinung, Kernelement). */
  readonly hervorgehoben: boolean;
  /** Fundstelle der Suche. */
  readonly treffer: boolean;
}

/**
 * Zerteilt einen Textblock so, dass beide Auszeichnungen nebeneinander
 * bestehen können.
 *
 * Die amtliche Auszeichnung und die Fundstelle der Suche dürfen ineinander
 * liegen, ohne einander zu überschreiben: Die eine gehört zum Wortlaut, die
 * andere zur Suche. Sie zu vermischen hieße, dem Nutzer eine Auszeichnung zu
 * zeigen, die im Katalog nicht steht.
 */
export function zerteilen(text: RichText, spannen: readonly Spanne[]): Stueck[] {
  const segmente = text.segmente.length > 0 ? text.segmente : [{ t: text.text }];

  const stuecke: Stueck[] = [];
  let stelle = 0;

  const istTreffer = (i: number): boolean => spannen.some((s) => i >= s.von && i < s.bis);

  for (const segment of segmente) {
    for (const zeichen of segment.t) {
      const treffer = istTreffer(stelle);
      const hervorgehoben = segment.h === true;
      const letztes = stuecke[stuecke.length - 1];
      if (letztes?.treffer === treffer && letztes.hervorgehoben === hervorgehoben) {
        stuecke[stuecke.length - 1] = { ...letztes, t: letztes.t + zeichen };
      } else {
        stuecke.push({ t: zeichen, hervorgehoben, treffer });
      }
      stelle += zeichen.length;
    }
  }

  return stuecke;
}
