/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import { debugLogger, type Config } from '@google/gemini-cli-core';
import { useStdin } from 'ink';
import { MultiMap } from 'mnemonist';
import type React from 'react';
import {
  createContext,
  useCallback,
  useContext,
  useEffect,
  useMemo,
  useRef,
} from 'react';

import { ESC } from '../utils/input.js';
import { parseMouseEvent } from '../utils/mouse.js';
import { FOCUS_IN, FOCUS_OUT } from '../hooks/useFocus.js';
import { appEvents, AppEvent } from '../../utils/events.js';
import { terminalCapabilityManager } from '../utils/terminalCapabilityManager.js';
import { useSettingsStore } from './SettingsContext.js';

export const BACKSLASH_ENTER_TIMEOUT = 5;
export const ESC_TIMEOUT = 50;
export const PASTE_TIMEOUT = 30_000;
export const FAST_RETURN_TIMEOUT = 30;

export enum KeypressPriority {
  Low = -100,
  Normal = 0,
  High = 100,
  Critical = 200,
}

// Parse the key itself
const KEY_INFO_MAP: Record<
  string,
  { name: string; shift?: boolean; ctrl?: boolean }
> = {
  '[200~': { name: 'paste-start' },
  '[201~': { name: 'paste-end' },
  '[[A': { name: 'f1' },
  '[[B': { name: 'f2' },
  '[[C': { name: 'f3' },
  '[[D': { name: 'f4' },
  '[[E': { name: 'f5' },
  '[1~': { name: 'home' },
  '[2~': { name: 'insert' },
  '[3~': { name: 'delete' },
  '[4~': { name: 'end' },
  '[5~': { name: 'pageup' },
  '[6~': { name: 'pagedown' },
  '[7~': { name: 'home' },
  '[8~': { name: 'end' },
  '[11~': { name: 'f1' },
  '[12~': { name: 'f2' },
  '[13~': { name: 'f3' },
  '[14~': { name: 'f4' },
  '[15~': { name: 'f5' },
  '[17~': { name: 'f6' },
  '[18~': { name: 'f7' },
  '[19~': { name: 'f8' },
  '[20~': { name: 'f9' },
  '[21~': { name: 'f10' },
  '[23~': { name: 'f11' },
  '[24~': { name: 'f12' },
  '[25~': { name: 'f13' },
  '[26~': { name: 'f14' },
  '[28~': { name: 'f15' },
  '[29~': { name: 'f16' },
  '[31~': { name: 'f17' },
  '[32~': { name: 'f18' },
  '[33~': { name: 'f19' },
  '[34~': { name: 'f20' },
  '[A': { name: 'up' },
  '[B': { name: 'down' },
  '[C': { name: 'right' },
  '[D': { name: 'left' },
  '[E': { name: 'clear' },
  '[F': { name: 'end' },
  '[H': { name: 'home' },
  '[P': { name: 'f1' },
  '[Q': { name: 'f2' },
  '[R': { name: 'f3' },
  '[S': { name: 'f4' },
  OA: { name: 'up' },
  OB: { name: 'down' },
  OC: { name: 'right' },
  OD: { name: 'left' },
  OE: { name: 'clear' },
  OF: { name: 'end' },
  OH: { name: 'home' },
  OP: { name: 'f1' },
  OQ: { name: 'f2' },
  OR: { name: 'f3' },
  OS: { name: 'f4' },
  OZ: { name: 'tab', shift: true }, // SS3 Shift+Tab variant for Windows terminals
  '[[5~': { name: 'pageup' },
  '[[6~': { name: 'pagedown' },
  '[a': { name: 'up', shift: true },
  '[b': { name: 'down', shift: true },
  '[c': { name: 'right', shift: true },
  '[d': { name: 'left', shift: true },
  '[e': { name: 'clear', shift: true },
  '[2$': { name: 'insert', shift: true },
  '[3$': { name: 'delete', shift: true },
  '[5$': { name: 'pageup', shift: true },
  '[6$': { name: 'pagedown', shift: true },
  '[7$': { name: 'home', shift: true },
  '[8$': { name: 'end', shift: true },
  '[Z': { name: 'tab', shift: true },
  Oa: { name: 'up', ctrl: true },
  Ob: { name: 'down', ctrl: true },
  Oc: { name: 'right', ctrl: true },
  Od: { name: 'left', ctrl: true },
  Oe: { name: 'clear', ctrl: true },
  '[2^': { name: 'insert', ctrl: true },
  '[3^': { name: 'delete', ctrl: true },
  '[5^': { name: 'pageup', ctrl: true },
  '[6^': { name: 'pagedown', ctrl: true },
  '[7^': { name: 'home', ctrl: true },
  '[8^': { name: 'end', ctrl: true },
};

