/*
  Simple DirectMedia Layer
  Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
  Copyright (C) 2024 Wim Taymans <wtaymans@redhat.com>

  This software is provided 'as-is', without any express or implied
  warranty.  In no event will the authors be held liable for any damages
  arising from the use of this software.

  Permission is granted to anyone to use this software for any purpose,
  including commercial applications, and to alter it and redistribute it
  freely, subject to the following restrictions:

  1. The origin of this software must not be misrepresented; you must not
     claim that you wrote the original software. If you use this software
     in a product, an acknowledgment in the product documentation would be
     appreciated but is not required.
  2. Altered source versions must be plainly marked as such, and must not be
     misrepresented as being the original software.
  3. This notice may not be removed or altered from any source distribution.
*/
#include "SDL_internal.h"

#ifdef SDL_CAMERA_DRIVER_PIPEWIRE

#include "../SDL_syscamera.h"

#include <spa/utils/type.h>
#include <spa/pod/builder.h>
#include <spa/pod/iter.h>
#include <spa/param/video/raw.h>
#include <spa/param/video/format.h>
#include <spa/utils/result.h>
#include <spa/utils/json.h>

#include <pipewire/pipewire.h>
#include <pipewire/extensions/metadata.h>

#define PW_POD_BUFFER_LENGTH         1024
#define PW_THREAD_NAME_BUFFER_LENGTH 128
#define PW_MAX_IDENTIFIER_LENGTH     256

#define PW_REQUIRED_MAJOR       1
#define PW_REQUIRED_MINOR       0
#define PW_REQUIRED_PATCH       0

enum PW_READY_FLAGS
{
    PW_READY_FLAG_BUFFER_ADDED = 0x1,
    PW_READY_FLAG_STREAM_READY = 0x2,
    PW_READY_FLAG_ALL_BITS = 0x3
};

#define PW_ID_TO_HANDLE(x) (void *)((uintptr_t)x)
#define PW_HANDLE_TO_ID(x) (uint32_t)((uintptr_t)x)

static bool pipewire_initialized = false;

// Pipewire entry points
static const char *(*PIPEWIRE_pw_get_library_version)(void);
#if PW_CHECK_VERSION(0, 3, 75)
static bool (*PIPEWIRE_pw_check_library_version)(int major, int minor, int micro);
#endif
static void (*PIPEWIRE_pw_init)(int *, char ***);
static void (*PIPEWIRE_pw_deinit)(void);
static struct pw_main_loop *(*PIPEWIRE_pw_main_loop_new)(const struct spa_dict *loop);
static struct pw_loop *(*PIPEWIRE_pw_main_loop_get_loop)(struct pw_main_loop *loop);
static int (*PIPEWIRE_pw_main_loop_run)(struct pw_main_loop *loop);
static int (*PIPEWIRE_pw_main_loop_quit)(struct pw_main_loop *loop);
static void(*PIPEWIRE_pw_main_loop_destroy)(struct pw_main_loop *loop);
static struct pw_thread_loop *(*PIPEWIRE_pw_thread_loop_new)(const char *, const struct spa_dict *);
static void (*PIPEWIRE_pw_thread_loop_destroy)(struct pw_thread_loop *);
static void (*PIPEWIRE_pw_thread_loop_stop)(struct pw_thread_loop *);
static struct pw_loop *(*PIPEWIRE_pw_thread_loop_get_loop)(struct pw_thread_loop *);
static void (*PIPEWIRE_pw_thread_loop_lock)(struct pw_thread_loop *);
static void (*PIPEWIRE_pw_thread_loop_unlock)(struct pw_thread_loop *);
static void (*PIPEWIRE_pw_thread_loop_signal)(struct pw_thread_loop *, bool);
static void (*PIPEWIRE_pw_thread_loop_wait)(struct pw_thread_loop *);
static int (*PIPEWIRE_pw_thread_loop_start)(struct pw_thread_loop *);
static struct pw_context *(*PIPEWIRE_pw_context_new)(struct pw_loop *, struct pw_properties *, size_t);
static void (*PIPEWIRE_pw_context_destroy)(struct pw_context *);
static struct pw_core *(*PIPEWIRE_pw_context_connect)(struct pw_context *, struct pw_properties *, size_t);
static void (*PIPEWIRE_pw_proxy_add_object_listener)(struct pw_proxy *, struct spa_hook *, const void *, void *);
static void (*PIPEWIRE_pw_proxy_add_listener)(struct pw_proxy *, struct spa_hook *, const struct pw_proxy_events *, void *);
static void *(*PIPEWIRE_pw_proxy_get_user_data)(struct pw_proxy *);
static void (*PIPEWIRE_pw_proxy_destroy)(struct pw_proxy *);
static int (*PIPEWIRE_pw_core_disconnect)(struct pw_core *);
static struct pw_node_info * (*PIPEWIRE_pw_node_info_merge)(struct pw_node_info *info, const struct pw_node_info *update, bool reset);
static void (*PIPEWIRE_pw_node_info_free)(struct pw_node_info *info);
static struct pw_stream *(*PIPEWIRE_pw_stream_new)(struct pw_core *, const char *, struct pw_properties *);
static void (*PIPEWIRE_pw_stream_add_listener)(struct pw_stream *stream, struct spa_hook *listener, const struct pw_stream_events *events, void *data);
static void (*PIPEWIRE_pw_stream_destroy)(struct pw_stream *);
static int (*PIPEWIRE_pw_stream_connect)(struct pw_stream *, enum pw_direction, uint32_t, enum pw_stream_flags,
                                         const struct spa_pod **, uint32_t);
