/** @file
 *
 * VBox PulseAudio backend
 */

/*
 * Copyright (C) 2006-2007 Sun Microsystems, Inc.
 *
 * This file is part of VirtualBox Open Source Edition (OSE), as
 * available from http://www.virtualbox.org. This file is free software;
 * you can redistribute it and/or modify it under the terms of the GNU
 * General Public License (GPL) as published by the Free Software
 * Foundation, in version 2 as it comes in the "COPYING" file of the
 * VirtualBox OSE distribution. VirtualBox OSE is distributed in the
 * hope that it will be useful, but WITHOUT ANY WARRANTY of any kind.
 *
 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa
 * Clara, CA 95054 USA or visit http://www.sun.com if you need
 * additional information or have any questions.
 */


/*******************************************************************************
*   Header Files                                                               *
*******************************************************************************/
#define LOG_GROUP LOG_GROUP_DEV_AUDIO
#include <VBox/log.h>
#include <iprt/mem.h>

#include <pulse/pulseaudio.h>
#include "pulse_stubs.h"

#include "../../vl_vbox.h"
#include "audio.h"
#define AUDIO_CAP "pulse"
#include "audio_int.h"
#include <stdio.h>

#define MAX_LOG_REL_ERRORS 32

/*
 * We use a g_pMainLoop in a separate thread g_pContext. We have to call functions for
 * manipulating objects either from callback functions or we have to protect
 * these functions by pa_threaded_mainloop_lock() / pa_threaded_mainloop_unlock().
 */
static struct pa_threaded_mainloop *g_pMainLoop;
static struct pa_context           *g_pContext;

typedef struct PulseVoice
{
    HWVoiceOut  hw;
    void       *pPCMBuf;
    pa_stream  *pStream;
    int         fOpSuccess;
    unsigned    cErrors;
} PulseVoice;

static struct
{
    int         buffer_msecs_out;
    int         buffer_msecs_in;
} conf
=
{
    INIT_FIELD (.buffer_msecs_out = ) 100,
    INIT_FIELD (.buffer_msecs_in  = ) 100,
};

struct pulse_params_req
{
    int                 freq;
    pa_sample_format_t  pa_format;
    int                 nchannels;
};

struct pulse_params_obt
{
    int                 freq;
    pa_sample_format_t  pa_format;
    int                 nchannels;
    unsigned long       buffer_size;
};

static void pulse_check_fatal (PulseVoice *pulse, int rc)
{
    if (rc == PA_ERR_CONNECTIONTERMINATED)
    {
        /* XXX runtime warning */
        LogRel(("Pulse: Audio input/output stopped!\n"));
        pulse->cErrors = MAX_LOG_REL_ERRORS;
    }
}

static pa_sample_format_t aud_to_pulsefmt (audfmt_e fmt)
{
    switch (fmt)
    {
        case AUD_FMT_U8:
            return PA_SAMPLE_U8;

        case AUD_FMT_S16:
            return PA_SAMPLE_S16LE;

#ifdef PA_SAMPLE_S32LE
        case AUD_FMT_S32:
            return PA_SAMPLE_S32LE;
#endif

        default:
            dolog ("Bad audio format %d\n", fmt);
            return PA_SAMPLE_U8;
    }
}


static int pulse_to_audfmt (pa_sample_format_t pulsefmt, audfmt_e *fmt, int *endianess)
{
    switch (pulsefmt)
    {
        case PA_SAMPLE_U8:
            *endianess = 0;
            *fmt = AUD_FMT_U8;
            break;

        case PA_SAMPLE_S16LE:
            *fmt = AUD_FMT_S16;
            *endianess = 0;
            break;

        case PA_SAMPLE_S16BE:
            *fmt = AUD_FMT_S16;
            *endianess = 1;
            break;

#ifdef PA_SAMPLE_S32LE
        case PA_SAMPLE_S32LE:
            *fmt = AUD_FMT_S32;
            *endianess = 0;
            break;
#endif

#ifdef PA_SAMPLE_S32BE
        case PA_SAMPLE_S32BE:
            *fmt = AUD_FMT_S32;
            *endianess = 1;
            break;
#endif

        default:
            return -1;
    }
    return 0;
}

