import { useId, useMemo, useRef, useState } from 'react';

import type { Glossar } from '../../../../shared/glossar';
import { useVerzoegerteAnsage } from '../../hooks/useVerzoegerteAnsage';
import { useVorlesen } from '../../hooks/useVorlesen';
import { Begriffserklaerung } from './Begriffserklaerung';
import { Hinweis } from './Hinweis';

export interface GlossaransichtProps {
  readonly einstiegRef: React.RefObject<HTMLHeadingElement | null>;
  readonly glossar: Glossar | null;
  readonly laedt: boolean;
  /** Sprachausgabe eingeschaltet (Einstellung „Vorlesen“). */
  readonly vorlesenAktiv?: boolean;
  /** Sprechgeschwindigkeit; Stufen in `shared/sprechtempo.ts`. */
  readonly sprechtempo?: number;
  readonly onZurueck: () => void;
}

/**
 * Der Anfangsbuchstabe eines Begriffs für die Sprungleiste.
 *
 * Umlaute fallen auf den Grundbuchstaben: Die deutsche Sortierung reiht
 * „Überlassen“ vor „Umgang“ ein – ein eigener Ü-Knopf zerrisse also die
 * U-Gruppe und zeigte mitten in sie hinein.
 */
function anfangsbuchstabe(begriff: string): string {
  const erster = begriff.charAt(0).toLocaleUpperCase('de');
  switch (erster) {
    case 'Ä':
      return 'A';
    case 'Ö':
      return 'O';
    case 'Ü':
      return 'U';
    default:
      return erster;
  }
}

/**
 * Das vollständige Glossar.
 *
 * Erfüllt WCAG 3.1.3 (Ungewöhnliche Wörter) und 3.1.4 (Abkürzungen): Es muss
 * einen Weg geben, die Bedeutung eines Fachworts nachzuschlagen. Erreichbar
 * ist die Ansicht allein vom Startbildschirm aus („Fachbegriffe
 * nachschlagen“); an der Frage selbst stehen stattdessen die dort
 * vorkommenden Einträge direkt (BegriffeZurFrage).
 *
 * Gefiltert wird im Feld, nicht über Reiter: Wer ein bestimmtes Wort sucht,
 * tippt es – und wer stöbern will, lässt das Feld leer. Die Zahl der Treffer
 * geht über eine Live-Region heraus, damit auch ohne Blick auf die Liste
 * erkennbar ist, ob das Tippen etwas gefunden hat.
 *
 * Fürs Stöbern gibt es die A-bis-Z-Leiste: Sie springt zum ersten Eintrag
 * des Buchstabens und nimmt den Fokus mit. Bei aktivem Filter verschwindet
 * sie – das erzeugt weniger Sonderfälle als ein Neurechnen: Die gefilterte
 * Liste ist kurz und bereits das Sprungziel, und eine Leiste, deren Knöpfe
 * bei jedem Anschlag kommen und gehen, wäre ein bewegliches Ziel.
 */
