const interpolatedRead = (buffer, index) => {
  if (buffer.length <= 1) {
    return 0;
  }
  const prev = Math.max(0, Math.floor(index));
  const next = Math.min(buffer.length - 1, prev + 1);
  const amountOfNextSample = index - prev;
  const amountOfPrevSample = 1 - amountOfNextSample;
  return buffer[prev] * amountOfPrevSample + buffer[next] * amountOfNextSample;
};

const SampleVoice = (
  { velocity, trigger, gate }, 
  { attack, decay, sustain, release, pitch, gain, pan, output, buffer },
) => {

  const processEnvelope = useEnvelope();
  const amplitude = processEnvelope(trigger, gate, attack, decay, sustain, release);

  multiply(amplitude, velocity, amplitude);
  multiply(amplitude, gain, amplitude);

  if (allEqual(amplitude, 0)) {
    return;
  }
  const readIndex = useVal(0);
  const isPlaying = useVal(false);

  const voiceOutput = useStereoSignal();
  const leftBuffer = buffer.channelDataList[0];

  const panLeft = useMonoSignal();
  const panRight = useMonoSignal();
  multiply(pan, useConstantBuffer(-1), panLeft);
  
  add(panLeft, useConstantBuffer(1), panLeft);
  clamp(panLeft, panLeft, 0, 1);
  
  add(pan, useConstantBuffer(1), panRight);
  clamp(panRight, panRight, 0, 1);
  
  multiply(panLeft, amplitude, panLeft);
  multiply(panRight, amplitude, panRight);

  if (leftBuffer.length <= 1) return;

  const rightBuffer = buffer.channelDataList.length === 1 ? leftBuffer : buffer.channelDataList[1];

  voiceOutput[0].fill(0);
  voiceOutput[1].fill(0);

  for (let i = 0; i < output[0].length; i++) {
    if (trigger[i] === 1) {
      isPlaying.current = true;
      readIndex.current = 0;
    } else if (amplitude[i] === 0) {
      isPlaying.current = false;
    }

    if (isPlaying.current) {
      if (readIndex.current < (leftBuffer.length - 1)) {
        voiceOutput[0][i] += interpolatedRead(leftBuffer, readIndex.current);
        voiceOutput[1][i] += interpolatedRead(rightBuffer, readIndex.current);
        readIndex.current += transposeSemitones(1, pitch[i]);
      }
    }
  }

  multiply(voiceOutput[0], panLeft, voiceOutput[0]);
  multiply(voiceOutput[1], panRight, voiceOutput[1]);
  add(output[0], voiceOutput[0], output[0]);
  add(output[1], voiceOutput[1], output[1]);
};

const getSampleData = (scope) => {
  const results = [];
  for (let i = 0; i < 12; i++) {
    const pitches = [i + 36];
    results.push({
      pitches,
      scope: {
        attack: scope[`attack${i + 1}`],
        decay: scope[`decay${i + 1}`],
        sustain: scope[`sustain${i + 1}`],
        release: scope[`release${i + 1}`],
        pitch: scope[`pitch${i + 1}`],
        gain: scope[`gain${i + 1}`],
        pan: scope[`pan${i + 1}`],

        buffer: scope[`sample${i + 1}`],
        output: scope.output,
      },
    });
  }
  return results;
};

const SamplerInstrument = (scope, globals) => {
  scope.output[0].fill(0);
  scope.output[1].fill(0);
  const sampleData = useReturnedConst(getSampleData, scope);

  for (let i = 0; i < sampleData.length; i++) {
    const data = sampleData[i];
    const voice = useSingleVoice(scope.input, data.pitches, 1);
    const exit = enterChildScope();
    SampleVoice(voice, data.scope);
    exit();
  }

  multiply(scope.output[0], scope.gain, scope.output[0]);
  multiply(scope.output[1], scope.gain, scope.output[1]);
};

const sampleCol = (num) => {
  const content = (color) => {
    return col([
      constantSetter('focusedSample', num, box(
        audioBufferPicker(`sample${num}`),
        100,
        100,
        color,
        4
      ))
    ])
  }
  return conditional(
    'focusedSample', 
    { [num]: content(gray250), _other: content(gray125) }
  );
};

const sampleSetting = (id, text) => box(
  col([
    knob(id),
    spacer(5),
    label(text)
  ]),
  50,
  70
);