static void context_state_callback(pa_context *c, void *userdata)
{
    switch (pa_context_get_state(c))
    {
        case PA_CONTEXT_READY:
        case PA_CONTEXT_TERMINATED:
        case PA_CONTEXT_FAILED:
            pa_threaded_mainloop_signal(g_pMainLoop, 0);
            break;
        default:
            break;
    }
}

static void stream_state_callback(pa_stream *s, void *userdata)
{
    switch (pa_stream_get_state(s))
    {
        case PA_STREAM_READY:
        case PA_STREAM_FAILED:
        case PA_STREAM_TERMINATED:
            pa_threaded_mainloop_signal(g_pMainLoop, 0);
            break;
        default:
            break;
    }
}

static void stream_latency_update_callback(pa_stream *s, void *userdata)
{
    pa_threaded_mainloop_signal(g_pMainLoop, 0);
}

static int pulse_open (int fIn, struct pulse_params_req *req,
                       struct pulse_params_obt *obt, pa_stream **ppStream)
{
    pa_sample_spec        sspec;
    pa_channel_map        cmap;
    pa_stream            *pStream = NULL;
    pa_buffer_attr        bufAttr;
    const pa_buffer_attr *pBufAttr;
    const pa_sample_spec *pSampSpec;
    char                  achPCMName[64];
    pa_stream_flags_t     flags;
    int                   ms = fIn ? conf.buffer_msecs_in : conf.buffer_msecs_out;
    const char           *stream_name = audio_get_stream_name();

    RTStrPrintf(achPCMName, sizeof(achPCMName), "%.32s%s%s%s",
                stream_name ? stream_name : "",
                stream_name ? " (" : "",
                fIn ? "pcm_in" : "pcm_out",
                stream_name ? ")" : "");
    sspec.rate     = req->freq;
    sspec.channels = req->nchannels;
    sspec.format   = req->pa_format;

    LogRel(("Pulse: open %s rate=%dHz channels=%d format=%s\n",
                fIn ? "PCM_IN" : "PCM_OUT", req->freq, req->nchannels,
                pa_sample_format_to_string(req->pa_format)));

    if (!pa_sample_spec_valid(&sspec))
    {
        LogRel(("Pulse: Unsupported sample specification\n"));
        goto fail;
    }

    pa_channel_map_init_auto(&cmap, sspec.channels, PA_CHANNEL_MAP_ALSA);

#if 0
    pa_cvolume_reset(&volume, sspec.channels);
#endif

    pa_threaded_mainloop_lock(g_pMainLoop);

    if (!(pStream = pa_stream_new(g_pContext, achPCMName, &sspec, &cmap)))
    {
        LogRel(("Pulse: Cannot create stream %s\n", achPCMName));
        goto unlock_and_fail;
    }

    pSampSpec      = pa_stream_get_sample_spec(pStream);
    obt->pa_format = pSampSpec->format;
    obt->nchannels = pSampSpec->channels;
    obt->freq      = pSampSpec->rate;

    pa_stream_set_state_callback(pStream, stream_state_callback, NULL);
    pa_stream_set_latency_update_callback(pStream, stream_latency_update_callback, NULL);

    memset(&bufAttr, 0, sizeof(bufAttr));
    bufAttr.tlength   = (pa_bytes_per_second(pSampSpec) * ms) / 1000;
    bufAttr.maxlength = (bufAttr.tlength*3) / 2;
    bufAttr.minreq    = pa_bytes_per_second(pSampSpec) / 100;    /* 10ms */
    bufAttr.prebuf    = bufAttr.tlength - bufAttr.minreq;
    bufAttr.fragsize  = pa_bytes_per_second(pSampSpec) / 100;    /* 10ms */