static enum pw_stream_state (*PIPEWIRE_pw_stream_get_state)(struct pw_stream *stream, const char **error);
static struct pw_buffer *(*PIPEWIRE_pw_stream_dequeue_buffer)(struct pw_stream *);
static int (*PIPEWIRE_pw_stream_queue_buffer)(struct pw_stream *, struct pw_buffer *);
static struct pw_properties *(*PIPEWIRE_pw_properties_new)(const char *, ...)SPA_SENTINEL;
static struct pw_properties *(*PIPEWIRE_pw_properties_new_dict)(const struct spa_dict *dict);
static int (*PIPEWIRE_pw_properties_set)(struct pw_properties *, const char *, const char *);
static int (*PIPEWIRE_pw_properties_setf)(struct pw_properties *, const char *, const char *, ...) SPA_PRINTF_FUNC(3, 4);

#ifdef SDL_CAMERA_DRIVER_PIPEWIRE_DYNAMIC

static const char *pipewire_library = SDL_CAMERA_DRIVER_PIPEWIRE_DYNAMIC;
static SDL_SharedObject *pipewire_handle = NULL;

static bool pipewire_dlsym(const char *fn, void **addr)
{
    *addr = SDL_LoadFunction(pipewire_handle, fn);
    if (!*addr) {
        // Don't call SDL_SetError(): SDL_LoadFunction already did.
        return false;
    }

    return true;
}

