const downsampleCache: Map<
  Float32Array,
  Map<number, { tops: number[]; bottoms: number[] }>
> = new Map();

const traceWaveform = ({
  ctx,
  channelData,
  topPadding = 0,
  inverse = false,
  normalize = true,
  enableCache = true,
  useLineVariant = false,
  pxPerPoint,
  overReadSamples = 256,
}: {
  ctx: CanvasRenderingContext2D;
  channelData: Float32Array;
  topPadding: number;
  inverse?: boolean;
  normalize?: boolean;
  enableCache?: boolean;
  useLineVariant?: boolean;
  pxPerPoint?: number;
  overReadSamples?: number;
}) => {
  const canvas = ctx.canvas;
  const halfHeight = (canvas.height - topPadding) / 2;

  const _pxPerPoint =
    pxPerPoint !== undefined ? pxPerPoint : useLineVariant ? 10 : 2;

  let tops: number[] = [];
  let bottoms: number[] = [];
  const cached = downsampleCache.get(channelData)?.get(canvas.width);

  if (!cached || !enableCache) {
    let normalizationMultiple = 1;

    if (normalize) {
      let maxAbsSample = 0;
      for (let i = 0; i < channelData.length; i++) {
        maxAbsSample = Math.max(maxAbsSample, Math.abs(channelData[i]));
      }
      normalizationMultiple = maxAbsSample ? 1 / maxAbsSample : 0;
    }

    const computedTops: number[] = [];
    const computedBottoms: number[] = [];

    for (let x = 0; x < canvas.width; x += _pxPerPoint) {
      const firstSampleInPixel = Math.floor(
        Math.max(
          0,
          (x / (canvas.width - 1)) * channelData.length - overReadSamples / 2
        )
      );
      const lastSampleInPixel = Math.floor(
        Math.min(
          ((x + _pxPerPoint) / (canvas.width - 1)) * channelData.length +
            overReadSamples / 2,
          channelData.length - 1
        )
      );
      let top = -Infinity;
      let bottom = Infinity;
      for (let i = firstSampleInPixel; i <= lastSampleInPixel; i++) {
        top = Math.max(top, channelData[i]);
        bottom = Math.min(bottom, channelData[i]);
      }
      computedTops.push(top * normalizationMultiple);
      computedBottoms.push(bottom * normalizationMultiple);
    }

    tops = computedTops;
    bottoms = computedBottoms;

    if (enableCache) {
      let channelCache = downsampleCache.get(channelData);
      if (!channelCache) {
        channelCache = new Map<number, { tops: number[]; bottoms: number[] }>();
      }
      channelCache.set(canvas.width, {
        tops: computedTops,
        bottoms: computedBottoms,
      });
      downsampleCache.set(channelData, channelCache);
    }
  }

  if (cached) {
    tops = cached.tops;
    bottoms = cached.bottoms;
  }

  if (!useLineVariant) {
    if (inverse) {
      ctx.moveTo(0, 0);
      ctx.lineTo(0, tops[0] * -halfHeight + halfHeight + topPadding);
    } else {
      ctx.moveTo(0, tops[0] * -halfHeight + halfHeight + topPadding);
    }
  }

  if (useLineVariant) {
    ctx.strokeStyle = '#666';
    ctx.lineWidth = 3;
    ctx.lineCap = 'round';
  }

  let lastX = 0;
  for (let x = _pxPerPoint; x < canvas.width; x += _pxPerPoint) {
    const currentBucket = tops[x / _pxPerPoint];
    const prevBucket = tops[Math.max(0, (x - _pxPerPoint) / _pxPerPoint)];
    const nextBucket =
      tops[Math.min(tops.length - 1, (x + _pxPerPoint) / _pxPerPoint)];
    const top = (currentBucket + prevBucket + nextBucket) / 3;
    if (useLineVariant) {
      const bCurrentBucket = bottoms[x / _pxPerPoint];
      const bPrevBucket = bottoms[Math.max(0, (x - _pxPerPoint) / _pxPerPoint)];
      const bNextBucket =
        bottoms[Math.min(bottoms.length - 1, (x + _pxPerPoint) / _pxPerPoint)];
      const bottom = (bCurrentBucket + bPrevBucket + bNextBucket) / 3;
      ctx.moveTo(x, top * -halfHeight + halfHeight + topPadding - 1);
      ctx.lineTo(x, bottom * -halfHeight + halfHeight + topPadding + 1);
    } else {
      ctx.lineTo(x, top * -halfHeight + halfHeight + topPadding - 1);
    }

    lastX = x;
  }

  if (!useLineVariant) {
    ctx.lineTo(canvas.width, halfHeight + topPadding);
    if (inverse) {
      ctx.lineTo(canvas.width, 0);
      ctx.lineTo(0, 0);
      ctx.moveTo(0, canvas.height);
      ctx.lineTo(0, bottoms[0] * -halfHeight + halfHeight + topPadding);
      for (let x = _pxPerPoint; x < canvas.width; x++) {
        const currentBucket = bottoms[x / _pxPerPoint];
        const prevBucket =
          bottoms[Math.max(0, (x - _pxPerPoint) / _pxPerPoint)];
        const nextBucket =
          bottoms[
            Math.min(bottoms.length - 1, (x + _pxPerPoint) / _pxPerPoint)
          ];
        const bottom = (currentBucket + prevBucket + nextBucket) / 3;
        ctx.lineTo(x, bottom * -halfHeight + halfHeight + topPadding + 1);
        lastX = x;
      }
      ctx.lineTo(canvas.width, halfHeight + topPadding);
      ctx.lineTo(canvas.width, canvas.height);
      ctx.lineTo(0, canvas.height);
    } else {
      ctx.lineTo(canvas.width, halfHeight + topPadding);
      for (let x = lastX; x >= 0; x--) {
        const currentBucket = bottoms[x / _pxPerPoint];
        const prevBucket =
          bottoms[Math.max(0, (x - _pxPerPoint) / _pxPerPoint)];
        const nextBucket =
          bottoms[
            Math.min(bottoms.length - 1, (x + _pxPerPoint) / _pxPerPoint)
          ];
        const bottom = (currentBucket + prevBucket + nextBucket) / 3;
        ctx.lineTo(x, bottom * -halfHeight + halfHeight + topPadding + 1);
      }
      ctx.lineTo(0, tops[0] * halfHeight + halfHeight + topPadding);
    }
  } else {
    ctx.stroke();
  }
};

export default traceWaveform;
