/*
Simpleaudio Python Extension
Copyright (C) 2015, Joe Hamilton
MIT License (see LICENSE.txt)
*/

#include "simpleaudio.h"
#include <Windows.h>
#include <mmreg.h>
#include <stdlib.h>

#define SYS_STR_LEN (SA_ERR_STR_LEN / 2)

#define NUM_BUFS (2)

#define WIN_EXCEPTION(my_msg, code, err_msg, str_ptr) \
    snprintf(str_ptr, SA_ERR_STR_LEN, "%s -- CODE: %d -- MSG: %s", my_msg, code, err_msg); \
    PyErr_SetString(sa_python_error, str_ptr);

enum {
    FILL_BUFFER_WRITE = 0,
    FILL_BUFFER_DRAINING = 1,
    FILL_BUFFER_DONE = 2
};


MMRESULT fill_buffer(WAVEHDR* wave_header, audio_blob_t* audio_blob) {
    int want = wave_header->dwBufferLength;
    int have = audio_blob->len_bytes - audio_blob->used_bytes;
    int stop_flag = 0;
    MMRESULT result;

    dbg2("fill_buffer call with audio blob at %p\n", audio_blob);

    grab_mutex(audio_blob->play_list_item->mutex);
    stop_flag = audio_blob->play_list_item->stop_flag;
    release_mutex(audio_blob->play_list_item->mutex);

    dbg2("stop flag: %d\n", stop_flag);

    /* if there's still audio yet to buffer ... */
    if (have > 0 && !stop_flag) {
        dbg2("still filling buffers\n");

        if (have > want) {have = want;}
        result = waveOutUnprepareHeader(audio_blob->handle, wave_header, sizeof(WAVEHDR));
        if (result != MMSYSERR_NOERROR) {return result;}
        memcpy(wave_header->lpData, &((char*)audio_blob->buffer_obj.buf)[audio_blob->used_bytes], have);
        result = waveOutPrepareHeader(audio_blob->handle, wave_header, sizeof(WAVEHDR));
        if (result != MMSYSERR_NOERROR) {return result;}
        result = waveOutWrite(audio_blob->handle, wave_header, sizeof(WAVEHDR));
        if (result != MMSYSERR_NOERROR) {return result;}
        wave_header->dwBufferLength = have;
        audio_blob->used_bytes += have;
    /* ... no more audio left to buffer */
    } else {
        if (audio_blob->num_buffers > 0) {
            dbg2("done buffering - dellocating a buffer\n");

            PyMem_RawFree(wave_header->lpData);
            PyMem_RawFree(wave_header);
            audio_blob->num_buffers--;
        }
        if (audio_blob->num_buffers == 0) {
            dbg2("playback finished - cleaning up\n");

            /* all done, cleanup */
            waveOutClose(audio_blob->handle);
            destroy_audio_blob(audio_blob);
            /* admitted, this is terrible */
            return MMSYSERR_NOERROR - 1;
        }
    }

    return MMSYSERR_NOERROR;
}

DWORD WINAPI bufferThread(LPVOID thread_param) {
    audio_blob_t* audio_blob = (audio_blob_t*)thread_param;
    MSG message;
    WAVEHDR* wave_header;
    MMRESULT result;

    dbg1("buffer thread started with audio blob at %p\n", thread_param);

    /* wait for the "audio block done" message" */
    while (1) {
        GetMessage(&message, NULL, 0, 0);
        if (message.message == MM_WOM_DONE) {
            wave_header = (WAVEHDR*)message.lParam;
            result = fill_buffer(wave_header, audio_blob);
            if (result != MMSYSERR_NOERROR) {
                dbg1("result code %d - quitting buffer loop\n", result);

                break;
            }
        }
        if (message.message == WM_QUIT) {
            dbg1("got WM_QUIT message\n");

            break;
        }
    }

    dbg1("buffer thread done\n");

    return 0;
}