// Kitty Keyboard Protocol (CSI u) code mappings
const KITTY_CODE_MAP: Record<number, { name: string; sequence?: string }> = {
  2: { name: 'insert' },
  3: { name: 'delete' },
  5: { name: 'pageup' },
  6: { name: 'pagedown' },
  9: { name: 'tab' },
  13: { name: 'enter' },
  14: { name: 'up' },
  15: { name: 'down' },
  16: { name: 'right' },
  17: { name: 'left' },
  27: { name: 'escape' },
  32: { name: 'space', sequence: ' ' },
  127: { name: 'backspace' },
  57358: { name: 'capslock' },
  57359: { name: 'scrolllock' },
  57360: { name: 'numlock' },
  57361: { name: 'printscreen' },
  57362: { name: 'pausebreak' },
  57409: { name: 'numpad_decimal', sequence: '.' },
  57410: { name: 'numpad_divide', sequence: '/' },
  57411: { name: 'numpad_multiply', sequence: '*' },
  57412: { name: 'numpad_subtract', sequence: '-' },
  57413: { name: 'numpad_add', sequence: '+' },
  57414: { name: 'enter' },
  57416: { name: 'numpad_separator', sequence: ',' },
  // Function keys F13-F35, not standard, but supported by Kitty
  ...Object.fromEntries(
    Array.from({ length: 23 }, (_, i) => [302 + i, { name: `f${13 + i}` }]),
  ),
  // Numpad keys in Numeric Keypad Mode (CSI u codes 57399-57408)
  ...Object.fromEntries(
    Array.from({ length: 10 }, (_, i) => [
      57399 + i,
      { name: `numpad${i}`, sequence: String(i) },
    ]),
  ),
};

// Numpad keys in Application Keypad Mode (SS3 sequences)
const NUMPAD_MAP: Record<string, string> = {
  Oj: '*',
  Ok: '+',
  Om: '-',
  Oo: '/',
  Op: '0',
  Oq: '1',
  Or: '2',
  Os: '3',
  Ot: '4',
  Ou: '5',
  Ov: '6',
  Ow: '7',
  Ox: '8',
  Oy: '9',
  On: '.',
};

const kUTF16SurrogateThreshold = 0x10000; // 2 ** 16
function charLengthAt(str: string, i: number): number {
  if (str.length <= i) {
    // Pretend to move to the right. This is necessary to autocomplete while
    // moving to the right.
    return 1;
  }
  const code = str.codePointAt(i);
  return code !== undefined && code >= kUTF16SurrogateThreshold ? 2 : 1;
}

// Note: we do not convert alt+z, alt+shift+z, or alt+v here
// because mac users have alternative hotkeys.
const MAC_ALT_KEY_CHARACTER_MAP: Record<string, string> = {
  '\u222B': 'b', // "∫" back one word
  '\u0192': 'f', // "ƒ" forward one word
  '\u00B5': 'm', // "µ" toggle markup view
  '\u03A9': 'z', // "Ω" Option+z
  '\u00B8': 'Z', // "¸" Option+Shift+z
  '\u2202': 'd', // "∂" delete word forward
};

