/*
 * sndio output driver. This file is part of Shairport Sync.
 * Copyright (c) 2013 Dimitri Sokolyuk <demon@dim13.org>
 * Copyright (c) 2017 Tobias Kortkamp <t@tobik.me>
 *
 * Modifications for audio synchronisation
 * and related work, copyright (c) Mike Brady 2014 -- 2017
 * All rights reserved.
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#include "audio.h"
#include "common.h"
#include <pthread.h>
#include <sndio.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

static void help(void);
static int init(int, char **);
static void onmove_cb(void *, int);
static void deinit(void);
static void start(int, int);
static int play(void *, int);
static void stop(void);
static void onmove_cb(void *, int);
static int delay(long *);
static void flush(void);

audio_output audio_sndio = {.name = "sndio",
                            .help = &help,
                            .init = &init,
                            .deinit = &deinit,
                            .prepare = NULL,
                            .start = &start,
                            .stop = &stop,
                            .is_running = NULL,
                            .flush = &flush,
                            .delay = &delay,
                            .play = &play,
                            .volume = NULL,
                            .parameters = NULL,
                            .mute = NULL};

static pthread_mutex_t sndio_mutex = PTHREAD_MUTEX_INITIALIZER;
static struct sio_hdl *hdl;
static int framesize;
static size_t played;
static size_t written;
int64_t time_of_last_onmove_cb;
int at_least_one_onmove_cb_seen;
struct sio_par par;

struct sndio_formats {
  const char *name;
  sps_format_t fmt;
  unsigned int rate;
  unsigned int bits;
  unsigned int bps;
  unsigned int sig;
  unsigned int le;
};

static struct sndio_formats formats[] = {{"S8", SPS_FORMAT_S8, 44100, 8, 1, 1, SIO_LE_NATIVE},
                                         {"U8", SPS_FORMAT_U8, 44100, 8, 1, 0, SIO_LE_NATIVE},
                                         {"S16", SPS_FORMAT_S16, 44100, 16, 2, 1, SIO_LE_NATIVE},
                                         {"AUTOMATIC", SPS_FORMAT_S16, 44100, 16, 2, 1,
                                          SIO_LE_NATIVE}, // TODO: make this really automatic?
                                         {"S24", SPS_FORMAT_S24, 44100, 24, 4, 1, SIO_LE_NATIVE},
                                         {"S24_3LE", SPS_FORMAT_S24_3LE, 44100, 24, 3, 1, 1},
                                         {"S24_3BE", SPS_FORMAT_S24_3BE, 44100, 24, 3, 1, 0},
                                         {"S32", SPS_FORMAT_S32, 44100, 24, 4, 1, SIO_LE_NATIVE}};

static void help() { printf("    -d output-device    set the output device [default*|...]\n"); }

static int init(int argc, char **argv) {
  int found, opt, round, rate, bufsz;
  unsigned int i;
  const char *devname, *tmp;

  // set up default values first

  sio_initpar(&par);
  par.rate = 44100;
  par.pchan = 2;
  par.bits = 16;
  par.bps = SIO_BPS(par.bits);
  par.le = 1;
  par.sig = 1;
  devname = SIO_DEVANY;

  config.audio_backend_buffer_desired_length = 1.0;
  config.audio_backend_buffer_interpolation_threshold_in_seconds =
      0.25; // below this, soxr interpolation will not occur -- it'll be basic interpolation
            // instead.
  config.audio_backend_latency_offset = 0;

  // get settings from settings file

  // do the "general" audio  options. Note, these options are in the "general" stanza!
  parse_general_audio_options();

  // get the specific settings

  if (config.cfg != NULL) {
    if (!config_lookup_string(config.cfg, "sndio.device", &devname))
      devname = SIO_DEVANY;
    if (config_lookup_int(config.cfg, "sndio.rate", &rate)) {
      if (rate % 44100 == 0 && rate >= 44100 && rate <= 352800) {
        par.rate = rate;
      } else {
        die("sndio: output rate must be a multiple of 44100 and 44100 <= rate <= "
            "352800");
      }
    }
    if (config_lookup_int(config.cfg, "sndio.bufsz", &bufsz)) {
      if (bufsz > 0) {
        par.appbufsz = bufsz;
      } else {
        die("sndio: bufsz must be > 0");
      }
    }
    if (config_lookup_int(config.cfg, "sndio.round", &round)) {
      if (round > 0) {
        par.round = round;
      } else {
        die("sndio: round must be > 0");
      }
    }
    if (config_lookup_string(config.cfg, "sndio.format", &tmp)) {
      for (i = 0, found = 0; i < sizeof(formats) / sizeof(formats[0]); i++) {
        if (strcasecmp(formats[i].name, tmp) == 0) {
          config.output_format = formats[i].fmt;
          found = 1;
          break;
        }
      }
      if (!found)
        die("Invalid output format \"%s\". Should be one of: S8, U8, S16, S24, "
            "S24_3LE, S24_3BE, S32, Automatic",
            tmp);
    }
  }
  optind = 1; // optind=0 is equivalent to optind=1 plus special behaviour
  argv--;     // so we shift the arguments to satisfy getopt()
  argc++;
  while ((opt = getopt(argc, argv, "d:")) > 0) {
    switch (opt) {
    case 'd':
      devname = optarg;
      break;
    default:
      help();
      die("Invalid audio option -%c specified", opt);
    }
  }
  if (optind < argc)
    die("Invalid audio argument: %s", argv[optind]);
  pthread_mutex_lock(&sndio_mutex);
  debug(1, "Output device name is \"%s\".", devname);
  hdl = sio_open(devname, SIO_PLAY, 0);
  if (!hdl)
    die("sndio: cannot open audio device");

  written = played = 0;
  time_of_last_onmove_cb = 0;
  at_least_one_onmove_cb_seen = 0;

  for (i = 0; i < sizeof(formats) / sizeof(formats[0]); i++) {
    if (formats[i].fmt == config.output_format) {
      par.bits = formats[i].bits;
      par.bps = formats[i].bps;
      par.sig = formats[i].sig;
      par.le = formats[i].le;
      break;
    }
  }

  if (!sio_setpar(hdl, &par) || !sio_getpar(hdl, &par))
    die("sndio: failed to set audio parameters");
  for (i = 0, found = 0; i < sizeof(formats) / sizeof(formats[0]); i++) {
    if (formats[i].bits == par.bits && formats[i].bps == par.bps && formats[i].sig == par.sig &&
        formats[i].le == par.le && formats[i].rate == par.rate) {
      config.output_format = formats[i].fmt;
      found = 1;
      break;
    }
  }
  if (!found)
    die("sndio: could not set output device to the required format and rate.");

  framesize = par.bps * par.pchan;
  config.output_rate = par.rate;
  config.audio_backend_buffer_desired_length = 1.0 * par.bufsz / par.rate;
  config.audio_backend_latency_offset = 0;

  sio_onmove(hdl, onmove_cb, NULL);

  pthread_mutex_unlock(&sndio_mutex);
  return 0;
}

static void deinit() {
  pthread_mutex_lock(&sndio_mutex);
  sio_close(hdl);
  pthread_mutex_unlock(&sndio_mutex);
}

static void start(__attribute__((unused)) int sample_rate,
                  __attribute__((unused)) int sample_format) {
  pthread_mutex_lock(&sndio_mutex);
  if (!sio_start(hdl))
    die("sndio: unable to start");
  written = played = 0;
  time_of_last_onmove_cb = 0;
  at_least_one_onmove_cb_seen = 0;
  pthread_mutex_unlock(&sndio_mutex);
}

static int play(void *buf, int frames) {
  if (frames > 0) {
    pthread_mutex_lock(&sndio_mutex);
    written += sio_write(hdl, buf, frames * framesize);
    pthread_mutex_unlock(&sndio_mutex);
  }
  return 0;
}

static void stop() {
  pthread_mutex_lock(&sndio_mutex);
  if (!sio_stop(hdl))
    die("sndio: unable to stop");
  written = played = 0;
  pthread_mutex_unlock(&sndio_mutex);
}

static void onmove_cb(__attribute__((unused)) void *arg, int delta) {
  time_of_last_onmove_cb = get_absolute_time_in_ns();
  at_least_one_onmove_cb_seen = 1;
  played += delta;
}

static int delay(long *_delay) {
  pthread_mutex_lock(&sndio_mutex);
  size_t estimated_extra_frames_output = 0;
  if (at_least_one_onmove_cb_seen) { // when output starts, the onmove_cb callback will be made
    // calculate the difference in time between now and when the last callback occurred,
    // and use it to estimate the frames that would have been output
    uint64_t time_difference = get_absolute_time_in_ns() - time_of_last_onmove_cb;
    uint64_t frame_difference = (time_difference * par.rate) / 1000000000;
    estimated_extra_frames_output = frame_difference;
    // sanity check -- total estimate can not exceed frames written.
    if ((estimated_extra_frames_output + played) > written / framesize) {
      // debug(1,"play estimate fails sanity check, possibly due to running on a VM");
      estimated_extra_frames_output = 0; // can't make any sensible guess
    }
    // debug(1,"Frames played to last cb: %d, estimated to current time:
    // %d.",played,estimated_extra_frames_output);
  }
  *_delay = (written / framesize) - (played + estimated_extra_frames_output);
  pthread_mutex_unlock(&sndio_mutex);
  return 0;
}

static void flush() {
  pthread_mutex_lock(&sndio_mutex);
  if (!sio_stop(hdl) || !sio_start(hdl))
    die("sndio: unable to flush");
  written = played = 0;
  pthread_mutex_unlock(&sndio_mutex);
}
