/* SPDX-License-Identifier: LGPL-2.1-or-later */
/*
 * Copyright (C) 2020, Collabora Ltd.
 *     Author: Nicolas Dufresne <nicolas.dufresne@collabora.com>
 *
 * gstlibcameraallocator.cpp - GStreamer Custom Allocator
 */

#include "gstlibcameraallocator.h"

#include <libcamera/camera.h>
#include <libcamera/framebuffer_allocator.h>
#include <libcamera/stream.h>

#include "gstlibcamera-utils.h"

using namespace libcamera;

static gboolean gst_libcamera_allocator_release(GstMiniObject *mini_object);

/**
 * \struct FrameWrap
 * \brief An internal wrapper to track the relation between FrameBuffer and
 * GstMemory(s)
 *
 * This wrapper maintains a count of the outstanding GstMemory (there may be
 * multiple GstMemory per FrameBuffer), and give back the FrameBuffer to the
 * allocator pool when all memory objects have returned.
 */

struct FrameWrap {
	FrameWrap(GstAllocator *allocator, FrameBuffer *buffer,
		  gpointer stream);
	~FrameWrap();

	void acquirePlane() { ++outstandingPlanes_; }
	bool releasePlane() { return --outstandingPlanes_ == 0; }

	static GQuark getQuark();

	gpointer stream_;
	FrameBuffer *buffer_;
	std::vector<GstMemory *> planes_;
	gint outstandingPlanes_;
};

FrameWrap::FrameWrap(GstAllocator *allocator, FrameBuffer *buffer,
		     gpointer stream)

	: stream_(stream),
	  buffer_(buffer),
	  outstandingPlanes_(0)
{
	for (const FrameBuffer::Plane &plane : buffer->planes()) {
		GstMemory *mem = gst_fd_allocator_alloc(allocator, plane.fd.fd(), plane.length,
							GST_FD_MEMORY_FLAG_DONT_CLOSE);
		gst_mini_object_set_qdata(GST_MINI_OBJECT(mem), getQuark(), this, nullptr);
		GST_MINI_OBJECT(mem)->dispose = gst_libcamera_allocator_release;
		g_object_unref(mem->allocator);
		planes_.push_back(mem);
	}
}

FrameWrap::~FrameWrap()
{
	for (GstMemory *mem : planes_) {
		GST_MINI_OBJECT(mem)->dispose = nullptr;
		g_object_ref(mem->allocator);
		gst_memory_unref(mem);
	}
}

GQuark FrameWrap::getQuark()
{
	static gsize frame_quark = 0;

	if (g_once_init_enter(&frame_quark)) {
		GQuark quark = g_quark_from_string("GstLibcameraFrameWrap");
		g_once_init_leave(&frame_quark, quark);
	}

	return frame_quark;
}

/**
 * \struct _GstLibcameraAllocator
 * \brief A pooling GstDmaBufAllocator for libcamera
 *
 * This is a pooling GstDmaBufAllocator implementation. This implementation override
 * the dispose function of memory object in order to keep them alive when they
 * are disposed by downstream elements.
 */
struct _GstLibcameraAllocator {
	GstDmaBufAllocator parent;
	FrameBufferAllocator *fb_allocator;
	/*
	 * A hash table using Stream pointer as key and returning a GQueue of
	 * FrameWrap.
	 */
	GHashTable *pools;
};

G_DEFINE_TYPE(GstLibcameraAllocator, gst_libcamera_allocator,
	      GST_TYPE_DMABUF_ALLOCATOR)

static gboolean
gst_libcamera_allocator_release(GstMiniObject *mini_object)
{
	GstMemory *mem = GST_MEMORY_CAST(mini_object);
	GstLibcameraAllocator *self = GST_LIBCAMERA_ALLOCATOR(mem->allocator);
	GLibLocker lock(GST_OBJECT(self));
	auto *frame = reinterpret_cast<FrameWrap *>(gst_mini_object_get_qdata(mini_object, FrameWrap::getQuark()));

	gst_memory_ref(mem);

	if (frame->releasePlane()) {
		auto *pool = reinterpret_cast<GQueue *>(g_hash_table_lookup(self->pools, frame->stream_));
		g_return_val_if_fail(pool, TRUE);
		g_queue_push_tail(pool, frame);
	}

	/* Keep last in case we are holding on the last allocator ref. */
	g_object_unref(mem->allocator);

	/* Return FALSE so that our mini object isn't freed. */
	return FALSE;
}

