// SPDX-License-Identifier: GPL-2.0-only
/*
 * GPIO chardev test helper
 *
 * Copyright (C) 2016 Bamvor Jian Zhang
 */

#define _GNU_SOURCE
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <getopt.h>
#include <sys/ioctl.h>
#include <libmount.h>
#include <err.h>
#include <dirent.h>
#include <linux/gpio.h>
#include "../../../gpio/gpio-utils.h"

#define CONSUMER	"gpio-selftest"
#define	GC_NUM		10
enum direction {
	OUT,
	IN
};

static int get_debugfs(char **path)
{
	struct libmnt_context *cxt;
	struct libmnt_table *tb;
	struct libmnt_iter *itr = NULL;
	struct libmnt_fs *fs;
	int found = 0, ret;

	cxt = mnt_new_context();
	if (!cxt)
		err(EXIT_FAILURE, "libmount context allocation failed");

	itr = mnt_new_iter(MNT_ITER_FORWARD);
	if (!itr)
		err(EXIT_FAILURE, "failed to initialize libmount iterator");

	if (mnt_context_get_mtab(cxt, &tb))
		err(EXIT_FAILURE, "failed to read mtab");

	while (mnt_table_next_fs(tb, itr, &fs) == 0) {
		const char *type = mnt_fs_get_fstype(fs);

		if (!strcmp(type, "debugfs")) {
			found = 1;
			break;
		}
	}
	if (found) {
		ret = asprintf(path, "%s/gpio", mnt_fs_get_target(fs));
		if (ret < 0)
			err(EXIT_FAILURE, "failed to format string");
	}

	mnt_free_iter(itr);
	mnt_free_context(cxt);

	if (!found)
		return -1;

	return 0;
}

static int gpio_debugfs_get(const char *consumer, int *dir, int *value)
{
	char *debugfs;
	FILE *f;
	char *line = NULL;
	size_t len = 0;
	char *cur;
	int found = 0;

	if (get_debugfs(&debugfs) != 0)
		err(EXIT_FAILURE, "debugfs is not mounted");

	f = fopen(debugfs, "r");
	if (!f)
		err(EXIT_FAILURE, "read from gpio debugfs failed");

	/*
	 * gpio-2   (                    |gpio-selftest               ) in  lo
	 */
	while (getline(&line, &len, f) != -1) {
		cur = strstr(line, consumer);
		if (cur == NULL)
			continue;

		cur = strchr(line, ')');
		if (!cur)
			continue;

		cur += 2;
		if (!strncmp(cur, "out", 3)) {
			*dir = OUT;
			cur += 4;
		} else if (!strncmp(cur, "in", 2)) {
			*dir = IN;
			cur += 4;
		}

		if (!strncmp(cur, "hi", 2))
			*value = 1;
		else if (!strncmp(cur, "lo", 2))
			*value = 0;

		found = 1;
		break;
	}
	free(debugfs);
	fclose(f);
	free(line);

	if (!found)
		return -1;

	return 0;
}

static struct gpiochip_info *list_gpiochip(const char *gpiochip_name, int *ret)
{
	struct gpiochip_info *cinfo;
	struct gpiochip_info *current;
	const struct dirent *ent;
	DIR *dp;
	char *chrdev_name;
	int fd;
	int i = 0;

	cinfo = calloc(sizeof(struct gpiochip_info) * 4, GC_NUM + 1);
	if (!cinfo)
		err(EXIT_FAILURE, "gpiochip_info allocation failed");

	current = cinfo;
	dp = opendir("/dev");
	if (!dp) {
		*ret = -errno;
		goto error_out;
	} else {
		*ret = 0;
	}

	while (ent = readdir(dp), ent) {
		if (check_prefix(ent->d_name, "gpiochip")) {
			*ret = asprintf(&chrdev_name, "/dev/%s", ent->d_name);
			if (*ret < 0)
				goto error_out;

			fd = open(chrdev_name, 0);
			if (fd == -1) {
				*ret = -errno;
				fprintf(stderr, "Failed to open %s\n",
					chrdev_name);
				goto error_close_dir;
			}
			*ret = ioctl(fd, GPIO_GET_CHIPINFO_IOCTL, current);
			if (*ret == -1) {
				perror("Failed to issue CHIPINFO IOCTL\n");
				goto error_close_dir;
			}
			close(fd);
			if (strcmp(current->label, gpiochip_name) == 0
			    || check_prefix(current->label, gpiochip_name)) {
				*ret = 0;
				current++;
				i++;
			}
		}
	}

