#include "gdpd.hpp"

using namespace godot;

void Gdpd::_register_methods() {
  register_method("get_available_input_devices", &Gdpd::get_available_input_devices);
  register_method("get_available_output_devices", &Gdpd::get_available_output_devices);
  register_method("init_devices", &Gdpd::init_devices);
  register_method("init", &Gdpd::init);
  register_method("streamstart", &Gdpd::streamstart);
  register_method("stop", &Gdpd::stop);
  register_method("openPatch", &Gdpd::openPatch);
  register_method("closePatch", &Gdpd::closePatch);
  register_method("subscribe", &Gdpd::subscribe);
  register_method("has_message", &Gdpd::has_message);
  register_method("get_next", &Gdpd::get_next);
  register_method("start_message", &Gdpd::start_message);
  register_method("add_symbol", &Gdpd::add_symbol);
  register_method("add_float", &Gdpd::add_float);
  register_method("finish_list", &Gdpd::finish_list);
  register_method("computeAudio", &Gdpd::computeAudio);
  register_method("set_volume", &Gdpd::set_volume);
  register_method("set_gui_path", &Gdpd::set_gui_path);
}

int Gdpd::audioCallback(void *outputBuffer, void *inputBuffer,
                        unsigned int nBufferFrames, double streamTime,
                        RtAudioStreamStatus status, void *userData){
  Gdpd* gdpd = static_cast<Gdpd*>(userData);
  gdpd->processAudio(outputBuffer, inputBuffer, nBufferFrames, streamTime,
                     status, userData);
  return 0;
}

Gdpd::Gdpd() : m_vol(0), m_gui_path("") {
  //create message array
  m_messages = new Array();
  m_init=false;
}

void Gdpd::_init() {

}

Gdpd::~Gdpd() {

}

Array Gdpd::get_available_input_devices() {
  Array gdlist;
  for(int d=0; d<m_audio.getDeviceCount(); d++) {
    if(m_audio.getDeviceInfo(d).inputChannels>0) {
      gdlist.push_back(m_audio.getDeviceInfo(d).name.c_str());
    }
  }
  return gdlist;
}

Array Gdpd::get_available_output_devices() {
  Array gdlist;
  for(int d=0; d<m_audio.getDeviceCount(); d++) {
    if(m_audio.getDeviceInfo(d).outputChannels>0) {
      gdlist.push_back(m_audio.getDeviceInfo(d).name.c_str());
    }
  }
  return gdlist;
}


int Gdpd::init_devices(String inputDevice, String outputDevice) {
  std::wstring inpWs = inputDevice.unicode_str();
  std::string inpStr(inpWs.begin(), inpWs.end());
  std::wstring outWs = outputDevice.unicode_str();
  std::string outStr(outWs.begin(), outWs.end());

  for(int d=0; d<m_audio.getDeviceCount(); d++) {
    std::string n = m_audio.getDeviceInfo(d).name;
    if(n.compare(inpStr)==0) {
      m_inputDevice = d;
    }
    if(n.compare(outStr)==0) {
      m_outputDevice = d;
    }
  }

  RtAudio::DeviceInfo inpInfo = m_audio.getDeviceInfo(m_inputDevice);
  RtAudio::DeviceInfo outInfo = m_audio.getDeviceInfo(m_outputDevice);
  m_nbInputs = inpInfo.inputChannels;
  m_nbOutputs = outInfo.outputChannels;
  m_sampleRate = outInfo.preferredSampleRate;
  m_bufferFrames = 128;
  return start();
}

int Gdpd::init(int nbInputs, int nbOutputs, int sampleRate, int bufferSize) {
  m_inputDevice = m_audio.getDefaultInputDevice();
  m_outputDevice = m_audio.getDefaultOutputDevice();
  RtAudio::DeviceInfo inpInfo = m_audio.getDeviceInfo(m_inputDevice);
  RtAudio::DeviceInfo outInfo = m_audio.getDeviceInfo(m_outputDevice);
  m_nbInputs = std::min<int>(nbInputs, inpInfo.inputChannels);
  m_nbOutputs = std::min<int>(nbOutputs, outInfo.outputChannels);
  m_sampleRate = sampleRate;
  m_bufferFrames = std::max<int>(64, bufferSize);
  return start();
}

