import { shallowRef, watch } from 'vue';
import type { ShallowRef } from 'vue';
import { defaultWindow } from '@/types';
import type { ConfigurableWindow } from '@/types';
import { useEventListener } from '@/composables/browser/useEventListener';
import { useMediaQuery } from '@/composables/browser/useMediaQuery';
import { tryOnMounted } from '@/composables/lifecycle/tryOnMounted';

/**
 * Which window dimensions to track.
 *
 * - `'inner'` — `window.innerWidth/innerHeight` (or `documentElement.clientWidth/clientHeight`
 *   when `includeScrollbar` is `false`). The viewport size.
 * - `'outer'` — `window.outerWidth/outerHeight`. The whole browser window, including chrome.
 * - `'visual'` — `window.visualViewport` size, accounting for pinch-zoom scale. Useful on
 *   mobile where the visual viewport differs from the layout viewport.
 */
export type WindowSizeType = 'inner' | 'outer' | 'visual';

export interface UseWindowSizeOptions extends ConfigurableWindow {
  /**
   * The initial width, used before the window is available (e.g. during SSR).
   *
   * @default Number.POSITIVE_INFINITY
   */
  initialWidth?: number;

  /**
   * The initial height, used before the window is available (e.g. during SSR).
   *
   * @default Number.POSITIVE_INFINITY
   */
  initialHeight?: number;

  /**
   * Listen to orientation changes via a `(orientation: portrait)` media query.
   *
   * @default true
   */
  listenOrientation?: boolean;

  /**
   * Use `window.innerWidth/innerHeight` (includes scrollbar) instead of
   * `documentElement.clientWidth/clientHeight`. Only affects the `'inner'` type.
   *
   * @default true
   */
  includeScrollbar?: boolean;

  /**
   * Which window dimensions to track.
   *
   * @default 'inner'
   */
  type?: WindowSizeType;
}

export interface UseWindowSizeReturn {
  width: ShallowRef<number>;
  height: ShallowRef<number>;
}

/**
 * @name useWindowSize
 * @category Elements
 * @description Reactive window size. Tracks the inner viewport, the outer window, or the
 * visual viewport (pinch-zoom aware), and reacts to resize and orientation changes.
 *
 * @param {UseWindowSizeOptions} [options={}] Options
 * @returns {UseWindowSizeReturn} Reactive `width` and `height`
 *
 * @example
 * const { width, height } = useWindowSize();
 *
 * @example
 * // Track the pinch-zoom aware visual viewport on mobile
 * const { width, height } = useWindowSize({ type: 'visual' });
 *
 * @since 0.0.14
 */
export function useWindowSize(options: UseWindowSizeOptions = {}): UseWindowSizeReturn {
  const {
    window = defaultWindow,
    initialWidth = Number.POSITIVE_INFINITY,
    initialHeight = Number.POSITIVE_INFINITY,
    listenOrientation = true,
    includeScrollbar = true,
    type = 'inner',
  } = options;

  const width = shallowRef(initialWidth);
  const height = shallowRef(initialHeight);

  const update = (): void => {
    if (!window)
      return;

    if (type === 'outer') {
      width.value = window.outerWidth;
      height.value = window.outerHeight;
    }
    else if (type === 'visual' && window.visualViewport) {
      const { width: visualWidth, height: visualHeight, scale } = window.visualViewport;
      width.value = Math.round(visualWidth * scale);
      height.value = Math.round(visualHeight * scale);
    }
    else if (includeScrollbar) {
      width.value = window.innerWidth;
      height.value = window.innerHeight;
    }
    else {
      width.value = window.document.documentElement.clientWidth;
      height.value = window.document.documentElement.clientHeight;
    }
  };

  update();
  tryOnMounted(update);

  const listenerOptions = { passive: true } as const;

  useEventListener('resize', update, listenerOptions);

  // Reactive getter target: auto-binds when `visualViewport` becomes available and
  // is a no-op otherwise (SSR / unsupported), without recreating listeners.
  if (type === 'visual')
    useEventListener(() => window?.visualViewport, 'resize', update, listenerOptions);

  if (listenOrientation) {
    const orientation = useMediaQuery('(orientation: portrait)', { window });
    watch(orientation, update);
  }

  return { width, height };
}
