/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
 * Copyright (C) 2021, Ideas on Board Oy
 *
 * kms_sink.cpp - KMS Sink
 */

#include "kms_sink.h"

#include <array>
#include <algorithm>
#include <assert.h>
#include <iostream>
#include <limits.h>
#include <memory>
#include <stdint.h>
#include <string.h>

#include <libcamera/camera.h>
#include <libcamera/formats.h>
#include <libcamera/framebuffer.h>
#include <libcamera/stream.h>

#include "drm.h"

KMSSink::KMSSink(const std::string &connectorName)
	: connector_(nullptr), crtc_(nullptr), plane_(nullptr), mode_(nullptr)
{
	int ret = dev_.init();
	if (ret < 0)
		return;

	/*
	 * Find the requested connector. If no specific connector is requested,
	 * pick the first connected connector or, if no connector is connected,
	 * the first connector with unknown status.
	 */
	for (const DRM::Connector &conn : dev_.connectors()) {
		if (!connectorName.empty()) {
			if (conn.name() != connectorName)
				continue;

			connector_ = &conn;
			break;
		}

		if (conn.status() == DRM::Connector::Connected) {
			connector_ = &conn;
			break;
		}

		if (!connector_ && conn.status() == DRM::Connector::Unknown)
			connector_ = &conn;
	}

	if (!connector_) {
		if (!connectorName.empty())
			std::cerr
				<< "Connector " << connectorName << " not found"
				<< std::endl;
		else
			std::cerr << "No connected connector found" << std::endl;
		return;
	}

	dev_.requestComplete.connect(this, &KMSSink::requestComplete);
}

void KMSSink::mapBuffer(libcamera::FrameBuffer *buffer)
{
	std::array<uint32_t, 4> strides = {};

	/* \todo Should libcamera report per-plane strides ? */
	unsigned int uvStrideMultiplier;

	switch (format_) {
	case libcamera::formats::NV24:
	case libcamera::formats::NV42:
		uvStrideMultiplier = 4;
		break;
	case libcamera::formats::YUV420:
	case libcamera::formats::YVU420:
	case libcamera::formats::YUV422:
		uvStrideMultiplier = 1;
		break;
	default:
		uvStrideMultiplier = 2;
		break;
	}

	strides[0] = stride_;
	for (unsigned int i = 1; i < buffer->planes().size(); ++i)
		strides[i] = stride_ * uvStrideMultiplier / 2;

	std::unique_ptr<DRM::FrameBuffer> drmBuffer =
		dev_.createFrameBuffer(*buffer, format_, size_, strides);
	if (!drmBuffer)
		return;

	buffers_.emplace(std::piecewise_construct,
			 std::forward_as_tuple(buffer),
			 std::forward_as_tuple(std::move(drmBuffer)));
}