const sampleSettings = (num) => col([
  spacer(10),
  row([
    box(
      row([
        sampleSetting(`pitch${num}`, 'Pitch'),
        spacer(20),
        sampleSetting(`attack${num}`, 'Attack'),
        sampleSetting(`decay${num}`, 'Decay'),
        sampleSetting(`sustain${num}`, 'Sustain'),
        sampleSetting(`release${num}`, 'Release'),
        spacer(20),
        sampleSetting(`pan${num}`, 'Pan'),
        sampleSetting(`gain${num}`, 'Gain'),
      ]),
      440,
      90,
      gray200,
      8
    ),
    spacer(100),
    sampleSetting('gain', 'Master')
  ])
]);

const sampleSettingsPicker = () => {
  const conditions = {};
  for (let i = 1; i <= 12; i ++) {
    conditions[i] = sampleSettings(i);
  }
  return conditional('focusedSample', conditions);
}

this.constructDevice = () =>
  constructPerBufferDevice({
    noteInputHint: {
      interface: NoteInterfaceType.Pads,
      usedPitches: {
        48: 'Sample 1',
        49: 'Sample 2',
        50: 'Sample 3',
        51: 'Sample 4',

        52: 'Sample 5',
        53: 'Sample 6',
        54: 'Sample 7',
        55: 'Sample 8',

        56: 'Sample 9',
        57: 'Sample 10',
        58: 'Sample 11',
        59: 'Sample 12',
      },
    },
    implementation: SamplerInstrument,
    inputs: {
      input: midi('In'),
      gain: dbSetting('Gain', { defaultValue: -6 }),
      ...new Array(12).fill(0).reduce(
        (a, c, i) => ({
          [`attack${i + 1}`]: adsrTimeSetting(`Attack ${i + 1}`, 1),
          [`decay${i + 1}`]: adsrTimeSetting(`Decay ${i + 1}`, 100),
          [`sustain${i + 1}`]: zeroOneSetting(`Sustain ${i + 1}`, 1),
          [`release${i + 1}`]: adsrTimeSetting(`Release ${i + 1}`, 50),
          [`gain${i + 1}`]: dbSetting(`Gain ${i + 1}`, 0),
          [`pan${i + 1}`]: numberSetting(`Pan ${i + 1}`, { defaultValue: 0, min: -1, max: 1 }),
          [`pitch${i + 1}`]: numberSetting(`Pitch ${i + 1}`, { defaultValue: 0, min: -24, max: 24 }),
          ...a,
        }),
        {}
      ),
    },
    outputs: {
      output: stereo('Out'),
    },
    constants: {
      sample1: audioBufferConstant('Sample 1'),
      sample2: audioBufferConstant('Sample 2'),
      sample3: audioBufferConstant('Sample 3'),
      sample4: audioBufferConstant('Sample 4'),

      sample5: audioBufferConstant('Sample 5'),
      sample6: audioBufferConstant('Sample 6'),
      sample7: audioBufferConstant('Sample 7'),
      sample8: audioBufferConstant('Sample 8'),

      sample9: audioBufferConstant('Sample 9'),
      sample10: audioBufferConstant('Sample 10'),
      sample11: audioBufferConstant('Sample 11'),
      sample12: audioBufferConstant('Sample 12'),

      focusedSample: enumConstant('Focused Sample', new Array(12).fill(0).map((_, i) => i), 'LFO 1'),
    },
    layout: col([
      row([
        sampleCol(7),
        spacer(8),
        sampleCol(8),
        spacer(8),
        sampleCol(9),
        spacer(8),
        sampleCol(10),
        spacer(8),
        sampleCol(11),
        spacer(8),
        sampleCol(12),
      ]),
      spacer(10),
      row([
        sampleCol(1),
        spacer(8),
        sampleCol(2),
        spacer(8),
        sampleCol(3),
        spacer(8),
        sampleCol(4),
        spacer(8),
        sampleCol(5),
        spacer(8),
        sampleCol(6),
      ]),
      spacer(10),
      sampleSettingsPicker()
    ]),
    presetTags: {
      'Instrument': [
        'Drums',
        'FX',
        'Other'
      ],
      'Genre': [
        'Ambient',
        'Chill',
        'Dubstep',
        'EDM',
        'Funk',
        'Hip Hop',
        'House',
        'Jazz',
        'Lo-Fi',
        'Soundtrack',
        'Metal',
        'Pop',
        'R&B',
        'Rock',
        'Techno',
        'Trance',
      ]
    },
  });
