#include "device.h"

#include <errno.h>
#include <fcntl.h>
#include <glib.h>
#include <stdio.h>
#include <string.h>
#include <sys/ioctl.h>
#include <unistd.h>

bool
mp_find_device_path(struct media_v2_intf_devnode devnode, char *path, int length)
{
	char uevent_path[256];
	snprintf(uevent_path, 256, "/sys/dev/char/%d:%d/uevent", devnode.major,
		 devnode.minor);

	FILE *f = fopen(uevent_path, "r");
	if (!f) {
		return false;
	}

	char line[512];
	while (fgets(line, 512, f)) {
		if (strncmp(line, "DEVNAME=", 8) == 0) {
			// Drop newline
			int length = strlen(line);
			if (line[length - 1] == '\n')
				line[length - 1] = '\0';

			snprintf(path, length, "/dev/%s", line + 8);
			return true;
		}
	}

	fclose(f);

	return false;
}

struct _MPDevice {
	int fd;

	struct media_device_info info;

	struct media_v2_entity *entities;
	size_t num_entities;
	struct media_v2_interface *interfaces;
	size_t num_interfaces;
	struct media_v2_pad *pads;
	size_t num_pads;
	struct media_v2_link *links;
	size_t num_links;
};

static void
errno_printerr(const char *s)
{
	g_printerr("MPDevice: %s error %d, %s\n", s, errno, strerror(errno));
}

static int
xioctl(int fd, int request, void *arg)
{
	int r;
	do {
		r = ioctl(fd, request, arg);
	} while (r == -1 && errno == EINTR);
	return r;
}

MPDevice *
mp_device_find(const char *driver_name)
{
	MPDeviceList *list = mp_device_list_new();

	MPDevice *found_device = mp_device_list_find_remove(&list, driver_name);

	mp_device_list_free(list);

	return found_device;
}

MPDevice *
mp_device_open(const char *path)
{
	int fd = open(path, O_RDWR);
	if (fd == -1) {
		errno_printerr("open");
		return NULL;
	}

	return mp_device_new(fd);
}

MPDevice *
mp_device_new(int fd)
{
	// Get the topology of the media device
	struct media_v2_topology topology = {};
	if (xioctl(fd, MEDIA_IOC_G_TOPOLOGY, &topology) == -1 ||
	    topology.num_entities == 0) {
		close(fd);
		return NULL;
	}

	// Create the device
	MPDevice *device = calloc(1, sizeof(MPDevice));
	device->fd = fd;
	device->entities =
		calloc(topology.num_entities, sizeof(struct media_v2_entity));
	device->num_entities = topology.num_entities;
	device->interfaces =
		calloc(topology.num_interfaces, sizeof(struct media_v2_interface));
	device->num_interfaces = topology.num_interfaces;
	device->pads = calloc(topology.num_pads, sizeof(struct media_v2_pad));
	device->num_pads = topology.num_pads;
	device->links = calloc(topology.num_links, sizeof(struct media_v2_link));
	device->num_links = topology.num_links;

	// Get the actual devices and interfaces
	topology.ptr_entities = (uint64_t)device->entities;
	topology.ptr_interfaces = (uint64_t)device->interfaces;
	topology.ptr_pads = (uint64_t)device->pads;
	topology.ptr_links = (uint64_t)device->links;
	if (xioctl(fd, MEDIA_IOC_G_TOPOLOGY, &topology) == -1) {
		errno_printerr("MEDIA_IOC_G_TOPOLOGY");
		mp_device_close(device);
		return NULL;
	}

	// Get device info
	if (xioctl(fd, MEDIA_IOC_DEVICE_INFO, &device->info) == -1) {
		errno_printerr("MEDIA_IOC_DEVICE_INFO");
		mp_device_close(device);
		return NULL;
	}

	return device;
}

