#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <stdint.h>
#include <erl_nif.h>
#include <alsa/asoundlib.h>
#ifdef __AVX__
#include <immintrin.h>
#endif

#define INT24_MAX_I  8388607
#define INT24_MIN_I  -8388607
#define INT24_MAX_F  8388607.0f
#define INT24_MIN_F  -8388607.0f

#define INT16_MAX_I  32767
#define INT16_MIN_I  -32767
#define INT16_MAX_F  32767.0f
#define INT16_MIN_F  -32767.0f

#define FRAME_TYPE float
#define FRAME_SIZE sizeof(FRAME_TYPE)

static const snd_pcm_format_t formats[] = {
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
  SND_PCM_FORMAT_FLOAT_LE,
  SND_PCM_FORMAT_S32_LE,
  SND_PCM_FORMAT_S24_LE,
  SND_PCM_FORMAT_S16_LE
#else
  SND_PCM_FORMAT_FLOAT_BE,
  SND_PCM_FORMAT_S32_BE,
  SND_PCM_FORMAT_S24_BE,
  SND_PCM_FORMAT_S16_BE
#endif
};

#define NUMFORMATS (sizeof(formats) / sizeof(formats[0]))

typedef struct
{
  snd_pcm_t * handle;
  unsigned int format_index;
  snd_pcm_access_t access;
  unsigned int period_size;
  unsigned int channels;
  FRAME_TYPE** channel_bufs;
  int fd;
} xalsa_t;

static ErlNifResourceType* res_type;

static int set_hwparams(xalsa_t * unit,
                        unsigned int *rate, unsigned int *channels, unsigned int n,
                        snd_pcm_uframes_t *buffer_size,
                        snd_pcm_uframes_t *period_size){
  int err;
  /*
     rate     period_size
     44100    256
     48000    256
     96000    512
     192000   1024
  */
  snd_pcm_hw_params_t *params;
  snd_pcm_t *handle = unit->handle;
  snd_pcm_hw_params_malloc(&params);
  err = snd_pcm_hw_params_any(handle, params);
  if (err < 0) {
    printf("Broken configuration for playback: no configurations available: %s\n",
	   snd_strerror(err));
    return err;
  }

  /* set hardware resampling */
  err = snd_pcm_hw_params_set_rate_resample(handle, params, 1);
  if (err < 0) {
    printf("Resampling setup failed for playback: %s\n", snd_strerror(err));
    return err;
  }
  /* set the interleaved read/write format */
  unit->access = SND_PCM_ACCESS_MMAP_NONINTERLEAVED;
  err = snd_pcm_hw_params_set_access(handle, params, unit->access);
  if (err < 0) {
    unit->access = SND_PCM_ACCESS_MMAP_INTERLEAVED;
    err = snd_pcm_hw_params_set_access(handle, params, unit->access);
    if (err < 0) {
      printf("Access type not available for playback: %s\n", snd_strerror(err));
      return err;
    }
    printf("Interleaved access for playback\n");
  }
  /* set the sample format */
  unsigned int format_index = 0;
  while((err = snd_pcm_hw_params_set_format(handle, params, formats[format_index]) < 0) &&
        format_index < NUMFORMATS) {
    format_index++;
  }
  if (err < 0) {
    printf("Sample format not available for playback: %s\n", snd_strerror(err));
    return err;
  }
  printf("Sample format selected for playback: %i\n", format_index);
  unit->format_index = format_index;

  /* set the count of channels */
  err = snd_pcm_hw_params_set_channels_near(handle, params, channels);
  if (err < 0) {
    printf("Channels count (%i) not available for playbacks: %s\n", *channels, snd_strerror(err));
    return err;
  }
  unit->channels = *channels;

  /* set the stream rate */
  err = snd_pcm_hw_params_set_rate_near(handle, params, rate, 0);
  if (err < 0) {
    printf("Rate %iHz not available for playback: %s\n", *rate, snd_strerror(err));
    return err;
  }

  err = snd_pcm_hw_params_set_period_size_near(handle, params, period_size, 0);
  if (err < 0) {
    printf("Unable to set period size for playback: %s\n", snd_strerror(err));
    return err;
  }
  unit->period_size = *period_size;

  *buffer_size = n * (*period_size);
  err = snd_pcm_hw_params_set_buffer_size_near(handle, params, buffer_size);
  if (err < 0) {
    printf("Unable to set buffer size for playback: %s\r\n", snd_strerror(err));
    return err;
  }

  /* write the parameters to device */
  err = snd_pcm_hw_params(handle, params);
  snd_pcm_hw_params_free(params);
  if (err < 0) {
    printf("Unable to set hw params for playback: %s\n", snd_strerror(err));
    return err;
  }
  /* Alloc channel buffers */
  unit->channel_bufs = enif_alloc(sizeof(FRAME_TYPE*) * unit->channels);
  if(unit->channel_bufs == NULL) return -1;
  for(unsigned int channel = 0; channel < unit->channels; channel++){
    unit->channel_bufs[channel] = enif_alloc(FRAME_SIZE * unit->period_size);
    if(unit->channel_bufs[channel] == NULL) return -1;
  }
  return format_index;
}

