let u8;
let wasm;
let pptr;
let bptr;
let pptrs;
let bptrs;

{
  const module = new WebAssembly.Module(await import('fs/promises').then(fs => fs.readFile(new URL('../../src/opus/opus.wasm', import.meta.url))));
  const instance = new WebAssembly.Instance(module, {
    wasi_snapshot_preview1: { fd_seek() { }, fd_write() { }, fd_close() { }, proc_exit() { } },

    env: {
      emscripten_notify_memory_growth() {
        u8 = new Uint8Array(wasm.memory.buffer);
        pptrs = u8.subarray(pptr, pptr + 2 ** 13);
        bptrs = u8.subarray(bptr, bptr + 2 ** 15);
      },
    },
  });

  wasm = instance.exports;
  pptr = wasm.malloc(2 ** 13);
  bptr = wasm.malloc(2 ** 15);
  u8 = new Uint8Array(wasm.memory.buffer);
  pptrs = u8.subarray(pptr, pptr + 2 ** 13);
  bptrs = u8.subarray(bptr, bptr + 2 ** 15);
}

function clamp(min, int, max) { const t = int < min ? min : int; return t > max ? max : t; }
function err(code) { if (0 > code) throw new Error(`opus: ${load_static_string(u8, wasm.opus_strerror(code))}`); else return code; }
function cgc(f) { return !('FinalizationRegistry' in globalThis) ? { delete(_) { }, add(_, __) { } } : { r: new FinalizationRegistry(f), delete(k) { this.r.unregister(k); }, add(k, v) { this.r.register(k, v, k); } }; }

function load_static_string(u8, ptr) {
  let s = '';
  const l = u8.length | 0;

  for (let o = ptr | 0; o < l; o++) {
    const x = u8[o];
    if (0 === x) break;
    s += String.fromCharCode(x);
  }

  return s;
}

export const ctl = {
  auto: -1000,
  bitrate_max: -1,
  reset_state: 4028,

  signal: {
    auto: -1000, // default
    voice: 3001, // best for voice
    music: 3002, // best for audio/music
  },

  application: {
    voip: 2048, // best for voice
    audio: 2049, // best for audio/music
    restricted_lowdelay: 2051, // best for low coding delay
  },

  bandwidth: {
    auto: -1000,
    wideband: 1103, // 8 KHz
    fullband: 1105, // 20 KHz
    narrowband: 1101, // 4 KHz
    mediumband: 1102, // 6 KHz
    superwideband: 1104, // 12 KHz
  },

  framesize: {
    5: 5002, // 5ms frames
    10: 5003, // 10ms frames
    20: 5004, // 20ms frames
    40: 5005, // 40ms frames
    60: 5006, // 60ms frames
    80: 5007, // 80ms frames
    100: 5008, // 100ms frames
    120: 5009, // 120ms frames
    [2.5]: 5001, // 2.5ms frames
    arg: 5000, // encode argument
  },

  set: {
    vbr: 4006,
    dtx: 4016,
    gain: 4034,
    signal: 4024,
    bitrate: 4002,
    bandwidth: 4008,
    lsb_depth: 4036,
    complexity: 4010,
    inband_fec: 4012,
    application: 4000,
    max_bandwidth: 4004,
    vbr_constraint: 4020,
    force_channels: 4022,
    packet_loss_perc: 4014,
    prediction_disabled: 4042,
    expert_frame_duration: 4040,
    phase_inversion_disabled: 4046,
  },

  get: {
    vbr: 4007,
    dtx: 4017,
    gain: 4045,
    pitch: 4033,
    signal: 4025,
    in_dtx: 4049,
    bitrate: 4003,
    lsb_depth: 4037,
    bandwidth: 4009,
    lookahead: 4027,
    complexity: 4011,
    inband_fec: 4013,
    application: 4001,
    sample_rate: 4029,
    final_range: 4031,
    max_bandwidth: 4005,
    vbr_constraint: 4021,
    force_channels: 4023,
    packet_loss_perc: 4015,
    prediction_disabled: 4043,
    last_packet_duration: 4039,
    expert_frame_duration: 4041,
    phase_inversion_disabled: 4047,
  },
};

