/*
 * Copyright (c) 2015 Andrew Kelley
 *
 * This file is part of libsoundio, which is MIT licensed.
 * See http://opensource.org/licenses/MIT
 */

#include "soundio_private.h"
#include "os.h"
#include "util.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <stdint.h>

static int usage(char *exe) {
    fprintf(stderr, "Usage: %s [--backend dummy|alsa|pulseaudio|jack|coreaudio|wasapi] [--latency seconds]\n", exe);
    return 1;
}

static void write_sample_s16ne(char *ptr, double sample) {
    int16_t *buf = (int16_t *)ptr;
    double range = (double)INT16_MAX - (double)INT16_MIN;
    double val = sample * range / 2.0;
    *buf = val;
}

static void write_sample_s32ne(char *ptr, double sample) {
    int32_t *buf = (int32_t *)ptr;
    double range = (double)INT32_MAX - (double)INT32_MIN;
    double val = sample * range / 2.0;
    *buf = val;
}

static void write_sample_float32ne(char *ptr, double sample) {
    float *buf = (float *)ptr;
    *buf = sample;
}

static void write_sample_float64ne(char *ptr, double sample) {
    double *buf = (double *)ptr;
    *buf = sample;
}

static void (*write_sample)(char *ptr, double sample);

static int frames_until_pulse = 0;
static int pulse_frames_left = -1;
static const double PI = 3.14159265358979323846264338328;
static double seconds_offset = 0.0;

static struct SoundIoRingBuffer pulse_rb;

static void write_time(struct SoundIoOutStream *outstream, double extra) {
    double latency;
    int err;
    if ((err = soundio_outstream_get_latency(outstream, &latency))) {
        soundio_panic("getting latency: %s", soundio_strerror(err));
    }
    double now = soundio_os_get_time();
    double audible_time = now + latency + extra;
    double *write_ptr = (double *)soundio_ring_buffer_write_ptr(&pulse_rb);
    *write_ptr = audible_time;
    soundio_ring_buffer_advance_write_ptr(&pulse_rb, sizeof(double));
}

static void write_callback(struct SoundIoOutStream *outstream, int frame_count_min, int frame_count_max) {
    double float_sample_rate = outstream->sample_rate;
    double seconds_per_frame = 1.0f / float_sample_rate;
    struct SoundIoChannelArea *areas;
    int err;

    int frames_left = frame_count_max;

    while (frames_left > 0) {
        int frame_count = frames_left;

        if ((err = soundio_outstream_begin_write(outstream, &areas, &frame_count)))
            soundio_panic("begin write: %s", soundio_strerror(err));

        if (!frame_count)
            break;

        const struct SoundIoChannelLayout *layout = &outstream->layout;

        double pitch = 440.0;
        double radians_per_second = pitch * 2.0 * PI;
        for (int frame = 0; frame < frame_count; frame += 1) {
            double sample;
            if (frames_until_pulse <= 0) {
                if (pulse_frames_left == -1) {
                    pulse_frames_left = 0.25 * float_sample_rate;
                    write_time(outstream, seconds_per_frame * frame); // announce beep start
                }
                if (pulse_frames_left > 0) {
                    pulse_frames_left -= 1;
                    sample = sinf((seconds_offset + frame * seconds_per_frame) * radians_per_second);
                } else {
                    frames_until_pulse = (0.5 + (rand() / (double)RAND_MAX) * 2.0) * float_sample_rate;
                    pulse_frames_left = -1;
                    sample = 0.0;
                    write_time(outstream, seconds_per_frame * frame); // announce beep end
                }
            } else {
                frames_until_pulse -= 1;
                sample = 0.0;
            }
            for (int channel = 0; channel < layout->channel_count; channel += 1) {
                write_sample(areas[channel].ptr, sample);
                areas[channel].ptr += areas[channel].step;
            }
        }

        seconds_offset += seconds_per_frame * frame_count;

        if ((err = soundio_outstream_end_write(outstream)))
            soundio_panic("end write: %s", soundio_strerror(err));

        frames_left -= frame_count;
    }
}

static void underflow_callback(struct SoundIoOutStream *outstream) {
    soundio_panic("underflow\n");
}