int Gdpd::start() {
  RtAudio::StreamParameters outParams, inpParams;
  inpParams.deviceId = m_inputDevice;
  inpParams.nChannels = m_nbInputs;
  outParams.deviceId = m_outputDevice;
  outParams.nChannels = m_nbOutputs;
  print("Output channels = "+std::to_string(outParams.nChannels));
  print("Input channels = "+std::to_string(inpParams.nChannels));

  if(!m_pd.init(m_nbInputs, m_nbOutputs, m_sampleRate, true)) {
    print("GDPD : Error starting libpd");
    return 1;
  }

  //libpd_set_verbose(1);

  if (m_gui_path != "") {
    libpd_start_gui((char*)m_gui_path.c_str());
  }

  //load externals
  //earplug~
  earplug_tilde_setup();

  //start dsp
  // m_pd.computeAudio(true);

  //intialize rtaudio
  if(m_audio.getDeviceCount()==0){
    Godot::print("There are no available sound devices.");
  }

  RtAudio::StreamOptions options;
  options.streamName = "gdpd";
  options.flags = RTAUDIO_SCHEDULE_REALTIME;
  if(m_audio.getCurrentApi() != RtAudio::MACOSX_CORE) {
    options.flags |= RTAUDIO_MINIMIZE_LATENCY;
  }
  if (m_nbInputs == 0) {
    try {
      m_audio.openStream(&outParams, NULL, RTAUDIO_FLOAT32,
                         m_sampleRate, &m_bufferFrames, &audioCallback,
                         this, &options);
      // m_audio.startStream();
      // print("Stream started");
    }
    catch(RtAudioError& e) {
      Godot::print(e.getMessage().c_str());
    }
  } else {
    try {
      m_audio.openStream(&outParams, &inpParams, RTAUDIO_FLOAT32,
                         m_sampleRate, &m_bufferFrames, &audioCallback,
                         this, &options);
      // m_audio.startStream();
      // print("Stream started");
    }
    catch(RtAudioError& e) {
      Godot::print(e.getMessage().c_str());
    }
  }

  //create message hook
  m_pd.subscribe("to_gdpd");
  m_pd.setReceiver(this);
  m_init=true;

  print("Initialized");

  return 0;
}

void Gdpd::streamstart() {
  m_audio.startStream();
  print("Stream started");
}

void Gdpd::stop() {
  m_audio.stopStream();
  m_audio.closeStream();
  m_pd.computeAudio(false);
  print("Stopped");

  if (m_gui_path != "") {
    libpd_stop_gui();
  }
}

void Gdpd::processAudio(void *outputBuffer, void *inputBuffer,
                        unsigned int nBufferFrames, double streamTime,
                        RtAudioStreamStatus status, void *userData) {
  int ticks = nBufferFrames / libpd_blocksize();

  m_pd.processFloat(ticks, (float*)inputBuffer, (float*)outputBuffer);

  //volume control on the output
  for(int b=0; b<nBufferFrames*m_nbOutputs; ++b) {
    ((float*)outputBuffer)[b]*=m_vol;
  }
}

int Gdpd::openPatch(godot::String baseStr, godot::String dirStr) {
  std::wstring baseWs = baseStr.unicode_str();
  std::string baseS(baseWs.begin(), baseWs.end());
  std::wstring dirWs = dirStr.unicode_str();
  std::string dirS(dirWs.begin(), dirWs.end());

  pd::Patch p1 = m_pd.openPatch(baseS.c_str(), dirS.c_str());
  if(!p1.isValid()) {
    print("Could not open patch "+baseS);
  }
  else {
    print("Opened patch "+baseS+ " [" + std::to_string(p1.dollarZero()) + "]");
    m_patchsMap[p1.dollarZero()] = p1;
  }

  return p1.dollarZero();
}

void Gdpd::closePatch(int id) {
  if(m_patchsMap.find(id) != m_patchsMap.end()) {
    // print("Closing patch, id: "+ std::to_string(id));
    std::string baseS = m_patchsMap[id].filename();
    m_pd.closePatch(m_patchsMap[id]);
    m_patchsMap.erase(id);
    print("Closed patch "+baseS+ " ["+ std::to_string(id) + "]");
  }
}

void Gdpd::subscribe(String symbStr) {
  std::wstring symbWs = symbStr.unicode_str();
  std::string symbS(symbWs.begin(), symbWs.end());
  m_pd.subscribe(symbS.c_str());
}

bool Gdpd::has_message() {
  //receive new messages
  m_pd.receiveMessages();

  //return if more than one message
  int size = m_messages->size();
  return size>0;
}

Array Gdpd::get_next() {
  Array msg = m_messages->pop_front();
  return msg;
}

int Gdpd::blocksize() {
  int blocksize = libpd_blocksize();
  return blocksize;
}

int Gdpd::start_message(int nbValues) {
  int res = libpd_start_message(nbValues);
  return res;
}

void Gdpd::add_symbol(String symbStr) {
  std::wstring symbWs = symbStr.unicode_str();
  std::string symbS(symbWs.begin(), symbWs.end());
  libpd_add_symbol(symbS.c_str());
}

void Gdpd::add_float(float val) {
  libpd_add_float(val);
}

int Gdpd::finish_list(String destStr) {
  std::wstring destWs = destStr.unicode_str();
  std::string destS(destWs.begin(), destWs.end());
  int res = libpd_finish_list(destS.c_str());
  return res;
}

void Gdpd::print(const std::string& message) {
  Godot::print((std::string("GDPD : ")+message).c_str());
}

void Gdpd::receiveList(const std::string& dest, const pd::List& list) {
  Array gdlist;

  for(int i = 0; i < list.len(); ++i) {
    if(list.isFloat(i)) {
      gdlist.push_back(list.getFloat(i));
    }
    else if(list.isSymbol(i)) {
      String symbStr(list.getSymbol(i).c_str());
      gdlist.push_back(symbStr);
    }
  }

  m_messages->push_back(gdlist);
}

void Gdpd::set_volume(float vol) {
  m_vol=vol;
}

void Gdpd::set_gui_path(godot::String pathStr) {
  std::wstring pathWs = pathStr.unicode_str();
  std::string pathS(pathWs.begin(), pathWs.end());

  m_gui_path = pathS;
}

void Gdpd::computeAudio(bool state) { //[; pd dsp 0/1 (
  m_pd.computeAudio(state);
}
