/* -*- c++ -*- */
/*
 * Copyright 2006-2011,2013-2014 Free Software Foundation, Inc.
 *
 * This file is part of GNU Radio
 *
 * SPDX-License-Identifier: GPL-3.0-or-later
 *
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#ifdef _MSC_VER
#include <io.h>
#endif

#include "../audio_registry.h"
#include "portaudio_impl.h"
#include "portaudio_sink.h"
#include <gnuradio/io_signature.h>
#include <gnuradio/prefs.h>
#include <string_view>
#include <cstdio>
#include <future>
#include <stdexcept>

namespace gr {
namespace audio {

sink::sptr
portaudio_sink_fcn(int sampling_rate, const std::string& device_name, bool ok_to_block)
{
    return sink::sptr(new portaudio_sink(sampling_rate, device_name, ok_to_block));
}


constexpr auto SAMPLE_FORMAT = paFloat32;
typedef float sample_t;

// Number of portaudio buffers in the ringbuffer
constexpr unsigned int N_BUFFERS = 4;

static std::string default_device_name()
{
    return prefs::singleton()->get_string("audio_portaudio", "default_output_device", "");
}

void portaudio_sink::create_ringbuffer(void)
{
    int bufsize_samples =
        d_portaudio_buffer_size_frames * d_output_parameters.channelCount;

    if (d_verbose)
        d_debug_logger->info("ring buffer size  = {:d} frames",
                             N_BUFFERS * bufsize_samples /
                                 d_output_parameters.channelCount);

    // FYI, the buffer indices are in units of samples.
    d_writer = gr::make_buffer(
        N_BUFFERS * bufsize_samples, sizeof(sample_t), N_BUFFERS * bufsize_samples, 1);
    d_reader = gr::buffer_add_reader(d_writer, 0);
}


void portaudio_underrun_notification(gr::logger_ptr logger)
{
    auto r = ::write(2, "aU", 2);
    if (r == -1) {
        logger->error("portaudio_source_callback write error to stderr.");
    }
}
/*
 * This routine will be called by the PortAudio engine when audio is needed.
 * It may called at interrupt level on some machines so don't do anything
 * that could mess up the system like calling malloc() or free().
 *
 * Our job is to write framesPerBuffer frames into outputBuffer.
 */
int portaudio_sink_callback(const void* inputBuffer,
                            void* outputBuffer,
                            unsigned long framesPerBuffer,
                            const PaStreamCallbackTimeInfo* timeInfo,
                            PaStreamCallbackFlags statusFlags,
                            void* arg)
{
    auto self = reinterpret_cast<portaudio_sink*>(arg);
    int nreqd_samples = framesPerBuffer * self->d_output_parameters.channelCount;

    int navail_samples = self->d_reader->items_available();

    if (nreqd_samples <= navail_samples) { // We've got enough data...
        {
            gr::thread::scoped_lock guard(self->d_ringbuffer_mutex);

            memcpy(outputBuffer,
                   self->d_reader->read_pointer(),
                   nreqd_samples * sizeof(sample_t));
            self->d_reader->update_read_pointer(nreqd_samples);

            self->d_ringbuffer_ready = true;
        }

        // Tell the sink thread there is new room in the ringbuffer.
        self->d_ringbuffer_cond.notify_one();
        return paContinue;
    }

    else { // underrun
        auto future_local = std::async(&portaudio_underrun_notification, self->d_logger);
        // FIXME we should transfer what we've got and pad the rest
        memset(outputBuffer, 0, nreqd_samples * sizeof(sample_t));

        self->d_ringbuffer_ready = true;
        self->d_ringbuffer_cond.notify_one(); // Tell the sink to get going!

        return paContinue;
    }
}

// ----------------------------------------------------------------

portaudio_sink::portaudio_sink(int sampling_rate,
                               const std::string device_name,
                               bool ok_to_block)
    : sync_block("audio_portaudio_sink",
                 io_signature::make(0, 0, 0),
                 io_signature::make(0, 0, 0)),
      d_sampling_rate(sampling_rate),
      d_device_name(device_name.empty() ? default_device_name() : device_name),
      d_ok_to_block(ok_to_block),
      d_verbose(prefs::singleton()->get_bool("audio_portaudio", "verbose", false)),
      d_portaudio_buffer_size_frames(0),
      d_stream(0),
      d_ringbuffer_mutex(),
      d_ringbuffer_cond(),
      d_ringbuffer_ready(false)
{
    memset(&d_output_parameters, 0, sizeof(d_output_parameters));

    PaError err;
    int i, numDevices;
    PaDeviceIndex device = 0;
    const PaDeviceInfo* deviceInfo = NULL;

    err = Pa_Initialize();
    if (err != paNoError) {
        bail("Initialize failed", err);
    }

    if (d_verbose)
        print_devices(d_logger);

    numDevices = Pa_GetDeviceCount();
    if (numDevices < 0)
        bail("Pa Device count failed", 0);
    if (numDevices == 0)
        bail("no devices available", 0);

    if (d_device_name.empty()) {
        // FIXME Get smarter about picking something
        d_debug_logger->info("Using Default Devicee");
        device = Pa_GetDefaultOutputDevice();
        deviceInfo = Pa_GetDeviceInfo(device);
        d_logger->info("{:s} is the chosen device using {:s} as the host",
                       deviceInfo->name,
                       Pa_GetHostApiInfo(deviceInfo->hostApi)->name);
    } else {
        bool found = false;
        d_debug_logger->info("Test Devices");
        for (i = 0; i < numDevices; i++) {
            deviceInfo = Pa_GetDeviceInfo(i);
            d_debug_logger->info("Testing device name: {:s}...", deviceInfo->name);
            if (deviceInfo->maxOutputChannels <= 0) {
                continue;
            }

            if (std::string_view::npos !=
                std::string_view{ deviceInfo->name }.find(d_device_name)) {
                d_debug_logger->info("  Chosen!");
                device = i;
                d_debug_logger->info("{:s} using {:s} as the host",
                                     d_device_name,
                                     Pa_GetHostApiInfo(deviceInfo->hostApi)->name);
                found = true;
                deviceInfo = Pa_GetDeviceInfo(device);
                i = numDevices; // force loop exit
            }
        }

        if (!found) {
            bail("Failed to find specified device name", 0);
        }
    }

    d_output_parameters.device = device;
    d_output_parameters.channelCount = deviceInfo->maxOutputChannels;
    d_output_parameters.sampleFormat = SAMPLE_FORMAT;
    d_output_parameters.suggestedLatency = deviceInfo->defaultLowOutputLatency;
    d_output_parameters.hostApiSpecificStreamInfo = NULL;

    // We fill in the real channelCount in check_topology when we know
    // how many inputs are connected to us.

    // Now that we know the maximum number of channels (allegedly)
    // supported by the h/w, we can compute a reasonable input
    // signature.  The portaudio specs say that they'll accept any
    // number of channels from 1 to max.
    set_input_signature(
        io_signature::make(1, deviceInfo->maxOutputChannels, sizeof(sample_t)));
}

