// SPDX-License-Identifier: LGPL-2.1-or-later
// SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>

#include "internal.h"

typedef struct {
	PyObject_HEAD;
	struct gpiod_line_request *request;
	unsigned int *offsets;
	enum gpiod_line_value *values;
	size_t num_lines;
	struct gpiod_edge_event_buffer *buffer;
} request_object;

static void internal_request_release(request_object *self)
{
	if (self->request){
		Py_BEGIN_ALLOW_THREADS;
		gpiod_line_request_release(self->request);
		Py_END_ALLOW_THREADS;
		self->request = NULL;
	}
}

static void request_finalize(request_object *self)
{
	internal_request_release(self);

	if (self->offsets)
		PyMem_Free(self->offsets);

	if (self->values)
		PyMem_Free(self->values);

	if (self->buffer)
		gpiod_edge_event_buffer_free(self->buffer);
}

static PyObject *
request_chip_name(request_object *self, void *Py_UNUSED(ignored))
{
	return PyUnicode_FromString(
			gpiod_line_request_get_chip_name(self->request));
}

static PyObject *
request_num_lines(request_object *self, void *Py_UNUSED(ignored))
{
	return PyLong_FromUnsignedLong(
			gpiod_line_request_get_num_requested_lines(self->request));
}

static PyObject *request_offsets(request_object *self, void *Py_UNUSED(ignored))
{
	PyObject *lines, *line;
	unsigned int *offsets;
	size_t num_lines, i;
	int ret;

	num_lines = gpiod_line_request_get_num_requested_lines(self->request);

	offsets = PyMem_Calloc(num_lines, sizeof(unsigned int));
	if (!offsets)
		return PyErr_NoMemory();

	gpiod_line_request_get_requested_offsets(self->request, offsets, num_lines);

	lines = PyList_New(num_lines);
	if (!lines) {
		PyMem_Free(offsets);
		return NULL;
	}

	for (i = 0; i < num_lines; i++) {
		line = PyLong_FromUnsignedLong(offsets[i]);
		if (!line) {
			Py_DECREF(lines);
			PyMem_Free(offsets);
			return NULL;
		}

		ret = PyList_SetItem(lines, i, line);
		if (ret) {
			Py_DECREF(lines);
			PyMem_Free(offsets);
			return NULL;
		}
	}

	PyMem_Free(offsets);
	return lines;
}

static PyObject *request_fd(request_object *self, void *Py_UNUSED(ignored))
{
	return PyLong_FromLong(gpiod_line_request_get_fd(self->request));
}

static PyGetSetDef request_getset[] = {
	{
		.name = "chip_name",
		.get = (getter)request_chip_name,
	},
	{
		.name = "num_lines",
		.get = (getter)request_num_lines,
	},
	{
		.name = "offsets",
		.get = (getter)request_offsets,
	},
	{
		.name = "fd",
		.get = (getter)request_fd,
	},
	{ }
};

static PyObject *
request_release(request_object *self, PyObject *Py_UNUSED(ignored))
{
	internal_request_release(self);

	Py_RETURN_NONE;
}

static void clear_buffers(request_object *self)
{
	memset(self->offsets, 0, self->num_lines * sizeof(unsigned int));
	memset(self->values, 0, self->num_lines * sizeof(int));
}