PyObject* play_os(Py_buffer buffer_obj, int len_samples, int num_channels, int bytes_per_chan,
                  int sample_rate, play_item_t* play_list_head, int latency_us) {
    char err_msg_buf[SA_ERR_STR_LEN];
    char sys_msg_buf[SA_ERR_STR_LEN / 2];
    audio_blob_t* audio_blob;
    WAVEFORMATEX audio_format;
    MMRESULT result;
    HANDLE thread_handle = NULL;
    DWORD thread_id;
    int bytes_per_frame = bytes_per_chan * num_channels;
    WAVEHDR* temp_wave_hdr;
    int buffer_size;
    int i;

    DBG_PLAY_OS_CALL

    buffer_size = get_buffer_size(latency_us / NUM_BUFS, sample_rate, bytes_per_chan * num_channels);

    audio_blob = create_audio_blob();
    audio_blob->buffer_obj = buffer_obj;
    audio_blob->list_mutex = play_list_head->mutex;
    audio_blob->len_bytes = len_samples * bytes_per_frame;
    audio_blob->num_buffers = NUM_BUFS;

    /* setup the linked list item for this playback buffer */
    grab_mutex(play_list_head->mutex);
    audio_blob->play_list_item = new_list_item(play_list_head);
    release_mutex(play_list_head->mutex);

    /* windows audio device and format headers setup */
    if (bytes_per_chan < 4) {
        audio_format.wFormatTag = WAVE_FORMAT_PCM;
    } else {
        audio_format.wFormatTag = WAVE_FORMAT_IEEE_FLOAT;
    }
    audio_format.nChannels = num_channels;

    audio_format.nSamplesPerSec = sample_rate;
    audio_format.nBlockAlign = bytes_per_frame;
    /* per MSDN WAVEFORMATEX documentation */
    audio_format.nAvgBytesPerSec = audio_format.nSamplesPerSec * audio_format.nBlockAlign;
    audio_format.wBitsPerSample = bytes_per_chan * 8;
    audio_format.cbSize = 0;

    /* create the cleanup thread so we can return after calling waveOutWrite
       SEE :http://msdn.microsoft.com/en-us/library/windows/desktop/ms682516(v=vs.85).aspx
    */
    thread_handle = CreateThread(NULL, 0, bufferThread, audio_blob, 0, &thread_id);
    if (thread_handle != NULL) {
        /* Close so we don't leak handles - similar to detatched POSIX threads */
        CloseHandle(thread_handle);
    } else {
        DWORD lastError = GetLastError();
        /* lang code : US En */
        FormatMessage((FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS), NULL, lastError, 0x0409, sys_msg_buf, SYS_STR_LEN, NULL);
        WIN_EXCEPTION("Failed to start cleanup thread.", 0, sys_msg_buf, err_msg_buf);

        destroy_audio_blob(audio_blob);
        return NULL;
    }

    /* open a handle to the default audio device */
    result = waveOutOpen((HWAVEOUT*)&audio_blob->handle, WAVE_MAPPER, &audio_format, thread_id, 0, CALLBACK_THREAD);
    if (result != MMSYSERR_NOERROR) {
        waveOutGetErrorText(result, sys_msg_buf, SYS_STR_LEN);
        WIN_EXCEPTION("Failed to open audio device.", result, sys_msg_buf, err_msg_buf);

        PostThreadMessage(thread_id, WM_QUIT, 0, 0);
        destroy_audio_blob(audio_blob);
        return NULL;
    }

    dbg1("allocating %d buffers of %d bytes\n", NUM_BUFS, buffer_size);

    for (i = 0; i < NUM_BUFS; i++) {
        temp_wave_hdr = PyMem_RawMalloc(sizeof(WAVEHDR));
        memset(temp_wave_hdr, 0, sizeof(WAVEHDR));
        temp_wave_hdr->lpData = PyMem_RawMalloc(buffer_size);
        temp_wave_hdr->dwBufferLength = buffer_size;

        result = fill_buffer(temp_wave_hdr, audio_blob);
        if (result != MMSYSERR_NOERROR) {
            waveOutGetErrorText(result, sys_msg_buf, SYS_STR_LEN);
            WIN_EXCEPTION("Failed to buffer audio.", result, sys_msg_buf, err_msg_buf);

            PostThreadMessage(thread_id, WM_QUIT, 0, 0);
            waveOutUnprepareHeader(audio_blob->handle, temp_wave_hdr, sizeof(WAVEHDR));
            waveOutClose(audio_blob->handle);
            destroy_audio_blob(audio_blob);
            return NULL;
        }
    }

    return PyLong_FromUnsignedLongLong(audio_blob->play_list_item->play_id);
}
