<script lang="ts">
  import { onMount } from 'svelte';

  interface Props {
    buffer: AudioBuffer | null;
    playbackPosition?: number;
  }

  let { buffer, playbackPosition = 0 }: Props = $props();
  let canvas: HTMLCanvasElement;

  onMount(() => {
    const resizeObserver = new ResizeObserver(() => {
      if (canvas) {
        updateCanvasSize();
        draw();
      }
    });

    resizeObserver.observe(canvas.parentElement!);

    return () => resizeObserver.disconnect();
  });

  $effect(() => {
    buffer;
    playbackPosition;
    draw();
  });

  function updateCanvasSize() {
    const parent = canvas.parentElement!;
    canvas.width = parent.clientWidth;
    canvas.height = parent.clientHeight;
  }

  function calculateLevels(): [number, number] {
    if (!buffer) return [-Infinity, -Infinity];

    const numChannels = buffer.numberOfChannels;
    const duration = buffer.length / buffer.sampleRate;

    if (playbackPosition <= 0 || playbackPosition >= duration) {
      return [-Infinity, -Infinity];
    }

    const windowSize = Math.floor(buffer.sampleRate * 0.05);
    const currentSample = Math.floor(playbackPosition * buffer.sampleRate);
    const startSample = Math.max(0, currentSample - windowSize);
    const endSample = Math.min(buffer.length, currentSample);

    const leftData = new Float32Array(endSample - startSample);
    buffer.copyFromChannel(leftData, 0, startSample);

    let leftSum = 0;
    for (let i = 0; i < leftData.length; i++) {
      leftSum += leftData[i] * leftData[i];
    }
    const leftRMS = Math.sqrt(leftSum / leftData.length);
    const leftDB = leftRMS > 0 ? 20 * Math.log10(leftRMS) : -Infinity;

    let rightDB = leftDB;
    if (numChannels > 1) {
      const rightData = new Float32Array(endSample - startSample);
      buffer.copyFromChannel(rightData, 1, startSample);

      let rightSum = 0;
      for (let i = 0; i < rightData.length; i++) {
        rightSum += rightData[i] * rightData[i];
      }
      const rightRMS = Math.sqrt(rightSum / rightData.length);
      rightDB = rightRMS > 0 ? 20 * Math.log10(rightRMS) : -Infinity;
    }

    return [leftDB, rightDB];
  }

  function draw() {
    if (!canvas) return;

    const ctx = canvas.getContext('2d')!;
    const width = canvas.width;
    const height = canvas.height;

    ctx.fillStyle = '#0a0a0a';
    ctx.fillRect(0, 0, width, height);

    if (!buffer) return;

    const [leftDB, rightDB] = calculateLevels();
    const isHorizontal = width > height;

    if (isHorizontal) {
      drawHorizontal(ctx, leftDB, rightDB, width, height);
    } else {
      drawVertical(ctx, leftDB, rightDB, width, height);
    }
  }

  function drawVertical(ctx: CanvasRenderingContext2D, leftDB: number, rightDB: number, width: number, height: number) {
    const channelWidth = width / 2;

    drawChannelVertical(ctx, 0, leftDB, channelWidth, height);
    drawChannelVertical(ctx, channelWidth, rightDB, channelWidth, height);

    ctx.strokeStyle = '#333';
    ctx.lineWidth = 1;
    ctx.beginPath();
    ctx.moveTo(channelWidth, 0);
    ctx.lineTo(channelWidth, height);
    ctx.stroke();
  }

  function drawHorizontal(ctx: CanvasRenderingContext2D, leftDB: number, rightDB: number, width: number, height: number) {
    const channelHeight = height / 2;

    drawChannelHorizontal(ctx, 0, leftDB, width, channelHeight);
    drawChannelHorizontal(ctx, channelHeight, rightDB, width, channelHeight);

    ctx.strokeStyle = '#333';
    ctx.lineWidth = 1;
    ctx.beginPath();
    ctx.moveTo(0, channelHeight);
    ctx.lineTo(width, channelHeight);
    ctx.stroke();
  }

  function dbToY(db: number, height: number): number {
    const minDB = -60;
    const maxDB = 0;
    const clampedDB = Math.max(minDB, Math.min(maxDB, db));
    const normalized = (clampedDB - minDB) / (maxDB - minDB);
    return height - (normalized * height);
  }

  function dbToX(db: number, width: number): number {
    const minDB = -60;
    const maxDB = 0;
    const clampedDB = Math.max(minDB, Math.min(maxDB, db));
    const normalized = (clampedDB - minDB) / (maxDB - minDB);
    return normalized * width;
  }

  function drawChannelVertical(ctx: CanvasRenderingContext2D, x: number, levelDB: number, width: number, height: number) {
    const gridMarks = [0, -3, -6, -10, -20, -40, -60];
    ctx.strokeStyle = '#222';
    ctx.lineWidth = 1;
    for (const db of gridMarks) {
      const y = dbToY(db, height);
      ctx.beginPath();
      ctx.moveTo(x, y);
      ctx.lineTo(x + width, y);
      ctx.stroke();
    }

    if (levelDB === -Infinity) return;

    const segments = [
      { startDB: -60, endDB: -18, color: '#00ff00' },
      { startDB: -18, endDB: -6, color: '#ffff00' },
      { startDB: -6, endDB: 0, color: '#ff0000' }
    ];

    for (const segment of segments) {
      if (levelDB >= segment.startDB) {
        const startY = dbToY(segment.startDB, height);
        const endY = dbToY(segment.endDB, height);

        const clampedLevelDB = Math.min(levelDB, segment.endDB);
        const levelY = dbToY(clampedLevelDB, height);

        const segmentHeight = startY - levelY;

        if (segmentHeight > 0) {
          ctx.fillStyle = segment.color;
          ctx.fillRect(x, levelY, width, segmentHeight);
        }
      }
    }
  }

  function drawChannelHorizontal(ctx: CanvasRenderingContext2D, y: number, levelDB: number, width: number, height: number) {
    const gridMarks = [0, -3, -6, -10, -20, -40, -60];
    ctx.strokeStyle = '#222';
    ctx.lineWidth = 1;
    for (const db of gridMarks) {
      const x = dbToX(db, width);
      ctx.beginPath();
      ctx.moveTo(x, y);
      ctx.lineTo(x, y + height);
      ctx.stroke();
    }

    if (levelDB === -Infinity) return;

    const segments = [
      { startDB: -60, endDB: -18, color: '#00ff00' },
      { startDB: -18, endDB: -6, color: '#ffff00' },
      { startDB: -6, endDB: 0, color: '#ff0000' }
    ];

    for (const segment of segments) {
      if (levelDB >= segment.startDB) {
        const startX = dbToX(segment.startDB, width);
        const endX = dbToX(segment.endDB, width);

        const clampedLevelDB = Math.min(levelDB, segment.endDB);
        const levelX = dbToX(clampedLevelDB, width);

        const segmentWidth = levelX - startX;

        if (segmentWidth > 0) {
          ctx.fillStyle = segment.color;
          ctx.fillRect(startX, y, segmentWidth, height);
        }
      }
    }
  }
</script>

<canvas bind:this={canvas}></canvas>

<style>
  canvas {
    width: 100%;
    height: 100%;
  }
</style>