static PyObject *request_get_values(request_object *self, PyObject *args)
{
	PyObject *offsets, *values, *val, *type, *iter, *next;
	Py_ssize_t num_offsets, pos;
	int ret;

	ret = PyArg_ParseTuple(args, "OO", &offsets, &values);
	if (!ret)
		return NULL;

	num_offsets = PyObject_Size(offsets);
	if (num_offsets < 0)
		return NULL;

	if (num_offsets > (Py_ssize_t)self->num_lines) {
		PyErr_SetString(PyExc_ValueError,
				"number of offsets exceeds the number of requested lines");
		return NULL;
	}

	iter = PyObject_GetIter(offsets);
	if (!iter)
		return NULL;

	clear_buffers(self);

	for (pos = 0;; pos++) {
		next = PyIter_Next(iter);
		if (!next) {
			Py_DECREF(iter);
			if (PyErr_Occurred())
				return NULL;
			break;
		}

		self->offsets[pos] = Py_gpiod_PyLongAsUnsignedInt(next);
		Py_DECREF(next);
		if (PyErr_Occurred()) {
			Py_DECREF(iter);
			return NULL;
		}
	}

	Py_BEGIN_ALLOW_THREADS;
	ret = gpiod_line_request_get_values_subset(self->request,
						   num_offsets,
						   self->offsets,
						   self->values);
	Py_END_ALLOW_THREADS;
	if (ret)
		return Py_gpiod_SetErrFromErrno();

	type = Py_gpiod_GetModuleAttrString("gpiod.line", "Value");
	if (!type)
		return NULL;

	for (pos = 0; pos < num_offsets; pos++) {
		val = PyObject_CallFunction(type, "i", self->values[pos]);
		if (!val) {
			Py_DECREF(type);
			return NULL;
		}

		ret = PyList_SetItem(values, pos, val);
		if (ret) {
			Py_DECREF(type);
			return NULL;
		}
	}

	Py_DECREF(type);
	Py_RETURN_NONE;
}

static PyObject *request_set_values(request_object *self, PyObject *args)
{
	PyObject *values, *key, *val, *val_stripped;
	Py_ssize_t pos = 0, index = 0;
	int ret;

	ret = PyArg_ParseTuple(args, "O", &values);
	if (!ret)
		return NULL;

	if (PyObject_Size(values) > (Py_ssize_t)self->num_lines) {
		PyErr_SetString(PyExc_ValueError,
				"number of offsets exceeds the number of requested lines");
		return NULL;
	}

	clear_buffers(self);

	/* Note: pos may not be contiguous. */
	while (PyDict_Next(values, &pos, &key, &val)) {
		self->offsets[index] = Py_gpiod_PyLongAsUnsignedInt(key);
		if (PyErr_Occurred())
			return NULL;

		val_stripped = PyObject_GetAttrString(val, "value");
		if (!val_stripped)
			return NULL;

		self->values[index] = PyLong_AsLong(val_stripped);
		Py_DECREF(val_stripped);
		if (PyErr_Occurred())
			return NULL;

		index++;
	}

	Py_BEGIN_ALLOW_THREADS;
	ret = gpiod_line_request_set_values_subset(self->request,
						   index,
						   self->offsets,
						   self->values);
	Py_END_ALLOW_THREADS;
	if (ret)
		return Py_gpiod_SetErrFromErrno();

	Py_RETURN_NONE;
}

static PyObject *request_reconfigure_lines(request_object *self, PyObject *args)
{
	struct gpiod_line_config *line_cfg;
	PyObject *line_cfg_obj;
	int ret;

	ret = PyArg_ParseTuple(args, "O", &line_cfg_obj);
	if (!ret)
		return NULL;

	line_cfg = Py_gpiod_LineConfigGetData(line_cfg_obj);
	if (!line_cfg)
		return NULL;

	Py_BEGIN_ALLOW_THREADS;
	ret = gpiod_line_request_reconfigure_lines(self->request, line_cfg);
	Py_END_ALLOW_THREADS;
	if (ret)
		return Py_gpiod_SetErrFromErrno();

	Py_RETURN_NONE;
}

