/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
 * Copyright (C) 2019, Google Inc.
 *
 * libcamera Camera API tests
 *
 * Test importing buffers exported from the VIVID output device into a Camera
 */

#include <algorithm>
#include <iostream>
#include <numeric>
#include <random>
#include <vector>

#include "device_enumerator.h"
#include "media_device.h"
#include "v4l2_videodevice.h"

#include "camera_test.h"
#include "test.h"

using namespace libcamera;

/* Keep SINK_BUFFER_COUNT > CAMERA_BUFFER_COUNT + 1 */
static constexpr unsigned int SINK_BUFFER_COUNT = 8;
static constexpr unsigned int CAMERA_BUFFER_COUNT = 4;

class FrameSink
{
public:
	FrameSink()
		: video_(nullptr)
	{
	}

	int init()
	{
		int ret;

		/* Locate and open the video device. */
		std::string videoDeviceName = "vivid-000-vid-out";

		std::unique_ptr<DeviceEnumerator> enumerator =
			DeviceEnumerator::create();
		if (!enumerator) {
			std::cout << "Failed to create device enumerator" << std::endl;
			return TestFail;
		}

		if (enumerator->enumerate()) {
			std::cout << "Failed to enumerate media devices" << std::endl;
			return TestFail;
		}

		DeviceMatch dm("vivid");
		dm.add(videoDeviceName);

		media_ = enumerator->search(dm);
		if (!media_) {
			std::cout << "No vivid output device available" << std::endl;
			return TestSkip;
		}

		video_ = V4L2VideoDevice::fromEntityName(media_.get(), videoDeviceName);
		if (!video_) {
			std::cout << "Unable to open " << videoDeviceName << std::endl;
			return TestFail;
		}

		if (video_->open())
			return TestFail;

		/* Configure the format. */
		ret = video_->getFormat(&format_);
		if (ret) {
			std::cout << "Failed to get format on output device" << std::endl;
			return ret;
		}

		format_.size.width = 1920;
		format_.size.height = 1080;
		format_.fourcc = V4L2_PIX_FMT_RGB24;
		format_.planesCount = 1;
		format_.planes[0].size = 1920 * 1080 * 3;
		format_.planes[0].bpl = 1920 * 3;

		if (video_->setFormat(&format_)) {
			cleanup();
			return TestFail;
		}

		/* Export the buffers to a pool. */
		pool_.createBuffers(SINK_BUFFER_COUNT);
		ret = video_->exportBuffers(&pool_);
		if (ret) {
			std::cout << "Failed to export buffers" << std::endl;
			cleanup();
			return TestFail;
		}

		/* Only use the first CAMERA_BUFFER_COUNT buffers to start with. */
		availableBuffers_.resize(CAMERA_BUFFER_COUNT);
		std::iota(availableBuffers_.begin(), availableBuffers_.end(), 0);

		/* Connect the buffer ready signal. */
		video_->bufferReady.connect(this, &FrameSink::bufferComplete);

		return TestPass;
	}

	void cleanup()
	{
		if (video_) {
			video_->streamOff();
			video_->releaseBuffers();
			video_->close();

			delete video_;
			video_ = nullptr;
		}

		if (media_)
			media_->release();
	}

	int start()
	{
		requestsCount_ = 0;
		done_ = false;

		int ret = video_->streamOn();
		if (ret < 0)
			return ret;

		/* Queue all the initial requests. */
		for (unsigned int index = 0; index < CAMERA_BUFFER_COUNT; ++index)
			queueRequest(index);

		return 0;
	}

	int stop()
	{
		return video_->streamOff();
	}

	void requestComplete(uint64_t cookie, const Buffer *metadata)
	{
		unsigned int index = cookie;

		Buffer *buffer = new Buffer(index, metadata);
		int ret = video_->queueBuffer(buffer);
		if (ret < 0)
			std::cout << "Failed to queue buffer to sink" << std::endl;
	}

	bool done() const { return done_; }

	PixelFormat format() const
	{
		return video_->toPixelFormat(format_.fourcc);
	}

	const Size &size() const
	{
		return format_.size;
	}

	Signal<uint64_t, int> requestReady;

private:
	void queueRequest(unsigned int index)
	{
		auto it = std::find(availableBuffers_.begin(),
				    availableBuffers_.end(), index);
		availableBuffers_.erase(it);

		uint64_t cookie = index;
		BufferMemory &mem = pool_.buffers()[index];
		int dmabuf = mem.planes()[0].dmabuf();

		requestReady.emit(cookie, dmabuf);

		requestsCount_++;
	}

	void bufferComplete(Buffer *buffer)
	{
		availableBuffers_.push_back(buffer->index());

		/*
		 * Pick the buffer for the next request among the available
		 * buffers.
		 *
		 * For the first 20 frames, select the buffer that has just
		 * completed to keep the mapping of dmabuf fds to buffers
		 * unchanged in the camera.
		 *
		 * For the next 20 frames, cycle randomly over the available
		 * buffers. The mapping should still be kept unchanged, as the
		 * camera should map using the cached fds.
		 *
		 * For the last 20 frames, cycles through all buffers, which
		 * should trash the mappings.
		 */
		unsigned int index = buffer->index();
		delete buffer;

		std::cout << "Completed buffer, request=" << requestsCount_
			  << ", available buffers=" << availableBuffers_.size()
			  << std::endl;

		if (requestsCount_ >= 60) {
			if (availableBuffers_.size() == SINK_BUFFER_COUNT)
				done_ = true;
			return;
		}

		if (requestsCount_ == 40) {
			/* Add the remaining of the buffers. */
			for (unsigned int i = CAMERA_BUFFER_COUNT;
			     i < SINK_BUFFER_COUNT; ++i)
				availableBuffers_.push_back(i);
		}

		if (requestsCount_ >= 20) {
			/*
			 * Wait until we have enough buffers to make this
			 * meaningful. Preferably half of the camera buffers,
			 * but no less than 2 in any case.
			 */
			const unsigned int min_pool_size =
				std::min(CAMERA_BUFFER_COUNT / 2, 2U);
			if (availableBuffers_.size() < min_pool_size)
				return;

			/* Pick a buffer at random. */
			unsigned int pos = random_() % availableBuffers_.size();
			index = availableBuffers_[pos];
		}

		queueRequest(index);
	}