function nonKeyboardEventFilter(
  keypressHandler: KeypressHandler,
): KeypressHandler {
  return (key: Key) => {
    if (
      !parseMouseEvent(key.sequence) &&
      key.sequence !== FOCUS_IN &&
      key.sequence !== FOCUS_OUT
    ) {
      keypressHandler(key);
    }
  };
}

/**
 * Converts return keys pressed quickly after insertable keys into a shift+return
 *
 * This is to accommodate older terminals that paste text without bracketing.
 */
function bufferFastReturn(keypressHandler: KeypressHandler): KeypressHandler {
  let lastKeyTime = 0;
  return (key: Key) => {
    const now = Date.now();
    if (key.name === 'enter' && now - lastKeyTime <= FAST_RETURN_TIMEOUT) {
      keypressHandler({
        ...key,
        name: 'enter',
        shift: true, // to make it a newline, not a submission
        alt: false,
        ctrl: false,
        cmd: false,
        sequence: '\r',
        insertable: true,
      });
    } else {
      keypressHandler(key);
    }
    lastKeyTime = key.insertable ? now : 0;
  };
}

/**
 * Buffers "/" keys to see if they are followed return.
 * Will flush the buffer if no data is received for DRAG_COMPLETION_TIMEOUT_MS
 * or when a null key is received.
 */
function bufferBackslashEnter(
  keypressHandler: KeypressHandler,
): KeypressHandler {
  const bufferer = (function* (): Generator<void, void, Key | null> {
    while (true) {
      const key = yield;

      if (key == null) {
        continue;
      } else if (key.sequence !== '\\') {
        keypressHandler(key);
        continue;
      }

      const timeoutId = setTimeout(
        () => bufferer.next(null),
        BACKSLASH_ENTER_TIMEOUT,
      );
      const nextKey = yield;
      clearTimeout(timeoutId);

      if (nextKey === null) {
        keypressHandler(key);
      } else if (nextKey.name === 'enter') {
        keypressHandler({
          ...nextKey,
          shift: true,
          sequence: '\r', // Corrected escaping for newline
        });
      } else {
        keypressHandler(key);
        keypressHandler(nextKey);
      }
    }
  })();

  bufferer.next(); // prime the generator so it starts listening.

  return (key: Key) => {
    bufferer.next(key);
  };
}

/**
 * Buffers paste events between paste-start and paste-end sequences.
 * Will flush the buffer if no data is received for PASTE_TIMEOUT ms or
 * when a null key is received.
 */
function bufferPaste(keypressHandler: KeypressHandler): KeypressHandler {
  const bufferer = (function* (): Generator<void, void, Key | null> {
    while (true) {
      let key = yield;

      if (key === null) {
        continue;
      } else if (key.name !== 'paste-start') {
        keypressHandler(key);
        continue;
      }

      let buffer = '';
      while (true) {
        const timeoutId = setTimeout(() => bufferer.next(null), PASTE_TIMEOUT);
        key = yield;
        clearTimeout(timeoutId);

        if (key === null) {
          appEvents.emit(AppEvent.PasteTimeout);
          break;
        }

        if (key.name === 'paste-end') {
          break;
        }
        buffer += key.sequence;
      }

      if (buffer.length > 0) {
        keypressHandler({
          name: 'paste',
          shift: false,
          alt: false,
          ctrl: false,
          cmd: false,
          insertable: true,
          sequence: buffer,
        });
      }
    }
  })();
  bufferer.next(); // prime the generator so it starts listening.

  return (key: Key) => {
    bufferer.next(key);
  };
}

/**
 * Turns raw data strings into keypress events sent to the provided handler.
 * Buffers escape sequences until a full sequence is received or
 * until a timeout occurs.
 */
