/*
 * QEMU OS X CoreAudio audio driver
 *
 * Copyright (c) 2008 The Android Open Source Project
 * Copyright (c) 2005 Mike Kronenberg
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include <CoreAudio/CoreAudio.h>
#include <string.h>             /* strerror */
#include <pthread.h>            /* pthread_X */

#include "vl.h"

#define AUDIO_CAP "coreaudio"
#include "audio_int.h"

#define  ENABLE_IN  1

#if 0
#  define  D(...)  fprintf(stderr, __VA_ARGS__)
#else
#  define  D(...)  ((void)0)
#endif

struct {
    int out_buffer_frames;
    int out_nbuffers;
    int in_buffer_frames;
    int in_nbuffers;
    int isAtexit;
} conf = {
    .out_buffer_frames = 512,
    .out_nbuffers = 4,
    .in_buffer_frames = 512,
    .in_nbuffers = 4,
    .isAtexit = 0
};

/***************************************************************************************/
/***************************************************************************************/
/***                                                                                 ***/
/***       U T I L I T Y   R O U T I N E S                                           ***/
/***                                                                                 ***/
/***************************************************************************************/
/***************************************************************************************/

static void coreaudio_logstatus (OSStatus status)
{
    char *str = "BUG";

    switch(status) {
    case kAudioHardwareNoError:
        str = "kAudioHardwareNoError";
        break;

    case kAudioHardwareNotRunningError:
        str = "kAudioHardwareNotRunningError";
        break;

    case kAudioHardwareUnspecifiedError:
        str = "kAudioHardwareUnspecifiedError";
        break;

    case kAudioHardwareUnknownPropertyError:
        str = "kAudioHardwareUnknownPropertyError";
        break;

    case kAudioHardwareBadPropertySizeError:
        str = "kAudioHardwareBadPropertySizeError";
        break;

    case kAudioHardwareIllegalOperationError:
        str = "kAudioHardwareIllegalOperationError";
        break;

    case kAudioHardwareBadDeviceError:
        str = "kAudioHardwareBadDeviceError";
        break;

    case kAudioHardwareBadStreamError:
        str = "kAudioHardwareBadStreamError";
        break;

    case kAudioHardwareUnsupportedOperationError:
        str = "kAudioHardwareUnsupportedOperationError";
        break;

    case kAudioDeviceUnsupportedFormatError:
        str = "kAudioDeviceUnsupportedFormatError";
        break;

    case kAudioDevicePermissionsError:
        str = "kAudioDevicePermissionsError";
        break;

    default:
        AUD_log (AUDIO_CAP, "Reason: status code %ld\n", status);
        return;
    }

    AUD_log (AUDIO_CAP, "Reason: %s\n", str);
}

static void GCC_FMT_ATTR (2, 3) coreaudio_logerr (
    OSStatus status,
    const char *fmt,
    ...
    )
{
    va_list ap;

    va_start (ap, fmt);
    AUD_log (AUDIO_CAP, fmt, ap);
    va_end (ap);

    coreaudio_logstatus (status);
}

static void GCC_FMT_ATTR (3, 4) coreaudio_logerr2 (
    OSStatus status,
    const char *typ,
    const char *fmt,
    ...
    )
{
    va_list ap;

    AUD_log (AUDIO_CAP, "Could not initialize %s\n", typ);

    va_start (ap, fmt);
    AUD_vlog (AUDIO_CAP, fmt, ap);
    va_end (ap);

    coreaudio_logstatus (status);
}

static void coreaudio_atexit (void)
{
    conf.isAtexit = 1;
}

/***************************************************************************************/
/***************************************************************************************/
/***                                                                                 ***/
/***       S H A R E D   I N / O U T   V O I C E                                     ***/
/***                                                                                 ***/
/***************************************************************************************/
/***************************************************************************************/

typedef struct coreAudioVoice {
    pthread_mutex_t              mutex;
    AudioDeviceID                deviceID;
    Boolean                      isInput;
    UInt32                       bufferFrameSize;
    AudioStreamBasicDescription  streamBasicDescription;
    AudioDeviceIOProc            ioproc;
    int                          live;
    int                          decr;
    int                          pos;
} coreaudioVoice;