#define SDL_PIPEWIRE_SYM(x)                                     \
    if (!pipewire_dlsym(#x, (void **)(char *)&PIPEWIRE_##x))    \
        return false

static bool load_pipewire_library(void)
{
    pipewire_handle = SDL_LoadObject(pipewire_library);
    return pipewire_handle ? true : false;
}

static void unload_pipewire_library(void)
{
    if (pipewire_handle) {
        SDL_UnloadObject(pipewire_handle);
        pipewire_handle = NULL;
    }
}

#else

#define SDL_PIPEWIRE_SYM(x) PIPEWIRE_##x = x

static bool load_pipewire_library(void)
{
    return true;
}

static void unload_pipewire_library(void)
{
    // Nothing to do
}

#endif // SDL_CAMERA_DRIVER_PIPEWIRE_DYNAMIC

static bool load_pipewire_syms(void)
{
    SDL_PIPEWIRE_SYM(pw_get_library_version);
#if PW_CHECK_VERSION(0, 3, 75)
    SDL_PIPEWIRE_SYM(pw_check_library_version);
#endif
    SDL_PIPEWIRE_SYM(pw_init);
    SDL_PIPEWIRE_SYM(pw_deinit);
    SDL_PIPEWIRE_SYM(pw_main_loop_new);
    SDL_PIPEWIRE_SYM(pw_main_loop_get_loop);
    SDL_PIPEWIRE_SYM(pw_main_loop_run);
    SDL_PIPEWIRE_SYM(pw_main_loop_quit);
    SDL_PIPEWIRE_SYM(pw_main_loop_destroy);
    SDL_PIPEWIRE_SYM(pw_thread_loop_new);
    SDL_PIPEWIRE_SYM(pw_thread_loop_destroy);
    SDL_PIPEWIRE_SYM(pw_thread_loop_stop);
    SDL_PIPEWIRE_SYM(pw_thread_loop_get_loop);
    SDL_PIPEWIRE_SYM(pw_thread_loop_lock);
    SDL_PIPEWIRE_SYM(pw_thread_loop_unlock);
    SDL_PIPEWIRE_SYM(pw_thread_loop_signal);
    SDL_PIPEWIRE_SYM(pw_thread_loop_wait);
    SDL_PIPEWIRE_SYM(pw_thread_loop_start);
    SDL_PIPEWIRE_SYM(pw_context_new);
    SDL_PIPEWIRE_SYM(pw_context_destroy);
    SDL_PIPEWIRE_SYM(pw_context_connect);
    SDL_PIPEWIRE_SYM(pw_proxy_add_listener);
    SDL_PIPEWIRE_SYM(pw_proxy_add_object_listener);
    SDL_PIPEWIRE_SYM(pw_proxy_get_user_data);
    SDL_PIPEWIRE_SYM(pw_proxy_destroy);
    SDL_PIPEWIRE_SYM(pw_core_disconnect);
    SDL_PIPEWIRE_SYM(pw_node_info_merge);
    SDL_PIPEWIRE_SYM(pw_node_info_free);
    SDL_PIPEWIRE_SYM(pw_stream_new);
    SDL_PIPEWIRE_SYM(pw_stream_add_listener);
    SDL_PIPEWIRE_SYM(pw_stream_destroy);
    SDL_PIPEWIRE_SYM(pw_stream_connect);
    SDL_PIPEWIRE_SYM(pw_stream_get_state);
    SDL_PIPEWIRE_SYM(pw_stream_dequeue_buffer);
    SDL_PIPEWIRE_SYM(pw_stream_queue_buffer);
    SDL_PIPEWIRE_SYM(pw_properties_new);
    SDL_PIPEWIRE_SYM(pw_properties_new_dict);
    SDL_PIPEWIRE_SYM(pw_properties_set);
    SDL_PIPEWIRE_SYM(pw_properties_setf);

    return true;
}

static bool init_pipewire_library(void)
{
    if (load_pipewire_library()) {
        if (load_pipewire_syms()) {
            PIPEWIRE_pw_init(NULL, NULL);
            return true;
        }
    }
    return false;
}

static void deinit_pipewire_library(void)
{
    PIPEWIRE_pw_deinit();
    unload_pipewire_library();
}

// The global hotplug thread and associated objects.
static struct
{
    struct pw_thread_loop *loop;

    struct pw_context *context;

    struct pw_core *core;
    struct spa_hook core_listener;
    int server_major;
    int server_minor;
    int server_patch;
    int last_seq;
    int pending_seq;

    struct pw_registry *registry;
    struct spa_hook registry_listener;

    struct spa_list global_list;

    bool have_1_0_5;
    bool init_complete;
    bool events_enabled;
} hotplug;

struct global
{
    struct spa_list link;

    const struct global_class *class;

    uint32_t id;
    uint32_t permissions;
    struct pw_properties *props;

    char *name;

    struct pw_proxy *proxy;
    struct spa_hook proxy_listener;
    struct spa_hook object_listener;

    int changed;
    void *info;
    struct spa_list pending_list;
    struct spa_list param_list;

    bool added;
};

struct global_class
{
    const char *type;
    uint32_t version;
    const void *events;
    int (*init) (struct global *g);
    void (*destroy) (struct global *g);
};

struct param {
    uint32_t id;
    int32_t seq;
    struct spa_list link;
    struct spa_pod *param;
};

static uint32_t param_clear(struct spa_list *param_list, uint32_t id)
{
    struct param *p, *t;
    uint32_t count = 0;

    spa_list_for_each_safe(p, t, param_list, link) {
        if (id == SPA_ID_INVALID || p->id == id) {
            spa_list_remove(&p->link);
            free(p); // This should NOT be SDL_free()
            count++;
        }
    }
    return count;
}

#if PW_CHECK_VERSION(0,3,60)
#define SPA_PARAMS_INFO_SEQ(p)  ((p).seq)
#else
#define SPA_PARAMS_INFO_SEQ(p)  ((p).padding[0])
#endif

static struct param *param_add(struct spa_list *params,
                int seq, uint32_t id, const struct spa_pod *param)
{
    struct param *p;

    if (id == SPA_ID_INVALID) {
        if (param == NULL || !spa_pod_is_object(param)) {
            errno = EINVAL;
            return NULL;
        }
        id = SPA_POD_OBJECT_ID(param);
    }

    p = malloc(sizeof(*p) + (param != NULL ? SPA_POD_SIZE(param) : 0));
    if (p == NULL)
        return NULL;

    p->id = id;
    p->seq = seq;
    if (param != NULL) {
        p->param = SPA_PTROFF(p, sizeof(*p), struct spa_pod);
        SDL_memcpy(p->param, param, SPA_POD_SIZE(param));
    } else {
        param_clear(params, id);
        p->param = NULL;
    }
    spa_list_append(params, &p->link);

    return p;
}

static void param_update(struct spa_list *param_list, struct spa_list *pending_list,
                        uint32_t n_params, struct spa_param_info *params)
{
    struct param *p, *t;
    uint32_t i;

    for (i = 0; i < n_params; i++) {
        spa_list_for_each_safe(p, t, pending_list, link) {
            if (p->id == params[i].id &&
                p->seq != SPA_PARAMS_INFO_SEQ(params[i]) &&
                p->param != NULL) {
                    spa_list_remove(&p->link);
                    free(p); // This should NOT be SDL_free()
            }
        }
    }
    spa_list_consume(p, pending_list, link) {
        spa_list_remove(&p->link);
        if (p->param == NULL) {
            param_clear(param_list, p->id);
            free(p); // This should NOT be SDL_free()
        } else {
            spa_list_append(param_list, &p->link);
        }
    }
}

static struct sdl_video_format {
    SDL_PixelFormat format;
    SDL_Colorspace colorspace;
    uint32_t id;
} sdl_video_formats[] = {
    { SDL_PIXELFORMAT_RGBX32, SDL_COLORSPACE_SRGB, SPA_VIDEO_FORMAT_RGBx },
    { SDL_PIXELFORMAT_XRGB32, SDL_COLORSPACE_SRGB, SPA_VIDEO_FORMAT_xRGB },
    { SDL_PIXELFORMAT_BGRX32, SDL_COLORSPACE_SRGB, SPA_VIDEO_FORMAT_BGRx },
    { SDL_PIXELFORMAT_XBGR32, SDL_COLORSPACE_SRGB, SPA_VIDEO_FORMAT_xBGR },
    { SDL_PIXELFORMAT_RGBA32, SDL_COLORSPACE_SRGB, SPA_VIDEO_FORMAT_RGBA },
    { SDL_PIXELFORMAT_ARGB32, SDL_COLORSPACE_SRGB, SPA_VIDEO_FORMAT_ARGB },
    { SDL_PIXELFORMAT_BGRA32, SDL_COLORSPACE_SRGB, SPA_VIDEO_FORMAT_BGRA },
    { SDL_PIXELFORMAT_ABGR32, SDL_COLORSPACE_SRGB, SPA_VIDEO_FORMAT_ABGR },
    { SDL_PIXELFORMAT_RGB24, SDL_COLORSPACE_SRGB, SPA_VIDEO_FORMAT_RGB },
    { SDL_PIXELFORMAT_BGR24, SDL_COLORSPACE_SRGB, SPA_VIDEO_FORMAT_BGR },
    { SDL_PIXELFORMAT_YV12, SDL_COLORSPACE_BT709_LIMITED, SPA_VIDEO_FORMAT_YV12 },
    { SDL_PIXELFORMAT_IYUV, SDL_COLORSPACE_BT709_LIMITED, SPA_VIDEO_FORMAT_I420 },
    { SDL_PIXELFORMAT_YUY2, SDL_COLORSPACE_BT709_LIMITED, SPA_VIDEO_FORMAT_YUY2 },
    { SDL_PIXELFORMAT_UYVY, SDL_COLORSPACE_BT709_LIMITED, SPA_VIDEO_FORMAT_UYVY },
    { SDL_PIXELFORMAT_YVYU, SDL_COLORSPACE_BT709_LIMITED, SPA_VIDEO_FORMAT_YVYU },
    { SDL_PIXELFORMAT_NV12, SDL_COLORSPACE_BT709_LIMITED, SPA_VIDEO_FORMAT_NV12 },
    { SDL_PIXELFORMAT_NV21, SDL_COLORSPACE_BT709_LIMITED, SPA_VIDEO_FORMAT_NV21 }
};

static uint32_t sdl_format_to_id(SDL_PixelFormat format)
{
    struct sdl_video_format *f;
    SPA_FOR_EACH_ELEMENT(sdl_video_formats, f) {
        if (f->format == format)
            return f->id;
    }
    return SPA_VIDEO_FORMAT_UNKNOWN;
}

static void id_to_sdl_format(uint32_t id, SDL_PixelFormat *format, SDL_Colorspace *colorspace)
{
    struct sdl_video_format *f;
    SPA_FOR_EACH_ELEMENT(sdl_video_formats, f) {
        if (f->id == id) {
            *format = f->format;
            *colorspace = f->colorspace;
            return;
        }
    }
    *format = SDL_PIXELFORMAT_UNKNOWN;
    *colorspace = SDL_COLORSPACE_UNKNOWN;
}

struct SDL_PrivateCameraData
{
    struct pw_stream *stream;
    struct spa_hook stream_listener;

    struct pw_array buffers;
};

static void on_process(void *data)
{
    PIPEWIRE_pw_thread_loop_signal(hotplug.loop, false);
}

static void on_stream_state_changed(void *data, enum pw_stream_state old,
                enum pw_stream_state state, const char *error)
{
    SDL_Camera *device = data;
    switch (state) {
    case PW_STREAM_STATE_UNCONNECTED:
        break;
    case PW_STREAM_STATE_STREAMING:
        SDL_CameraPermissionOutcome(device, true);
        break;
    default:
        break;
    }
}

static void on_stream_param_changed(void *data, uint32_t id, const struct spa_pod *param)
{
}

static void on_add_buffer(void *data, struct pw_buffer *buffer)
{
    SDL_Camera *device = data;
    pw_array_add_ptr(&device->hidden->buffers, buffer);
}

static void on_remove_buffer(void *data, struct pw_buffer *buffer)
{
    SDL_Camera *device = data;
    struct pw_buffer **p;
    pw_array_for_each(p, &device->hidden->buffers) {
        if (*p == buffer) {
            pw_array_remove(&device->hidden->buffers, p);
            return;
        }
    }
}

static const struct pw_stream_events stream_events = {
    .version = PW_VERSION_STREAM_EVENTS,
    .add_buffer = on_add_buffer,
    .remove_buffer = on_remove_buffer,
    .state_changed = on_stream_state_changed,
    .param_changed = on_stream_param_changed,
    .process = on_process,
};

static bool PIPEWIRECAMERA_OpenDevice(SDL_Camera *device, const SDL_CameraSpec *spec)
{
    struct pw_properties *props;
    const struct spa_pod *params[3];
    int res, n_params = 0;
    uint8_t buffer[1024];
    struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));

    if (!device) {
        return false;
    }
    device->hidden = (struct SDL_PrivateCameraData *) SDL_calloc(1, sizeof (struct SDL_PrivateCameraData));
    if (device->hidden == NULL) {
        return false;
    }
    pw_array_init(&device->hidden->buffers, 64);

    PIPEWIRE_pw_thread_loop_lock(hotplug.loop);

    props = PIPEWIRE_pw_properties_new(PW_KEY_MEDIA_TYPE, "Video",
                    PW_KEY_MEDIA_CATEGORY, "Capture",
                    PW_KEY_MEDIA_ROLE, "Camera",
                    PW_KEY_TARGET_OBJECT, device->name,
                    NULL);
    if (props == NULL) {
        return false;
    }

    device->hidden->stream = PIPEWIRE_pw_stream_new(hotplug.core, "SDL PipeWire Camera", props);
    if (device->hidden->stream == NULL) {
        return false;
    }

    PIPEWIRE_pw_stream_add_listener(device->hidden->stream,
                    &device->hidden->stream_listener,
                    &stream_events, device);

    if (spec->format == SDL_PIXELFORMAT_MJPG) {
        params[n_params++] = spa_pod_builder_add_object(&b,
                SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat,
                SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_video),
                        SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_mjpg),
                        SPA_FORMAT_VIDEO_size, SPA_POD_Rectangle(&SPA_RECTANGLE(spec->width, spec->height)),
                        SPA_FORMAT_VIDEO_framerate,
                    SPA_POD_Fraction(&SPA_FRACTION(spec->framerate_numerator, spec->framerate_denominator)));
    } else {
        params[n_params++] = spa_pod_builder_add_object(&b,
                SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat,
                SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_video),
                        SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw),
                        SPA_FORMAT_VIDEO_format, SPA_POD_Id(sdl_format_to_id(spec->format)),
                        SPA_FORMAT_VIDEO_size, SPA_POD_Rectangle(&SPA_RECTANGLE(spec->width, spec->height)),
                        SPA_FORMAT_VIDEO_framerate,
                    SPA_POD_Fraction(&SPA_FRACTION(spec->framerate_numerator, spec->framerate_denominator)));
    }

    if ((res = PIPEWIRE_pw_stream_connect(device->hidden->stream,
                                    PW_DIRECTION_INPUT,
                                    PW_ID_ANY,
                                    PW_STREAM_FLAG_AUTOCONNECT |
                                    PW_STREAM_FLAG_MAP_BUFFERS,
                                    params, n_params)) < 0) {
        return false;
    }

    PIPEWIRE_pw_thread_loop_unlock(hotplug.loop);

    return true;
}