static PyObject *request_read_edge_events(request_object *self, PyObject *args)
{
	PyObject *max_events_obj, *event_obj, *events, *type;
	size_t max_events, num_events, i;
	struct gpiod_edge_event *event;
	int ret;

	ret = PyArg_ParseTuple(args, "O", &max_events_obj);
	if (!ret)
		return NULL;

	if (!Py_IsNone(max_events_obj)) {
		max_events = PyLong_AsSize_t(max_events_obj);
		if (PyErr_Occurred())
			return NULL;
	} else {
		max_events = 64;
	}

	Py_BEGIN_ALLOW_THREADS;
	ret = gpiod_line_request_read_edge_events(self->request,
						 self->buffer, max_events);
	Py_END_ALLOW_THREADS;
	if (ret < 0)
		return Py_gpiod_SetErrFromErrno();

	num_events = ret;

	events = PyList_New(num_events);
	if (!events)
		return NULL;

	type = Py_gpiod_GetModuleAttrString("gpiod.edge_event", "EdgeEvent");
	if (!type) {
		Py_DECREF(events);
		return NULL;
	}

	for (i = 0; i < num_events; i++) {
		event = gpiod_edge_event_buffer_get_event(self->buffer, i);
		if (!event) {
			Py_DECREF(events);
			Py_DECREF(type);
			return NULL;
		}

		event_obj = PyObject_CallFunction(type, "iKiii",
				gpiod_edge_event_get_event_type(event),
				gpiod_edge_event_get_timestamp_ns(event),
				gpiod_edge_event_get_line_offset(event),
				gpiod_edge_event_get_global_seqno(event),
				gpiod_edge_event_get_line_seqno(event));
		if (!event_obj) {
			Py_DECREF(events);
			Py_DECREF(type);
			return NULL;
		}

		ret = PyList_SetItem(events, i, event_obj);
		if (ret) {
			Py_DECREF(events);
			Py_DECREF(type);
			return NULL;
		}
	}

	Py_DECREF(type);
	return events;
}

static PyMethodDef request_methods[] = {
	{
		.ml_name = "release",
		.ml_meth = (PyCFunction)request_release,
		.ml_flags = METH_NOARGS,
	},
	{
		.ml_name = "get_values",
		.ml_meth = (PyCFunction)request_get_values,
		.ml_flags = METH_VARARGS,
	},
	{
		.ml_name = "set_values",
		.ml_meth = (PyCFunction)request_set_values,
		.ml_flags = METH_VARARGS,
	},
	{
		.ml_name = "reconfigure_lines",
		.ml_meth = (PyCFunction)request_reconfigure_lines,
		.ml_flags = METH_VARARGS,
	},
	{
		.ml_name = "read_edge_events",
		.ml_meth = (PyCFunction)request_read_edge_events,
		.ml_flags = METH_VARARGS,
	},
	{ }
};

PyTypeObject request_type = {
	PyVarObject_HEAD_INIT(NULL, 0)
	.tp_name = "gpiod._ext.Request",
	.tp_basicsize = sizeof(request_object),
	.tp_flags = Py_TPFLAGS_DEFAULT,
	.tp_finalize = (destructor)request_finalize,
	.tp_dealloc = (destructor)Py_gpiod_dealloc,
	.tp_getset = request_getset,
	.tp_methods = request_methods,
};

PyObject *Py_gpiod_MakeRequestObject(struct gpiod_line_request *request,
				     size_t event_buffer_size)
{
	struct gpiod_edge_event_buffer *buffer;
	enum gpiod_line_value *values;
	request_object *req_obj;
	unsigned int *offsets;
	size_t num_lines;

	num_lines = gpiod_line_request_get_num_requested_lines(request);

	req_obj = PyObject_New(request_object, &request_type);
	if (!req_obj)
		return NULL;

	offsets = PyMem_Calloc(num_lines, sizeof(unsigned int));
	if (!offsets) {
		Py_DECREF(req_obj);
		return NULL;
	}

	values = PyMem_Calloc(num_lines, sizeof(int));
	if (!values) {
		PyMem_Free(offsets);
		Py_DECREF(req_obj);
		return NULL;
	}

	buffer = gpiod_edge_event_buffer_new(event_buffer_size);
	if (!buffer) {
		PyMem_Free(values);
		PyMem_Free(offsets);
		Py_DECREF(req_obj);
		return Py_gpiod_SetErrFromErrno();
	}

	req_obj->request = request;
	req_obj->offsets = offsets;
	req_obj->values = values;
	req_obj->num_lines = num_lines;
	req_obj->buffer = buffer;

	return (PyObject *)req_obj;
}
