import { useCallback, useEffect, useState } from 'react';

import { bridge } from '../bridge';

/**
 * Der frühere Ablageort im `localStorage`.
 *
 * Wird nur noch für die einmalige Übernahme gelesen (siehe
 * {@link useKuerzelEinstellung}); geschrieben wird hierher nichts mehr.
 */
const ALTER_SPEICHERSCHLUESSEL = 'waffensachkunde.tastenkuerzel';

export interface KuerzelEinstellung {
  readonly aktiv: boolean;
  readonly setzen: (aktiv: boolean) => void;
  /**
   * `true`, wenn die Kürzel abgeschaltet sind, weil ein Hilfsmittel erkannt
   * wurde – und nicht, weil jemand sie ausdrücklich abgeschaltet hat. Die
   * Einstellungen erklären den Unterschied.
   */
  readonly wegenHilfsmittelAus: boolean;
  /** Meldung, wenn die Wahl nicht dauerhaft gesichert werden konnte. */
  readonly meldung: string | null;
}

/** Die Wahl aus dem früheren Ablageort, oder `null`, wenn dort nichts steht. */
function altwert(): boolean | null {
  try {
    const wert = window.localStorage.getItem(ALTER_SPEICHERSCHLUESSEL);
    return wert === null ? null : wert !== 'aus';
  } catch {
    return null;
  }
}

/** Löscht den früheren Ablageort – erst, wenn die Datei den Wert hat. */
function altwertVergessen(): void {
  try {
    window.localStorage.removeItem(ALTER_SPEICHERSCHLUESSEL);
  } catch {
    /* Ohne localStorage gibt es auch nichts zu vergessen. */
  }
}

/**
 * Ein-/Ausschalter für die Zeichentasten-Kürzel (WCAG 2.1.4).
 *
 * Die Voreinstellung richtet sich danach, ob ein Hilfsmittel läuft: Im
 * Lesemodus von NVDA und JAWS sind die Ziffern für die Navigation nach
 * Überschriftenebenen belegt, die Kürzel gingen dort also ins Leere oder
 * störten. Wer nichts entschieden hat, bekommt sie deshalb bei erkanntem
 * Screenreader ausgeschaltet.
 *
 * Eine ausdrückliche Entscheidung hat immer Vorrang und wird nie
 * überschrieben – auch dann nicht, wenn später ein Hilfsmittel startet.
 *
 * ## Wo der Wert liegt
 *
 * In `einstellungen.json`, wie jede andere Nutzereinstellung – über
 * denselben Weg wie etwa „Vorlesen“: `einstellungen:lesen` beim Aufbau,
 * `einstellungen:schreiben` bei jeder Änderung. Bis 0.21.0 lag er allein im
 * `localStorage` des Renderers; damit stand er außerhalb von Einstellungen
 * und Profil und überlebte keinen Gerätewechsel.
 *
 * **Kein Zwischenspeicher im `localStorage`**, anders als beim Farbschema:
 * Dort muss der Wert vor dem ersten Zeichnen anliegen, sonst blitzt das
 * falsche Thema auf. Hier hängt nichts am ersten Bild – die Kürzel wirken
 * erst, wenn jemand eine Taste drückt, und das ist frühestens in der
 * Lernsitzung. Bis der Anwendungskern geantwortet hat, gilt deshalb die
 * Voreinstellung, genau wie bei den übrigen Schaltern in `useEinstellungen`.
 */
