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

#include "drm.h"

#include <algorithm>
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <iostream>
#include <set>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/types.h>

#include <libcamera/framebuffer.h>
#include <libcamera/geometry.h>
#include <libcamera/pixel_format.h>

#include <libdrm/drm_mode.h>

#include "event_loop.h"

namespace DRM {

Object::Object(Device *dev, uint32_t id, Type type)
	: id_(id), dev_(dev), type_(type)
{
	/* Retrieve properties from the objects that support them. */
	if (type != TypeConnector && type != TypeCrtc &&
	    type != TypeEncoder && type != TypePlane)
		return;

	/*
	 * We can't distinguish between failures due to the object having no
	 * property and failures due to other conditions. Assume we use the API
	 * correctly and consider the object has no property.
	 */
	drmModeObjectProperties *properties = drmModeObjectGetProperties(dev->fd(), id, type);
	if (!properties)
		return;

	properties_.reserve(properties->count_props);
	for (uint32_t i = 0; i < properties->count_props; ++i)
		properties_.emplace_back(properties->props[i],
					 properties->prop_values[i]);

	drmModeFreeObjectProperties(properties);
}

Object::~Object()
{
}

const Property *Object::property(const std::string &name) const
{
	for (const PropertyValue &pv : properties_) {
		const Property *property = static_cast<const Property *>(dev_->object(pv.id()));
		if (property && property->name() == name)
			return property;
	}

	return nullptr;
}

const PropertyValue *Object::propertyValue(const std::string &name) const
{
	for (const PropertyValue &pv : properties_) {
		const Property *property = static_cast<const Property *>(dev_->object(pv.id()));
		if (property && property->name() == name)
			return &pv;
	}

	return nullptr;
}

Property::Property(Device *dev, drmModePropertyRes *property)
	: Object(dev, property->prop_id, TypeProperty),
	  name_(property->name), flags_(property->flags),
	  values_(property->values, property->values + property->count_values),
	  blobs_(property->blob_ids, property->blob_ids + property->count_blobs)
{
	if (drm_property_type_is(property, DRM_MODE_PROP_RANGE))
		type_ = TypeRange;
	else if (drm_property_type_is(property, DRM_MODE_PROP_ENUM))
		type_ = TypeEnum;
	else if (drm_property_type_is(property, DRM_MODE_PROP_BLOB))
		type_ = TypeBlob;
	else if (drm_property_type_is(property, DRM_MODE_PROP_BITMASK))
		type_ = TypeBitmask;
	else if (drm_property_type_is(property, DRM_MODE_PROP_OBJECT))
		type_ = TypeObject;
	else if (drm_property_type_is(property, DRM_MODE_PROP_SIGNED_RANGE))
		type_ = TypeSignedRange;
	else
		type_ = TypeUnknown;

	for (int i = 0; i < property->count_enums; ++i)
		enums_[property->enums[i].value] = property->enums[i].name;
}

Blob::Blob(Device *dev, const libcamera::Span<const uint8_t> &data)
	: Object(dev, 0, Object::TypeBlob)
{
	drmModeCreatePropertyBlob(dev->fd(), data.data(), data.size(), &id_);
}

Blob::~Blob()
{
	if (isValid())
		drmModeDestroyPropertyBlob(device()->fd(), id());
}

Mode::Mode(const drmModeModeInfo &mode)
	: drmModeModeInfo(mode)
{
}

std::unique_ptr<Blob> Mode::toBlob(Device *dev) const
{
	libcamera::Span<const uint8_t> data{ reinterpret_cast<const uint8_t *>(this),
					     sizeof(*this) };
	return std::make_unique<Blob>(dev, data);
}

Crtc::Crtc(Device *dev, const drmModeCrtc *crtc, unsigned int index)
	: Object(dev, crtc->crtc_id, Object::TypeCrtc), index_(index)
{
}

Encoder::Encoder(Device *dev, const drmModeEncoder *encoder)
	: Object(dev, encoder->encoder_id, Object::TypeEncoder),
	  type_(encoder->encoder_type)
{
	const std::list<Crtc> &crtcs = dev->crtcs();
	possibleCrtcs_.reserve(crtcs.size());

	for (const Crtc &crtc : crtcs) {
		if (encoder->possible_crtcs & (1 << crtc.index()))
			possibleCrtcs_.push_back(&crtc);
	}

	possibleCrtcs_.shrink_to_fit();
}

namespace {

const std::map<uint32_t, const char *> connectorTypeNames{
	{ DRM_MODE_CONNECTOR_Unknown, "Unknown" },
	{ DRM_MODE_CONNECTOR_VGA, "VGA" },
	{ DRM_MODE_CONNECTOR_DVII, "DVI-I" },
	{ DRM_MODE_CONNECTOR_DVID, "DVI-D" },
	{ DRM_MODE_CONNECTOR_DVIA, "DVI-A" },
	{ DRM_MODE_CONNECTOR_Composite, "Composite" },
	{ DRM_MODE_CONNECTOR_SVIDEO, "S-Video" },
	{ DRM_MODE_CONNECTOR_LVDS, "LVDS" },
	{ DRM_MODE_CONNECTOR_Component, "Component" },
	{ DRM_MODE_CONNECTOR_9PinDIN, "9-Pin-DIN" },
	{ DRM_MODE_CONNECTOR_DisplayPort, "DP" },
	{ DRM_MODE_CONNECTOR_HDMIA, "HDMI-A" },
	{ DRM_MODE_CONNECTOR_HDMIB, "HDMI-B" },
	{ DRM_MODE_CONNECTOR_TV, "TV" },
	{ DRM_MODE_CONNECTOR_eDP, "eDP" },
	{ DRM_MODE_CONNECTOR_VIRTUAL, "Virtual" },
	{ DRM_MODE_CONNECTOR_DSI, "DSI" },
	{ DRM_MODE_CONNECTOR_DPI, "DPI" },
};

} /* namespace */

Connector::Connector(Device *dev, const drmModeConnector *connector)
	: Object(dev, connector->connector_id, Object::TypeConnector),
	  type_(connector->connector_type)
{
	auto typeName = connectorTypeNames.find(connector->connector_type);
	if (typeName == connectorTypeNames.end()) {
		std::cerr
			<< "Invalid connector type "
			<< connector->connector_type << std::endl;
		typeName = connectorTypeNames.find(DRM_MODE_CONNECTOR_Unknown);
	}

	name_ = std::string(typeName->second) + "-"
	      + std::to_string(connector->connector_type_id);

	switch (connector->connection) {
	case DRM_MODE_CONNECTED:
		status_ = Status::Connected;
		break;

	case DRM_MODE_DISCONNECTED:
		status_ = Status::Disconnected;
		break;

	case DRM_MODE_UNKNOWNCONNECTION:
	default:
		status_ = Status::Unknown;
		break;
	}

	const std::list<Encoder> &encoders = dev->encoders();

	encoders_.reserve(connector->count_encoders);

	for (int i = 0; i < connector->count_encoders; ++i) {
		uint32_t encoderId = connector->encoders[i];
		auto encoder = std::find_if(encoders.begin(), encoders.end(),
					    [=](const Encoder &e) {
						    return e.id() == encoderId;
					    });
		if (encoder == encoders.end()) {
			std::cerr
				<< "Encoder " << encoderId << " not found"
				<< std::endl;
			continue;
		}

		encoders_.push_back(&*encoder);
	}

	encoders_.shrink_to_fit();

	modes_ = { connector->modes, connector->modes + connector->count_modes };
}

Plane::Plane(Device *dev, const drmModePlane *plane)
	: Object(dev, plane->plane_id, Object::TypePlane),
	  possibleCrtcsMask_(plane->possible_crtcs)
{
	formats_ = { plane->formats, plane->formats + plane->count_formats };

	const std::list<Crtc> &crtcs = dev->crtcs();
	possibleCrtcs_.reserve(crtcs.size());

	for (const Crtc &crtc : crtcs) {
		if (plane->possible_crtcs & (1 << crtc.index()))
			possibleCrtcs_.push_back(&crtc);
	}

	possibleCrtcs_.shrink_to_fit();
}

bool Plane::supportsFormat(const libcamera::PixelFormat &format) const
{
	return std::find(formats_.begin(), formats_.end(), format.fourcc())
		!= formats_.end();
}

int Plane::setup()
{
	const PropertyValue *pv = propertyValue("type");
	if (!pv)
		return -EINVAL;

	switch (pv->value()) {
	case DRM_PLANE_TYPE_OVERLAY:
		type_ = TypeOverlay;
		break;

	case DRM_PLANE_TYPE_PRIMARY:
		type_ = TypePrimary;
		break;

	case DRM_PLANE_TYPE_CURSOR:
		type_ = TypeCursor;
		break;

	default:
		return -EINVAL;
	}

	return 0;
}

FrameBuffer::FrameBuffer(Device *dev)
	: Object(dev, 0, Object::TypeFb)
{
}

FrameBuffer::~FrameBuffer()
{
	for (const auto &plane : planes_) {
		struct drm_gem_close gem_close = {
			.handle = plane.second.handle,
			.pad = 0,
		};
		int ret;

		do {
			ret = ioctl(device()->fd(), DRM_IOCTL_GEM_CLOSE, &gem_close);
		} while (ret == -1 && (errno == EINTR || errno == EAGAIN));

		if (ret == -1) {
			ret = -errno;
			std::cerr
				<< "Failed to close GEM object: "
				<< strerror(-ret) << std::endl;
		}
	}

	drmModeRmFB(device()->fd(), id());
}

AtomicRequest::AtomicRequest(Device *dev)
	: dev_(dev), valid_(true)
{
	request_ = drmModeAtomicAlloc();
	if (!request_)
		valid_ = false;
}

AtomicRequest::~AtomicRequest()
{
	if (request_)
		drmModeAtomicFree(request_);
}

int AtomicRequest::addProperty(const Object *object, const std::string &property,
			       uint64_t value)
{
	if (!valid_)
		return -EINVAL;

	const Property *prop = object->property(property);
	if (!prop) {
		valid_ = false;
		return -EINVAL;
	}

	return addProperty(object->id(), prop->id(), value);
}

int AtomicRequest::addProperty(const Object *object, const std::string &property,
			       std::unique_ptr<Blob> blob)
{
	if (!valid_)
		return -EINVAL;

	const Property *prop = object->property(property);
	if (!prop) {
		valid_ = false;
		return -EINVAL;
	}

	int ret = addProperty(object->id(), prop->id(), blob->id());
	if (ret < 0)
		return ret;

	blobs_.emplace_back(std::move(blob));

	return 0;
}

int AtomicRequest::addProperty(uint32_t object, uint32_t property, uint64_t value)
{
	int ret = drmModeAtomicAddProperty(request_, object, property, value);
	if (ret < 0) {
		valid_ = false;
		return ret;
	}

	return 0;
}

int AtomicRequest::commit(unsigned int flags)
{
	if (!valid_)
		return -EINVAL;

	uint32_t drmFlags = 0;
	if (flags & FlagAllowModeset)
		drmFlags |= DRM_MODE_ATOMIC_ALLOW_MODESET;
	if (flags & FlagAsync)
		drmFlags |= DRM_MODE_PAGE_FLIP_EVENT | DRM_MODE_ATOMIC_NONBLOCK;

	return drmModeAtomicCommit(dev_->fd(), request_, drmFlags, this);
}

Device::Device()
	: fd_(-1)
{
}

Device::~Device()
{
	if (fd_ != -1)
		drmClose(fd_);
}

int Device::init()
{
	int ret = openCard();
	if (ret < 0) {
		std::cerr << "Failed to open any DRM/KMS device: "
			  << strerror(-ret) << std::endl;
		return ret;
	}

	/*
	 * Enable the atomic APIs. This also automatically enables the
	 * universal planes API.
	 */
	ret = drmSetClientCap(fd_, DRM_CLIENT_CAP_ATOMIC, 1);
	if (ret < 0) {
		ret = -errno;
		std::cerr
			<< "Failed to enable atomic capability: "
			<< strerror(-ret) << std::endl;
		return ret;
	}

	/* List all the resources. */
	ret = getResources();
	if (ret < 0)
		return ret;

	EventLoop::instance()->addFdEvent(fd_, EventLoop::Read,
					  std::bind(&Device::drmEvent, this));

	return 0;
}

int Device::openCard()
{
	const std::string dirName = "/dev/dri/";
	int ret = -ENOENT;

	/*
	 * Open the first DRM/KMS device beginning with /dev/dri/card. The
	 * libdrm drmOpen*() functions require either a module name or a bus ID,
	 * which we don't have, so bypass them. The automatic module loading and
	 * device node creation from drmOpen() is of no practical use as any
	 * modern system will handle that through udev or an equivalent
	 * component.
	 */
	DIR *folder = opendir(dirName.c_str());
	if (!folder) {
		ret = -errno;
		std::cerr << "Failed to open " << dirName
			  << " directory: " << strerror(-ret) << std::endl;
		return ret;
	}

	for (struct dirent *res; (res = readdir(folder));) {
		if (strncmp(res->d_name, "card", 4))
			continue;

		const std::string devName = dirName + res->d_name;
		fd_ = open(devName.c_str(), O_RDWR | O_CLOEXEC);
		if (fd_ >= 0) {
			ret = 0;
			break;
		}

		ret = -errno;
		std::cerr << "Failed to open DRM/KMS device " << devName << ": "
			  << strerror(-ret) << std::endl;
	}

	closedir(folder);

	return ret;
}

int Device::getResources()
{
	int ret;

	std::unique_ptr<drmModeRes, decltype(&drmModeFreeResources)> resources{
		drmModeGetResources(fd_),
		&drmModeFreeResources
	};
	if (!resources) {
		ret = -errno;
		std::cerr
			<< "Failed to get DRM/KMS resources: "
			<< strerror(-ret) << std::endl;
		return ret;
	}

	for (int i = 0; i < resources->count_crtcs; ++i) {
		drmModeCrtc *crtc = drmModeGetCrtc(fd_, resources->crtcs[i]);
		if (!crtc) {
			ret = -errno;
			std::cerr
				<< "Failed to get CRTC: " << strerror(-ret)
				<< std::endl;
			return ret;
		}

		crtcs_.emplace_back(this, crtc, i);
		drmModeFreeCrtc(crtc);

		Crtc &obj = crtcs_.back();
		objects_[obj.id()] = &obj;
	}

	for (int i = 0; i < resources->count_encoders; ++i) {
		drmModeEncoder *encoder =
			drmModeGetEncoder(fd_, resources->encoders[i]);
		if (!encoder) {
			ret = -errno;
			std::cerr
				<< "Failed to get encoder: " << strerror(-ret)
				<< std::endl;
			return ret;
		}

		encoders_.emplace_back(this, encoder);
		drmModeFreeEncoder(encoder);

		Encoder &obj = encoders_.back();
		objects_[obj.id()] = &obj;
	}

	for (int i = 0; i < resources->count_connectors; ++i) {
		drmModeConnector *connector =
			drmModeGetConnector(fd_, resources->connectors[i]);
		if (!connector) {
			ret = -errno;
			std::cerr
				<< "Failed to get connector: " << strerror(-ret)
				<< std::endl;
			return ret;
		}

		connectors_.emplace_back(this, connector);
		drmModeFreeConnector(connector);

		Connector &obj = connectors_.back();
		objects_[obj.id()] = &obj;
	}

	std::unique_ptr<drmModePlaneRes, decltype(&drmModeFreePlaneResources)> planes{
		drmModeGetPlaneResources(fd_),
		&drmModeFreePlaneResources
	};
	if (!planes) {
		ret = -errno;
		std::cerr
			<< "Failed to get DRM/KMS planes: "
			<< strerror(-ret) << std::endl;
		return ret;
	}

	for (uint32_t i = 0; i < planes->count_planes; ++i) {
		drmModePlane *plane =
			drmModeGetPlane(fd_, planes->planes[i]);
		if (!plane) {
			ret = -errno;
			std::cerr
				<< "Failed to get plane: " << strerror(-ret)
				<< std::endl;
			return ret;
		}

		planes_.emplace_back(this, plane);
		drmModeFreePlane(plane);

		Plane &obj = planes_.back();
		objects_[obj.id()] = &obj;
	}

	/* Set the possible planes for each CRTC. */
	for (Crtc &crtc : crtcs_) {
		for (const Plane &plane : planes_) {
			if (plane.possibleCrtcsMask_ & (1 << crtc.index()))
				crtc.planes_.push_back(&plane);
		}
	}

	/* Collect all property IDs and create Property instances. */
	std::set<uint32_t> properties;
	for (const auto &object : objects_) {
		for (const PropertyValue &value : object.second->properties())
			properties.insert(value.id());
	}

	for (uint32_t id : properties) {
		drmModePropertyRes *property = drmModeGetProperty(fd_, id);
		if (!property) {
			ret = -errno;
			std::cerr
				<< "Failed to get property: " << strerror(-ret)
				<< std::endl;
			continue;
		}

		properties_.emplace_back(this, property);
		drmModeFreeProperty(property);

		Property &obj = properties_.back();
		objects_[obj.id()] = &obj;
	}

	/* Finally, perform all delayed setup of mode objects. */
	for (auto &object : objects_) {
		ret = object.second->setup();
		if (ret < 0) {
			std::cerr
				<< "Failed to setup object " << object.second->id()
				<< ": " << strerror(-ret) << std::endl;
			return ret;
		}
	}

	return 0;
}

const Object *Device::object(uint32_t id)
{
	const auto iter = objects_.find(id);
	if (iter == objects_.end())
		return nullptr;

	return iter->second;
}

std::unique_ptr<FrameBuffer> Device::createFrameBuffer(
	const libcamera::FrameBuffer &buffer,
	const libcamera::PixelFormat &format,
	const libcamera::Size &size,
	const std::array<uint32_t, 4> &strides)
{
	std::unique_ptr<FrameBuffer> fb{ new FrameBuffer(this) };

	uint32_t handles[4] = {};
	uint32_t offsets[4] = {};
	int ret;

	const std::vector<libcamera::FrameBuffer::Plane> &planes = buffer.planes();

	unsigned int i = 0;
	for (const libcamera::FrameBuffer::Plane &plane : planes) {
		int fd = plane.fd.get();
		uint32_t handle;

		auto iter = fb->planes_.find(fd);
		if (iter == fb->planes_.end()) {
			ret = drmPrimeFDToHandle(fd_, plane.fd.get(), &handle);
			if (ret < 0) {
				ret = -errno;
				std::cerr
					<< "Unable to import framebuffer dmabuf: "
					<< strerror(-ret) << std::endl;
				return nullptr;
			}

			fb->planes_[fd] = { handle };
		} else {
			handle = iter->second.handle;
		}

		handles[i] = handle;
		offsets[i] = plane.offset;
		++i;
	}

	ret = drmModeAddFB2(fd_, size.width, size.height, format.fourcc(), handles,
			    strides.data(), offsets, &fb->id_, 0);
	if (ret < 0) {
		ret = -errno;
		std::cerr
			<< "Failed to add framebuffer: "
			<< strerror(-ret) << std::endl;
		return nullptr;
	}

	return fb;
}

void Device::drmEvent()
{
	drmEventContext ctx{};
	ctx.version = DRM_EVENT_CONTEXT_VERSION;
	ctx.page_flip_handler = &Device::pageFlipComplete;

	drmHandleEvent(fd_, &ctx);
}

void Device::pageFlipComplete([[maybe_unused]] int fd,
			      [[maybe_unused]] unsigned int sequence,
			      [[maybe_unused]] unsigned int tv_sec,
			      [[maybe_unused]] unsigned int tv_usec,
			      void *user_data)
{
	AtomicRequest *request = static_cast<AtomicRequest *>(user_data);
	request->device()->requestComplete.emit(request);
}

} /* namespace DRM */