import clsx from 'clsx';
import { useEffect, useMemo, useRef, useState } from 'react';
import { twMerge } from 'tailwind-merge';
import { useDebounceValue, useResizeObserver } from 'usehooks-ts';

import { decodeAudioData, getAudioSamples } from '@/utils/waveform';

export type LinesWaveformProps = {
  className?: string;
  svgClassName?: string;
  src?: string;
  sampleRate?: number;
  strokeWidth?: number;
  strokeGap?: number;
  height?: number;
  debounceInterval?: number;
  throttleInterval?: number;
};

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

const LinesWaveform: React.FC<Props> = (props) => {
  const {
    children,
    className,
    svgClassName,
    src,
    sampleRate,
    strokeWidth = 1.5,
    strokeGap = 2 * strokeWidth,
    height: displayHeight,
    debounceInterval = 200,
    throttleInterval = 600,
    ...restProps
  } = props;

  const ref = useRef<HTMLDivElement>(null);

  const [[waveformWidth, waveformHeight], setWaveformSize] = useState([
    0,
    displayHeight ?? 100,
  ]);

  useResizeObserver({
    ref: ref as React.RefObject<HTMLDivElement>, // https://github.com/juliencrn/usehooks-ts/pull/675
    onResize() {
      if (ref.current) {
        setWaveformSize([ref.current.clientWidth, ref.current.clientHeight]);
      }
    },
  });

  // Determine how many lines we want to use to render the full waveform
  const debounceOptions = useMemo(
    () => ({ leading: true, trailing: true, maxWait: throttleInterval }),
    [throttleInterval]
  );
  const defaultSvgRenderProperties = useRef({
    width: 0,
    height: 0,
    strokeWidth: 0,
    strokeGap: 0,
    waveformLines: 0,
  });
  const [svgRenderProperties, setSvgRenderProperties] = useDebounceValue(
    defaultSvgRenderProperties.current,
    debounceInterval,
    debounceOptions
  );
  useEffect(() => {
    const width = waveformWidth;
    const height = displayHeight ?? waveformHeight;
    if (waveformWidth)
      setSvgRenderProperties({
        width,
        height,
        strokeWidth,
        strokeGap,
        waveformLines:
          strokeWidth + strokeGap &&
          Math.floor(waveformWidth / (strokeWidth + strokeGap)),
      });
  }, [
    strokeWidth,
    strokeGap,
    waveformWidth,
    waveformHeight,
    displayHeight,
    setSvgRenderProperties,
  ]);

  const [audioData, setAudioData] = useState<AudioBuffer | null>(null);
  useEffect(() => {
    async function getAudioData(audioUrl: string) {
      try {
        const decodedAudioData = await decodeAudioData(audioUrl);
        if (decodedAudioData) {
          setAudioData(decodedAudioData);
        }
      } catch (e) {
        console.error(e);
      }
    }

    if (src) getAudioData(src);
  }, [src, sampleRate]);

  const { width, height, viewBox, pathData } = useMemo(() => {
    const { width, height, strokeGap, strokeWidth, waveformLines } =
      svgRenderProperties;
    // Sample the wave based on the number of lines we want to display
    const samples =
      waveformLines && audioData
        ? getAudioSamples(audioData, { numSamples: waveformLines })
        : [];
    // Calculate line spacing and center horizontally
    const intervalX = strokeGap + strokeWidth;
    const startX = 0.5 * intervalX;
    // Normalize to the samples we haveVertically scale to leave a little space at the top and bottom
    const scale = Math.max(...samples) / 0.75;
    return {
      width,
      height,
      viewBox: `0 0 ${width} ${height}`,
      pathData: samples
        .map((v, i) => {
          const y = scale && Math.round((height * v) / scale) - strokeWidth;
          return y <= 0
            ? ''
            : `M${startX + i * intervalX} ${0.5 * (height - y)}l0 ${y}`;
        })
        .join(''),
    };
  }, [audioData, svgRenderProperties]);

  return (
    <div
      className={twMerge(
        'relative flex flex-row items-stretch justify-stretch',
        className
      )}
      ref={ref}
      {...restProps}
    >
      <svg
        className={twMerge(
          clsx('absolute inset-0 h-full w-full', {
            'animate-waveform-in animate-duration-100': !!pathData,
          }),
          svgClassName
        )}
        viewBox={viewBox}
        width={width}
        height={height}
        preserveAspectRatio='none'
        fill='none'
        stroke='currentColor'
        strokeWidth={strokeWidth}
        strokeLinecap='round'
      >
        <path
          d={pathData}
          fill='none'
          stroke='currentColor'
          vectorEffect='non-scaling-stroke'
        />
      </svg>
    </div>
  );
};

export default LinesWaveform;