static void PIPEWIRECAMERA_CloseDevice(SDL_Camera *device)
{
    if (!device) {
        return;
    }

    PIPEWIRE_pw_thread_loop_lock(hotplug.loop);
    if (device->hidden) {
        if (device->hidden->stream)
            PIPEWIRE_pw_stream_destroy(device->hidden->stream);
        pw_array_clear(&device->hidden->buffers);
        SDL_free(device->hidden);
        device->hidden = NULL;
    }
    PIPEWIRE_pw_thread_loop_unlock(hotplug.loop);
}

static bool PIPEWIRECAMERA_WaitDevice(SDL_Camera *device)
{
    PIPEWIRE_pw_thread_loop_lock(hotplug.loop);
    PIPEWIRE_pw_thread_loop_wait(hotplug.loop);
    PIPEWIRE_pw_thread_loop_unlock(hotplug.loop);
    return true;
}

static SDL_CameraFrameResult PIPEWIRECAMERA_AcquireFrame(SDL_Camera *device, SDL_Surface *frame, Uint64 *timestampNS)
{
    struct pw_buffer *b;

    PIPEWIRE_pw_thread_loop_lock(hotplug.loop);
    b = NULL;
    while (true) {
        struct pw_buffer *t;
        if ((t = PIPEWIRE_pw_stream_dequeue_buffer(device->hidden->stream)) == NULL)
            break;
        if (b)
            PIPEWIRE_pw_stream_queue_buffer(device->hidden->stream, b);
        b = t;
    }
    if (b == NULL) {
        PIPEWIRE_pw_thread_loop_unlock(hotplug.loop);
        return SDL_CAMERA_FRAME_SKIP;
    }

#if PW_CHECK_VERSION(1,0,5)
    *timestampNS = hotplug.have_1_0_5 ? b->time : SDL_GetTicksNS();
#else
    *timestampNS = SDL_GetTicksNS();
#endif
    frame->pixels = b->buffer->datas[0].data;
    if (frame->format == SDL_PIXELFORMAT_MJPG) {
        frame->pitch = b->buffer->datas[0].chunk->size;
    } else {
        frame->pitch = b->buffer->datas[0].chunk->stride;
    }

    PIPEWIRE_pw_thread_loop_unlock(hotplug.loop);

    return SDL_CAMERA_FRAME_READY;
}

