/*
 * Embedded Linux library
 * Copyright (C) 2019  Geanix
 *
 * SPDX-License-Identifier: LGPL-2.1-or-later
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#define _GNU_SOURCE
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <dirent.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <linux/gpio.h>

#include "private.h"
#include "strv.h"
#include "useful.h"
#include "gpio.h"

struct l_gpio_chip {
	int fd;
	char *name;
	char *label;
	uint32_t n_lines;
};

struct l_gpio_writer {
	int fd;
	uint32_t n_offsets;
};

struct l_gpio_reader {
	int fd;
	uint32_t n_offsets;
};

static bool chip_has_line_label(const char *chip_name, const char *line_label)
{
	struct l_gpio_chip *chip;
	bool has_label;

	chip = l_gpio_chip_new(chip_name);
	if (!chip)
		return false;

	has_label = l_gpio_chip_find_line_offset(chip, line_label, NULL);

	l_gpio_chip_free(chip);

	return has_label;
}

LIB_EXPORT char **l_gpio_chips_with_line_label(const char *line_label)
{
	struct dirent *entry;
	DIR *dp;
	char **chips = NULL;

	dp = opendir("/sys/bus/gpio/devices");
	if (dp == NULL)
		return NULL;

	while ((entry = readdir(dp))) {
		if (entry->d_type != DT_LNK)
			continue;

		if (!l_str_has_prefix(entry->d_name, "gpiochip"))
			continue;

		if (chip_has_line_label(entry->d_name, line_label))
			chips = l_strv_append(chips, entry->d_name);
	}

	closedir(dp);

	return chips;
}

LIB_EXPORT struct l_gpio_chip *l_gpio_chip_new(const char *chip_name)
{
	struct l_gpio_chip *chip;
	struct gpiochip_info info;
	char *path;
	int fd;
	int ret;

	if (unlikely(!chip_name))
		return NULL;

	path = l_strdup_printf("/dev/%s", chip_name);
	fd = open(path, O_RDONLY | O_CLOEXEC);
	l_free(path);

	if (fd < 0)
		return NULL;

	memset(&info, 0, sizeof(info));

	ret = ioctl(fd, GPIO_GET_CHIPINFO_IOCTL, &info);
	if (ret < 0) {
		close(fd);
		return NULL;
	}

	chip = l_new(struct l_gpio_chip, 1);
	chip->fd = fd;
	chip->n_lines = info.lines;
	chip->label = l_strndup(info.label, sizeof(info.label));
	chip->name = l_strdup(chip_name);

	return chip;
}

LIB_EXPORT const char *l_gpio_chip_get_label(struct l_gpio_chip *chip)
{
	if (unlikely(!chip))
		return NULL;

	return chip->label;
}

LIB_EXPORT const char *l_gpio_chip_get_name(struct l_gpio_chip *chip)
{
	if (unlikely(!chip))
		return NULL;

	return chip->name;
}

LIB_EXPORT uint32_t l_gpio_chip_get_num_lines(struct l_gpio_chip *chip)
{
	if (unlikely(!chip))
		return 0;

	return chip->n_lines;
}

LIB_EXPORT void l_gpio_chip_free(struct l_gpio_chip *chip)
{
	if (unlikely(!chip))
		return;

	if (chip->fd >= 0)
		close(chip->fd);

	l_free(chip->name);
	l_free(chip->label);
	l_free(chip);
}

LIB_EXPORT bool l_gpio_chip_find_line_offset(struct l_gpio_chip *chip,
						const char *line_label,
						uint32_t *line_offset)
{
	struct gpioline_info info;
	uint32_t i;

	if (unlikely(!chip))
		return false;

	if (unlikely(!line_label))
		return false;

	for (i = 0; i < chip->n_lines; i++) {
		memset(&info, 0, sizeof(info));
		info.line_offset = i;

		if (ioctl(chip->fd, GPIO_GET_LINEINFO_IOCTL, &info) < 0)
			return false;

		if (strcmp(info.name, line_label) != 0)
			continue;

		if (line_offset)
			*line_offset = i;

		return true;
	}

	return false;
}

LIB_EXPORT char *l_gpio_chip_get_line_label(struct l_gpio_chip *chip,
						uint32_t offset)
{
	struct gpioline_info info;

	if (unlikely(!chip))
		return NULL;

	if (unlikely(offset >= chip->n_lines))
		return NULL;

	memset(&info, 0, sizeof(info));
	info.line_offset = offset;

	if (ioctl(chip->fd, GPIO_GET_LINEINFO_IOCTL, &info) < 0)
		return NULL;

	return l_strdup(info.name);
}

LIB_EXPORT char *l_gpio_chip_get_line_consumer(struct l_gpio_chip *chip,
						uint32_t offset)
{
	struct gpioline_info info;

	if (unlikely(!chip))
		return NULL;

	if (unlikely(offset >= chip->n_lines))
		return NULL;

	memset(&info, 0, sizeof(info));
	info.line_offset = offset;

	if (ioctl(chip->fd, GPIO_GET_LINEINFO_IOCTL, &info) < 0)
		return NULL;

	return l_strdup(info.consumer);
}

LIB_EXPORT struct l_gpio_writer *l_gpio_writer_new(struct l_gpio_chip *chip,
						const char *consumer,
						uint32_t n_offsets,
						const uint32_t offsets[],
						const uint32_t values[])
{
	struct l_gpio_writer *writer;
	struct gpiohandle_request request;
	uint32_t i;

	if (unlikely(!chip))
		return NULL;

	if (unlikely(n_offsets == 0 || n_offsets > GPIOHANDLES_MAX))
		return NULL;

	if (unlikely(!offsets))
		return NULL;

	memset(&request, 0, sizeof(request));
	l_strlcpy(request.consumer_label, consumer, 32);
	request.lines = n_offsets;
	request.flags = GPIOHANDLE_REQUEST_OUTPUT;

	for (i = 0; i < n_offsets; i++) {
		if (offsets[i] >= chip->n_lines)
			return NULL;

		request.lineoffsets[i] = offsets[i];
		request.default_values[i] = values[i];
	}

	if (ioctl(chip->fd, GPIO_GET_LINEHANDLE_IOCTL, &request) < 0)
		return NULL;

	if (request.fd <= 0)
		return NULL;

	writer = l_new(struct l_gpio_writer, 1);
	writer->fd = request.fd;
	writer->n_offsets = n_offsets;

	return writer;
}

LIB_EXPORT void l_gpio_writer_free(struct l_gpio_writer *writer)
{
	if (unlikely(!writer))
		return;

	if (writer->fd >= 0)
		close(writer->fd);

	l_free(writer);
}

LIB_EXPORT bool l_gpio_writer_set(struct l_gpio_writer *writer, uint32_t n_values,
					const uint32_t values[])
{
	struct gpiohandle_data data;
	uint32_t i;

	if (unlikely(!writer))
		return false;

	if (unlikely(!values))
		return false;

	if (unlikely(n_values != writer->n_offsets))
		return false;

	for (i = 0; i < n_values; i++)
		data.values[i] = values[i];

	if (ioctl(writer->fd, GPIOHANDLE_SET_LINE_VALUES_IOCTL, &data) < 0)
		return false;

	return true;
}

LIB_EXPORT struct l_gpio_reader *l_gpio_reader_new(struct l_gpio_chip *chip,
							const char *consumer,
							uint32_t n_offsets,
							const uint32_t offsets[])
{
	struct l_gpio_reader *reader;
	struct gpiohandle_request request;
	uint32_t i;

	if (unlikely(!chip))
		return NULL;

	if (unlikely(n_offsets == 0 || n_offsets > GPIOHANDLES_MAX))
		return NULL;

	if (unlikely(!offsets))
		return NULL;

	memset(&request, 0, sizeof(request));
	l_strlcpy(request.consumer_label, consumer, 32);
	request.lines = n_offsets;
	request.flags = GPIOHANDLE_REQUEST_INPUT;

	for (i = 0; i < n_offsets; i++) {
		if (offsets[i] >= chip->n_lines)
			return NULL;

		request.lineoffsets[i] = offsets[i];
	}

	if (ioctl(chip->fd, GPIO_GET_LINEHANDLE_IOCTL, &request) < 0)
		return NULL;

	if (request.fd <= 0)
		return NULL;

	reader = l_new(struct l_gpio_reader, 1);
	reader->fd = request.fd;
	reader->n_offsets = n_offsets;

	return reader;
}

LIB_EXPORT void l_gpio_reader_free(struct l_gpio_reader *reader)
{
	if (unlikely(!reader))
		return;

	if (reader->fd >= 0)
		close(reader->fd);

	l_free(reader);
}

LIB_EXPORT bool l_gpio_reader_get(struct l_gpio_reader *reader,
					uint32_t n_values, uint32_t values[])
{
	struct gpiohandle_data data;
	uint32_t i;

	if (unlikely(!reader))
		return false;

	if (unlikely(n_values != reader->n_offsets))
		return false;

	if (unlikely(!values))
		return false;

	if (ioctl(reader->fd, GPIOHANDLE_GET_LINE_VALUES_IOCTL, &data) < 0)
		return false;

	for (i = 0; i < n_values; i++)
		values[i] = data.values[i];

	return true;
}
