import type { VoidFunction } from '@robonen/stdlib';
import { isFunction } from '@robonen/stdlib';
import { computed } from 'vue';
import type { MaybeComputedElementRef } from '@/composables/component/unrefElement';
import { unrefElement } from '@/composables/component/unrefElement';
import { useEventListener } from '@/composables/browser/useEventListener';
import { tryOnScopeDispose } from '@/composables/lifecycle/tryOnScopeDispose';

const DEFAULT_DELAY = 500;
const DEFAULT_THRESHOLD = 10;

export interface OnLongPressPosition {
  x: number;
  y: number;
}

export interface OnLongPressModifiers {
  /**
   * Call `stopPropagation` on the pointer events.
   *
   * @default false
   */
  stop?: boolean;

  /**
   * Register the underlying listeners with `{ once: true }`.
   *
   * @default false
   */
  once?: boolean;

  /**
   * Call `preventDefault` on the pointer events. Forces non-passive listeners.
   *
   * @default false
   */
  prevent?: boolean;

  /**
   * Register the underlying listeners in the capture phase.
   *
   * @default false
   */
  capture?: boolean;

  /**
   * Only react when the event originates from the target element itself
   * (i.e. ignore events that bubbled up from descendants).
   *
   * @default false
   */
  self?: boolean;
}

export interface OnLongPressOptions {
  /**
   * Time in milliseconds the pointer must be held before the handler fires.
   * A function receives the originating `pointerdown` event and returns the delay.
   *
   * @default 500
   */
  delay?: number | ((event: PointerEvent) => number);

  /**
   * Pointer-event behaviour flags.
   */
  modifiers?: OnLongPressModifiers;

  /**
   * Maximum pointer travel (in pixels) tolerated before the press is cancelled.
   * Pass `false` to disable movement cancellation entirely.
   *
   * @default 10
   */
  distanceThreshold?: number | false;

  /**
   * Invoked on pointer release. Receives the press duration (ms), the distance
   * travelled (px), whether the press qualified as a long press, and the event.
   */
  onMouseUp?: (duration: number, distance: number, isLongPress: boolean, event: PointerEvent) => void;
}

export type OnLongPressReturn = VoidFunction;

/**
 * @name onLongPress
 * @category Sensors
 * @description Directive-like helper that invokes a handler after a sustained long press
 * on a target element. Movement beyond `distanceThreshold` cancels the press, and an
 * optional `onMouseUp` callback reports the press duration, distance, and long-press status.
 * Listeners are passive by default and registered via `useEventListener` for automatic cleanup.
 *
 * @param {MaybeComputedElementRef} target Element to watch for long presses
 * @param {(event: PointerEvent) => void} handler Callback fired once the press passes `delay`
 * @param {OnLongPressOptions} [options={}] Long-press configuration
 * @returns {OnLongPressReturn} Stop handle that removes all listeners
 *
 * @example
 * onLongPress(el, () => console.log('held!'));
 *
 * @example
 * // Custom delay, cancel-on-move disabled, release reporting
 * const stop = onLongPress(el, onHeld, {
 *   delay: 800,
 *   distanceThreshold: false,
 *   modifiers: { prevent: true },
 *   onMouseUp: (duration, distance, isLongPress) => {
 *     if (!isLongPress) onTap();
 *   },
 * });
 *
 * @since 0.0.14
 */
export function onLongPress(
  target: MaybeComputedElementRef,
  handler: (event: PointerEvent) => void,
  options: OnLongPressOptions = {},
): OnLongPressReturn {
  const { delay = DEFAULT_DELAY, modifiers = {}, distanceThreshold = DEFAULT_THRESHOLD, onMouseUp } = options;
  const { self: selfOnly = false, prevent = false, stop: stopProp = false, capture = false, once = false } = modifiers;

  // Resolve the element once and cache it; reactive targets re-trigger this getter
  // and the underlying useEventListener watch re-binds automatically.
  const elementRef = computed(() => unrefElement(target));

  // Hoisted as a function-valued delay only when the caller supplies one,
  // so the common numeric path skips the per-press branch entirely.
  const getDelay = isFunction(delay)
    ? delay
    : (): number => delay;

  let timeout: ReturnType<typeof setTimeout> | undefined;
  let posStart: OnLongPressPosition | undefined;
  let startTimestamp: number | undefined;
  let hasLongPressed = false;

  function clear(): void {
    if (timeout !== undefined) {
      clearTimeout(timeout);
      timeout = undefined;
    }
    posStart = undefined;
    startTimestamp = undefined;
    hasLongPressed = false;
  }

  function isForeignTarget(event: PointerEvent): boolean {
    return selfOnly && event.target !== elementRef.value;
  }

  function applyModifiers(event: PointerEvent): void {
    if (prevent)
      event.preventDefault();
    if (stopProp)
      event.stopPropagation();
  }

  function onDown(event: PointerEvent): void {
    if (isForeignTarget(event))
      return;

    clear();
    applyModifiers(event);

    posStart = { x: event.x, y: event.y };
    startTimestamp = event.timeStamp;

    timeout = setTimeout(() => {
      hasLongPressed = true;
      handler(event);
    }, getDelay(event));
  }

  function onMove(event: PointerEvent): void {
    if (isForeignTarget(event) || !posStart || distanceThreshold === false)
      return;

    applyModifiers(event);

    const dx = event.x - posStart.x;
    const dy = event.y - posStart.y;
    const distance = Math.hypot(dx, dy);

    if (distance >= distanceThreshold)
      clear();
  }

  function onRelease(event: PointerEvent): void {
    const _posStart = posStart;
    const _startTimestamp = startTimestamp;
    const _hasLongPressed = hasLongPressed;
    clear();

    if (!onMouseUp || !_posStart || _startTimestamp === undefined)
      return;

    if (isForeignTarget(event))
      return;

    applyModifiers(event);

    const dx = event.x - _posStart.x;
    const dy = event.y - _posStart.y;
    const distance = Math.hypot(dx, dy);

    onMouseUp(event.timeStamp - _startTimestamp, distance, _hasLongPressed, event);
  }

  // Passive unless the caller intends to preventDefault, in which case the
  // listener must be active for the browser to honour it.
  const listenerOptions: AddEventListenerOptions = {
    capture,
    once,
    passive: !prevent,
  };

  const cleanups = [
    useEventListener(elementRef, 'pointerdown', onDown, listenerOptions),
    useEventListener(elementRef, 'pointermove', onMove, listenerOptions),
    useEventListener(elementRef, ['pointerup', 'pointerleave'], onRelease, listenerOptions),
  ];

  // The listeners above self-dispose with the scope, but a delay timer armed
  // by a press that never released would outlive the component and fire the
  // handler against a dead scope.
  tryOnScopeDispose(clear);

  return (): void => {
    clear();
    cleanups.forEach(stop => stop());
  };
}