static inline UInt32
coreaudio_voice_isPlaying (coreaudioVoice*  core)
{
    OSStatus status;
    UInt32 result = 0;
    UInt32 propertySize = sizeof(core->deviceID);
    status = AudioDeviceGetProperty(
        core->deviceID, 0, core->isInput,
        kAudioDevicePropertyDeviceIsRunning, &propertySize, &result);
    if (status != kAudioHardwareNoError) {
        coreaudio_logerr(status,
                         "Could not determine whether Device is playing\n");
    }
    return result;
}

static int
coreaudio_voice_lock (coreaudioVoice*  core, const char *fn_name)
{
    int err;

    err = pthread_mutex_lock (&core->mutex);
    if (err) {
        dolog ("Could not lock voice for %s\nReason: %s\n",
               fn_name, strerror (err));
        return -1;
    }
    return 0;
}

static int
coreaudio_voice_unlock (coreaudioVoice*  core, const char *fn_name)
{
    int err;

    err = pthread_mutex_unlock (&core->mutex);
    if (err) {
        dolog ("Could not unlock voice for %s\nReason: %s\n",
               fn_name, strerror (err));
        return -1;
    }
    return 0;
}

static int
coreaudio_voice_ctl (coreaudioVoice*  core, int cmd)
{
    OSStatus status;

    switch (cmd) {
    case VOICE_ENABLE:
        /* start playback */
        D("%s: %s started\n", __FUNCTION__, core->isInput ? "input" : "output");
        if (!coreaudio_voice_isPlaying(core)) {
            status = AudioDeviceStart(core->deviceID, core->ioproc);
            if (status != kAudioHardwareNoError) {
                coreaudio_logerr (status, "Could not resume playback\n");
            }
        }
        break;

    case VOICE_DISABLE:
        /* stop playback */
        D("%s: %s stopped\n", __FUNCTION__, core->isInput ? "input" : "output");
        if (!conf.isAtexit) {
            if (coreaudio_voice_isPlaying(core)) {
                status = AudioDeviceStop(core->deviceID, core->ioproc);
                if (status != kAudioHardwareNoError) {
                    coreaudio_logerr (status, "Could not pause playback\n");
                }
            }
        }
        break;
    }
    return 0;
}

static void
coreaudio_voice_fini (coreaudioVoice*  core)
{
    OSStatus status;
    int err;

    if (!conf.isAtexit) {
        /* stop playback */
        coreaudio_voice_ctl(core, VOICE_DISABLE);

        /* remove callback */
        status = AudioDeviceRemoveIOProc(core->deviceID, core->ioproc);
        if (status != kAudioHardwareNoError) {
            coreaudio_logerr (status, "Could not remove IOProc\n");
        }
    }
    core->deviceID = kAudioDeviceUnknown;

    /* destroy mutex */
    err = pthread_mutex_destroy(&core->mutex);
    if (err) {
        dolog("Could not destroy mutex\nReason: %s\n", strerror (err));
    }
}


