import { AudioPlaybackContext } from '../lib/useAudioPlayback';
import styled from '@emotion/styled';
import React from 'react';
import { useCallback, useContext, useEffect, useMemo, useRef } from 'react';

const Wrapper = styled.div`
  pointer-events: none;
  position: fixed;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;

  display: flex;
`;

const Canvas = styled.canvas`
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
`;

const FAST_HALF_LIFE = 0.25;
const energy = [0, 0, 0, 0];

const dot = (ctx: CanvasRenderingContext2D, x: number, y: number, p: number) => {
  ctx.moveTo(x * p - 1, y * p);
  ctx.lineTo(x * p + 1, y * p);
};

const box = (ctx: CanvasRenderingContext2D, x: number, y: number, p: number) => {
  ctx.rect(x * p - p / 2, y * p - p / 2, p, p);
};

const cross = (ctx: CanvasRenderingContext2D, x: number, y: number, p: number) => {
  ctx.moveTo(x * p - 3, y * p - 3);
  ctx.lineTo(x * p + 3, y * p + 3);

  ctx.moveTo(x * p - 3, y * p + 3);
  ctx.lineTo(x * p + 3, y * p - 3);
};

const crossbox = (ctx: CanvasRenderingContext2D, x: number, y: number, p: number) => {
  cross(ctx, x, y, p);
  box(ctx, x, y, p);
};

const fwdSlash = (ctx: CanvasRenderingContext2D, x: number, y: number, p: number) => {
  ctx.moveTo(x * p - 3, y * p + 3);
  ctx.lineTo(x * p + 3, y * p - 3);
};

const bckSlash = (ctx: CanvasRenderingContext2D, x: number, y: number, p: number) => {
  ctx.moveTo(x * p - 3, y * p - 3);
  ctx.lineTo(x * p + 3, y * p + 3);
};

const SHAPE_CYCLE_BEATS = 8;

const getNoise = (x: number, y: number, t: number) => {
  const n = Math.sin(x * 12) + Math.sin(y * 12) + t;
  return (Math.abs(n) / 2) % 1;
};

const getCellValue = (
  x: number,
  y: number,
  w: number,
  h: number,
  spectrum: Uint8Array | null,
  beats: number,
  automata: { x: number; y: number; influence: number }[]
) => {
  const minD = Math.max(w, h) - 1;
  const xp = 2 * (x / minD - w / 2 / minD);
  const yp = 2 * (y / minD - h / 2 / minD);

  let specShift = 0;

  if (spectrum) {
    const p = Math.pow(x / w, 2);
    const s = spectrum[Math.floor(p * spectrum.length * 0.2)] / 255;
    specShift = s * (0.5 + 1.25 * p) > 0.4 ? 0.05 : 0;
  }

  let shapeShift =
    (Math.abs(xp * Math.sin(beats / SHAPE_CYCLE_BEATS)) + Math.abs(yp * Math.cos(beats / SHAPE_CYCLE_BEATS))) * 0.5;

  let automataShift = 0;

  automata.forEach((a) => {
    let d = Math.max(Math.abs(xp - a.x), Math.abs(yp - a.y));
    if (d < 0.2 && a.influence === 0) {
      automataShift += Math.cos(d * Math.PI) / 5 + getNoise(x, y, beats / 32) * 0.05;
    }

    if ((d < 0.2 && a.influence === 1) || (d > 0.1 && d < 0.15 && a.influence === 0)) {
      automataShift += y % 2 === 0 ? 0.15 : -0.15;
    }

    if (d < 0.2 && a.influence === 2) {
      const lineLength = getNoise(y, y, beats / 32) * 20;
      if (x % 20 < lineLength) {
        automataShift += 0.15;
      }
    }

    if (d < 0.2 && a.influence === 3) {
      automataShift += 0.15;
    }
  });

  return automataShift !== 0 ? automataShift * 2 + 0.5 * specShift : Math.max(shapeShift - specShift, 2 * specShift);
};