    flags = PA_STREAM_INTERPOLATE_TIMING | PA_STREAM_AUTO_TIMING_UPDATE;
    if (fIn)
    {
        if (pa_stream_connect_record(pStream, /*dev=*/NULL, &bufAttr, flags) < 0)
        {
            LogRel(("Pulse: Cannot connect record stream : %s\n",
                    pa_strerror(pa_context_errno(g_pContext))));
            goto disconnect_unlock_and_fail;
        }
    }
    else
    {
        if (pa_stream_connect_playback(pStream, /*dev=*/NULL, &bufAttr, flags,
                                       NULL, NULL) < 0)
        {
            LogRel(("Pulse: Cannot connect playback stream: %s\n",
                    pa_strerror(pa_context_errno(g_pContext))));
            goto disconnect_unlock_and_fail;
        }
    }

    /* Wait until the stream is ready */
    for (;;)
    {
        pa_stream_state_t sstate;
        pa_threaded_mainloop_wait(g_pMainLoop);
        sstate = pa_stream_get_state(pStream);
        if (sstate == PA_STREAM_READY)
            break;
        else if (sstate == PA_STREAM_FAILED || sstate == PA_STREAM_TERMINATED)
        {
            LogRel(("Pulse: Failed to initialize stream (state %d)\n", sstate));
            goto disconnect_unlock_and_fail;
        }
    }

    pBufAttr = pa_stream_get_buffer_attr(pStream);
    obt->buffer_size = pBufAttr->maxlength;

    pa_threaded_mainloop_unlock(g_pMainLoop);

    LogRel(("Pulse: buffer settings: max=%d tlength=%d prebuf=%d minreq=%d\n",
            pBufAttr->maxlength, pBufAttr->tlength, pBufAttr->prebuf, pBufAttr->minreq));

    *ppStream = pStream;
    return 0;

disconnect_unlock_and_fail:
    pa_stream_disconnect(pStream);

unlock_and_fail:
    pa_threaded_mainloop_unlock(g_pMainLoop);

fail:
    if (pStream)
        pa_stream_unref(pStream);

    *ppStream = NULL;
    return -1;
}

static int pulse_init_out (HWVoiceOut *hw, audsettings_t *as)
{
    PulseVoice *pulse = (PulseVoice *) hw;
    struct pulse_params_req req;
    struct pulse_params_obt obt;
    audfmt_e effective_fmt;
    int endianness;
    audsettings_t obt_as;

    req.pa_format   = aud_to_pulsefmt (as->fmt);
    req.freq        = as->freq;
    req.nchannels   = as->nchannels;

    if (pulse_open (/*fIn=*/0, &req, &obt, &pulse->pStream))
        return -1;

    if (pulse_to_audfmt (obt.pa_format, &effective_fmt, &endianness))
    {
        LogRel(("Pulse: Cannot find audio format %d\n", obt.pa_format));
        return -1;
    }

    obt_as.freq       = obt.freq;
    obt_as.nchannels  = obt.nchannels;
    obt_as.fmt        = effective_fmt;
    obt_as.endianness = endianness;

    audio_pcm_init_info (&hw->info, &obt_as);
    hw->samples = obt.buffer_size >> hw->info.shift;

    pulse->pPCMBuf = RTMemAllocZ(obt.buffer_size);
    if (!pulse->pPCMBuf)
    {
        LogRel(("Pulse: Could not allocate DAC buffer of %d bytes\n", obt.buffer_size));
        return -1;
    }

    return 0;
}

static void pulse_fini_out (HWVoiceOut *hw)
{
    PulseVoice *pulse = (PulseVoice *)hw;
    if (pulse->pStream)
    {
        pa_stream_disconnect(pulse->pStream);
        pa_stream_unref(pulse->pStream);
        pulse->pStream = NULL;
    }
    if (pulse->pPCMBuf)
    {
        RTMemFree (pulse->pPCMBuf);
        pulse->pPCMBuf = NULL;
    }
}

