export type ViewportObservationReason =
  | "initial"
  | "window.resize"
  | "window.scroll"
  | "orientationchange"
  | "focusin"
  | "focusout"
  | "visibilitychange"
  | "visualViewport.resize"
  | "visualViewport.scroll"
  | "virtualKeyboard.geometrychange";

export interface BrowserViewportMetrics {
  layoutHeight: number;
  layoutWidth: number;
  documentClientHeight: number;
  scrollY: number;
  visualViewportSupported: boolean;
  visualHeight: number;
  visualWidth: number;
  visualOffsetTop: number;
  visualOffsetLeft: number;
  visualScale: number;
  virtualKeyboardSupported: boolean;
  virtualKeyboardOverlaysContent: boolean | null;
  virtualKeyboardRect: {
    x: number;
    y: number;
    width: number;
    height: number;
  } | null;
}

export interface ViewportObservation {
  reason: ViewportObservationReason;
  metrics: BrowserViewportMetrics;
}

interface VirtualKeyboardLike extends EventTarget {
  readonly overlaysContent?: boolean;
  readonly boundingRect?: Pick<
    DOMRectReadOnly,
    "height" | "width" | "x" | "y"
  >;
}

interface NavigatorWithVirtualKeyboard extends Navigator {
  readonly virtualKeyboard?: VirtualKeyboardLike;
}

type ViewportObserver = (observation: ViewportObservation) => void;

const observers = new Set<ViewportObserver>();
let removeBrowserListeners: (() => void) | null = null;

export function observeBrowserViewport(
  observer: ViewportObserver,
  options: { emitInitial?: boolean } = {},
): () => void {
  if (typeof window === "undefined") return () => undefined;

  observers.add(observer);
  ensureBrowserListeners();
  if (options.emitInitial !== false) {
    observer({
      reason: "initial",
      metrics: readBrowserViewportMetrics(),
    });
  }

  return () => {
    observers.delete(observer);
    if (observers.size === 0) {
      removeBrowserListeners?.();
      removeBrowserListeners = null;
    }
  };
}

export function readBrowserViewportMetrics(): BrowserViewportMetrics {
  const visualViewport = window.visualViewport;
  const virtualKeyboard = getVirtualKeyboard();
  const rect = virtualKeyboard?.boundingRect;

  return {
    layoutHeight: window.innerHeight,
    layoutWidth: window.innerWidth,
    documentClientHeight: document.documentElement.clientHeight,
    scrollY: window.scrollY,
    visualViewportSupported: visualViewport != null,
    visualHeight: visualViewport?.height ?? window.innerHeight,
    visualWidth: visualViewport?.width ?? window.innerWidth,
    visualOffsetTop: visualViewport?.offsetTop ?? 0,
    visualOffsetLeft: visualViewport?.offsetLeft ?? 0,
    visualScale: visualViewport?.scale ?? 1,
    virtualKeyboardSupported: virtualKeyboard != null,
    virtualKeyboardOverlaysContent:
      virtualKeyboard?.overlaysContent ?? null,
    virtualKeyboardRect: rect
      ? {
          x: rect.x ?? 0,
          y: rect.y ?? 0,
          width: rect.width ?? 0,
          height: rect.height ?? 0,
        }
      : null,
  };
}

export function isViewportGeometryObservation(
  reason: ViewportObservationReason,
): boolean {
  return (
    reason === "initial" ||
    reason === "window.resize" ||
    reason === "orientationchange" ||
    reason === "visualViewport.resize" ||
    reason === "visualViewport.scroll" ||
    reason === "virtualKeyboard.geometrychange"
  );
}

function ensureBrowserListeners(): void {
  if (removeBrowserListeners) return;

  const removers: Array<() => void> = [];
  const listen = (
    target: EventTarget | null | undefined,
    eventName: string,
    reason: ViewportObservationReason,
  ) => {
    if (!target) return;
    const handler = () => notifyObservers(reason);
    target.addEventListener(eventName, handler);
    removers.push(() => target.removeEventListener(eventName, handler));
  };

  listen(window, "resize", "window.resize");
  listen(window, "scroll", "window.scroll");
  listen(window, "orientationchange", "orientationchange");
  listen(document, "focusin", "focusin");
  listen(document, "focusout", "focusout");
  listen(document, "visibilitychange", "visibilitychange");
  listen(window.visualViewport, "resize", "visualViewport.resize");
  listen(window.visualViewport, "scroll", "visualViewport.scroll");
  listen(
    getVirtualKeyboard(),
    "geometrychange",
    "virtualKeyboard.geometrychange",
  );

  removeBrowserListeners = () => {
    removers.forEach((remove) => remove());
  };
}

function notifyObservers(reason: ViewportObservationReason): void {
  const observation = {
    reason,
    metrics: readBrowserViewportMetrics(),
  };
  observers.forEach((observer) => observer(observation));
}

function getVirtualKeyboard(): VirtualKeyboardLike | null {
  return (
    (navigator as NavigatorWithVirtualKeyboard).virtualKeyboard ?? null
  );
}
