const APP_VIEWPORT_HEIGHT_VAR = '--app-viewport-height';
const APP_VIEWPORT_WIDTH_VAR = '--app-viewport-width';
const APP_VISUAL_VIEWPORT_HEIGHT_VAR = '--app-visual-viewport-height';
const APP_VISUAL_VIEWPORT_WIDTH_VAR = '--app-visual-viewport-width';
const APP_KEYBOARD_OPEN_CLASSNAME = 'app-virtual-keyboard-open';
const APP_STANDALONE_MODE_CLASSNAME = 'app-standalone-mode';
const KEYBOARD_OPEN_HEIGHT_THRESHOLD_PX = 120;
const VIEWPORT_RECOVERY_INTERVAL_MS = 120;
const VIEWPORT_RECOVERY_MAX_DURATION_MS = 1800;
const VIEWPORT_RECOVERY_STABLE_TICKS = 3;
let viewportRecoveryAnimationFrameId = 0;
const viewportRecoveryTimeoutIds = new Set<number>();

function roundViewportSize(value: number) {
  return Math.round(value * 100) / 100;
}

function getLayoutViewportSize() {
  if (typeof window === 'undefined') {
    return {
      height: 0,
      width: 0,
    };
  }

  const documentElement = document.documentElement;
  const fallbackHeight = documentElement?.clientHeight ?? 0;
  const fallbackWidth = documentElement?.clientWidth ?? 0;

  return {
    height: roundViewportSize(Math.max(window.innerHeight || 0, fallbackHeight)),
    width: roundViewportSize(Math.max(window.innerWidth || 0, fallbackWidth)),
  };
}

function getVisualViewportSize() {
  if (typeof window === 'undefined') {
    return {
      height: 0,
      width: 0,
    };
  }

  const viewport = window.visualViewport;

  return {
    height: roundViewportSize(viewport?.height ?? window.innerHeight ?? 0),
    width: roundViewportSize(viewport?.width ?? window.innerWidth ?? 0),
  };
}

function isStandaloneDisplayMode() {
  if (typeof window === 'undefined') {
    return false;
  }

  return (
    window.matchMedia?.('(display-mode: standalone)').matches === true ||
    (window.navigator as Navigator & { standalone?: boolean }).standalone === true
  );
}

function resetDocumentViewportOffset() {
  if (typeof window === 'undefined' || typeof document === 'undefined') {
    return;
  }

  const documentElement = document.documentElement;
  const body = document.body;
  const viewport = window.visualViewport;
  const layoutScrollTop = Math.max(window.scrollY || 0, documentElement?.scrollTop || 0, body?.scrollTop || 0);
  const visualOffsetTop = Math.max(viewport?.offsetTop ?? 0, viewport?.pageTop ?? 0);

  if (layoutScrollTop <= 0 && visualOffsetTop <= 0) {
    return;
  }

  window.scrollTo(0, 0);

  if (documentElement) {
    documentElement.scrollTop = 0;
  }

  if (body) {
    body.scrollTop = 0;
  }
}

function getViewportRecoveryStateKey() {
  if (typeof window === 'undefined' || typeof document === 'undefined') {
    return 'ssr';
  }

  const documentElement = document.documentElement;
  const body = document.body;
  const viewport = window.visualViewport;
  const layoutViewport = getLayoutViewportSize();
  const visualViewport = getVisualViewportSize();

  return JSON.stringify({
    layoutHeight: layoutViewport.height,
    layoutWidth: layoutViewport.width,
    visualHeight: visualViewport.height,
    visualWidth: visualViewport.width,
    scrollY: roundViewportSize(Math.max(window.scrollY || 0, documentElement?.scrollTop || 0, body?.scrollTop || 0)),
    offsetTop: roundViewportSize(Math.max(viewport?.offsetTop ?? 0, viewport?.pageTop ?? 0)),
  });
}