export function useKuerzelEinstellung(hilfsmittelAktiv = false): KuerzelEinstellung {
  const [wahl, setzeWahl] = useState<boolean | null>(null);
  /* Ohne Kanal wird nie etwas nachgeladen – dann ist der Anfangswert schon
     der Endwert. Dieselbe Überlegung wie in `useEinstellungen`: Das gleich
     hier zu entscheiden erspart einen Zustandswechsel aus dem Effekt heraus. */
  const [geladen, setzeGeladen] = useState(() => bridge()?.einstellungenLesen === undefined);
  const [meldung, setzeMeldung] = useState<string | null>(null);

  useEffect(() => {
    let lebt = true;

    const lesen = bridge()?.einstellungenLesen;
    if (!lesen) {
      return;
    }

    void lesen()
      .then((einstellungen) => {
        if (!lebt) {
          return;
        }

        if (typeof einstellungen.tastenkuerzel === 'boolean') {
          setzeWahl(einstellungen.tastenkuerzel);
          /* Die Datei führt. Ein Rest aus der Zeit davor wird ab hier nie
             mehr gelesen und darf weg. */
          altwertVergessen();
          return;
        }

        /*
          Einmalige Übernahme des alten `localStorage`-Werts.

          Wer die Kürzel vor der Umstellung abgeschaltet hat, soll sie nicht
          beim nächsten Start wieder eingeschaltet vorfinden. Steht in der
          Datei nichts, gilt deshalb der alte Wert – er wird übernommen, in
          die Datei geschrieben und danach gelöscht.

          Gelöscht wird ERST NACH dem erfolgreichen Schreiben: Scheitert es
          (voller Datenträger, schreibgeschütztes Verzeichnis), bleibt der
          alte Eintrag stehen, und der nächste Start versucht es erneut.
          Andersherum wäre die Wahl endgültig fort.
        */
        const alt = altwert();
        if (alt === null) {
          return;
        }
        setzeWahl(alt);

        const schreiben = bridge()?.einstellungenSchreiben;
        if (!schreiben) {
          return;
        }
        void schreiben({ tastenkuerzel: alt })
          .then(() => {
            altwertVergessen();
          })
          .catch((fehler: unknown) => {
            console.warn('[tastenkuerzel] bisherige Wahl nicht übernommen:', fehler);
          });
      })
      .catch((fehler: unknown) => {
        console.warn('[tastenkuerzel] Einstellungen konnten nicht gelesen werden:', fehler);
      })
      .finally(() => {
        if (lebt) {
          setzeGeladen(true);
        }
      });

    return () => {
      lebt = false;
    };
  }, []);

  const setzen = useCallback((neu: boolean) => {
    /* Sofort wirksam, unabhängig davon, ob das Sichern gelingt – dieselbe
       Regel wie in `useEinstellungen`: Ein Schalter, der erst nach einer
       Rückmeldung umspringt, fühlt sich kaputt an. */
    setzeWahl(neu);
    setzeMeldung(null);

    const schreiben = bridge()?.einstellungenSchreiben;
    if (!schreiben) {
      return;
    }

    void schreiben({ tastenkuerzel: neu }).catch((fehler: unknown) => {
      console.warn('[tastenkuerzel] konnte nicht gespeichert werden:', fehler);
      setzeMeldung(
        'Diese Einstellung konnte nicht dauerhaft gespeichert werden und gilt nur für diese Sitzung.',
      );
    });
  }, []);

  const aktiv = wahl ?? !hilfsmittelAktiv;

  return {
    aktiv,
    setzen,
    /* Erst begründen, wenn feststeht, dass es nichts zu lesen gab: Solange
       die Datei noch antwortet, wäre „wegen Hilfsmittel aus“ geraten – und
       der Hinweistext am Schalter blitzte auf, um gleich wieder zu
       verschwinden. */
    wegenHilfsmittelAus: geladen && wahl === null && hilfsmittelAktiv,
    meldung,
  };
}

