<script lang="ts">
  import Icon from '$lib/components/elements/icon.svelte';
  import type { AssetStore, LiteBucket } from '$lib/stores/assets-store.svelte';
  import { mobileDevice } from '$lib/stores/mobile-device.svelte';
  import { fromLocalDateTime, type ScrubberListener } from '$lib/utils/timeline-util';
  import { mdiPlay } from '@mdi/js';
  import { clamp } from 'lodash-es';
  import { DateTime } from 'luxon';
  import { onMount } from 'svelte';
  import { fade, fly } from 'svelte/transition';

  interface Props {
    timelineTopOffset?: number;
    timelineBottomOffset?: number;
    height?: number;
    assetStore: AssetStore;
    scrubOverallPercent?: number;
    scrubBucketPercent?: number;
    scrubBucket?: { bucketDate: string | undefined };
    leadout?: boolean;
    onScrub?: ScrubberListener;
    onScrubKeyDown?: (event: KeyboardEvent, element: HTMLElement) => void;
    startScrub?: ScrubberListener;
    stopScrub?: ScrubberListener;
  }

  let {
    timelineTopOffset = 0,
    timelineBottomOffset = 0,
    height = 0,
    assetStore,
    scrubOverallPercent = 0,
    scrubBucketPercent = 0,
    scrubBucket = undefined,
    leadout = false,
    onScrub = undefined,
    onScrubKeyDown = undefined,
    startScrub = undefined,
    stopScrub = undefined,
  }: Props = $props();

  let isHover = $state(false);
  let isDragging = $state(false);
  let hoverY = $state(0);
  let clientY = 0;
  let windowHeight = $state(0);
  let scrollBar: HTMLElement | undefined = $state();

  const toScrollY = (percent: number) => percent * (height - HOVER_DATE_HEIGHT * 2);
  const toTimelineY = (scrollY: number) => scrollY / (height - HOVER_DATE_HEIGHT * 2);

  const HOVER_DATE_HEIGHT = 31.75;
  const MIN_YEAR_LABEL_DISTANCE = 16;
  const MIN_DOT_DISTANCE = 8;

  const toScrollFromBucketPercentage = (
    scrubBucket: { bucketDate: string | undefined } | undefined,
    scrubBucketPercent: number,
    scrubOverallPercent: number,
  ) => {
    if (scrubBucket) {
      let offset = relativeTopOffset;
      let match = false;
      for (const segment of segments) {
        if (segment.bucketDate === scrubBucket.bucketDate) {
          offset += scrubBucketPercent * segment.height;
          match = true;
          break;
        }
        offset += segment.height;
      }
      if (!match) {
        offset += scrubBucketPercent * relativeBottomOffset;
      }
      // 2px is the height of the indicator
      return offset - 2;
    } else if (leadout) {
      let offset = relativeTopOffset;
      for (const segment of segments) {
        offset += segment.height;
      }
      offset += scrubOverallPercent * relativeBottomOffset;
      return offset - 2;
    } else {
      // 2px is the height of the indicator
      return scrubOverallPercent * (height - HOVER_DATE_HEIGHT * 2) - 2;
    }
  };
  let scrollY = $derived(toScrollFromBucketPercentage(scrubBucket, scrubBucketPercent, scrubOverallPercent));
  let timelineFullHeight = $derived(assetStore.scrubberTimelineHeight + timelineTopOffset + timelineBottomOffset);
  let relativeTopOffset = $derived(toScrollY(timelineTopOffset / timelineFullHeight));
  let relativeBottomOffset = $derived(toScrollY(timelineBottomOffset / timelineFullHeight));

  type Segment = {
    count: number;
    height: number;
    dateFormatted: string;
    bucketDate: string;
    date: DateTime;
    hasLabel: boolean;
    hasDot: boolean;
  };

  const calculateSegments = (buckets: LiteBucket[]) => {
    let height = 0;
    let dotHeight = 0;

    let segments: Segment[] = [];
    let previousLabeledSegment: Segment | undefined;

    for (const [i, bucket] of buckets.entries()) {
      const scrollBarPercentage = bucket.bucketHeight / timelineFullHeight;

      const segment = {
        count: bucket.assetCount,
        height: toScrollY(scrollBarPercentage),
        bucketDate: bucket.bucketDate,
        date: fromLocalDateTime(bucket.bucketDate),
        dateFormatted: bucket.bucketDateFormattted,
        hasLabel: false,
        hasDot: false,
      };

      if (i === 0) {
        segment.hasDot = true;
        segment.hasLabel = true;
        previousLabeledSegment = segment;
      } else {
        if (previousLabeledSegment?.date?.year !== segment.date.year && height > MIN_YEAR_LABEL_DISTANCE) {
          height = 0;
          segment.hasLabel = true;
          previousLabeledSegment = segment;
        }
        if (i !== 1 && segment.height > 5 && dotHeight > MIN_DOT_DISTANCE) {
          segment.hasDot = true;
          dotHeight = 0;
        }

        height += segment.height;
        dotHeight += segment.height;
      }
      segments.push(segment);
    }

    return segments;
  };
  let activeSegment: HTMLElement | undefined = $state();
  const segments = $derived(calculateSegments(assetStore.scrubberBuckets));
  const hoverLabel = $derived(activeSegment?.dataset.label);
  const bucketDate = $derived(activeSegment?.dataset.timeSegmentBucketDate);
  const scrollHoverLabel = $derived.by(() => {
    const y = scrollY;
    let cur = 0;
    for (const segment of segments) {
      if (y <= cur + segment.height + relativeTopOffset) {
        return segment.dateFormatted;
      }
      cur += segment.height;
    }
    return '';
  });

  const handleMouseEvent = (event: { clientY: number; isDragging?: boolean }) => {
    const wasDragging = isDragging;

    isDragging = event.isDragging ?? isDragging;
    clientY = event.clientY;

    if (!scrollBar) {
      return;
    }

    const rect = scrollBar.getBoundingClientRect()!;
    const lower = 0;
    const upper = rect?.height - HOVER_DATE_HEIGHT * 2;
    hoverY = clamp(clientY - rect?.top - HOVER_DATE_HEIGHT, lower, upper);
    const x = rect!.left + rect!.width / 2;
    const elems = document.elementsFromPoint(x, clientY);
    const segment = elems.find(({ id }) => id === 'time-segment');
    let bucketPercentY = 0;
    if (segment) {
      activeSegment = segment as HTMLElement;

      const sr = segment.getBoundingClientRect();
      const sy = sr.y;
      const relativeY = clientY - sy;
      bucketPercentY = relativeY / sr.height;
    } else {
      const leadin = elems.find(({ id }) => id === 'lead-in');
      if (leadin) {
        activeSegment = leadin as HTMLElement;
      } else {
        activeSegment = undefined;
        bucketPercentY = 0;
      }
    }

    const scrollPercent = toTimelineY(hoverY);
    if (wasDragging === false && isDragging) {
      void startScrub?.(bucketDate, scrollPercent, bucketPercentY);
      void onScrub?.(bucketDate, scrollPercent, bucketPercentY);
    }

    if (wasDragging && !isDragging) {
      void stopScrub?.(bucketDate, scrollPercent, bucketPercentY);
      return;
    }

    if (!isDragging) {
      return;
    }

    void onScrub?.(bucketDate, scrollPercent, bucketPercentY);
  };
  const getTouch = (event: TouchEvent) => {
    if (event.touches.length === 1) {
      return event.touches[0];
    }
    return null;
  };
  const onTouchStart = (event: TouchEvent) => {
    const touch = getTouch(event);
    if (!touch) {
      isHover = false;
      return;
    }
    const elements = document.elementsFromPoint(touch.clientX, touch.clientY);
    const isHoverScrollbar = elements.some(({ id }) => {
      return id === 'immich-scrubbable-scrollbar' || id === 'time-label';
    });

    isHover = isHoverScrollbar;

    if (isHoverScrollbar) {
      handleMouseEvent({
        clientY: touch.clientY,
        isDragging: true,
      });
    }
  };
  const onTouchEnd = () => {
    if (isHover) {
      isHover = false;
    }
    handleMouseEvent({
      clientY,
      isDragging: false,
    });
  };
  const onTouchMove = (event: TouchEvent) => {
    const touch = getTouch(event);
    if (touch && isDragging) {
      handleMouseEvent({
        clientY: touch.clientY,
      });
      event.preventDefault();
    } else {
      isHover = false;
    }
  };
  onMount(() => {
    const opts = {
      passive: false,
    };
    globalThis.addEventListener('touchmove', onTouchMove, opts);
    return () => {
      globalThis.removeEventListener('touchmove', onTouchMove);
    };
  });
  const usingMobileDevice = $derived(mobileDevice.pointerCoarse);
