/**
 * Description: This library is able to play soundfonts (sf2 files) with the Web Audio API..
 *
 * @package     midi-Synth
 * @category    module js
 * @author      felixroos <https://github.com/felixroos/sfumato>
 * @copyright   sfumato
 * @license     GPL-2.0-or-later
 */

var bufNode;
var volNode;
var panNode;

function applyOptions(ctx, source, out, options) {
  let { time = ctx.currentTime } = options
  const {
    midi,
    start,
    vel = 0.3,
    vol = 0.3,
    exp = 0,
    pan = 0,
    startLoop,
    endLoop,
    sampleRate,
    originalPitch,
    pitchCorrection,
    type,
    sampleModes = 0,
    overridingRootKey,
    fineTune = 0,
    startloopAddrsOffset = 0,
    startloopAddrsCoarseOffset = 0,
    endloopAddrsOffset = 0,
    endloopAddrsCoarseOffset = 0,
    delayVolEnv = -12000,
    attackVolEnv = -12000,
    holdVolEnv = -12000,
    decayVolEnv = -12000,
    sustainVolEnv = 0,
    releaseVolEnv = -12000,
    ...rest
  } = options
  // console.log('options', options);
  const rootKey =
    overridingRootKey !== undefined && overridingRootKey !== -1
      ? overridingRootKey
      : originalPitch

  const baseDetune = 100 * rootKey + pitchCorrection - fineTune
  const cents = midi * 100 - baseDetune
  const playbackRate = 1.0 * Math.pow(2, cents / 1200)

  source.playbackRate.value = playbackRate
  const loopStart =
    startLoop + startloopAddrsOffset + startloopAddrsCoarseOffset * 32768
  const loopEnd =
    endLoop + endloopAddrsOffset + endloopAddrsCoarseOffset * 32768

  if (loopEnd > loopStart && sampleModes === 1) {
    source.loopStart = loopStart / sampleRate
    source.loopEnd = loopEnd / sampleRate
    source.loop = true
  } else if (sampleModes === 3) {
    console.warn("unimplemented sampleMode 3 (play till end on note off)")
  }
  // type === sampleModes ?
  const unimplemented = Object.keys(rest).filter(
    k => !["name", "instrument", "keyRange", "sampleID", "end"].includes(k)
  )
  if (unimplemented.length) {
    /*     console.warn(
      "unimplemented options:",
      Object.fromEntries(unimplemented.map((key) => [key, rest[key]]))
    ); */
  }
  const volume = ctx.createGain()

  const volEnv = [
    time,
    0,
    tc2s(vel)*vol*exp,
    tc2s(delayVolEnv),
    tc2s(attackVolEnv),
    tc2s(holdVolEnv),
    tc2s(decayVolEnv),
    // When 96 dB of attenuation is reached in the final gain amplifier, an abrupt jump to zero gain (infinite dBof attenuation) occurs. In a 16-bit system, this jump is inaudible.
    sustainVolEnv >= 960 ? 0 : 1 - normalizePermille(sustainVolEnv),
    tc2s(releaseVolEnv)
  ]
  const release = volume.gain.dahdsr(...volEnv)
  const panner = ctx.createStereoPanner()
  panner.pan.value = pan / 1000

  source.connect(volume)
  volume.connect(panner)
  panner.connect(out)

  source.start(time)

  bufNode=source;
  volNode=volume;
  panNode=panner;

  return (end = ctx.currentTime) => {
    source.stop(end + tc2s(releaseVolEnv))
    release(end)
  }
}

function getBufferSourceFromSample(ctx, sample, out, options = {}) {
  const { header, data: int16 } = sample
  const float32 = new Float32Array(int16.length)
  for (let i = 0; i < int16.length; i++) {
    // scale Int16Array between -1 and 1
    float32[i] = int16[i] / 32768
  }
  const buffer = ctx.createBuffer(1, float32.length, header.sampleRate)
  const channelData = buffer.getChannelData(0)
  channelData.set(float32)
  const source = ctx.createBufferSource()
  source.buffer = buffer
  options = { ...header, ...options } // merge sample header and options
  return applyOptions(ctx, source, out, options)
}

function readableGenerators(unreadable) {
  return Object.fromEntries(
    Object.entries(unreadable).map(([key, value]) => {
      const name = generators[key]
      if (["keyRange", "velRange"].includes(name)) {
        return [name, value.range]
      }
      return [name, value.value]
    })
  )
}
const isActiveZone = (zone, midi) =>
  !zone.keyRange || (zone.keyRange.lo <= midi && midi <= zone.keyRange.hi)

const mergeGenerators = (preset, instrument, izone, pzone) => {
  Object.fromEntries(
    Object.entries(generators).map(([index, key]) => [
      key,
      getGeneratorValue(parseInt(index), izone, instrument, pzone, preset)
    ])
  )
}

const getActiveZones = (preset, midi) => {
  // console.log('preset', preset);
  const activeZones = preset.zones
    .filter(pzone => isActiveZone(pzone, midi) && pzone.instrument)
    .map(pzone => {
      return pzone.instrument.zones
        .filter(izone => isActiveZone(izone, midi))
        .map(izone => {
          const mergedGenerators = getGeneratorValues(izone, pzone, preset)
          // console.log('generators', mergedGenerators);
          return {
            ...izone,
            mergedGenerators: mergedGenerators
          }
        })
    })
    .flat()
  // console.log('activeZones', activeZones);
  return activeZones
}

const startPresetNote = (ctx, preset, midi, vel, vol, exp, out, time = ctx.currentTime) => {
  const zones = getActiveZones(preset, midi)
  // console.log('start zones', time, ctx.currentTime);
  const releases = zones.map(zone =>
    getBufferSourceFromSample(ctx, zone.sample, out, {
      ...zone.mergedGenerators,
      midi,
      vel,
      vol,
      exp,
      time,
    })
  )
  return (end = ctx.currentTime) => {
    releases.forEach(release => release(end))
    synth.setAudioNodesSF(bufNode, volNode, panNode)
  }
}