	if ((!*ret && i == 0) || *ret < 0) {
		free(cinfo);
		cinfo = NULL;
	}
	if (!*ret && i > 0) {
		cinfo = realloc(cinfo, sizeof(struct gpiochip_info) * 4 * i);
		*ret = i;
	}

error_close_dir:
	closedir(dp);
error_out:
	if (*ret < 0)
		err(EXIT_FAILURE, "list gpiochip failed: %s", strerror(*ret));

	return cinfo;
}

int gpio_pin_test(struct gpiochip_info *cinfo, int line, int flag, int value)
{
	struct gpiohandle_data data;
	unsigned int lines[] = {line};
	int fd;
	int debugfs_dir = IN;
	int debugfs_value = 0;
	int ret;

	data.values[0] = value;
	ret = gpiotools_request_linehandle(cinfo->name, lines, 1, flag, &data,
					   CONSUMER);
	if (ret < 0)
		goto fail_out;
	else
		fd = ret;

	ret = gpio_debugfs_get(CONSUMER, &debugfs_dir, &debugfs_value);
	if (ret) {
		ret = -EINVAL;
		goto fail_out;
	}
	if (flag & GPIOHANDLE_REQUEST_INPUT) {
		if (debugfs_dir != IN) {
			errno = -EINVAL;
			ret = -errno;
		}
	} else if (flag & GPIOHANDLE_REQUEST_OUTPUT) {
		if (flag & GPIOHANDLE_REQUEST_ACTIVE_LOW)
			debugfs_value = !debugfs_value;

		if (!(debugfs_dir == OUT && value == debugfs_value)) {
			errno = -EINVAL;
			ret = -errno;
		}
	}
	gpiotools_release_linehandle(fd);

fail_out:
	if (ret)
		err(EXIT_FAILURE, "gpio<%s> line<%d> test flag<0x%x> value<%d>",
		    cinfo->name, line, flag, value);

	return ret;
}

void gpio_pin_tests(struct gpiochip_info *cinfo, unsigned int line)
{
	printf("line<%d>", line);
	gpio_pin_test(cinfo, line, GPIOHANDLE_REQUEST_OUTPUT, 0);
	printf(".");
	gpio_pin_test(cinfo, line, GPIOHANDLE_REQUEST_OUTPUT, 1);
	printf(".");
	gpio_pin_test(cinfo, line,
		      GPIOHANDLE_REQUEST_OUTPUT | GPIOHANDLE_REQUEST_ACTIVE_LOW,
		      0);
	printf(".");
	gpio_pin_test(cinfo, line,
		      GPIOHANDLE_REQUEST_OUTPUT | GPIOHANDLE_REQUEST_ACTIVE_LOW,
		      1);
	printf(".");
	gpio_pin_test(cinfo, line, GPIOHANDLE_REQUEST_INPUT, 0);
	printf(".");
}

/*
 * ./gpio-mockup-chardev gpio_chip_name_prefix is_valid_gpio_chip
 * Return 0 if successful or exit with EXIT_FAILURE if test failed.
 * gpio_chip_name_prefix: The prefix of gpiochip you want to test. E.g.
 *			  gpio-mockup
 * is_valid_gpio_chip:	  Whether the gpio_chip is valid. 1 means valid,
 *			  0 means invalid which could not be found by
 *			  list_gpiochip.
 */
int main(int argc, char *argv[])
{
	char *prefix;
	int valid;
	struct gpiochip_info *cinfo;
	struct gpiochip_info *current;
	int i;
	int ret;

	if (argc < 3) {
		printf("Usage: %s prefix is_valid", argv[0]);
		exit(EXIT_FAILURE);
	}

	prefix = argv[1];
	valid = strcmp(argv[2], "true") == 0 ? 1 : 0;

	printf("Test gpiochip %s: ", prefix);
	cinfo = list_gpiochip(prefix, &ret);
	if (!cinfo) {
		if (!valid && ret == 0) {
			printf("Invalid test successful\n");
			ret = 0;
			goto out;
		} else {
			ret = -EINVAL;
			goto out;
		}
	} else if (cinfo && !valid) {
		ret = -EINVAL;
		goto out;
	}
	current = cinfo;
	for (i = 0; i < ret; i++) {
		gpio_pin_tests(current, 0);
		gpio_pin_tests(current, current->lines - 1);
		gpio_pin_tests(current, random() % current->lines);
		current++;
	}
	ret = 0;
	printf("successful\n");

out:
	if (ret)
		fprintf(stderr, "gpio<%s> test failed\n", prefix);

	if (cinfo)
		free(cinfo);

	if (ret)
		exit(EXIT_FAILURE);

	return ret;
}