static int
coreaudio_voice_init (coreaudioVoice*    core,
                      audsettings_t*     as,
                      int                frameSize,
                      AudioDeviceIOProc  ioproc,
                      void*              hw,
                      int                input)
{
    OSStatus  status;
    UInt32    propertySize;
    int       err;
    int       bits = 8;
    AudioValueRange frameRange;
    const char*  typ = input ? "input" : "playback";

    core->isInput = input ? true : false;

    /* create mutex */
    err = pthread_mutex_init(&core->mutex, NULL);
    if (err) {
        dolog("Could not create mutex\nReason: %s\n", strerror (err));
        return -1;
    }

    if (as->fmt == AUD_FMT_S16 || as->fmt == AUD_FMT_U16) {
        bits = 16;
    }

    // TODO: audio_pcm_init_info (&hw->info, as);
    /* open default output device */
   /* note: we use DefaultSystemOutputDevice because DefaultOutputDevice seems to
    * always link to the internal speakers, and not the ones selected through system properties
    * go figure...
    */
    propertySize = sizeof(core->deviceID);
    status = AudioHardwareGetProperty(
        input ? kAudioHardwarePropertyDefaultInputDevice :
                kAudioHardwarePropertyDefaultSystemOutputDevice,
        &propertySize,
        &core->deviceID);
    if (status != kAudioHardwareNoError) {
        coreaudio_logerr2 (status, typ,
                           "Could not get default %s device\n", typ);
        return -1;
    }
    if (core->deviceID == kAudioDeviceUnknown) {
        dolog ("Could not initialize %s - Unknown Audiodevice\n", typ);
        return -1;
    }

    /* get minimum and maximum buffer frame sizes */
    propertySize = sizeof(frameRange);
    status = AudioDeviceGetProperty(
        core->deviceID,
        0,
        core->isInput,
        kAudioDevicePropertyBufferFrameSizeRange,
        &propertySize,
        &frameRange);
    if (status != kAudioHardwareNoError) {
        coreaudio_logerr2 (status, typ,
                           "Could not get device buffer frame range\n");
        return -1;
    }

    if (frameRange.mMinimum > frameSize) {
        core->bufferFrameSize = (UInt32) frameRange.mMinimum;
        dolog ("warning: Upsizing Output Buffer Frames to %f\n", frameRange.mMinimum);
    }
    else if (frameRange.mMaximum < frameSize) {
        core->bufferFrameSize = (UInt32) frameRange.mMaximum;
        dolog ("warning: Downsizing Output Buffer Frames to %f\n", frameRange.mMaximum);
    }
    else {
        core->bufferFrameSize = frameSize;
    }

    /* set Buffer Frame Size */
    propertySize = sizeof(core->bufferFrameSize);
    status = AudioDeviceSetProperty(
        core->deviceID,
        NULL,
        0,
        core->isInput,
        kAudioDevicePropertyBufferFrameSize,
        propertySize,
        &core->bufferFrameSize);
    if (status != kAudioHardwareNoError) {
        coreaudio_logerr2 (status, typ,
                           "Could not set device buffer frame size %ld\n",
                           core->bufferFrameSize);
        return -1;
    }

    /* get Buffer Frame Size */
    propertySize = sizeof(core->bufferFrameSize);
    status = AudioDeviceGetProperty(
        core->deviceID,
        0,
        core->isInput,
        kAudioDevicePropertyBufferFrameSize,
        &propertySize,
        &core->bufferFrameSize);
    if (status != kAudioHardwareNoError) {
        coreaudio_logerr2 (status, typ,
                           "Could not get device buffer frame size\n");
        return -1;
    }
    // TODO: hw->samples = *pNBuffers * core->bufferFrameSize;

    /* get StreamFormat */
    propertySize = sizeof(core->streamBasicDescription);
    status = AudioDeviceGetProperty(
        core->deviceID,
        0,
        core->isInput,
        kAudioDevicePropertyStreamFormat,
        &propertySize,
        &core->streamBasicDescription);
    if (status != kAudioHardwareNoError) {
        coreaudio_logerr2 (status, typ,
                           "Could not get Device Stream properties\n");
        core->deviceID = kAudioDeviceUnknown;
        return -1;
    }

    /* set Samplerate */
    core->streamBasicDescription.mSampleRate = (Float64) as->freq;
    propertySize = sizeof(core->streamBasicDescription);
    status = AudioDeviceSetProperty(
        core->deviceID,
        0,
        0,
        core->isInput,
        kAudioDevicePropertyStreamFormat,
        propertySize,
        &core->streamBasicDescription);
    if (status != kAudioHardwareNoError) {
        coreaudio_logerr2 (status, typ, "Could not set samplerate %d\n",
                           as->freq);
        core->deviceID = kAudioDeviceUnknown;
        return -1;
    }

    /* set Callback */
    core->ioproc = ioproc;
    status = AudioDeviceAddIOProc(core->deviceID, ioproc, hw);
    if (status != kAudioHardwareNoError) {
        coreaudio_logerr2 (status, typ, "Could not set IOProc\n");
        core->deviceID = kAudioDeviceUnknown;
        return -1;
    }

    /* start Playback */
    if (!input && !coreaudio_voice_isPlaying(core)) {
        status = AudioDeviceStart(core->deviceID, core->ioproc);
        if (status != kAudioHardwareNoError) {
            coreaudio_logerr2 (status, typ, "Could not start playback\n");
            AudioDeviceRemoveIOProc(core->deviceID, core->ioproc);
            core->deviceID = kAudioDeviceUnknown;
            return -1;
        }
    }

    return 0;
}


