#include "MIDIPlayer.h"

#include "config.h"

#include <signal.h>
#include <fmt/format.h>

#include <algorithm>
#include <chrono>
#include <iostream>
#include <string>
#include <thread>
#include <unordered_set>
#include <stdexcept>

namespace bp = boost::process;
namespace fs = std::filesystem;

using namespace std::chrono_literals;

namespace {
  std::unordered_set<std::string> supported_devices{"AudioBox 22 VSL", "CH345", "M2"};
}

MIDIPlayer::MIDIPlayer(Config& config):
  m_config(config),
  m_child{},
  m_dir{m_config.get_file_path()},
  m_file{}
{
  std::vector<std::string> list = get_filelist();

  if (list.size() > 0) {
    m_file = list[0];
  }

  init_seq();
  iterate_ports();
  close_seq();
}

void MIDIPlayer::start()
{
  if (m_child.valid() && m_child.running()) {
    stop();
  } else {
    m_child = bp::child(fmt::format("aplaymidi-mp -c -p{} \"{}\"", m_client, m_file).c_str());//, bp::std_out > bp::null);
    if (!m_child.valid() || !m_child.running()) {
      throw std::runtime_error("aplaymidi not started");
    }
  }
}

void MIDIPlayer::stop()
{
  // note:: m_child.terminate() would kill via SIGKILL, preventing note offs

  if (m_child.valid()) {
    int result = kill(m_child.native_handle(), SIGTERM);
    if (result < 0) {
      std::cerr << "Error in MIDIPlayer::stop(): kill() unsuccessful\n";
    }
  }
}

bool MIDIPlayer::is_playing()
{
  if (!m_child.valid()) {
    return false;
  }
  return m_child.running();
}

void MIDIPlayer::set_file(const std::string& filename)
{
  m_file = filename;

  if (is_playing()) {
    stop();
    std::this_thread::sleep_for(100ms);
    start();
  }
}

std::string MIDIPlayer::get_file()
{
  return m_file;
}

std::vector<std::string> MIDIPlayer::get_filelist()
{
  std::vector<std::string> result;
  for (auto const& dir_entry: fs::directory_iterator{m_dir}) {
    fs::path entry{dir_entry.path()};
    fs::path extension = entry.extension();
    if (extension == ".midi" || extension == ".mid") {
      result.push_back(entry.filename());
    }
    if (result.size() == 99) {
      break;
    }
  }
  std::sort(result.begin(), result.end());
  return result;
}

void MIDIPlayer::init_seq(void)
{
  /* open sequencer */
  int err = snd_seq_open(&m_seq, "default", SND_SEQ_OPEN_DUPLEX, 0);
  if (err < 0)
  {
    throw std::runtime_error("snd_seq_open()");
  }
}

void MIDIPlayer::close_seq(void)
{
  int err = snd_seq_close(m_seq);
  if (err < 0)
  {
    throw std::runtime_error("snd_seq_close()");
  }
}

void MIDIPlayer::iterate_ports(void)
{
  bool found{};

  snd_seq_client_info_t *cinfo;
  snd_seq_port_info_t *pinfo;

  snd_seq_client_info_alloca(&cinfo);
  snd_seq_port_info_alloca(&pinfo);

  snd_seq_client_info_set_client(cinfo, -1);
  while (snd_seq_query_next_client(m_seq, cinfo) >= 0) {
    int client = snd_seq_client_info_get_client(cinfo);

    snd_seq_port_info_set_client(pinfo, client);
    snd_seq_port_info_set_port(pinfo, -1);
    while (snd_seq_query_next_port(m_seq, pinfo) >= 0) {
      /* port must understand MIDI messages */
      if (!(snd_seq_port_info_get_type(pinfo)
            & SND_SEQ_PORT_TYPE_MIDI_GENERIC))
              continue;
      /* we need both WRITE and SUBS_WRITE */
      if ((snd_seq_port_info_get_capability(pinfo)
           & (SND_SEQ_PORT_CAP_WRITE | SND_SEQ_PORT_CAP_SUBS_WRITE))
          != (SND_SEQ_PORT_CAP_WRITE | SND_SEQ_PORT_CAP_SUBS_WRITE))
              continue;
      if (supported_devices.contains(std::string{snd_seq_client_info_get_name(cinfo)})) {
        m_client = snd_seq_port_info_get_client(pinfo);
        found = true;
      }
      //std::cout << fmt::format("DEBUG: {}", get_midi_port()) << std::endl;
      //printf("%3d:%-3d  %-32.32s %s\n",
      //       snd_seq_port_info_get_client(pinfo),
      //       snd_seq_port_info_get_port(pinfo),
      //       snd_seq_client_info_get_name(cinfo),
      //       snd_seq_port_info_get_name(pinfo));
    }
  }

  if (!found) {
    throw std::runtime_error("No MIDI device found");
  }
}

int MIDIPlayer::get_midi_port()
{
  return m_client;
}