/*
 * QEMU OS X CoreAudio audio driver
 *
 * 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 "qemu/osdep.h"
#include <CoreAudio/CoreAudio.h>
#include <pthread.h>            /* pthread_X */

#include "qemu/main-loop.h"
#include "qemu/module.h"
#include "qemu/error-report.h"
#include "qemu/audio.h"
#include "qom/object.h"
#include "audio_int.h"

#define TYPE_AUDIO_COREAUDIO "audio-coreaudio"
OBJECT_DECLARE_SIMPLE_TYPE(AudioCoreaudio, AUDIO_COREAUDIO)

struct AudioCoreaudio {
    AudioMixengBackend parent_obj;
};

typedef struct coreaudioVoiceOut {
    HWVoiceOut hw;
    pthread_mutex_t buf_mutex;
    AudioDeviceID device_id;
    int frame_size_setting;
    uint32_t buffer_count;
    UInt32 device_frame_size;
    AudioDeviceIOProcID ioprocid;
    bool enabled;
} CoreaudioVoiceOut;

static const AudioObjectPropertyAddress voice_out_addr = {
    kAudioHardwarePropertyDefaultOutputDevice,
    kAudioObjectPropertyScopeGlobal,
    kAudioObjectPropertyElementMain
};

static OSStatus coreaudio_get_voice_out(AudioDeviceID *id)
{
    UInt32 size = sizeof(*id);

    return AudioObjectGetPropertyData(kAudioObjectSystemObject,
                                      &voice_out_addr,
                                      0,
                                      NULL,
                                      &size,
                                      id);
}

static OSStatus coreaudio_get_out_framesizerange(AudioDeviceID id,
                                                 AudioValueRange *framerange)
{
    UInt32 size = sizeof(*framerange);
    AudioObjectPropertyAddress addr = {
        kAudioDevicePropertyBufferFrameSizeRange,
        kAudioDevicePropertyScopeOutput,
        kAudioObjectPropertyElementMain
    };

    return AudioObjectGetPropertyData(id,
                                      &addr,
                                      0,
                                      NULL,
                                      &size,
                                      framerange);
}

static OSStatus coreaudio_get_out_framesize(AudioDeviceID id, UInt32 *framesize)
{
    UInt32 size = sizeof(*framesize);
    AudioObjectPropertyAddress addr = {
        kAudioDevicePropertyBufferFrameSize,
        kAudioDevicePropertyScopeOutput,
        kAudioObjectPropertyElementMain
    };

    return AudioObjectGetPropertyData(id,
                                      &addr,
                                      0,
                                      NULL,
                                      &size,
                                      framesize);
}

static OSStatus coreaudio_set_out_framesize(AudioDeviceID id, UInt32 *framesize)
{
    UInt32 size = sizeof(*framesize);
    AudioObjectPropertyAddress addr = {
        kAudioDevicePropertyBufferFrameSize,
        kAudioDevicePropertyScopeOutput,
        kAudioObjectPropertyElementMain
    };

    return AudioObjectSetPropertyData(id,
                                      &addr,
                                      0,
                                      NULL,
                                      size,
                                      framesize);
}

static OSStatus coreaudio_set_out_streamformat(AudioDeviceID id,
                                               AudioStreamBasicDescription *d)
{
    UInt32 size = sizeof(*d);
    AudioObjectPropertyAddress addr = {
        kAudioDevicePropertyStreamFormat,
        kAudioDevicePropertyScopeOutput,
        kAudioObjectPropertyElementMain
    };

    return AudioObjectSetPropertyData(id,
                                      &addr,
                                      0,
                                      NULL,
                                      size,
                                      d);
}

static OSStatus coreaudio_get_out_isrunning(AudioDeviceID id, UInt32 *result)
{
    UInt32 size = sizeof(*result);
    AudioObjectPropertyAddress addr = {
        kAudioDevicePropertyDeviceIsRunning,
        kAudioDevicePropertyScopeOutput,
        kAudioObjectPropertyElementMain
    };

    return AudioObjectGetPropertyData(id,
                                      &addr,
                                      0,
                                      NULL,
                                      &size,
                                      result);
}

