type NearState = 'hidden' | 'visible' | 'active';

type Item = {
  el: HTMLDivElement;
  rect: DOMRect;
  state: NearState;
};

const SHOW_DIST = 20;
const SHOW_DIST2 = SHOW_DIST * SHOW_DIST;

class NearManager {
  private items: Item[] = [];
  private dragging = false;
  private rafId: number | null = null;

  constructor() {
    window.addEventListener('pointermove', this.onMove);
    window.addEventListener('dragover', this.onMove);
    window.addEventListener('resize', this.updateRects);
    window.addEventListener('scroll', this.updateRects, true);
  }

  setDragging(v: boolean) {
    this.dragging = v;

    if (!v) {
      this.items.forEach((i) => this.setState(i, 'hidden'));
    }
  }

  register(el: HTMLDivElement) {
    this.items.push({
      el,
      rect: el.getBoundingClientRect(),
      state: 'hidden',
    });
  }

  unregister(el: HTMLDivElement) {
    this.items = this.items.filter((i) => i.el !== el);
  }

  private updateRects = () => {
    this.items.forEach(
      (i) => (i.rect = i.el.getBoundingClientRect())
    );
  };

  private onMove = (e: MouseEvent | PointerEvent | DragEvent) => {
    if (!this.dragging) return;

    const x = e.clientX;
    const y = e.clientY;
    if (x == null || y == null) return;

    if (this.rafId) return;

    this.rafId = requestAnimationFrame(() => {
      this.rafId = null;

      for (const item of this.items) {
        const elAtPoint = document.elementFromPoint(x, y);

        if (elAtPoint && item.el.contains(elAtPoint)) {
          this.setState(item, 'active');
          continue;
        }

        const d2 = distanceToRectSquared(x, y, item.rect);

        if (d2 <= SHOW_DIST2) {
          this.setState(item, 'visible');
        } else {
          this.setState(item, 'hidden');
        }
      }
    });
  };

  private setState(item: Item, next: NearState) {
    if (item.state === next) return;
    item.state = next;

    const el = item.el;
    el.classList.remove(
      'near-hidden',
      'near-visible',
      'near-active'
    );

    el.classList.add(`near-${next}`);
  }
}

export const nearManager = new NearManager();

function distanceToRectSquared(
  x: number,
  y: number,
  rect: DOMRect
) {
  const dx = Math.max(rect.left - x, 0, x - rect.right);
  const dy = Math.max(rect.top - y, 0, y - rect.bottom);
  return dx * dx + dy * dy;
}