<script lang="ts">
import type { Direction } from '../../utilities/config-provider';
import type { PrimitiveProps } from '../../internal/primitive';
import type { Orientation } from '../../utilities/roving-focus';

/**
 * A collection of navigation links and disclosure menus, typically used for the
 * primary site header. `NavigationMenuRoot` owns the open state and hover/click
 * timing for the whole menu, rendering as a `<nav>` landmark and providing context
 * to every list, item, trigger, content, viewport, and indicator beneath it. Reach
 * for it over a generic dropdown when you need keyboard-accessible, animatable
 * mega-menu panels that share a single active state.
 */
export interface NavigationMenuRootProps extends PrimitiveProps {
  /** Uncontrolled initial value. */
  defaultValue?: string;
  /** Reading direction. Falls back to `ConfigProvider`. */
  dir?: Direction;
  /** Menu orientation. @default 'horizontal' */
  orientation?: Orientation;
  /**
   * Time (ms) between pointer entering a trigger and the menu opening.
   * @default 200
   */
  delayDuration?: number;
  /**
   * Window (ms) during which switching triggers skips `delayDuration`.
   * @default 300
   */
  skipDelayDuration?: number;
  /** Disable opening via click. @default false */
  disableClickTrigger?: boolean;
  /** Disable opening via hover. @default false */
  disableHoverTrigger?: boolean;
  /** Disable closing when pointer leaves the menu. @default false */
  disablePointerLeaveClose?: boolean;
  /** Unmount content when hidden. @default true */
  unmountOnHide?: boolean;
}

export interface NavigationMenuRootEmits {
  'update:modelValue': [value: string];
}
</script>

<script setup lang="ts">
import type { Ref } from 'vue';

import { computed, onScopeDispose, onWatcherCleanup, ref, shallowRef, toRef, watchEffect } from 'vue';

import { useForwardExpose, useId } from '@robonen/vue';
import { useCollectionProvider } from '../../utilities/collection';
import { useConfig } from '../../utilities/config-provider';
import { Primitive } from '../../internal/primitive';
import { provideNavigationMenuContext } from './context';
import { EVENT_ROOT_CONTENT_DISMISS, NAVIGATION_MENU_COLLECTION_KEY } from './utils';

defineOptions({ inheritAttrs: false });

const {
  defaultValue,
  dir,
  orientation = 'horizontal',
  delayDuration = 200,
  skipDelayDuration = 300,
  disableClickTrigger = false,
  disableHoverTrigger = false,
  disablePointerLeaveClose = false,
  unmountOnHide = true,
  as = 'nav',
} = defineProps<NavigationMenuRootProps>();

defineEmits<NavigationMenuRootEmits>();

defineSlots<{
  default?: (props: { modelValue: string }) => unknown;
}>();

const config = useConfig();
const dirRef = computed<Direction>(() => dir ?? config.dir.value);

const localValue = ref<string>(defaultValue ?? '');
/** Controlled active item value. Use `v-model`. */
const modelValue = defineModel<string | undefined>({
  default: undefined,
  get: v => v ?? localValue.value,
  set: (v) => {
    const next = v ?? '';
    localValue.value = next;
    return next;
  },
}) as unknown as Ref<string>;

const previousValue = ref<string>('');

const baseId = useId(undefined, 'primitives-navigation-menu');
const { forwardRef, currentElement: rootNavigationMenu } = useForwardExpose();

const indicatorTrack = shallowRef<HTMLElement | undefined>(undefined);
const viewport = shallowRef<HTMLElement | undefined>(undefined);
const activeTrigger = shallowRef<HTMLElement | undefined>(undefined);

const { getItems, CollectionSlot } = useCollectionProvider<{ value: string }>(NAVIGATION_MENU_COLLECTION_KEY);

// Manual debounce — open delay shrinks to 150ms once the menu is open or while
// the skip window is active (so moving between triggers feels instantaneous).
let debounceTimer: ReturnType<typeof setTimeout> | undefined;
let skipDelayTimer: ReturnType<typeof setTimeout> | undefined;
const isDelaySkipped = ref(false);

function clearDebounce() {
  if (debounceTimer !== undefined) {
    clearTimeout(debounceTimer);
    debounceTimer = undefined;
  }
}

function clearSkipDelay() {
  if (skipDelayTimer !== undefined) {
    clearTimeout(skipDelayTimer);
    skipDelayTimer = undefined;
  }
}

function triggerSkipDelay() {
  clearSkipDelay();
  isDelaySkipped.value = true;
  skipDelayTimer = setTimeout(() => {
    isDelaySkipped.value = false;
    skipDelayTimer = undefined;
  }, skipDelayDuration);
}

const computedDelay = computed(() => {
  const isOpen = modelValue.value !== '';
  if (isOpen || isDelaySkipped.value) return 150;
  return delayDuration;
});

function debouncedSet(val: string) {
  clearDebounce();
  debounceTimer = setTimeout(() => {
    previousValue.value = modelValue.value;
    modelValue.value = val;
    debounceTimer = undefined;
  }, computedDelay.value);
}

function cancelDebounce() {
  clearDebounce();
}

watchEffect(() => {
  if (!modelValue.value) return;
  const items = getItems().map(i => i.ref);
  // Trigger id pattern: `${baseId}-trigger-${value}`
  const matched = items.find(item => item.id.includes(`-trigger-${modelValue.value}`));
  if (matched) activeTrigger.value = matched;
});

function onItemDismiss() {
  previousValue.value = modelValue.value;
  modelValue.value = '';
}

// Custom event isn't part of HTMLElementEventMap so wire it up manually.
watchEffect(() => {
  const el = rootNavigationMenu.value;
  if (!el) return;
  el.addEventListener(EVENT_ROOT_CONTENT_DISMISS, onItemDismiss);
  onWatcherCleanup(() => el.removeEventListener(EVENT_ROOT_CONTENT_DISMISS, onItemDismiss));
});

onScopeDispose(() => {
  clearDebounce();
  clearSkipDelay();
});

provideNavigationMenuContext({
  isRootMenu: true,
  modelValue,
  previousValue,
  baseId,
  dir: dirRef,
  orientation,
  disableClickTrigger: toRef(() => disableClickTrigger),
  disableHoverTrigger: toRef(() => disableHoverTrigger),
  disablePointerLeaveClose: toRef(() => disablePointerLeaveClose),
  unmountOnHide: toRef(() => unmountOnHide),
  rootNavigationMenu,
  activeTrigger,
  onActiveTriggerChange: (el) => { activeTrigger.value = el; },
  indicatorTrack,
  onIndicatorTrackChange: (el) => { indicatorTrack.value = el; },
  viewport,
  onViewportChange: (el) => { viewport.value = el; },
  onTriggerEnter: (val) => {
    debouncedSet(val);
  },
  onTriggerLeave: () => {
    triggerSkipDelay();
    debouncedSet('');
  },
  onContentEnter: () => {
    cancelDebounce();
  },
  onContentLeave: () => {
    if (!disablePointerLeaveClose) debouncedSet('');
  },
  onItemSelect: (val) => {
    previousValue.value = modelValue.value;
    modelValue.value = val;
  },
  onItemDismiss,
});

</script>

<template>
  <CollectionSlot>
    <Primitive
      :ref="forwardRef"
      :as="as"
      :aria-label="($attrs['aria-label'] ?? 'Main') as string"
      :data-orientation="orientation"
      :dir="dirRef"
      data-primitives-navigation-menu
      v-bind="$attrs"
    >
      <slot :model-value="modelValue" />
    </Primitive>
  </CollectionSlot>
</template>
