const beatsPerRotation = 8;
const numberOfRotationalBars = 120;
const numberOfBackgroundBars = 40;

const MeterInputs = ({ white, green, red, whiteLevel, greenLevel, redLevel }) => {
  writeSignalLevel(white, whiteLevel);
  writeSignalLevel(red, redLevel);
  writeSignalLevel(green, greenLevel);
}

const colors = {
  'whiteLevel': 'white',
  'redLevel': 'red',
  'greenLevel': 'green',
};

const Visualizer = (ctx, settings) => {
  if (!ctx.flakes || !ctx.bars) {
    ctx.flakes = [];
    ctx.bars = {};
  }
  const now = Date.now();
  const rotation = ((numberOfRotationalBars * (now / 1000) * (100/60) / beatsPerRotation)) % numberOfRotationalBars;
  const roundedRotation = Math.floor(rotation);
  const w = ctx.canvas.width;
  const h = ctx.canvas.height;
  const minD = Math.min(w, h);
  ctx.fillStyle = gray100;
  ctx.fillRect(0, 0, w, h);

  if (!ctx.bars[roundedRotation]) {
    ctx.bars[roundedRotation] = {};
  }

  ['redLevel', 'greenLevel', 'whiteLevel'].forEach((c, i) => {
    if (!ctx.bars[roundedRotation][c]) {
      ctx.bars[roundedRotation][c] = {};
    }
    ctx.bars[roundedRotation][c].m = Math.pow(settings[c], 2);
    ctx.bars[roundedRotation][c].t = now;
  });

  for (let i = 0; i < numberOfBackgroundBars; i ++) {
    const m = (1 + (Math.sin((i/numberOfBackgroundBars) * 2 * Math.PI + (now / 1000)))) / 4 + 1/4;
    ctx.fillStyle = gray150;
    ctx.fillRect(
      (w * (1/numberOfBackgroundBars) * i) + 1,
      h/2,
      (w * 1/numberOfBackgroundBars) - 2,
      (h/2) * m
    );
    ctx.fillRect(
      (w * (1/numberOfBackgroundBars) * i) + 1,
      h/2 * (1 - m),
      (w * 1/numberOfBackgroundBars) - 2,
      (h/2) * m
    );
  }

  Object.keys(ctx.bars).forEach((r) => {
    ctx.save();
    ctx.translate(w/2, h/2);
    ctx.rotate((Number(r) / numberOfRotationalBars) * Math.PI * 2);
    ctx.translate(-w/2, -h/2);
    Object.keys(ctx.bars[r]).forEach((c, i, l) => {
      const stackDepth = (l.length - i);
      const beamWidth = stackDepth * (minD / 500);
      ctx.fillStyle = colors[c];
      const barHeight = (
        ((minD * ctx.bars[r][c].m) / 4)
        *
        (1 - Math.max(0, Math.min(1, (now - ctx.bars[r][c].t) / 4000)))
      ) + beamWidth;
      if (barHeight >= beamWidth) {
        ctx.beginPath();
        const xOffset = Math.sin(i + (now / 1000)) * (minD / 20);
        const yOffset = Math.cos((i * .5) + (now / 1000)) * (minD / 20);
        roundedRect(
          ctx,
          xOffset + (w/2) - beamWidth,
          yOffset + (
            (minD/2) +
            (stackDepth * minD/8) -
            barHeight / 2
          ),
          beamWidth * 2,
          barHeight,
          beamWidth
        );
        // don't wobble around:
        // roundedRect(
        //   ctx,
        //   (w/2) - beamWidth,
        //   (
        //     (minD/2) +
        //     (stackDepth * minD/8) -
        //     barHeight / 2
        //   ),
        //   beamWidth * 2,
        //   barHeight,
        //   beamWidth
        // );
        ctx.fill();
      }
    });
    ctx.restore();
  });

  if (Math.random() < settings.snow) {
    ctx.flakes.push({
      v: 0.4,
      x: Math.random() * w,
      c: 0,
      y: 0
    });
  }

  ctx.fillStyle = '#fff';

  ctx.flakes.forEach((f, i) => {
    ctx.fillRect(f.x, f.y, 2, 2);
    f.y += f.v;
    f.x += Math.sin(((i * 90) + (f.y * 5) + Date.now()) / 1000) / 8
  });

  for (let i = ctx.flakes.length - 1; i >= 0; i --) {
    if (ctx.flakes[i].y >= h) {
      ctx.flakes.splice(i, 1);
    }
  }
};

this.constructDevice = () => constructPerBufferDevice({
  // no audio processing.
  implementation: MeterInputs,
  inputs: {
    white: stereo('White'),
    green: stereo('Green'),
    red: stereo('Red'),
    snow: zeroOneSetting('Snowiness', 0.1)
  },
  sharedArrays: {
    whiteLevel: 1,
    greenLevel: 1,
    redLevel: 1,
  },
  layout: row([
    titled.knob('snow'),
    pinnable(canvas(
      'main',
      Visualizer,
      200,
      200
    ))
  ])
});