export function Glossaransicht({
  einstiegRef,
  glossar,
  laedt,
  vorlesenAktiv = false,
  sprechtempo,
  onZurueck,
}: GlossaransichtProps): React.JSX.Element {
  const titelId = useId();
  const feldId = useId();
  const [filter, setzeFilter] = useState('');
  /* Eine Instanz für die ganze Ansicht: Die Sprachausgabe hat nur eine
     Stimme, zwei Einträge könnten ohnehin nicht gleichzeitig sprechen. */
  const stimme = useVorlesen(vorlesenAktiv, 'glossar', sprechtempo);

  const eintraege = useMemo(() => {
    const alle = [...(glossar?.eintraege ?? [])].sort((a, b) =>
      a.begriff.localeCompare(b.begriff, 'de'),
    );
    const gesucht = filter.trim().toLowerCase();
    if (gesucht.length === 0) {
      return alle;
    }
    /* Auch die Wortformen durchsuchen: Wer „führt" eingibt, sucht den
       Eintrag „Führen" – und weiß nicht, unter welchem Stichwort er steht. */
    return alle.filter(
      (eintrag) =>
        eintrag.begriff.toLowerCase().includes(gesucht) ||
        eintrag.kurz.toLowerCase().includes(gesucht) ||
        eintrag.varianten.some((form) => form.toLowerCase().includes(gesucht)),
    );
  }, [glossar, filter]);

  /* Buchstabe → Begriff des jeweils ersten Eintrags. Nur vorhandene
     Buchstaben: Ein gesperrter Knopf „J“ ohne Eintrag wäre ein Angebot ohne
     Ziel – er entfällt ganz. */
  const sprungziele = useMemo(() => {
    const ziele = new Map<string, string>();
    for (const eintrag of eintraege) {
      const buchstabe = anfangsbuchstabe(eintrag.begriff);
      if (!ziele.has(buchstabe)) {
        ziele.set(buchstabe, eintrag.begriff);
      }
    }
    return ziele;
  }, [eintraege]);

  const trefferansage = useVerzoegerteAnsage(
    glossar === null
      ? ''
      : eintraege.length === glossar.eintraege.length
        ? `${String(eintraege.length)} Einträge`
        : `${String(eintraege.length)} von ${String(glossar.eintraege.length)} Einträgen`,
  );

  /** Überschrift je Begriff – Sprungziel für Leiste und „Siehe auch“. */
  const ueberschriften = useRef(new Map<string, HTMLHeadingElement>());

  /** Rollt zur Überschrift und nimmt den Fokus mit. */
  const zurUeberschrift = (begriff: string): boolean => {
    const ziel = ueberschriften.current.get(begriff);
    if (ziel === undefined) {
      return false;
    }
    /* Erst rollen, dann fokussieren: `focus()` allein schöbe die Überschrift
       nur eben in den Sichtbereich – ans untere Fensterende. `block: 'start'`
       stellt sie nach oben, wo der Eintrag beginnt. Kein `behavior: 'smooth'`:
       Ohne Angabe gilt das CSS, und dort schaltet `prefers-reduced-motion`
       jedes Rollen auf sofort (tokens.css). jsdom kennt die Methode nicht,
       daher der Typtest. */
    if (typeof ziel.scrollIntoView === 'function') {
      ziel.scrollIntoView({ block: 'start' });
    }
    ziel.focus({ preventScroll: true });
    return true;
  };

  const zumBuchstaben = (buchstabe: string): void => {
    const begriff = sprungziele.get(buchstabe);
    if (begriff !== undefined) {
      zurUeberschrift(begriff);
    }
  };

  /**
   * Springt zu einem verwandten Begriff.
   *
   * **Der Filter muss zuerst weg.** Steht einer, ist der Zieleintrag
   * vielleicht gar nicht gerendert, und der Sprung liefe ins Leere – der
   * Fokus bliebe stehen, ohne dass etwas geschähe. Deshalb wird der Filter
   * geleert und der Sprung auf den nächsten Bildaufbau verschoben; erst dann
   * steht die Überschrift im Baum.
   */
  const zumBegriff = (begriff: string): void => {
    if (zurUeberschrift(begriff)) {
      return;
    }
    setzeFilter('');
    /* Nach dem Bildaufbau, nicht davor: Zu diesem Zeitpunkt hat React die
       ausgefilterten Einträge wieder eingehängt und ihre Refs gesetzt. */
    window.requestAnimationFrame(() => {
      zurUeberschrift(begriff);
    });
  };

  return (
    <section className="glossar abschnitt" aria-labelledby={titelId}>
      <h1 id={titelId} ref={einstiegRef} tabIndex={-1}>
        Glossar
      </h1>

      <p>
        Das Waffenrecht benutzt Alltagswörter mit eigener Bedeutung. „Führen“ heißt nicht lenken,
        „Erwerben“ hat mit Kaufen nichts zu tun. Wo das Gesetz einen Begriff selbst bestimmt, steht
        hier diese Bestimmung.
      </p>

      {laedt && glossar === null ? <p>Das Glossar wird geladen …</p> : null}

      {!laedt && glossar === null ? (
        <Hinweis art="hinweis">
          Das Glossar ist zurzeit nicht verfügbar. Sie können ohne es weiterlernen.
        </Hinweis>
      ) : null}

      {glossar !== null ? (
        <>
          <div className="glossar__suche">
            <label htmlFor={feldId}>Begriff suchen</label>
            <input
              id={feldId}
              type="search"
              className="glossar__feld"
              value={filter}
              onChange={(ereignis) => {
                setzeFilter(ereignis.target.value);
              }}
            />
          </div>

          {/*
            Die Trefferzahl zieht erst nach einer Ruhepause nach.

            Bis 0.22.0 stand hier der Zustand unmittelbar: Beim Tippen von
            „Führen“ bekam ein Screenreader sechs einander unterbrechende
            Zahlenansagen zwischen die Zeichen-Echos gemischt – während die
            Fragensuche für dieselbe Art Auskunft längst eine Ruhepause
            einlegte. Zwei Verhaltensweisen für dieselbe Sache; jetzt teilen
            sich beide `useVerzoegerteAnsage`.
          */}
          <p className="glossar__anzahl" role="status">
            {trefferansage}
          </p>

          {filter.trim().length === 0 && sprungziele.size > 0 ? (
            <nav className="glossar__buchstaben" aria-label="Anfangsbuchstaben">
              {[...sprungziele.keys()].map((buchstabe) => (
                <button
                  key={buchstabe}
                  type="button"
                  className="knopf glossar__buchstabe"
                  onClick={() => {
                    zumBuchstaben(buchstabe);
                  }}
                >
                  {buchstabe}
                </button>
              ))}
            </nav>
          ) : null}

          {eintraege.length === 0 ? (
            <p>Zu dieser Eingabe gibt es keinen Eintrag.</p>
          ) : (
            <div className="glossar__liste">
              {eintraege.map((eintrag) => (
                <Begriffserklaerung
                  key={eintrag.begriff}
                  eintrag={eintrag}
                  ebene={2}
                  /* Jede Überschrift meldet sich jetzt an, nicht mehr nur die
                     erste je Buchstabe: Die A-bis-Z-Leiste braucht nur die
                     ersten, „Siehe auch“ braucht jede. */
                  ueberschriftRef={(element) => {
                    if (element === null) {
                      ueberschriften.current.delete(eintrag.begriff);
                    } else {
                      ueberschriften.current.set(eintrag.begriff, element);
                    }
                  }}
                  onSprungZu={zumBegriff}
                  {...(vorlesenAktiv && stimme.moeglich ? { stimme } : {})}
                />
              ))}
            </div>
          )}

          <p className="glossar__herkunft">
            Die Begriffsbestimmungen stammen aus dem Gesetz und sind nach § 5 Abs. 1 UrhG
            gemeinfrei. Zusammenstellung und Auswahl sind eine Ergänzung dieser Software und nicht
            Teil des amtlichen Fragenkatalogs.
          </p>
        </>
      ) : null}

      <div className="glossar__aktionen">
        <button type="button" className="knopf" onClick={onZurueck}>
          Zum Start
        </button>
      </div>
    </section>
  );
}