static void PIPEWIRECAMERA_ReleaseFrame(SDL_Camera *device, SDL_Surface *frame)
{
    struct pw_buffer **p;
    PIPEWIRE_pw_thread_loop_lock(hotplug.loop);
    pw_array_for_each(p, &device->hidden->buffers) {
        if ((*p)->buffer->datas[0].data == frame->pixels) {
            PIPEWIRE_pw_stream_queue_buffer(device->hidden->stream, (*p));
            break;
        }
    }
    PIPEWIRE_pw_thread_loop_unlock(hotplug.loop);
}

static void collect_rates(CameraFormatAddData *data, struct param *p, SDL_PixelFormat sdlfmt, SDL_Colorspace colorspace, const struct spa_rectangle *size)
{
    const struct spa_pod_prop *prop;
    struct spa_pod * values;
    uint32_t i, n_vals, choice;
    struct spa_fraction *rates;

    prop = spa_pod_find_prop(p->param, NULL, SPA_FORMAT_VIDEO_framerate);
    if (prop == NULL)
        return;

    values = spa_pod_get_values(&prop->value, &n_vals, &choice);
    if (values->type != SPA_TYPE_Fraction || n_vals == 0)
        return;

    rates = SPA_POD_BODY(values);
    switch (choice) {
    case SPA_CHOICE_None:
        n_vals = 1;
    SDL_FALLTHROUGH;
    case SPA_CHOICE_Enum:
        for (i = 0; i < n_vals; i++) {
            if (!SDL_AddCameraFormat(data, sdlfmt, colorspace, size->width, size->height, rates[i].num, rates[i].denom)) {
                return;  // Probably out of memory; we'll go with what we have, if anything.
            }
        }
        break;
    default:
        SDL_Log("CAMERA: unimplemented choice:%d", choice);
        break;
    }
}