void
mp_device_close(MPDevice *device)
{
	close(device->fd);
	free(device->entities);
	free(device->interfaces);
	free(device->pads);
	free(device->links);
	free(device);
}

bool
mp_device_setup_link(MPDevice *device, uint32_t source_pad_id, uint32_t sink_pad_id,
		     bool enabled)
{
	const struct media_v2_pad *source_pad =
		mp_device_get_pad(device, source_pad_id);
	g_return_val_if_fail(source_pad, false);

	const struct media_v2_pad *sink_pad = mp_device_get_pad(device, sink_pad_id);
	g_return_val_if_fail(sink_pad, false);

	struct media_link_desc link = {};
	link.flags = enabled ? MEDIA_LNK_FL_ENABLED : 0;
	link.source.entity = source_pad->entity_id;
	link.source.index = 0;
	link.sink.entity = sink_pad->entity_id;
	link.sink.index = 0;
	if (xioctl(device->fd, MEDIA_IOC_SETUP_LINK, &link) == -1) {
		errno_printerr("MEDIA_IOC_SETUP_LINK");
		return false;
	}

	return true;
}

const struct media_v2_entity *
mp_device_find_entity(const MPDevice *device, const char *driver_name)
{
	int length = strlen(driver_name);

	// Find the entity from the name
	for (uint32_t i = 0; i < device->num_entities; ++i) {
		if (strncmp(device->entities[i].name, driver_name, length) == 0) {
			return &device->entities[i];
		}
	}
	return NULL;
}

const struct media_v2_entity *
mp_device_find_entity_type(const MPDevice *device, const uint32_t type)
{
	// Find the entity from the entity type
	for (uint32_t i = 0; i < device->num_entities; ++i) {
		if (device->entities[i].function == type) {
			return &device->entities[i];
		}
	}
	return NULL;
}

const struct media_device_info *
mp_device_get_info(const MPDevice *device)
{
	return &device->info;
}

const struct media_v2_entity *
mp_device_get_entity(const MPDevice *device, uint32_t id)
{
	for (int i = 0; i < device->num_entities; ++i) {
		if (device->entities[i].id == id) {
			return &device->entities[i];
		}
	}
	return NULL;
}

const struct media_v2_entity *
mp_device_get_entities(const MPDevice *device)
{
	return device->entities;
}

size_t
mp_device_get_num_entities(const MPDevice *device)
{
	return device->num_entities;
}

const struct media_v2_interface *
mp_device_find_entity_interface(const MPDevice *device, uint32_t entity_id)
{
	// Find the interface through the link
	const struct media_v2_link *link = mp_device_find_link_to(device, entity_id);
	if (!link) {
		return NULL;
	}
	return mp_device_get_interface(device, link->source_id);
}

const struct media_v2_interface *
mp_device_get_interface(const MPDevice *device, uint32_t id)
{
	for (int i = 0; i < device->num_interfaces; ++i) {
		if (device->interfaces[i].id == id) {
			return &device->interfaces[i];
		}
	}
	return NULL;
}

const struct media_v2_interface *
mp_device_get_interfaces(const MPDevice *device)
{
	return device->interfaces;
}

size_t
mp_device_get_num_interfaces(const MPDevice *device)
{
	return device->num_interfaces;
}

const struct media_v2_pad *
mp_device_get_pad_from_entity(const MPDevice *device, uint32_t entity_id)
{
	for (int i = 0; i < device->num_pads; ++i) {
		if (device->pads[i].entity_id == entity_id) {
			return &device->pads[i];
		}
	}
	return NULL;
}

const struct media_v2_pad *
mp_device_get_pad(const MPDevice *device, uint32_t id)
{
	for (int i = 0; i < device->num_pads; ++i) {
		if (device->pads[i].id == id) {
			return &device->pads[i];
		}
	}
	return NULL;
}

const struct media_v2_pad *
mp_device_get_pads(const MPDevice *device)
{
	return device->pads;
}