/***************************************************************************************/
/***************************************************************************************/
/***                                                                                 ***/
/***       O U T P U T   V O I C E                                                   ***/
/***                                                                                 ***/
/***************************************************************************************/
/***************************************************************************************/

typedef struct coreaudioVoiceOut {
    HWVoiceOut                   hw;
    coreaudioVoice               core[1];
} coreaudioVoiceOut;

#define  CORE_OUT(hw)  ((coreaudioVoiceOut*)(hw))->core


static int
coreaudio_run_out (HWVoiceOut *hw)
{
    int live, decr;
    coreaudioVoice  *core = CORE_OUT(hw);

    if (coreaudio_voice_lock (core, "coreaudio_run_out")) {
        return 0;
    }

    live = audio_pcm_hw_get_live_out (hw);

    if (core->decr > live) {
        ldebug ("core->decr %d live %d core->live %d\n",
                core->decr,
                live,
                core->live);
    }

    decr        = audio_MIN (core->decr, live);
    core->decr -= decr;
    core->live  = live - decr;
    hw->rpos    = core->pos;

    coreaudio_voice_unlock (core, "coreaudio_run_out");
    return decr;
}


/* callback to feed audiooutput buffer */
static OSStatus
audioOutDeviceIOProc(
    AudioDeviceID inDevice,
    const AudioTimeStamp* inNow,
    const AudioBufferList* inInputData,
    const AudioTimeStamp* inInputTime,
    AudioBufferList* outOutputData,
    const AudioTimeStamp* inOutputTime,
    void* hwptr)
{
    UInt32 frame, frameCount;
    float *out = outOutputData->mBuffers[0].mData;
    HWVoiceOut *hw = hwptr;
    coreaudioVoice *core = CORE_OUT(hw);
    int rpos, live;
    st_sample_t *src;
#ifndef FLOAT_MIXENG
#ifdef RECIPROCAL
    const float scale = 1.f / UINT_MAX;
#else
    const float scale = UINT_MAX;
#endif
#endif

    if (coreaudio_voice_lock (core, "audioDeviceIOProc")) {
        inInputTime = 0;
        return 0;
    }

    frameCount = core->bufferFrameSize;
    live = core->live;

    /* if there are not enough samples, set signal and return */
    if (live < frameCount) {
        inInputTime = 0;
        coreaudio_voice_unlock (core, "audioDeviceIOProc(empty)");
        return 0;
    }

    rpos = core->pos;
    src = hw->mix_buf + rpos;

    /* fill buffer */
    for (frame = 0; frame < frameCount; frame++) {
#ifdef FLOAT_MIXENG
        *out++ = src[frame].l; /* left channel */
        *out++ = src[frame].r; /* right channel */
#else
#ifdef RECIPROCAL
        *out++ = src[frame].l * scale; /* left channel */
        *out++ = src[frame].r * scale; /* right channel */
#else
        *out++ = src[frame].l / scale; /* left channel */
        *out++ = src[frame].r / scale; /* right channel */
#endif
#endif
    }

    rpos = (rpos + frameCount) % hw->samples;
    core->decr += frameCount;
    core->pos  = rpos;

    coreaudio_voice_unlock (core, "audioDeviceIOProc");
    return 0;
}

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