static void collect_size(CameraFormatAddData *data, struct param *p, SDL_PixelFormat sdlfmt, SDL_Colorspace colorspace)
{
    const struct spa_pod_prop *prop;
    struct spa_pod * values;
    uint32_t i, n_vals, choice;
    struct spa_rectangle *rectangles;

    prop = spa_pod_find_prop(p->param, NULL, SPA_FORMAT_VIDEO_size);
    if (prop == NULL)
        return;

    values = spa_pod_get_values(&prop->value, &n_vals, &choice);
    if (values->type != SPA_TYPE_Rectangle || n_vals == 0)
        return;

    rectangles = SPA_POD_BODY(values);
    switch (choice) {
    case SPA_CHOICE_None:
        n_vals = 1;
    SDL_FALLTHROUGH;
    case SPA_CHOICE_Enum:
        for (i = 0; i < n_vals; i++) {
            collect_rates(data, p, sdlfmt, colorspace, &rectangles[i]);
        }
        break;
    default:
        SDL_Log("CAMERA: unimplemented choice:%d", choice);
        break;
    }
}

static void collect_raw(CameraFormatAddData *data, struct param *p)
{
    const struct spa_pod_prop *prop;
    SDL_PixelFormat sdlfmt;
    SDL_Colorspace colorspace;
    struct spa_pod * values;
    uint32_t i, n_vals, choice, *ids;

    prop = spa_pod_find_prop(p->param, NULL, SPA_FORMAT_VIDEO_format);
    if (prop == NULL)
        return;

    values = spa_pod_get_values(&prop->value, &n_vals, &choice);
    if (values->type != SPA_TYPE_Id || n_vals == 0)
        return;

    ids = SPA_POD_BODY(values);
    switch (choice) {
    case SPA_CHOICE_None:
        n_vals = 1;
	SDL_FALLTHROUGH;
    case SPA_CHOICE_Enum:
        for (i = 0; i < n_vals; i++) {
            id_to_sdl_format(ids[i], &sdlfmt, &colorspace);
            if (sdlfmt == SDL_PIXELFORMAT_UNKNOWN) {
                continue;
            }
            collect_size(data, p, sdlfmt, colorspace);
        }
        break;
    default:
        SDL_Log("CAMERA: unimplemented choice: %d", choice);
        break;
    }
}

static void collect_format(CameraFormatAddData *data, struct param *p)
{
    const struct spa_pod_prop *prop;
    struct spa_pod * values;
    uint32_t i, n_vals, choice, *ids;

    prop = spa_pod_find_prop(p->param, NULL, SPA_FORMAT_mediaSubtype);
    if (prop == NULL)
        return;

    values = spa_pod_get_values(&prop->value, &n_vals, &choice);
    if (values->type != SPA_TYPE_Id || n_vals == 0)
        return;

    ids = SPA_POD_BODY(values);
    switch (choice) {
    case SPA_CHOICE_None:
        n_vals = 1;
    SDL_FALLTHROUGH;
    case SPA_CHOICE_Enum:
        for (i = 0; i < n_vals; i++) {
            switch (ids[i]) {
            case SPA_MEDIA_SUBTYPE_raw:
                collect_raw(data, p);
                break;
            case SPA_MEDIA_SUBTYPE_mjpg:
                collect_size(data, p, SDL_PIXELFORMAT_MJPG, SDL_COLORSPACE_JPEG);
                break;
            default:
                // Unsupported format
                break;
            }
        }
        break;
    default:
        SDL_Log("CAMERA: unimplemented choice: %d", choice);
        break;
    }
}

static void add_device(struct global *g)
{
    struct param *p;
    CameraFormatAddData data;

    SDL_zero(data);

    spa_list_for_each(p, &g->param_list, link) {
        if (p->id != SPA_PARAM_EnumFormat)
            continue;

        collect_format(&data, p);
    }
    if (data.num_specs > 0) {
        SDL_AddCamera(g->name, SDL_CAMERA_POSITION_UNKNOWN,
				    data.num_specs, data.specs, g);
    }
    SDL_free(data.specs);

    g->added = true;
}

static void PIPEWIRECAMERA_DetectDevices(void)
{
    struct global *g;

    PIPEWIRE_pw_thread_loop_lock(hotplug.loop);

    // Wait until the initial registry enumeration is complete
    while (!hotplug.init_complete) {
        PIPEWIRE_pw_thread_loop_wait(hotplug.loop);
    }

    spa_list_for_each (g, &hotplug.global_list, link) {
	    if (!g->added) {
                add_device(g);
	    }
    }

    hotplug.events_enabled = true;

    PIPEWIRE_pw_thread_loop_unlock(hotplug.loop);
}

static void PIPEWIRECAMERA_FreeDeviceHandle(SDL_Camera *device)
{
}

static void do_resync(void)
{
    hotplug.pending_seq = pw_core_sync(hotplug.core, PW_ID_CORE, 0);
}

/** node */
static void node_event_info(void *object, const struct pw_node_info *info)
{
    struct global *g = object;
    uint32_t i;

    info = g->info = PIPEWIRE_pw_node_info_merge(g->info, info, g->changed == 0);
    if (info == NULL)
        return;

    if (info->change_mask & PW_NODE_CHANGE_MASK_PARAMS) {
        for (i = 0; i < info->n_params; i++) {
            uint32_t id = info->params[i].id;
            int res;

            if (info->params[i].user == 0)
                continue;
            info->params[i].user = 0;

	    if (id != SPA_PARAM_EnumFormat)
		    continue;

            param_add(&g->pending_list, SPA_PARAMS_INFO_SEQ(info->params[i]), id, NULL);
            if (!(info->params[i].flags & SPA_PARAM_INFO_READ))
                continue;

            res = pw_node_enum_params((struct pw_node*)g->proxy,
                        ++SPA_PARAMS_INFO_SEQ(info->params[i]), id, 0, -1, NULL);
            if (SPA_RESULT_IS_ASYNC(res))
                SPA_PARAMS_INFO_SEQ(info->params[i]) = res;

	    g->changed++;
        }
    }
    do_resync();
}

