interface EventMap {
  keypress: CLIKeypressEvent;
  char: CLICharEvent;
  activate: Event;
  deactivate: Event;
  exit: Event;
  enter: Event;
  backspace: Event;
  escape: Event;
  delete: Event;
  "arrow-left": Event;
  "arrow-right": Event;
  "arrow-up": Event;
  "arrow-down": Event;
  // [key: string]: Event;
}

interface EventDetailMap {
  keypress: {
    key: number;
    sequence?: Uint8Array;
  };
  char: EventDetailMap["keypress"] & {
    char: string;
  };
}

export type TypedEventTarget<EventMap extends object> = {
  new (): IntermediateEventTarget<EventMap>;
};

export interface IntermediateEventTarget<EventMap> extends EventTarget {
  addEventListener<K extends keyof EventMap>(
    type: K,
    listener: (
      event: EventMap[K] extends Event ? EventMap[K] : Event,
    ) => EventMap[K] extends Event ? void : never,
    options?: boolean | AddEventListenerOptions,
  ): void;

  addEventListener(
    type: string,
    listener: EventListenerOrEventListenerObject,
    options?: boolean | AddEventListenerOptions,
  ): void;

  removeEventListener<K extends keyof EventMap>(
    type: K,
    listener: (
      event: EventMap[K] extends Event ? EventMap[K] : Event,
    ) => EventMap[K] extends Event ? void : never,
    options?: boolean | AddEventListenerOptions,
  ): void;

  removeEventListener(
    type: string,
    listener: EventListenerOrEventListenerObject,
    options?: boolean | AddEventListenerOptions,
  ): void;
}

const ManagerEventTarget = EventTarget as TypedEventTarget<EventMap>;

export class CLICharEvent extends CustomEvent<EventDetailMap["char"]> {
  constructor(detail: EventDetailMap["char"]) {
    super("char", { detail, cancelable: true });
  }
}
export class CLIKeypressEvent extends CustomEvent<EventDetailMap["keypress"]> {
  constructor(detail: EventDetailMap["keypress"]) {
    super("keypress", { detail, cancelable: true });
  }
}

export class InputManager extends ManagerEventTarget {
  private static instance = new InputManager();
  private active = false;

  static getInstance(): InputManager {
    return this.instance ??= new InputManager();
  }

  static addEventListener = InputManager.prototype.addEventListener.bind(
    this.instance,
  );
  static removeEventListener = InputManager.prototype.removeEventListener.bind(
    this.instance,
  );
  static dispatchEvent = InputManager.prototype.dispatchEvent.bind(
    this.instance,
  );

  activate({ raw = true }: { raw?: boolean } = {}) {
    if (this.active) return;
    this.active = true;
    this.dispatchEvent(new Event("activate"));
    this.listen(raw);
  }

  deactivate({ dactivateRaw = true }: { dactivateRaw?: boolean } = {}) {
    if (!this.active) return;
    this.active = false;
    this.dispatchEvent(new Event("deactivate"));
    if (dactivateRaw) Deno.stdin.setRaw(false);
  }

  once<T extends string>(type: T): Promise<Event> {
    return new Promise((resolve) => {
      const handler = (event: Event) => {
        this.removeEventListener(type, handler);
        resolve(event);
      };
      this.addEventListener(type, handler);
    });
  }

  private async listen(raw: boolean) {
    if (raw) await Deno.stdin.setRaw(true);
    const buf = new Uint8Array(64);

    while (this.active) {
      const n = await Deno.stdin.read(buf);
      if (n === null) break;

      let i = 0;
      while (i < n) {
        const byte = buf[i];

        // Ctrl+C
        if (byte === 3) {
          this.dispatchEvent(new Event("exit"));
          await Deno.stdin.setRaw(false);
          Deno.exit(130);
        }

        // Enter
        if (byte === 13) {
          this.dispatchEvent(new Event("enter"));
          i++;
          continue;
        }

        // Backspace
        if (byte === 127 || byte === 8) {
          this.dispatchEvent(new Event("backspace"));
          i++;
          continue;
        }

        if (byte === 27 && i + 1 >= n) {
          this.dispatchEvent(new Event("escape"));
          i++;
          continue;
        }

        // Escape sequences
        if (byte === 27 && i + 1 < n && buf[i + 1] === 91) {
          const code = buf[i + 2];
          switch (code) {
            case 65:
              this.dispatchEvent(new Event("arrow-up"));
              break;
            case 66:
              this.dispatchEvent(new Event("arrow-down"));
              break;
            case 67:
              this.dispatchEvent(new Event("arrow-right"));
              break;
            case 68:
              this.dispatchEvent(new Event("arrow-left"));
              break;
            case 51:
              if (i + 3 < n && buf[i + 3] === 126) {
                this.dispatchEvent(new Event("delete"));
                i += 4;
                continue;
              }
              break;
          }
          i += 3;
          continue;
        }

        // Printable ASCII
        if (byte >= 32 && byte <= 126) {
          this.dispatchEvent(
            new CLICharEvent({ key: byte, char: String.fromCharCode(byte) }),
          );
          i++;
          continue;
        }

        // Unknown
        this.dispatchEvent(
          new CLIKeypressEvent({ key: byte, sequence: buf.slice(i, i + 1) }),
        );
        i++;
      }
    }

    if (raw) await Deno.stdin.setRaw(false);
  }

  dispatchKey(key: string) {
    switch (key) {
      case "enter":
      case "backspace":
      case "arrow-up":
      case "arrow-down":
      case "arrow-right":
      case "arrow-left":
      case "delete":
      case "escape":
        this.dispatchEvent(new Event(key));
        break;
      default:
        this.dispatchEvent(
          new CLICharEvent({ key: key.charCodeAt(0), char: key }),
        );
    }
  }
}
