import clsx from 'clsx';
import { clamp } from 'lodash-es';
import React, { useCallback, useMemo, useRef, useState } from 'react';
import { twMerge } from 'tailwind-merge';

import { formatDuration } from '@/components/song/songUtils';
import useDragLifecycle from '@/hooks/useDragLifecycle';
import { PulsingLinesIcon } from '@/icons';

export type PlaybackProgressProps = {
  className?: string;
  timeClassName?: string;
  currentTimeClassName?: string;
  durationClassName?: string;
  trackClassName?: string;
  progressClassName?: string;
  remainingClassName?: string;
  buttonClassName?: string;
  tooltipClassName?: string;
  disabled?: boolean;
  currentTime?: number;
  duration?: number;
  estimatedDuration?: number;
  playbackProgress?: number;
  showTime?: boolean;
  showEstimatedDurationSpinner?: boolean;
  onSeekTo?: (time: number | null, prevTime: number | null) => void;
  onSeekStart?: (time: number | null, prevTime: number | null) => void;
  onSeekMove?: (time: number | null, prevTime: number | null) => void;
  onSeekEnd?: (time: number | null, prevTime: number | null) => void;
  renderTooltip?: React.ComponentType<{
    currentTime?: number;
    duration?: number;
    hoverTime?: number | null;
    x?: number;
    y?: number;
    width?: number;
    height?: number;
    isHovered?: boolean;
    disabled?: boolean;
    className?: string;
  }>;
};

export type Props = Omit<
  React.HTMLAttributes<HTMLDivElement>,
  keyof PlaybackProgressProps
> &
  PlaybackProgressProps;

