/*
 * Copyright (c) 2023-2025, Gregory Bertilson <gregory@ladybird.org>
 *
 * SPDX-License-Identifier: BSD-2-Clause
 */

#pragma once

#include "Forward.h"
#include "PlaybackStream.h"
#include <AK/Atomic.h>
#include <AK/AtomicRefCounted.h>
#include <AK/Error.h>
#include <AK/NonnullRefPtr.h>
#include <AK/Time.h>
#include <LibMedia/Audio/SampleSpecification.h>
#include <LibMedia/Export.h>
#include <pulse/pulseaudio.h>

namespace Audio {

class PulseAudioStream;

enum class PulseAudioContextState {
    Unconnected = PA_CONTEXT_UNCONNECTED,
    Connecting = PA_CONTEXT_CONNECTING,
    Authorizing = PA_CONTEXT_AUTHORIZING,
    SettingName = PA_CONTEXT_SETTING_NAME,
    Ready = PA_CONTEXT_READY,
    Failed = PA_CONTEXT_FAILED,
    Terminated = PA_CONTEXT_TERMINATED,
};

enum class PulseAudioErrorCode;

using PulseAudioDataRequestCallback = Function<ReadonlySpan<float>(PulseAudioStream&, Span<float> buffer)>;

// A wrapper around the PulseAudio main loop and context structs.
// Generally, only one instance of this should be needed for a single process.
class MEDIA_API PulseAudioContext
    : public AtomicRefCounted<PulseAudioContext> {
public:
    static ErrorOr<NonnullRefPtr<PulseAudioContext>> the();
    static bool is_connected();

    explicit PulseAudioContext(pa_threaded_mainloop*, pa_mainloop_api*, pa_context*);
    PulseAudioContext(PulseAudioContext const& other) = delete;
    ~PulseAudioContext();

    bool current_thread_is_main_loop_thread();
    void lock_main_loop();
    void unlock_main_loop();
    [[nodiscard]] auto main_loop_locker()
    {
        lock_main_loop();
        return ScopeGuard([this]() { unlock_main_loop(); });
    }
    // Waits for signal_to_wake() to be called.
    // This must be called with the main loop locked.
    void wait_for_signal();
    // Signals to wake all threads from calls to signal_to_wake()
    void signal_to_wake();

    PulseAudioContextState get_connection_state();
    bool connection_is_good();
    PulseAudioErrorCode get_last_error();

    void request_device_sample_specification();

    ErrorOr<NonnullRefPtr<PulseAudioStream>> create_stream(OutputState, u32 target_latency_ms, PulseAudioDataRequestCallback);

private:
    friend class PulseAudioStream;

    PulseAudioContext*& nullable_instance();

    pa_threaded_mainloop* m_main_loop { nullptr };
    pa_mainloop_api* m_api { nullptr };
    pa_context* m_context;

    SampleSpecification m_device_sample_specification;
};

enum class PulseAudioStreamState {
    Unconnected = PA_STREAM_UNCONNECTED,
    Creating = PA_STREAM_CREATING,
    Ready = PA_STREAM_READY,
    Failed = PA_STREAM_FAILED,
    Terminated = PA_STREAM_TERMINATED,
};

class PulseAudioStream : public AtomicRefCounted<PulseAudioStream> {
public:
    static constexpr bool start_corked = true;

    ~PulseAudioStream();

    PulseAudioStreamState get_connection_state();
    bool connection_is_good();

    // Sets the callback to be run when the server consumes more of the buffer than
    // has been written yet.
    void set_underrun_callback(Function<void()>);

    SampleSpecification sample_specification();
    u32 sample_rate();
    size_t sample_size();
    size_t frame_size();
    u8 channel_count();
    // Gets a data buffer that can be written to and then passed back to PulseAudio through
    // the write() function. This avoids a copy vs directly calling write().
    ErrorOr<Bytes> begin_write(size_t bytes_to_write = NumericLimits<size_t>::max());
    // Writes a data buffer to the playback stream.
    ErrorOr<void> write(ReadonlyBytes data);
    // Cancels the previous begin_write() call.
    ErrorOr<void> cancel_write();

    bool is_suspended() const;
    // Plays back all buffered data and corks the stream. Until resume() is called, no data
    // will be written to the stream.
    ErrorOr<void> drain_and_suspend();
    // Drops all buffered data and corks the stream. Until resume() is called, no data will
    // be written to the stream.
    ErrorOr<void> flush_and_suspend();
    // Uncorks the stream and forces data to be written to the buffers to force playback to
    // resume as soon as possible.
    ErrorOr<void> resume();
    AK::Duration total_time_played() const;

    ErrorOr<void> set_volume(double volume);

    void notify_data_available();

    PulseAudioContext& context() { return *m_context; }

private:
    friend class PulseAudioContext;

    explicit PulseAudioStream(PulseAudioContext&, pa_stream*);
    PulseAudioStream(PulseAudioStream const&) = delete;

    ErrorOr<void> wait_for_operation(pa_operation*, StringView error_message);

    void on_write_requested(size_t bytes_to_write);
    void queue_a_write_while_locked();

    NonnullRefPtr<PulseAudioContext> m_context;
    pa_stream* m_stream { nullptr };
    SampleSpecification m_sample_specification;
    bool m_started_playback { false };
    PulseAudioDataRequestCallback m_write_callback { nullptr };
    // Determines whether we will allow the write callback to run. This should only be true
    // if the stream is becoming or is already corked.
    bool m_suspended { false };

    enum CallbackState : u8 {
        Parked,
        Active,
        ActiveWithFutureData,
    };

    Atomic<CallbackState> m_callback_state { CallbackState::Parked };

    Function<void()> m_underrun_callback;
};

enum class PulseAudioErrorCode {
    OK = 0,
    AccessFailure,
    UnknownCommand,
    InvalidArgument,
    EntityExists,
    NoSuchEntity,
    ConnectionRefused,
    ProtocolError,
    Timeout,
    NoAuthenticationKey,
    InternalError,
    ConnectionTerminated,
    EntityKilled,
    InvalidServer,
    NoduleInitFailed,
    BadState,
    NoData,
    IncompatibleProtocolVersion,
    DataTooLarge,
    NotSupported,
    Unknown,
    NoExtension,
    Obsolete,
    NotImplemented,
    CalledFromFork,
    IOError,
    Busy,
    Sentinel
};

StringView pulse_audio_error_to_string(PulseAudioErrorCode code);

ErrorOr<Audio::ChannelMap> pulse_audio_channel_map_to_channel_map(pa_channel_map const&);
ErrorOr<pa_channel_map> channel_map_to_pulse_audio_channel_map(Audio::ChannelMap const&);

}