bool portaudio_sink::check_topology(int ninputs, int noutputs)
{
    PaError err;

    if (Pa_IsStreamActive(d_stream)) {
        Pa_CloseStream(d_stream);
        d_stream = 0;
        d_reader.reset(); // std::shared_ptr for d_reader = 0
        d_writer.reset(); // std::shared_ptr for d_write = 0
    }

    d_output_parameters.channelCount = ninputs; // # of channels we're really using

#if 1
    d_portaudio_buffer_size_frames =
        (int)(0.0213333333 * d_sampling_rate + 0.5); // Force 1024 frame buffers at 48000
    d_logger->info(
        "Latency = {:8.5f}, requested sampling_rate = {}", 0.0213333333, d_sampling_rate);
#endif
    err = Pa_OpenStream(&d_stream,
                        NULL, // No input
                        &d_output_parameters,
                        d_sampling_rate,
                        d_portaudio_buffer_size_frames,
                        paClipOff,
                        &portaudio_sink_callback,
                        (void*)this);

    if (err != paNoError) {
        output_error_msg("OpenStream failed", err);
        return false;
    }
    const PaStreamInfo* psi = Pa_GetStreamInfo(d_stream);
    if (true || std::abs(psi->sampleRate - d_sampling_rate) > 0.1) {
        d_logger->error("Requested sampling rate {} not achieved; using {} instead",
                        d_sampling_rate,
                        psi->sampleRate);
    }

#if 0

    d_portaudio_buffer_size_frames =
        (int)(d_output_parameters.suggestedLatency * psi->sampleRate);
    d_logger->info("Latency = {:7.4f}, psi->sampleRate = {:g}",
                    d_input_parameters.suggestedLatency,
                    psi->sampleRate);
#endif
    d_logger->info("d_portaudio_buffer_size_frames = {:d}",
                   d_portaudio_buffer_size_frames);

    assert(d_portaudio_buffer_size_frames != 0);

    create_ringbuffer();

    err = Pa_StartStream(d_stream);
    if (err != paNoError) {
        output_error_msg("StartStream failed", err);
        return false;
    }

    return true;
}

portaudio_sink::~portaudio_sink()
{
    Pa_StopStream(d_stream); // wait for output to drain
    Pa_CloseStream(d_stream);
    Pa_Terminate();
}

/*
 * This version consumes everything sent to it, blocking if required.
 * I think this will allow us better control of the total buffering/latency
 * in the audio path.
 */
int portaudio_sink::work(int noutput_items,
                         gr_vector_const_void_star& input_items,
                         gr_vector_void_star& output_items)
{
    const float** in = (const float**)&input_items[0];
    const unsigned nchan =
        d_output_parameters.channelCount; // # of channels == samples/frame

    int k;
    for (k = 0; k < noutput_items;) {
        int nframes = d_writer->space_available() / nchan; // How much space in ringbuffer
        if (nframes == 0) {                                // no room...
            if (d_ok_to_block) {
                {
                    gr::thread::scoped_lock guard(d_ringbuffer_mutex);
                    while (!d_ringbuffer_ready)
                        d_ringbuffer_cond.wait(guard);
                }
                continue;
            } else {
                // There's no room and we're not allowed to block.
                // (A USRP is most likely controlling the pacing through the pipeline.)
                // We drop the samples on the ground, and say we processed them all ;)
                //
                // FIXME, there's probably room for a bit more finesse here.
                return noutput_items;
            }
        }

        // We can write the smaller of the request and the room we've got
        {
            gr::thread::scoped_lock guard(d_ringbuffer_mutex);

            int nf = std::min(noutput_items - k, nframes);
            float* p = (float*)d_writer->write_pointer();

            for (int i = 0; i < nf; i++) {
                for (unsigned int c = 0; c < nchan; c++) {
                    *p++ = in[c][k + i];
                }
            }

            d_writer->update_write_pointer(nf * nchan);
            k += nf;

            d_ringbuffer_ready = false;
        }
    }

    return k; // tell how many we actually did
}

void portaudio_sink::output_error_msg(const char* msg, int err)
{
    d_logger->error("{:s}: {:s} {:s}", d_device_name, msg, Pa_GetErrorText(err));
}

void portaudio_sink::bail(const char* msg, int err)
{
    output_error_msg(msg, err);
    throw std::runtime_error("audio_portaudio_sink");
}

} /* namespace audio */
} /* namespace gr */
