import { Fragment, useEffect, useRef } from 'react';

import {
  HILFE_EINLEITUNG,
  HILFE_KAPITEL,
  HILFE_TITEL,
  type Hilfeblock,
  type Tastenzeile,
} from '../../../../shared/hilfe';

export interface HilfedialogProps {
  readonly offen: boolean;
  /**
   * Kapitel, bei dem die Hilfe aufschlägt.
   *
   * Ohne Angabe beginnt sie oben, beim Inhaltsverzeichnis. Kontextsensitives
   * F1 ist die eingeführte Windows-Konvention: Wer mitten in der
   * Prüfungssimulation drückt, landete bis 0.22.0 am Anfang des Handbuchs
   * und musste das passende von elf Kapiteln selbst heraussuchen –
   * ausgerechnet unter Zeitdruck, und für Screenreader-Nutzer hieß das, das
   * ganze Inhaltsverzeichnis durchzugehen.
   *
   * Eine unbekannte Kennung ist folgenlos: Dann bleibt es beim Anfang.
   */
  readonly anfangskapitel?: string | undefined;
  readonly onSchliessen: () => void;
}

/** Die Kennung einer Kapitelüberschrift – Ziel des Inhaltsverzeichnisses. */
function ankerId(kapitelId: string): string {
  return `hilfe-${kapitelId}`;
}

const VERBINDER: Readonly<Record<string, string>> = Object.freeze({
  plus: '+',
  bis: 'bis',
  oder: 'oder',
});

/**
 * Eine Tastenzeile: die Tasten selbst, mit ihrem Verhältnis dazwischen.
 *
 * Das Verhältnis steht als Wort oder Zeichen zwischen den Tasten und ist
 * **nicht** vor Bildschirmlesern verborgen: „Strg + Plus“ heißt gleichzeitig,
 * „1 bis 8“ heißt irgendeine davon. Wer den Unterschied nur über einen
 * Zwischenraum andeutet, sagt ihn für Hörende gar nicht.
 */
function Tasten({ zeile }: { readonly zeile: Tastenzeile }): React.JSX.Element {
  const verbinder = VERBINDER[zeile.verbindung ?? 'oder'] ?? 'oder';

  return (
    <>
      {zeile.tasten.map((taste, stelle) => (
        <Fragment key={taste}>
          {stelle > 0 ? <span className="hilfe__verbinder"> {verbinder} </span> : null}
          <kbd>{taste}</kbd>
        </Fragment>
      ))}
    </>
  );
}

/** Ein einzelner Baustein eines Kapitels. */
function Block({ block }: { readonly block: Hilfeblock }): React.JSX.Element {
  switch (block.art) {
    case 'absatz':
      return <p>{block.text}</p>;

    case 'liste':
      return (
        <ul>
          {block.punkte.map((punkt) => (
            <li key={punkt}>{punkt}</li>
          ))}
        </ul>
      );

    case 'schritte':
      /* Eine Abfolge, bei der die Reihenfolge zählt – deshalb <ol>. Ein
         Bildschirmleser sagt dann „1 von 5“ an, und wer bei Schritt 3
         aufhört, weiß, wie viel noch kommt. */
      return (
        <ol className="hilfe__schritte">
          {block.punkte.map((punkt) => (
            <li key={punkt}>{punkt}</li>
          ))}
        </ol>
      );

    case 'tasten':
      return (
        <dl className="hilfe__tasten">
          {block.zeilen.map((zeile) => (
            <Fragment key={zeile.wirkung}>
              <dt>
                <Tasten zeile={zeile} />
              </dt>
              <dd>{zeile.wirkung}</dd>
            </Fragment>
          ))}
        </dl>
      );
  }
}

/**
 * Das Handbuch – als modales Fenster über allem, was gerade zu sehen ist.
 *
 * ## Warum ein Dialog und keine eigene Ansicht
 *
 * Prüfschritt 3.2.6 („Konsistente Hilfe“) verlangt, dass die Hilfe auf allen
 * Bildschirmen an derselben Stelle steht. Eine eigene Ansicht könnte das
 * nicht einlösen: Sie würde die laufende Lernsitzung abräumen und den
 * Prüfungsbogen verlassen – ausgerechnet dort, wo man Hilfe am ehesten
 * braucht („wie merke ich mir eine Frage?“). Ein Dialog legt sich darüber und
 * nimmt nichts mit.
 *
 * ## Warum `<dialog>` und keine nachgebaute Ebene
 *
 * `showModal()` bringt vom Betriebssystem, was man sonst mühsam und
 * fehleranfällig nachbaut: Der Fokus bleibt im Dialog gefangen, alles
 * dahinter wird inert – auch für Bildschirmleser –, Esc schließt, und der
 * Fokus kehrt danach von selbst auf die auslösende Schaltfläche zurück. Das
 * ist genau der dokumentierte Plattformdienst, den EN 301 549 Abschnitt 11.5
 * verlangt.
 *
 * ## Was in jsdom davon fehlt
 *
 * jsdom 27 kennt das Element, aber **weder `showModal()` noch `close()`**
 * (nachgemessen). Die Unit-Tests laufen deshalb über den Rückfallweg – das
 * `open`-Attribut von Hand – und prüfen damit den Inhalt, die Struktur und
 * das Schließen per Esc, nicht aber Fokusfalle und Inertheit. Die messen die
 * Tests im echten Fenster; nur dort gibt es sie überhaupt.
 *
 * ## Warum h2 und nicht h1
 *
 * Jede Ansicht dieser Anwendung führt genau eine h1, die sagt, wo man ist –
 * ein Test wacht darüber. Der Dialog ist keine Ansicht, sondern liegt über
 * einer; eine zweite h1 im selben Dokument würde diese Zusage brechen.
 */