static ERL_NIF_TERM pcm_open_handle(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
  char device[128];                     /* playback device */
  int err = 0;
  xalsa_t * unit = enif_alloc_resource(res_type, sizeof(xalsa_t));
  snd_pcm_t** handle_p = &unit->handle;

  // we read the erlang string (erlang list)
  if (!enif_get_string(env, argv[0], device, 128, ERL_NIF_LATIN1)) {
    return enif_make_badarg(env); // blame user if address isn't a string
  }

  if ((err = snd_pcm_open(handle_p, device, SND_PCM_STREAM_PLAYBACK, SND_PCM_NONBLOCK)) < 0) {
    printf("Playback open error: %s\n", snd_strerror(err));
    return enif_make_badarg(env);
  }

  ERL_NIF_TERM term = enif_make_resource(env, unit);
  enif_release_resource(unit);
  return term;
}

static ERL_NIF_TERM pcm_close_handle(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
  int ret;

  xalsa_t * unit;
  if (!enif_get_resource(env, argv[0], res_type, (void**) &unit)){
    return enif_make_badarg(env);
  }

  ret = snd_pcm_close(unit->handle);
  return enif_make_int(env, ret);
}

static ERL_NIF_TERM pcm_dump(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
  int ret = 0;
  snd_output_t *output = NULL;
  int err;

  xalsa_t * unit;
  if (!enif_get_resource(env, argv[0], res_type, (void**) &unit)){
    return enif_make_badarg(env);
  }

  err = snd_output_stdio_attach(&output, stdout, 0);
  if (err < 0) {
    printf("Output failed: %s\n", snd_strerror(err));
    return -1;
  }

  ret = snd_pcm_dump(unit->handle, output);

  return enif_make_int(env, ret);
}

static ERL_NIF_TERM pcm_set_params(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
  unsigned int channels, rate, buffer_period_size_ratio;
  int ret;
  snd_pcm_uframes_t buffer_size, period_size; // ulong

  xalsa_t * unit;
  if (!enif_get_resource(env, argv[0], res_type, (void**) &unit)){
    return enif_make_badarg(env);
  }
  if (!enif_get_uint(env, argv[1], &channels)) {
    return enif_make_badarg(env);
  }
  if (!enif_get_uint(env, argv[2], &rate)) {
    return enif_make_badarg(env);
  }
  if (!enif_get_ulong(env, argv[3], &period_size)) {
    return enif_make_badarg(env);
  }
  if (!enif_get_uint(env, argv[4], &buffer_period_size_ratio)) {
    return enif_make_badarg(env);
  }

  buffer_size = buffer_period_size_ratio * period_size;

  ret = set_hwparams(unit, &rate, &channels,
                     buffer_period_size_ratio,
                     &buffer_size,
                     &period_size);
  if (ret < 0) {
    return enif_make_uint(env, ret);
  }else{
    unit->format_index = ret;
  }

  snd_pcm_sw_params_t *sw_params;
  snd_pcm_sw_params_malloc(&sw_params);
  snd_pcm_sw_params_current(unit->handle, sw_params);
  snd_pcm_sw_params_set_start_threshold(unit->handle, sw_params,
                                        (buffer_size / period_size) * period_size - 1);
  // snd_pcm_sw_params_set_start_threshold(unit->handle, sw_params, 0U);
  snd_pcm_sw_params_set_avail_min(unit->handle, sw_params, period_size);
  snd_pcm_sw_params(unit->handle, sw_params);
  snd_pcm_sw_params_free(sw_params);

  return enif_make_tuple4(env,
                          enif_make_uint(env, rate),
                          enif_make_uint(env, channels),
                          enif_make_ulong(env, buffer_size),
                          enif_make_ulong(env, period_size));
}

#define MAX(x,y) (((x)>(y)) ? (x) : (y))
#define MIN(x,y) (((x)<(y)) ? (x) : (y))