const PlaybackProgress: React.FC<Props> = (props) => {
  const {
    className,
    timeClassName,
    currentTimeClassName,
    durationClassName,
    trackClassName,
    progressClassName,
    remainingClassName,
    buttonClassName,
    tooltipClassName,
    children,
    currentTime,
    duration,
    estimatedDuration,
    playbackProgress = currentTime == null
      ? 0
      : duration
        ? currentTime / duration
        : estimatedDuration
          ? currentTime / estimatedDuration
          : 0,
    showTime = true,
    showEstimatedDurationSpinner = true,
    onSeekTo,
    onSeekStart,
    onSeekMove,
    onSeekEnd,
    disabled = (duration == null && estimatedDuration == null) ||
      !(onSeekTo || onSeekStart || onSeekMove || onSeekEnd),
    renderTooltip,
    ...restProps
  } = props;

  const trackRef = useRef<HTMLDivElement>(null);
  const buttonContainerRef = useRef<HTMLDivElement>(null);

  const [seekProgress, setSeekProgress] = useState<number | null>(null);
  const [isSeeking, setIsSeeking] = useState(false);

  // Hover state
  const [hoverData, setHoverData] = useState<{
    progress: number;
    clientX: number;
    clientY: number;
    relativeX: number;
    relativeY: number;
    trackWidth: number;
    trackHeight: number;
  } | null>(null);

  const timeScale = useCallback(
    () => duration ?? estimatedDuration ?? 1,
    [duration, estimatedDuration]
  );

  const updateHoverData = useCallback((clientX: number, clientY: number) => {
    const trackElement = trackRef.current;
    if (!trackElement) {
      setHoverData(null);
      return;
    }
    const rect = trackElement.getBoundingClientRect();
    const relativeX = clientX - rect.x;
    const progress = clamp(relativeX / rect.width, 0, 1);
    setHoverData({
      progress,
      clientX,
      clientY,
      relativeX,
      relativeY: clientY - rect.y,
      trackWidth: rect.width,
      trackHeight: rect.height,
    });
  }, []);

  const handleBarMouseMove = useCallback(
    (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
      updateHoverData(event.clientX, event.clientY);
    },
    [updateHoverData]
  );
  const handleBarMouseLeave = useCallback(() => {
    if (!isSeeking) {
      setHoverData(null);
    }
  }, [isSeeking]);

  const getSeekProgress = useCallback((prevProgress = 0, deltaX = 0) => {
    if (buttonContainerRef.current) {
      const rect = buttonContainerRef.current.getBoundingClientRect();
      const nextSeekProgress = clamp(
        rect.width ? prevProgress + deltaX / rect.width : 0,
        0,
        1
      );
      return nextSeekProgress;
    }
    return prevProgress;
  }, []);

  const [handleTrackSeekStart] = useDragLifecycle({
    customState: { prevPlaybackProgress: playbackProgress },
    onDragStart(data) {
      data.customState.prevPlaybackProgress = playbackProgress;
      setIsSeeking(true);
      if (trackRef.current) {
        const rect = trackRef.current.getBoundingClientRect();
        const nextSeekProgress = clamp(
          (data.clientX - rect.x) / rect.width,
          0,
          1
        );
        const prevTime = playbackProgress * timeScale();
        const time = nextSeekProgress * timeScale();
        data.customState.prevPlaybackProgress = nextSeekProgress;
        onSeekTo?.(time, prevTime);
        onSeekStart?.(time, prevTime);
        setSeekProgress(nextSeekProgress);
      }
    },
    onDragEnd(data) {
      if (trackRef.current) {
        const prevSeekProgress = data.customState.prevPlaybackProgress;
        const prevTime = prevSeekProgress * timeScale();
        const time =
          getSeekProgress(prevSeekProgress, data.deltaX) * timeScale();
        onSeekEnd?.(time, prevTime);
        onSeekTo?.(time, prevTime);
      }
      setSeekProgress(null);
      setIsSeeking(false);
      // Clear hover data if mouse is not over the track
      if (trackRef.current) {
        const rect = trackRef.current.getBoundingClientRect();
        const mouseX = data.clientX;
        const mouseY = data.clientY;
        if (
          mouseX < rect.x ||
          mouseX > rect.x + rect.width ||
          mouseY < rect.y ||
          mouseY > rect.y + rect.height
        ) {
          setHoverData(null);
        }
      }
    },
    onDragMove(data) {
      if (trackRef.current) {
        const prevSeekProgress = data.customState.prevPlaybackProgress;
        const nextSeekProgress = getSeekProgress(prevSeekProgress, data.deltaX);
        const prevTime = prevSeekProgress * timeScale();
        const time = nextSeekProgress * timeScale();
        onSeekMove?.(time, prevTime);
        setSeekProgress(nextSeekProgress);
        // Update hover data during seeking
        updateHoverData(data.clientX, data.clientY);
      }
    },
  });

  const [handleSeekStart] = useDragLifecycle({
    customState: { prevPlaybackProgress: playbackProgress },
    onDragStart(data) {
      data.customState.prevPlaybackProgress = playbackProgress;
      setIsSeeking(true);
      const prevTime = playbackProgress * timeScale();
      const time = getSeekProgress(playbackProgress, data.deltaX) * timeScale();
      onSeekStart?.(time, prevTime);
      setSeekProgress(playbackProgress);
    },
    onDragEnd(data) {
      if (trackRef.current) {
        const prevSeekProgress = data.customState.prevPlaybackProgress;
        const prevTime = prevSeekProgress * timeScale();
        const time =
          getSeekProgress(prevSeekProgress, data.deltaX) * timeScale();
        onSeekEnd?.(time, prevTime);
        onSeekTo?.(time, prevTime);
      }
      setSeekProgress(null);
      setIsSeeking(false);
      // Clear hover data if mouse is not over the track
      if (trackRef.current) {
        const rect = trackRef.current.getBoundingClientRect();
        const mouseX = data.clientX;
        const mouseY = data.clientY;
        if (
          mouseX < rect.x ||
          mouseX > rect.x + rect.width ||
          mouseY < rect.y ||
          mouseY > rect.y + rect.height
        ) {
          setHoverData(null);
        }
      }
    },
    onDragMove(data) {
      if (trackRef.current) {
        const prevSeekProgress = data.customState.prevPlaybackProgress;
        const nextSeekProgress = getSeekProgress(prevSeekProgress, data.deltaX);
        const prevTime = prevSeekProgress * timeScale();
        const time = nextSeekProgress * timeScale();
        onSeekMove?.(time, prevTime);
        setSeekProgress(nextSeekProgress);
        // Update hover data during seeking
        updateHoverData(data.clientX, data.clientY);
      }
    },
  });

  const displayedPlaybackProgress = useMemo(() => {
    const progress = seekProgress ?? playbackProgress;
    return Math.min(progress, 1);
  }, [playbackProgress, seekProgress]);

  return (
    <div
      className={twMerge(
        'relative flex flex-row items-center justify-center gap-2',
        // Button size
        '[--min-target-size:1rem]',
        '[--button-width:0.75rem]',
        '[--button-height:var(--button-width)]',
        // Visual space between button and track
        '[--button-border-width:0.125rem]',
        // Thickness of the progress bar track
        '[--track-width:0.25rem]',
        // Colors
        '[--track-progress-color:var(--color-foreground-primary-glass)]',
        '[--track-remaining-color:var(--color-background-fog-dense)]',
        '[--button-color:var(--color-foreground-primary-glass)]',
        '[--time-color:var(--color-foreground-tertiary-glass)]',
        className
      )}
      onMouseMove={handleBarMouseMove}
      onMouseLeave={handleBarMouseLeave}
      {...restProps}
    >
      {!showTime ? null : (
        <div
          className={twMerge(
            'min-w-8 text-right text-xs text-(--time-color)',
            timeClassName
          )}
        >
          {formatDuration(currentTime)}
        </div>
      )}
      <div
        className={clsx('relative min-w-0 flex-1 self-stretch', {
          'min-h-4': showTime,
        })}
      >
        <div
          className={clsx(
            'absolute inset-0',
            'before:absolute before:inset-x-0 before:top-1/2 before:-translate-y-1/2',
            'before:min-h-(--min-target-size)',
            {
              'cursor-pointer': !disabled,
            }
          )}
          ref={trackRef}
          onMouseDown={disabled ? undefined : handleTrackSeekStart}
          onTouchStart={disabled ? undefined : handleTrackSeekStart}
        >
          {typeof renderTooltip === 'function'
            ? React.createElement(renderTooltip, {
                x: hoverData?.relativeX,
                y: hoverData?.relativeY,
                width: hoverData?.trackWidth,
                height: hoverData?.trackHeight,
                isHovered: !!hoverData,
                currentTime: currentTime,
                duration: duration,
                hoverTime: hoverData
                  ? hoverData.progress * timeScale()
                  : undefined,
                disabled: false,
                className: tooltipClassName,
              })
            : null}

          <div
            className={twMerge(
              'absolute inset-x-0 top-1/2 h-(--track-width) -translate-y-1/2 overflow-clip rounded-sm',
              trackClassName
            )}
          >
            <div
              className={twMerge(
                'absolute inset-y-0 left-0 bg-(--track-progress-color)',
                progressClassName
              )}
              style={{
                width: `calc(max(0px, ${displayedPlaybackProgress} * (100% - var(--button-width))`,
              }}
            />
            <div
              className={twMerge(
                'absolute inset-y-0 right-0 bg-(--track-remaining-color)',
                remainingClassName
              )}
              style={{
                width: `calc(max(0px, ${1 - displayedPlaybackProgress} * (100% - var(--button-width)))`,
              }}
            />
          </div>
        </div>
        <div
          className='pointer-events-none absolute inset-x-1.5 inset-y-0'
          ref={buttonContainerRef}
        >
          <button
            className={clsx(
              'absolute inset-y-0 left-1/2 w-(--button-width) -translate-x-1/2',
              {
                'cursor-pointer': !disabled,
                'cursor-not-allowed opacity-50': disabled,
              }
            )}
            style={{ left: `${displayedPlaybackProgress * 100}%` }}
            onMouseDown={handleSeekStart}
            onTouchStart={handleSeekStart}
            disabled={disabled}
          >
            <span
              className={twMerge(
                'absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2',
                'h-(--button-height) w-(--button-width) rounded-full border-(length:--border-width)',
                'border-transparent bg-(--button-color) bg-clip-content',
                'pointer-events-auto',
                buttonClassName
              )}
            />
          </button>
        </div>
      </div>
      {!showTime ? null : (
        <div
          className={twMerge(
            'min-w-8 text-left text-xs text-(--time-color)',
            timeClassName
          )}
        >
          {duration != null || !showEstimatedDurationSpinner ? (
            formatDuration(duration)
          ) : (
            <PulsingLinesIcon />
          )}
        </div>
      )}
    </div>
  );
};

export default PlaybackProgress;
