import { useRef } from 'react';
import { TimelineState } from '../types';
import { viewportPxToSamples, samplesToViewportPx } from '../components/Timeline/calculator';
import { gray900 } from '../styles/colors_v2';

const maxDownsamplePower = 12;
const downsamplePowerStep = 1;

type DownsampleSet = {
  [key: string]: Int16Array
}

const downsampleCache: Map<Float32Array, DownsampleSet> = new Map();

const toFloat = (uInt8: number) => uInt8 / Math.pow(2, 15);
const toInt8 = (float: number) => Math.max(-32768, Math.min(32767, Math.round(float * Math.pow(2, 15))));

// Possible optimizations:
// 1. Find a way to do this in chunks, compute only the needed array size.

const makeDownsampleSet = (array: Float32Array) => {
  const output: DownsampleSet = {};
  output[1] = new Int16Array(array.length * 2);
  for (let i = 0; i < array.length; i ++) {
    output[1][0 + (i * 2)] = Math.min(0, toInt8(array[i]));
    output[1][1 + (i * 2)] = Math.max(0, toInt8(array[i]));
  }
  for (let p = downsamplePowerStep; p <= maxDownsamplePower; p += downsamplePowerStep) {
    const previousFactor = Math.pow(2, p - downsamplePowerStep);
    const factor = Math.pow(2, p);
    const previousArray = output[previousFactor];
    const stepSize = Math.pow(2, downsamplePowerStep);
    output[factor] = new Int16Array(previousArray.length / stepSize);
    let currentWriteIndex = 0;
    for (let i = 0; i < previousArray.length; i += stepSize * 2) {
      for (let j = 0; j < stepSize * 2; j += 2) {
        if (i + j <= previousArray.length) {
          output[factor][currentWriteIndex + 0] = Math.min(output[factor][currentWriteIndex + 0], previousArray[i + j + 0]);
          output[factor][currentWriteIndex + 1] = Math.max(output[factor][currentWriteIndex + 1], previousArray[i + j + 1]);
        }
      }
      currentWriteIndex += 2;
    }
  }
  return output as DownsampleSet;
}

const getDownsampledArray = (array: Float32Array, factor: number) => {
  const cached = downsampleCache.get(array);
  const result = cached || makeDownsampleSet(array);
  if (!cached) {
    downsampleCache.set(array, result);
  }
  return result[String(factor) as keyof DownsampleSet];
}

const getBestFactor = (input: number) => {
  for (let p = 0; p <= maxDownsamplePower; p += downsamplePowerStep) {
    const factor = Math.pow(2, p);
    if (factor > input / Math.pow(2, downsamplePowerStep)) {
      return factor;
    }
  }
  return Math.pow(2, maxDownsamplePower);
}

const bottoms: number[] = [];
const tops: number[] = [];