/* This function covers both MMAP_INTERLEAVED and MMAP_NONINTERLEAVED cases */
static ERL_NIF_TERM pcm_write(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
  unsigned int format_index, size; // No of frames (per channel)
  unsigned int i, channel;
  int err = 0;
  const snd_pcm_channel_area_t *areas;
  snd_pcm_uframes_t offset, frames;
  snd_pcm_sframes_t avail;

  xalsa_t * unit;
  if (!enif_get_resource(env, argv[0], res_type, (void**) &unit)){
    return enif_make_badarg(env);
  }

  size = unit->period_size;
  format_index = unit->format_index;

  avail = snd_pcm_avail_update(unit->handle);
  if (avail < size) {
    return enif_make_int(env, avail);
  }

  frames = size;
  err = snd_pcm_mmap_begin(unit->handle, &areas, &offset, &frames);
  if(err < 0){
    // printf("mmap begin error for playback:: %s\n", snd_strerror(err));
    return enif_make_int(env, err);
  }
  if(frames != size) {
    return enif_make_int(env, frames);
  }
  FRAME_TYPE max;
  unsigned int step;
  unsigned char * mmapp;
  FRAME_TYPE * f;
  int32_t tmp32;
  int16_t tmp16;
  FRAME_TYPE tmpf;
  for(channel = 0; channel < unit->channels; channel++){
    f = unit->channel_bufs[channel];
    step = areas[channel].step / 8;
    mmapp = (((unsigned char *)areas[channel].addr) + (areas[channel].first / 8));
    mmapp += offset * step;
    switch(format_index) {
    case 0:
      for(i = 0; i < frames; i++){
        *((FRAME_TYPE *) mmapp) = MAX(MIN(f[i], 1.0), -1.0);
        mmapp += step;
      }
      break;
    case 1:
    case 2:
      max = INT24_MAX_F;
      for(i = 0; i < frames; i++){
        if(f[i] <= -1.0f)
          tmp32 = INT24_MIN_I;
        else if(f[i] >= 1.0f)
          tmp32 = INT24_MAX_I;
        else
          tmp32 = (int32_t) (f[i] * max);

        if(format_index == 1)
          tmp32 = tmp32 << 8;

        *((int32_t *) mmapp) = tmp32;
        mmapp += step;
      }
      break;
    case 3:
      max = INT16_MAX_F;
      for(i = 0; i < frames; i++){
        tmpf = f[i];
        if(tmpf <= -1.0f)
          tmp16 = INT16_MIN_I;
        else if(tmpf >= 1.0f)
          tmp16 = INT16_MAX_I;
        else
          tmp16 = (int16_t) (tmpf * max);

        *((int16_t *) mmapp) = tmp16;
        mmapp += step;
      }
      break;
    }
  }
  err = snd_pcm_mmap_commit(unit->handle, offset, frames);
  if(err < 0){
    printf("mmap commit error for playback:: %s\n", snd_strerror(err));
  }
  return enif_make_int(env, err);
}

static ERL_NIF_TERM pcm_prepare(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
  xalsa_t * unit;
  if (!enif_get_resource(env, argv[0], res_type, (void**) &unit)){
    return enif_make_badarg(env);
  }
  return enif_make_int(env, snd_pcm_prepare(unit->handle));
}

static ERL_NIF_TERM pcm_start(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
  int err = 0;
  xalsa_t * unit;
  if (!enif_get_resource(env, argv[0], res_type, (void**) &unit)){
    return enif_make_badarg(env);
  }
  if (snd_pcm_state(unit->handle) == SND_PCM_STATE_PREPARED) {
    err = snd_pcm_start(unit->handle);
  }

  return enif_make_int(env, err);
}

static ERL_NIF_TERM pcm_recover(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
  int errno;
  xalsa_t * unit;
  if (!enif_get_resource(env, argv[0], res_type, (void**) &unit)){
    return enif_make_badarg(env);
  }
  if (!enif_get_int(env, argv[1], &errno)) {
    return enif_make_badarg(env);
  }
  return enif_make_int(env, snd_pcm_recover(unit->handle, errno, 1));
}


static ERL_NIF_TERM pcm_avail_update(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
  xalsa_t * unit;
  if (!enif_get_resource(env, argv[0], res_type, (void**) &unit)){
    return enif_make_badarg(env);
  }
  return enif_make_int(env, snd_pcm_avail_update(unit->handle));
}

/*   Callback handling - asynchronous notification */

static void pcm_async_callback(snd_async_handler_t *ahandler)
{
  ErlNifPid* pid = snd_async_handler_get_callback_private(ahandler);
  ErlNifEnv* msg_env = enif_alloc_env();
  if(!enif_send(NULL, pid, msg_env, enif_make_atom(msg_env, "pcm_ready4write"))){
    printf("xalsa_nif pcm_async_callback: enif_send failed\n");
  }
  enif_free_env(msg_env);
}