static void node_event_param(void *object, int seq,
                uint32_t id, uint32_t index, uint32_t next,
                const struct spa_pod *param)
{
    struct global *g = object;
    param_add(&g->pending_list, seq, id, param);
}

static const struct pw_node_events node_events = {
    .version = PW_VERSION_NODE_EVENTS,
    .info = node_event_info,
    .param = node_event_param,
};

static void node_destroy(struct global *g)
{
    if (g->info) {
        PIPEWIRE_pw_node_info_free(g->info);
        g->info = NULL;
    }
}


static const struct global_class node_class = {
    .type = PW_TYPE_INTERFACE_Node,
    .version = PW_VERSION_NODE,
    .events = &node_events,
    .destroy = node_destroy,
};

/** proxy */
static void proxy_removed(void *data)
{
    struct global *g = data;
    PIPEWIRE_pw_proxy_destroy(g->proxy);
}

static void proxy_destroy(void *data)
{
    struct global *g = data;
    spa_list_remove(&g->link);
    g->proxy = NULL;
    if (g->class) {
        if (g->class->events)
            spa_hook_remove(&g->object_listener);
        if (g->class->destroy)
            g->class->destroy(g);
    }
    param_clear(&g->param_list, SPA_ID_INVALID);
    param_clear(&g->pending_list, SPA_ID_INVALID);
    free(g->name); // This should NOT be SDL_free()
}

static const struct pw_proxy_events proxy_events = {
    .version = PW_VERSION_PROXY_EVENTS,
    .removed = proxy_removed,
    .destroy = proxy_destroy
};

// called with thread_loop lock
static void hotplug_registry_global_callback(void *object, uint32_t id,
		uint32_t permissions, const char *type, uint32_t version,
		const struct spa_dict *props)
{
    const struct global_class *class = NULL;
    struct pw_proxy *proxy;
    const char *str, *name = NULL;

    if (spa_streq(type, PW_TYPE_INTERFACE_Node)) {
        if (props == NULL)
            return;
        if (((str = spa_dict_lookup(props, PW_KEY_MEDIA_CLASS)) == NULL) ||
            (!spa_streq(str, "Video/Source")))
            return;

        if ((name = spa_dict_lookup(props, PW_KEY_NODE_DESCRIPTION)) == NULL &&
            (name = spa_dict_lookup(props, PW_KEY_NODE_NAME)) == NULL)
		name = "unnamed camera";

        class = &node_class;
    }
    if (class) {
        struct global *g;

        proxy = pw_registry_bind(hotplug.registry,
                            id, class->type, class->version,
                            sizeof(struct global));

        g = PIPEWIRE_pw_proxy_get_user_data(proxy);
        g->class = class;
        g->id = id;
        g->permissions = permissions;
        g->props = props ? PIPEWIRE_pw_properties_new_dict(props) : NULL;
        g->proxy = proxy;
        g->name = strdup(name);
        spa_list_init(&g->pending_list);
        spa_list_init(&g->param_list);
        spa_list_append(&hotplug.global_list, &g->link);

        PIPEWIRE_pw_proxy_add_listener(proxy,
                            &g->proxy_listener,
                            &proxy_events, g);

        if (class->events) {
            PIPEWIRE_pw_proxy_add_object_listener(proxy,
                                &g->object_listener,
                                class->events, g);
        }
        if (class->init)
            class->init(g);

        do_resync();
    }
}

// called with thread_loop lock
static void hotplug_registry_global_remove_callback(void *object, uint32_t id)
{
}

static const struct pw_registry_events hotplug_registry_events =
{
    .version = PW_VERSION_REGISTRY_EVENTS,
    .global = hotplug_registry_global_callback,
    .global_remove = hotplug_registry_global_remove_callback
};

static void parse_version(const char *str, int *major, int *minor, int *patch)
{
    if (SDL_sscanf(str, "%d.%d.%d", major, minor, patch) < 3) {
        *major = 0;
        *minor = 0;
        *patch = 0;
    }
}

// Core info, called with thread_loop lock
static void hotplug_core_info_callback(void *data, const struct pw_core_info *info)
{
    parse_version(info->version, &hotplug.server_major, &hotplug.server_minor, &hotplug.server_patch);
}

// Core sync points, called with thread_loop lock
static void hotplug_core_done_callback(void *object, uint32_t id, int seq)
{
    hotplug.last_seq = seq;
    if (id == PW_ID_CORE && seq == hotplug.pending_seq) {
        struct global *g;
        struct pw_node_info *info;

        spa_list_for_each(g, &hotplug.global_list, link) {
             if (!g->changed)
		     continue;

	     info = g->info;
             param_update(&g->param_list, &g->pending_list, info->n_params, info->params);

	     if (!g->added && hotplug.events_enabled) {
                 add_device(g);
	     }
        }
	hotplug.init_complete = true;
        PIPEWIRE_pw_thread_loop_signal(hotplug.loop, false);
    }
}
static const struct pw_core_events hotplug_core_events =
{
    .version = PW_VERSION_CORE_EVENTS,
    .info = hotplug_core_info_callback,
    .done = hotplug_core_done_callback
};