int main(int argc, char **argv) {
    char *exe = argv[0];
    enum SoundIoBackend backend = SoundIoBackendNone;
    double software_latency = 0.0;
    for (int i = 1; i < argc; i += 1) {
        char *arg = argv[i];
        if (arg[0] == '-' && arg[1] == '-') {
            i += 1;
            if (i >= argc) {
                return usage(exe);
            } else if (strcmp(arg, "--backend") == 0) {
                if (strcmp("dummy", argv[i]) == 0) {
                    backend = SoundIoBackendDummy;
                } else if (strcmp("alsa", argv[i]) == 0) {
                    backend = SoundIoBackendAlsa;
                } else if (strcmp("pulseaudio", argv[i]) == 0) {
                    backend = SoundIoBackendPulseAudio;
                } else if (strcmp("jack", argv[i]) == 0) {
                    backend = SoundIoBackendJack;
                } else if (strcmp("coreaudio", argv[i]) == 0) {
                    backend = SoundIoBackendCoreAudio;
                } else if (strcmp("wasapi", argv[i]) == 0) {
                    backend = SoundIoBackendWasapi;
                } else {
                    fprintf(stderr, "Invalid backend: %s\n", argv[i]);
                    return 1;
                }
            } else if (strcmp(arg, "--latency") == 0) {
                software_latency = atof(argv[i]);
            } else {
                return usage(exe);
            }
        } else {
            return usage(exe);
        }
    }

    struct SoundIo *soundio;
    if (!(soundio = soundio_create()))
        soundio_panic("out of memory");

    int err = (backend == SoundIoBackendNone) ?
        soundio_connect(soundio) : soundio_connect_backend(soundio, backend);

    if (err)
        soundio_panic("error connecting: %s", soundio_strerror(err));

    soundio_flush_events(soundio);

    int default_out_device_index = soundio_default_output_device_index(soundio);
    if (default_out_device_index < 0)
        soundio_panic("no output device found");

    struct SoundIoDevice *device = soundio_get_output_device(soundio, default_out_device_index);
    if (!device)
        soundio_panic("out of memory");

    fprintf(stderr, "Output device: %s\n", device->name);

    struct SoundIoOutStream *outstream = soundio_outstream_create(device);
    outstream->format = SoundIoFormatFloat32NE;
    outstream->write_callback = write_callback;
    outstream->underflow_callback = underflow_callback;
    outstream->software_latency = software_latency;

    if (soundio_device_supports_format(device, SoundIoFormatFloat32NE)) {
        outstream->format = SoundIoFormatFloat32NE;
        write_sample = write_sample_float32ne;
    } else if (soundio_device_supports_format(device, SoundIoFormatFloat64NE)) {
        outstream->format = SoundIoFormatFloat64NE;
        write_sample = write_sample_float64ne;
    } else if (soundio_device_supports_format(device, SoundIoFormatS32NE)) {
        outstream->format = SoundIoFormatS32NE;
        write_sample = write_sample_s32ne;
    } else if (soundio_device_supports_format(device, SoundIoFormatS16NE)) {
        outstream->format = SoundIoFormatS16NE;
        write_sample = write_sample_s16ne;
    } else {
        soundio_panic("No suitable device format available.\n");
    }

    if ((err = soundio_ring_buffer_init(&pulse_rb, 1024)))
        soundio_panic("ring buffer init: %s", soundio_strerror(err));

    if ((err = soundio_outstream_open(outstream)))
        soundio_panic("unable to open device: %s", soundio_strerror(err));

    if (outstream->layout_error)
        fprintf(stderr, "unable to set channel layout: %s\n", soundio_strerror(outstream->layout_error));

    if ((err = soundio_outstream_start(outstream)))
        soundio_panic("unable to start device: %s", soundio_strerror(err));

    bool beep_on = true;
    int count = 0;
    for (;;) {
        int fill_count = soundio_ring_buffer_fill_count(&pulse_rb);
        if (fill_count >= (int)sizeof(double)) {
            double *read_ptr = (double *)soundio_ring_buffer_read_ptr(&pulse_rb);
            double audible_time = *read_ptr;
            while (audible_time > soundio_os_get_time()) {
                // Burn the CPU while we wait for our precisely timed event.
            }
            if (beep_on) {
                fprintf(stderr, "BEEP %d start\n", count);
            } else {
                fprintf(stderr, "BEEP %d end\n", count++);
            }
            fflush(stderr);
            double off_by = soundio_os_get_time() - audible_time;
            if (off_by > 0.0001)
                fprintf(stderr, "off by %f\n", off_by);
            beep_on = !beep_on;
            soundio_ring_buffer_advance_read_ptr(&pulse_rb, sizeof(double));
        }
    }

    soundio_outstream_destroy(outstream);
    soundio_device_unref(device);
    soundio_destroy(soundio);
    return 0;
}

