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

import { enableMouseEvents, disableMouseEvents } from '@google/gemini-cli-core';
import {
  SGR_MOUSE_REGEX,
  X11_MOUSE_REGEX,
  SGR_EVENT_PREFIX,
  X11_EVENT_PREFIX,
  couldBeMouseSequence as inputCouldBeMouseSequence,
} from './input.js';

export type MouseEventName =
  | 'left-press'
  | 'left-release'
  | 'right-press'
  | 'right-release'
  | 'middle-press'
  | 'middle-release'
  | 'scroll-up'
  | 'scroll-down'
  | 'scroll-left'
  | 'scroll-right'
  | 'move'
  | 'double-click';

export const DOUBLE_CLICK_THRESHOLD_MS = 400;
export const DOUBLE_CLICK_DISTANCE_TOLERANCE = 2;

export interface MouseEvent {
  name: MouseEventName;
  col: number;
  row: number;
  shift: boolean;
  meta: boolean;
  ctrl: boolean;
  button: 'left' | 'middle' | 'right' | 'none';
}

export type MouseHandler = (event: MouseEvent) => void | boolean;

export function getMouseEventName(
  buttonCode: number,
  isRelease: boolean,
): MouseEventName | null {
  const isMove = (buttonCode & 32) !== 0;

  if (buttonCode === 66) {
    return 'scroll-left';
  } else if (buttonCode === 67) {
    return 'scroll-right';
  } else if ((buttonCode & 64) === 64) {
    if ((buttonCode & 1) === 0) {
      return 'scroll-up';
    } else {
      return 'scroll-down';
    }
  } else if (isMove) {
    return 'move';
  } else {
    const button = buttonCode & 3;
    const type = isRelease ? 'release' : 'press';
    switch (button) {
      case 0:
        return `left-${type}`;
      case 1:
        return `middle-${type}`;
      case 2:
        return `right-${type}`;
      default:
        return null;
    }
  }
}

function getButtonFromCode(code: number): MouseEvent['button'] {
  const button = code & 3;
  switch (button) {
    case 0:
      return 'left';
    case 1:
      return 'middle';
    case 2:
      return 'right';
    default:
      return 'none';
  }
}

export function parseSGRMouseEvent(
  buffer: string,
): { event: MouseEvent; length: number } | null {
  const match = buffer.match(SGR_MOUSE_REGEX);

  if (match) {
    const buttonCode = parseInt(match[1], 10);
    const col = parseInt(match[2], 10);
    const row = parseInt(match[3], 10);
    const action = match[4];
    const isRelease = action === 'm';

    const shift = (buttonCode & 4) !== 0;
    const meta = (buttonCode & 8) !== 0;
    const ctrl = (buttonCode & 16) !== 0;

    const name = getMouseEventName(buttonCode, isRelease);

    if (name) {
      return {
        event: {
          name,
          ctrl,
          meta,
          shift,
          col,
          row,
          button: getButtonFromCode(buttonCode),
        },
        length: match[0].length,
      };
    }
    return null;
  }

  return null;
}

export function parseX11MouseEvent(
  buffer: string,
): { event: MouseEvent; length: number } | null {
  const match = buffer.match(X11_MOUSE_REGEX);
  if (!match) return null;

  // The 3 bytes are in match[1]
  const b = match[1].charCodeAt(0) - 32;
  const col = match[1].charCodeAt(1) - 32;
  const row = match[1].charCodeAt(2) - 32;

  const shift = (b & 4) !== 0;
  const meta = (b & 8) !== 0;
  const ctrl = (b & 16) !== 0;
  const isMove = (b & 32) !== 0;
  const isWheel = (b & 64) !== 0;

  let name: MouseEventName | null = null;

  if (isWheel) {
    const button = b & 3;
    switch (button) {
      case 0:
        name = 'scroll-up';
        break;
      case 1:
        name = 'scroll-down';
        break;
      default:
        break;
    }
  } else if (isMove) {
    name = 'move';
  } else {
    const button = b & 3;
    if (button === 3) {
      // X11 reports 'release' (3) for all button releases without specifying which one.
      // We'll default to 'left-release' as a best-effort guess if we don't track state.
      name = 'left-release';
    } else {
      switch (button) {
        case 0:
          name = 'left-press';
          break;
        case 1:
          name = 'middle-press';
          break;
        case 2:
          name = 'right-press';
          break;
        default:
          break;
      }
    }
  }

  if (name) {
    let button = getButtonFromCode(b);
    if (name === 'left-release' && button === 'none') {
      button = 'left';
    }

    return {
      event: {
        name,
        ctrl,
        meta,
        shift,
        col,
        row,
        button,
      },
      length: match[0].length,
    };
  }
  return null;
}

export function parseMouseEvent(
  buffer: string,
): { event: MouseEvent; length: number } | null {
  return parseSGRMouseEvent(buffer) || parseX11MouseEvent(buffer);
}

export function isIncompleteMouseSequence(buffer: string): boolean {
  if (!inputCouldBeMouseSequence(buffer)) return false;

  // If it matches a complete sequence, it's not incomplete.
  if (parseMouseEvent(buffer)) return false;

  if (buffer.startsWith(X11_EVENT_PREFIX)) {
    // X11 needs exactly 3 bytes after prefix.
    return buffer.length < X11_EVENT_PREFIX.length + 3;
  }

  if (buffer.startsWith(SGR_EVENT_PREFIX)) {
    // SGR sequences end with 'm' or 'M'.
    // If it doesn't have it yet, it's incomplete.
    // Add a reasonable max length check to fail early on garbage.
    return !/[mM]/.test(buffer) && buffer.length < 50;
  }

  // It's a prefix of the prefix (e.g. "ESC" or "ESC [")
  return true;
}

export { enableMouseEvents, disableMouseEvents };