static ERL_NIF_TERM pcm_add_async_handler(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
  snd_async_handler_t *ahandler;
  int err;
  xalsa_t * unit;
  if (!enif_get_resource(env, argv[0], res_type, (void**) &unit)){
    return enif_make_badarg(env);
  }
  ErlNifPid* pid = (ErlNifPid*) enif_alloc(sizeof(ErlNifPid));
  if (!enif_get_local_pid(env, argv[1], pid)) {
    return enif_make_badarg(env);
  }
  err = snd_async_add_pcm_handler(&ahandler, unit->handle, pcm_async_callback, pid);
  return enif_make_int(env, err);
}

static ERL_NIF_TERM pcm_add_fd(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
  struct pollfd *ufds;
  int count, err;
  xalsa_t * unit;

  if (!enif_get_resource(env, argv[0], res_type, (void**) &unit)){
    return enif_make_badarg(env);
  }
  count = snd_pcm_poll_descriptors_count(unit->handle);
  if (count != 1) {
    printf("Invalid poll descriptors count %i\n", count);
    return enif_make_int(env, count);
  }

  ufds = enif_alloc(sizeof(struct pollfd) * (count + 2));
  if ((err = snd_pcm_poll_descriptors(unit->handle, ufds, count)) < 0) {
    printf("Unable to obtain poll descriptors for playback: %s\n", snd_strerror(err));
    return enif_make_int(env, err);
  }
  unit->fd = ufds[0].fd;
  enif_free(ufds);
  return enif_make_int(env, unit->fd);
}

static ERL_NIF_TERM pcm_enable_ready4write(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
  int rv;
  xalsa_t * unit;
  if (!enif_get_resource(env, argv[0], res_type, (void**) &unit)){
    return enif_make_badarg(env);
  }
  rv = enif_select_write(env,
                         (ErlNifEvent) unit->fd,
                         unit, // void* obj,
                         NULL,
                         enif_make_atom(env, "pcm_ready4write"),
                         NULL);
  if( rv < 0 ) {
    return enif_make_badarg(env);
  }
  return enif_make_int(env, rv);
}

static ERL_NIF_TERM sum_map(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]){
  unsigned int i, size; // No of frames
  ERL_NIF_TERM key, value, map_out;
  const ERL_NIF_TERM * tuple_array, * channel_array;
  ERL_NIF_TERM out_array[16]; // Max 16 channels per card
  unsigned int channel_arity;
  ErlNifMapIterator iter;
  ErlNifBinary bin;

  xalsa_t * unit;
  if (!enif_get_resource(env, argv[0], res_type, (void**) &unit)){
    return enif_make_badarg(env);
  }

  if (
      !enif_get_tuple(env, argv[1], (int *) &channel_arity, &channel_array) ||
      channel_arity != unit->channels) {
    return enif_make_badarg(env);
  }

  size = unit->period_size;
  unsigned int bytesize = size * FRAME_SIZE;

  for (unsigned int channel = 0; channel < unit->channels; channel++) {
    if (!enif_map_iterator_create(env, channel_array[channel],
                                  &iter, ERL_NIF_MAP_ITERATOR_FIRST))
      {
        return enif_make_badarg(env);
      }

    FRAME_TYPE *buf = (FRAME_TYPE *) unit->channel_bufs[channel];

    for(i = 0; i < size; i++)
      buf[i] = 0.0;

    map_out = enif_make_new_map(env);

    FRAME_TYPE * framesp, * z;
    unsigned int noframes;
    int arity, notify_flag;
    ERL_NIF_TERM new_binary;
    while (enif_map_iterator_get_pair(env, &iter, &key, &value)) {
      if(!(enif_get_tuple(env, value, &arity, &tuple_array) &&
           enif_get_int(env, tuple_array[0], &notify_flag) &&
           enif_inspect_binary(env, tuple_array[1], &bin))) {
        return enif_make_badarg(env);
      }
      framesp = (FRAME_TYPE *) bin.data;
      z = buf;
      noframes = (bin.size / FRAME_SIZE);
#ifdef __AVX__
#define AVXSTEP  sizeof(__m256) / FRAME_SIZE // 8
      __m256 ax, ay, az;

      for(i = 0; (i < size) && (i < noframes); i += AVXSTEP){
        ax = _mm256_loadu_ps(framesp);
        ay = _mm256_loadu_ps(z);
        az = _mm256_add_ps(ax,ay);
        _mm256_storeu_ps(z, az);
        framesp += AVXSTEP; z += AVXSTEP;
  }
#else
      for(i = 0; (i < size) && (i < noframes); i++){
        buf[i] += framesp[i];
      }
#endif

      if(i < noframes) { // bin.data larger than size
        unsigned int newsize = bin.size - bytesize;
        if(notify_flag && newsize <= bytesize){
          ErlNifPid pid;
          if(enif_get_local_pid(env, key, &pid)) {
            ErlNifEnv* msg_env = enif_alloc_env();
            enif_send(env, &pid, msg_env, enif_make_atom(msg_env, "ready4more"));
            enif_free_env(msg_env);
          }
          notify_flag = 0;
        }
        new_binary =
          enif_make_sub_binary(env,
                               tuple_array[1],
                               bytesize,
                               newsize);

        // insert it into the map along with its label converted as atom
        enif_make_map_put(env, map_out,
                          key,
                          enif_make_tuple2(env, enif_make_int(env, notify_flag), new_binary),
                          &map_out);
      } else if(notify_flag){
        ErlNifPid pid;
        if(enif_get_local_pid(env, key, &pid)) {
          ErlNifEnv* msg_env = enif_alloc_env();
          enif_send(env, &pid, msg_env, enif_make_atom(msg_env, "ready4more"));
          enif_free_env(msg_env);
        }
      }
      enif_map_iterator_next(env, &iter);
    }

    enif_map_iterator_destroy(env, &iter);
    out_array[channel] = map_out;
  }
  return enif_make_tuple_from_array(env, out_array, unit->channels);
}