</script>

<svelte:window
  bind:innerHeight={windowHeight}
  onmousemove={({ clientY }) => (isDragging || isHover) && handleMouseEvent({ clientY })}
  onmousedown={({ clientY }) => isHover && handleMouseEvent({ clientY, isDragging: true })}
  onmouseup={({ clientY }) => handleMouseEvent({ clientY, isDragging: false })}
  ontouchstart={onTouchStart}
  ontouchend={onTouchEnd}
  ontouchcancel={onTouchEnd}
/>

<div
  transition:fly={{ x: 50, duration: 250 }}
  tabindex="-1"
  role="scrollbar"
  aria-controls="time-label"
  aria-valuenow={scrollY + HOVER_DATE_HEIGHT}
  aria-valuemax={toScrollY(100)}
  aria-valuemin={toScrollY(0)}
  id="immich-scrubbable-scrollbar"
  class="absolute right-0 z-[1] select-none bg-immich-bg hover:cursor-row-resize"
  style:padding-top={HOVER_DATE_HEIGHT + 'px'}
  style:padding-bottom={HOVER_DATE_HEIGHT + 'px'}
  style:width={isDragging ? '100vw' : '60px'}
  style:height={height + 'px'}
  style:background-color={isDragging ? 'transparent' : 'transparent'}
  bind:this={scrollBar}
  onmouseenter={() => (isHover = true)}
  onmouseleave={() => (isHover = false)}
  onkeydown={(event) => onScrubKeyDown?.(event, event.currentTarget)}
  draggable="false"
