// SPDX-License-Identifier: LGPL-2.1-or-later
/*
 * This file is part of libgpiod.
 *
 * Copyright (C) 2017-2018 Bartosz Golaszewski <bartekgola@gmail.com>
 */

/* Implementation of the high-level API. */


#include <errno.h>
#include <gpiod.h>
#include <poll.h>
#include <stdio.h>
#include <string.h>

static int ctxless_flags_to_line_request_flags(bool active_low, int flags)
{
	int req_flags = 0;

	if (active_low)
		req_flags |= GPIOD_LINE_REQUEST_FLAG_ACTIVE_LOW;
	if (flags & GPIOD_CTXLESS_FLAG_OPEN_DRAIN)
		req_flags |= GPIOD_LINE_REQUEST_FLAG_OPEN_DRAIN;
	if (flags & GPIOD_CTXLESS_FLAG_OPEN_SOURCE)
		req_flags |= GPIOD_LINE_REQUEST_FLAG_OPEN_SOURCE;
	if (flags & GPIOD_CTXLESS_FLAG_BIAS_DISABLE)
		req_flags |= GPIOD_LINE_REQUEST_FLAG_BIAS_DISABLE;
	if (flags & GPIOD_CTXLESS_FLAG_BIAS_PULL_UP)
		req_flags |= GPIOD_LINE_REQUEST_FLAG_BIAS_PULL_UP;
	if (flags & GPIOD_CTXLESS_FLAG_BIAS_PULL_DOWN)
		req_flags |= GPIOD_LINE_REQUEST_FLAG_BIAS_PULL_DOWN;

	return req_flags;
}

int gpiod_ctxless_get_value(const char *device, unsigned int offset,
			    bool active_low, const char *consumer)
{
	int value, rv;

	rv = gpiod_ctxless_get_value_multiple(device, &offset, &value,
					      1, active_low, consumer);
	if (rv < 0)
		return rv;

	return value;
}

int gpiod_ctxless_get_value_ext(const char *device, unsigned int offset,
				bool active_low, const char *consumer,
				int flags)
{
	int value, rv;

	rv = gpiod_ctxless_get_value_multiple_ext(device, &offset, &value, 1,
						  active_low, consumer, flags);
	if (rv < 0)
		return rv;

	return value;
}

int gpiod_ctxless_get_value_multiple(const char *device,
				     const unsigned int *offsets, int *values,
				     unsigned int num_lines, bool active_low,
				     const char *consumer)
{
	int rv;

	rv = gpiod_ctxless_get_value_multiple_ext(device, offsets, values,
						  num_lines, active_low,
						  consumer, 0);
	return rv;
}

int gpiod_ctxless_get_value_multiple_ext(const char *device,
					 const unsigned int *offsets,
					 int *values, unsigned int num_lines,
					 bool active_low,
					 const char *consumer, int flags)
{
	struct gpiod_line_bulk bulk;
	struct gpiod_chip *chip;
	struct gpiod_line *line;
	unsigned int i;
	int rv, req_flags;

	if (!num_lines || num_lines > GPIOD_LINE_BULK_MAX_LINES) {
		errno = EINVAL;
		return -1;
	}

	chip = gpiod_chip_open_lookup(device);
	if (!chip)
		return -1;

	gpiod_line_bulk_init(&bulk);

	for (i = 0; i < num_lines; i++) {
		line = gpiod_chip_get_line(chip, offsets[i]);
		if (!line) {
			gpiod_chip_close(chip);
			return -1;
		}

		gpiod_line_bulk_add(&bulk, line);
	}

	req_flags = ctxless_flags_to_line_request_flags(active_low, flags);
	rv = gpiod_line_request_bulk_input_flags(&bulk, consumer, req_flags);
	if (rv < 0) {
		gpiod_chip_close(chip);
		return -1;
	}

	memset(values, 0, sizeof(*values) * num_lines);
	rv = gpiod_line_get_value_bulk(&bulk, values);

	gpiod_chip_close(chip);

	return rv;
}