export function applyViewportCssVars() {
  if (typeof document === 'undefined') {
    return;
  }

  const layoutViewport = getLayoutViewportSize();
  const rawVisualViewport = getVisualViewportSize();
  const keyboardInsetHeight = Math.max(0, layoutViewport.height - rawVisualViewport.height);
  const standaloneMode = isStandaloneDisplayMode();
  const visualViewport =
    standaloneMode && keyboardInsetHeight < KEYBOARD_OPEN_HEIGHT_THRESHOLD_PX ? layoutViewport : rawVisualViewport;

  document.documentElement.style.setProperty(APP_VIEWPORT_HEIGHT_VAR, `${layoutViewport.height}px`);
  document.documentElement.style.setProperty(APP_VIEWPORT_WIDTH_VAR, `${layoutViewport.width}px`);
  document.documentElement.style.setProperty(APP_VISUAL_VIEWPORT_HEIGHT_VAR, `${visualViewport.height}px`);
  document.documentElement.style.setProperty(APP_VISUAL_VIEWPORT_WIDTH_VAR, `${visualViewport.width}px`);
  document.documentElement.classList.toggle(APP_KEYBOARD_OPEN_CLASSNAME, keyboardInsetHeight >= KEYBOARD_OPEN_HEIGHT_THRESHOLD_PX);
  document.documentElement.classList.toggle(APP_STANDALONE_MODE_CLASSNAME, standaloneMode);
}

function cancelViewportRecoverySyncs() {
  if (typeof window === 'undefined') {
    return;
  }

  window.cancelAnimationFrame(viewportRecoveryAnimationFrameId);
  viewportRecoveryAnimationFrameId = 0;
  viewportRecoveryTimeoutIds.forEach((timeoutId) => {
    window.clearTimeout(timeoutId);
  });
  viewportRecoveryTimeoutIds.clear();
}

function scheduleViewportCssVarsSync(options?: { resetScroll?: boolean }) {
  if (typeof window === 'undefined') {
    return;
  }

  window.cancelAnimationFrame(viewportRecoveryAnimationFrameId);
  viewportRecoveryAnimationFrameId = window.requestAnimationFrame(() => {
    if (options?.resetScroll) {
      resetDocumentViewportOffset();
    }
    applyViewportCssVars();
  });
}

export function scheduleViewportRecoverySync() {
  if (typeof window === 'undefined') {
    return;
  }

  cancelViewportRecoverySyncs();
  scheduleViewportCssVarsSync({
    resetScroll: true,
  });
  const startedAt = Date.now();
  let previousState = getViewportRecoveryStateKey();
  let stableTicks = 0;

  const continueRecoverySync = () => {
    const timeoutId = window.setTimeout(() => {
      viewportRecoveryTimeoutIds.delete(timeoutId);
      scheduleViewportCssVarsSync({
        resetScroll: true,
      });

      const nextState = getViewportRecoveryStateKey();
      stableTicks = nextState === previousState ? stableTicks + 1 : 0;
      previousState = nextState;

      if (stableTicks >= VIEWPORT_RECOVERY_STABLE_TICKS || Date.now() - startedAt >= VIEWPORT_RECOVERY_MAX_DURATION_MS) {
        return;
      }

      continueRecoverySync();
    }, VIEWPORT_RECOVERY_INTERVAL_MS);

    viewportRecoveryTimeoutIds.add(timeoutId);
  };

  continueRecoverySync();
}

export function bindViewportCssVars() {
  if (typeof window === 'undefined') {
    return () => undefined;
  }

  const viewport = window.visualViewport;

  const handleWindowResize = () => {
    scheduleViewportCssVarsSync();
  };

  const handleViewportResize = () => {
    scheduleViewportCssVarsSync();
  };

  const handleViewportScroll = () => {
    scheduleViewportCssVarsSync();
  };

  const handleVisibilityChange = () => {
    if (document.visibilityState === 'visible') {
      scheduleViewportRecoverySync();
    }
  };

  scheduleViewportRecoverySync();
  window.addEventListener('resize', handleWindowResize);
  window.addEventListener('orientationchange', scheduleViewportRecoverySync);
  window.addEventListener('focus', scheduleViewportRecoverySync);
  window.addEventListener('pageshow', scheduleViewportRecoverySync);
  viewport?.addEventListener('resize', handleViewportResize);
  viewport?.addEventListener('scroll', handleViewportScroll);
  document.addEventListener('visibilitychange', handleVisibilityChange);

  return () => {
    cancelViewportRecoverySyncs();
    window.removeEventListener('resize', handleWindowResize);
    window.removeEventListener('orientationchange', scheduleViewportRecoverySync);
    window.removeEventListener('focus', scheduleViewportRecoverySync);
    window.removeEventListener('pageshow', scheduleViewportRecoverySync);
    viewport?.removeEventListener('resize', handleViewportResize);
    viewport?.removeEventListener('scroll', handleViewportScroll);
    document.removeEventListener('visibilitychange', handleVisibilityChange);
  };
}