int KMSSink::configure(const libcamera::CameraConfiguration &config)
{
	if (!connector_)
		return -EINVAL;

	crtc_ = nullptr;
	plane_ = nullptr;
	mode_ = nullptr;

	const libcamera::StreamConfiguration &cfg = config.at(0);

	/* Find the best mode for the stream size. */
	const std::vector<DRM::Mode> &modes = connector_->modes();

	unsigned int cfgArea = cfg.size.width * cfg.size.height;
	unsigned int bestDistance = UINT_MAX;

	for (const DRM::Mode &mode : modes) {
		unsigned int modeArea = mode.hdisplay * mode.vdisplay;
		unsigned int distance = modeArea > cfgArea ? modeArea - cfgArea
				      : cfgArea - modeArea;

		if (distance < bestDistance) {
			mode_ = &mode;
			bestDistance = distance;

			/*
			 * If the sizes match exactly, there will be no better
			 * match.
			 */
			if (distance == 0)
				break;
		}
	}

	if (!mode_) {
		std::cerr << "No modes\n";
		return -EINVAL;
	}

	int ret = configurePipeline(cfg.pixelFormat);
	if (ret < 0)
		return ret;

	size_ = cfg.size;
	stride_ = cfg.stride;

	/* Configure color space. */
	colorEncoding_ = std::nullopt;
	colorRange_ = std::nullopt;

	if (cfg.colorSpace->ycbcrEncoding == libcamera::ColorSpace::YcbcrEncoding::None)
		return 0;

	/*
	 * The encoding and range enums are defined in the kernel but not
	 * exposed in public headers.
	 */
	enum drm_color_encoding {
		DRM_COLOR_YCBCR_BT601,
		DRM_COLOR_YCBCR_BT709,
		DRM_COLOR_YCBCR_BT2020,
	};

	enum drm_color_range {
		DRM_COLOR_YCBCR_LIMITED_RANGE,
		DRM_COLOR_YCBCR_FULL_RANGE,
	};

	const DRM::Property *colorEncoding = plane_->property("COLOR_ENCODING");
	const DRM::Property *colorRange = plane_->property("COLOR_RANGE");

	if (colorEncoding) {
		drm_color_encoding encoding;

		switch (cfg.colorSpace->ycbcrEncoding) {
		case libcamera::ColorSpace::YcbcrEncoding::Rec601:
		default:
			encoding = DRM_COLOR_YCBCR_BT601;
			break;
		case libcamera::ColorSpace::YcbcrEncoding::Rec709:
			encoding = DRM_COLOR_YCBCR_BT709;
			break;
		case libcamera::ColorSpace::YcbcrEncoding::Rec2020:
			encoding = DRM_COLOR_YCBCR_BT2020;
			break;
		}

		for (const auto &[id, name] : colorEncoding->enums()) {
			if (id == encoding) {
				colorEncoding_ = encoding;
				break;
			}
		}
	}

	if (colorRange) {
		drm_color_range range;

		switch (cfg.colorSpace->range) {
		case libcamera::ColorSpace::Range::Limited:
		default:
			range = DRM_COLOR_YCBCR_LIMITED_RANGE;
			break;
		case libcamera::ColorSpace::Range::Full:
			range = DRM_COLOR_YCBCR_FULL_RANGE;
			break;
		}

		for (const auto &[id, name] : colorRange->enums()) {
			if (id == range) {
				colorRange_ = range;
				break;
			}
		}
	}

	if (!colorEncoding_ || !colorRange_)
		std::cerr << "Color space " << cfg.colorSpace->toString()
			  << " not supported by the display device."
			  << " Colors may be wrong." << std::endl;

	return 0;
}

int KMSSink::selectPipeline(const libcamera::PixelFormat &format)
{
	/*
	 * If the requested format has an alpha channel, also consider the X
	 * variant.
	 */
	libcamera::PixelFormat xFormat;

	switch (format) {
	case libcamera::formats::ABGR8888:
		xFormat = libcamera::formats::XBGR8888;
		break;
	case libcamera::formats::ARGB8888:
		xFormat = libcamera::formats::XRGB8888;
		break;
	case libcamera::formats::BGRA8888:
		xFormat = libcamera::formats::BGRX8888;
		break;
	case libcamera::formats::RGBA8888:
		xFormat = libcamera::formats::RGBX8888;
		break;
	}

	/*
	 * Find a CRTC and plane suitable for the request format and the
	 * connector at the end of the pipeline. Restrict the search to primary
	 * planes for now.
	 */
	for (const DRM::Encoder *encoder : connector_->encoders()) {
		for (const DRM::Crtc *crtc : encoder->possibleCrtcs()) {
			for (const DRM::Plane *plane : crtc->planes()) {
				if (plane->type() != DRM::Plane::TypePrimary)
					continue;

				if (plane->supportsFormat(format)) {
					crtc_ = crtc;
					plane_ = plane;
					format_ = format;
					return 0;
				}

				if (plane->supportsFormat(xFormat)) {
					crtc_ = crtc;
					plane_ = plane;
					format_ = xFormat;
					return 0;
				}
			}
		}
	}

	return -EPIPE;
}

int KMSSink::configurePipeline(const libcamera::PixelFormat &format)
{
	const int ret = selectPipeline(format);
	if (ret) {
		std::cerr
			<< "Unable to find display pipeline for format "
			<< format << std::endl;

		return ret;
	}

	std::cout
		<< "Using KMS plane " << plane_->id() << ", CRTC " << crtc_->id()
		<< ", connector " << connector_->name()
		<< " (" << connector_->id() << "), mode " << mode_->hdisplay
		<< "x" << mode_->vdisplay << "@" << mode_->vrefresh << std::endl;

	return 0;
}