static void
gst_libcamera_allocator_free_pool(gpointer data)
{
	GQueue *queue = reinterpret_cast<GQueue *>(data);
	FrameWrap *frame;

	while ((frame = reinterpret_cast<FrameWrap *>(g_queue_pop_head(queue)))) {
		g_warn_if_fail(frame->outstandingPlanes_ == 0);
		delete frame;
	}

	g_queue_free(queue);
}

static void
gst_libcamera_allocator_init(GstLibcameraAllocator *self)
{
	self->pools = g_hash_table_new_full(nullptr, nullptr, nullptr,
					    gst_libcamera_allocator_free_pool);
	GST_OBJECT_FLAG_SET(self, GST_ALLOCATOR_FLAG_CUSTOM_ALLOC);
}

static void
gst_libcamera_allocator_dispose(GObject *object)
{
	GstLibcameraAllocator *self = GST_LIBCAMERA_ALLOCATOR(object);

	if (self->pools) {
		g_hash_table_unref(self->pools);
		self->pools = nullptr;
	}

	G_OBJECT_CLASS(gst_libcamera_allocator_parent_class)->dispose(object);
}

static void
gst_libcamera_allocator_finalize(GObject *object)
{
	GstLibcameraAllocator *self = GST_LIBCAMERA_ALLOCATOR(object);

	delete self->fb_allocator;

	G_OBJECT_CLASS(gst_libcamera_allocator_parent_class)->finalize(object);
}

static void
gst_libcamera_allocator_class_init(GstLibcameraAllocatorClass *klass)
{
	auto *allocator_class = GST_ALLOCATOR_CLASS(klass);
	auto *object_class = G_OBJECT_CLASS(klass);

	object_class->dispose = gst_libcamera_allocator_dispose;
	object_class->finalize = gst_libcamera_allocator_finalize;
	allocator_class->alloc = nullptr;
}

GstLibcameraAllocator *
gst_libcamera_allocator_new(std::shared_ptr<Camera> camera,
			    CameraConfiguration *config_)
{
	auto *self = GST_LIBCAMERA_ALLOCATOR(g_object_new(GST_TYPE_LIBCAMERA_ALLOCATOR,
							  nullptr));

	self->fb_allocator = new FrameBufferAllocator(camera);
	for (StreamConfiguration &streamCfg : *config_) {
		Stream *stream = streamCfg.stream();
		gint ret;

		ret = self->fb_allocator->allocate(stream);
		if (ret == 0)
			return nullptr;

		GQueue *pool = g_queue_new();
		for (const std::unique_ptr<FrameBuffer> &buffer :
		     self->fb_allocator->buffers(stream)) {
			auto *fb = new FrameWrap(GST_ALLOCATOR(self),
						 buffer.get(), stream);
			g_queue_push_tail(pool, fb);
		}

		g_hash_table_insert(self->pools, stream, pool);
	}

	return self;
}

bool
gst_libcamera_allocator_prepare_buffer(GstLibcameraAllocator *self,
				       Stream *stream, GstBuffer *buffer)
{
	GLibLocker lock(GST_OBJECT(self));

	auto *pool = reinterpret_cast<GQueue *>(g_hash_table_lookup(self->pools, stream));
	g_return_val_if_fail(pool, false);

	auto *frame = reinterpret_cast<FrameWrap *>(g_queue_pop_head(pool));
	if (!frame)
		return false;

	for (GstMemory *mem : frame->planes_) {
		frame->acquirePlane();
		gst_buffer_append_memory(buffer, mem);
		g_object_ref(mem->allocator);
	}

	return true;
}

gsize
gst_libcamera_allocator_get_pool_size(GstLibcameraAllocator *self,
				      Stream *stream)
{
	GLibLocker lock(GST_OBJECT(self));

	auto *pool = reinterpret_cast<GQueue *>(g_hash_table_lookup(self->pools, stream));
	g_return_val_if_fail(pool, false);

	return pool->length;
}

FrameBuffer *
gst_libcamera_memory_get_frame_buffer(GstMemory *mem)
{
	auto *frame = reinterpret_cast<FrameWrap *>(gst_mini_object_get_qdata(GST_MINI_OBJECT_CAST(mem),
									      FrameWrap::getQuark()));
	return frame->buffer_;
}