const getKeyMap = () => new Map();

const ARP_TYPE = ['rising', 'falling', 'bounce'];
const RATE = {
  '1/64': 1 / 64,
  '1/32': 1 / 32,
  '1/16': 1 / 16,
  '1/8': 1 / 8,
  '1/6': 1 / 6,
  '1/4': 1 / 4,
  '1/3': 1 / 3,
  '1/2': 1 / 2,
};
const NOTE = {
  // Length of the note in relation to the rate
  '1/8': 1 / 8,
  '1/6': 1 / 6,
  '1/4': 1 / 4,
  '1/3': 1 / 3,
  '1/2': 1 / 2,
  '5/8': 5 / 8,
  '3/4': 3 / 4,
  '7/8': 7 / 8,
  1: 1,
};
const REPEAT = {
  '1x': 1,
  '2x': 2,
  '3x': 3,
  '4x': 4,
};

function turnOffAllKeys(events, heldKeyVelocities) {
  for (key of heldKeyVelocities.keys()) {
    events.push({
      type: 'NoteOff',
      pitch: key,
    });
  }
}

function pushNoteOn(events, key, velocity) {
  events.push({
    type: 'NoteOn',
    pitch: key,
    velocity: velocity,
  });
}

function playRisingKey(events, heldKeyVelocities, possibleKeys, keyIndex, playCount, repeatVal) {
  if (possibleKeys.length > 0) {
    if (playCount.current >= repeatVal) {
      playCount.current = 0;
      keyIndex.current++;
      if (keyIndex.current >= possibleKeys.length) {
        keyIndex.current = 0;
      }
    }
    playCount.current++;
    const key = possibleKeys[keyIndex.current];
    pushNoteOn(events, key, 1);
  }
}

function playFallingKey(events, heldKeyVelocities, possibleKeys, keyIndex, playCount, repeatVal) {
  if (possibleKeys.length > 0) {
    if (playCount.current >= repeatVal) {
      playCount.current = 0;
      keyIndex.current--;
      if (keyIndex.current < 0) {
        keyIndex.current = possibleKeys.length - 1;
      }
    }
    playCount.current++;
    const key = possibleKeys[keyIndex.current];
    pushNoteOn(events, key, heldKeyVelocities.get(key));
  }
}
function playBounceKey(events, heldKeyVelocities, possibleKeys, keyIndex, isRising, playCount, repeatVal) {
  if (possibleKeys.length > 1) {
    if (playCount.current >= repeatVal) {
      playCount.current = 0;
      if (isRising.current) {
        keyIndex.current++;
        if (keyIndex.current >= possibleKeys.length) {
          keyIndex.current = possibleKeys.length - 2;
          isRising.current = false;
        }
      } else {
        keyIndex.current--;
        if (keyIndex.current < 0) {
          keyIndex.current = 1;
          isRising.current = true;
        }
      }
    }
    playCount.current++;
    const key = possibleKeys[keyIndex.current];
    pushNoteOn(events, key, heldKeyVelocities.get(key));
  } else if (possibleKeys.length === 1) {
    pushNoteOn(events, key, heldKeyVelocities.get(possibleKeys[0]));
  }
}