size_t
mp_device_get_num_pads(const MPDevice *device)
{
	return device->num_pads;
}

const struct media_v2_link *
mp_device_find_entity_link(const MPDevice *device, uint32_t entity_id)
{
	const struct media_v2_pad *pad =
		mp_device_get_pad_from_entity(device, entity_id);
	const struct media_v2_link *link = mp_device_find_link_to(device, pad->id);
	if (link) {
		return link;
	}
	return mp_device_find_link_from(device, pad->id);
}

const struct media_v2_link *
mp_device_find_link_from(const MPDevice *device, uint32_t source)
{
	for (int i = 0; i < device->num_links; ++i) {
		if (device->links[i].source_id == source) {
			return &device->links[i];
		}
	}
	return NULL;
}

const struct media_v2_link *
mp_device_find_link_to(const MPDevice *device, uint32_t sink)
{
	for (int i = 0; i < device->num_links; ++i) {
		if (device->links[i].sink_id == sink) {
			return &device->links[i];
		}
	}
	return NULL;
}

const struct media_v2_link *
mp_device_find_link_between(const MPDevice *device, uint32_t source, uint32_t sink)
{
	for (int i = 0; i < device->num_links; ++i) {
		if (device->links[i].source_id == source &&
		    device->links[i].sink_id == sink) {
			return &device->links[i];
		}
	}
	return NULL;
}

const struct media_v2_link *
mp_device_get_link(const MPDevice *device, uint32_t id)
{
	for (int i = 0; i < device->num_links; ++i) {
		if (device->links[i].id == id) {
			return &device->links[i];
		}
	}
	return NULL;
}

const struct media_v2_link *
mp_device_get_links(const MPDevice *device)
{
	return device->links;
}

size_t
mp_device_get_num_links(const MPDevice *device)
{
	return device->num_links;
}

struct _MPDeviceList {
	MPDevice *device;
	MPDeviceList *next;
};

MPDeviceList *
mp_device_list_new()
{
	MPDeviceList *current = NULL;

	// Enumerate media device files
	struct dirent *dir;
	DIR *d = opendir("/dev");
	while ((dir = readdir(d)) != NULL) {
		if (strncmp(dir->d_name, "media", 5) == 0) {
			char path[261];
			snprintf(path, 261, "/dev/%s", dir->d_name);

			MPDevice *device = mp_device_open(path);

			if (device) {
				MPDeviceList *next = malloc(sizeof(MPDeviceList));
				next->device = device;
				next->next = current;
				current = next;
			}
		}
	}
	closedir(d);

	return current;
}

void
mp_device_list_free(MPDeviceList *device_list)
{
	while (device_list) {
		MPDeviceList *tmp = device_list;
		device_list = tmp->next;

		mp_device_close(tmp->device);
		free(tmp);
	}
}

MPDevice *
mp_device_list_find_remove(MPDeviceList **list, const char *driver_name)
{
	MPDevice *found_device = NULL;
	int length = strlen(driver_name);

	while (*list) {
		MPDevice *device = mp_device_list_get(*list);
		const struct media_device_info *info = mp_device_get_info(device);

		if (strncmp(info->driver, driver_name, length) == 0) {
			found_device = mp_device_list_remove(list);
			break;
		}

		list = &(*list)->next;
	}

	return found_device;
}

MPDevice *
mp_device_list_remove(MPDeviceList **device_list)
{
	MPDevice *device = (*device_list)->device;

	if ((*device_list)->next) {
		MPDeviceList *tmp = (*device_list)->next;
		**device_list = *tmp;
		free(tmp);
	} else {
		free(*device_list);
		*device_list = NULL;
	}

	return device;
}

MPDevice *
mp_device_list_get(const MPDeviceList *device_list)
{
	return device_list->device;
}

MPDeviceList *
mp_device_list_next(const MPDeviceList *device_list)
{
	return device_list->next;
}