static void coreaudio_logstatus(OSStatus status)
{
    const 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:
        error_printf(" Reason: status code %" PRId32, (int32_t)status);
        return;
    }

    error_printf(" Reason: %s", str);
}

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

    error_printf("coreaudio: ");
    va_start(ap, fmt);
    error_vprintf(fmt, ap);
    va_end(ap);
    coreaudio_logstatus(status);
    error_printf("\n");
}

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

    error_printf("coreaudio: Could not initialize %s: ", typ);
    va_start(ap, fmt);
    error_vprintf(fmt, ap);
    va_end(ap);
    coreaudio_logstatus(status);
    error_printf("\n");
}

#define coreaudio_playback_logerr(status, ...) \
    coreaudio_logerr2(status, "playback", __VA_ARGS__)

static int coreaudio_voice_out_buf_lock(CoreaudioVoiceOut *core,
                                        const char *fn_name)
{
    int err;

    err = pthread_mutex_lock(&core->buf_mutex);
    if (err) {
        error_report("coreaudio: Could not lock voice for %s: %s",
                     fn_name, strerror(err));
        return -1;
    }
    return 0;
}

static int coreaudio_voice_out_buf_unlock(CoreaudioVoiceOut *core,
                                          const char *fn_name)
{
    int err;

    err = pthread_mutex_unlock(&core->buf_mutex);
    if (err) {
        error_report("coreaudio: Could not unlock voice for %s: %s",
                     fn_name, strerror(err));
        return -1;
    }
    return 0;
}