export function Hilfedialog({
  offen,
  anfangskapitel,
  onSchliessen,
}: HilfedialogProps): React.JSX.Element {
  const dialogRef = useRef<HTMLDialogElement>(null);
  const titelRef = useRef<HTMLHeadingElement>(null);

  useEffect(() => {
    const dialog = dialogRef.current;
    if (dialog === null) {
      return;
    }

    if (offen && !dialog.open) {
      /* Der Rückfallweg gilt jsdom (siehe oben), nicht dem Auslieferungsfall:
         In Chromium gibt es showModal() immer. Ohne ihn liefen die
         Unit-Tests gegen einen leeren Dialog und meldeten trotzdem grün. */
      if (typeof dialog.showModal === 'function') {
        dialog.showModal();
      } else {
        dialog.setAttribute('open', '');
      }
      /*
        Zum passenden Kapitel, wenn eines genannt ist.

        Der Fokus geht dann dorthin statt auf die Titelüberschrift: Wer aus
        der Simulation heraus F1 drückt, soll das Kapitel zur Simulation
        hören, nicht „Hilfe zur Anwendung“ und danach ein Inhaltsverzeichnis.
        Das Verzeichnis bleibt erreichbar – es steht darüber.

        Eine unbekannte Kennung fällt still auf den Anfang zurück; eine Hilfe,
        die sich weigert aufzugehen, weil eine Ansicht kein Kapitel hat, wäre
        die schlechtere Antwort.
      */
      const ziel =
        anfangskapitel === undefined
          ? null
          : dialog.querySelector<HTMLElement>(`#${CSS.escape(ankerId(anfangskapitel))}`);

      if (ziel !== null) {
        if (typeof ziel.scrollIntoView === 'function') {
          ziel.scrollIntoView({ block: 'start' });
        }
        ziel.focus({ preventScroll: true });
        return;
      }

      /* Auf die Überschrift, nicht auf den Schließen-Knopf: Wer die Hilfe
         öffnet, soll zuerst hören, was er geöffnet hat, und nicht, wie er es
         wieder loswird. */
      titelRef.current?.focus();
      return;
    }

    if (!offen && dialog.open) {
      if (typeof dialog.close === 'function') {
        dialog.close();
      } else {
        dialog.removeAttribute('open');
      }
    }
  }, [offen, anfangskapitel]);

  /*
    Esc schließt.

    In Chromium erledigt das der modale Dialog selbst und meldet es über
    `close`. Dieser Zuhörer steht für den Rückfallweg – und dafür, dass eine
    Zusage im Handbuch („Esc schließt die Hilfe“) nicht davon abhängt, welche
    Umgebung gerade rendert. Doppelt gerufen schadet er nicht: Zumachen, was
    schon zu ist, ändert nichts.
  */
  useEffect(() => {
    const dialog = dialogRef.current;
    if (dialog === null || !offen) {
      return;
    }

    const beiTaste = (ereignis: KeyboardEvent): void => {
      if (ereignis.key === 'Escape') {
        onSchliessen();
      }
    };

    dialog.addEventListener('keydown', beiTaste);
    return () => {
      dialog.removeEventListener('keydown', beiTaste);
    };
  }, [offen, onSchliessen]);

  return (
    <dialog ref={dialogRef} className="hilfe" aria-labelledby="hilfe-titel" onClose={onSchliessen}>
      {/* Nur wenn offen: Ein geschlossener Dialog ist zwar unsichtbar, seine
          Überschriften stünden aber trotzdem im Baum und liefen jedem
          Überschriftentest der übrigen Ansichten in die Quere. */}
      {offen ? (
        <div className="hilfe__rahmen">
          <div className="hilfe__kopf">
            <h2 id="hilfe-titel" ref={titelRef} tabIndex={-1}>
              {HILFE_TITEL}
            </h2>
            <button type="button" className="knopf knopf--leise" onClick={onSchliessen}>
              Schließen
            </button>
          </div>

          <p className="hilfe__einleitung">{HILFE_EINLEITUNG}</p>

          <nav className="hilfe__verzeichnis" aria-label="Kapitel dieser Hilfe">
            <ol>
              {HILFE_KAPITEL.map((kapitel) => (
                <li key={kapitel.id}>
                  <a href={`#${ankerId(kapitel.id)}`}>{kapitel.titel}</a>
                  <span className="hilfe__verzeichniskurz"> – {kapitel.kurz}</span>
                </li>
              ))}
            </ol>
          </nav>

          {HILFE_KAPITEL.map((kapitel) => (
            <section key={kapitel.id} className="hilfe__kapitel">
              {/* tabIndex={-1}, damit der Sprung aus dem Verzeichnis den Fokus
                  wirklich hierher bewegt und nicht nur den Bildausschnitt. */}
              <h3 id={ankerId(kapitel.id)} tabIndex={-1}>
                {kapitel.titel}
              </h3>
              {kapitel.bloecke.map((block, stelle) => (
                <Block key={`${kapitel.id}-${String(stelle)}`} block={block} />
              ))}
            </section>
          ))}

          {/* Derselbe Weg hinaus wie oben, gleich beschriftet (WCAG 3.2.4).
              Bei einem langen Dokument will niemand erst nach oben scrollen,
              nur um wieder herauszukommen. */}
          <div className="hilfe__abschluss">
            <button type="button" className="knopf" onClick={onSchliessen}>
              Schließen
            </button>
          </div>
        </div>
      ) : null}
    </dialog>
  );
}