static int pulse_run_out (HWVoiceOut *hw)
{
    PulseVoice *pulse = (PulseVoice *) hw;
    int          csLive, csDecr = 0, csSamples, csToWrite, csAvail;
    size_t       cbAvail, cbToWrite;
    uint8_t     *pu8Dst;
    st_sample_t *psSrc;

    csLive = audio_pcm_hw_get_live_out (hw);
    if (!csLive)
        return 0;

    pa_threaded_mainloop_lock(g_pMainLoop);

    cbAvail = pa_stream_writable_size (pulse->pStream);
    if (cbAvail == (size_t)-1)
    {
        if (pulse->cErrors < MAX_LOG_REL_ERRORS)
        {
            int rc = pa_context_errno(g_pContext);
            pulse->cErrors++;
            LogRel(("Pulse: Failed to determine the writable size: %s\n",
                     pa_strerror(rc)));
            pulse_check_fatal(pulse, rc);
        }
        goto unlock_and_exit;
    }

    csAvail   = cbAvail >> hw->info.shift; /* bytes => samples */
    csDecr    = audio_MIN (csLive, csAvail);
    csSamples = csDecr;

    while (csSamples)
    {
        /* split request at the end of our samples buffer */
        csToWrite = audio_MIN (csSamples, hw->samples - hw->rpos);
        cbToWrite = csToWrite << hw->info.shift;
        psSrc     = hw->mix_buf + hw->rpos;
        pu8Dst    = advance (pulse->pPCMBuf, hw->rpos << hw->info.shift);

        hw->clip (pu8Dst, psSrc, csToWrite);

        if (pa_stream_write (pulse->pStream, pu8Dst, cbToWrite,
                             /*cleanup_callback=*/NULL, 0, PA_SEEK_RELATIVE) < 0)
        {
            LogRel(("Pulse: Failed to write %d samples: %s\n",
                    csToWrite, pa_strerror(pa_context_errno(g_pContext))));
            break;
        }
        hw->rpos   = (hw->rpos + csToWrite) % hw->samples;
        csSamples -= csToWrite;
    }

unlock_and_exit:
    pa_threaded_mainloop_unlock(g_pMainLoop);

    return csDecr;
}

static int pulse_write (SWVoiceOut *sw, void *buf, int len)
{
    return audio_pcm_sw_write (sw, buf, len);
}

static void stream_success_callback(pa_stream *pStream, int success, void *userdata)
{
    PulseVoice *pulse = (PulseVoice *) userdata;
    pulse->fOpSuccess = success;
    pa_threaded_mainloop_signal(g_pMainLoop, 0);
}

typedef enum
{
    Unpause  = 0,
    Pause    = 1,
    Flush    = 2,
    Trigger  = 3
} pulse_cmd_t;

static int pulse_ctrl (HWVoiceOut *hw, pulse_cmd_t cmd)
{
    PulseVoice *pulse = (PulseVoice *) hw;
    pa_operation *op = NULL;

    if (!pulse->pStream)
        return 0;

    pa_threaded_mainloop_lock(g_pMainLoop);
    switch (cmd)
    {
        case Pause:
            op = pa_stream_cork(pulse->pStream, 1, stream_success_callback, pulse);
            break;
        case Unpause:
            op = pa_stream_cork(pulse->pStream, 0, stream_success_callback, pulse);
            break;
        case Flush:
            op = pa_stream_flush(pulse->pStream, stream_success_callback, pulse);
            break;
        case Trigger:
            op = pa_stream_trigger(pulse->pStream, stream_success_callback, pulse);
            break;
        default:
            goto unlock_and_exit;
    }
    if (!op)
    {
        if (pulse->cErrors < MAX_LOG_REL_ERRORS)
        {
            int rc = pa_context_errno(g_pContext);
            pulse->cErrors++;
            LogRel(("Pulse: Failed ctrl cmd=%d to stream: %s\n",
                    cmd, pa_strerror(pa_context_errno(g_pContext))));
            pulse_check_fatal(pulse, rc);
        }
    }
    else
        pa_operation_unref(op);

unlock_and_exit:
    pa_threaded_mainloop_unlock(g_pMainLoop);
    return 0;
}

static int pulse_ctl_out (HWVoiceOut *hw, int cmd, ...)
{
    switch (cmd)
    {
        case VOICE_ENABLE:
            pulse_ctrl(hw, Unpause);
            pulse_ctrl(hw, Trigger);
            break;
        case VOICE_DISABLE:
            pulse_ctrl(hw, Flush);
            break;
        default:
            return -1;
    }
    return 0;
}

