<script setup lang="ts">
import type { SidebarProps } from '.';
import { defaultWindow } from '@vueuse/core';
import { computed, onBeforeUnmount, ref, watch } from 'vue';
import { cn } from '@/lib/utils';
import { Sheet, SheetContent } from '@/components/core/sheet';
import SheetDescription from '@/components/core/sheet/SheetDescription.vue';
import SheetHeader from '@/components/core/sheet/SheetHeader.vue';
import SheetTitle from '@/components/core/sheet/SheetTitle.vue';
import SidebarResizeHandle from './SidebarResizeHandle.vue';
import {
  SIDEBAR_WIDTH_DEFAULT,
  SIDEBAR_WIDTH_MAX,
  SIDEBAR_WIDTH_MIN,
  SIDEBAR_WIDTH_MOBILE,
  SIDEBAR_WIDTH_STORAGE_KEY,
  useSidebar,
} from './utils';

defineOptions({
  inheritAttrs: false,
});

const props = withDefaults(defineProps<SidebarProps>(), {
  side: 'left',
  variant: 'sidebar',
  collapsible: 'offcanvas',
  resizable: false,
  defaultWidth: SIDEBAR_WIDTH_DEFAULT,
  minWidth: SIDEBAR_WIDTH_MIN,
  maxWidth: SIDEBAR_WIDTH_MAX,
  widthStorageKey: SIDEBAR_WIDTH_STORAGE_KEY,
  resizeAriaLabel: 'Resize sidebar',
  keyboardResizeBy: 10,
});

const { isMobile, state, openMobile, setOpenMobile } = useSidebar();

const resolvedResizable = computed(
  () => props.resizeOptions !== undefined || props.resizable,
);
const minWidth = computed(() =>
  Math.min(
    props.resizeOptions?.minWidth ?? props.minWidth,
    props.resizeOptions?.maxWidth ?? props.maxWidth,
  ),
);
const maxWidth = computed(() =>
  Math.max(
    props.resizeOptions?.minWidth ?? props.minWidth,
    props.resizeOptions?.maxWidth ?? props.maxWidth,
  ),
);
const resolvedDefaultWidth = computed(
  () => props.resizeOptions?.defaultWidth ?? props.defaultWidth,
);
const resolvedWidthStorageKey = computed(
  () => props.resizeOptions?.widthStorageKey ?? props.widthStorageKey,
);
const resolvedResizeAriaLabel = computed(
  () => props.resizeOptions?.resizeAriaLabel ?? props.resizeAriaLabel,
);
const resolvedKeyboardResizeBy = computed(
  () => props.resizeOptions?.keyboardResizeBy ?? props.keyboardResizeBy,
);
const sidebarWidth = ref(getInitialSidebarWidth());
const isResizing = ref(false);
let resizeStartX = 0;
let resizeStartWidth = 0;

const canResize = computed(
  () =>
    resolvedResizable.value &&
    !isMobile.value &&
    (props.collapsible === 'none' || state.value === 'expanded'),
);
const sidebarStyle = computed(() => ({
  ...(resolvedResizable.value
    ? {
        '--sidebar-width': `${sidebarWidth.value}px`,
      }
    : {}),
}));

function clampWidth(width: number) {
  return Math.min(Math.max(width, minWidth.value), maxWidth.value);
}

function getStoredSidebarWidth() {
  if (!resolvedWidthStorageKey.value || !defaultWindow?.localStorage)
    return null;

  let stored: string | null;
  try {
    stored = defaultWindow.localStorage.getItem(resolvedWidthStorageKey.value);
  } catch {
    return null;
  }

  if (!stored) return null;
  const parsed = Number.parseFloat(stored);
  return Number.isFinite(parsed) ? parsed : null;
}

function getInitialSidebarWidth() {
  return clampWidth(getStoredSidebarWidth() ?? resolvedDefaultWidth.value);
}

function persistSidebarWidth(width: number) {
  if (!resolvedWidthStorageKey.value || !defaultWindow?.localStorage) return;

  try {
    defaultWindow.localStorage.setItem(
      resolvedWidthStorageKey.value,
      String(width),
    );
  } catch {
    // Ignore unavailable storage; resizing should still work for the session.
  }
}

function setSidebarWidth(width: number, persist = true) {
  const nextWidth = clampWidth(width);
  sidebarWidth.value = nextWidth;

  if (persist) {
    persistSidebarWidth(nextWidth);
  }
}

function getNextWidth(clientX: number) {
  const delta = clientX - resizeStartX;
  return props.side === 'right'
    ? resizeStartWidth - delta
    : resizeStartWidth + delta;
}

function stopResize() {
  isResizing.value = false;
  defaultWindow?.removeEventListener('pointermove', handlePointerMove);
  defaultWindow?.removeEventListener('pointerup', stopResize);
  defaultWindow?.removeEventListener('pointercancel', stopResize);
}

function handlePointerMove(event: PointerEvent) {
  if (!isResizing.value) return;
  event.preventDefault();
  setSidebarWidth(getNextWidth(event.clientX));
}