function createDataListener(keypressHandler: KeypressHandler) {
  const parser = emitKeys(keypressHandler);
  parser.next(); // prime the generator so it starts listening.

  let timeoutId: NodeJS.Timeout;
  return (data: string) => {
    clearTimeout(timeoutId);
    for (const char of data) {
      parser.next(char);
    }
    if (data.length !== 0) {
      timeoutId = setTimeout(() => parser.next(''), ESC_TIMEOUT);
    }
  };
}

/**
 * Translates raw keypress characters into key events.
 * Buffers escape sequences until a full sequence is received or
 * until an empty string is sent to indicate a timeout.
 */
function* emitKeys(
  keypressHandler: KeypressHandler,
): Generator<void, void, string> {
  const lang = process.env['LANG'] || '';
  const lcAll = process.env['LC_ALL'] || '';
  const isGreek = lang.startsWith('el') || lcAll.startsWith('el');

  while (true) {
    let ch = yield;
    let sequence = ch;
    let escaped = false;

    let name = undefined;
    let shift = false;
    let alt = false;
    let ctrl = false;
    let cmd = false;
    let code = undefined;
    let insertable = false;

    if (ch === ESC) {
      escaped = true;
      ch = yield;
      sequence += ch;

      if (ch === ESC) {
        ch = yield;
        sequence += ch;
      }
    }

    if (escaped && (ch === 'O' || ch === '[' || ch === ']')) {
      // ANSI escape sequence
      code = ch;
      let modifier = 0;

      if (ch === ']') {
        // OSC sequence
        // ESC ] <params> ; <data> BEL
        // ESC ] <params> ; <data> ESC \
        let buffer = '';

        // Read until BEL, `ESC \`, or timeout (empty string)
        while (true) {
          const next = yield;
          if (next === '' || next === '\u0007') {
            break;
          } else if (next === ESC) {
            const afterEsc = yield;
            if (afterEsc === '' || afterEsc === '\\') {
              break;
            }
            buffer += next + afterEsc;
            continue;
          }
          buffer += next;
        }

        // Check for OSC 52 (Clipboard) response
        // Format: 52;c;<base64> or 52;p;<base64>
        const match = /^52;[cp];(.*)$/.exec(buffer);
        if (match) {
          try {
            const base64Data = match[1];
            const decoded = Buffer.from(base64Data, 'base64').toString('utf-8');
            keypressHandler({
              name: 'paste',
              shift: false,
              alt: false,
              ctrl: false,
              cmd: false,
              insertable: true,
              sequence: decoded,
            });
          } catch (_e) {
            debugLogger.log('Failed to decode OSC 52 clipboard data');
          }
        }

        continue; // resume main loop
      } else if (ch === 'O') {
        // ESC O letter
        // ESC O modifier letter
        ch = yield;
        sequence += ch;

        if (ch >= '0' && ch <= '9') {
          modifier = parseInt(ch, 10) - 1;
          ch = yield;
          sequence += ch;
        }

        code += ch;
      } else if (ch === '[') {
        // ESC [ letter
        // ESC [ modifier letter
        // ESC [ [ modifier letter
        // ESC [ [ num char
        ch = yield;
        sequence += ch;

        if (ch === '[') {
          // \x1b[[A
          //      ^--- escape codes might have a second bracket
          code += ch;
          ch = yield;
          sequence += ch;
        }

        /*
         * Here and later we try to buffer just enough data to get
         * a complete ascii sequence.
         *
         * We have basically two classes of ascii characters to process:
         *
         *
         * 1. `\x1b[24;5~` should be parsed as { code: '[24~', modifier: 5 }
         *
         * This particular example is featuring Ctrl+F12 in xterm.
         *
         *  - `;5` part is optional, e.g. it could be `\x1b[24~`
         *  - first part can contain one or two digits
         *  - there is also special case when there can be 3 digits
         *    but without modifier. They are the case of paste bracket mode
         *
         * So the generic regexp is like /^(?:\d\d?(;\d)?[~^$]|\d{3}~)$/
         *
         *
         * 2. `\x1b[1;5H` should be parsed as { code: '[H', modifier: 5 }
         *
         * This particular example is featuring Ctrl+Home in xterm.
         *
         *  - `1;5` part is optional, e.g. it could be `\x1b[H`
         *  - `1;` part is optional, e.g. it could be `\x1b[5H`
         *
         * So the generic regexp is like /^((\d;)?\d)?[A-Za-z]$/
         *
         */
        const cmdStart = sequence.length - 1;

        // collect as many digits as possible
        while (ch >= '0' && ch <= '9') {
          ch = yield;
          sequence += ch;
        }

        // skip modifier
        if (ch === ';') {
          while (ch === ';') {
            ch = yield;
            sequence += ch;

            // collect as many digits as possible
            while (ch >= '0' && ch <= '9') {
              ch = yield;
              sequence += ch;
            }
          }
        } else if (ch === '<') {
          // SGR mouse mode
          ch = yield;
          sequence += ch;
          // Don't skip on empty string here to avoid timeouts on slow events.
          while (ch === '' || ch === ';' || (ch >= '0' && ch <= '9')) {
            ch = yield;
            sequence += ch;
          }
        } else if (ch === 'M') {
          // X11 mouse mode
          // three characters after 'M'
          ch = yield;
          sequence += ch;
          ch = yield;
          sequence += ch;
          ch = yield;
          sequence += ch;
        }

        /*
         * We buffered enough data, now trying to extract code
         * and modifier from it
         */
        const cmd = sequence.slice(cmdStart);
        let match;

        if ((match = /^(\d+)(?:;(\d+))?(?:;(\d+))?([~^$u])$/.exec(cmd))) {
          if (match[1] === '27' && match[3] && match[4] === '~') {
            // modifyOtherKeys format: CSI 27 ; modifier ; key ~
            // Treat as CSI u: key + 'u'
            code += match[3] + 'u';
            modifier = parseInt(match[2] ?? '1', 10) - 1;
          } else {
            code += match[1] + match[4];
            // Defaults to '1' if no modifier exists, resulting in a 0 modifier value
            modifier = parseInt(match[2] ?? '1', 10) - 1;
          }
        } else if ((match = /^(\d+)?(?:;(\d+))?([A-Za-z])$/.exec(cmd))) {
          code += match[3];
          modifier = parseInt(match[2] ?? match[1] ?? '1', 10) - 1;
        } else {
          code += cmd;
        }
      }

      // Parse the key modifier
      shift = !!(modifier & 1);
      alt = !!(modifier & 2);
      ctrl = !!(modifier & 4);
      cmd = !!(modifier & 8);

      const keyInfo = KEY_INFO_MAP[code];
      if (keyInfo) {
        name = keyInfo.name;
        if (keyInfo.shift) {
          shift = true;
        }
        if (keyInfo.ctrl) {
          ctrl = true;
        }
        if (name === 'space' && !ctrl && !cmd && !alt) {
          sequence = ' ';
          insertable = true;
        }
      } else {
        const numpadChar = NUMPAD_MAP[code];
        if (numpadChar) {
          name = numpadChar;
          if (!ctrl && !cmd && !alt) {
            sequence = numpadChar;
            insertable = true;
          }
        } else {
          name = 'undefined';
          if (code.endsWith('u') || code.endsWith('~')) {
            // CSI-u or tilde-coded functional keys: ESC [ <code> ; <mods> (u|~)
            const codeNumber = parseInt(code.slice(1, -1), 10);
            const mapped = KITTY_CODE_MAP[codeNumber];
            if (mapped) {
              name = mapped.name;
              if (mapped.sequence && !ctrl && !cmd && !alt) {
                sequence = mapped.sequence;
                insertable = true;
              }
            } else if (
              codeNumber >= 33 && // Printable characters start after space (32),
              codeNumber <= 0x10ffff && // Valid Unicode scalar values (excluding control characters)
              (codeNumber < 0xd800 || codeNumber > 0xdfff) // Exclude UTF-16 surrogate halves
            ) {
              // Valid printable Unicode scalar values (up to Unicode maximum)
              // Note: Kitty maps its special keys to the PUA (57344+), which are handled by KITTY_CODE_MAP above.
              const char = String.fromCodePoint(codeNumber);
              name = char.toLowerCase();
              if (char !== name) {
                shift = true;
              }
              if (!ctrl && !cmd && !alt) {
                sequence = char;
                insertable = true;
              }
            }
          }
        }
      }
    } else if (ch === '\r') {
      // carriage return
      name = 'enter';
      alt = escaped;
    } else if (escaped && ch === '\n') {
      // Alt+Enter (linefeed), should be consistent with carriage return
      name = 'enter';
      alt = escaped;
    } else if (ch === '\t') {
      // tab
      name = 'tab';
      alt = escaped;
    } else if (ch === '\b' || ch === '\x7f') {
      // backspace or ctrl+h
      name = 'backspace';
      alt = escaped;
    } else if (ch === ESC) {
      // escape key
      name = 'escape';
      alt = escaped;
    } else if (ch === ' ') {
      name = 'space';
      alt = escaped;
      insertable = true;
    } else if (!escaped && ch <= '\x1a') {
      // ctrl+letter
      name = String.fromCharCode(ch.charCodeAt(0) + 'a'.charCodeAt(0) - 1);
      ctrl = true;
    } else if (/^[0-9A-Za-z]$/.exec(ch) !== null) {
      // Letter, number, shift+letter
      name = ch.toLowerCase();
      shift = /^[A-Z]$/.exec(ch) !== null;
      alt = escaped;
      insertable = true;
    } else if (MAC_ALT_KEY_CHARACTER_MAP[ch]) {
      // Note: we do this even if we are not on Mac, because mac users may
      // remotely connect to non-Mac systems.
      // We skip this mapping for Greek users to avoid blocking the Omega character.
      if (isGreek && ch === '\u03A9') {
        insertable = true;
      } else {
        const mapped = MAC_ALT_KEY_CHARACTER_MAP[ch];
        name = mapped.toLowerCase();
        shift = mapped !== name;
        alt = true;
      }
    } else if (sequence === `${ESC}${ESC}`) {
      // Double escape
      name = 'escape';
      alt = false;

      // Emit first escape key here, then continue processing
      keypressHandler({
        name: 'escape',
        shift,
        alt,
        ctrl,
        cmd,
        insertable: false,
        sequence: ESC,
      });
    } else if (escaped) {
      // Escape sequence timeout
      name = ch.length ? undefined : 'escape';
      alt = ch.length > 0;
    } else {
      // Any other character is considered printable.
      name = ch.toLowerCase();
      if (ch !== name) {
        shift = true;
      }
      insertable = true;
    }

    if (
      (sequence.length !== 0 && (name !== undefined || escaped)) ||
      charLengthAt(sequence, 0) === sequence.length
    ) {
      keypressHandler({
        name: name || '',
        shift,
        alt,
        ctrl,
        cmd,
        insertable,
        sequence,
      });
    }
    // Unrecognized or broken escape sequence, don't emit anything
  }
}

