import { readable, type Readable } from 'svelte/store';

export const LONG_PRESS_MS = 450;
export const LONG_PRESS_SLOP = 8;

export interface PressEvent {
  clientX: number;
  clientY: number;
  currentTarget: EventTarget | null;
}

export interface Timers {
  set(fn: () => void, ms: number): unknown;
  clear(handle: unknown): void;
}

const realTimers: Timers = {
  set: (fn, ms) => setTimeout(fn, ms),
  clear: (handle) => clearTimeout(handle as ReturnType<typeof setTimeout>)
};

export class PressTracker {
  fired = false;
  private handle: unknown = null;
  private x = 0;
  private y = 0;

  constructor(
    private readonly onFire: (x: number, y: number) => void,
    private readonly timers: Timers = realTimers,
    private readonly delay = LONG_PRESS_MS,
    private readonly slop = LONG_PRESS_SLOP
  ) {}

  get pending(): boolean {
    return this.handle !== null;
  }

  start(x: number, y: number): void {
    this.cancel();
    this.fired = false;
    this.x = x;
    this.y = y;
    this.handle = this.timers.set(() => {
      this.handle = null;
      this.fired = true;
      this.onFire(this.x, this.y);
    }, this.delay);
  }

  move(x: number, y: number): void {
    if (this.handle === null) return;
    if (Math.hypot(x - this.x, y - this.y) > this.slop) this.cancel();
  }

  flush(): void {
    if (this.handle === null) return;
    this.cancel();
    this.fired = true;
    this.onFire(this.x, this.y);
  }

  cancel(): void {
    if (this.handle === null) return;
    this.timers.clear(this.handle);
    this.handle = null;
  }
}

type Handler = ((ev: PressEvent) => void) | null | undefined;

interface Options {
  on: Handler;
  allowSelection?: boolean;
  ignore?: string;
}

type Param = Handler | Options;

function split(param: Param): Options {
  return typeof param === 'function' || !param ? { on: param } : param;
}

export function longpress(node: HTMLElement, param: Param) {
  let opts = split(param);
  let current = opts.on;
  let pointerId = -1;

  const tracker = new PressTracker((x, y) => {
    navigator.vibrate?.(10);
    current?.({ clientX: x, clientY: y, currentTarget: node });
  });

  const apply = () => {
    node.style.setProperty('-webkit-touch-callout', 'none');
    if (!opts.allowSelection) node.style.setProperty('user-select', 'none');
  };
  apply();

  function down(e: PointerEvent) {
    if (!current || e.pointerType === 'mouse' || !e.isPrimary) return;
    if (opts.ignore && (e.target as Element).closest(opts.ignore)) return;
    pointerId = e.pointerId;
    tracker.start(e.clientX, e.clientY);
  }
  function move(e: PointerEvent) {
    if (e.pointerId === pointerId) tracker.move(e.clientX, e.clientY);
  }
  function end(e: PointerEvent) {
    if (e.pointerId !== pointerId) return;
    tracker.cancel();
    if (tracker.fired) setTimeout(() => (tracker.fired = false), 400);
  }
  function click(e: MouseEvent) {
    if (!tracker.fired) return;
    tracker.fired = false;
    e.preventDefault();
    e.stopImmediatePropagation();
  }
  function contextmenu(e: Event) {
    if (!tracker.fired && !tracker.pending) return;
    e.preventDefault();
    e.stopImmediatePropagation();
    tracker.flush();
  }
  const cancel = () => tracker.cancel();

  node.addEventListener('pointerdown', down);
  node.addEventListener('pointermove', move);
  node.addEventListener('pointerup', end);
  node.addEventListener('pointercancel', end);
  node.addEventListener('click', click, true);
  node.addEventListener('contextmenu', contextmenu, true);
  window.addEventListener('scroll', cancel, true);

  return {
    update(next: Param) {
      opts = split(next);
      current = opts.on;
      apply();
    },
    destroy() {
      tracker.cancel();
      node.removeEventListener('pointerdown', down);
      node.removeEventListener('pointermove', move);
      node.removeEventListener('pointerup', end);
      node.removeEventListener('pointercancel', end);
      node.removeEventListener('click', click, true);
      node.removeEventListener('contextmenu', contextmenu, true);
      window.removeEventListener('scroll', cancel, true);
    }
  };
}

export const finePointer: Readable<boolean> = readable(true, (set) => {
  if (typeof window === 'undefined') return;
  const query = window.matchMedia('(pointer: fine)');
  set(query.matches);
  const update = () => set(query.matches);
  query.addEventListener('change', update);
  return () => query.removeEventListener('change', update);
});