/* When in a container, the library version can differ from the underlying core version,
 * so make sure the underlying Pipewire implementation meets the version requirement.
 */
static bool pipewire_server_version_at_least(int major, int minor, int patch)
{
    return (hotplug.server_major >= major) &&
           (hotplug.server_major > major || hotplug.server_minor >= minor) &&
           (hotplug.server_major > major || hotplug.server_minor > minor || hotplug.server_patch >= patch);
}

// The hotplug thread
static bool hotplug_loop_init(void)
{
    int res;

    spa_list_init(&hotplug.global_list);

#if PW_CHECK_VERSION(0, 3, 75)
    hotplug.have_1_0_5 = PIPEWIRE_pw_check_library_version(1,0,5);
#else
    hotplug.have_1_0_5 = false;
#endif

    hotplug.loop = PIPEWIRE_pw_thread_loop_new("SDLPwCameraPlug", NULL);
    if (!hotplug.loop) {
        return SDL_SetError("Pipewire: Failed to create hotplug detection loop (%i)", errno);
    }

    hotplug.context = PIPEWIRE_pw_context_new(PIPEWIRE_pw_thread_loop_get_loop(hotplug.loop), NULL, 0);
    if (!hotplug.context) {
        return SDL_SetError("Pipewire: Failed to create hotplug detection context (%i)", errno);
    }

    hotplug.core = PIPEWIRE_pw_context_connect(hotplug.context, NULL, 0);
    if (!hotplug.core) {
        return SDL_SetError("Pipewire: Failed to connect hotplug detection context (%i)", errno);
    }
    spa_zero(hotplug.core_listener);
    pw_core_add_listener(hotplug.core, &hotplug.core_listener, &hotplug_core_events, NULL);

    hotplug.registry = pw_core_get_registry(hotplug.core, PW_VERSION_REGISTRY, 0);
    if (!hotplug.registry) {
        return SDL_SetError("Pipewire: Failed to acquire hotplug detection registry (%i)", errno);
    }

    spa_zero(hotplug.registry_listener);
    pw_registry_add_listener(hotplug.registry, &hotplug.registry_listener, &hotplug_registry_events, NULL);

    do_resync();

    res = PIPEWIRE_pw_thread_loop_start(hotplug.loop);
    if (res != 0) {
        return SDL_SetError("Pipewire: Failed to start hotplug detection loop");
    }

    PIPEWIRE_pw_thread_loop_lock(hotplug.loop);
    while (!hotplug.init_complete) {
        PIPEWIRE_pw_thread_loop_wait(hotplug.loop);
    }
    PIPEWIRE_pw_thread_loop_unlock(hotplug.loop);

    if (!pipewire_server_version_at_least(PW_REQUIRED_MAJOR, PW_REQUIRED_MINOR, PW_REQUIRED_PATCH)) {
        return SDL_SetError("Pipewire: server version is too old %d.%d.%d < %d.%d.%d",
			hotplug.server_major, hotplug.server_minor, hotplug.server_patch,
                    PW_REQUIRED_MAJOR, PW_REQUIRED_MINOR, PW_REQUIRED_PATCH);
    }

    return true;
}


static void PIPEWIRECAMERA_Deinitialize(void)
{
    if (pipewire_initialized) {
        if (hotplug.loop) {
            PIPEWIRE_pw_thread_loop_lock(hotplug.loop);
        }
        if (hotplug.registry) {
            spa_hook_remove(&hotplug.registry_listener);
            PIPEWIRE_pw_proxy_destroy((struct pw_proxy *)hotplug.registry);
        }
        if (hotplug.core) {
            spa_hook_remove(&hotplug.core_listener);
            PIPEWIRE_pw_core_disconnect(hotplug.core);
        }
        if (hotplug.context) {
            PIPEWIRE_pw_context_destroy(hotplug.context);
        }
        if (hotplug.loop) {
            PIPEWIRE_pw_thread_loop_unlock(hotplug.loop);
            PIPEWIRE_pw_thread_loop_destroy(hotplug.loop);
        }
        deinit_pipewire_library();
        spa_zero(hotplug);
        pipewire_initialized = false;
    }
}

static bool PIPEWIRECAMERA_Init(SDL_CameraDriverImpl *impl)
{
    if (!pipewire_initialized) {

        if (!init_pipewire_library()) {
            return false;
        }

        pipewire_initialized = true;

        if (!hotplug_loop_init()) {
            PIPEWIRECAMERA_Deinitialize();
            return false;
        }
    }

    impl->DetectDevices = PIPEWIRECAMERA_DetectDevices;
    impl->OpenDevice = PIPEWIRECAMERA_OpenDevice;
    impl->CloseDevice = PIPEWIRECAMERA_CloseDevice;
    impl->WaitDevice = PIPEWIRECAMERA_WaitDevice;
    impl->AcquireFrame = PIPEWIRECAMERA_AcquireFrame;
    impl->ReleaseFrame = PIPEWIRECAMERA_ReleaseFrame;
    impl->FreeDeviceHandle = PIPEWIRECAMERA_FreeDeviceHandle;
    impl->Deinitialize = PIPEWIRECAMERA_Deinitialize;

    return true;
}

CameraBootStrap PIPEWIRECAMERA_bootstrap = {
    "pipewire", "SDL PipeWire camera driver", PIPEWIRECAMERA_Init, false
};

#endif  // SDL_CAMERA_DRIVER_PIPEWIRE