/**
 * Steht gerade ein modaler Dialog offen?
 *
 * `showModal()` macht den Hintergrund inert – für Zeiger und für
 * Bildschirmleser. Für Tastendrücke gilt das **nicht**: Sie steigen bis zum
 * `window` auf, und der Zuhörer unten hängt genau dort. Bis Fassung 0.24.1
 * wirkten die Zeichenkürzel der Lernsitzung deshalb durch die Hilfe hindurch.
 * Wer in einer Sitzung F1 drückte und dann die Eingabetaste, bestätigte
 * damit seine – womöglich leere – Antwort, ohne die Frage überhaupt zu sehen:
 * Die Frage zählte als falsch, und die Wiedervorlage rechnete damit weiter.
 *
 * Gefragt wird nach dem Dokument und nicht nach einem durchgereichten
 * Zustand: Dieselbe Falle stellt jeder künftige modale Dialog erneut, und
 * eine Wache, die nur den heute bekannten Fall kennt, hilft dann nicht.
 */
function dialogOffen(): boolean {
  return document.querySelector('dialog[open]') !== null;
}

/** Schreibt ein Zeichenkürzel in ein Textfeld? Dann hat das Feld Vorrang. */
function istTexteingabe(ziel: EventTarget | null): boolean {
  if (!(ziel instanceof HTMLElement)) {
    return false;
  }
  if (ziel.isContentEditable) {
    return true;
  }
  if (ziel instanceof HTMLTextAreaElement || ziel instanceof HTMLSelectElement) {
    return true;
  }
  if (ziel instanceof HTMLInputElement) {
    return !['radio', 'checkbox', 'button', 'submit', 'reset'].includes(ziel.type);
  }
  return false;
}

/** Elemente, die die Eingabetaste bereits selbst auslösen. */
function loestEingabetasteSelbstAus(ziel: EventTarget | null): boolean {
  if (!(ziel instanceof HTMLElement)) {
    return false;
  }
  return (
    ziel instanceof HTMLButtonElement ||
    ziel instanceof HTMLAnchorElement ||
    ziel.tagName === 'SUMMARY'
  );
}

export interface KuerzelBelegung {
  readonly aktiv: boolean;
  /** Ziffern 1 bis 8 wählen die Antwortoption an dieser Anzeigeposition. */
  readonly aufZiffer: (position: number) => void;
  /** Eingabetaste: bestätigen bzw. weiter. */
  readonly aufEingabe: () => void;
  /** „M“ merkt die Frage bzw. hebt das Merken auf. */
  readonly aufMerken: () => void;
}

/**
 * Tastenkürzel der Lernsitzung.
 *
 * Sie greifen nur, wenn kein Textfeld im Fokus ist, und niemals zusammen mit
 * Strg/Alt/Meta. Die Eingabetaste bleibt Schaltflächen und Links überlassen,
 * damit sie nicht doppelt auslöst.
 */
export function useTastenkuerzel(belegung: KuerzelBelegung): void {
  const { aktiv, aufZiffer, aufEingabe, aufMerken } = belegung;

  useEffect(() => {
    if (!aktiv) {
      return undefined;
    }

    function beiTaste(ereignis: KeyboardEvent): void {
      if (ereignis.defaultPrevented || ereignis.ctrlKey || ereignis.altKey || ereignis.metaKey) {
        return;
      }
      /* Solange ein Dialog offen steht, gehört die Tastatur ihm. Siehe
         {@link dialogOffen} – der Hintergrund ist für Zeiger und
         Bildschirmleser inert, für Tastendrücke war er es nicht. */
      if (dialogOffen()) {
        return;
      }
      const ziel = ereignis.target;
      if (istTexteingabe(ziel)) {
        return;
      }

      if (ereignis.key >= '1' && ereignis.key <= '8') {
        ereignis.preventDefault();
        aufZiffer(Number(ereignis.key));
        return;
      }

      if (ereignis.key === 'Enter' && !loestEingabetasteSelbstAus(ziel)) {
        ereignis.preventDefault();
        aufEingabe();
        return;
      }

      if (ereignis.key === 'm' || ereignis.key === 'M') {
        ereignis.preventDefault();
        aufMerken();
      }
    }

    window.addEventListener('keydown', beiTaste);
    return () => {
      window.removeEventListener('keydown', beiTaste);
    };
  }, [aktiv, aufZiffer, aufEingabe, aufMerken]);
}
