import { getFrequency, logger, register } from '@strudel/core';
import { getAudioContext } from '@strudel/webaudio';
import csd from './project.csd?raw';
// import livecodeOrc from './livecode.orc?raw';
import presetsOrc from './presets.orc?raw';

let csoundLoader, _csound;

// initializes csound + can be used to reevaluate given instrument code
export async function loadCSound(code = '') {
  await init();
  if (code) {
    code = `${code}`;
    //     ^       ^
    // wrapping in backticks makes sure it works when calling as templated function
    await _csound?.evalCode(code);
  }
}
export const loadcsound = loadCSound;
export const loadCsound = loadCSound;

export const csound = register('csound', (instrument, pat) => {
  instrument = instrument || 'triangle';
  init(); // not async to support csound inside other patterns + to be able to call pattern methods after it
  // TODO: find a alternative way to wait for csound to load (to wait with first time playback)
  return pat.onTrigger((time, hap) => {
    if (!_csound) {
      logger('[csound] not loaded yet', 'warning');
      return;
    }
    hap.ensureObjectValue();
    let { gain = 0.8 } = hap.value;
    gain *= 0.2;

    const freq = Math.round(getFrequency(hap));
    const controls = Object.entries({ ...hap.value, freq })
      .flat()
      .join('/');
    // TODO: find out how to send a precise ctx based time
    // http://www.csounds.com/manual/html/i.html
    const params = [
      `"${instrument}"`, // p1: instrument name
      time - getAudioContext().currentTime, //.toFixed(precision), // p2: starting time in arbitrary unit called beats
      hap.duration + 0, // p3: duration in beats
      // instrument specific params:
      freq, //.toFixed(precision), // p4: frequency
      gain, // p5: gain
      `"${controls}"`, // p6 controls as string (like superdirt osc message)
    ];
    const msg = `i ${params.join(' ')}`;
    _csound.inputMessage(msg);
  });
});

function eventLogger(type, args) {
  const [msg] = args;
  if (
    type === 'message' &&
    (['[commit: HEAD]'].includes(msg) ||
      msg.startsWith('--Csound version') ||
      msg.startsWith('libsndfile') ||
      msg.startsWith('sr =') ||
      msg.startsWith('0dBFS') ||
      msg.startsWith('audio buffered') ||
      msg.startsWith('writing') ||
      msg.startsWith('SECTION 1:'))
  ) {
    // ignore
    return;
  }
  let logType = 'info';
  if (msg.startsWith('error:')) {
    logType = 'error';
  }
  logger(`[csound] ${msg || ''}`, logType);
}

async function load() {
  if (window.__csound__) {
    // Allows using some other csound instance.
    // In that case, the external Csound is responsible
    // for compiling an orchestra and starting to perform.
    logger('[load] Using external Csound', 'warning');
    _csound = window.__csound__;
    return _csound;
  } else {
    const { Csound } = await import('@csound/browser');
    _csound = await Csound({ audioContext: getAudioContext() });
    _csound.removeAllListeners('message');
    ['message'].forEach((k) => _csound.on(k, (...args) => eventLogger(k, args)));
    await _csound.setOption('-m0d'); // see -m flag https://csound.com/docs/manual/CommandFlags.html
    await _csound.setOption('--sample-accurate');
    await _csound.setOption('-odac');
    await _csound.compileCsdText(csd);
    // await _csound.compileOrc(livecodeOrc);
    await _csound.compileOrc(presetsOrc);
    await _csound.start();
    return _csound;
  }
}

async function init() {
  csoundLoader = csoundLoader || load();
  return csoundLoader;
}

let orcCache = {};
export async function loadOrc(url) {
  await init();
  if (typeof url !== 'string') {
    throw new Error('loadOrc: expected url string');
  }
  if (url.startsWith('github:')) {
    const [_, path] = url.split('github:');
    url = `https://raw.githubusercontent.com/${path}`;
  }
  if (!orcCache[url]) {
    orcCache[url] = fetch(url)
      .then((res) => res.text())
      .then((code) => _csound.compileOrc(code));
  }
  await orcCache[url];
}

/**
 * Sends notes to Csound for rendering with MIDI semantics. The hap value is
 * translated to these Csound pfields:
 *
 *  p1 -- Csound instrument either as a number (1-based, can be a fraction),
 *        or as a string name.
 *  p2 -- time in beats (usually seconds) from start of performance.
 *  p3 -- duration in beats (usually seconds).
 *  p4 -- MIDI key number (as a real number, not an integer but in [0, 127].
 *  p5 -- MIDI velocity (as a real number, not an integer but in [0, 127].
 *  p6 -- Strudel controls, as a string.
 */
export const csoundm = register('csoundm', (instrument, pat) => {
  let p1 = instrument;
  if (typeof instrument === 'string') {
    p1 = `"${instrument}"`;
  }
  init(); // not async to support csound inside other patterns + to be able to call pattern methods after it
  return pat.onTrigger((tidal_time, hap) => {
    if (!_csound) {
      logger('[csound] not loaded yet', 'warning');
      return;
    }
    if (typeof hap.value !== 'object') {
      throw new Error('csound only support objects as hap values');
    }
    // Time in seconds counting from now.
    const p2 = tidal_time - getAudioContext().currentTime;
    const p3 = hap.duration.valueOf() + 0;
    const frequency = getFrequency(hap);
    let { gain = 1, velocity = 0.9 } = hap.value;
    velocity = gain * velocity;
    // Translate frequency to MIDI key number _without_ rounding.
    const C4 = 261.62558;
    let octave = Math.log(frequency / C4) / Math.log(2.0) + 8.0;
    const p4 = octave * 12.0 - 36.0;
    // We prefer floating point precision, but over the MIDI range [0, 127].
    const p5 = 127 * velocity;
    // The Strudel controls as a string.
    const p6 = Object.entries({ ...hap.value, frequency })
      .flat()
      .join('/');
    const i_statement = `i ${p1} ${p2} ${p3} ${p4} ${p5} "${p6}"`;
    console.log('[csoundm]:', i_statement);
    _csound.inputMessage(i_statement);
  });
});
