#include <errno.h>
#include <fcntl.h>
#include <poll.h>
#include <pthread.h>
#include <unistd.h>

#include <sys/audioio.h>
#include <sys/ioctl.h>

#include <libaudcore/audstrings.h>
#include <libaudcore/i18n.h>
#include <libaudcore/plugin.h>
#include <libaudcore/ringbuf.h>
#include <libaudcore/runtime.h>

class NetBSDOutput : public OutputPlugin
{
public:
    static const char about[];
    static const char * const defaults[];
    static constexpr PluginInfo info = {
        "NetBSD Audio Output",
        "netbsdout",
        about
    };

    constexpr NetBSDOutput () : OutputPlugin (info, 10) {}

    bool init ();
    void cleanup ();

    bool open_audio (int format, int rate, int chans, String & error);
    void close_audio ();

    int write_audio (const void * data, int size);
    void pause (bool pause);

    void period_wait ();
    int get_delay ();

    void set_volume(StereoVolume volume);
    StereoVolume get_volume();

    void flush ();
    void drain ();
};

__attribute__((visibility("default"))) NetBSDOutput aud_plugin_instance;

static String audio_path;
static int audio_fd;

static bool audio_paused, audio_flushed;

static pthread_mutex_t nbout_mutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t nbout_cond = PTHREAD_COND_INITIALIZER;

struct fmt_conv {
    int aud_format;
    unsigned int encoding;
    unsigned int bits;
};

const char
NetBSDOutput::about[] =
    "NetBSD Audio Plugin";

const char * const
NetBSDOutput::defaults[] = {
    "audio_dev", "/dev/audio",
    nullptr
};

static bool
pause_audio (bool pause)
{
    audio_info_t info;

    AUDIO_INITINFO (& info);

    info.play.pause = pause;

    if (ioctl(audio_fd, AUDIO_SETINFO, & info) != 0) {
        AUDERR ("Unable to set pause: %s.\n", strerror (errno));
        return false;
    }

    return true;
}

bool
NetBSDOutput::init ()
{
    aud_config_set_defaults ("netbsd", defaults);
    audio_path = aud_get_str ("netbsd", "audio_dev");

    audio_fd = open (audio_path, O_WRONLY);

    if (audio_fd == -1) {
        AUDERR ("Failed to open %s: %s.\n", (const char *) audio_path, strerror (errno));
        return false;
    }

    return true;
}

void
NetBSDOutput::cleanup ()
{
    close (audio_fd);
}

/*
 * Converts the format libaudcore provides to something usable
 * in audio_info_t, then passes along the information to the
 * audio device.
 */
bool
NetBSDOutput::open_audio (int format,
                          int rate,
                          int chans,
                          String & error)
{
    /* TODO: Use FMT_S24_* or FMT_S24_3* */
    audio_info_t info;
    int props;
    /* Audacious->NetBSD format table */
    static const struct fmt_conv fmt_table[] = {
        {FMT_S8, AUDIO_ENCODING_SLINEAR, 8},
        {FMT_U8, AUDIO_ENCODING_ULINEAR, 8},
        {FMT_S16_LE, AUDIO_ENCODING_SLINEAR_LE, 16},
        {FMT_S16_BE, AUDIO_ENCODING_SLINEAR_BE, 16},
        {FMT_U16_LE, AUDIO_ENCODING_ULINEAR_LE, 16},
        {FMT_U16_BE, AUDIO_ENCODING_ULINEAR_BE, 16},
        {FMT_S32_LE, AUDIO_ENCODING_SLINEAR_LE, 32},
        {FMT_S32_BE, AUDIO_ENCODING_SLINEAR_BE, 32},
        {FMT_U32_LE, AUDIO_ENCODING_ULINEAR_LE, 32},
        {FMT_U32_BE, AUDIO_ENCODING_ULINEAR_BE, 32}
    };
    struct fmt_conv cur_format = { -1, 0, 0 };

    if (ioctl (audio_fd, AUDIO_GETPROPS, & props) == -1) {
        error = String (str_printf
            ("Failed to get audio properties on %s: %s.",
            (const char *) audio_path, strerror (errno)));
        return false;
    }

    if ((props | AUDIO_PROP_PLAYBACK) != AUDIO_PROP_PLAYBACK) {
        error = String (str_printf
            ("Device %s does not support playback.",
            (const char *) audio_path));
    }

    AUDIO_INITINFO (&info);

    /* Loop to find the right format, if it's supported */
    for (auto & conv : fmt_table) {
        if (conv.aud_format == format) {
            cur_format = conv;
            break;
        }
    }

    if (cur_format.aud_format == -1) {
        error = String ("Audio format not supported.\n");
        return false;
    }

    info.play.encoding = cur_format.encoding;
    info.play.precision = cur_format.bits;

    /* Channels and sample rate are straightforward, at least */
    info.play.channels = chans;
    info.play.sample_rate = rate;
    info.mode = AUMODE_PLAY;

    if (ioctl (audio_fd, AUDIO_SETINFO, & info) == -1) {
        error = String (str_printf
             ("Failed to set track info on %s: %s.\n",
             (const char *) audio_path, strerror (errno)));
        return false;
    }

    return true;
}

