import styled from '@emotion/styled';
import { useAnimationFrame } from 'framer-motion';
import { useCallback, useEffect, useRef } from 'react';

import useClickDrag from '@/hooks/useClickDrag';

import { isFallbackSampleAudio } from './useUrlAudioSampler';

interface CreateWaveformProps {
  isPlaying?: boolean;
  sampleAudio: (progress: number) => number;
  getCurrentProgress: () => number;
  setCurrentProgress: (progress: number) => void;
  frameCountRef?: React.RefObject<number>;
}

const Wrapper = styled.div`
  position: relative;
  width: 100%;
  height: 100%;
  overflow: hidden;
  border-radius: 12px;
  canvas {
    position: absolute;
    top: 0;
    left: 0;
  }
`;

const BAR_WIDTH = 1;
const BAR_SPACING = 2;

const Seekbar = styled.div`
  width: 2px;
  height: 100%;
  background-color: var(--color-foreground-primary);
  cursor: ew-resize;
  position: absolute;
  top: 0;
  bottom: 0;
  margin-left: -2px;
  &:before {
    content: '';
    display: block;
    position: absolute;
    top: 0;
    left: -4px;
    right: -4px;
    bottom: 0;
  }
`;

const FALLBACK_FRAME_COUNT_REF = { current: 0 };

export default function CreateWaveform({
  isPlaying,
  sampleAudio,
  getCurrentProgress,
  setCurrentProgress,
  frameCountRef = FALLBACK_FRAME_COUNT_REF,
}: CreateWaveformProps) {
  const wrapperRef = useRef<HTMLDivElement>(null);
  const canvasRef = useRef<HTMLCanvasElement>(null);

  const render = useCallback(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;

    const wrapper = wrapperRef.current;
    if (!wrapper) return;

    const ctx = canvas.getContext('2d');
    if (!ctx) return;

    canvas.width = wrapper.clientWidth * window.devicePixelRatio;
    canvas.height = wrapper.clientHeight * window.devicePixelRatio;
    canvas.style.transform = `scale(${1 / window.devicePixelRatio})`;
    canvas.style.transformOrigin = 'top left';
    ctx.save();
    ctx.scale(window.devicePixelRatio, window.devicePixelRatio);

    const maxHeight = wrapper.clientHeight - 16;

    const totalBars = Math.floor(
      wrapper.clientWidth / (BAR_WIDTH + BAR_SPACING)
    );
    const barsBefore = Math.ceil(getCurrentProgress() * totalBars);

    const drawBar = (i: number) => {
      const height = Math.max(
        BAR_WIDTH,
        sampleAudio(i / totalBars) * maxHeight
      );
      ctx.moveTo(
        i * (BAR_WIDTH + BAR_SPACING),
        wrapper.clientHeight / 2 - height / 2
      );
      ctx.lineTo(
        i * (BAR_WIDTH + BAR_SPACING),
        wrapper.clientHeight / 2 + height / 2
      );
    };

    if (isFallbackSampleAudio(sampleAudio)) {
      ctx.fillStyle = '#fff8';
      ctx.font = '12px Arial';
      ctx.textAlign = 'center';
      ctx.textBaseline = 'middle';
      ctx.fillText(
        'Loading',
        wrapper.clientWidth / 2,
        wrapper.clientHeight / 2
      );
    } else {
      ctx.strokeStyle = '#fd429c';
      ctx.beginPath();
      for (let i = 0; i < barsBefore; i++) {
        drawBar(i);
      }
      ctx.stroke();

      ctx.beginPath();
      ctx.strokeStyle = '#fff8';
      for (let i = barsBefore; i <= totalBars; i++) {
        drawBar(i);
      }
      ctx.stroke();
    }
    ctx.restore();
  }, [sampleAudio, getCurrentProgress, frameCountRef]);

  useEffect(() => {
    if (!wrapperRef.current) return;

    const resizeObserver = new ResizeObserver(() => {
      frameCountRef.current++;
    });

    resizeObserver.observe(wrapperRef.current);

    return () => {
      resizeObserver.disconnect();
    };
  }, []);

  const seekbarRef = useRef<HTMLDivElement>(null);

  const receiveClickDragRef = useClickDrag(
    useCallback(
      ({ clientX, event }) => {
        const wrapper = event.currentTarget as HTMLDivElement;
        if (!wrapper) return;
        const rect = wrapper.getBoundingClientRect();
        const progress = Math.max(
          0,
          Math.min(1, (clientX - rect.left) / rect.width)
        );
        setCurrentProgress(progress);
        frameCountRef.current++;
        return {
          onMouseMove: ({ clientX }) => {
            const progress = Math.max(
              0,
              Math.min(1, (clientX - rect.left) / rect.width)
            );
            setCurrentProgress(progress);
            frameCountRef.current++;
          },
        };
      },
      [setCurrentProgress]
    )
  );

  const lastFrameCountRef = useRef(-1);
  const lastProgressRef = useRef(-1);
  useAnimationFrame(
    useCallback(() => {
      const progress = getCurrentProgress();
      if (isPlaying || lastProgressRef.current !== progress) {
        frameCountRef.current++;
        lastProgressRef.current = progress;
      }
      if (lastFrameCountRef.current === frameCountRef.current) return;

      lastFrameCountRef.current = frameCountRef.current;
      render();

      if (!seekbarRef.current) return;

      seekbarRef.current.style.left = `${progress * 100}%`;
    }, [getCurrentProgress, render])
  );

  const receiveWrapperRef = useCallback(
    (ref: HTMLDivElement) => {
      wrapperRef.current = ref;
      receiveClickDragRef(ref);
      frameCountRef.current++;
    },
    [receiveClickDragRef]
  );

  useEffect(() => {
    frameCountRef.current++;
  }, [sampleAudio, getCurrentProgress]);

  return (
    <Wrapper ref={receiveWrapperRef}>
      <canvas ref={canvasRef} />
      <Seekbar ref={seekbarRef} />
    </Wrapper>
  );
}
