#include <stdio.h>
#include <libconfig.h>
#include <malloc.h>
#include <string.h>
#include <dirent.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <errno.h>
#include <linux/media.h>
#include <stdint.h>
#include <limits.h>
#include <linux/videodev2.h>
#include "libmegapixels.h"
#include "mode.h"
#include "util.h"
#include "log.h"
#include "flash.h"

char *
find_path_for_devnode(struct media_v2_intf_devnode devnode)
{
	char uevent_path[PATH_MAX] = "";
	snprintf(uevent_path, PATH_MAX, "/sys/dev/char/%d:%d/uevent", devnode.major, devnode.minor);

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

	char line[PATH_MAX];
	char path[PATH_MAX];
	while (fgets(line, PATH_MAX, fp)) {
		if (strncmp(line, "DEVNAME=", 8) == 0) {
			// Drop newline
			unsigned long length = strlen(line);
			if (line[length - 1] == '\n')
				line[length - 1] = '\0';

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

int
find_media_node(libmegapixels_camera *camera, const char *media_name, const char *sensor_name)
{
	struct dirent *dir;
	DIR *d = opendir("/dev");
	while ((dir = readdir(d)) != NULL) {
		if (strncmp(dir->d_name, "media", 5) == 0) {
			char path[PATH_MAX];
			snprintf(path, PATH_MAX, "/dev/%s", dir->d_name);

			int media_fd = open(path, O_RDWR);
			if (media_fd == -1) {
				fprintf(stderr, "Could not open %s\n", path);
				continue;
			}

			struct media_device_info mdi;
			if (xioctl(media_fd, MEDIA_IOC_DEVICE_INFO, &mdi) == -1) {
				fprintf(stderr, "Could not MDI\n");
				close(media_fd);
			}
			if (strcmp(mdi.driver, media_name) != 0 && strcmp(mdi.model, media_name) != 0) {
				close(media_fd);
				continue;
			}

			// This media device matches on model or driver, scan the entities for the sensor
			struct media_v2_topology topology = {0};
			if (xioctl(media_fd, MEDIA_IOC_G_TOPOLOGY, &topology) == -1 ||
				topology.num_entities == 0) {
				close(media_fd);
				continue;
			}

			struct media_v2_entity *entities = calloc(topology.num_entities, sizeof(struct media_v2_entity));
			struct media_v2_interface *interfaces = calloc(topology.num_interfaces, sizeof(struct media_v2_interface));
			struct media_v2_pad *pads = calloc(topology.num_pads, sizeof(struct media_v2_pad));
			struct media_v2_link *links = calloc(topology.num_links, sizeof(struct media_v2_link));

			topology.ptr_entities = (uint64_t) entities;
			topology.ptr_interfaces = (uint64_t) interfaces;
			topology.ptr_pads = (uint64_t) pads;
			topology.ptr_links = (uint64_t) links;

			if (xioctl(media_fd, MEDIA_IOC_G_TOPOLOGY, &topology) == -1) {
				close(media_fd);
				continue;
			}

			// Find the sensor
			unsigned long len = strlen(sensor_name);
			int found = 0;
			for (int i = 0; i < topology.num_entities; i++) {
				if (strncmp(entities[i].name, sensor_name, len) == 0) {
					found++;
					for (int j = 0; j < topology.num_links; j++) {
						if (links[j].sink_id != entities[i].id) {
							continue;
						}
						for (int k = 0; k < topology.num_interfaces; k++) {
							if (interfaces[k].id != links[j].source_id) {
								continue;
							}

							camera->sensor_path = find_path_for_devnode(interfaces[k].devnode);
							break;
						}
					}
					break;
				}
			}

			// Find the flash
			for (int i = 0; i < topology.num_entities; i++) {
				if (entities[i].function == MEDIA_ENT_F_FLASH) {
					for (int j = 0; j < topology.num_links; j++) {
						if (links[j].sink_id != entities[i].id) {
							continue;
						}
						for (int k = 0; k < topology.num_interfaces; k++) {
							if (interfaces[k].id != links[j].source_id) {
								continue;
							}

							camera->flash_path = find_path_for_devnode(interfaces[k].devnode);
							camera->flash_type = LIBMEGAPIXELS_FLASH_V4L;
							break;
						}
					}
					break;
				}
			}


			// Find the lens actuator
			for (int i = 0; i < topology.num_entities; i++) {
				if (entities[i].function == MEDIA_ENT_F_LENS) {
					for (int j = 0; j < topology.num_links; j++) {
						if (links[j].sink_id != entities[i].id) {
							continue;
						}
						for (int k = 0; k < topology.num_interfaces; k++) {
							if (interfaces[k].id != links[j].source_id) {
								continue;
							}

							camera->lens_path = find_path_for_devnode(interfaces[k].devnode);
							break;
						}
					}
					break;
				}
			}


			// Find the bridge
			for (int i = 0; i < topology.num_entities; i++) {
				if (entities[i].function == MEDIA_ENT_F_IO_V4L) {
					found++;
					for (int j = 0; j < topology.num_links; j++) {
						if (links[j].sink_id != entities[i].id) {
							continue;
						}
						for (int k = 0; k < topology.num_interfaces; k++) {
							if (interfaces[k].id != links[j].source_id) {
								continue;
							}

							camera->video_path = find_path_for_devnode(interfaces[k].devnode);
							break;
						}
					}
					break;
				}
			}
			camera->num_handles = 0;
			for (int i = 0; i < topology.num_links; i++) {
				if (!(links[i].flags & MEDIA_LNK_FL_INTERFACE_LINK)) {
					continue;
				}
				camera->num_handles++;
			}

			camera->handles = calloc(camera->num_handles, sizeof(libmegapixels_subdev));
			int h = 0;
			for (int i = 0; i < topology.num_links; i++) {
				if (!(links[i].flags & MEDIA_LNK_FL_INTERFACE_LINK)) {
					continue;
				}
				libmegapixels_subdev *sd;
				sd = malloc(sizeof(libmegapixels_subdev));
				camera->handles[h] = sd;
				camera->handles[h]->entity_id = links[i].sink_id;
				camera->handles[h]->fd = 0;

				for (int j = 0; j < topology.num_interfaces; j++) {
					if (links[i].source_id != interfaces[j].id) {
						continue;
					}
					camera->handles[h]->path = find_path_for_devnode(interfaces[j].devnode);
				}
				h++;
			}

			close(media_fd);
			if (found == 2) {
				camera->media_path = strdup(path);
				return 1;
			}
		}

	}
	closedir(d);
	return -1;
}

int
load_camera(libmegapixels_devconfig *config, config_t *cfg, const char *name, int lint)
{
	const char *sensor_driver, *bridge_driver, *flashpath;
	config_setting_t *root = config_lookup(cfg, name);
	log_debug("Loading camera '%s'\n", name);
	if (!config_setting_lookup_string(root, "SensorDriver", &sensor_driver)) {
		log_debug("  Section is missing SensorDriver\n");
		return -1;
	}
	if (!config_setting_lookup_string(root, "BridgeDriver", &bridge_driver)) {
		log_debug("  Section is missing BridgeDriver\n");
		return -1;
	}
	libmegapixels_camera *camera;
	camera = malloc(sizeof(libmegapixels_camera));
	camera->sensor_fd = 0;
	camera->media_fd = 0;
	camera->video_fd = 0;
	camera->flash_fd = 0;
	camera->lens_fd = 0;
	camera->flash_path = NULL;
	camera->lens_path = NULL;
	camera->flash_type = LIBMEGAPIXELS_FLASH_SCREEN;

	// Don't access hardware in linting mode
	if (lint == 0) {
		int res = find_media_node(camera, bridge_driver, sensor_driver);
		if (res < 0) {
			log_debug("  Could not find media node with this sensor\n");
			free(camera);
			return -1;
		}
	}

	camera->name = strdup(name);
	camera->sensor_name = strdup(sensor_driver);
	camera->bridge_name = strdup(bridge_driver);
	config->cameras = realloc(config->cameras, (config->count + 1) * sizeof(config->cameras));
	if (config->cameras == NULL) {
		return -1;
	}
	camera->index = config->count;
	config->cameras[config->count++] = camera;

	if (config_setting_lookup_string(root, "FlashPath", &flashpath)) {
		camera->flash_type = LIBMEGAPIXELS_FLASH_LED;
		libmegapixels_flash_cleanup_flash_path(camera);
		camera->flash_path = libmegapixels_flash_get_led_path(flashpath);

		if (camera->flash_path == NULL) {
			log_error("Failed to get LED flash path\n");
			return -1;
		}
	}

	// You do not need to set FlashDisplay in device config files, unless for some reason there's a V4L flash device that isn't meant to be there
	int flashDisplay = 0;
	if (config_setting_lookup_bool(root, "FlashDisplay", &flashDisplay)) {
		if (flashDisplay) {
			camera->flash_type = LIBMEGAPIXELS_FLASH_SCREEN;
			libmegapixels_flash_cleanup_flash_path(camera);
		}
	}

	config_setting_t *modes = config_setting_lookup(root, "Modes");
	config_setting_t *mode;

	if (modes == NULL) {
		log_error("Sensor '%s' does not define any modes\n", camera->name);
		return -1;
	}

	int num_modes = config_setting_length(modes);
	camera->modes = malloc(num_modes * sizeof(libmegapixels_mode *));
	camera->num_modes = num_modes;
	int n = 0;
	while (1) {
		mode = config_setting_get_elem(modes, n);
		if (mode == NULL) {
			break;
		}
		libmegapixels_mode *mm = malloc(sizeof(libmegapixels_mode));
		camera->modes[n] = mm;
		if (!config_setting_lookup_int(mode, "Width", &mm->width)) {
			log_error("Missing Width\n");
			return -1;
		}
		if (!config_setting_lookup_int(mode, "Height", &mm->height)) {
			log_error("Missing Height\n");
			return -1;
		}
		if (!config_setting_lookup_int(mode, "Rate", &mm->rate)) {
			log_error("Missing Rate\n");
			return -1;
		}

		const char *fmt;
		config_setting_lookup_string(mode, "Format", &fmt);
		mm->format = libmegapixels_format_name_to_index(fmt);
		if (!mm->format) {
			log_error("Unknown format '%s'\n", fmt);
			return -1;
		}
		mm->v4l_pixfmt = libmegapixels_format_to_v4l_pixfmt(mm->format);
		mm->media_busfmt = libmegapixels_format_to_media_busfmt(mm->format);

		const char *xfer;
		if (config_setting_lookup_string(mode, "Transfer", &xfer)) {
			if (strcmp(xfer, "srgb") == 0) {
				mm->xfer = LIBMEGAPIXELS_XFER_SRGB;
			} else {
				mm->xfer = LIBMEGAPIXELS_XFER_RAW;
			}
		} else {
			mm->xfer = LIBMEGAPIXELS_XFER_RAW;
		}

		if (!config_setting_lookup_int(mode, "Rotate", &mm->rotation)) {
			mm->rotation = 0;
		}
		if (!config_setting_lookup_bool(mode, "Mirror", &mm->mirrored)) {
			mm->mirrored = 0;
		}

		if (!config_setting_lookup_float(mode, "FocalLength", &mm->focal_length)) {
			mm->focal_length = 0.0f;
		}
		if (!config_setting_lookup_float(mode, "FNumber", &mm->f_number)) {
			mm->f_number = 0.0f;
		}
		char modename[32] = {0};
		mode_snprintf(modename, 31, mm);
		log_debug("  Adding mode [%s]\n", modename);

		config_setting_t *cmds = config_setting_lookup(mode, "Pipeline");
		config_setting_t *cmd;

		if (cmds == NULL) {
			log_error("Mode has no pipeline\n");
			n++;
			continue;
		}

		int num_cmds = config_setting_length(cmds);
		mm->cmds = (libmegapixels_cmd **) calloc(num_cmds, sizeof(libmegapixels_cmd *));
		mm->num_cmds = num_cmds;
		int m = 0;
		int last_width = camera->modes[n]->width;
		int last_height = camera->modes[n]->height;
		int last_format = camera->modes[n]->format;

		while (1) {
			cmd = config_setting_get_elem(cmds, m);
			if (cmd == NULL) {
				break;
			}
			libmegapixels_cmd *cur = calloc(1, sizeof(libmegapixels_cmd));
			mm->cmds[m] = cur;
			const char *type;
			if (!config_setting_lookup_string(cmd, "Type", &type)) {
				break;
			}
			if (strcmp(type, "Link") == 0) {
				camera->modes[n]->cmds[m]->type = LIBMEGAPIXELS_CMD_LINK;
				if (!config_setting_lookup_string(cmd, "From", &cur->entity_from)) {
					fprintf(stderr, "Missing From\n");
					break;
				}
				if (!config_setting_lookup_string(cmd, "To", &cur->entity_to)) {
					fprintf(stderr, "Missing To\n");
					break;
				}
				if (!config_setting_lookup_int(cmd, "FromPad", &cur->pad_from)) {
					cur->pad_from = 0;
				}
				if (!config_setting_lookup_int(cmd, "ToPad", &cur->pad_to)) {
					cur->pad_to = 0;
				}
				if (!config_setting_lookup_bool(cmd, "ExactName", &cur->exact_name)) {
					cur->exact_name = 0;
				}
			} else if (strcmp(type, "Mode") == 0) {
				camera->modes[n]->cmds[m]->type = LIBMEGAPIXELS_CMD_MODE;
				if (!config_setting_lookup_string(cmd, "Entity", &cur->entity_from)) {
					fprintf(stderr, "Missing entity\n");
					break;
				}
				if (!config_setting_lookup_int(cmd, "Width", &cur->width)) {
					cur->width = last_width;
				}
				last_width = cur->width;
				if (!config_setting_lookup_int(cmd, "Height", &cur->height)) {
					cur->height = last_height;
				}
				last_height = cur->height;
				if (!config_setting_lookup_int(cmd, "Pad", &cur->pad_from)) {
					cur->pad_from = 0;
				}
				const char *newfmt;
				if (config_setting_lookup_string(cmd, "Format", &newfmt)) {
					cur->format = libmegapixels_format_name_to_index(newfmt);
					last_format = cur->format;
				} else {
					cur->format = last_format;
				}
				if (!config_setting_lookup_bool(cmd, "SkipTry", &cur->skip_try)) {
					cur->skip_try = 0;
				}
				if (!config_setting_lookup_bool(cmd, "ExactName", &cur->exact_name)) {
					cur->exact_name = 0;
				}
			} else if (strcmp(type, "Rate") == 0) {
				camera->modes[n]->cmds[m]->type = LIBMEGAPIXELS_CMD_INTERVAL;
				if (!config_setting_lookup_string(cmd, "Entity", &cur->entity_from)) {
					fprintf(stderr, "Missing entity\n");
					break;
				}
				if (!config_setting_lookup_int(cmd, "Rate", &cur->rate)) {
					cur->rate = mm->rate;
				}
				if (!config_setting_lookup_bool(cmd, "SkipTry", &cur->skip_try)) {
					cur->skip_try = 0;
				}
				if (!config_setting_lookup_bool(cmd, "ExactName", &cur->exact_name)) {
					cur->exact_name = 0;
				}
			} else if (strcmp(type, "Crop") == 0) {
				camera->modes[n]->cmds[m]->type = LIBMEGAPIXELS_CMD_CROP;
				if (!config_setting_lookup_string(cmd, "Entity", &cur->entity_from)) {
					fprintf(stderr, "Missing entity\n");
					break;
				}
				if (!config_setting_lookup_int(cmd, "Width", &cur->width)) {
					cur->width = camera->modes[n]->width;
				}
				last_width = cur->width;
				if (!config_setting_lookup_int(cmd, "Height", &cur->height)) {
					cur->height = camera->modes[n]->height;
				}
				last_height = cur->height;
				if (!config_setting_lookup_int(cmd, "Top", &cur->top)) {
					cur->top = 0;
				}
				if (!config_setting_lookup_int(cmd, "Left", &cur->left)) {
					cur->left = 0;
				}
				if (!config_setting_lookup_int(cmd, "Pad", &cur->pad_from)) {
					cur->pad_from = 0;
				}
				if (!config_setting_lookup_bool(cmd, "SkipTry", &cur->skip_try)) {
					cur->skip_try = 0;
				}
				if (!config_setting_lookup_bool(cmd, "ExactName", &cur->exact_name)) {
					cur->exact_name = 0;
				}
			}
			m++;
		}
		n++;
	}

	if (n == 0) {
		log_debug("No modes defined for sensor '%s'\n", camera->name);
		return -1;
	}

	return 0;
}

int
libmegapixels_load_file(libmegapixels_devconfig *config, const char *file)
{
	return libmegapixels_load_file_lint(config, file, 0);
}

int
libmegapixels_load_file_lint(libmegapixels_devconfig *config, const char *file, int linting)
{
	if (config->loaded_config) {
		log_error("Config already loaded\n");
		return 0;
	}
	log_debug("Load config file %s\n", file);
	config->loaded_config = 1;
	config_t cfg;
	config_setting_t *setting, *member;

	config->path = strdup(file);
	if (config->count == 0) {
		config->cameras = malloc(0);
	}

	config_init(&cfg);
	if (!config_read_file(&cfg, file)) {
		if (config_error_type(&cfg) == CONFIG_ERR_FILE_IO) {
			fprintf(stderr, "Could not read %s: %s\n", file, config_error_text(&cfg));
		} else {
			fprintf(stderr, "Could not read %s:\n", file);
			fprintf(stderr, "\t%s:%d - %s\n",
				config_error_file(&cfg), config_error_line(&cfg), config_error_text(&cfg));
		}
		config_destroy(&cfg);
		return 0;
	}

	int version = 0;
	if (config_lookup_int(&cfg, "Version", &version)) {
		if (version != 1) {
			fprintf(stderr, "Could not load '%s': Invalid version %d\n", file, version);
			return 0;
		}
	} else {
		fprintf(stderr, "Could not load '%s': Missing version\n", file);
		return 0;
	}

	if (!config_lookup_string(&cfg, "Make", &config->make)) {
		config->make = strdup("Megapixels");
	}
	if (!config_lookup_string(&cfg, "Model", &config->model)) {
		config->model = strdup("Camera");
	}

	setting = config_root_setting(&cfg);
	int n = 0;

	while (1) {
		member = config_setting_get_elem(setting, n++);
		if (member == NULL) {
			break;
		}

		if (strcmp(member->name, "Version") == 0) {
			continue;
		}
		if (strcmp(member->name, "Make") == 0) {
			continue;
		}
		if (strcmp(member->name, "Model") == 0) {
			continue;
		}
		load_camera(config, &cfg, member->name, linting);
	}

	return 1;
}

void
uvc_create_modes(libmegapixels_camera *camera, int fd)
{
	struct v4l2_fmtdesc fmtdesc = {0};
	fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
	while (xioctl(fd, VIDIOC_ENUM_FMT, &fmtdesc) == 0) {
		fmtdesc.index++;
		libmegapixels_v4l_pixfmt_to_index(fmtdesc.pixelformat);
		int format = libmegapixels_v4l_pixfmt_to_index(fmtdesc.pixelformat);
		if (!format) {
			// fprintf(stderr, "Unsupported format: %s\n", pixfmt_to_str(fmtdesc.pixelformat));
			continue;
		}

		struct v4l2_frmsizeenum framesize = {0};
		framesize.pixel_format = fmtdesc.pixelformat;

		while (xioctl(fd, VIDIOC_ENUM_FRAMESIZES, &framesize) == 0) {
			if (framesize.type == V4L2_FRMSIZE_TYPE_DISCRETE) {

				struct v4l2_frmivalenum frameinterval = {0};
				frameinterval.pixel_format = framesize.pixel_format;
				frameinterval.width = framesize.discrete.width;
				frameinterval.height = framesize.discrete.height;

				while (xioctl(fd, VIDIOC_ENUM_FRAMEINTERVALS, &frameinterval) == 0) {
					frameinterval.index++;
					if (frameinterval.type == V4L2_FRMIVAL_TYPE_DISCRETE) {
						libmegapixels_mode *mode = calloc(1, sizeof(libmegapixels_mode));
						mode->format = format;
						mode->v4l_pixfmt = fmtdesc.pixelformat;
						mode->width = (int) framesize.discrete.width;
						mode->height = (int) framesize.discrete.height;
						mode->media_busfmt = libmegapixels_format_to_media_busfmt(format);
						mode->rotation = 0;
						mode->rate = (int) ((double) frameinterval.discrete.denominator /
							(double) frameinterval.discrete.numerator);

						camera->modes = realloc(camera->modes, (camera->num_modes + 1) * sizeof(camera->modes));
						if (camera->modes == NULL) {
							return;
						}
						camera->modes[camera->num_modes++] = mode;
					}
				}
			}
			framesize.index++;
		}
	}
}

int
libmegapixels_load_uvc(libmegapixels_devconfig *config)
{
	if (config->loaded_uvc) {
		log_error("libmegapixels_load_uvc was already called\n");
		return 0;
	}
	log_debug("Loading UVC cameras\n");
	config->loaded_uvc = 1;

	struct dirent *dir;
	DIR *d = opendir("/dev");
	while ((dir = readdir(d)) != NULL) {
		if (strncmp(dir->d_name, "video", 5) == 0) {
			char path[PATH_MAX];
			snprintf(path, PATH_MAX, "/dev/%s", dir->d_name);
			int fd = open(path, O_RDWR);
			if (fd < 0) {
				continue;
			}
			struct v4l2_capability vid_cap = {0};
			if (xioctl(fd, VIDIOC_QUERYCAP, &vid_cap) == -1) {
				perror("QUERYCAP");
				continue;
			}
			if (strcmp((char *) vid_cap.driver, "uvcvideo") != 0) {
				continue;
			}

			if (!(vid_cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) {
				continue;
			}

			if (!(vid_cap.capabilities & V4L2_CAP_STREAMING)) {
				continue;
			}

			struct v4l2_fmtdesc fmtdesc = {0};
			fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
			if (xioctl(fd, VIDIOC_ENUM_FMT, &fmtdesc) != 0) {
				continue;
			}

			libmegapixels_camera *camera;
			camera = calloc(1, sizeof(libmegapixels_camera));
			camera->name = strdup((const char *) vid_cap.card);
			camera->video_path = strdup(path);

			uvc_create_modes(camera, fd);

			close(fd);

			config->cameras = realloc(config->cameras, (config->count + 1) * sizeof(config->cameras));
			if (config->cameras == NULL) {
				return -1;
			}
			camera->index = config->count;
			config->cameras[config->count++] = camera;
		}
	}

	closedir(d);
	return 1;
}

int
libmegapixels_init(libmegapixels_devconfig **config)
{
	*config = calloc(1, sizeof(libmegapixels_devconfig));
	init_log(0);
	return 1;
}
