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

import { useStdin } from 'ink';
import type React from 'react';
import {
  createContext,
  useCallback,
  useContext,
  useEffect,
  useLayoutEffect,
  useMemo,
  useRef,
} from 'react';
import { ESC } from '../utils/input.js';
import { debugLogger } from '@google/gemini-cli-core';
import { appEvents, AppEvent } from '../../utils/events.js';
import {
  isIncompleteMouseSequence,
  parseMouseEvent,
  type MouseEvent,
  type MouseEventName,
  type MouseHandler,
  DOUBLE_CLICK_THRESHOLD_MS,
  DOUBLE_CLICK_DISTANCE_TOLERANCE,
} from '../utils/mouse.js';
import { useSettingsStore } from './SettingsContext.js';

export type { MouseEvent, MouseEventName, MouseHandler };

const MAX_MOUSE_BUFFER_SIZE = 4096;

interface MouseContextValue {
  subscribe: (handler: MouseHandler) => void;
  unsubscribe: (handler: MouseHandler) => void;
  broadcast: (event: MouseEvent) => void;
}

const MouseContext = createContext<MouseContextValue | undefined>(undefined);

export function useMouseContext() {
  const context = useContext(MouseContext);
  if (!context) {
    throw new Error('useMouseContext must be used within a MouseProvider');
  }
  return context;
}

export function useMouse(handler: MouseHandler, { isActive = true } = {}) {
  const { subscribe, unsubscribe } = useMouseContext();

  useLayoutEffect(() => {
    if (!isActive) {
      return;
    }

    subscribe(handler);
    return () => unsubscribe(handler);
  }, [isActive, handler, subscribe, unsubscribe]);
}

export function MouseProvider({
  children,
  mouseEventsEnabled,
}: {
  children: React.ReactNode;
  mouseEventsEnabled?: boolean;
}) {
  const { settings } = useSettingsStore();
  const debugKeystrokeLogging = settings.merged.general.debugKeystrokeLogging;

  const { stdin } = useStdin();
  const subscribers = useRef<Set<MouseHandler>>(new Set()).current;
  const lastClickRef = useRef<{
    time: number;
    col: number;
    row: number;
  } | null>(null);

  const subscribe = useCallback(
    (handler: MouseHandler) => {
      subscribers.add(handler);
    },
    [subscribers],
  );

  const unsubscribe = useCallback(
    (handler: MouseHandler) => {
      subscribers.delete(handler);
    },
    [subscribers],
  );

  const broadcast = useCallback(
    (event: MouseEvent) => {
      let handled = false;
      for (const handler of subscribers) {
        if (handler(event) === true) {
          handled = true;
        }
      }

      if (event.name === 'left-press') {
        const now = Date.now();
        const lastClick = lastClickRef.current;
        if (
          lastClick &&
          now - lastClick.time < DOUBLE_CLICK_THRESHOLD_MS &&
          Math.abs(event.col - lastClick.col) <=
            DOUBLE_CLICK_DISTANCE_TOLERANCE &&
          Math.abs(event.row - lastClick.row) <= DOUBLE_CLICK_DISTANCE_TOLERANCE
        ) {
          const doubleClickEvent: MouseEvent = {
            ...event,
            name: 'double-click',
          };
          for (const handler of subscribers) {
            handler(doubleClickEvent);
          }
          lastClickRef.current = null;
        } else {
          lastClickRef.current = { time: now, col: event.col, row: event.row };
        }
      }

      if (
        !handled &&
        event.name === 'move' &&
        event.col >= 0 &&
        event.row >= 0 &&
        event.button === 'left'
      ) {
        // Terminal apps only receive mouse move events when the mouse is down
        // so this always indicates a mouse drag that the user was expecting
        // would trigger text selection but does not as we are handling mouse
        // events not the terminal.
        appEvents.emit(AppEvent.SelectionWarning);
      }
    },
    [subscribers],
  );

  useEffect(() => {
    if (!mouseEventsEnabled) {
      return;
    }

    let mouseBuffer = '';

    const handleData = (data: Buffer | string) => {
      mouseBuffer += typeof data === 'string' ? data : data.toString('utf-8');

      // Safety cap to prevent infinite buffer growth on garbage
      if (mouseBuffer.length > MAX_MOUSE_BUFFER_SIZE) {
        mouseBuffer = mouseBuffer.slice(-MAX_MOUSE_BUFFER_SIZE);
      }

      while (mouseBuffer.length > 0) {
        const parsed = parseMouseEvent(mouseBuffer);

        if (parsed) {
          if (debugKeystrokeLogging) {
            debugLogger.log(
              '[DEBUG] Mouse event parsed:',
              JSON.stringify(parsed.event),
            );
          }
          broadcast(parsed.event);
          mouseBuffer = mouseBuffer.slice(parsed.length);
          continue;
        }

        if (isIncompleteMouseSequence(mouseBuffer)) {
          break; // Wait for more data
        }

        // Not a valid sequence at start, and not waiting for more data.
        // Discard garbage until next possible sequence start.
        const nextEsc = mouseBuffer.indexOf(ESC, 1);
        if (nextEsc !== -1) {
          mouseBuffer = mouseBuffer.slice(nextEsc);
          // Loop continues to try parsing at new location
        } else {
          mouseBuffer = '';
          break;
        }
      }
    };

    stdin.on('data', handleData);

    return () => {
      stdin.removeListener('data', handleData);
    };
  }, [stdin, mouseEventsEnabled, broadcast, debugKeystrokeLogging]);

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

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