int KMSSink::start()
{
	int ret = FrameSink::start();
	if (ret < 0)
		return ret;

	/* Disable all CRTCs and planes to start from a known valid state. */
	DRM::AtomicRequest request(&dev_);

	for (const DRM::Crtc &crtc : dev_.crtcs())
		request.addProperty(&crtc, "ACTIVE", 0);

	for (const DRM::Plane &plane : dev_.planes()) {
		request.addProperty(&plane, "CRTC_ID", 0);
		request.addProperty(&plane, "FB_ID", 0);
	}

	ret = request.commit(DRM::AtomicRequest::FlagAllowModeset);
	if (ret < 0) {
		std::cerr
			<< "Failed to disable CRTCs and planes: "
			<< strerror(-ret) << std::endl;
		return ret;
	}

	return 0;
}

int KMSSink::stop()
{
	/* Display pipeline. */
	DRM::AtomicRequest request(&dev_);

	request.addProperty(connector_, "CRTC_ID", 0);
	request.addProperty(crtc_, "ACTIVE", 0);
	request.addProperty(crtc_, "MODE_ID", 0);
	request.addProperty(plane_, "CRTC_ID", 0);
	request.addProperty(plane_, "FB_ID", 0);

	int ret = request.commit(DRM::AtomicRequest::FlagAllowModeset);
	if (ret < 0) {
		std::cerr
			<< "Failed to stop display pipeline: "
			<< strerror(-ret) << std::endl;
		return ret;
	}

	/* Free all buffers. */
	pending_.reset();
	queued_.reset();
	active_.reset();
	buffers_.clear();

	return FrameSink::stop();
}

bool KMSSink::testModeSet(DRM::FrameBuffer *drmBuffer,
			  const libcamera::Rectangle &src,
			  const libcamera::Rectangle &dst)
{
	DRM::AtomicRequest drmRequest{ &dev_ };

	drmRequest.addProperty(connector_, "CRTC_ID", crtc_->id());

	drmRequest.addProperty(crtc_, "ACTIVE", 1);
	drmRequest.addProperty(crtc_, "MODE_ID", mode_->toBlob(&dev_));

	drmRequest.addProperty(plane_, "CRTC_ID", crtc_->id());
	drmRequest.addProperty(plane_, "FB_ID", drmBuffer->id());
	drmRequest.addProperty(plane_, "SRC_X", src.x << 16);
	drmRequest.addProperty(plane_, "SRC_Y", src.y << 16);
	drmRequest.addProperty(plane_, "SRC_W", src.width << 16);
	drmRequest.addProperty(plane_, "SRC_H", src.height << 16);
	drmRequest.addProperty(plane_, "CRTC_X", dst.x);
	drmRequest.addProperty(plane_, "CRTC_Y", dst.y);
	drmRequest.addProperty(plane_, "CRTC_W", dst.width);
	drmRequest.addProperty(plane_, "CRTC_H", dst.height);

	return !drmRequest.commit(DRM::AtomicRequest::FlagAllowModeset |
				  DRM::AtomicRequest::FlagTestOnly);
}

bool KMSSink::setupComposition(DRM::FrameBuffer *drmBuffer)
{
	/*
	 * Test composition options, from most to least desirable, to select the
	 * best one.
	 */
	const libcamera::Rectangle framebuffer{ size_ };
	const libcamera::Rectangle display{ 0, 0, mode_->hdisplay, mode_->vdisplay };

	/* 1. Scale the frame buffer to full screen, preserving aspect ratio. */
	libcamera::Rectangle src = framebuffer;
	libcamera::Rectangle dst = display.size().boundedToAspectRatio(framebuffer.size())
						 .centeredTo(display.center());

	if (testModeSet(drmBuffer, src, dst)) {
		std::cout << "KMS: full-screen scaled output, square pixels"
			  << std::endl;
		src_ = src;
		dst_ = dst;
		return true;
	}

	/*
	 * 2. Scale the frame buffer to full screen, without preserving aspect
	 *    ratio.
	 */
	src = framebuffer;
	dst = display;

	if (testModeSet(drmBuffer, src, dst)) {
		std::cout << "KMS: full-screen scaled output, non-square pixels"
			  << std::endl;
		src_ = src;
		dst_ = dst;
		return true;
	}

	/* 3. Center the frame buffer on the display. */
	src = display.size().centeredTo(framebuffer.center()).boundedTo(framebuffer);
	dst = framebuffer.size().centeredTo(display.center()).boundedTo(display);

	if (testModeSet(drmBuffer, src, dst)) {
		std::cout << "KMS: centered output" << std::endl;
		src_ = src;
		dst_ = dst;
		return true;
	}

	/* 4. Align the frame buffer on the top-left of the display. */
	src = framebuffer.boundedTo(display);
	dst = display.boundedTo(framebuffer);

	if (testModeSet(drmBuffer, src, dst)) {
		std::cout << "KMS: top-left aligned output" << std::endl;
		src_ = src;
		dst_ = dst;
		return true;
	}

	return false;
}