>
  {#if !usingMobileDevice && hoverLabel && (isHover || isDragging)}
    <div
      id="time-label"
      class={[
        { 'border-b-2': isDragging },
        { 'rounded-bl-md': !isDragging },
        'truncate opacity-85 pointer-events-none absolute right-0 z-[100] min-w-20 max-w-64 w-fit rounded-tl-md  border-immich-primary bg-immich-bg py-1 px-1 text-sm font-medium shadow-[0_0_8px_rgba(0,0,0,0.25)] dark:border-immich-dark-primary dark:bg-immich-dark-gray dark:text-immich-dark-fg',
      ]}
      style:top="{hoverY + 2}px"
    >
      {hoverLabel}
    </div>
  {/if}
  {#if usingMobileDevice && ((assetStore.scrolling && scrollHoverLabel) || isHover || isDragging)}
    <div
      id="time-label"
      class="rounded-l-full w-[32px] pl-2 text-white bg-immich-primary dark:bg-gray-600 hover:cursor-pointer select-none"
      style:top="{scrollY + HOVER_DATE_HEIGHT - 25}px"
      style:height="50px"
      style:right="0"
      style:position="absolute"
      in:fade={{ duration: 200 }}
      out:fade={{ duration: 200 }}
    >
      <Icon path={mdiPlay} size="20" class="-rotate-90 relative top-[9px] -right-[2px]" />
      <Icon path={mdiPlay} size="20" class="rotate-90 relative top-[1px] -right-[2px]" />
      {#if (assetStore.scrolling && scrollHoverLabel) || isHover || isDragging}
        <p
          transition:fade={{ duration: 200 }}
          style:bottom={50 / 2 - 30 / 2 + 'px'}
          style:right="36px"
          style:width="fit-content"
          class="truncate pointer-events-none absolute text-sm rounded-full w-[32px] py-2 px-4 text-white bg-immich-primary/90 dark:bg-gray-500 hover:cursor-pointer select-none font-semibold"
        >
          {scrollHoverLabel}
        </p>
      {/if}
    </div>
  {/if}
  <!-- Scroll Position Indicator Line -->
  {#if !usingMobileDevice && !isDragging}
    <div
      class="absolute right-0 h-[2px] w-10 bg-immich-primary dark:bg-immich-dark-primary"
      style:top="{scrollY + HOVER_DATE_HEIGHT}px"
    >
      {#if assetStore.scrolling && scrollHoverLabel}
        <p
          transition:fade={{ duration: 200 }}
          class="truncate pointer-events-none absolute right-0 bottom-0 z-[100] min-w-20 max-w-64 w-fit rounded-tl-md border-b-2 border-immich-primary bg-immich-bg/80 py-1 px-1 text-sm font-medium shadow-[0_0_8px_rgba(0,0,0,0.25)] dark:border-immich-dark-primary dark:bg-immich-dark-gray/80 dark:text-immich-dark-fg"
        >
          {scrollHoverLabel}
        </p>
      {/if}
    </div>
  {/if}
  <div id="lead-in" class="relative" style:height={relativeTopOffset + 'px'} data-label={segments.at(0)?.dateFormatted}>
    {#if relativeTopOffset > 6}
      <div class="absolute right-[0.75rem] h-[4px] w-[4px] rounded-full bg-gray-300"></div>
    {/if}
  </div>
  <!-- Time Segment -->
  {#each segments as segment (segment.date)}
    <div
      id="time-segment"
      class="relative"
      data-time-segment-bucket-date={segment.date}
      data-label={segment.dateFormatted}
      style:height={segment.height + 'px'}
    >
      {#if !usingMobileDevice && segment.hasLabel}
        <div class="absolute right-[1.25rem] top-[-16px] z-10 text-[12px] dark:text-immich-dark-fg font-immich-mono">
          {segment.date.year}
        </div>
      {/if}
      {#if !usingMobileDevice && segment.hasDot}
        <div class="absolute right-[0.75rem] bottom-0 h-[4px] w-[4px] rounded-full bg-gray-300"></div>
      {/if}
    </div>
  {/each}
  <div id="lead-out" class="relative" style:height={relativeBottomOffset + 'px'}></div>
</div>

<style>
  #immich-scrubbable-scrollbar,
  #time-segment {
    contain: layout size style;
  }
</style>