const convert = {
  fc: {
    1: 1,
    2: 2,
    'auto': [ctl.auto],
    [ctl.auto]: 'auto',
  },

  s: {
    [ctl.auto]: 'auto',
    [ctl.signal.voice]: 'voice',
    [ctl.signal.music]: 'music',
  },

  a: {
    [ctl.application.voip]: 'voip',
    [ctl.application.audio]: 'audio',
    [ctl.application.restricted_lowdelay]: 'restricted_lowdelay',
  },

  b: {
    [ctl.auto]: 'auto',
    [ctl.bandwidth.wideband]: 'wideband',
    [ctl.bandwidth.fullband]: 'fullband',
    [ctl.bandwidth.narrowband]: 'narrowband',
    [ctl.bandwidth.mediumband]: 'mediumband',
    [ctl.bandwidth.superwideband]: 'superwideband',
  },

  efd: {
    [ctl.framesize[5]]: 5,
    [ctl.framesize[10]]: 10,
    [ctl.framesize[20]]: 20,
    [ctl.framesize[40]]: 40,
    [ctl.framesize[60]]: 60,
    [ctl.framesize[80]]: 80,
    [ctl.framesize[2.5]]: 2.5,
    [ctl.framesize[100]]: 100,
    [ctl.framesize[120]]: 120,
    [ctl.framesize.arg]: 'arg',
  },
}

const pptrl = 2 ** 13;
const gc = cgc(ptr => wasm.free(ptr));