static ERL_NIF_TERM float_list_to_binary(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]){
  ERL_NIF_TERM float_list, new_binary, head, tail;
  unsigned int list_length, bin_size;
  double frame;
  FRAME_TYPE * float_data;

  float_list = argv[0];
  if(!enif_get_list_length(env, float_list, &list_length)){
    return enif_make_badarg(env);
  }

  bin_size = list_length * FRAME_SIZE;
  float_data = (FRAME_TYPE *) enif_make_new_binary(env, bin_size, &new_binary);
  while(enif_get_list_cell(env, float_list, &head, &tail)){
    if(!enif_get_double(env, head, &frame)) {
      return enif_make_badarg(env);
    }
    *float_data++ = (FRAME_TYPE) frame;
    float_list = tail;
  }
  return new_binary;
}

static ErlNifFunc nif_funcs[] = {
  {"open_handle", 1, pcm_open_handle, ERL_NIF_DIRTY_JOB_IO_BOUND},
  {"close_handle", 1, pcm_close_handle},
  {"dump", 1, pcm_dump, ERL_NIF_DIRTY_JOB_IO_BOUND},
  {"set_params", 5, pcm_set_params, ERL_NIF_DIRTY_JOB_IO_BOUND},
  {"write", 1, pcm_write},
  {"prepare", 1, pcm_prepare},
  {"start", 1, pcm_start},
  {"recover", 2, pcm_recover},
  {"avail_update", 1, pcm_avail_update},
  {"add_async_handler", 2, pcm_add_async_handler},
  {"add_fd", 1, pcm_add_fd},
  {"enable_ready4write", 1, pcm_enable_ready4write},
  {"sum_map", 2, sum_map},
  {"float_list_to_binary", 1, float_list_to_binary}
};

// ErlNifResourceDtor
static void pcm_resource_dtor(ErlNifEnv* env, void * obj){
  xalsa_t * unit = (xalsa_t*) obj;
  for(unsigned int channel = 0; channel < unit->channels; channel++){
    enif_free(unit->channel_bufs[channel]);
  }
  enif_free(unit->channel_bufs);
}

static int open_pcm_resource_type(ErlNifEnv* env)
{
  const char* mod = "xalsa_nif";
  const char* resource_type = "pcm";
  int flags = ERL_NIF_RT_CREATE | ERL_NIF_RT_TAKEOVER;
  res_type = enif_open_resource_type(env, mod, resource_type,
                                     pcm_resource_dtor, flags, NULL);
  return ((res_type == NULL) ? -1:0);
}

static int load(ErlNifEnv* caller_env, void** priv_data, ERL_NIF_TERM load_info)
{
  return open_pcm_resource_type(caller_env);
}

static int upgrade(ErlNifEnv* caller_env, void** priv_data, void** old_priv_data,
		   ERL_NIF_TERM load_info)
{
  return open_pcm_resource_type(caller_env);
}

ERL_NIF_INIT(xalsa_pcm, nif_funcs, load, NULL, upgrade, NULL);
