export interface VirtualElementLike {
  getBoundingClientRect: () => DOMRect | DOMRectReadOnly;
}

export type Placement =
  | 'top'
  | 'top-start'
  | 'top-end'
  | 'bottom'
  | 'bottom-start'
  | 'bottom-end'
  | 'left'
  | 'left-start'
  | 'left-end'
  | 'right'
  | 'right-start'
  | 'right-end';

export interface PositioningOptions {
  placement?: Placement;
  offset?: number;
  padding?: number;
  flip?: boolean;
  shift?: boolean;
}

export interface PositionResult {
  x: number;
  y: number;
  placement: Placement;
}

function getRect(reference: Element | VirtualElementLike): DOMRect | DOMRectReadOnly {
  if (reference instanceof Element) return reference.getBoundingClientRect();
  return reference.getBoundingClientRect();
}

function getOppositePlacement(placement: Placement): Placement {
  if (placement.startsWith('top')) return placement.replace('top', 'bottom') as Placement;
  if (placement.startsWith('bottom')) return placement.replace('bottom', 'top') as Placement;
  if (placement.startsWith('left')) return placement.replace('left', 'right') as Placement;
  return placement.replace('right', 'left') as Placement;
}

function computeCoords(
  referenceRect: DOMRect | DOMRectReadOnly,
  floatingRect: Pick<DOMRect | DOMRectReadOnly, 'width' | 'height'>,
  placement: Placement,
  gap: number,
): { x: number; y: number } {
  const [side, align] = placement.split('-') as [string, string | undefined];
  let x = referenceRect.left;
  let y = referenceRect.top;

  switch (side) {
    case 'top':
      y = referenceRect.top - floatingRect.height - gap;
      break;
    case 'bottom':
      y = referenceRect.bottom + gap;
      break;
    case 'left':
      x = referenceRect.left - floatingRect.width - gap;
      break;
    case 'right':
      x = referenceRect.right + gap;
      break;
  }

  if (side === 'top' || side === 'bottom') {
    if (align === 'start') x = referenceRect.left;
    else if (align === 'end') x = referenceRect.right - floatingRect.width;
    else x = referenceRect.left + referenceRect.width / 2 - floatingRect.width / 2;
  } else {
    if (align === 'start') y = referenceRect.top;
    else if (align === 'end') y = referenceRect.bottom - floatingRect.height;
    else y = referenceRect.top + referenceRect.height / 2 - floatingRect.height / 2;
  }

  return { x, y };
}

function overflowAmount(
  coords: { x: number; y: number },
  floatingRect: Pick<DOMRect | DOMRectReadOnly, 'width' | 'height'>,
  padding: number,
): number {
  const left = Math.max(0, padding - coords.x);
  const top = Math.max(0, padding - coords.y);
  const right = Math.max(0, coords.x + floatingRect.width - (window.innerWidth - padding));
  const bottom = Math.max(0, coords.y + floatingRect.height - (window.innerHeight - padding));
  return left + top + right + bottom;
}

function shiftIntoViewport(
  coords: { x: number; y: number },
  floatingRect: Pick<DOMRect | DOMRectReadOnly, 'width' | 'height'>,
  padding: number,
): { x: number; y: number } {
  const minX = padding;
  const maxX = Math.max(padding, window.innerWidth - padding - floatingRect.width);
  const minY = padding;
  const maxY = Math.max(padding, window.innerHeight - padding - floatingRect.height);
  return {
    x: Math.min(maxX, Math.max(minX, coords.x)),
    y: Math.min(maxY, Math.max(minY, coords.y)),
  };
}

export async function computeAnchoredPosition(
  reference: Element | VirtualElementLike,
  floating: HTMLElement,
  options: PositioningOptions = {},
): Promise<PositionResult> {
  const placement = options.placement ?? 'bottom';
  const gap = options.offset ?? 8;
  const padding = options.padding ?? 8;
  const doFlip = options.flip !== false;
  const doShift = options.shift !== false;

  const referenceRect = getRect(reference);
  const floatingRect = floating.getBoundingClientRect();

  let bestPlacement = placement;
  let coords = computeCoords(referenceRect, floatingRect, placement, gap);
  let bestOverflow = overflowAmount(coords, floatingRect, padding);

  if (doFlip) {
    const opposite = getOppositePlacement(placement);
    const flippedCoords = computeCoords(referenceRect, floatingRect, opposite, gap);
    const flippedOverflow = overflowAmount(flippedCoords, floatingRect, padding);
    if (flippedOverflow < bestOverflow) {
      bestOverflow = flippedOverflow;
      bestPlacement = opposite;
      coords = flippedCoords;
    }
  }

  if (doShift || bestOverflow > 0) {
    coords = shiftIntoViewport(coords, floatingRect, padding);
  }

  return { x: coords.x, y: coords.y, placement: bestPlacement };
}

export function autoUpdateAnchoredPosition(
  update: () => void,
): () => void {
  let raf = 0;
  const schedule = () => {
    if (raf) return;
    raf = window.requestAnimationFrame(() => {
      raf = 0;
      update();
    });
  };

  window.addEventListener('resize', schedule);
  window.addEventListener('scroll', schedule, true);

  return () => {
    if (raf) window.cancelAnimationFrame(raf);
    window.removeEventListener('resize', schedule);
    window.removeEventListener('scroll', schedule, true);
  };
}
