#include "AudioEvent.hpp"

#include <iostream>
#include <bits/unique_lock.h>

#include "utils/audio/audio.hpp"


namespace drp::event {


AudioEvent::AudioEvent() {
    this->stream = nullptr;
    this->audioLock = std::unique_lock(this->audioMutex);

    this->streamChannels = 0;
    this->streamSampleFormat = 0;
    this->streamRate = 0;

    // start a thread for the player
    this->playerThread = std::thread(&AudioEvent::loopPlay, this);
}

AudioEvent::~AudioEvent() {
    // stop any currently playing audio
    Pa_StopStream(this->stream);
    // close the audio stream
    if (const PaError error = Pa_CloseStream(this->stream))
        std::cerr << "[Event - Audio] Could not close the stream: " << std::string(Pa_GetErrorText(error)) << std::endl;
}


void AudioEvent::handle(
    Context& context,
    std::vector<std::uint8_t>& data,
    const sockaddr_storage& fromAddress,
    const socklen_t fromAddressLength
) {
    // get the audio data in the content
    const auto audioData = packet::audio::AudioPacketData::deserialize(data);

    // save it in the audio queue
    this->audioQueue.push(audioData);
    // notify that a new audio chunk is available
    this->audioCondition.notify_one();
}


void AudioEvent::updateAudioStream(const int channels, const std::uint32_t sampleFormat, const double sampleRate) {
    // check if any information changed. If no, ignore this
    if (
        this->streamChannels == channels &&
        this->streamSampleFormat == sampleFormat &&
        this->streamRate == sampleRate
    )
        return;

    // close the current stream
    // ignore errors that could happen if no audio is currently playing
    Pa_CloseStream(&this->stream);

    // open a new stream with the new settings
    if (const PaError error = Pa_OpenDefaultStream(
        &this->stream,
        0,
        channels,
        sampleFormat,
        sampleRate,
        paFramesPerBufferUnspecified,
        nullptr,
        nullptr
    ) != paNoError)
        throw std::runtime_error("[Event - Audio] Could not open the stream: " + std::string(Pa_GetErrorText(error)));

    // update the new audio values
    this->streamChannels = channels;
    this->streamSampleFormat = sampleFormat;
    this->streamRate = sampleRate;
}


void AudioEvent::loopPlay() {
    while (true) {
        // wait for a new element in the audio queue
        this->audioCondition.wait(
            this->audioLock,
            [this] { return !this->audioQueue.empty(); }
        );
        // get the most recent audio chunk
        const auto audioPacket = this->audioQueue.top();

        // if the packet should have started playing before, skip it
        if (audioPacket.timePlay < std::chrono::high_resolution_clock::now()) {
            this->audioQueue.pop();
            continue;
        }

        // update the stream with the new audio settings
        this->updateAudioStream(
            audioPacket.channels,
            audioPacket.sampleFormat,
            audioPacket.sampleRate
        );

        // wait until it must be played
        std::this_thread::sleep_until(audioPacket.timePlay);

        auto cTimePlay = std::chrono::high_resolution_clock::to_time_t(audioPacket.timePlay);
        std::cout << "[Event - Audio] Playing: " << std::ctime(&cTimePlay) << std::endl;

        // immediately stop playing music
        // this avoids an offset created if this client's clock is too ahead of the others
        // don't handle errors since audio might not be playing before
        Pa_AbortStream(this->stream);

        // play the new audio data
        if (const int error = Pa_StartStream(this->stream) != paNoError)
            throw std::runtime_error("[Event - Audio] Could not start the PortAudio stream: " + std::string(Pa_GetErrorText(error)));

        // write the new audio data into the audio buffer
        const int error = Pa_WriteStream(
            this->stream,
            audioPacket.content.data(),
            audioPacket.content.size() / Pa_GetSampleSize(this->streamSampleFormat) / this->streamChannels
        );
        switch (error) {
            // success
            case paNoError:
                break;
            // the output might be very slightly underflowed,
            // causing a very small period where no noise will be played.
            case paOutputUnderflowed:
                break;

            default:
                std::cerr << "[Event - Audio] Could not write to the audio stream: " << Pa_GetErrorText(error) << std::endl;
        }

        // remove the audio chunk
        this->audioQueue.pop();
    }
}


}