const Arp = ({ input, output, rate, note, arpType, repeat }, { clock, bpm, playing }) => {
  const repeatVal = ~~repeat[0];
  const lastBeat = useVal(0);
  const keyIndex = useVal(0); // Used for rising, falling, bounce modes
  const isRising = useVal(true); // Used for bounce mode
  const playCount = useVal(0); // Used for counting number of repeat a note was played

  output.events = new Array(bufferSize);

  const heldKeyVelocities = useReturnedConst(getKeyMap);
  const beatsSinceFirstHeldNote = useMonoSignal();
  const currentSampleBeats = useVal(0);
  const previousPlaying = useVal(false);
  let keyPressed = false;

  for (let i = 0; i < input.events.length; i++) {
    const events = input.events[i];
    if (events) {
      for (let j = 0; j < events.length; j++) {
        const event = events[j];
        if (event.type === 'NoteOn') {
          heldKeyVelocities.set(event.pitch, event.velocity);
          if (!playing) keyPressed = true;
        } else if (event.type === 'NoteOff') {
          heldKeyVelocities.delete(event.pitch);
          if (!output.events[i]) {
            output.events[i] = [];
          }
          output.events[i].push(event);
        }
      }
    }

    beatsSinceFirstHeldNote[i] = currentSampleBeats.current;

    if (heldKeyVelocities.size === 0 && currentSampleBeats.current > 0) {
      currentSampleBeats.current = 0;
    } else {
      currentSampleBeats.current += bpm[i] / 60 / sampleRate;
    }
  }

  const effectiveClock = playing ? clock : beatsSinceFirstHeldNote;

  for (let i = 0; i < effectiveClock.length; i++) {
    // Because a beat is actually a quarter note
    const rateVal = rate[i] * 4;
    const noteVal = note[i] * rateVal;

    const currentBeat = effectiveClock[i];
    // Detect when the note should turn off
    if ((keyPressed && i === 0) || currentBeat > lastBeat.current + noteVal) {
      if (!output.events[i]) {
        output.events[i] = [];
      }
      turnOffAllKeys(output.events[i], heldKeyVelocities);
    }
    // Detect when the note should turn on
    if ((keyPressed && i === 0) || currentBeat > lastBeat.current + rateVal) {
      if (!output.events[i]) {
        output.events[i] = [];
      }
      const possibleKeys = Array.from(heldKeyVelocities.keys());
      possibleKeys.sort((a, b) => a - b);
      if (arpType === 'bounce') {
        playBounceKey(output.events[i], heldKeyVelocities, possibleKeys, keyIndex, isRising, playCount, repeatVal);
      } else if (arpType === 'rising') {
        playRisingKey(output.events[i], heldKeyVelocities, possibleKeys, keyIndex, playCount, repeatVal);
      } else if (arpType === 'falling') {
        playFallingKey(output.events[i], heldKeyVelocities, possibleKeys, keyIndex, playCount, repeatVal);
      }
      lastBeat.current = currentBeat;
    }
  }

  if (previousPlaying.current && !playing) {
    for (key of heldKeyVelocities.keys()) {
      output.events[0].push({
        type: 'NoteOff',
        pitch: key,
      });
      heldKeyVelocities.delete(key);
    }
  }

  previousPlaying.current = playing;
};

