/*
 * Fooyin
 * Copyright © 2024, Luke Taylor <luket@pm.me>
 *
 * Fooyin is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Fooyin is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Fooyin.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

#include "pipewirestream.h"

#include "pipewireutils.h"

#include <core/engine/audiobuffer.h>

#include <pipewire/keys.h>
#include <pipewire/version.h>
#include <spa/param/param.h>
#include <spa/param/props.h>
#include <spa/pod/builder.h>
#include <spa/pod/vararg.h>

#include <QDebug>

#include <array>

#ifdef __clang__
#pragma clang diagnostic ignored "-Wgnu-statement-expression-from-macro-expansion"
#pragma clang diagnostic ignored "-Wc99-extensions"
#endif

namespace {
#if !PW_CHECK_VERSION(0, 3, 50)
int pw_stream_get_time_n(struct pw_stream* stream, struct pw_time* time, size_t /*size*/)
{
    return pw_stream_get_time(stream, time);
}
#endif
} // namespace

namespace Fooyin::Pipewire {
PipewireStream::PipewireStream(PipewireCore* core, const AudioFormat& format, const int targetBufferFrames,
                               const QString& device)
    : m_channelCount{static_cast<uint32_t>(std::max(1, format.channelCount()))}
{
    struct pw_properties* props = pw_properties_new(PW_KEY_MEDIA_TYPE, "Audio", PW_KEY_MEDIA_CATEGORY, "Playback",
                                                    PW_KEY_MEDIA_ROLE, "Music", PW_KEY_APP_ID, "fooyin",
                                                    PW_KEY_APP_ICON_NAME, "fooyin", PW_KEY_APP_NAME, "fooyin", nullptr);

    pw_properties_setf(props, PW_KEY_NODE_RATE, "1/%u", format.sampleRate());
    if(targetBufferFrames > 0 && format.sampleRate() > 0) {
        pw_properties_setf(props, PW_KEY_NODE_LATENCY, "%u/%u", static_cast<uint32_t>(targetBufferFrames),
                           static_cast<uint32_t>(format.sampleRate()));
    }

    if(!device.isEmpty()) {
        pw_properties_setf(props, PW_KEY_TARGET_OBJECT, "%s", device.toUtf8().constData());
    }

    m_stream.reset(pw_stream_new(core->core(), "Playback", props));

    if(!m_stream) {
        qCWarning(PIPEWIRE) << "Failed to create stream";
    }
}

PipewireStream::~PipewireStream()
{
    spa_hook_remove(&m_streamListener);
}

pw_stream_state PipewireStream::state()
{
    return pw_stream_get_state(m_stream.get(), nullptr);
}

std::optional<PipewireStream::TimeInfo> PipewireStream::time() const
{
    if(!m_stream) {
        return {};
    }

    pw_time streamTime{};
    if(pw_stream_get_time_n(m_stream.get(), &streamTime, sizeof(streamTime)) < 0) {
        return {};
    }

    TimeInfo info;
    info.now    = streamTime.now;
    info.rate   = streamTime.rate;
    info.ticks  = streamTime.ticks;
    info.delay  = streamTime.delay;
    info.queued = streamTime.queued;
#if PW_CHECK_VERSION(0, 3, 50)
    info.buffered      = streamTime.buffered;
    info.queuedBuffers = streamTime.queued_buffers;
    info.availBuffers  = streamTime.avail_buffers;
#endif
#if PW_CHECK_VERSION(1, 1, 0)
    info.size = streamTime.size;
#endif

    return info;
}

void PipewireStream::setActive(bool active)
{
    pw_stream_set_active(m_stream.get(), active);
}

void PipewireStream::setVolume(float volume)
{
    std::vector channelVolumes(m_channelCount, volume);
    if(pw_stream_set_control(m_stream.get(), SPA_PROP_channelVolumes, m_channelCount, channelVolumes.data(), 0) < 0) {
        qCWarning(PIPEWIRE) << "Failed to set channel volumes";

        if(pw_stream_set_control(m_stream.get(), SPA_PROP_volume, 1, &volume, 0) < 0) {
            qCWarning(PIPEWIRE) << "Failed to set volume";
        }
    }

#if PW_CHECK_VERSION(0, 3, 70)
    std::array<uint8_t, 128> paramBuffer;
    auto builder      = SPA_POD_BUILDER_INIT(paramBuffer.data(), paramBuffer.size());
    const auto* param = static_cast<const spa_pod*>(spa_pod_builder_add_object(
        &builder, SPA_TYPE_OBJECT_Props, SPA_PARAM_Props, SPA_PROP_mute, SPA_POD_Bool(volume <= 0.0F)));
    if(pw_stream_set_param(m_stream.get(), SPA_PARAM_Props, param) < 0) {
        qCWarning(PIPEWIRE) << "Failed to set mute state";
    }
#endif
}

pw_buffer* PipewireStream::dequeueBuffer()
{
    return pw_stream_dequeue_buffer(m_stream.get());
}

void PipewireStream::queueBuffer(pw_buffer* buffer)
{
    pw_stream_queue_buffer(m_stream.get(), buffer);
}

void PipewireStream::flush(bool drain)
{
    pw_stream_flush(m_stream.get(), drain);
}

bool PipewireStream::connect(uint32_t id, const spa_direction& direction, std::vector<const spa_pod*>& params,
                             const pw_stream_flags& flags)
{
    if(pw_stream_connect(m_stream.get(), direction, id, flags, params.data(), params.size()) < 0) {
        qCWarning(PIPEWIRE) << "Failed to connect to stream";
        return false;
    }

    return true;
}

void PipewireStream::addListener(const pw_stream_events& streamEvents, void* data)
{
    pw_stream_add_listener(m_stream.get(), &m_streamListener, &streamEvents, data);
}
} // namespace Fooyin::Pipewire