bool KMSSink::processRequest(libcamera::Request *camRequest)
{
	/*
	 * Perform a very crude rate adaptation by simply dropping the request
	 * if the display queue is full.
	 */
	if (pending_)
		return true;

	libcamera::FrameBuffer *buffer = camRequest->buffers().begin()->second;
	auto iter = buffers_.find(buffer);
	if (iter == buffers_.end())
		return true;

	DRM::FrameBuffer *drmBuffer = iter->second.get();

	unsigned int flags = DRM::AtomicRequest::FlagAsync;
	std::unique_ptr<DRM::AtomicRequest> drmRequest =
		std::make_unique<DRM::AtomicRequest>(&dev_);
	drmRequest->addProperty(plane_, "FB_ID", drmBuffer->id());

	if (!active_ && !queued_) {
		/* Enable the display pipeline on the first frame. */
		if (!setupComposition(drmBuffer)) {
			std::cerr << "Failed to setup composition" << std::endl;
			return true;
		}

		drmRequest->addProperty(connector_, "CRTC_ID", crtc_->id());

		drmRequest->addProperty(crtc_, "ACTIVE", 1);
		drmRequest->addProperty(crtc_, "MODE_ID", mode_->toBlob(&dev_));

		drmRequest->addProperty(plane_, "CRTC_ID", crtc_->id());
		drmRequest->addProperty(plane_, "SRC_X", src_.x << 16);
		drmRequest->addProperty(plane_, "SRC_Y", src_.y << 16);
		drmRequest->addProperty(plane_, "SRC_W", src_.width << 16);
		drmRequest->addProperty(plane_, "SRC_H", src_.height << 16);
		drmRequest->addProperty(plane_, "CRTC_X", dst_.x);
		drmRequest->addProperty(plane_, "CRTC_Y", dst_.y);
		drmRequest->addProperty(plane_, "CRTC_W", dst_.width);
		drmRequest->addProperty(plane_, "CRTC_H", dst_.height);

		if (colorEncoding_)
			drmRequest->addProperty(plane_, "COLOR_ENCODING", *colorEncoding_);
		if (colorRange_)
			drmRequest->addProperty(plane_, "COLOR_RANGE", *colorRange_);

		flags |= DRM::AtomicRequest::FlagAllowModeset;
	}

	pending_ = std::make_unique<Request>(std::move(drmRequest), camRequest);

	std::lock_guard<std::mutex> lock(lock_);

	if (!queued_) {
		int ret = pending_->drmRequest_->commit(flags);
		if (ret < 0) {
			std::cerr
				<< "Failed to commit atomic request: "
				<< strerror(-ret) << std::endl;
			/* \todo Implement error handling */
		}

		queued_ = std::move(pending_);
	}

	return false;
}

void KMSSink::requestComplete([[maybe_unused]] DRM::AtomicRequest *request)
{
	std::lock_guard<std::mutex> lock(lock_);

	assert(queued_ && queued_->drmRequest_.get() == request);

	/* Complete the active request, if any. */
	if (active_)
		requestProcessed.emit(active_->camRequest_);

	/* The queued request becomes active. */
	active_ = std::move(queued_);

	/* Queue the pending request, if any. */
	if (pending_) {
		pending_->drmRequest_->commit(DRM::AtomicRequest::FlagAsync);
		queued_ = std::move(pending_);
	}
}