static int pulse_init_in (HWVoiceIn *hw, audsettings_t *as)
{
    PulseVoice *pulse = (PulseVoice *) hw;
    struct pulse_params_req req;
    struct pulse_params_obt obt;
    audfmt_e effective_fmt;
    int endianness;
    audsettings_t obt_as;

    req.pa_format   = aud_to_pulsefmt (as->fmt);
    req.freq        = as->freq;
    req.nchannels   = as->nchannels;

    if (pulse_open (/*fIn=*/1, &req, &obt, &pulse->pStream))
        return -1;

    if (pulse_to_audfmt (obt.pa_format, &effective_fmt, &endianness))
    {
        LogRel(("Pulse: Cannot find audio format %d\n", obt.pa_format));
        return -1;
    }

    obt_as.freq       = obt.freq;
    obt_as.nchannels  = obt.nchannels;
    obt_as.fmt        = effective_fmt;
    obt_as.endianness = endianness;

    audio_pcm_init_info (&hw->info, &obt_as);

    /* pcm_in: reserve twice as the maximum buffer length because of peek()/drop(). */
    hw->samples = 2 * (obt.buffer_size >> hw->info.shift);

    /* no buffer for input */
    pulse->pPCMBuf = NULL;

    return 0;
}

static void pulse_fini_in (HWVoiceIn *hw)
{
    PulseVoice *pulse = (PulseVoice *)hw;
    if (pulse->pStream)
    {
        pa_stream_disconnect(pulse->pStream);
        pa_stream_unref(pulse->pStream);
        pulse->pStream = NULL;
    }
    if (pulse->pPCMBuf)
    {
        RTMemFree (pulse->pPCMBuf);
        pulse->pPCMBuf = NULL;
    }
}

static int pulse_run_in (HWVoiceIn *hw)
{
    PulseVoice *pulse = (PulseVoice *) hw;
    int    csDead, csDecr = 0, csSamples, csRead, csAvail;
    size_t cbAvail;
    const void  *pu8Src;
    st_sample_t *psDst;

    csDead = hw->samples - audio_pcm_hw_get_live_in (hw);

    if (!csDead)
        return 0; /* no buffer available */

    pa_threaded_mainloop_lock(g_pMainLoop);

    if (pa_stream_peek(pulse->pStream, &pu8Src, &cbAvail) < 0)
    {
        LogRel(("Pulse: Peek failed: %s\n",
                pa_strerror(pa_context_errno(g_pContext))));
        goto unlock_and_exit;
    }
    if (!pu8Src)
        goto unlock_and_exit;

    csAvail = cbAvail >> hw->info.shift;
    csDecr  = audio_MIN (csDead, csAvail);

    csSamples = csDecr;

    while (csSamples)
    {
        /* split request at the end of our samples buffer */
        psDst      = hw->conv_buf + hw->wpos;
        csRead     = audio_MIN (csSamples, hw->samples - hw->wpos);
        hw->conv (psDst, pu8Src, csRead, &nominal_volume);
        hw->wpos   = (hw->wpos + csRead) % hw->samples;
        csSamples -= csRead;
        pu8Src     = (const void*)((uint8_t*)pu8Src + (csRead << hw->info.shift));
    }

    pa_stream_drop(pulse->pStream);

unlock_and_exit:
    pa_threaded_mainloop_unlock(g_pMainLoop);

    return csDecr;
}

static int pulse_read (SWVoiceIn *sw, void *buf, int size)
{
    return audio_pcm_sw_read (sw, buf, size);
}

static int pulse_ctl_in (HWVoiceIn *hw, int cmd, ...)
{
    return 0;
}