static int
coreaudio_init_out (HWVoiceOut *hw, audsettings_t *as)
{
    coreaudioVoice*  core = CORE_OUT(hw);
    int              err;

    audio_pcm_init_info (&hw->info, as);

    err = coreaudio_voice_init( core, as, conf.out_buffer_frames, audioOutDeviceIOProc, hw, 0 );
    if (err < 0)
        return err;

    hw->samples = core->bufferFrameSize * conf.out_nbuffers;
    return 0;
}

static void
coreaudio_fini_out (HWVoiceOut *hw)
{

    coreaudioVoice*  core = CORE_OUT(hw);

    coreaudio_voice_fini(core);
}

static int
coreaudio_ctl_out (HWVoiceOut *hw, int cmd, ...)
{
    coreaudioVoice*  core = CORE_OUT(hw);

    return coreaudio_voice_ctl(core, cmd);
}

/***************************************************************************************/
/***************************************************************************************/
/***                                                                                 ***/
/***       I N P U T   V O I C E                                                     ***/
/***                                                                                 ***/
/***************************************************************************************/
/***************************************************************************************/



typedef struct coreaudioVoiceIn {
    HWVoiceIn        hw;
    coreaudioVoice   core[1];
} coreaudioVoiceIn;

#define  CORE_IN(hw)  ((coreaudioVoiceIn*)(hw))->core


static int
coreaudio_run_in (HWVoiceIn *hw)
{
    int decr;

    coreaudioVoice  *core = CORE_IN(hw);

    if (coreaudio_voice_lock (core, "coreaudio_run_in")) {
        return 0;
    }

    D("%s: core.decr=%d core.pos=%d\n", __FUNCTION__, core->decr, core->pos);
    decr        = core->decr;
    core->decr -= decr;
    hw->wpos    = core->pos;

    coreaudio_voice_unlock (core, "coreaudio_run_in");
    return decr;
}


/* callback to feed audiooutput buffer */
static OSStatus
audioInDeviceIOProc(
    AudioDeviceID inDevice,
    const AudioTimeStamp* inNow,
    const AudioBufferList* inInputData,
    const AudioTimeStamp* inInputTime,
    AudioBufferList* outOutputData,
    const AudioTimeStamp* inOutputTime,
    void* hwptr)
{
    UInt32 frame, frameCount;
    float *in = inInputData->mBuffers[0].mData;
    HWVoiceIn *hw = hwptr;
    coreaudioVoice *core = CORE_IN(hw);
    int wpos, avail;
    st_sample_t *dst;
#ifndef FLOAT_MIXENG
#ifdef RECIPROCAL
    const float scale = 1.f / UINT_MAX;
#else
    const float scale = UINT_MAX;
#endif
#endif

    if (coreaudio_voice_lock (core, "audioDeviceIOProc")) {
        inInputTime = 0;
        return 0;
    }

    frameCount = core->bufferFrameSize;
    avail      = hw->samples - hw->total_samples_captured - core->decr;

    D("%s: enter avail=%d core.decr=%d core.pos=%d hw.samples=%d hw.total_samples_captured=%d frameCount=%d\n",
      __FUNCTION__, avail, core->decr, core->pos, hw->samples, hw->total_samples_captured, (int)frameCount);

    /* if there are not enough samples, set signal and return */
    if (avail < frameCount) {
        inInputTime = 0;
        coreaudio_voice_unlock (core, "audioDeviceIOProc(empty)");
        return 0;
    }

    wpos = core->pos;
    dst  = hw->conv_buf + wpos;

    /* fill buffer */
    for (frame = 0; frame < frameCount; frame++) {
#ifdef FLOAT_MIXENG
        dst[frame].l = *in++; /* left channel */
        dst[frame].r = *in++; /* right channel */
#else
#ifdef RECIPROCAL
        dst[frame].l = *in++ * scale; /* left channel */
        dst[frame].r = *in++ * scale; /* right channel */
#else
        dst[frame].l = *in++ / scale; /* left channel */
        dst[frame].r = *in++ / scale; /* right channel */
#endif
#endif
    }

    wpos = (wpos + frameCount) % hw->samples;
    core->decr += frameCount;
    core->pos   = wpos;

    D("exit: core.decr=%d core.pos=%d\n", core->decr, core->pos);
    coreaudio_voice_unlock (core, "audioDeviceIOProc");
    return 0;
}