int gpiod_ctxless_set_value(const char *device, unsigned int offset, int value,
			    bool active_low, const char *consumer,
			    gpiod_ctxless_set_value_cb cb, void *data)
{
	return gpiod_ctxless_set_value_multiple(device, &offset, &value, 1,
						active_low, consumer, cb, data);
}

int gpiod_ctxless_set_value_ext(const char *device, unsigned int offset,
				int value, bool active_low,
				const char *consumer,
				gpiod_ctxless_set_value_cb cb,
				void *data, int flags)
{
	return gpiod_ctxless_set_value_multiple_ext(device, &offset, &value,
						    1, active_low, consumer,
						    cb, data, flags);
}

int gpiod_ctxless_set_value_multiple(const char *device,
				     const unsigned int *offsets,
				     const int *values, unsigned int num_lines,
				     bool active_low, const char *consumer,
				     gpiod_ctxless_set_value_cb cb, void *data)
{
	return gpiod_ctxless_set_value_multiple_ext(device, offsets, values,
						    num_lines, active_low,
						    consumer, cb, data, 0);
}

int gpiod_ctxless_set_value_multiple_ext(
			const char *device, const unsigned int *offsets,
			const int *values, unsigned int num_lines,
			bool active_low, const char *consumer,
			gpiod_ctxless_set_value_cb cb, void *data, int flags)
{
	struct gpiod_line_bulk bulk;
	struct gpiod_chip *chip;
	struct gpiod_line *line;
	unsigned int i;
	int rv, req_flags;

	if (!num_lines || num_lines > GPIOD_LINE_BULK_MAX_LINES) {
		errno = EINVAL;
		return -1;
	}

	chip = gpiod_chip_open_lookup(device);
	if (!chip)
		return -1;

	gpiod_line_bulk_init(&bulk);

	for (i = 0; i < num_lines; i++) {
		line = gpiod_chip_get_line(chip, offsets[i]);
		if (!line) {
			gpiod_chip_close(chip);
			return -1;
		}

		gpiod_line_bulk_add(&bulk, line);
	}

	req_flags = ctxless_flags_to_line_request_flags(active_low, flags);
	rv = gpiod_line_request_bulk_output_flags(&bulk, consumer,
						  req_flags, values);
	if (rv < 0) {
		gpiod_chip_close(chip);
		return -1;
	}

	if (cb)
		cb(data);

	gpiod_chip_close(chip);

	return 0;
}

static int basic_event_poll(unsigned int num_lines,
			    struct gpiod_ctxless_event_poll_fd *fds,
			    const struct timespec *timeout,
			    void *data GPIOD_UNUSED)
{
	struct pollfd poll_fds[GPIOD_LINE_BULK_MAX_LINES];
	unsigned int i;
	int rv, ret;

	if (!num_lines || num_lines > GPIOD_LINE_BULK_MAX_LINES)
		return GPIOD_CTXLESS_EVENT_POLL_RET_ERR;

	memset(poll_fds, 0, sizeof(poll_fds));

	for (i = 0; i < num_lines; i++) {
		poll_fds[i].fd = fds[i].fd;
		poll_fds[i].events = POLLIN | POLLPRI;
	}

	rv = ppoll(poll_fds, num_lines, timeout, NULL);
	if (rv < 0) {
		if (errno == EINTR)
			return GPIOD_CTXLESS_EVENT_POLL_RET_TIMEOUT;
		else
			return GPIOD_CTXLESS_EVENT_POLL_RET_ERR;
	} else if (rv == 0) {
		return GPIOD_CTXLESS_EVENT_POLL_RET_TIMEOUT;
	}

	ret = rv;
	for (i = 0; i < num_lines; i++) {
		if (poll_fds[i].revents) {
			fds[i].event = true;
			if (!--rv)
				break;
		}
	}

	return ret;
}