class RawDecoder {
  #ptr = 0;
  constructor({ channels, sample_rate }) {
    gc.add(this, this.#ptr = wasm.malloc(wasm.opus_decoder_get_size(this.channels = channels || 2)));
    try { err(wasm.opus_decoder_init(this.#ptr, sample_rate || 48000, this.channels)); } catch (e) { throw (this.drop(), e); }
  }

  drop() { if (this.#ptr) (gc.delete(this), wasm.free(this.#ptr), this.#ptr = 0); }
  ctl(cmd, arg) { if (arg == null) return wasm.opus_decoder_ctl_get(this.#ptr, cmd); else return err(wasm.opus_decoder_ctl_set(this.#ptr, cmd, arg)); }

  decode(buffer) {
    pptrs.set(buffer = new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength));
    return bptrs.slice(0, 2 * this.channels * err(wasm.opus_decode(this.#ptr, pptr, buffer.length, bptr, 5760, 0)));
  }
}

class RawEncoder {
  #ptr = 0;
  constructor({ channels, application, sample_rate }) {
    this.channels = channels || 2;
    gc.add(this, this.#ptr = wasm.malloc(wasm.opus_encoder_get_size(this.channels)));
    application = ({ voip: 2048, audio: 2049, restricted_lowdelay: 2051 })[application || 'audio'];
    try { err(wasm.opus_encoder_init(this.#ptr, sample_rate || 48000, this.channels, application)); } catch (e) { throw (this.drop(), e); }
  }

  drop() { if (this.#ptr) (gc.delete(this), wasm.free(this.#ptr), this.#ptr = 0); }
  ctl(cmd, arg) { if (arg == null) return wasm.opus_encoder_ctl_get(this.#ptr, cmd); else return err(wasm.opus_encoder_ctl_set(this.#ptr, cmd, arg)); }

  encode(buffer) {
    bptrs.set(buffer = new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength));
    return pptrs.slice(0, err(wasm.opus_encode(this.#ptr, bptr, buffer.length / 2 / this.channels, pptr, pptrl)));
  }
}

export class Decoder extends RawDecoder {
  constructor({ channels = 2, sample_rate = 48000 } = {}) {
    super({ channels, sample_rate });

    this.channels = channels;
  }

  // https://www.opus-codec.org/docs/opus_api-1.3.1/group__opus__decoderctls.html
  get gain() { return this.ctl(ctl.get.gain); }
  set gain(int) { this.ctl(ctl.set.gain, clamp(-32768, int, 32767)); }
  get pitch() { const x = this.ctl(ctl.get.pitch); return 0 === x ? null : x }
  get last_packet_duration() { return this.ctl(ctl.get.last_packet_duration); }

  // https://www.opus-codec.org/docs/opus_api-1.3.1/group__opus__genericctls.html
  reset() { this.ctl(ctl.reset_state); }
  get in_dtx() { return !!this.ctl(ctl.get.in_dtx); }
  get sample_rate() { return this.ctl(ctl.get.sample_rate); }
  get bandwidth() { return convert.b[this.ctl(ctl.get.bandwidth)]; }
  get phase_inversion_disabled() { return !!this.ctl(ctl.get.phase_inversion_disabled); }
  set phase_inversion_disabled(bool) { this.ctl(ctl.set.phase_inversion_disabled, bool ? 1 : 0); }
}

export class Encoder extends RawEncoder {
  constructor({ channels = 2, application, sample_rate = 48000 } = {}) {
    super({ channels, sample_rate, application: application || 'audio' });

    this.channels = channels;
  }

  async *encode_pcm_stream(size, iter) {
    let needle = 0;
    const self = this;
    const r = 2 * size * this.channels;

    const t8 = new Uint8Array(r);

    function* consume(buf) {
      let offset = 0;

      while (offset < buf.length) {
        const s = buf.subarray(offset, offset += r);

        if (r === s.length) yield self.encode(s);
        else {
          t8.set(s, needle);
          needle += s.length;

          break;
        };
      }
    };

    for await (const buffer of iter) {
      const b8 = new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);

      if (0 === needle) for (const x of consume(b8)) yield x;
      else {
        const rr = r - needle;
        if (rr > b8.length) (t8.set(b8, needle), needle += b8.length);
        else {
          t8.set(b8.subarray(0, rr), needle);
          yield (needle = 0, self.encode(t8));
          for (const x of consume(b8.subarray(rr))) yield x;
        }
      }
    }
  }

  // https://www.opus-codec.org/docs/opus_api-1.3.1/group__opus__genericctls.html
  reset() { this.ctl(ctl.reset_state); }
  get in_dtx() { return !!this.ctl(ctl.get.in_dtx); }
  get sample_rate() { return this.ctl(ctl.get.sample_rate); }
  get bandwidth() { return convert.b[this.ctl(ctl.get.bandwidth)]; }
  get phase_inversion_disabled() { return !!this.ctl(ctl.get.phase_inversion_disabled); }
  set phase_inversion_disabled(bool) { this.ctl(ctl.set.phase_inversion_disabled, bool ? 1 : 0); }

  // https://www.opus-codec.org/docs/opus_api-1.3.1/group__opus__encoderctls.html
  get vbr() { return !!this.ctl(ctl.get.vbr); }
  get dtx() { return !!this.ctl(ctl.get.dtx); }
  get bitrate() { return this.ctl(ctl.get.bitrate); }
  get lookahead() { return this.ctl(ctl.get.lookahead); }
  get lsb_depth() { return this.ctl(ctl.get.lsb_depth); }
  get complexity() { return this.ctl(ctl.get.complexity); }
  get inband_fec() { return !!this.ctl(ctl.get.inband_fec); }
  get signal() { return convert.s[this.ctl(ctl.get.signal)]; }
  get packet_loss() { return this.ctl(ctl.get.packet_loss_perc); }
  get vbr_constraint() { return !!this.ctl(ctl.get.vbr_constraint); }
  get application() { return convert.a[this.ctl(ctl.get.application)]; }
  get max_bandwidth() { return convert.b[this.ctl(ctl.get.max_bandwidth)]; }
  get force_channels() { return convert.fc[this.ctl(ctl.get.force_channels)]; }
  get prediction_disabled() { return !!this.ctl(ctl.get.prediction_disabled); }
  get expert_frame_duration() { return covert.efd[this.ctl(ctl.get.expert_frame_duration)]; }

  set vbr(bool) { this.ctl(ctl.set.vbr, bool ? 1 : 0); }
  set dtx(bool) { this.ctl(ctl.set.dtx, bool ? 1 : 0); }
  set signal(arg) { this.ctl(ctl.set.signal, ctl.signal[arg]); }
  set inband_fec(bool) { this.ctl(ctl.set.inband_fec, bool ? 1 : 0); }
  set lsb_depth(int) { this.ctl(ctl.set.lsb_depth, clamp(8, int, 24)); }
  set bandwidth(arg) { this.ctl(ctl.set.bandwidth, ctl.bandwidth[arg]); }
  set complexity(int) { this.ctl(ctl.set.complexity, clamp(0, int, 10)); }
  set vbr_constraint(bool) { this.ctl(ctl.set.vbr_constraint, bool ? 1 : 0); }
  set application(arg) { this.ctl(ctl.set.application, ctl.application[arg]); }
  set force_channels(arg) { this.ctl(ctl.set.force_channels, convert.fc[arg]); }
  set packet_loss(int) { this.ctl(ctl.set.packet_loss_perc, clamp(0, int, 100)); }
  set prediction_disabled(bool) { this.ctl(ctl.set.prediction_disabled, bool ? 1 : 0); }
  set expert_frame_duration(arg) { this.ctl(ctl.set.expert_frame_duration, ctl.framesize[arg]) };
  set max_bandwidth(arg) { this.ctl(ctl.set.max_bandwidth, ctl.bandwidth[arg === 'auto' ? 'fullband' : arg]); }
  set bitrate(arg) { this.ctl(ctl.set.bitrate, arg === 'max' ? ctl.bitrate_max : (arg === 'auto' ? ctl.auto : clamp(500, arg | 0, 512000))); }
}