static void *pulse_audio_init (void)
{
    int rc;

    rc = audioLoadPulseLib();
    if (RT_FAILURE(rc))
    {
        LogRel(("Pulse: Failed to load the PulseAudio shared library! Error %Rrc\n", rc));
        return NULL;
    }
    if (!(g_pMainLoop = pa_threaded_mainloop_new()))
    {
        LogRel(("Pulse: Failed to allocate main loop: %s\n",
                 pa_strerror(pa_context_errno(g_pContext))));
        goto fail;
    }
    if (!(g_pContext = pa_context_new(pa_threaded_mainloop_get_api(g_pMainLoop), "VBox")))
    {
        LogRel(("Pulse: Failed to allocate context: %s\n",
                 pa_strerror(pa_context_errno(g_pContext))));
        goto fail;
    }
    if (pa_threaded_mainloop_start(g_pMainLoop) < 0)
    {
        LogRel(("Pulse: Failed to start threaded mainloop: %s\n",
                 pa_strerror(pa_context_errno(g_pContext))));
        goto fail;
    }

    pa_context_set_state_callback(g_pContext, context_state_callback, NULL);
    pa_threaded_mainloop_lock(g_pMainLoop);

    if (pa_context_connect(g_pContext, /*server=*/NULL, 0, NULL) < 0)
    {
        LogRel(("Pulse: Failed to connect to server: %s\n",
                 pa_strerror(pa_context_errno(g_pContext))));
        goto unlock_and_fail;
    }

    /* Wait until the g_pContext is ready */
    for (;;)
    {
        pa_context_state_t cstate;
        pa_threaded_mainloop_wait(g_pMainLoop);
        cstate = pa_context_get_state(g_pContext);
        if (cstate == PA_CONTEXT_READY)
            break;
        else if (cstate == PA_CONTEXT_TERMINATED || cstate == PA_CONTEXT_FAILED)
        {
            LogRel(("Pulse: Failed to initialize context (state %d)\n", cstate));
            goto unlock_and_fail;
        }
    }
    pa_threaded_mainloop_unlock(g_pMainLoop);

    return &conf;

unlock_and_fail:
    if (g_pMainLoop)
        pa_threaded_mainloop_unlock(g_pMainLoop);

fail:
    if (g_pMainLoop)
        pa_threaded_mainloop_stop(g_pMainLoop);

    if (g_pContext)
    {
        pa_context_disconnect(g_pContext);
        pa_context_unref(g_pContext);
        g_pContext = NULL;
    }
    if (g_pMainLoop)
    {
        pa_threaded_mainloop_free(g_pMainLoop);
        g_pMainLoop = NULL;
    }
    return NULL;
}

static void pulse_audio_fini (void *opaque)
{
    if (g_pMainLoop)
        pa_threaded_mainloop_stop(g_pMainLoop);
    if (g_pContext)
    {
        pa_context_disconnect(g_pContext);
        pa_context_unref(g_pContext);
        g_pContext = NULL;
    }
    if (g_pMainLoop)
    {
        pa_threaded_mainloop_free(g_pMainLoop);
        g_pMainLoop = NULL;
    }
    (void) opaque;
}

static struct audio_option pulse_options[] =
{
    {"DAC_MS", AUD_OPT_INT, &conf.buffer_msecs_out,
     "DAC period size in milliseconds", NULL, 0},
    {"ADC_MS", AUD_OPT_INT, &conf.buffer_msecs_in,
     "ADC period size in milliseconds", NULL, 0},
    {NULL, 0, NULL, NULL, NULL, 0}
};

static struct audio_pcm_ops pulse_pcm_ops =
{
    pulse_init_out,
    pulse_fini_out,
    pulse_run_out,
    pulse_write,
    pulse_ctl_out,

    pulse_init_in,
    pulse_fini_in,
    pulse_run_in,
    pulse_read,
    pulse_ctl_in
};

struct audio_driver pulse_audio_driver =
{
    INIT_FIELD (name           = ) "pulse",
    INIT_FIELD (descr          = ) "PulseAudio http://www.pulseaudio.org",
    INIT_FIELD (options        = ) pulse_options,
    INIT_FIELD (init           = ) pulse_audio_init,
    INIT_FIELD (fini           = ) pulse_audio_fini,
    INIT_FIELD (pcm_ops        = ) &pulse_pcm_ops,
    INIT_FIELD (can_be_default = ) 1,
    INIT_FIELD (max_voices_out = ) INT_MAX,
    INIT_FIELD (max_voices_in  = ) INT_MAX,
    INIT_FIELD (voice_size_out = ) sizeof (PulseVoice),
    INIT_FIELD (voice_size_in  = ) sizeof (PulseVoice)
};