int gpiod_ctxless_event_loop(const char *device, unsigned int offset,
			     bool active_low, const char *consumer,
			     const struct timespec *timeout,
			     gpiod_ctxless_event_poll_cb poll_cb,
			     gpiod_ctxless_event_handle_cb event_cb,
			     void *data)
{
	return gpiod_ctxless_event_monitor(device,
					   GPIOD_CTXLESS_EVENT_BOTH_EDGES,
					   offset, active_low, consumer,
					   timeout, poll_cb, event_cb, data);
}

int gpiod_ctxless_event_loop_multiple(const char *device,
				      const unsigned int *offsets,
				      unsigned int num_lines, bool active_low,
				      const char *consumer,
				      const struct timespec *timeout,
				      gpiod_ctxless_event_poll_cb poll_cb,
				      gpiod_ctxless_event_handle_cb event_cb,
				      void *data)
{
	return gpiod_ctxless_event_monitor_multiple(
				device, GPIOD_CTXLESS_EVENT_BOTH_EDGES,
				offsets, num_lines, active_low, consumer,
				timeout, poll_cb, event_cb, data);
}

int gpiod_ctxless_event_monitor(const char *device, int event_type,
				unsigned int offset, bool active_low,
				const char *consumer,
				const struct timespec *timeout,
				gpiod_ctxless_event_poll_cb poll_cb,
				gpiod_ctxless_event_handle_cb event_cb,
				void *data)
{
	return gpiod_ctxless_event_monitor_multiple(device, event_type,
						    &offset, 1, active_low,
						    consumer, timeout,
						    poll_cb, event_cb, data);
}

int gpiod_ctxless_event_monitor_ext(const char *device, int event_type,
				    unsigned int offset, bool active_low,
				    const char *consumer,
				    const struct timespec *timeout,
				    gpiod_ctxless_event_poll_cb poll_cb,
				    gpiod_ctxless_event_handle_cb event_cb,
				    void *data, int flags)
{
	return gpiod_ctxless_event_monitor_multiple_ext(
			device, event_type, &offset, 1, active_low,
			consumer, timeout, poll_cb, event_cb, data, flags);
}

int gpiod_ctxless_event_monitor_multiple(
			const char *device, int event_type,
			const unsigned int *offsets,
			unsigned int num_lines, bool active_low,
			const char *consumer,
			const struct timespec *timeout,
			gpiod_ctxless_event_poll_cb poll_cb,
			gpiod_ctxless_event_handle_cb event_cb,
			void *data)
{
	return gpiod_ctxless_event_monitor_multiple_ext(
			device, event_type, offsets,
			num_lines, active_low, consumer, timeout,
			poll_cb, event_cb, data, 0);
}