#define COREAUDIO_WRAPPER_FUNC(name, ret_type, args_decl, args)       \
    static ret_type glue(coreaudio_, name)args_decl                   \
    {                                                                 \
        CoreaudioVoiceOut *core = (CoreaudioVoiceOut *)hw;            \
        ret_type ret;                                                 \
                                                                      \
        if (coreaudio_voice_out_buf_lock(core, "coreaudio_" #name)) { \
            return 0;                                                 \
        }                                                             \
                                                                      \
        ret = glue(audio_generic_, name)args;                         \
                                                                      \
        coreaudio_voice_out_buf_unlock(core, "coreaudio_" #name);     \
        return ret;                                                   \
    }
COREAUDIO_WRAPPER_FUNC(buffer_get_free, size_t, (HWVoiceOut *hw), (hw))
COREAUDIO_WRAPPER_FUNC(get_buffer_out, void *, (HWVoiceOut *hw, size_t *size),
                       (hw, size))
COREAUDIO_WRAPPER_FUNC(put_buffer_out, size_t,
                       (HWVoiceOut *hw, void *buf, size_t size),
                       (hw, buf, size))
COREAUDIO_WRAPPER_FUNC(write, size_t, (HWVoiceOut *hw, void *buf, size_t size),
                       (hw, buf, size))
#undef COREAUDIO_WRAPPER_FUNC

/*
 * callback to feed audiooutput buffer. called without BQL.
 * allowed to lock "buf_mutex", but disallowed to have any other locks.
 */
static OSStatus out_device_ioproc(
    AudioDeviceID inDevice,
    const AudioTimeStamp *inNow,
    const AudioBufferList *inInputData,
    const AudioTimeStamp *inInputTime,
    AudioBufferList *outOutputData,
    const AudioTimeStamp *inOutputTime,
    void *hwptr)
{
    UInt32 frame_size, pending_frames;
    void *out = outOutputData->mBuffers[0].mData;
    HWVoiceOut *hw = hwptr;
    CoreaudioVoiceOut *core = hwptr;
    size_t len;

    if (coreaudio_voice_out_buf_lock(core, "out_device_ioproc")) {
        inInputTime = 0;
        return 0;
    }

    if (inDevice != core->device_id) {
        coreaudio_voice_out_buf_unlock(core, "out_device_ioproc(old device)");
        return 0;
    }

    frame_size = core->device_frame_size;
    pending_frames = hw->pending_emul / hw->info.bytes_per_frame;

    /* if there are not enough samples, set signal and return */
    if (pending_frames < frame_size) {
        inInputTime = 0;
        coreaudio_voice_out_buf_unlock(core, "out_device_ioproc(empty)");
        return 0;
    }

    len = frame_size * hw->info.bytes_per_frame;
    while (len) {
        size_t write_len, start;

        start = audio_ring_posb(hw->pos_emul, hw->pending_emul, hw->size_emul);
        assert(start < hw->size_emul);

        write_len = MIN(MIN(hw->pending_emul, len),
                        hw->size_emul - start);

        memcpy(out, hw->buf_emul + start, write_len);
        hw->pending_emul -= write_len;
        len -= write_len;
        out += write_len;
    }

    coreaudio_voice_out_buf_unlock(core, "out_device_ioproc");
    return 0;
}

static OSStatus init_out_device(CoreaudioVoiceOut *core)
{
    AudioDeviceID device_id;
    AudioDeviceIOProcID ioprocid;
    AudioValueRange value_range;
    OSStatus status;
    UInt32 device_frame_size;

    AudioStreamBasicDescription stream_basic_description = {
        .mBitsPerChannel = audio_format_bits(core->hw.info.af),
        .mBytesPerFrame = core->hw.info.bytes_per_frame,
        .mBytesPerPacket = core->hw.info.bytes_per_frame,
        .mChannelsPerFrame = core->hw.info.nchannels,
        .mFormatFlags = kLinearPCMFormatFlagIsFloat,
        .mFormatID = kAudioFormatLinearPCM,
        .mFramesPerPacket = 1,
        .mSampleRate = core->hw.info.freq
    };

    status = coreaudio_get_voice_out(&device_id);
    if (status != kAudioHardwareNoError) {
        coreaudio_playback_logerr(status,
                                  "Could not get default output device");
        return status;
    }
    if (device_id == kAudioDeviceUnknown) {
        error_report("coreaudio: Could not initialize playback: "
                     "Unknown audio device");
        return status;
    }

    /* get minimum and maximum buffer frame sizes */
    status = coreaudio_get_out_framesizerange(device_id, &value_range);
    if (status == kAudioHardwareBadObjectError) {
        return 0;
    }
    if (status != kAudioHardwareNoError) {
        coreaudio_playback_logerr(status,
                                  "Could not get device buffer frame range");
        return status;
    }

    if (value_range.mMinimum > core->frame_size_setting) {
        device_frame_size = value_range.mMinimum;
        warn_report("coreaudio: Upsizing buffer frames to %f",
                    value_range.mMinimum);
    } else if (value_range.mMaximum < core->frame_size_setting) {
        device_frame_size = value_range.mMaximum;
        warn_report("coreaudio: Downsizing buffer frames to %f",
                    value_range.mMaximum);
    } else {
        device_frame_size = core->frame_size_setting;
    }

    /* set Buffer Frame Size */
    status = coreaudio_set_out_framesize(device_id, &device_frame_size);
    if (status == kAudioHardwareBadObjectError) {
        return 0;
    }
    if (status != kAudioHardwareNoError) {
        coreaudio_playback_logerr(status,
                                  "Could not set device buffer frame size %" PRIu32,
                                  (uint32_t)device_frame_size);
        return status;
    }

    /* get Buffer Frame Size */
    status = coreaudio_get_out_framesize(device_id, &device_frame_size);
    if (status == kAudioHardwareBadObjectError) {
        return 0;
    }
    if (status != kAudioHardwareNoError) {
        coreaudio_playback_logerr(status,
                                  "Could not get device buffer frame size");
        return status;
    }

    /* set Samplerate */
    status = coreaudio_set_out_streamformat(device_id,
                                            &stream_basic_description);
    if (status == kAudioHardwareBadObjectError) {
        return 0;
    }
    if (status != kAudioHardwareNoError) {
        coreaudio_playback_logerr(status,
                                  "Could not set samplerate %lf",
                                  stream_basic_description.mSampleRate);
        return status;
    }

    /*
     * set Callback.
     *
     * On macOS 11.3.1, Core Audio calls AudioDeviceIOProc after calling an
     * internal function named HALB_Mutex::Lock(), which locks a mutex in
     * HALB_IOThread::Entry(void*). HALB_Mutex::Lock() is also called in
     * AudioObjectGetPropertyData, which is called by coreaudio driver.
     * Therefore, the specified callback must be designed to avoid a deadlock
     * with the callers of AudioObjectGetPropertyData.
     */
    ioprocid = NULL;
    status = AudioDeviceCreateIOProcID(device_id,
                                       out_device_ioproc,
                                       &core->hw,
                                       &ioprocid);
    if (status == kAudioHardwareBadDeviceError) {
        return 0;
    }
    if (status != kAudioHardwareNoError || ioprocid == NULL) {
        coreaudio_playback_logerr(status, "Could not set IOProc");
        return status;
    }

    core->device_id = device_id;
    core->device_frame_size = device_frame_size;
    core->hw.samples = core->buffer_count * core->device_frame_size;
    audio_generic_initialize_buffer_out(&core->hw);
    core->ioprocid = ioprocid;

    return 0;
}

static void fini_out_device(CoreaudioVoiceOut *core)
{
    OSStatus status;
    UInt32 isrunning;

    /* stop playback */
    status = coreaudio_get_out_isrunning(core->device_id, &isrunning);
    if (status != kAudioHardwareBadObjectError) {
        if (status != kAudioHardwareNoError) {
            coreaudio_logerr(status,
                             "Could not determine whether device is playing");
        }

        if (isrunning) {
            status = AudioDeviceStop(core->device_id, core->ioprocid);
            if (status != kAudioHardwareBadDeviceError && status != kAudioHardwareNoError) {
                coreaudio_logerr(status, "Could not stop playback");
            }
        }
    }

    /* remove callback */
    status = AudioDeviceDestroyIOProcID(core->device_id,
                                        core->ioprocid);
    if (status != kAudioHardwareBadDeviceError && status != kAudioHardwareNoError) {
        coreaudio_logerr(status, "Could not remove IOProc");
    }
    core->device_id = kAudioDeviceUnknown;
}

static void update_out_device_playback_state(CoreaudioVoiceOut *core)
{
    OSStatus status;
    UInt32 isrunning;

    status = coreaudio_get_out_isrunning(core->device_id, &isrunning);
    if (status != kAudioHardwareNoError) {
        if (status != kAudioHardwareBadObjectError) {
            coreaudio_logerr(status,
                             "Could not determine whether device is playing");
        }

        return;
    }

    if (core->enabled) {
        /* start playback */
        if (!isrunning) {
            status = AudioDeviceStart(core->device_id, core->ioprocid);
            if (status != kAudioHardwareBadDeviceError && status != kAudioHardwareNoError) {
                coreaudio_logerr(status, "Could not resume playback");
            }
        }
    } else {
        /* stop playback */
        if (isrunning) {
            status = AudioDeviceStop(core->device_id,
                                     core->ioprocid);
            if (status != kAudioHardwareBadDeviceError && status != kAudioHardwareNoError) {
                coreaudio_logerr(status, "Could not pause playback");
            }
        }
    }
}

/* called without BQL. */
static OSStatus handle_voice_out_change(
    AudioObjectID in_object_id,
    UInt32 in_number_addresses,
    const AudioObjectPropertyAddress *in_addresses,
    void *in_client_data)
{
    CoreaudioVoiceOut *core = in_client_data;

    bql_lock();

    if (core->device_id) {
        fini_out_device(core);
    }

    init_out_device(core);

    if (core->device_id) {
        update_out_device_playback_state(core);
    }

    bql_unlock();
    return 0;
}

static int coreaudio_init_out(HWVoiceOut *hw, struct audsettings *as)
{
    OSStatus status;
    CoreaudioVoiceOut *core = (CoreaudioVoiceOut *)hw;
    int err;
    Audiodev *dev = hw->s->dev;
    AudiodevCoreaudioPerDirectionOptions *cpdo = dev->u.coreaudio.out;
    struct audsettings obt_as;

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

    obt_as = *as;
    as = &obt_as;
    as->fmt = AUDIO_FORMAT_F32;
    audio_pcm_init_info(&hw->info, as);

    core->frame_size_setting = audio_buffer_frames(
        qapi_AudiodevCoreaudioPerDirectionOptions_base(cpdo), as, 11610);

    core->buffer_count = cpdo->has_buffer_count ? cpdo->buffer_count : 4;

    status = AudioObjectAddPropertyListener(kAudioObjectSystemObject,
                                            &voice_out_addr,
                                            handle_voice_out_change,
                                            core);
    if (status != kAudioHardwareNoError) {
        coreaudio_playback_logerr(status,
                                  "Could not listen to voice property change");
        return -1;
    }

    if (init_out_device(core)) {
        status = AudioObjectRemovePropertyListener(kAudioObjectSystemObject,
                                                   &voice_out_addr,
                                                   handle_voice_out_change,
                                                   core);
        if (status != kAudioHardwareNoError) {
            coreaudio_playback_logerr(status,
                                      "Could not remove voice property change listener");
        }

        return -1;
    }

    return 0;
}

static void coreaudio_fini_out (HWVoiceOut *hw)
{
    OSStatus status;
    int err;
    CoreaudioVoiceOut *core = (CoreaudioVoiceOut *)hw;

    status = AudioObjectRemovePropertyListener(kAudioObjectSystemObject,
                                               &voice_out_addr,
                                               handle_voice_out_change,
                                               core);
    if (status != kAudioHardwareNoError) {
        coreaudio_logerr(status, "Could not remove voice property change listener");
    }

    fini_out_device(core);

    /* destroy mutex */
    err = pthread_mutex_destroy(&core->buf_mutex);
    if (err) {
        error_report("coreaudio: Could not destroy mutex: %s", strerror(err));
    }
}

static void coreaudio_enable_out(HWVoiceOut *hw, bool enable)
{
    CoreaudioVoiceOut *core = (CoreaudioVoiceOut *)hw;

    core->enabled = enable;
    update_out_device_playback_state(core);
}

static void audio_coreaudio_class_init(ObjectClass *klass, const void *data)
{
    AudioMixengBackendClass *k = AUDIO_MIXENG_BACKEND_CLASS(klass);

    k->max_voices_out = 1;
    k->max_voices_in = 0;
    k->voice_size_out = sizeof(CoreaudioVoiceOut);
    k->voice_size_in = 0;

    k->init_out = coreaudio_init_out;
    k->fini_out = coreaudio_fini_out;
    /* wrapper for audio_generic_write */
    k->write = coreaudio_write;
    /* wrapper for audio_generic_buffer_get_free */
    k->buffer_get_free = coreaudio_buffer_get_free;
    /* wrapper for audio_generic_get_buffer_out */
    k->get_buffer_out = coreaudio_get_buffer_out;
    /* wrapper for audio_generic_put_buffer_out */
    k->put_buffer_out = coreaudio_put_buffer_out;
    k->enable_out = coreaudio_enable_out;
}

static const TypeInfo audio_types[] = {
    {
        .name = TYPE_AUDIO_COREAUDIO,
        .parent = TYPE_AUDIO_MIXENG_BACKEND,
        .instance_size = sizeof(AudioCoreaudio),
        .class_init = audio_coreaudio_class_init,
    },
};

DEFINE_TYPES(audio_types)
module_obj(TYPE_AUDIO_COREAUDIO);