void
NetBSDOutput::close_audio ()
{
    AUDDBG ("close_audio is a stub!\n"); /* XXX */
}

int
NetBSDOutput::write_audio (const void * data,
                           int size)
{
    int res, len;

    if ((res = pthread_mutex_lock (& nbout_mutex)) != 0) {
        AUDERR ("Couldn't lock mutex: %s.\n", strerror (res));
        return 0;
    }

    len = write(audio_fd, data, size);

    pthread_mutex_unlock (& nbout_mutex);
    return len;
}

void
NetBSDOutput::period_wait ()
{
    struct pollfd fds = {
        audio_fd,
        POLLOUT,
        0
    };
    int res;

    if ((res = pthread_mutex_lock (& nbout_mutex)) != 0) {
        AUDERR ("Couldn't lock mutex: %s.\n", strerror (res));
        return;
    }

    while ((res = poll(& fds, 1, 0)) == 0) {
        if (audio_flushed) {
            AUDDBG ("Audio flushed, exiting period_wait loop...\n");
            break;
        }

        pthread_cond_wait (& nbout_cond, & nbout_mutex);
    }

    pthread_mutex_unlock (& nbout_mutex);
}

void
NetBSDOutput::pause (bool pause)
{
    int res;

    AUDDBG ("Setting pause = %s.\n", pause ? "true" : "false");

    if ((res = pthread_mutex_lock (& nbout_mutex)) != 0) {
        AUDERR ("Couldn't lock mutex: %s.\n", strerror (res));
        return;
    }

    if (pause_audio (pause)) {
        audio_paused = pause;
    }

    pthread_cond_broadcast (& nbout_cond);
    pthread_mutex_unlock (& nbout_mutex);
}

void
NetBSDOutput::drain ()
{
    int res;

    AUDDBG ("Draining...\n");

    if ((res = pthread_mutex_lock (& nbout_mutex)) != 0) {
        AUDERR ("Couldn't lock mutex: %s.\n", strerror (res));
        return;
    }

    if (ioctl (audio_fd, AUDIO_DRAIN) != 0) {
        AUDERR ("Couldn't drain audio: %s\n.", strerror (errno));
    }

    pthread_mutex_unlock (& nbout_mutex);
}

int
NetBSDOutput::get_delay ()
{
    audio_info_t info;
    unsigned int delay_time;
    int res;

    if ((res = pthread_mutex_lock (& nbout_mutex)) != 0) {
        AUDERR ("Couldn't lock mutex: %s.\n", strerror (res));
        return -1;
    }

    if (ioctl (audio_fd, AUDIO_GETBUFINFO, & info) != 0) {
        AUDERR ("Couldn't get audio info: %s.\n", strerror (errno));
        return -1;
    }

    /* 
     * Calculate the time remaining by dividing the bytes left to read
     * by (sample rate * bits of precision).
     */
    if (info.play.sample_rate > 0 && info.play.precision > 0) {
        delay_time = aud::rdiv (info.play.seek,
            (info.play.sample_rate * info.play.precision));
    } else {
        AUDERR ("Sample rate and precision are both 0\n");
        return -1;
    }

    pthread_mutex_unlock (& nbout_mutex);
    return delay_time;
}

void
NetBSDOutput::flush ()
{
    int res;
    
    AUDDBG ("Flushing!\n");

    if ((res = pthread_mutex_lock (& nbout_mutex)) != 0) {
        AUDERR ("Couldn't lock mutex: %s.\n", strerror (res));
        return;
    }

    if (ioctl (audio_fd, AUDIO_FLUSH) != 0) {
        AUDERR ("Couldn't flush audio: %s\n.", strerror (errno));
    }

    audio_flushed = true;

    pthread_cond_broadcast (& nbout_cond);
    pthread_mutex_unlock (& nbout_mutex);
}

StereoVolume
NetBSDOutput::get_volume ()
{
    audio_info_t info;
    int aud_vol;

    if (ioctl (audio_fd, AUDIO_GETINFO, & info) != 0) {
        AUDERR ("Unable to get volume: %s.\n", strerror (errno));
        return { 0, 0 };
    }

    aud_vol = aud::rescale ((int)info.play.gain - AUDIO_MIN_GAIN,
                            AUDIO_MAX_GAIN - AUDIO_MIN_GAIN,
                            100);

    return { aud_vol, aud_vol };
}

void
NetBSDOutput::set_volume (StereoVolume volume)
{
    audio_info_t info;

    AUDIO_INITINFO (& info);
    info.play.gain = aud::rescale (aud::max
        (volume.left, volume.right), 100,
        AUDIO_MAX_GAIN) + AUDIO_MIN_GAIN;

    if (ioctl(audio_fd, AUDIO_SETINFO, & info) != 0) {
        AUDERR ("Unable to set volume: %s.\n", strerror (errno));
    }
}