int gpiod_ctxless_event_monitor_multiple_ext(
			const char *device, int event_type,
			const unsigned int *offsets,
			unsigned int num_lines, bool active_low,
			const char *consumer, const struct timespec *timeout,
			gpiod_ctxless_event_poll_cb poll_cb,
			gpiod_ctxless_event_handle_cb event_cb,
			void *data, int flags)
{
	struct gpiod_ctxless_event_poll_fd fds[GPIOD_LINE_BULK_MAX_LINES];
	struct gpiod_line_request_config conf;
	struct gpiod_line_event event;
	struct gpiod_line_bulk bulk;
	int rv, ret, evtype, cnt;
	struct gpiod_chip *chip;
	struct gpiod_line *line;
	unsigned int i;

	if (!num_lines || num_lines > GPIOD_LINE_BULK_MAX_LINES) {
		errno = EINVAL;
		return -1;
	}

	if (!poll_cb)
		poll_cb = basic_event_poll;

	chip = gpiod_chip_open_lookup(device);
	if (!chip)
		return -1;

	gpiod_line_bulk_init(&bulk);

	for (i = 0; i < num_lines; i++) {
		line = gpiod_chip_get_line(chip, offsets[i]);
		if (!line) {
			gpiod_chip_close(chip);
			return -1;
		}

		gpiod_line_bulk_add(&bulk, line);
	}

	conf.flags = ctxless_flags_to_line_request_flags(active_low, flags);
	conf.consumer = consumer;

	if (event_type == GPIOD_CTXLESS_EVENT_RISING_EDGE) {
		conf.request_type = GPIOD_LINE_REQUEST_EVENT_RISING_EDGE;
	} else if (event_type == GPIOD_CTXLESS_EVENT_FALLING_EDGE) {
		conf.request_type = GPIOD_LINE_REQUEST_EVENT_FALLING_EDGE;
	} else if (event_type == GPIOD_CTXLESS_EVENT_BOTH_EDGES) {
		conf.request_type = GPIOD_LINE_REQUEST_EVENT_BOTH_EDGES;
	} else {
		errno = -EINVAL;
		ret = -1;
		goto out;
	}

	rv = gpiod_line_request_bulk(&bulk, &conf, NULL);
	if (rv) {
		ret = -1;
		goto out;
	}

	memset(fds, 0, sizeof(fds));
	for (i = 0; i < num_lines; i++) {
		line = gpiod_line_bulk_get_line(&bulk, i);
		fds[i].fd = gpiod_line_event_get_fd(line);
	}

	for (;;) {
		for (i = 0; i < num_lines; i++)
			fds[i].event = false;

		cnt = poll_cb(num_lines, fds, timeout, data);
		if (cnt == GPIOD_CTXLESS_EVENT_POLL_RET_ERR) {
			ret = -1;
			goto out;
		} else if (cnt == GPIOD_CTXLESS_EVENT_POLL_RET_TIMEOUT) {
			rv = event_cb(GPIOD_CTXLESS_EVENT_CB_TIMEOUT,
				      0, &event.ts, data);
			if (rv == GPIOD_CTXLESS_EVENT_CB_RET_ERR) {
				ret = -1;
				goto out;
			} else if (rv == GPIOD_CTXLESS_EVENT_CB_RET_STOP) {
				ret = 0;
				goto out;
			}
		} else if (cnt == GPIOD_CTXLESS_EVENT_POLL_RET_STOP) {
			ret = 0;
			goto out;
		}

		for (i = 0; i < num_lines; i++) {
			if (!fds[i].event)
				continue;

			line = gpiod_line_bulk_get_line(&bulk, i);
			rv = gpiod_line_event_read(line, &event);
			if (rv < 0) {
				ret = rv;
				goto out;
			}

			if (event.event_type == GPIOD_LINE_EVENT_RISING_EDGE)
				evtype = GPIOD_CTXLESS_EVENT_CB_RISING_EDGE;
			else
				evtype = GPIOD_CTXLESS_EVENT_CB_FALLING_EDGE;

			rv = event_cb(evtype, gpiod_line_offset(line),
				      &event.ts, data);
			if (rv == GPIOD_CTXLESS_EVENT_CB_RET_ERR) {
				ret = -1;
				goto out;
			} else if (rv == GPIOD_CTXLESS_EVENT_CB_RET_STOP) {
				ret = 0;
				goto out;
			}

			if (!--cnt)
				break;
		}
	}

out:
	gpiod_chip_close(chip);

	return ret;
}

int gpiod_ctxless_find_line(const char *name, char *chipname,
			    size_t chipname_size, unsigned int *offset)
{
	struct gpiod_chip *chip;
	struct gpiod_line *line;

	line = gpiod_line_find(name);
	if (!line) {
		if (errno == ENOENT)
			return 0;
		else
			return -1;
	}

	chip = gpiod_line_get_chip(line);
	snprintf(chipname, chipname_size, "%s", gpiod_chip_name(chip));
	*offset = gpiod_line_offset(line);
	gpiod_chip_close(chip);

	return 1;
}