// Possible optimizations:
// 1. Reuse values to reduce the need for GCs
const makeRenderFunction = (canvas: HTMLCanvasElement, width: number, height: number, audioBuffers: AudioBuffer[], getTimelineState: () => TimelineState, fillColor: string, strokeColor: string) => {
  const ctx = canvas.getContext('2d');
  const toCSSPixel = (screenPx: number) => screenPx / window.devicePixelRatio;
  const toScreenPixel = (cssPx: number) => cssPx * window.devicePixelRatio;
  let channelData: ReturnType<typeof getDownsampledArray>;
  const pxToSamples = (state: TimelineState, px: number) => viewportPxToSamples(state, toCSSPixel(px));
  const samplesToPx = (state: TimelineState, samples: number) => toScreenPixel(samplesToViewportPx(state, samples));
  if (ctx) {
    let lastState: TimelineState | null = null;
    let lastSelection: [number | null, number | null] | null;
    return (selectionStart: number | null, selectionEnd: number | null) => {
      const state = getTimelineState();
      if (
        lastState &&
        lastSelection &&
        !Object.keys(state).find((k: any) => (state as any)[k] !== (lastState as any)[k]) &&
        lastSelection[0] === selectionStart && lastSelection[1] === selectionEnd
      ) {
        // would render the same thing as last time. bail.
        return;
      } else if (!lastSelection) {
        lastSelection = [null, null];
      }
      lastState = state;
      lastSelection[0] = selectionStart;
      lastSelection[1] = selectionEnd;
      ctx.clearRect(0, 0, width, height);
      let offset = 0;

      let selectionStartPx = Math.max(-1, Math.floor(samplesToPx(state, 0)));
      let selectionWidthPx = 0;

      if (selectionStart !== null && selectionEnd !== null) {
        ctx.fillStyle = fillColor;
        const targetLeft = samplesToPx(state, selectionStart);
        selectionStartPx = Math.ceil(Math.max(0, targetLeft));
        selectionWidthPx = Math.floor(
          Math.max(
            targetLeft < 0 ? 0 : 1,
            Math.min(
              width,
              Math.ceil(samplesToPx(state, selectionEnd))
            ) - selectionStartPx
          )
        );
        ctx.fillRect(selectionStartPx, 0, selectionWidthPx, height);
      }

      const selectionEndPx = selectionStartPx + selectionWidthPx;

      audioBuffers.forEach((audioBuffer) => {
        const numberOfChannels = audioBuffer.numberOfChannels;
        const channelHeight = height / numberOfChannels;
        const halfChannelHeight = channelHeight / 2;

        const targetLastRenderedX = Math.ceil(samplesToPx(state, offset + audioBuffer.length));
        const targetFirstRenderedX = Math.floor(samplesToPx(state, offset));

        if (targetLastRenderedX < -1 || targetFirstRenderedX > width + 2) {
          offset += audioBuffer.length;
          return;
        }

        const lastRenderedX = Math.min(width + 2, targetLastRenderedX);
        const firstRenderedX = Math.max(-1, targetFirstRenderedX);

        for (let channelNumber = 0; channelNumber < numberOfChannels; channelNumber ++) {
          const scaleFactor = getBestFactor((pxToSamples(state, width) - pxToSamples(state, 0)) / width);
          channelData = getDownsampledArray(audioBuffer.getChannelData(channelNumber), scaleFactor);
          const channelZero = channelHeight * (channelNumber + 0.5);
          const drawWaveformSection = (inputFrom: number, inputTo: number) => {
            const from = Math.floor(Math.max(-1, Math.min(width, inputFrom)));
            const to = Math.ceil(Math.max(-1, Math.min(width, inputTo)));
            if (from === to) {
              return;
            }
            bottoms.splice(0, bottoms.length);
            tops.splice(0, tops.length);
            ctx.lineWidth = window.devicePixelRatio;

            for (let x = from; x < to; x ++) {
              const firstSampleInPixel = Math.floor((pxToSamples(state, x) - offset) / scaleFactor);
              const firstSampleInNextPixel = Math.floor((pxToSamples(state, x+1) - offset) / scaleFactor);

              let min = toFloat(channelData[(firstSampleInPixel * 2) + 0] || 0);
              let max = toFloat(channelData[(firstSampleInPixel * 2) + 1] || 0);

              for (let i = firstSampleInPixel; i < firstSampleInNextPixel; i ++) {
                min = Math.min(toFloat(channelData[(i * 2) + 0] || 0), min);
                max = Math.max(toFloat(channelData[(i * 2) + 1] || 0), max);
              }

              // min *= 10;
              // max *= 10;

              const bottom = Math.round(channelZero + Math.min((min * halfChannelHeight), -0.5));
              const top = Math.round(channelZero + Math.max((max * halfChannelHeight), 0.5));
              // ctx.fillRect(x, top, 1, Math.max(1, bottom - top));
              bottoms.push(bottom);
              tops.push(top);
            }

            ctx.beginPath();
            let i = 0;
            ctx.moveTo(from, tops[i]);
            for (let x = from; x < to; x ++) {
              ctx.lineTo(x, tops[i]);
              i ++;
            }
            for (let x = to; x > from; x --) {
              ctx.lineTo(x, bottoms[i]);
              i --;
            }
            ctx.lineTo(from, bottoms[0]);
            ctx.lineTo(from, tops[0]);
            ctx.closePath();
            ctx.fill();
            ctx.stroke();
          }

          // selection contains buffer
          if (selectionStartPx <= firstRenderedX && selectionEndPx >= lastRenderedX) {
            ctx.strokeStyle = 'rgba(255,255,255,0.25)';
            ctx.fillStyle = gray900;
            drawWaveformSection(firstRenderedX, lastRenderedX);

          // buffer contains selection
          } else if (selectionStartPx >= firstRenderedX && selectionStartPx <= lastRenderedX) {
            ctx.strokeStyle = strokeColor;
            ctx.fillStyle = fillColor;
            drawWaveformSection(firstRenderedX, selectionStartPx);
            ctx.strokeStyle = 'rgba(255,255,255,0.25)';
            ctx.fillStyle = gray900;
            drawWaveformSection(selectionStartPx, selectionEndPx);
            ctx.strokeStyle = strokeColor;
            ctx.fillStyle = fillColor;
            drawWaveformSection(selectionEndPx, lastRenderedX);

          // selection starts in buffer
          } else if (selectionStartPx >= firstRenderedX && selectionEndPx >= lastRenderedX) {
            ctx.strokeStyle = strokeColor;
            ctx.fillStyle = fillColor;
            drawWaveformSection(firstRenderedX, selectionStartPx);
            ctx.strokeStyle = 'rgba(255,255,255,0.25)';
            ctx.fillStyle = gray900;
            drawWaveformSection(selectionStartPx, lastRenderedX);

          // selection ends in buffer
          } else if (selectionStartPx <= firstRenderedX && selectionEndPx <= lastRenderedX) {
            ctx.strokeStyle = 'rgba(255,255,255,0.25)';
            ctx.fillStyle = gray900;
            drawWaveformSection(firstRenderedX, selectionEndPx);
            ctx.strokeStyle = strokeColor;
            ctx.fillStyle = fillColor;
            drawWaveformSection(selectionEndPx, lastRenderedX);

          // no overlap
          } else {
            ctx.strokeStyle = strokeColor;
            ctx.fillStyle = fillColor;
            drawWaveformSection(firstRenderedX, lastRenderedX);
          }

        }

        offset += audioBuffer.length;
      });
    }
  }
}

// useMemo was failing me here, so it's re-implemented manually. (Why???)
export default (canvas: HTMLCanvasElement | undefined, width: number, height: number, audioBuffers: AudioBuffer[] | undefined, getTimelineState: () => TimelineState, fillColor: string, strokeColor: string) => {
  const lastArgsRef = useRef<any>({});
  const lastRenderFunctionRef = useRef<any>(null);
  const match = (
    canvas === lastArgsRef.current.canvas &&
    width === lastArgsRef.current.width &&
    height === lastArgsRef.current.height &&
    audioBuffers === lastArgsRef.current.audioBuffers &&
    getTimelineState === lastArgsRef.current.getTimelineState &&
    fillColor === lastArgsRef.current.fillColor &&
    strokeColor === lastArgsRef.current.strokeColor
  );
  if (!match) {
    lastArgsRef.current = { canvas, audioBuffers, getTimelineState, width, height, fillColor, strokeColor };
    if (canvas && audioBuffers) {
      lastRenderFunctionRef.current = makeRenderFunction(canvas, width, height, audioBuffers, getTimelineState, fillColor, strokeColor);
    }
  }
  return lastRenderFunctionRef.current;
};
