#pragma once

#include "config.h"
#include "log.h"

#include <libreichwein/file.h>

#include <alsa/asoundlib.h>
#include <fmt/format.h>

#include <iostream>
#include <string>
#include <vector>

using namespace std::string_literals;

class PCM
{
public:
  PCM(): m_phase(1000000)
  {
    // prepare the sample
    std::string data_s = Reichwein::File::getFile("media/click.s16le");
    m_data.resize(data_s.size() / 2); // src is in bytes
    memcpy(m_data.data(), data_s.data(), data_s.size());

    // non-blocking
    if ((err = snd_pcm_open(&handle, device, SND_PCM_STREAM_PLAYBACK, SND_PCM_NONBLOCK)) < 0) {
      throw std::runtime_error(fmt::format("Playback open error: {}", snd_strerror(err)));
    }
    if ((err = snd_pcm_set_params(handle,
                                  SND_PCM_FORMAT_S16_LE,
                                  SND_PCM_ACCESS_RW_INTERLEAVED,
                                  1,
                                  f_sample,
                                  1,
                                  pcm_latency_us)) < 0) {   // latency in us
      throw std::runtime_error(fmt::format("Playback open error: {}", snd_strerror(err)));
    }

    generate();

    npfd = snd_pcm_poll_descriptors_count(handle);
    if (npfd < 0) {
      throw std::runtime_error("snd_pcm_poll_descriptors_count() failed");
    }
    pfd = (struct pollfd *)malloc(npfd * sizeof(struct pollfd));
    if (pfd == nullptr) {
      throw std::runtime_error("alloca() error for PCM");
    }
    if (0 > snd_pcm_poll_descriptors(handle, pfd, npfd))
    {
      throw std::runtime_error("snd_pcm_poll_descriptors() failure");
    }

    if (0 > snd_pcm_start(handle))
    {
     throw std::runtime_error("PCM could not be started");
    }

    if (npfd != 1) {
      std::cout << "Warning: " << std::to_string(npfd) << " poll fds for pcm" << std::endl;
    } else if (fd() <= 2) {
      std::cout << "Warning: Bad PCM fd: " << std::to_string(fd()) << std::endl;
    }
  }

  ~PCM()
  {
    // pass the remaining samples, otherwise they're dropped in close
    err = snd_pcm_drain(handle);
    if (err < 0)
        std::cerr << fmt::format("snd_pcm_drain failed: {}", snd_strerror(err)) << std::endl;
    snd_pcm_close(handle);

    free(pfd);
  }

  void click()
  {
    snd_pcm_sframes_t delay;
    if (0 > snd_pcm_delay(handle, &delay)) {
    }

    m_phase = 0; // - click_latency_frames + delay
  }

  // generate 1 buffer size
  void generate()
  {
    int i;

    for (i = 0; i < nframes; i++) {
      if (m_phase < 0 || m_phase >= m_data.size())
      {
        buffer[i] = 0;
      }
      else
      {
        buffer[i] = m_data[m_phase];
      }
      m_phase++;
    }
  }

  int fd()
  {
    return pfd->fd;
  }

  // write from buffer to ALSA PCM
  void write()
  {
    snd_pcm_sframes_t written = snd_pcm_writei(handle, buffer, nframes);
    if (written < 0) {
        if (written == -EPIPE) {
          std::cout << "Warning: PCM underrun" << std::endl;
        }
        std::cout << "Recovering." << std::endl;
        written = snd_pcm_recover(handle, written, 0);
    }
    if (written < 0) {
        throw std::runtime_error("snd_pcm_writei failed: "s + snd_strerror(written));
    }

    if (written != nframes) {
      std::cout << "Warning: written " << std::to_string(written) << " frames instead of "<< std::to_string(nframes) << std::endl;
    }

    snd_pcm_sframes_t avail;
    snd_pcm_sframes_t delay;
    if (0 > snd_pcm_avail_delay(handle, &avail, &delay)) {
      log_cout << "Error detecting avail and delay" << std::endl;
    } else {
      log_cout << fmt::format("Delay: {}, avail. buffer; {} frames", delay, avail) << std::endl;
    }

    generate();
  }

  bool wait_for_event()
  {
    int result = poll(pfd, npfd, 10000);
    if (result > 0)
    {
      // event
      return true;
    }
    else if (result == 0) {
      // timeout
      return false;
    } else {
      throw std::runtime_error("Poll unsuccessful");
    }
  }

  bool write_available()
  {
    snd_pcm_sframes_t result = snd_pcm_avail(handle);
    if (0 > result) {
      std::cerr << "Error: snd_pcm_avail()" << std::endl;
      //throw std::runtime_error("snd_pcm_avail()");
    }

    return result >= nframes;
  }

private:
  int err;
  snd_pcm_t *handle;

  int npfd;
  struct pollfd* pfd;

  std::vector<uint16_t> m_data;
  int32_t m_phase;
  int16_t buffer[nframes];
};