	std::shared_ptr<MediaDevice> media_;
	V4L2VideoDevice *video_;
	BufferPool pool_;
	V4L2DeviceFormat format_;

	unsigned int requestsCount_;
	std::vector<int> availableBuffers_;
	std::random_device random_;

	bool done_;
};

class BufferImportTest : public CameraTest, public Test
{
public:
	BufferImportTest()
		: CameraTest("VIMC Sensor B")
	{
	}

	void queueRequest(uint64_t cookie, int dmabuf)
	{
		Request *request = camera_->createRequest(cookie);

		std::unique_ptr<Buffer> buffer = stream_->createBuffer({ dmabuf, -1, -1 });
		request->addBuffer(move(buffer));
		camera_->queueRequest(request);
	}

protected:
	void bufferComplete(Request *request, Buffer *buffer)
	{
		if (buffer->status() != Buffer::BufferSuccess)
			return;

		unsigned int index = buffer->index();
		int dmabuf = buffer->dmabufs()[0];

		/* Record dmabuf to index remappings. */
		bool remapped = false;
		if (bufferMappings_.find(index) != bufferMappings_.end()) {
			if (bufferMappings_[index] != dmabuf)
				remapped = true;
		}

		std::cout << "Completed request " << framesCaptured_
			  << ": dmabuf fd " << dmabuf
			  << " -> index " << index
			  << " (" << (remapped ? 'R' : '-') << ")"
			  << std::endl;

		if (remapped)
			bufferRemappings_.push_back(framesCaptured_);

		bufferMappings_[index] = dmabuf;
		framesCaptured_++;

		sink_.requestComplete(request->cookie(), buffer);

		if (framesCaptured_ == 60)
			sink_.stop();
	}

	int initCamera()
	{
		if (camera_->acquire()) {
			std::cout << "Failed to acquire the camera" << std::endl;
			return TestFail;
		}

		/*
		 * Configure the Stream to work with externally allocated
		 * buffers by setting the memoryType to ExternalMemory.
		 */
		std::unique_ptr<CameraConfiguration> config;
		config = camera_->generateConfiguration({ StreamRole::VideoRecording });
		if (!config || config->size() != 1) {
			std::cout << "Failed to generate configuration" << std::endl;
			return TestFail;
		}

		StreamConfiguration &cfg = config->at(0);
		cfg.size = sink_.size();
		cfg.pixelFormat = sink_.format();
		cfg.bufferCount = CAMERA_BUFFER_COUNT;
		cfg.memoryType = ExternalMemory;

		if (camera_->configure(config.get())) {
			std::cout << "Failed to set configuration" << std::endl;
			return TestFail;
		}

		stream_ = cfg.stream();

		/* Allocate buffers. */
		if (camera_->allocateBuffers()) {
			std::cout << "Failed to allocate buffers" << std::endl;
			return TestFail;
		}

		/* Connect the buffer completed signal. */
		camera_->bufferCompleted.connect(this, &BufferImportTest::bufferComplete);

		return TestPass;
	}

	int init()
	{
		if (status_ != TestPass)
			return status_;

		int ret = sink_.init();
		if (ret != TestPass) {
			cleanup();
			return ret;
		}

		ret = initCamera();
		if (ret != TestPass) {
			cleanup();
			return ret;
		}

		sink_.requestReady.connect(this, &BufferImportTest::queueRequest);
		return TestPass;
	}

	int run()
	{
		int ret;

		framesCaptured_ = 0;

		if (camera_->start()) {
			std::cout << "Failed to start camera" << std::endl;
			return TestFail;
		}

		ret = sink_.start();
		if (ret < 0) {
			std::cout << "Failed to start sink" << std::endl;
			return TestFail;
		}

		EventDispatcher *dispatcher = cm_->eventDispatcher();

		Timer timer;
		timer.start(5000);
		while (timer.isRunning() && !sink_.done())
			dispatcher->processEvents();

		std::cout << framesCaptured_ << " frames captured, "
			  << bufferRemappings_.size() << " buffers remapped"
			  << std::endl;

		if (framesCaptured_ < 60) {
			std::cout << "Too few frames captured" << std::endl;
			return TestFail;
		}

		if (bufferRemappings_.empty()) {
			std::cout << "No buffer remappings" << std::endl;
			return TestFail;
		}

		if (bufferRemappings_[0] < 40) {
			std::cout << "Early buffer remapping" << std::endl;
			return TestFail;
		}

		return TestPass;
	}

	void cleanup()
	{
		sink_.cleanup();

		camera_->stop();
		camera_->freeBuffers();
	}

private:
	Stream *stream_;

	std::map<unsigned int, int> bufferMappings_;
	std::vector<unsigned int> bufferRemappings_;
	unsigned int framesCaptured_;

	FrameSink sink_;
};

TEST_REGISTER(BufferImportTest);