const topSpacing = 50;
const noteHeight = 35;
const noteYSpacing = 10;
const noteWidth = 100;
const Visualizer = (ctx, { note, rate, arpType, repeat }) => {
  if (rate <= 0) {
    return;
  }
  ctx.fillStyle = gray200;
  const width = ctx.canvas.width;
  const height = ctx.canvas.height;
  const repeatInt = Math.round(repeat);
  ctx.fillRect(0, 0, width, height);

  let x = 10;
  let y = topSpacing + noteHeight / 2;
  let rateMultiplier = rate * 4;
  let fullNoteWidth = noteWidth * rateMultiplier;
  let renderedNoteWidth = Math.max(3.5, fullNoteWidth * note);
  let repeatVal = 1;

  ctx.strokeStyle = blue400;

  // Draw header
  ctx.fillStyle = gray150;
  ctx.fillRect(0, 0, width, topSpacing);
  // Draw quarter note bars
  ctx.strokeStyle = 'rgba(255,255,255,0.4)';
  ctx.lineWidth = 2;
  ctx.font = '30px Arial';
  ctx.fillStyle = 'rgba(255,255,255,0.4)';
  ctx.textAlign = 'center';
  ctx.fillText('♩', 35, 35);
  for (x = 10; x < width; x += noteWidth) {
    ctx.beginPath();
    ctx.moveTo(x, 0);
    ctx.lineTo(x, height);
    ctx.stroke();
  }

  ctx.strokeStyle = 'rgba(255,255,255,0.2)';
  ctx.lineWidth = 1;
  // Draw note bars
  for (x = 10; x < width; x += fullNoteWidth) {
    ctx.beginPath();
    ctx.moveTo(x, topSpacing);
    ctx.lineTo(x, height);
    ctx.stroke();
  }

  ctx.fillStyle = blue400;
  let yPos = 0; // yPos will be used to calculate the grid position

  switch (arpType) {
    case 'falling':
      yPos = 0;
      for (x = 10; x < width; x += fullNoteWidth) {
        y = yPos * (noteHeight + noteYSpacing) + topSpacing + noteYSpacing;
        ctx.fillRect(x + 2, y + 1, renderedNoteWidth - 2, noteHeight - 2);
        if (repeatVal >= repeatInt) {
          yPos += 1;
          repeatVal = 1;
        } else {
          repeatVal += 1;
        }
        if (yPos > 3) yPos = 0;
      }
      break;
    case 'rising':
      yPos = 3;
      for (x = 10; x < width; x += fullNoteWidth) {
        y = yPos * (noteHeight + noteYSpacing) + topSpacing + noteYSpacing;
        ctx.fillRect(x + 2, y + 1, renderedNoteWidth - 2, noteHeight - 2);
        if (repeatVal >= repeatInt) {
          yPos -= 1;
          repeatVal = 1;
        } else {
          repeatVal += 1;
        }
        if (yPos < 0) yPos = 3;
      }
      break;
    case 'bounce':
      yPos = -3;
      for (x = 10; x < width; x += fullNoteWidth) {
        y = Math.abs(yPos) * (noteHeight + noteYSpacing) + topSpacing + noteYSpacing;
        ctx.fillRect(x + 2, y + 1, renderedNoteWidth - 2, noteHeight - 2);
        if (repeatVal >= repeatInt) {
          yPos += 1;
          repeatVal = 1;
        } else {
          repeatVal += 1;
        }
        if (yPos > 2) yPos = -3;
      }
      break;
  }
};

this.constructDevice = () =>
  constructPerBufferDevice({
    implementation: Arp,
    layout: themed(
      DeviceLayoutTheme.Skeuomorphic,
      col([
        canvas('visualizer', Visualizer, 400, 120),
        spacer(10),
        row([
          spacer(10),
          col([
            knob('rate', { showLabel: false, options: Object.values(RATE), curve: Curve.Log2 }),
            spacer(10),
            readout('rate', { options: Object.values(RATE), labels: Object.keys(RATE) }),
            label('Rate'),
          ]),
          spacer(10),
          col([
            knob('note', { showLabel: false, options: Object.values(NOTE) }),
            spacer(10),
            readout('note', { options: Object.values(NOTE), labels: Object.keys(NOTE) }),
            label('Note Length'),
          ]),
          spacer(10),
          col([
            knob('repeat', { showLabel: false, options: Object.values(REPEAT) }),
            spacer(10),
            readout('repeat', { options: Object.values(REPEAT), labels: Object.keys(REPEAT) }),
            label('Repeat'),
          ]),
          spacer(40),
          col([selector('arpType'), spacer(10), box(label('Pattern'), 100, 20)]),
          spacer(10),
        ]),
        spacer(10),
      ])
    ),
    constants: {
      arpType: enumConstant('ARP Type', ARP_TYPE),
    },
    inputs: {
      input: midi('In'),
      rate: numberSetting('Rate', { defaultValue: 1 / 8, min: 1 / 64, max: 1 / 2 }),
      note: numberSetting('Note', { defaultValue: 1, min: 1 / 8, max: 1 }),
      repeat: numberSetting('Times', { defaultValue: 1, min: 1, max: 4 }),
    },
    outputs: {
      output: midi('Out'),
    },
  });