function startResize(event: PointerEvent) {
  if (!canResize.value) return;

  event.preventDefault();
  isResizing.value = true;
  resizeStartX = event.clientX;
  resizeStartWidth = sidebarWidth.value;
  defaultWindow?.addEventListener('pointermove', handlePointerMove);
  defaultWindow?.addEventListener('pointerup', stopResize);
  defaultWindow?.addEventListener('pointercancel', stopResize);
}

function resizeBy(delta: number) {
  const directionalDelta = props.side === 'right' ? -delta : delta;
  setSidebarWidth(sidebarWidth.value + directionalDelta);
}

function handleResizeKeydown(event: KeyboardEvent) {
  if (!canResize.value) return;

  if (event.key === 'ArrowLeft') {
    event.preventDefault();
    resizeBy(-resolvedKeyboardResizeBy.value);
  } else if (event.key === 'ArrowRight') {
    event.preventDefault();
    resizeBy(resolvedKeyboardResizeBy.value);
  } else if (event.key === 'Home') {
    event.preventDefault();
    setSidebarWidth(minWidth.value);
  } else if (event.key === 'End') {
    event.preventDefault();
    setSidebarWidth(maxWidth.value);
  }
}

watch(
  () => [
    props.defaultWidth,
    props.minWidth,
    props.maxWidth,
    props.resizeOptions?.defaultWidth,
    props.resizeOptions?.minWidth,
    props.resizeOptions?.maxWidth,
  ],
  // Clamp config-driven changes without overwriting the user's stored width preference.
  () => setSidebarWidth(sidebarWidth.value, false),
);

onBeforeUnmount(stopResize);
</script>

<template>
  <div
    v-if="collapsible === 'none'"
    data-slot="sidebar"
    :data-resizable="resolvedResizable ? '' : undefined"
    :style="sidebarStyle"
    :class="
      cn(
        'bg-sidebar text-sidebar-foreground relative flex h-full w-(--sidebar-width) flex-col',
        $attrs.class ?? '',
      )
    "
    v-bind="$attrs"
  >
    <slot />
    <SidebarResizeHandle
      v-if="canResize"
      :accessible-label="resolvedResizeAriaLabel"
      :is-resizing="isResizing"
      :side="props.side"
      :value-min="minWidth"
      :value-max="maxWidth"
      :value-now="sidebarWidth"
      @keydown="handleResizeKeydown"
      @pointerdown="startResize"
    />
  </div>

  <Sheet
    v-else-if="isMobile"
    :open="openMobile"
    v-bind="$attrs"
    @update:open="setOpenMobile"
  >
    <SheetContent
      data-sidebar="sidebar"
      data-slot="sidebar"
      data-mobile="true"
      :side="side"
      class="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden"
      :style="{
        '--sidebar-width': SIDEBAR_WIDTH_MOBILE,
      }"
    >
      <SheetHeader class="sr-only">
        <SheetTitle>Sidebar</SheetTitle>
        <SheetDescription>Displays the mobile sidebar.</SheetDescription>
      </SheetHeader>
      <div class="flex h-full w-full flex-col">
        <slot />
      </div>
    </SheetContent>
  </Sheet>

  <div
    v-else
    class="group peer text-sidebar-foreground hidden md:block"
    data-slot="sidebar"
    :data-state="state"
    :data-collapsible="state === 'collapsed' ? collapsible : ''"
    :data-variant="variant"
    :data-side="side"
    :data-resizable="resolvedResizable ? '' : undefined"
    :style="sidebarStyle"
  >
    <!-- This is what handles the sidebar gap on desktop  -->
    <div
      :class="
        cn(
          'relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear',
          'group-data-[collapsible=offcanvas]:w-0',
          'group-data-[side=right]:rotate-180',
          variant === 'floating' || variant === 'inset'
            ? 'group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+1rem)]'
            : 'group-data-[collapsible=icon]:w-(--sidebar-width-icon)',
        )
      "
    />
    <div
      :class="
        cn(
          'fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex',
          isResizing && 'transition-none',
          side === 'left'
            ? 'left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]'
            : 'right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]',
          // Adjust the padding for floating and inset variants.
          variant === 'floating' || variant === 'inset'
            ? 'p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+1rem+2px)]'
            : 'group-data-[collapsible=icon]:w-(--sidebar-width-icon)',
          variant !== 'floating' &&
            variant !== 'inset' &&
            resolvedResizable &&
            'group-data-[side=left]:border-r group-data-[side=right]:border-l',
          $attrs.class ?? '',
        )
      "
      v-bind="$attrs"
    >
      <div
        data-sidebar="sidebar"
        class="bg-sidebar group-data-[variant=floating]:border-sidebar-border flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm"
      >
        <slot />
      </div>
      <SidebarResizeHandle
        v-if="canResize"
        :accessible-label="resolvedResizeAriaLabel"
        :is-resizing="isResizing"
        :side="props.side"
        :value-min="minWidth"
        :value-max="maxWidth"
        :value-now="sidebarWidth"
        @keydown="handleResizeKeydown"
        @pointerdown="startResize"
      />
    </div>
  </div>
</template>