const TrackVisualizer = ({
  bands,
  xOffset,
  yOffsetBottom,
  pitch,
  getSpectrum,
}: {
  bands: number[][];
  xOffset: number;
  yOffsetBottom: number;
  pitch: number;
  getSpectrum: () => Uint8Array | null;
}) => {
  const { playing, duration, getCurrentTime } = useContext(AudioPlaybackContext);

  const scaledBands = useMemo(() => {
    return bands.map((b) => {
      const squared = b.map((v) => v * v);
      const avg = squared.reduce((a, v) => a + v, 0) / squared.length;
      return squared.map((v) => v / avg);
    });
  }, [bands]);

  const canvasRef = useRef<HTMLCanvasElement | null>(null);
  const ctxRef = useRef<CanvasRenderingContext2D | null>(null);
  const playingRef = useRef<boolean>(false);

  useEffect(() => {
    const handleResize = () => {
      const canvas = canvasRef.current;
      if (!canvas) return;
      canvas.width = Math.round(canvas.parentElement!.clientWidth / 2) * 2;
      canvas.height = Math.round(canvas.parentElement!.clientHeight / 2) * 2;
    };
    window.addEventListener('resize', handleResize);
  }, []);

  const mousePosRef = useRef<{ x: number; y: number }>({ x: 0, y: 0 });

  const startPlaying = useCallback(() => {
    const canvas = canvasRef.current;
    const ctx = ctxRef.current;
    if (!canvas || !ctx) return () => {};

    canvas.width = Math.round(canvas.parentElement!.clientWidth / 2) * 2;
    canvas.height = Math.round(canvas.parentElement!.clientHeight / 2) * 2;

    const bandLength = scaledBands[0].length;
    const automata = [
      { x: 0.35, y: 0, influence: 0 },
      { x: -0.35, y: 0, influence: 1 },
      { x: -0.35, y: -0.55, influence: 2 },
      { x: 0.35, y: -0.55, influence: 3 },
    ];

    let x = 0;
    let y = 0;
    let lastShuffleTime = -1000;
    let wanderX = 0;
    let wanderY = 0;

    const frame = (frameDelta: number) => {
      const time = getCurrentTime();
      const delta = frameDelta;
      const decayPow = delta / FAST_HALF_LIFE;
      const friction = Math.pow(0.05, decayPow);

      const progress = time / duration;
      const index = Math.floor(progress * bandLength);

      scaledBands.forEach((b, i) => {
        if (!energy[i]) energy[i] = 0;
        energy[i] -= delta / 2;
        energy[i] *= Math.pow(0.25, decayPow);
        if (playingRef.current) {
          energy[i] += b[index] * 0.05;
        }
      });

      const wPx = canvas.width;
      const hPx = canvas.height;
      const w = wPx / pitch;
      const h = hPx / pitch;

      const minD = Math.min(wPx, hPx);

      const s = Math.max(energy[0], energy[1], energy[2], energy[3]) - 0.00001;

      if (energy[0] > 2 && lastShuffleTime < time - 0.5) {
        lastShuffleTime = time;
        automata[0].influence = Math.floor(Math.random() * 4);
        automata[1].influence = Math.floor(Math.random() * 4);
        automata[2].influence = Math.floor(Math.random() * 4);
        automata[3].influence = Math.floor(Math.random() * 4);
      }
      // wanderY += Math.sin(energy[0] * 2);
      // wanderX += Math.sin(energy[0] * -2);
      wanderX = mousePosRef.current.x;
      wanderY = mousePosRef.current.y;

      ctx.clearRect(0, 0, wPx, hPx);

      if (s <= 0) return;

      ctx.save();
      ctx.translate(1, pitch + (hPx % pitch) - 1);

      ctx.fillStyle = 'black';
      ctx.lineWidth = 2;

      const spectrum = getSpectrum();
      const highContrastMax = 0.58;
      const highContrastMin = 0.05;
      const contrastFalloff = 0.1;

      for (let y = 0; y < h + 1; y++) {
        let contrast = 1;
        const yp = y / h;
        if (yp > highContrastMax) {
          contrast = 1 - (yp - highContrastMax) / contrastFalloff;
        } else if (yp < highContrastMin) {
          contrast = yp / highContrastMin;
        }

        contrast = Math.round((1 - Math.max(0.2, contrast * 0.75)) * 255);
        ctx.strokeStyle = `rgb(${contrast},${contrast},${contrast})`;
        ctx.beginPath();

        for (let x = 0; x < w; x++) {
          const xpx = x * pitch;
          const ypx = y * pitch;
          const d = Math.max(Math.abs(xpx - wanderX), Math.abs(ypx - wanderY));
          ctx.save();
          // ctx.translate(xpx, ypx);
          // ctx.scale(
          //   1 - Math.min(.5, d/500),
          //   1 - Math.min(.5, d/500)
          // );
          // ctx.translate(-xpx, -ypx);

          const s = getCellValue(x, y, w, h, spectrum, time * (120 / 60), automata);
          let c = Math.min(1, Math.max(0, s));

          if (c > 0.9) {
            crossbox(ctx, x, y, pitch);
          } else if (c > 0.7) {
            box(ctx, x, y, pitch);
          } else if (c > 0.5) {
            cross(ctx, x, y, pitch);
          } else if (c > 0.4) {
            fwdSlash(ctx, x, y, pitch);
          } else if (c > 0.3) {
            bckSlash(ctx, x, y, pitch);
          } else if (c > 0.1) {
            dot(ctx, x, y, pitch);
          }

          ctx.restore();
        }

        ctx.stroke();
      }

      ctx.restore();
    };

    let stopped = false;

    let lastFrameTime = Date.now();
    const frameLoop = (restart?: boolean) => {
      if (!stopped) {
        const now = Date.now();
        frame((now - lastFrameTime) / 1000);
        lastFrameTime = now;
        requestAnimationFrame(() => frameLoop());
      }
    };

    frameLoop(true);

    return () => (stopped = true);
  }, [duration, scaledBands, getCurrentTime]);

  const receiveCanvasRef = useCallback(
    (canvas: HTMLCanvasElement | null) => {
      if (canvas) {
        canvasRef.current = canvas;
        ctxRef.current = canvas.getContext('2d');
      } else {
        canvasRef.current = null;
        ctxRef.current = null;
      }
    },
    [bands, playing]
  );

  useEffect(() => {
    return startPlaying();
  }, [startPlaying]);

  useEffect(() => {
    playingRef.current = playing;
  }, [playing]);

  const wrapperRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const handleMouseMove = (e: MouseEvent) => {
      mousePosRef.current = { x: e.clientX, y: e.clientY };
    };
    window.addEventListener('mousemove', handleMouseMove);
    return () => window.removeEventListener('mousemove', handleMouseMove);
  }, []);

  return (
    <Wrapper style={{ left: xOffset, right: xOffset, bottom: -1 * yOffsetBottom, top: -2 * pitch }} ref={wrapperRef}>
      <Canvas ref={receiveCanvasRef} />
    </Wrapper>
  );
};

export default TrackVisualizer;