static int
coreaudio_read (SWVoiceIn *sw, void *buf, int len)
{
    int  result = audio_pcm_sw_read(sw, buf, len);
    D("%s: audio_pcm_sw_read(%d) returned %d\n", __FUNCTION__, len, result);
    return result;
}

static int
coreaudio_init_in (HWVoiceIn *hw, audsettings_t *as)
{
    coreaudioVoice*  core = CORE_IN(hw);
    int              err;

    audio_pcm_init_info (&hw->info, as);

    err = coreaudio_voice_init( core, as, conf.in_buffer_frames, audioInDeviceIOProc, hw, 1 );
    if (err < 0) {
        return err;
    }

    hw->samples = core->bufferFrameSize * conf.in_nbuffers;
    return 0;
}

static void
coreaudio_fini_in (HWVoiceIn *hw)
{

    coreaudioVoice*  core = CORE_IN(hw);

    coreaudio_voice_fini(core);
}

static int
coreaudio_ctl_in (HWVoiceIn *hw, int cmd, ...)
{
    coreaudioVoice*  core = CORE_IN(hw);

    return coreaudio_voice_ctl(core, cmd);
}

static void*
coreaudio_audio_init (void)
{
    atexit(coreaudio_atexit);
    return &coreaudio_audio_init;
}

static void
coreaudio_audio_fini (void *opaque)
{
    (void) opaque;
}

static struct audio_option coreaudio_options[] = {
    {"OUT_BUFFER_SIZE", AUD_OPT_INT, &conf.out_buffer_frames,
     "Size of the output buffer in frames", NULL, 0},
    {"OUT_BUFFER_COUNT", AUD_OPT_INT, &conf.out_nbuffers,
     "Number of output buffers", NULL, 0},
    {"IN_BUFFER_SIZE", AUD_OPT_INT, &conf.in_buffer_frames,
     "Size of the input buffer in frames", NULL, 0},
    {"IN_BUFFER_COUNT", AUD_OPT_INT, &conf.in_nbuffers,
     "Number of input buffers", NULL, 0},
    {NULL, 0, NULL, NULL, NULL, 0}
};

static struct audio_pcm_ops coreaudio_pcm_ops = {
    coreaudio_init_out,
    coreaudio_fini_out,
    coreaudio_run_out,
    coreaudio_write,
    coreaudio_ctl_out,

#if ENABLE_IN
    coreaudio_init_in,
    coreaudio_fini_in,
    coreaudio_run_in,
    coreaudio_read,
    coreaudio_ctl_in
#else
    NULL,
    NULL,
    NULL,
    NULL,
    NULL
#endif
};

struct audio_driver coreaudio_audio_driver = {
    INIT_FIELD (name           = ) "coreaudio",
    INIT_FIELD (descr          = )
    "CoreAudio (developer.apple.com/audio/coreaudio.html)",
    INIT_FIELD (options        = ) coreaudio_options,
    INIT_FIELD (init           = ) coreaudio_audio_init,
    INIT_FIELD (fini           = ) coreaudio_audio_fini,
    INIT_FIELD (pcm_ops        = ) &coreaudio_pcm_ops,
    INIT_FIELD (can_be_default = ) 1,
#if ENABLE_IN
    INIT_FIELD (max_voices_out = ) 1,
    INIT_FIELD (max_voices_in  = ) 1,
    INIT_FIELD (voice_size_out = ) sizeof (coreaudioVoiceOut),
    INIT_FIELD (voice_size_in  = ) sizeof (coreaudioVoiceIn),
#else
    INIT_FIELD (max_voices_out = ) 1,
    INIT_FIELD (max_voices_in  = ) 0,
    INIT_FIELD (voice_size_out = ) sizeof (coreaudioVoiceOut),
    INIT_FIELD (voice_size_in  = ) 0,
#endif
};