export interface Key {
  name: string;
  shift: boolean;
  alt: boolean;
  ctrl: boolean;
  cmd: boolean; // Command/Windows/Super key
  insertable: boolean;
  sequence: string;
}

export type KeypressHandler = (key: Key) => boolean | void;

interface KeypressContextValue {
  subscribe: (
    handler: KeypressHandler,
    priority?: KeypressPriority | boolean,
  ) => void;
  unsubscribe: (handler: KeypressHandler) => void;
}

const KeypressContext = createContext<KeypressContextValue | undefined>(
  undefined,
);

export function useKeypressContext() {
  const context = useContext(KeypressContext);
  if (!context) {
    throw new Error(
      'useKeypressContext must be used within a KeypressProvider',
    );
  }
  return context;
}

export function KeypressProvider({
  children,
  config,
}: {
  children: React.ReactNode;
  config?: Config;
}) {
  const { settings } = useSettingsStore();
  const debugKeystrokeLogging = settings.merged.general.debugKeystrokeLogging;

  const { stdin, setRawMode } = useStdin();

  const subscribersToPriority = useRef<Map<KeypressHandler, number>>(
    new Map(),
  ).current;
  const subscribers = useRef(
    new MultiMap<number, KeypressHandler>(Set),
  ).current;
  const sortedPriorities = useRef<number[]>([]);

  const subscribe = useCallback(
    (
      handler: KeypressHandler,
      priority: KeypressPriority | boolean = KeypressPriority.Normal,
    ) => {
      const p =
        typeof priority === 'boolean'
          ? priority
            ? KeypressPriority.High
            : KeypressPriority.Normal
          : priority;

      subscribersToPriority.set(handler, p);
      const hadPriority = subscribers.has(p);
      subscribers.set(p, handler);

      if (!hadPriority) {
        // Cache sorted priorities only when a new priority level is added
        sortedPriorities.current = Array.from(subscribers.keys()).sort(
          (a, b) => b - a,
        );
      }
    },
    [subscribers, subscribersToPriority],
  );

  const unsubscribe = useCallback(
    (handler: KeypressHandler) => {
      const p = subscribersToPriority.get(handler);
      if (p !== undefined) {
        subscribers.remove(p, handler);
        subscribersToPriority.delete(handler);

        if (!subscribers.has(p)) {
          // Cache sorted priorities only when a priority level is completely removed
          sortedPriorities.current = Array.from(subscribers.keys()).sort(
            (a, b) => b - a,
          );
        }
      }
    },
    [subscribers, subscribersToPriority],
  );

  const broadcast = useCallback(
    (key: Key) => {
      if (debugKeystrokeLogging) {
        debugLogger.log('[DEBUG] Keystroke:', JSON.stringify(key));
      }
      // Use cached sorted priorities to avoid sorting on every keypress
      for (const p of sortedPriorities.current) {
        const set = subscribers.get(p);
        if (!set) continue;

        // Within a priority level, use stack behavior (last subscribed is first to handle)
        const handlers = Array.from(set).reverse();
        for (const handler of handlers) {
          if (handler(key) === true) {
            return;
          }
        }
      }
    },
    [subscribers, debugKeystrokeLogging],
  );

  useEffect(() => {
    terminalCapabilityManager.enableSupportedModes();

    const wasRaw = stdin.isRaw;
    if (wasRaw === false) {
      setRawMode(true);
    }

    process.stdin.setEncoding('utf8'); // Make data events emit strings

    let processor = nonKeyboardEventFilter(broadcast);
    if (!terminalCapabilityManager.isKittyProtocolEnabled()) {
      processor = bufferFastReturn(processor);
    }
    processor = bufferBackslashEnter(processor);
    processor = bufferPaste(processor);
    let dataListener = createDataListener(processor);

    if (debugKeystrokeLogging) {
      const old = dataListener;
      dataListener = (data: string) => {
        if (data.length > 0) {
          debugLogger.log(`[DEBUG] Raw StdIn: ${JSON.stringify(data)}`);
        }
        old(data);
      };
    }

    stdin.on('data', dataListener);
    return () => {
      stdin.removeListener('data', dataListener);
      if (wasRaw === false) {
        setRawMode(false);
      }
    };
  }, [stdin, setRawMode, config, debugKeystrokeLogging, broadcast]);

  const contextValue = useMemo(
    () => ({ subscribe, unsubscribe }),
    [subscribe, unsubscribe],
  );

  return (
    <KeypressContext.Provider value={contextValue}>
      {children}
    </KeypressContext.Provider>
  );
}
